Skip to content

Commit 5887322

Browse files
committed
Lexical: Added details toolbar
Includes unwrap and toggle open actions.
1 parent 3f86937 commit 5887322

File tree

7 files changed

+144
-6
lines changed

7 files changed

+144
-6
lines changed
Lines changed: 1 addition & 0 deletions
Loading

resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type SerializedDetailsNode = Spread<{
1818
export class DetailsNode extends ElementNode {
1919
__id: string = '';
2020
__summary: string = '';
21+
__open: boolean = false;
2122

2223
static getType() {
2324
return 'details';
@@ -43,11 +44,22 @@ export class DetailsNode extends ElementNode {
4344
return self.__summary;
4445
}
4546

47+
setOpen(open: boolean) {
48+
const self = this.getWritable();
49+
self.__open = open;
50+
}
51+
52+
getOpen(): boolean {
53+
const self = this.getLatest();
54+
return self.__open;
55+
}
56+
4657
static clone(node: DetailsNode): DetailsNode {
4758
const newNode = new DetailsNode(node.__key);
4859
newNode.__id = node.__id;
4960
newNode.__dir = node.__dir;
5061
newNode.__summary = node.__summary;
62+
newNode.__open = node.__open;
5163
return newNode;
5264
}
5365

@@ -61,17 +73,34 @@ export class DetailsNode extends ElementNode {
6173
el.setAttribute('dir', this.__dir);
6274
}
6375

76+
if (this.__open) {
77+
el.setAttribute('open', 'true');
78+
}
79+
6480
const summary = document.createElement('summary');
6581
summary.textContent = this.__summary;
6682
summary.setAttribute('contenteditable', 'false');
83+
summary.addEventListener('click', event => {
84+
event.preventDefault();
85+
_editor.update(() => {
86+
this.select();
87+
})
88+
});
89+
6790
el.append(summary);
6891

6992
return el;
7093
}
7194

7295
updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
96+
97+
if (prevNode.__open !== this.__open) {
98+
dom.toggleAttribute('open', this.__open);
99+
}
100+
73101
return prevNode.__id !== this.__id
74-
|| prevNode.__dir !== this.__dir;
102+
|| prevNode.__dir !== this.__dir
103+
|| prevNode.__summary !== this.__summary;
75104
}
76105

77106
static importDOM(): DOMConversionMap|null {
@@ -114,6 +143,8 @@ export class DetailsNode extends ElementNode {
114143
elem.removeAttribute('contenteditable');
115144
}
116145

146+
element.removeAttribute('open');
147+
117148
return {element};
118149
}
119150

resources/js/wysiwyg/ui/defaults/buttons/objects.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import editIcon from "@icons/edit.svg";
1919
import diagramIcon from "@icons/editor/diagram.svg";
2020
import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
2121
import detailsIcon from "@icons/editor/details.svg";
22+
import detailsToggleIcon from "@icons/editor/details-toggle.svg";
23+
import tableDeleteIcon from "@icons/editor/table-delete.svg";
24+
import tagIcon from "@icons/tag.svg";
2225
import mediaIcon from "@icons/editor/media.svg";
2326
import {$createDetailsNode, $isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
2427
import {$isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
@@ -29,7 +32,7 @@ import {
2932
} from "../../../utils/selection";
3033
import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
3134
import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
32-
import {$showImageForm, $showLinkForm} from "../forms/objects";
35+
import {$showDetailsForm, $showImageForm, $showLinkForm} from "../forms/objects";
3336
import {formatCodeBlock} from "../../../utils/formats";
3437

3538
export const link: EditorButtonDefinition = {
@@ -216,4 +219,58 @@ export const details: EditorButtonDefinition = {
216219
isActive(selection: BaseSelection | null): boolean {
217220
return $selectionContainsNodeType(selection, $isDetailsNode);
218221
}
222+
}
223+
224+
export const detailsEditLabel: EditorButtonDefinition = {
225+
label: 'Edit label',
226+
icon: tagIcon,
227+
action(context: EditorUiContext) {
228+
context.editor.getEditorState().read(() => {
229+
const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
230+
if ($isDetailsNode(details)) {
231+
$showDetailsForm(details, context);
232+
}
233+
})
234+
},
235+
isActive(selection: BaseSelection | null): boolean {
236+
return false;
237+
}
238+
}
239+
240+
export const detailsToggle: EditorButtonDefinition = {
241+
label: 'Toggle open/closed',
242+
icon: detailsToggleIcon,
243+
action(context: EditorUiContext) {
244+
context.editor.update(() => {
245+
const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
246+
if ($isDetailsNode(details)) {
247+
details.setOpen(!details.getOpen());
248+
context.manager.triggerLayoutUpdate();
249+
}
250+
})
251+
},
252+
isActive(selection: BaseSelection | null): boolean {
253+
return false;
254+
}
255+
}
256+
257+
export const detailsUnwrap: EditorButtonDefinition = {
258+
label: 'Unwrap',
259+
icon: tableDeleteIcon,
260+
action(context: EditorUiContext) {
261+
context.editor.update(() => {
262+
const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
263+
if ($isDetailsNode(details)) {
264+
const children = details.getChildren();
265+
for (const child of children) {
266+
details.insertBefore(child);
267+
}
268+
details.remove();
269+
context.manager.triggerLayoutUpdate();
270+
}
271+
})
272+
},
273+
isActive(selection: BaseSelection | null): boolean {
274+
return false;
275+
}
219276
}

resources/js/wysiwyg/ui/defaults/forms/objects.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import searchIcon from "@icons/search.svg";
1919
import {showLinkSelector} from "../../../utils/links";
2020
import {LinkField} from "../../framework/blocks/link-field";
2121
import {insertOrUpdateLink} from "../../../utils/formats";
22+
import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
2223

2324
export function $showImageForm(image: ImageNode, context: EditorUiContext) {
2425
const imageModal: EditorFormModal = context.manager.createModal('image');
@@ -262,4 +263,37 @@ export const media: EditorFormDefinition = {
262263
}
263264
},
264265
],
266+
};
267+
268+
export function $showDetailsForm(details: DetailsNode|null, context: EditorUiContext) {
269+
const linkModal = context.manager.createModal('details');
270+
if (!details) {
271+
return;
272+
}
273+
274+
linkModal.show({
275+
summary: details.getSummary()
276+
});
277+
}
278+
279+
export const details: EditorFormDefinition = {
280+
submitText: 'Save',
281+
async action(formData, context: EditorUiContext) {
282+
context.editor.update(() => {
283+
const node = $getNodeFromSelection($getSelection(), $isDetailsNode);
284+
const summary = (formData.get('summary') || '').toString().trim();
285+
if ($isDetailsNode(node)) {
286+
node.setSummary(summary);
287+
}
288+
});
289+
290+
return true;
291+
},
292+
fields: [
293+
{
294+
label: 'Toggle label',
295+
name: 'summary',
296+
type: 'text',
297+
},
298+
],
265299
};

resources/js/wysiwyg/ui/defaults/modals.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {EditorFormModalDefinition} from "../framework/modals";
2-
import {image, link, media} from "./forms/objects";
2+
import {details, image, link, media} from "./forms/objects";
33
import {source} from "./forms/controls";
44
import {cellProperties, rowProperties, tableProperties} from "./forms/tables";
55

@@ -32,4 +32,8 @@ export const modals: Record<string, EditorFormModalDefinition> = {
3232
title: 'Table Properties',
3333
form: tableProperties,
3434
},
35+
details: {
36+
title: 'Edit collapsible block',
37+
form: details,
38+
}
3539
};

resources/js/wysiwyg/ui/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {LexicalEditor} from "lexical";
22
import {
3-
getCodeToolbarContent,
3+
getCodeToolbarContent, getDetailsToolbarContent,
44
getImageToolbarContent,
55
getLinkToolbarContent,
66
getMainEditorFullToolbar, getTableToolbarContent
@@ -56,14 +56,17 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
5656
selector: '.editor-code-block-wrap',
5757
content: getCodeToolbarContent(),
5858
});
59-
6059
manager.registerContextToolbar('table', {
6160
selector: 'td,th',
6261
content: getTableToolbarContent(),
6362
displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
6463
return originalTarget.closest('table') as HTMLTableElement;
6564
}
6665
});
66+
manager.registerContextToolbar('details', {
67+
selector: 'details',
68+
content: getDetailsToolbarContent(),
69+
});
6770

6871
// Register image decorator listener
6972
manager.registerDecoratorType('code', CodeBlockDecorator);

resources/js/wysiwyg/ui/toolbars.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import {
6868
} from "./defaults/buttons/lists";
6969
import {
7070
codeBlock,
71-
details,
71+
details, detailsEditLabel, detailsToggle, detailsUnwrap,
7272
diagram, diagramManager,
7373
editCodeBlock,
7474
horizontalRule,
@@ -253,4 +253,12 @@ export function getTableToolbarContent(): EditorUiElement[] {
253253
new EditorButton(deleteColumn),
254254
]),
255255
];
256+
}
257+
258+
export function getDetailsToolbarContent(): EditorUiElement[] {
259+
return [
260+
new EditorButton(detailsEditLabel),
261+
new EditorButton(detailsToggle),
262+
new EditorButton(detailsUnwrap),
263+
];
256264
}

0 commit comments

Comments
 (0)