/* * This file is part of TREB. * * TREB is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any * later version. * * TREB is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along * with TREB. If not, see . * * Copyright 2022-2026 trebco, llc. * info@treb.app * */ import { Editor, type NodeDescriptor } from './editor'; export type KeyEventCallbackType = (event: KeyboardEvent) => void; export class ExternalEditor extends Editor { public key_event_callback?: KeyEventCallbackType; public get active() { return this.nodes.length > 0; } public Reset() { this.AttachNodes(); this.key_event_callback = undefined; } /** * attach to a set of nodes (one is fine). */ public AttachNodes(nodes: HTMLElement[] = [], assume_formula = true) { this.assume_formula = assume_formula; // try to preserve any nodes/descriptors we've already "cooked", // since this may get called multiple times when you switch between // fields. // (that's less true than before, but still might happen). let descriptors: NodeDescriptor[] = []; descriptors = nodes.map(node => { for (const compare of this.nodes) { if (compare.node === node) { return compare; } } return { node }; // not found, return a new one }); // we should probably clean up here. if there's overlap we will just // add a new one. note that we're looping over the *old* set here, // so don't try to optimize by moving this into another loop. for (const descriptor of this.nodes) { if (descriptor.listeners) { for (const [key, value] of descriptor.listeners.entries()) { descriptor.node.removeEventListener(key, value); } descriptor.listeners.clear(); } } this.nodes = descriptors; for (const descriptor of this.nodes) { // check if we need to flush the cached text if (descriptor.formatted_text === descriptor.node.textContent) { const test = descriptor.node.innerHTML.length; if (descriptor.check !== test) { descriptor.formatted_text = undefined; // flush } } // if it's already focused, set as active if (descriptor.node.ownerDocument.activeElement === descriptor.node) { this.active_editor = descriptor; } // format this.UpdateText(descriptor, { toll_events: true }); // add listeners this.RegisterListener(descriptor, 'focusin', () => { // console.info('focusin'); this.active_editor = descriptor; }); this.RegisterListener(descriptor, 'focusout', () => { // console.info('focusout'); this.active_editor = undefined; }); this.RegisterListener(descriptor, 'keydown', (event: KeyboardEvent) => { if (this.selecting) { switch (event.key) { case 'ArrowDown': case 'ArrowUp': case 'ArrowLeft': case 'ArrowRight': break; case '/': if (event.ctrlKey) { break; } // otherwise fall through default: return; } } // we need to pass this event up to grid to handle. we might // also want to filter it (let the handler dp that) this.key_event_callback?.(event); }); this.RegisterListener(descriptor, 'input', (event: Event) => { // we're filtering on trusted here because we send an event. // but when we send that event we also trigger an update. // so... why not just run through the event handler? if (event.isTrusted) { this.UpdateText(descriptor); this.UpdateColors(); // will send a local event } }); } this.UpdateColors(true); // always send an event, just in case } }