aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/autoComplete.ts115
-rw-r--r--src/lib/base.ts32
-rw-r--r--src/lib/button.ts29
-rw-r--r--src/lib/color.ts36
-rw-r--r--src/lib/contextMenu.ts35
-rw-r--r--src/lib/dom.ts6
-rw-r--r--src/lib/form.ts54
-rw-r--r--src/lib/h.ts31
-rw-r--r--src/lib/icons.ts66
-rw-r--r--src/lib/layout.ts15
-rw-r--r--src/lib/modal.ts28
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)
+}