UNPKG

@lobehub/editor

Version:

A powerful and extensible rich text editor built on Meta's Lexical framework, providing a modern editing experience with React integration.

167 lines (160 loc) 6.59 kB
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import { $insertList, $isListItemNode, $isListNode } from '@lexical/list'; import { $findMatchingParent, calculateZoomLevel, isHTMLElement, mergeRegister } from '@lexical/utils'; import { $getNearestNodeFromDOMNode, $getSelection, $isElementNode, $isRangeSelection, COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, createCommand, getNearestEditorFromDOMNode } from 'lexical'; export var INSERT_CHECK_LIST_COMMAND = createCommand('INSERT_CHECK_LIST_COMMAND'); function handleCheckItemEvent(event, callback) { var target = event.target; if (!isHTMLElement(target)) { return; } // Ignore clicks on LI that have nested lists var firstChild = target.firstChild; if (isHTMLElement(firstChild) && (firstChild.tagName === 'UL' || firstChild.tagName === 'OL')) { return; } var parentNode = target.parentNode; // @ts-ignore internal field if (!parentNode || parentNode.__lexicalListType !== 'check') { return; } var rect = target.getBoundingClientRect(); var zoom = calculateZoomLevel(target); var clientX = event.clientX / zoom; // Use getComputedStyle if available, otherwise fallback to 0px width var beforeStyles = window.getComputedStyle ? window.getComputedStyle(target, '::before') : { width: '0px' }; var beforeWidthInPixels = parseFloat(beforeStyles.width); var beforeMargin = parseFloat(beforeStyles.marginLeft); // Make click area slightly larger for touch devices to improve accessibility var isTouchEvent = event.pointerType === 'touch'; var clickAreaPadding = isTouchEvent ? 32 : 0; // Add 32px padding for touch events if (target.dir === 'rtl' ? clientX < rect.right + clickAreaPadding && clientX > rect.right - beforeWidthInPixels - clickAreaPadding : clientX > rect.left + beforeMargin - clickAreaPadding && clientX < rect.left + beforeMargin + beforeWidthInPixels + clickAreaPadding) { callback(); } } function handleClick(event) { handleCheckItemEvent(event, function () { if (isHTMLElement(event.target)) { var domNode = event.target; var editor = getNearestEditorFromDOMNode(domNode); if (editor && editor.isEditable()) { editor.update(function () { var node = $getNearestNodeFromDOMNode(domNode); if ($isListItemNode(node)) { domNode.focus(); node.toggleChecked(); } }); } } }); } function handlePointerDown(event) { handleCheckItemEvent(event, function () { // Prevents caret moving when clicking on check mark event.preventDefault(); }); } function getActiveCheckListItem() { var activeElement = document.activeElement; return isHTMLElement(activeElement) && activeElement.tagName === 'LI' && activeElement.parentNode && // @ts-ignore internal field activeElement.parentNode.__lexicalListType === 'check' ? activeElement : null; } function findCheckListItemSibling(node, backward) { var sibling = backward ? node.getPreviousSibling() : node.getNextSibling(); var parent = node; // Going up in a tree to get non-null sibling while (!sibling && $isListItemNode(parent)) { // Get li -> parent ul/ol -> parent li parent = parent.getParentOrThrow().getParent(); if (parent) { sibling = backward ? parent.getPreviousSibling() : parent.getNextSibling(); } } // Going down in a tree to get first non-nested list item while ($isListItemNode(sibling)) { var firstChild = backward ? sibling.getLastChild() : sibling.getFirstChild(); if (!$isListNode(firstChild)) { return sibling; } sibling = backward ? firstChild.getLastChild() : firstChild.getFirstChild(); } return null; } function handleArrowUpOrDown(event, editor, backward) { var activeItem = getActiveCheckListItem(); if (activeItem) { editor.update(function () { var listItem = $getNearestNodeFromDOMNode(activeItem); if (!$isListItemNode(listItem)) { return; } var nextListItem = findCheckListItemSibling(listItem, backward); if (nextListItem) { nextListItem.selectStart(); var dom = editor.getElementByKey(nextListItem.__key); if (dom) { event.preventDefault(); setTimeout(function () { dom.focus(); }, 0); } } }); } return false; } export function registerCheckList(editor) { return mergeRegister(editor.registerCommand(INSERT_CHECK_LIST_COMMAND, function () { $insertList('check'); return true; }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_DOWN_COMMAND, function (event) { return handleArrowUpOrDown(event, editor, false); }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_UP_COMMAND, function (event) { return handleArrowUpOrDown(event, editor, true); }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_LEFT_COMMAND, function (event) { return editor.getEditorState().read(function () { var selection = $getSelection(); if ($isRangeSelection(selection) && selection.isCollapsed()) { var anchor = selection.anchor; var isElement = anchor.type === 'element'; if (isElement || anchor.offset === 0) { var anchorNode = anchor.getNode(); var elementNode = $findMatchingParent(anchorNode, function (node) { return $isElementNode(node) && !node.isInline(); }); if ($isListItemNode(elementNode)) { var parent = elementNode.getParent(); if ($isListNode(parent) && parent.getListType() === 'check' && (isElement || elementNode.getFirstDescendant() === anchorNode)) { var domNode = editor.getElementByKey(elementNode.__key); if (domNode && document.activeElement !== domNode) { domNode.focus(); event.preventDefault(); return true; } } } } } return false; }); }, COMMAND_PRIORITY_LOW), editor.registerRootListener(function (rootElement, prevElement) { if (rootElement !== null) { rootElement.addEventListener('click', handleClick); rootElement.addEventListener('pointerdown', handlePointerDown); } if (prevElement !== null) { prevElement.removeEventListener('click', handleClick); prevElement.removeEventListener('pointerdown', handlePointerDown); } })); }