From 4219994f6e5abc3515bcc6de9bddf42757f805b5 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 15 Dec 2024 05:30:38 -0500 Subject: [PATCH] janky 'toolset' demo --- demo/propagator-toolbar.html | 46 ---------- demo/toolset.html | 52 +++++++++++ src/folk-toolbar.ts | 126 -------------------------- src/folk-toolset.ts | 157 +++++++++++++++++++++++++++++++++ src/standalone/folk-toolbar.ts | 6 -- src/standalone/folk-toolset.ts | 6 ++ 6 files changed, 215 insertions(+), 178 deletions(-) delete mode 100644 demo/propagator-toolbar.html create mode 100644 demo/toolset.html delete mode 100644 src/folk-toolbar.ts create mode 100644 src/folk-toolset.ts delete mode 100644 src/standalone/folk-toolbar.ts create mode 100644 src/standalone/folk-toolset.ts diff --git a/demo/propagator-toolbar.html b/demo/propagator-toolbar.html deleted file mode 100644 index 41e7da0..0000000 --- a/demo/propagator-toolbar.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Toolbar Demo - - - - - - - - - - - - - - - - - - diff --git a/demo/toolset.html b/demo/toolset.html new file mode 100644 index 0000000..c0df420 --- /dev/null +++ b/demo/toolset.html @@ -0,0 +1,52 @@ + + + + + + Toolbar Demo + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/folk-toolbar.ts b/src/folk-toolbar.ts deleted file mode 100644 index f964a03..0000000 --- a/src/folk-toolbar.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { css } from './common/tags.ts'; - -const styles = css` - :host { - position: fixed; - bottom: 16px; - left: 50%; - transform: translateX(-50%); - background: white; - padding: 8px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - display: flex; - gap: 8px; - } - - button { - padding: 8px 16px; - border-radius: 4px; - border: 1px solid #ccc; - background: white; - cursor: pointer; - } - - button.active { - background: #eee; - } -`; - -export class FolkToolbar extends HTMLElement { - static tagName = 'folk-toolbar'; - - static define() { - if (customElements.get(this.tagName)) return; - customElements.define(this.tagName, this); - } - - #mode: 'idle' | 'connecting' = 'idle'; - #sourceElement: Element | null = null; - #connectBtn: HTMLButtonElement; - - constructor() { - super(); - - const shadow = this.attachShadow({ mode: 'open' }); - shadow.adoptedStyleSheets = [styles]; - - const connectBtn = document.createElement('button'); - connectBtn.textContent = 'Connect Elements'; - connectBtn.addEventListener('click', () => this.toggleConnectionMode()); - this.#connectBtn = connectBtn; - shadow.appendChild(connectBtn); - - document.addEventListener('click', this.handleDocumentClick.bind(this)); - } - - toggleConnectionMode() { - if (this.#mode === 'idle') { - this.#mode = 'connecting'; - this.#sourceElement = null; - document.body.style.cursor = 'crosshair'; - this.#connectBtn.classList.add('active'); - this.#connectBtn.textContent = 'Select Source Element...'; - } else { - this.#mode = 'idle'; - this.#sourceElement = null; - document.body.style.cursor = ''; - this.#connectBtn.classList.remove('active'); - this.#connectBtn.textContent = 'Connect Elements'; - } - } - - handleDocumentClick(event: MouseEvent) { - if (this.#mode !== 'connecting') return; - - // Prevent clicking toolbar itself - if (event.composedPath().includes(this)) return; - - event.preventDefault(); - - const target = event.target as Element; - - if (!this.#sourceElement) { - // First click - select source - this.#sourceElement = target; - this.#connectBtn.textContent = 'Select Target Element...'; - } else { - // Second click - create connection - this.createConnection(this.#sourceElement, target); - this.#sourceElement = null; - this.#mode = 'idle'; - document.body.style.cursor = ''; - this.#connectBtn.classList.remove('active'); - this.#connectBtn.textContent = 'Connect Elements'; - } - } - - createConnection(source: Element, target: Element) { - const sourceId = source.id || this.ensureElementId(source); - const targetId = target.id || this.ensureElementId(target); - - // hack because we gotta sort out usage of constructor vs connectedCallback - const propagator = new DOMParser().parseFromString( - ` - - `, - 'text/html' - ).body.firstElementChild; - - if (propagator) { - document.body.appendChild(propagator); - } - } - - ensureElementId(element: Element): string { - if (!element.id) { - element.id = `folk-element-${Math.random().toString(36).slice(2)}`; - } - return element.id; - } -} diff --git a/src/folk-toolset.ts b/src/folk-toolset.ts new file mode 100644 index 0000000..4d2d7bf --- /dev/null +++ b/src/folk-toolset.ts @@ -0,0 +1,157 @@ +import { FolkShape } from './folk-shape'; + +export abstract class FolkInteractionHandler extends HTMLElement { + abstract readonly events: string[]; + abstract handleEvent(event: Event): void; + + static toolbar: FolkToolset | null = null; + + protected button: HTMLButtonElement; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + const style = document.createElement('style'); + style.textContent = ` + button { + padding: 8px 16px; + border: 2px solid transparent; + cursor: pointer; + } + :host(.active) button { + background-color: #00aaff; + outline: none; + } + `; + + this.button = document.createElement('button'); + this.shadowRoot!.appendChild(style); + this.shadowRoot!.appendChild(this.button); + this.button.addEventListener('click', () => this.activate()); + } + + activate() { + console.log('activate', this); + FolkToolset.setActiveTool(this); + } +} + +export class FolkShapeTool extends FolkInteractionHandler { + static tagName = 'folk-shape-tool'; + readonly events = ['pointerdown']; + + constructor() { + super(); + this.button.textContent = 'Create Shape'; + } + + handleEvent(event: Event): void { + if (!(event instanceof PointerEvent)) return; + const target = event.target as HTMLElement; + if (!target || target instanceof FolkShape) return; + + event.stopImmediatePropagation(); + + const shape = new FolkShape(); + const rect = target.getBoundingClientRect(); + const width = 100; + const height = 100; + shape.x = event.clientX - rect.left - width / 2; + shape.y = event.clientY - rect.top - height / 2; + shape.width = width; + shape.height = height; + + target.appendChild(shape); + shape.focus(); + } + + static define() { + if (!customElements.get(this.tagName)) { + customElements.define(this.tagName, this); + } + } +} + +export class FolkDeleteTool extends FolkInteractionHandler { + static tagName = 'folk-delete-tool'; + readonly events = ['pointerdown']; + + constructor() { + super(); + this.button.textContent = 'Delete'; + } + + handleEvent(event: Event): void { + if (!(event instanceof PointerEvent)) return; + const target = event.target as HTMLElement; + if (!target || !(target instanceof FolkShape)) return; + event.stopImmediatePropagation(); + target.remove(); + } + + static define() { + if (!customElements.get(this.tagName)) { + customElements.define(this.tagName, this); + } + } +} + +export class FolkToolset extends HTMLElement { + static tagName = 'folk-toolset'; + private static instance: FolkToolset | null = null; + private currentHandler: ((event: Event) => void) | null = null; + private activeTool: FolkInteractionHandler | null = null; + + static setActiveTool(tool: FolkInteractionHandler) { + if (this.instance) { + this.instance.activateTool(tool); + } + } + + constructor() { + super(); + FolkToolset.instance = this; + } + + private activateTool(tool: FolkInteractionHandler) { + // Remove active class from previous tool + if (this.activeTool) { + this.activeTool.classList.remove('active'); + } + + // Deactivate current handler + if (this.currentHandler) { + tool.events.forEach((event) => { + this.removeEventListener(event, this.currentHandler!, true); + }); + } + + // If clicking same tool, just deactivate + if (this.currentHandler === tool.handleEvent.bind(tool)) { + this.currentHandler = null; + this.activeTool = null; + return; + } + + // Activate new handler + this.currentHandler = tool.handleEvent.bind(tool); + tool.events.forEach((event) => { + this.addEventListener(event, this.currentHandler!, true); + }); + + // Add active class to new tool + tool.classList.add('active'); + this.activeTool = tool; + } + + static define() { + if (!customElements.get(this.tagName)) { + customElements.define(this.tagName, this); + } + } +} + +FolkShapeTool.define(); +FolkDeleteTool.define(); +FolkToolset.define(); diff --git a/src/standalone/folk-toolbar.ts b/src/standalone/folk-toolbar.ts deleted file mode 100644 index 65efce5..0000000 --- a/src/standalone/folk-toolbar.ts +++ /dev/null @@ -1,6 +0,0 @@ -import './folk-event-propagator'; -import { FolkToolbar } from '../folk-toolbar'; - -FolkToolbar.define(); - -export { FolkToolbar }; diff --git a/src/standalone/folk-toolset.ts b/src/standalone/folk-toolset.ts new file mode 100644 index 0000000..36e2f4c --- /dev/null +++ b/src/standalone/folk-toolset.ts @@ -0,0 +1,6 @@ +import './folk-event-propagator'; +import { FolkToolset } from '../folk-toolset'; + +FolkToolset.define(); + +export { FolkToolset as FolkToolbar };