diff --git a/.gitignore b/.gitignore index 7b12c1b..505e58e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ docs /coverage .rete-cli .sonar +/tmp \ No newline at end of file diff --git a/src/extensions/keyboard.ts b/src/extensions/keyboard/index.ts similarity index 62% rename from src/extensions/keyboard.ts rename to src/extensions/keyboard/index.ts index 0b0df2d..28e1709 100644 --- a/src/extensions/keyboard.ts +++ b/src/extensions/keyboard/index.ts @@ -1,7 +1,8 @@ import { BaseSchemes } from 'rete' -import { HistoryPlugin } from '..' -import { Action } from '../types' +import { HistoryPlugin } from '../..' +import { Action } from '../../types' +import { isEditableElement } from './utils' /** * Adds keyboard shortcuts for history undo/redo @@ -11,12 +12,17 @@ export function keyboard(plugin: document.addEventListener('keydown', e => { if (!e.ctrlKey && !e.metaKey) return + // Don't trigger history actions if user is typing in an editable element + if (isEditableElement(e.target)) return + switch (e.code) { case 'KeyZ': void plugin.undo() + e.preventDefault() break case 'KeyY': void plugin.redo() + e.preventDefault() break default: } diff --git a/src/extensions/keyboard/utils.ts b/src/extensions/keyboard/utils.ts new file mode 100644 index 0000000..44dec7f --- /dev/null +++ b/src/extensions/keyboard/utils.ts @@ -0,0 +1,62 @@ +/** + * Utility functions for keyboard extension + * @module + */ + +/** + * Checks if an input element accepts text input + * @param element Input element to check + * @returns True if the input accepts text + */ +export function isTextInput(element: HTMLInputElement): boolean { + const inputType = element.type.toLowerCase() + const nonTextInputTypes = ['button', 'checkbox', 'radio', 'submit', 'reset', 'file', 'image', 'hidden'] + + return !nonTextInputTypes.includes(inputType) +} + +/** + * Checks if an element or its parent is contentEditable + * @param element Element to check + * @returns True if element is contentEditable + */ +export function isContentEditable(element: Element): boolean { + let current: Element | null = element + + while (current) { + if (current.hasAttribute('contenteditable')) { + const contentEditable = current.getAttribute('contenteditable')?.toLowerCase() + + return contentEditable === 'true' || contentEditable === '' + } + current = current.parentElement + } + + return false +} + +/** + * Checks if an element is an input element that accepts text + * @param element Element to check + * @returns True if element is a text input + */ +function isTextInputElement(element: Element): boolean { + const tagName = element.tagName.toLowerCase() + + if (tagName === 'input' && element instanceof HTMLInputElement) { + return isTextInput(element) + } + + return tagName === 'textarea' +} + +/** + * Checks if the event target is an editable element where text input is expected + * @param target Event target element + * @returns True if the target is an editable element + */ +export function isEditableElement(target: EventTarget | null): boolean { + if (!target || !(target instanceof Element)) return false + + return isTextInputElement(target) || isContentEditable(target) +}