Skip to content

Commit

Permalink
[feat] controller factory refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
braebo committed Feb 29, 2024
1 parent aaf67da commit c376918
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 149 deletions.
124 changes: 124 additions & 0 deletions src/lib/gui/controllers/number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { Input, ElementMap, InputOptions, NumberInputOptions } from '../inputs/Input'

import { create } from '../../utils/create'
import { svgChevron } from '../svg/chevron'

/**
* Controller factory funtions create the DOM elements that are
* used to control the input, and bind their change events to
* the input's {@link Input.updateState | updateState} method.
*/
type ControllerFactory<
TElement extends Element | ElementMap,
TInput extends Input = Input<any>,
TOptions extends InputOptions<any> = InputOptions,
> = (input: TInput, opts: TOptions, parent?: HTMLElement) => TElement

export const numberController: ControllerFactory<HTMLInputElement> = (input, opts, parent) => {
const controller = create<HTMLInputElement>('input', {
type: 'number',
classes: ['gui-input-number-input'],
value: String(input.state.value),
parent,
})

if ('step' in opts) {
controller.step = String(opts.step as NumberInputOptions['step'])
}

input.listen(controller, 'input', input.updateState)

return controller
}

export const rangeController: ControllerFactory<HTMLInputElement> = (input, opts, parent) => {
const range = create<HTMLInputElement>('input', {
type: 'range',
classes: ['gui-input-number-range'],
value: String(input.state.value),
parent,
})

if ('min' in opts) range.min = String(opts.min)
if ('max' in opts) range.max = String(opts.max)
if ('step' in opts) range.step = String(opts.step)

input.listen(range, 'input', input.updateState)

return range
}

export const numberControllerButtons: ControllerFactory<{
container: HTMLDivElement
increment: HTMLDivElement
decrement: HTMLDivElement
}> = (input, opts, parent) => {
const container = create<HTMLDivElement>('div', {
classes: ['gui-input-number-buttons-container'],
parent,
})

const increment = create<HTMLDivElement>('div', {
classes: ['gui-input-number-button', 'gui-input-number-buttons-increment'],
parent: container,
})
increment.appendChild(svgChevron())
input.listen(increment, 'pointerdown', rampChangeUp)

const decrement = create<HTMLDivElement>('div', {
classes: ['gui-input-number-button', 'gui-input-number-buttons-decrement'],
parent: container,
})
const upsideDownChevron = svgChevron()
upsideDownChevron.setAttribute('style', 'transform: rotate(180deg)')
decrement.appendChild(upsideDownChevron)
input.listen(decrement, 'pointerdown', rampChangeDown)

function rampChange(direction = 1) {
const step = 'step' in opts ? (opts.step as number) : 1

let delay = 300
let stop = false
let delta = 0
let timeout: ReturnType<typeof setTimeout>

const change = () => {
clearTimeout(timeout)
if (stop) return

delta += delay
if (delta > 1000) {
delay /= 2
delta = 0
}

input.updateState(input.state.value + step * direction)
timeout = setTimeout(change, delay)
}

const stopChanging = () => {
stop = true
window.removeEventListener('pointerup', stopChanging)
window.removeEventListener('pointercancel', stopChanging)
}

window.addEventListener('pointercancel', stopChanging, { once: true })
window.addEventListener('pointerup', stopChanging, { once: true })

change()
}

function rampChangeUp() {
rampChange(1)
}

function rampChangeDown() {
rampChange(-1)
}

return {
container,
increment,
decrement,
} as const satisfies ElementMap
}
46 changes: 18 additions & 28 deletions src/lib/gui/inputs/Color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { Input } from './Input'

export type ColorMode = 'hex' | 'rgb' | 'hsl' | 'rgbString' | 'hslString' | 'array'

type ElementMap = {
[key: string]: HTMLInputElement | HTMLDivElement | ElementMap
}

export interface ColorControllerElements {
[key: string]: HTMLInputElement | HTMLDivElement | ElementMap
container: HTMLInputElement
input: HTMLInputElement
sliders: {
Expand Down Expand Up @@ -40,15 +45,6 @@ export class InputColor extends Input<Color, ColorInputOptions, ColorControllerE
initialValue: Color
opts: ColorInputOptions

// declare elements: {
// container: HTMLElement
// title: HTMLElement
// content: HTMLElement
// drawer: HTMLElement
// drawerToggle: HTMLElement
// color: ColorControllerElements
// }

#log = new Logger('InputColor', { fg: 'cyan' })
#disposeCallbacks = new Set<() => void>()

Expand Down Expand Up @@ -81,15 +77,15 @@ export class InputColor extends Input<Color, ColorInputOptions, ColorControllerE
this.elements.controller = colorController(this, opts)

this.state.subscribe((v) => {
this.elements.color.input.value = String(v)
this.elements.controller.input.value = String(v)

this.callOnChange()
})
}

refreshSliders(mode = this.opts.mode) {
if (mode === 'rgb') {
for (const [k, v] of Object.entries(this.elements.color.sliders)) {
for (const [k, v] of Object.entries(this.elements.controller.sliders)) {
if (k === 'container') continue
;(v as HTMLInputElement).disabled = false
;(v as HTMLInputElement).min = '0'
Expand All @@ -99,7 +95,7 @@ export class InputColor extends Input<Color, ColorInputOptions, ColorControllerE
}

if (mode === 'hsl') {
for (const [k, v] of Object.entries(this.elements.color.sliders)) {
for (const [k, v] of Object.entries(this.elements.controller.sliders)) {
if (k === 'container') continue
;(v as HTMLInputElement).disabled = false
;(v as HTMLInputElement).min = '0'
Expand All @@ -109,23 +105,12 @@ export class InputColor extends Input<Color, ColorInputOptions, ColorControllerE
}

// Disable the sliders.
for (const [k, v] of Object.entries(this.elements.color.sliders)) {
for (const [k, v] of Object.entries(this.elements.controller.sliders)) {
if (k === 'container') continue
;(v as HTMLInputElement).disabled = true
}
}

#listen = (element: HTMLElement, event: string, cb: (e: Event) => void) => {
element.addEventListener(event, cb)
this.#disposeCallbacks.add(() => {
element.removeEventListener(event, cb)
})
}

isColor = (v: any): v is Color => {
return v?.isColor
}

updateState = (v: Color | Event) => {
if (v instanceof Event) {
if (v?.target && 'valueAsColor' in v.target) {
Expand All @@ -136,6 +121,10 @@ export class InputColor extends Input<Color, ColorInputOptions, ColorControllerE
}
}

isColor = (v: any): v is Color => {
return v?.isColor
}

dispose() {
super.dispose()

Expand All @@ -145,8 +134,7 @@ export class InputColor extends Input<Color, ColorInputOptions, ColorControllerE
}
}

export function colorController(input: Input, options: ColorInputOptions) {

export function colorController(input: Input<Color>, options: ColorInputOptions) {
const elements = {
container: create('div', {
classes: ['gui-input-color-container'],
Expand All @@ -171,7 +159,7 @@ export function colorController(input: Input, options: ColorInputOptions) {

elements.sliders.container = create('div', {
classes: ['gui-input-color-range-container'],
parent: elements.content,
parent: input.elements.content,
})

for (const key of ['a', 'b', 'c'] as const) {
Expand All @@ -184,4 +172,6 @@ export function colorController(input: Input, options: ColorInputOptions) {
input.listen(elements.sliders[key], 'input', input.updateState)
}
//⌟
}

return elements
}
Loading

0 comments on commit c376918

Please sign in to comment.