Skip to content

Commit b8ddf4d

Browse files
committed
Make placeholder navigation more intuitive
1 parent c2a4231 commit b8ddf4d

File tree

8 files changed

+182
-77
lines changed

8 files changed

+182
-77
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ELEMENT_PLACEHOLDER, ELEMENT_REGELVERK_CONTAINER, UNCHANGEABLE } from '@app/plate/plugins/element-types';
2+
import { NodeApi, type Path } from 'platejs';
3+
import type { PlateEditor } from 'platejs/react';
4+
5+
export const isEditableTextNode = (editor: PlateEditor, path: Path): boolean => {
6+
const parent = editor.api.parent(path);
7+
8+
if (parent === undefined) {
9+
return false;
10+
}
11+
12+
const [parentNode] = parent;
13+
14+
if (parentNode.type === ELEMENT_PLACEHOLDER) {
15+
return true;
16+
}
17+
18+
const ancestors = NodeApi.ancestors(editor, path, { reverse: true });
19+
20+
for (const [ancestor] of ancestors) {
21+
if (
22+
NodeApi.isEditor(ancestor) ||
23+
ancestor.type === ELEMENT_PLACEHOLDER ||
24+
ancestor.type === ELEMENT_REGELVERK_CONTAINER
25+
) {
26+
return true;
27+
}
28+
29+
if (UNCHANGEABLE.includes(ancestor.type)) {
30+
return false;
31+
}
32+
}
33+
34+
return false;
35+
};
Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import { merge } from '@app/functions/classes';
22
import type { SpellCheckLanguage } from '@app/hooks/use-smart-editor-language';
3-
import { ELEMENT_MALTEKST, ELEMENT_PLACEHOLDER } from '@app/plate/plugins/element-types';
4-
import type { FormattedText } from '@app/plate/types';
3+
import { isEditableTextNode } from '@app/plate/functions/is-editable-text';
54
import {
65
PlateContent,
76
type PlateContentProps,
87
type PlateEditor,
98
useEditorReadOnly,
109
useEditorRef,
1110
} from '@platejs/core/react';
12-
import { NodeApi } from 'platejs';
13-
import type { HTMLAttributes } from 'react';
11+
import type { TText } from 'platejs';
1412

1513
interface Props extends PlateContentProps {
1614
lang: SpellCheckLanguage;
@@ -27,44 +25,24 @@ export const KabalPlateEditor = ({ className, spellCheck = true, ...props }: Pro
2725
className={merge('min-h-full outline-none', className)}
2826
spellCheck={spellCheck}
2927
renderLeaf={({ attributes, children, text }) => (
30-
<span {...attributes} contentEditable={contentEditable(editor, readOnly, text)} suppressContentEditableWarning>
28+
<span
29+
{...attributes}
30+
contentEditable={readOnly ? false : getContentEditable(editor, text)}
31+
suppressContentEditableWarning
32+
>
3133
{children}
3234
</span>
3335
)}
3436
/>
3537
);
3638
};
3739

38-
const contentEditable = (
39-
editor: PlateEditor,
40-
isReadOnly: boolean,
41-
text: FormattedText,
42-
): HTMLAttributes<HTMLSpanElement>['contentEditable'] => {
43-
if (isReadOnly) {
44-
return undefined;
45-
}
46-
47-
const path = editor.api.findPath(text);
40+
const getContentEditable = (editor: PlateEditor, textNode: TText) => {
41+
const path = editor.api.findPath(textNode);
4842

4943
if (path === undefined) {
5044
return false;
5145
}
5246

53-
const ancestorEntries = NodeApi.ancestors(editor, path, { reverse: true });
54-
55-
if (ancestorEntries === undefined) {
56-
return false;
57-
}
58-
59-
for (const [node] of ancestorEntries) {
60-
if (node.type === ELEMENT_PLACEHOLDER) {
61-
return true;
62-
}
63-
64-
if (node.type === ELEMENT_MALTEKST) {
65-
return false;
66-
}
67-
}
68-
69-
return true;
47+
return isEditableTextNode(editor, path);
7048
};

frontend/src/plate/plugins/placeholder/arrows.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { isMetaKey, Keys } from '@app/keys';
2+
import { ELEMENT_PLACEHOLDER } from '@app/plate/plugins/element-types';
3+
import { selectNextTextNode } from '@app/plate/plugins/placeholder/select-next-text-node';
4+
import { selectPreviousTextNode } from '@app/plate/plugins/placeholder/select-previous-text-node';
5+
import type { PlateEditor } from 'platejs/react';
6+
import type { KeyboardEvent } from 'react';
7+
8+
export const handleNavigation = (editor: PlateEditor, event: KeyboardEvent): boolean => {
9+
if (editor.selection === null || isMetaKey(event) || editor.api.isExpanded()) {
10+
return false;
11+
}
12+
13+
if (
14+
event.key !== Keys.Enter &&
15+
event.key !== Keys.ArrowRight &&
16+
event.key !== Keys.ArrowLeft &&
17+
event.key !== Keys.ArrowUp &&
18+
event.key !== Keys.ArrowDown
19+
) {
20+
return false;
21+
}
22+
23+
const placeholderEntry = editor.api.node({ match: { type: ELEMENT_PLACEHOLDER } });
24+
25+
if (placeholderEntry === undefined) {
26+
return false;
27+
}
28+
29+
const [, placeholderPath] = placeholderEntry;
30+
31+
if (event.key === Keys.Enter) {
32+
event.preventDefault();
33+
event.stopPropagation();
34+
35+
event.shiftKey ? selectPreviousTextNode(editor, placeholderPath) : selectNextTextNode(editor, placeholderPath);
36+
return true; // Prevent further handling
37+
}
38+
39+
if (
40+
event.key === Keys.ArrowDown ||
41+
(event.key === Keys.ArrowRight && editor.api.isEnd(editor.selection.focus, placeholderPath))
42+
) {
43+
event.preventDefault();
44+
event.stopPropagation();
45+
46+
selectNextTextNode(editor, placeholderPath);
47+
return true; // Prevent further handling
48+
}
49+
50+
if (
51+
event.key === Keys.ArrowUp ||
52+
(event.key === Keys.ArrowLeft && editor.api.isStart(editor.selection.focus, placeholderPath))
53+
) {
54+
event.preventDefault();
55+
event.stopPropagation();
56+
57+
selectPreviousTextNode(editor, placeholderPath);
58+
return true; // Prevent further handling
59+
}
60+
61+
return false;
62+
};

frontend/src/plate/plugins/placeholder/redaktoer.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isMetaKey } from '@app/keys';
22
import { RedaktørPlaceholder } from '@app/plate/components/placeholder/placeholder';
3-
import { handleArrows } from '@app/plate/plugins/placeholder/arrows';
3+
import { handleNavigation } from '@app/plate/plugins/placeholder/handle-navigation';
44
import { parsers } from '@app/plate/plugins/placeholder/html-parsers';
55
import { handleSelectAll } from '@app/plate/plugins/placeholder/select-all';
66
import { isPlaceholderActive } from '@app/plate/utils/queries';
@@ -17,12 +17,17 @@ export const RedaktoerPlaceholderPlugin = createPlatePlugin({
1717
isInline: true,
1818
component: RedaktørPlaceholder,
1919
},
20+
rules: { selection: { affinity: 'directional' } }, // Makes it possible to place the caret at the edges of the placeholder in Chrome.
2021
handlers: {
2122
onKeyDown: ({ editor, event }) => {
22-
if (handleSelectAll(editor, event) || handleArrows(editor, event)) {
23+
if (handleSelectAll(editor, event)) {
2324
return;
2425
}
2526

27+
if (handleNavigation(editor, event)) {
28+
return true; // Prevent further handling
29+
}
30+
2631
if (isMetaKey(event) && event.key.toLowerCase() === 'j') {
2732
event.preventDefault();
2833
event.stopPropagation();

frontend/src/plate/plugins/placeholder/saksbehandler.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { isMetaKey } from '@app/keys';
1+
import { isMetaKey, Keys } from '@app/keys';
22
import { SaksbehandlerPlaceholder } from '@app/plate/components/placeholder/placeholder';
3-
import { handleArrows } from '@app/plate/plugins/placeholder/arrows';
3+
import { handleNavigation } from '@app/plate/plugins/placeholder/handle-navigation';
44
import { parsers } from '@app/plate/plugins/placeholder/html-parsers';
55
import { handleSelectAll } from '@app/plate/plugins/placeholder/select-all';
6+
import { withOverrides } from '@app/plate/plugins/placeholder/with-overrides';
67
import { createPlatePlugin, type PlateEditor } from '@platejs/core/react';
78
import { ElementApi, type NodeEntry } from 'platejs';
89
import type { BasePoint } from 'slate';
910
import type { PlaceholderElement } from '../../types';
1011
import { ELEMENT_PLACEHOLDER } from '../element-types';
11-
import { withOverrides } from './with-overrides';
1212

1313
export const SaksbehandlerPlaceholderPlugin = createPlatePlugin({
1414
key: ELEMENT_PLACEHOLDER,
@@ -18,13 +18,18 @@ export const SaksbehandlerPlaceholderPlugin = createPlatePlugin({
1818
isInline: true,
1919
component: SaksbehandlerPlaceholder,
2020
},
21+
rules: { selection: { affinity: 'directional' } }, // Makes it possible to place the caret at the edges of the placeholder in Chrome.
2122
handlers: {
2223
onKeyDown: ({ editor, event }) => {
23-
if (handleSelectAll(editor, event) || handleArrows(editor, event)) {
24+
if (handleSelectAll(editor, event)) {
2425
return;
2526
}
2627

27-
if (isMetaKey(event) && event.key.toLowerCase() === 'j') {
28+
if (handleNavigation(editor, event)) {
29+
return true; // Prevent further handling
30+
}
31+
32+
if (isMetaKey(event) && event.key.toLowerCase() === Keys.J) {
2833
event.preventDefault();
2934
event.stopPropagation();
3035

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { isEditableTextNode } from '@app/plate/functions/is-editable-text';
2+
import { type Path, TextApi, type TText } from 'platejs';
3+
import type { PlateEditor } from 'platejs/react';
4+
5+
export const selectNextTextNode = (editor: PlateEditor, currentPath: Path): boolean => {
6+
const next = editor.api.next<TText>({
7+
at: currentPath,
8+
mode: 'lowest',
9+
text: true,
10+
match: (n, p) => TextApi.isText(n) && isEditableTextNode(editor, p),
11+
});
12+
13+
if (next === undefined) {
14+
return false;
15+
}
16+
17+
const [, nextPath] = next;
18+
19+
const nextStartPoint = editor.api.start(nextPath);
20+
21+
if (nextStartPoint === undefined) {
22+
return false;
23+
}
24+
25+
editor.tf.select(nextStartPoint);
26+
editor.tf.focus();
27+
28+
return true;
29+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { isEditableTextNode } from '@app/plate/functions/is-editable-text';
2+
import { type Path, TextApi, type TText } from 'platejs';
3+
import type { PlateEditor } from 'platejs/react';
4+
5+
export const selectPreviousTextNode = (editor: PlateEditor, currentPath: Path): boolean => {
6+
const previous = editor.api.previous<TText>({
7+
at: currentPath,
8+
mode: 'lowest',
9+
text: true,
10+
match: (n, p) => TextApi.isText(n) && isEditableTextNode(editor, p),
11+
});
12+
13+
if (previous === undefined) {
14+
return false;
15+
}
16+
17+
const [, previousPath] = previous;
18+
19+
const previousEndPoint = editor.api.end(previousPath);
20+
21+
if (previousEndPoint === undefined) {
22+
return false;
23+
}
24+
25+
editor.tf.select(previousEndPoint);
26+
editor.tf.focus();
27+
28+
return true;
29+
};

0 commit comments

Comments
 (0)