feat: single-click text inputs to edit, drag/resize on shape body
Shapes are now grabbable/draggable/resizable on first click (body or handles), editable on double-click, but clicking directly on a text input (textarea, contenteditable, text input) immediately enters edit mode and focuses the input — no double-click required. Works by hit-testing text inputs at pointer coordinates before initiating drag, checking both light DOM and shadow DOM of slotted content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8bd899d146
commit
6dae60ea1f
|
|
@ -340,6 +340,36 @@ export class FolkShape extends FolkElement {
|
|||
this.dispatchEvent(new CustomEvent("edit-enter"));
|
||||
}
|
||||
|
||||
/** Check if a screen-space point overlaps a text input inside this shape's slotted content. */
|
||||
#findTextInputAtPoint(clientX: number, clientY: number): HTMLElement | null {
|
||||
const slot = (this.renderRoot as ShadowRoot).querySelector("slot");
|
||||
if (!slot) return null;
|
||||
|
||||
const TEXT_SELECTOR = 'textarea, [contenteditable="true"], input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="hidden"])';
|
||||
|
||||
for (const el of slot.assignedElements()) {
|
||||
// Check light DOM children
|
||||
const lightInputs = el.querySelectorAll<HTMLElement>(TEXT_SELECTOR);
|
||||
for (const input of lightInputs) {
|
||||
const rect = input.getBoundingClientRect();
|
||||
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
// Check shadow DOM children (one level deep — covers all folk-* shapes)
|
||||
if ((el as Element).shadowRoot) {
|
||||
const shadowInputs = (el as Element).shadowRoot!.querySelectorAll<HTMLElement>(TEXT_SELECTOR);
|
||||
for (const input of shadowInputs) {
|
||||
const rect = input.getBoundingClientRect();
|
||||
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
exitEditMode() {
|
||||
if (!this.#editing) return;
|
||||
this.#editing = false;
|
||||
|
|
@ -462,7 +492,15 @@ export class FolkShape extends FolkElement {
|
|||
if (event.type === "touchstart" && event.touches.length === 1) {
|
||||
if (!isValidDragTarget) return;
|
||||
|
||||
// Check if touch is over a text input — enter edit mode instead of dragging
|
||||
const touch = event.touches[0];
|
||||
const textInput = this.#findTextInputAtPoint(touch.clientX, touch.clientY);
|
||||
if (textInput) {
|
||||
this.enterEditMode();
|
||||
requestAnimationFrame(() => textInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.#lastTouchPos = { x: touch.clientX, y: touch.clientY };
|
||||
this.#isTouchDragging = true;
|
||||
this.#internals.states.add("move");
|
||||
|
|
@ -520,6 +558,18 @@ export class FolkShape extends FolkElement {
|
|||
// Allow drag from: the host itself, a handle, or a drag handle element
|
||||
if (target !== this && !handle && !isDragHandle) return;
|
||||
|
||||
// If clicking on the shape body (not a handle), check if a text input
|
||||
// is under the pointer — if so, enter edit mode immediately instead of dragging.
|
||||
if (!handle && (target === this || isDragHandle)) {
|
||||
const textInput = this.#findTextInputAtPoint(event.clientX, event.clientY);
|
||||
if (textInput) {
|
||||
this.enterEditMode();
|
||||
// Re-dispatch click so the input receives focus after pointer-events are enabled
|
||||
requestAnimationFrame(() => textInput.focus());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (handle?.startsWith("rotation")) {
|
||||
const parentRotateOrigin = this.#rect.toParentSpace({
|
||||
x: this.#rect.width * this.#rect.rotateOrigin.x,
|
||||
|
|
|
|||
Loading…
Reference in New Issue