diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/autoComplete.ts | 115 | ||||
-rw-r--r-- | src/lib/base.ts | 32 | ||||
-rw-r--r-- | src/lib/button.ts | 29 | ||||
-rw-r--r-- | src/lib/color.ts | 36 | ||||
-rw-r--r-- | src/lib/contextMenu.ts | 35 | ||||
-rw-r--r-- | src/lib/dom.ts | 6 | ||||
-rw-r--r-- | src/lib/form.ts | 54 | ||||
-rw-r--r-- | src/lib/h.ts | 31 | ||||
-rw-r--r-- | src/lib/icons.ts | 66 | ||||
-rw-r--r-- | src/lib/layout.ts | 15 | ||||
-rw-r--r-- | src/lib/modal.ts | 28 |
11 files changed, 447 insertions, 0 deletions
diff --git a/src/lib/autoComplete.ts b/src/lib/autoComplete.ts new file mode 100644 index 0000000..b0a79eb --- /dev/null +++ b/src/lib/autoComplete.ts @@ -0,0 +1,115 @@ +import { h, Children, concatClassName } from 'lib/h' +import * as Button from 'lib/button' + +export function create( + attrs: object, + id: string, + keys: string[], + renderEntry: (entry: string) => Element, + onInput: (value: string) => void +): Element { + const completion = h('div', {}) + + const updateCompletion = (target: EventTarget, value: string) => { + const entries = search(value, keys) + mountOn( + completion, + renderCompletion( + renderEntry, + selected => { + (target as HTMLInputElement).value = selected + completion.remove + removeChildren(completion) + onInput(selected) + }, + entries + ) + ) + } + + const input = h('input', + concatClassName( + { ...attrs, + id, + autocomplete: 'off', + onfocus: (e: Event) => { + if (e.target !== null) { + const target = e.target as HTMLInputElement + updateCompletion(target, target.value) + } + }, + oninput: (e: Event) => { + if (e.target !== null) { + const target = e.target as HTMLInputElement + updateCompletion(target, target.value) + onInput(target.value) + } + } + }, + 'g-AutoComplete__Input' + ) + ) as HTMLInputElement + + input.addEventListener('blur', (e: MouseEvent) => { + if (e.relatedTarget === null) { + removeChildren(completion) + } + }) + + return h('div', + { className: 'g-AutoComplete' }, + input, + completion, + Button.raw( + { className: 'g-AutoComplete__Clear', + type: 'button', + onclick: () => { + onInput('') + input.value = '' + input.focus() + } + }, + 'x' + ) + ) +} + +function renderCompletion( + renderEntry: (entry: string) => Element, + onSelect: (entry: string) => void, + entries: string[] +): Element { + return h('div', + { className: 'g-AutoComplete__Completion' }, + ...entries.map(c => + Button.raw( + { className: 'g-AutoComplete__Entry', + type: 'button', + onclick: (e: Event) => { + e.stopPropagation() + e.preventDefault() + onSelect(c) + } + }, + renderEntry(c) + ) + ) + ) +} + +function search(s: string, xs: string[]): string[] { + return xs.filter(x => x.includes(s)) +} + +function mountOn(base: Element, ...children: Element[]) { + removeChildren(base) + children.forEach(child => base.appendChild(child)) +} + +function removeChildren(base: Element) { + const firstChild = base.firstChild + if (firstChild !== null) { + base.removeChild(firstChild) + removeChildren(base) + } +} diff --git a/src/lib/base.ts b/src/lib/base.ts new file mode 100644 index 0000000..59c91cc --- /dev/null +++ b/src/lib/base.ts @@ -0,0 +1,32 @@ +export const b2: string[] = + '01'.split('') + +export const b16: string[] = + '0123456789abcdef'.split('') + +export const b62: string[] = + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') + +export function encode(n: bigint, charset: string[]): string { + const base = BigInt(charset.length) + + if (n == BigInt(0)) { + return '0' + } else { + var xs = [] + while (n > BigInt(0)) { + xs.push(charset[Number(n % base)]) + n = n / base + } + return xs.reverse().join('') + } +} + +export function decode(xs: string, charset: string[]): bigint { + const base = BigInt(charset.length) + + return xs + .split('') + .reverse() + .reduce((acc, x, i) => acc + (BigInt(charset.indexOf(x)) * (base ** BigInt(i))), BigInt(0)) +} diff --git a/src/lib/button.ts b/src/lib/button.ts new file mode 100644 index 0000000..794df35 --- /dev/null +++ b/src/lib/button.ts @@ -0,0 +1,29 @@ +import { h, Children, concatClassName } from 'lib/h' + +export function raw(attrs: object, ...children: Children): Element { + return h('button', + concatClassName(attrs, 'g-Button__Raw'), + ...children + ) +} + +export function text(attrs: object, ...children: Children): Element { + return h('button', + concatClassName(attrs, 'g-Button__Text'), + ...children + ) +} + +export function action(attrs: object, ...children: Children): Element { + return h('button', + concatClassName(attrs, 'g-Button__Action'), + ...children + ) +} + +export function cancel(attrs: object, ...children: Children): Element { + return h('button', + concatClassName(attrs, 'g-Button__Cancel'), + ...children + ) +} diff --git a/src/lib/color.ts b/src/lib/color.ts new file mode 100644 index 0000000..59b320d --- /dev/null +++ b/src/lib/color.ts @@ -0,0 +1,36 @@ +interface Color { + red: number, + green: number, + blue: number, +} + +export function parse(str: string): Color { + return { + red: parseInt(str.slice(1,3), 16), + green: parseInt(str.slice(3,5), 16), + blue: parseInt(str.slice(5,7), 16), + } +} + +// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrastratio +export function contrastRatio(c1: Color, c2: Color): number { + const r1 = relativeLuminance(c1) + const r2 = relativeLuminance(c2) + + return r1 > r2 + ? (r1 + 0.05) / (r2 + 0.05) + : (r2 + 0.05) / (r1 + 0.05) +} + +function relativeLuminance(c: Color): number { + return ( + 0.2126 * fromSRGB(c.red / 255) + + 0.7152 * fromSRGB(c.green / 255) + + 0.0722 * fromSRGB(c.blue / 255)) +} + +function fromSRGB(sRGB: number): number { + return sRGB <= 0.03928 + ? sRGB / 12.92 + : Math.pow(((sRGB + 0.055) / 1.055), 2.4) +} diff --git a/src/lib/contextMenu.ts b/src/lib/contextMenu.ts new file mode 100644 index 0000000..6edd567 --- /dev/null +++ b/src/lib/contextMenu.ts @@ -0,0 +1,35 @@ +import { h } from 'lib/h' + +interface Action { + label: string, + action: () => void +} + +export function show(event: MouseEvent, actions: Action[]) { + const menu = h('div', + { id: 'g-ContextMenu', + style: `left: ${event.pageX.toString()}px; top: ${event.pageY.toString()}px` + }, + ...actions.map(({ label, action }) => + h('div', + { className: 'g-ContextMenu__Entry', + onclick: () => action() + }, + label + ) + ) + ) + + document.body.appendChild(menu) + + // Remove on click or context menu + setTimeout(() => { + const f = () => { + document.body.removeChild(menu) + document.body.removeEventListener('click', f) + document.body.removeEventListener('contextmenu', f) + } + document.body.addEventListener('click', f) + document.body.addEventListener('contextmenu', f) + }, 0) +} diff --git a/src/lib/dom.ts b/src/lib/dom.ts new file mode 100644 index 0000000..2ab4de5 --- /dev/null +++ b/src/lib/dom.ts @@ -0,0 +1,6 @@ +export function replaceChildren(parent: Element, ...newChildren: Element[]) { + while (parent.lastChild) { + parent.removeChild(parent.lastChild) + } + newChildren.forEach(c => parent.appendChild(c)) +} diff --git a/src/lib/form.ts b/src/lib/form.ts new file mode 100644 index 0000000..04a2654 --- /dev/null +++ b/src/lib/form.ts @@ -0,0 +1,54 @@ +import { h } from 'lib/h' +import * as Layout from 'lib/layout' +import * as Button from 'lib/button' + +interface InputParams { + label: string, + attrs: object, +} + +export function input({ label, attrs }: InputParams): Element { + return h('label', + { className: 'g-Form__Field' }, + label, + h('input', attrs), + ) +} + +interface ColorInputParams { + colors: string[], + label: string, + initValue: string, + onInput: (value: string) => void, +} + +export function colorInput({ colors, label, initValue, onInput }: ColorInputParams): Element { + const input = h('input', + { value: initValue, + type: 'color', + oninput: (e: Event) => { + if (e.target !== null) { + onInput((e.target as HTMLInputElement).value) + } + } + } + ) as HTMLInputElement + return h('label', + { className: 'g-Form__Field' }, + label, + Layout.line( + {}, + input, + ...colors.map(color => + Button.raw({ className: 'g-Form__Color', + style: `background-color: ${color}`, + type: 'button', + onclick: () => { + input.value = color + onInput(color) + } + }) + ) + ) + ) +} diff --git a/src/lib/h.ts b/src/lib/h.ts new file mode 100644 index 0000000..7e93311 --- /dev/null +++ b/src/lib/h.ts @@ -0,0 +1,31 @@ +type Child = Element | Text | string | number + +export type Children = Child[] + +export function h(tagName: string, attrs: object, ...children: Children): Element { + let elem = document.createElement(tagName) + elem = Object.assign(elem, attrs) + appendChildren(elem, ...children) + return elem +} + +export function s(tagName: string, attrs: object, ...children: Children): Element { + let elem = document.createElementNS('http://www.w3.org/2000/svg', tagName) + Object.entries(attrs).forEach(([key, value]) => elem.setAttribute(key, value)) + appendChildren(elem, ...children) + return elem +} + +function appendChildren(elem: Element, ...children: Children) { + for (const child of children) { + if (typeof child === 'number') + elem.append(child.toString()) + else + elem.append(child) + } +} + +export function concatClassName(attrs: any, className: string): object { + const existingClassName = 'className' in attrs ? attrs['className'] : undefined + return { ...attrs, className: `${className} ${existingClassName}` } +} diff --git a/src/lib/icons.ts b/src/lib/icons.ts new file mode 100644 index 0000000..8db4e17 --- /dev/null +++ b/src/lib/icons.ts @@ -0,0 +1,66 @@ +import { h, s } from 'lib/h' + +export function get(key: string, attrs: object = {}): Element { + const elem = fromKey(key) + if (elem !== undefined) { + Object.entries(attrs).forEach(([key, value]) => { + elem.setAttribute(key, value) + }) + return elem + } else { + return h('span', {}) + } +} + +// https://yqnn.github.io/svg-path-editor/ +function fromKey(key: string): Element | undefined { + if (key == 'house') { + return s('svg', + { viewBox: '0 0 10 10' }, + s('g', { 'stroke': 'none' }, + s('path', { d: 'M0 4V5H1.5V10H4V7C4.4 6.5 5.6 6.5 6 7V10H8.5V5H10V4L5 0Z' }) + ) + ) + } else if (key == 'music') { + return s('svg', + { viewBox: '0 0 10 10' }, + s('g', { 'stroke': 'none' }, + s('ellipse', { cx: '2', cy: '8.5', rx: '2', ry: '1.5' }), + s('ellipse', { cx: '8', cy: '7', rx: '2', ry: '1.5' }), + s('path', { d: 'M2.5 8.5 H4 V4.5 L8.5 3 V7 H10 V0 L2.5 2.5 Z' }), + ) + ) + } else if (key == 'shopping-cart') { + return s('svg', + { viewBox: '0 0 10 10' }, + s('circle', { cx: '3.3', cy: '8.5', r: '0.8' }), + s('circle', { cx: '7.3', cy: '8.5', r: '0.8' }), + s('path', { d: 'M.5.6C1.3.6 1.8.7 2.1 1L2.3 6H8.5', fill: 'transparent' }), + s('path', { d: 'M2.3 1.9H9.4L8.6 4H2.4' }), + ) + } else if (key == 'medical') { + return s('svg', + { viewBox: '0 0 10 10' }, + s('path', { d: 'M5 1V9M1 5H9', style: 'stroke-width: 3' }), + ) + } else if (key == 'envelope') { + return s('svg', + { viewBox: '0 0 10 10' }, + s('path', { d: 'M.5 2.5H9.5V7.5H.5ZM.5 3.4 3.5 5Q5 5.8 6.6 5L9.5 3.4', style: 'fill: transparent' }), + ) + } +} + +// Good to add: +// - loisir / cinéma / piscine +// - école +// - gare +// - bus +export function keys(): string[] { + return [ 'house', + 'music', + 'shopping-cart', + 'medical', + 'envelope', + ] +} diff --git a/src/lib/layout.ts b/src/lib/layout.ts new file mode 100644 index 0000000..1e38bfd --- /dev/null +++ b/src/lib/layout.ts @@ -0,0 +1,15 @@ +import { h, Children, concatClassName } from 'lib/h' + +export function section(attrs: object, ...children: Children): Element { + return h('div', + concatClassName(attrs, 'g-Layout__Section'), + ...children + ) +} + +export function line(attrs: object, ...children: Children): Element { + return h('div', + concatClassName(attrs, 'g-Layout__Line'), + ...children + ) +} diff --git a/src/lib/modal.ts b/src/lib/modal.ts new file mode 100644 index 0000000..8454e1c --- /dev/null +++ b/src/lib/modal.ts @@ -0,0 +1,28 @@ +import { h } from 'lib/h' +import * as Button from 'lib/button' + +export function show(content: Element) { + document.body.appendChild(h('div', + { id: 'g-Modal' }, + h('div', + { className: 'g-Modal__Curtain', + onclick: () => hide() + } + ), + h('div', + { className: 'g-Modal__Window' }, + Button.raw( + { className: 'g-Modal__Close', + onclick: () => hide() + }, + 'x' + ), + content + ) + )) +} + +export function hide() { + const modal = document.querySelector('#g-Modal') + modal && document.body.removeChild(modal) +} |