Skip to content

Commit b24d60e

Browse files
committed
Lexical: Started UI fundementals with basic button
1 parent 0f8bd86 commit b24d60e

8 files changed

Lines changed: 288 additions & 32 deletions

File tree

resources/js/wysiwyg/helpers.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {$createParagraphNode, $getSelection, BaseSelection, LexicalEditor} from "lexical";
2+
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
3+
import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
4+
import {$setBlocksType} from "@lexical/selection";
5+
6+
export function selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean {
7+
if (!selection) {
8+
return false;
9+
}
10+
11+
for (const node of selection.getNodes()) {
12+
if (matcher(node)) {
13+
return true;
14+
}
15+
16+
for (const parent of node.getParents()) {
17+
if (matcher(parent)) {
18+
return true;
19+
}
20+
}
21+
}
22+
23+
return false;
24+
}
25+
26+
export function toggleSelectionBlockNodeType(editor: LexicalEditor, matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {
27+
editor.update(() => {
28+
const selection = $getSelection();
29+
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
30+
if (selection && matcher(blockElement)) {
31+
$setBlocksType(selection, $createParagraphNode);
32+
} else {
33+
$setBlocksType(selection, creator);
34+
}
35+
});
36+
}

resources/js/wysiwyg/index.ts

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
1-
import {
2-
$createParagraphNode,
3-
$getRoot,
4-
$getSelection,
5-
COMMAND_PRIORITY_LOW,
6-
createCommand,
7-
createEditor, CreateEditorArgs,
8-
} from 'lexical';
1+
import {$getRoot, createEditor, CreateEditorArgs} from 'lexical';
92
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
103
import {registerRichText} from '@lexical/rich-text';
11-
import {$getNearestBlockElementAncestorOrThrow, mergeRegister} from '@lexical/utils';
4+
import {mergeRegister} from '@lexical/utils';
125
import {$generateNodesFromDOM} from '@lexical/html';
13-
import {$setBlocksType} from '@lexical/selection';
146
import {getNodesForPageEditor} from './nodes';
15-
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from './nodes/callout';
7+
import {buildEditorUI} from "./ui";
168

179
export function createPageEditorInstance(editArea: HTMLElement) {
1810
const config: CreateEditorArgs = {
@@ -42,25 +34,29 @@ export function createPageEditorInstance(editArea: HTMLElement) {
4234
const debugView = document.getElementById('lexical-debug');
4335
editor.registerUpdateListener(({editorState}) => {
4436
console.log('editorState', editorState.toJSON());
45-
debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
37+
if (debugView) {
38+
debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
39+
}
4640
});
4741

48-
// Example of creating, registering and using a custom command
42+
buildEditorUI(editArea, editor);
4943

50-
const SET_BLOCK_CALLOUT_COMMAND = createCommand();
51-
editor.registerCommand(SET_BLOCK_CALLOUT_COMMAND, (category: CalloutCategory = 'info') => {
52-
const selection = $getSelection();
53-
const blockElement = $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]);
54-
if ($isCalloutNode(blockElement)) {
55-
$setBlocksType(selection, $createParagraphNode);
56-
} else {
57-
$setBlocksType(selection, () => $createCalloutNode(category));
58-
}
59-
return true;
60-
}, COMMAND_PRIORITY_LOW);
44+
// Example of creating, registering and using a custom command
6145

62-
const button = document.getElementById('lexical-button');
63-
button.addEventListener('click', event => {
64-
editor.dispatchCommand(SET_BLOCK_CALLOUT_COMMAND, 'info');
65-
});
46+
// const SET_BLOCK_CALLOUT_COMMAND = createCommand();
47+
// editor.registerCommand(SET_BLOCK_CALLOUT_COMMAND, (category: CalloutCategory = 'info') => {
48+
// const selection = $getSelection();
49+
// const blockElement = $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]);
50+
// if ($isCalloutNode(blockElement)) {
51+
// $setBlocksType(selection, $createParagraphNode);
52+
// } else {
53+
// $setBlocksType(selection, () => $createCalloutNode(category));
54+
// }
55+
// return true;
56+
// }, COMMAND_PRIORITY_LOW);
57+
//
58+
// const button = document.getElementById('lexical-button');
59+
// button.addEventListener('click', event => {
60+
// editor.dispatchCommand(SET_BLOCK_CALLOUT_COMMAND, 'info');
61+
// });
6662
}

resources/js/wysiwyg/nodes/callout.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ export class CalloutNode extends ElementNode {
3333
this.__category = category;
3434
}
3535

36+
setCategory(category: CalloutCategory) {
37+
const self = this.getWritable();
38+
self.__category = category;
39+
}
40+
41+
getCategory(): CalloutCategory {
42+
const self = this.getLatest();
43+
return self.__category;
44+
}
45+
3646
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
3747
const element = document.createElement('p');
3848
element.classList.add('callout', this.__category || '');
@@ -112,3 +122,7 @@ export function $createCalloutNode(category: CalloutCategory = 'info') {
112122
export function $isCalloutNode(node: LexicalNode | null | undefined) {
113123
return node instanceof CalloutNode;
114124
}
125+
126+
export function $isCalloutNodeOfCategory(node: LexicalNode | null | undefined, category: CalloutCategory = 'info') {
127+
return node instanceof CalloutNode && (node as CalloutNode).getCategory() === category;
128+
}

resources/js/wysiwyg/nodes/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
22
import {CalloutNode} from './callout';
3-
import {KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical";
3+
import {ElementNode, KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical";
44
import {CustomParagraphNode} from "./custom-paragraph";
55

66
/**
@@ -20,3 +20,6 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
2020
}
2121
];
2222
}
23+
24+
export type LexicalNodeMatcher = (node: LexicalNode|null|undefined) => boolean;
25+
export type LexicalElementNodeCreator = () => ElementNode;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {BaseSelection, LexicalEditor} from "lexical";
2+
3+
export interface EditorButtonDefinition {
4+
label: string;
5+
action: (editor: LexicalEditor) => void;
6+
isActive: (selection: BaseSelection|null) => boolean;
7+
}
8+
9+
export class EditorButton {
10+
#definition: EditorButtonDefinition;
11+
#editor: LexicalEditor;
12+
#dom: HTMLButtonElement;
13+
14+
constructor(definition: EditorButtonDefinition, editor: LexicalEditor) {
15+
this.#definition = definition;
16+
this.#editor = editor;
17+
this.#dom = this.buildDOM();
18+
}
19+
20+
private buildDOM(): HTMLButtonElement {
21+
const button = document.createElement("button");
22+
button.setAttribute('type', 'button');
23+
button.textContent = this.#definition.label;
24+
button.classList.add('editor-toolbar-button');
25+
26+
button.addEventListener('click', event => {
27+
this.runAction();
28+
});
29+
30+
return button;
31+
}
32+
33+
getDOMElement(): HTMLButtonElement {
34+
return this.#dom;
35+
}
36+
37+
runAction() {
38+
this.#definition.action(this.#editor);
39+
}
40+
41+
updateActiveState(selection: BaseSelection|null) {
42+
const isActive = this.#definition.isActive(selection);
43+
this.#dom.classList.toggle('editor-toolbar-button-active', isActive);
44+
}
45+
}

resources/js/wysiwyg/ui/index.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
$getSelection,
3+
BaseSelection,
4+
COMMAND_PRIORITY_LOW,
5+
LexicalEditor,
6+
SELECTION_CHANGE_COMMAND
7+
} from "lexical";
8+
import {$createCalloutNode, $isCalloutNodeOfCategory} from "../nodes/callout";
9+
import {selectionContainsNodeType, toggleSelectionBlockNodeType} from "../helpers";
10+
import {EditorButton, EditorButtonDefinition} from "./editor-button";
11+
12+
const calloutButton: EditorButtonDefinition = {
13+
label: 'Info Callout',
14+
action(editor: LexicalEditor) {
15+
toggleSelectionBlockNodeType(
16+
editor,
17+
(node) => $isCalloutNodeOfCategory(node, 'info'),
18+
() => $createCalloutNode('info'),
19+
)
20+
},
21+
isActive(selection: BaseSelection|null): boolean {
22+
return selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, 'info'));
23+
}
24+
}
25+
26+
const toolbarButtonDefinitions: EditorButtonDefinition[] = [
27+
calloutButton,
28+
];
29+
30+
export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
31+
const toolbarContainer = document.createElement('div');
32+
toolbarContainer.classList.add('editor-toolbar-container');
33+
34+
const buttons = toolbarButtonDefinitions.map(definition => {
35+
return new EditorButton(definition, editor);
36+
});
37+
38+
const buttonElements = buttons.map(button => button.getDOMElement());
39+
40+
toolbarContainer.append(...buttonElements);
41+
element.before(toolbarContainer);
42+
43+
// Update button states on editor selection change
44+
editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
45+
const selection = $getSelection();
46+
for (const button of buttons) {
47+
button.updateActiveState(selection);
48+
}
49+
return false;
50+
}, COMMAND_PRIORITY_LOW);
51+
}

resources/views/pages/parts/wysiwyg-editor.blade.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
77
class="">
88

9-
<div>
10-
<button type="button" id="lexical-button">Callout</button>
11-
</div>
9+
<style>
10+
.editor-toolbar-button-active {
11+
background-color: tomato;
12+
}
13+
</style>
1214

1315
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
1416
<p id="Content!">Some <strong>content</strong> here</p>

0 commit comments

Comments
 (0)