-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(ui): use web components for jokers, playing cards, and hand …
…levels
1 parent
ac8793f
commit 7659c97
Showing
10 changed files
with
889 additions
and
882 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
const registeredContainers = new Set<Element>() | ||
|
||
export class DraggableCard extends HTMLElement { | ||
container: HTMLElement | ||
|
||
constructor () { | ||
super() | ||
|
||
const container = this.closest('[data-drop-zone]') | ||
if (!container) { | ||
throw new Error(`${this.tagName} is a DraggableCard but is not an descendant of an element with the “data-drop-zone” attribute.`) | ||
} | ||
this.container = container as HTMLElement | ||
|
||
if (!registeredContainers.has(this.container)) { | ||
registeredContainers.add(this.container) | ||
|
||
this.container.addEventListener('dragenter', this.handleDragOver) | ||
this.container.addEventListener('dragover', this.handleDragOver) | ||
} | ||
} | ||
|
||
handleDragStart (event: DragEvent) { | ||
if (event.dataTransfer === null || !(event.target instanceof Element)) { | ||
return | ||
} | ||
|
||
if (this.container) { | ||
const draggedElIndex = Array.from(this.container.children).findIndex((el) => el === event.target) | ||
if (draggedElIndex !== -1) { | ||
event.dataTransfer.effectAllowed = 'move' | ||
event.dataTransfer.dropEffect = 'move' | ||
event.dataTransfer.setData('text/plain', String(draggedElIndex)) | ||
} | ||
} | ||
} | ||
|
||
handleDragEnd (event: DragEvent) { | ||
const draggedEl = getDragEventData(event, this.container) | ||
if (draggedEl) { | ||
drop(this.container, draggedEl, event.clientX) | ||
} | ||
} | ||
|
||
handleDragOver (event: DragEvent) { | ||
if (getDragEventData(event, this.container)) { | ||
event.preventDefault() | ||
} | ||
} | ||
} | ||
|
||
function getDragEventData (event: DragEvent, container: Element): Element | null { | ||
if (event.dataTransfer === null || !(event.target instanceof Element)) { | ||
return null | ||
} | ||
|
||
const draggedElIndex = Number(event.dataTransfer.getData('text/plain')) | ||
if (!Number.isNaN(draggedElIndex) && container) { | ||
const draggedEl = container.children[draggedElIndex] | ||
|
||
if (draggedEl) { | ||
return draggedEl | ||
} | ||
} | ||
|
||
return null | ||
} | ||
|
||
function drop (container: Element, draggedEl: Element, dragClientX: number) { | ||
// Determining the insertion coordinate for the element … | ||
const containerRect = container.getBoundingClientRect() | ||
// The drop target X coordinate relative to the container. | ||
const dropTargetX = dragClientX - containerRect.left | ||
|
||
for (const el of container.children) { | ||
const { left, width } = el.getBoundingClientRect() | ||
// The element's X coordinate relative to the container. | ||
const elStartX = left - containerRect.left | ||
// The element's halfway point X coordinate relative the container. | ||
const halfwayPointX = elStartX + width / 2 | ||
|
||
if (dropTargetX < halfwayPointX) { | ||
el.insertAdjacentElement('beforebegin', draggedEl) | ||
return | ||
} | ||
} | ||
|
||
container.insertAdjacentElement('beforeend', draggedEl) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import type { HandName } from '#lib/types.js' | ||
|
||
export class HandLevel extends HTMLElement { | ||
static { | ||
if (window.customElements.get('hand-level') === undefined) { | ||
window.customElements.define('hand-level', HandLevel) | ||
} | ||
} | ||
|
||
hasRendered = false | ||
fragment: Element | ||
nameEl: HTMLElement | ||
levelInput: HTMLInputElement | ||
playsInput: HTMLInputElement | ||
|
||
constructor () { | ||
super() | ||
|
||
const template = document.querySelector('template#hand-level') as HTMLTemplateElement | ||
this.fragment = template.content.cloneNode(true) as Element | ||
|
||
this.nameEl = this.fragment.querySelector('[data-h-name]') as HTMLElement | ||
this.levelInput = this.fragment.querySelector('[data-h-level]') as HTMLInputElement | ||
this.playsInput = this.fragment.querySelector('[data-h-plays]') as HTMLInputElement | ||
} | ||
|
||
get [Symbol.toStringTag] () { | ||
return this.tagName | ||
} | ||
|
||
get handName () { | ||
return this.nameEl.textContent as HandName | ||
} | ||
|
||
get level () { | ||
return Number(this.levelInput.value) | ||
} | ||
|
||
get plays () { | ||
return Number(this.playsInput.value) | ||
} | ||
|
||
connectedCallback () { | ||
if (!this.isConnected) { | ||
return | ||
} | ||
|
||
// Prevent re-rendering when moving the element around with the drag'n'drop API | ||
if (!this.hasRendered) { | ||
this.render() | ||
} | ||
} | ||
|
||
render () { | ||
this.innerHTML = '' | ||
this.appendChild(this.fragment) | ||
this.hasRendered = true | ||
} | ||
|
||
setHandLevel (handName: HandName, { level, plays }: { level: number, plays: number }) { | ||
this.nameEl.textContent = handName | ||
this.levelInput.value = String(level) | ||
this.playsInput.value = String(plays) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { DraggableCard } from './DraggableCard.js' | ||
import { uniqueId } from '#utilities/uniqueId.js' | ||
import { JOKER_DEFINITIONS } from '#lib/data.js' | ||
import { notNullish } from '#utilities/notNullish.js' | ||
import type { Joker, JokerEdition, JokerName, Rank, Suit } from '#lib/types.js' | ||
|
||
export class JokerCard extends DraggableCard { | ||
static { | ||
if (window.customElements.get('joker-card') === undefined) { | ||
window.customElements.define('joker-card', JokerCard) | ||
} | ||
} | ||
|
||
hasRendered = false | ||
fragment: Element | ||
removeButton: HTMLButtonElement | ||
nameSelect: HTMLSelectElement | ||
editionSelect: HTMLSelectElement | ||
plusChipsInput: HTMLInputElement | ||
plusMultiplierInput: HTMLInputElement | ||
timesMultiplierInput: HTMLInputElement | ||
isActiveCheckbox: HTMLInputElement | ||
rankSelect: HTMLSelectElement | ||
suitSelect: HTMLSelectElement | ||
|
||
constructor () { | ||
super() | ||
|
||
const template = document.querySelector('template#joker') as HTMLTemplateElement | ||
this.fragment = template.content.cloneNode(true) as Element | ||
this.classList.add('card') | ||
this.draggable = true | ||
const id = uniqueId() | ||
|
||
this.removeButton = this.fragment.querySelector('[data-remove-button]') as HTMLButtonElement | ||
this.removeButton.addEventListener('click', () => this.remove()) | ||
|
||
this.nameSelect = this.fragment.querySelector('[data-j-name]') as HTMLSelectElement | ||
this.nameSelect.name = `joker-name-${id}` | ||
|
||
this.editionSelect = this.fragment.querySelector('[data-j-edition]') as HTMLSelectElement | ||
this.editionSelect.name = `joker-edition-${id}` | ||
|
||
this.plusChipsInput = this.fragment.querySelector('[data-j-plus-chips]') as HTMLInputElement | ||
this.plusChipsInput.name = `joker-plusChips-${id}` | ||
|
||
this.plusMultiplierInput = this.fragment.querySelector('[data-j-plus-multiplier]') as HTMLInputElement | ||
this.plusMultiplierInput.name = `joker-plusMultiplier-${id}` | ||
|
||
this.timesMultiplierInput = this.fragment.querySelector('[data-j-times-multiplier]') as HTMLInputElement | ||
this.timesMultiplierInput.name = `joker-timesMultiplier-${id}` | ||
|
||
this.isActiveCheckbox = this.fragment.querySelector('[data-j-is-active]') as HTMLInputElement | ||
this.isActiveCheckbox.name = `joker-isActive-${id}` | ||
|
||
this.rankSelect = this.fragment.querySelector('[data-j-rank]') as HTMLSelectElement | ||
this.rankSelect.name = `joker-rank-${id}` | ||
|
||
this.suitSelect = this.fragment.querySelector('[data-j-suit]') as HTMLSelectElement | ||
this.suitSelect.name = `joker-suit-${id}` | ||
|
||
this.addEventListener('dragstart', this.handleDragStart) | ||
this.addEventListener('dragend', this.handleDragEnd) | ||
} | ||
|
||
get [Symbol.toStringTag] () { | ||
return this.tagName | ||
} | ||
|
||
get jokerName () { | ||
return this.nameSelect.value as JokerName | ||
} | ||
|
||
get edition () { | ||
return this.editionSelect.value as JokerEdition | ||
} | ||
|
||
get plusChips () { | ||
return Number(this.plusChipsInput.value) | ||
} | ||
|
||
get plusMultiplier () { | ||
return Number(this.plusMultiplierInput.value) | ||
} | ||
|
||
get timesMultiplier () { | ||
return Number(this.timesMultiplierInput.value) | ||
} | ||
|
||
get rank () { | ||
return this.rankSelect.value as Rank | ||
} | ||
|
||
get suit () { | ||
return this.suitSelect.value as Suit | ||
} | ||
|
||
get isActive () { | ||
return this.isActiveCheckbox.checked | ||
} | ||
|
||
connectedCallback () { | ||
if (!this.isConnected) { | ||
return | ||
} | ||
|
||
// Prevent re-rendering when moving the element around with the drag'n'drop API | ||
if (!this.hasRendered) { | ||
this.render() | ||
} | ||
} | ||
|
||
render () { | ||
this.innerHTML = '' | ||
this.appendChild(this.fragment) | ||
this.hasRendered = true | ||
} | ||
|
||
setJoker (joker: Joker) { | ||
const { | ||
name, | ||
edition, | ||
plusChips, | ||
plusMultiplier, | ||
timesMultiplier, | ||
isActive, | ||
rank, | ||
suit, | ||
} = joker | ||
const definition = JOKER_DEFINITIONS[name] | ||
|
||
this.nameSelect.value = name | ||
this.editionSelect.value = edition | ||
if (definition.hasPlusChipsInput) this.plusChipsInput.value = String(plusChips) | ||
if (definition.hasPlusMultiplierInput) this.plusMultiplierInput.value = String(plusMultiplier) | ||
if (definition.hasTimesMultiplierInput) this.timesMultiplierInput.value = String(timesMultiplier) | ||
if (definition.hasIsActiveInput) this.isActiveCheckbox.checked = Boolean(isActive) | ||
if (definition.hasRankInput && rank) this.rankSelect.value = String(rank) | ||
if (definition.hasSuitInput && suit) this.suitSelect.value = String(suit) | ||
|
||
this.updateState() | ||
} | ||
|
||
updateState () { | ||
const jokerName = this.nameSelect.value as JokerName | ||
const definition = JOKER_DEFINITIONS[jokerName] | ||
|
||
this.classList.remove( | ||
'--has-plus-chips', | ||
'--has-plus-multiplier', | ||
'--has-times-multiplier', | ||
'--has-is-active', | ||
'--has-rank', | ||
'--has-suit', | ||
) | ||
|
||
;[ | ||
definition.hasPlusChipsInput ? '--has-plus-chips' : null, | ||
definition.hasPlusMultiplierInput ? '--has-plus-multiplier': null, | ||
definition.hasTimesMultiplierInput ? '--has-times-multiplier': null, | ||
definition.hasIsActiveInput ? '--has-is-active': null, | ||
definition.hasRankInput ? '--has-rank': null, | ||
definition.hasSuitInput ? '--has-suit': null, | ||
].filter(notNullish).forEach((className) => this.classList.add(className)) | ||
} | ||
} |
Oops, something went wrong.