Skip to content

Commit

Permalink
refactor(ui): use web components for jokers, playing cards, and hand …
Browse files Browse the repository at this point in the history
…levels
  • Loading branch information
kleinfreund committed Mar 3, 2024
1 parent ac8793f commit 7659c97
Showing 10 changed files with 889 additions and 882 deletions.
820 changes: 298 additions & 522 deletions index.html

Large diffs are not rendered by default.

393 changes: 58 additions & 335 deletions src/ui/UiState.ts

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions src/ui/components/DraggableCard.ts
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)
}
65 changes: 65 additions & 0 deletions src/ui/components/HandLevel.ts
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)
}
}
166 changes: 166 additions & 0 deletions src/ui/components/JokerCard.ts
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))
}
}
Loading

0 comments on commit 7659c97

Please sign in to comment.