@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
JavaScript
/**
* 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);
}
}));
}