janky 'toolset' demo
This commit is contained in:
parent
1184f8a9b8
commit
4219994f6e
|
|
@ -1,46 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Toolbar Demo</title>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
folk-shape {
|
||||
background: transparent;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<folk-distance-field>
|
||||
<folk-shape id="box1" x="100" y="100" width="50" height="50"></folk-shape>
|
||||
<folk-shape id="box2" x="100" y="200" width="50" height="50"></folk-shape>
|
||||
<folk-shape id="box3" x="100" y="300" width="50" height="50"></folk-shape>
|
||||
<folk-shape id="box4" x="300" y="150" width="80" height="40"></folk-shape>
|
||||
<folk-shape id="box5" x="400" y="250" width="60" height="90"></folk-shape>
|
||||
<folk-shape id="box6" x="200" y="400" width="100" height="100"></folk-shape>
|
||||
<folk-shape id="box7" x="500" y="100" width="30" height="70"></folk-shape>
|
||||
</folk-distance-field>
|
||||
|
||||
<folk-toolbar></folk-toolbar>
|
||||
|
||||
<script type="module">
|
||||
import '../src/standalone/folk-shape.ts';
|
||||
import '../src/standalone/folk-distance-field.ts';
|
||||
import '../src/standalone/folk-toolbar.ts';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Toolbar Demo</title>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
folk-shape {
|
||||
background: transparent;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<folk-toolset>
|
||||
<folk-distance-field>
|
||||
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
|
||||
<folk-shape x="100" y="200" width="50" height="50"></folk-shape>
|
||||
<folk-shape x="100" y="300" width="50" height="50"></folk-shape>
|
||||
<folk-shape x="300" y="150" width="80" height="40"></folk-shape>
|
||||
<folk-shape x="400" y="250" width="60" height="90"></folk-shape>
|
||||
<folk-shape x="200" y="400" width="100" height="100"></folk-shape>
|
||||
<folk-shape x="500" y="100" width="30" height="70"></folk-shape>
|
||||
<folk-shape x="500" y="200">
|
||||
<folk-shape-tool></folk-shape-tool>
|
||||
</folk-shape>
|
||||
<folk-shape x="700" y="200">
|
||||
<folk-delete-tool></folk-delete-tool>
|
||||
</folk-shape>
|
||||
</folk-distance-field>
|
||||
</folk-toolset>
|
||||
|
||||
<script type="module">
|
||||
import '../src/standalone/folk-shape.ts';
|
||||
import '../src/standalone/folk-distance-field.ts';
|
||||
import '../src/standalone/folk-toolset.ts';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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(
|
||||
`
|
||||
<folk-event-propagator
|
||||
source="#${sourceId}"
|
||||
target="#${targetId}"
|
||||
trigger="click"
|
||||
expression="rotation: Math.random() * 360"
|
||||
></folk-event-propagator>
|
||||
`,
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import './folk-event-propagator';
|
||||
import { FolkToolbar } from '../folk-toolbar';
|
||||
|
||||
FolkToolbar.define();
|
||||
|
||||
export { FolkToolbar };
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import './folk-event-propagator';
|
||||
import { FolkToolset } from '../folk-toolset';
|
||||
|
||||
FolkToolset.define();
|
||||
|
||||
export { FolkToolset as FolkToolbar };
|
||||
Loading…
Reference in New Issue