Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ docs
/coverage
.rete-cli
.sonar
/tmp
10 changes: 8 additions & 2 deletions src/extensions/keyboard.ts → src/extensions/keyboard/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,12 +12,17 @@ export function keyboard<Schemes extends BaseSchemes, A extends Action>(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:
}
Expand Down
62 changes: 62 additions & 0 deletions src/extensions/keyboard/utils.ts
Original file line number Diff line number Diff line change
@@ -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)
}
Loading