-
-
Notifications
You must be signed in to change notification settings - Fork 80
feat(TKPlugin): implemented list-item-level TK detection and positioning #1606
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
631f87a
91e62ec
3c76005
7eb33a2
a6905d0
c3aec84
bd7544d
432e5f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||
| import CardContext from '../context/CardContext'; | ||||||||||||||||||||
| import {$createTKNode, $isTKNode, ExtendedTextNode, TKNode} from '@tryghost/kg-default-nodes'; | ||||||||||||||||||||
| import {$getNodeByKey, $getSelection, $isDecoratorNode, $isRangeSelection, TextNode} from 'lexical'; | ||||||||||||||||||||
| import {$isListItemNode, $isListNode} from '@lexical/list'; | ||||||||||||||||||||
| import {SELECT_CARD_COMMAND} from './KoenigBehaviourPlugin'; | ||||||||||||||||||||
| import {createPortal} from 'react-dom'; | ||||||||||||||||||||
| import {useCallback, useContext, useEffect, useState} from 'react'; | ||||||||||||||||||||
|
|
@@ -11,6 +12,41 @@ import {useTKContext} from '../context/TKContext'; | |||||||||||||||||||
| const REGEX = new RegExp(/(^|.)([^\p{L}\p{N}\s]*(TK|Tk|tk)+[^\p{L}\p{N}\s]*)(.)?/u); | ||||||||||||||||||||
| const WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // TK Indicator positioning constants | ||||||||||||||||||||
| const INDICATOR_OFFSET_RIGHT = -56; | ||||||||||||||||||||
| const INDICATOR_OFFSET_TOP = 4; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Node type constants | ||||||||||||||||||||
| const NODE_TYPES = { | ||||||||||||||||||||
| LIST_ITEM: 'LI' | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Helper function to get effective top-level element, treating list items as containers | ||||||||||||||||||||
| function getEffectiveTopLevelElement(node) { | ||||||||||||||||||||
| const topLevel = node.getTopLevelElement(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!topLevel) { | ||||||||||||||||||||
| return null; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // If the top-level element is not a list, return it directly | ||||||||||||||||||||
| if (!$isListNode(topLevel)) { | ||||||||||||||||||||
| return topLevel; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Find the containing list item for list nodes | ||||||||||||||||||||
| let currentNode = node; | ||||||||||||||||||||
| while (currentNode?.getParent()) { | ||||||||||||||||||||
| if ($isListItemNode(currentNode)) { | ||||||||||||||||||||
| return currentNode; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| currentNode = currentNode.getParent(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Fallback to top-level element if no list item found | ||||||||||||||||||||
| return topLevel; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function TKIndicator({editor, rootElement, parentKey, nodeKeys}) { | ||||||||||||||||||||
| const tkClasses = editor._config.theme.tk?.split(' ') || []; | ||||||||||||||||||||
| const tkHighlightClasses = editor._config.theme.tkHighlighted?.split(' ') || []; | ||||||||||||||||||||
|
|
@@ -20,14 +56,18 @@ function TKIndicator({editor, rootElement, parentKey, nodeKeys}) { | |||||||||||||||||||
| // position element relative to the TK Node containing element | ||||||||||||||||||||
| const calculatePosition = useCallback(() => { | ||||||||||||||||||||
| let top = 0; | ||||||||||||||||||||
| let right = -56; | ||||||||||||||||||||
| let right = INDICATOR_OFFSET_RIGHT; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const rootElementRect = rootElement.getBoundingClientRect(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const positioningElement = containingElement.querySelector('[data-kg-card]') || containingElement; | ||||||||||||||||||||
| // Determine positioning element based on container type | ||||||||||||||||||||
| const positioningElement = containingElement.nodeName === NODE_TYPES.LIST_ITEM | ||||||||||||||||||||
| ? containingElement | ||||||||||||||||||||
| : containingElement.querySelector('[data-kg-card]') || containingElement; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const positioningElementRect = positioningElement.getBoundingClientRect(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| top = positioningElementRect.top - rootElementRect.top + 4; | ||||||||||||||||||||
| top = positioningElementRect.top - rootElementRect.top + INDICATOR_OFFSET_TOP; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (positioningElementRect.right > rootElementRect.right) { | ||||||||||||||||||||
| right = right - (positioningElementRect.right - rootElementRect.right); | ||||||||||||||||||||
|
|
@@ -83,12 +123,17 @@ function TKIndicator({editor, rootElement, parentKey, nodeKeys}) { | |||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| nodeKeys.forEach((key) => { | ||||||||||||||||||||
| const element = editor.getElementByKey(key); | ||||||||||||||||||||
| if (!element) { | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (isHighlighted) { | ||||||||||||||||||||
| editor.getElementByKey(key).classList.remove(...tkClasses); | ||||||||||||||||||||
| editor.getElementByKey(key).classList.add(...tkHighlightClasses); | ||||||||||||||||||||
| element.classList.remove(...tkClasses); | ||||||||||||||||||||
| element.classList.add(...tkHighlightClasses); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| editor.getElementByKey(key).classList.add(...tkClasses); | ||||||||||||||||||||
| editor.getElementByKey(key).classList.remove(...tkHighlightClasses); | ||||||||||||||||||||
| element.classList.add(...tkClasses); | ||||||||||||||||||||
| element.classList.remove(...tkHighlightClasses); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
@@ -115,6 +160,11 @@ function TKIndicator({editor, rootElement, parentKey, nodeKeys}) { | |||||||||||||||||||
| }; | ||||||||||||||||||||
| }, [rootElement, containingElement, calculatePosition]); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Early return if containing element is not available (after all hooks) | ||||||||||||||||||||
| if (!containingElement) { | ||||||||||||||||||||
| return null; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const style = { | ||||||||||||||||||||
| top: `${position.top}px`, | ||||||||||||||||||||
| right: `${position.right}px` | ||||||||||||||||||||
|
|
@@ -157,7 +207,8 @@ export default function TKPlugin() { | |||||||||||||||||||
| if (mutation === 'destroyed') { | ||||||||||||||||||||
| removeEditorTkNode(editor.getKey(), tkNodeKey); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| const parentNodeKey = $getNodeByKey(tkNodeKey).getTopLevelElement()?.getKey(); | ||||||||||||||||||||
| const effectiveTopLevel = getEffectiveTopLevelElement($getNodeByKey(tkNodeKey)); | ||||||||||||||||||||
| const parentNodeKey = effectiveTopLevel?.getKey(); | ||||||||||||||||||||
| const topLevelNodeKey = parentEditorNodeKey || parentNodeKey; | ||||||||||||||||||||
|
Comment on lines
+214
to
216
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 🧩 Analysis chainGuard addEditorTkNode when no container key is resolved Effective container can be null in rare cases; avoid inserting undefined keys into tkNodeMap. Apply: -const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
-addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
+if (topLevelNodeKey) {
+ addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+}If you suspect this never happens in practice, quick check: 🏁 Script executed: #!/bin/bash
# Look for any safeguards already in addEditorTkNode implementation
rg -nC2 "function\s+addEditorTkNode|addEditorTkNode\s*\(" --type=js --type=jsxLength of output: 27 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "Git branch: $(git rev-parse --abbrev-ref HEAD || true)"
echo "--- Search for addEditorTkNode occurrences ---"
rg -nC3 "addEditorTkNode" || true
echo "--- Search for possible definitions ---"
rg -nC3 "function\s+addEditorTkNode" || true
rg -nC3 "const\s+addEditorTkNode\s*=" || true
rg -nC3 "export\s+function\s+addEditorTkNode" || true
echo "--- Search for tkNodeMap occurrences ---"
rg -nC3 "tkNodeMap" || true
echo "--- Show packages/koenig-lexical/src/plugins/TKPlugin.jsx (first 300 lines) ---"
if [ -f "packages/koenig-lexical/src/plugins/TKPlugin.jsx" ]; then
sed -n '1,300p' packages/koenig-lexical/src/plugins/TKPlugin.jsx
else
echo "TKPlugin.jsx not found at expected path; listing directory:"
ls -la packages/koenig-lexical/src/plugins || true
fiLength of output: 18029 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "---- show addEditorTkNode occurrences ----"
rg -nC3 "addEditorTkNode" packages/koenig-lexical || true
echo "---- print TKContext.jsx ----"
if [ -f "packages/koenig-lexical/src/context/TKContext.jsx" ]; then
sed -n '1,260p' packages/koenig-lexical/src/context/TKContext.jsx
echo "---- remainder ----"
sed -n '261,520p' packages/koenig-lexical/src/context/TKContext.jsx || true
else
echo "packages/koenig-lexical/src/context/TKContext.jsx not found"
ls -la packages/koenig-lexical/src || true
fiLength of output: 6996 Guard addEditorTkNode when no container key is resolved Effective container can be null; avoid inserting undefined keys into tkNodeMap. Apply: -const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
-addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
+if (topLevelNodeKey) {
+ addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+}File: packages/koenig-lexical/src/plugins/TKPlugin.jsx (around lines 208-211) 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -247,7 +298,7 @@ export default function TKPlugin() { | |||||||||||||||||||
| const parentContainer = editor.getElementByKey(parentKey); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!parentContainer) { | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
| return null; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return ( | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix crash when containingElement is null (initial render + observer)
calculatePosition dereferences containingElement before the post-hook early return; useEffect also observes a null element. Both can throw.
Apply:
Also applies to: 79-79, 150-161
🤖 Prompt for AI Agents