mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-02-23 22:19:56 +08:00
Includes unwrap and toggle open actions.
This commit is contained in:
parent
3f86937f74
commit
5887322178
1
resources/icons/editor/details-toggle.svg
Normal file
1
resources/icons/editor/details-toggle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewbox="0 0 24 24"><path d="M8.12 19.3c.39.39 1.02.39 1.41 0L12 16.83l2.47 2.47c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-3.17-3.17c-.39-.39-1.02-.39-1.41 0l-3.17 3.17c-.4.38-.4 1.02-.01 1.41zm7.76-14.6c-.39-.39-1.02-.39-1.41 0L12 7.17 9.53 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.03 0 1.42l3.17 3.17c.39.39 1.02.39 1.41 0l3.17-3.17c.4-.39.4-1.03.01-1.42z"/></svg>
|
After Width: | Height: | Size: 377 B |
@ -18,6 +18,7 @@ export type SerializedDetailsNode = Spread<{
|
||||
export class DetailsNode extends ElementNode {
|
||||
__id: string = '';
|
||||
__summary: string = '';
|
||||
__open: boolean = false;
|
||||
|
||||
static getType() {
|
||||
return 'details';
|
||||
@ -43,11 +44,22 @@ export class DetailsNode extends ElementNode {
|
||||
return self.__summary;
|
||||
}
|
||||
|
||||
setOpen(open: boolean) {
|
||||
const self = this.getWritable();
|
||||
self.__open = open;
|
||||
}
|
||||
|
||||
getOpen(): boolean {
|
||||
const self = this.getLatest();
|
||||
return self.__open;
|
||||
}
|
||||
|
||||
static clone(node: DetailsNode): DetailsNode {
|
||||
const newNode = new DetailsNode(node.__key);
|
||||
newNode.__id = node.__id;
|
||||
newNode.__dir = node.__dir;
|
||||
newNode.__summary = node.__summary;
|
||||
newNode.__open = node.__open;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
@ -61,17 +73,34 @@ export class DetailsNode extends ElementNode {
|
||||
el.setAttribute('dir', this.__dir);
|
||||
}
|
||||
|
||||
if (this.__open) {
|
||||
el.setAttribute('open', 'true');
|
||||
}
|
||||
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = this.__summary;
|
||||
summary.setAttribute('contenteditable', 'false');
|
||||
summary.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
_editor.update(() => {
|
||||
this.select();
|
||||
})
|
||||
});
|
||||
|
||||
el.append(summary);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
|
||||
|
||||
if (prevNode.__open !== this.__open) {
|
||||
dom.toggleAttribute('open', this.__open);
|
||||
}
|
||||
|
||||
return prevNode.__id !== this.__id
|
||||
|| prevNode.__dir !== this.__dir;
|
||||
|| prevNode.__dir !== this.__dir
|
||||
|| prevNode.__summary !== this.__summary;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap|null {
|
||||
@ -114,6 +143,8 @@ export class DetailsNode extends ElementNode {
|
||||
elem.removeAttribute('contenteditable');
|
||||
}
|
||||
|
||||
element.removeAttribute('open');
|
||||
|
||||
return {element};
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,9 @@ import editIcon from "@icons/edit.svg";
|
||||
import diagramIcon from "@icons/editor/diagram.svg";
|
||||
import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
|
||||
import detailsIcon from "@icons/editor/details.svg";
|
||||
import detailsToggleIcon from "@icons/editor/details-toggle.svg";
|
||||
import tableDeleteIcon from "@icons/editor/table-delete.svg";
|
||||
import tagIcon from "@icons/tag.svg";
|
||||
import mediaIcon from "@icons/editor/media.svg";
|
||||
import {$createDetailsNode, $isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
|
||||
import {$isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
||||
@ -29,7 +32,7 @@ import {
|
||||
} from "../../../utils/selection";
|
||||
import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
|
||||
import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
|
||||
import {$showImageForm, $showLinkForm} from "../forms/objects";
|
||||
import {$showDetailsForm, $showImageForm, $showLinkForm} from "../forms/objects";
|
||||
import {formatCodeBlock} from "../../../utils/formats";
|
||||
|
||||
export const link: EditorButtonDefinition = {
|
||||
@ -216,4 +219,58 @@ export const details: EditorButtonDefinition = {
|
||||
isActive(selection: BaseSelection | null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isDetailsNode);
|
||||
}
|
||||
}
|
||||
|
||||
export const detailsEditLabel: EditorButtonDefinition = {
|
||||
label: 'Edit label',
|
||||
icon: tagIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
|
||||
if ($isDetailsNode(details)) {
|
||||
$showDetailsForm(details, context);
|
||||
}
|
||||
})
|
||||
},
|
||||
isActive(selection: BaseSelection | null): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const detailsToggle: EditorButtonDefinition = {
|
||||
label: 'Toggle open/closed',
|
||||
icon: detailsToggleIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
|
||||
if ($isDetailsNode(details)) {
|
||||
details.setOpen(!details.getOpen());
|
||||
context.manager.triggerLayoutUpdate();
|
||||
}
|
||||
})
|
||||
},
|
||||
isActive(selection: BaseSelection | null): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const detailsUnwrap: EditorButtonDefinition = {
|
||||
label: 'Unwrap',
|
||||
icon: tableDeleteIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
|
||||
if ($isDetailsNode(details)) {
|
||||
const children = details.getChildren();
|
||||
for (const child of children) {
|
||||
details.insertBefore(child);
|
||||
}
|
||||
details.remove();
|
||||
context.manager.triggerLayoutUpdate();
|
||||
}
|
||||
})
|
||||
},
|
||||
isActive(selection: BaseSelection | null): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ import searchIcon from "@icons/search.svg";
|
||||
import {showLinkSelector} from "../../../utils/links";
|
||||
import {LinkField} from "../../framework/blocks/link-field";
|
||||
import {insertOrUpdateLink} from "../../../utils/formats";
|
||||
import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
|
||||
|
||||
export function $showImageForm(image: ImageNode, context: EditorUiContext) {
|
||||
const imageModal: EditorFormModal = context.manager.createModal('image');
|
||||
@ -262,4 +263,37 @@ export const media: EditorFormDefinition = {
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function $showDetailsForm(details: DetailsNode|null, context: EditorUiContext) {
|
||||
const linkModal = context.manager.createModal('details');
|
||||
if (!details) {
|
||||
return;
|
||||
}
|
||||
|
||||
linkModal.show({
|
||||
summary: details.getSummary()
|
||||
});
|
||||
}
|
||||
|
||||
export const details: EditorFormDefinition = {
|
||||
submitText: 'Save',
|
||||
async action(formData, context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const node = $getNodeFromSelection($getSelection(), $isDetailsNode);
|
||||
const summary = (formData.get('summary') || '').toString().trim();
|
||||
if ($isDetailsNode(node)) {
|
||||
node.setSummary(summary);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
label: 'Toggle label',
|
||||
name: 'summary',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import {EditorFormModalDefinition} from "../framework/modals";
|
||||
import {image, link, media} from "./forms/objects";
|
||||
import {details, image, link, media} from "./forms/objects";
|
||||
import {source} from "./forms/controls";
|
||||
import {cellProperties, rowProperties, tableProperties} from "./forms/tables";
|
||||
|
||||
@ -32,4 +32,8 @@ export const modals: Record<string, EditorFormModalDefinition> = {
|
||||
title: 'Table Properties',
|
||||
form: tableProperties,
|
||||
},
|
||||
details: {
|
||||
title: 'Edit collapsible block',
|
||||
form: details,
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import {LexicalEditor} from "lexical";
|
||||
import {
|
||||
getCodeToolbarContent,
|
||||
getCodeToolbarContent, getDetailsToolbarContent,
|
||||
getImageToolbarContent,
|
||||
getLinkToolbarContent,
|
||||
getMainEditorFullToolbar, getTableToolbarContent
|
||||
@ -56,7 +56,6 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
|
||||
selector: '.editor-code-block-wrap',
|
||||
content: getCodeToolbarContent(),
|
||||
});
|
||||
|
||||
manager.registerContextToolbar('table', {
|
||||
selector: 'td,th',
|
||||
content: getTableToolbarContent(),
|
||||
@ -64,6 +63,10 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
|
||||
return originalTarget.closest('table') as HTMLTableElement;
|
||||
}
|
||||
});
|
||||
manager.registerContextToolbar('details', {
|
||||
selector: 'details',
|
||||
content: getDetailsToolbarContent(),
|
||||
});
|
||||
|
||||
// Register image decorator listener
|
||||
manager.registerDecoratorType('code', CodeBlockDecorator);
|
||||
|
@ -68,7 +68,7 @@ import {
|
||||
} from "./defaults/buttons/lists";
|
||||
import {
|
||||
codeBlock,
|
||||
details,
|
||||
details, detailsEditLabel, detailsToggle, detailsUnwrap,
|
||||
diagram, diagramManager,
|
||||
editCodeBlock,
|
||||
horizontalRule,
|
||||
@ -253,4 +253,12 @@ export function getTableToolbarContent(): EditorUiElement[] {
|
||||
new EditorButton(deleteColumn),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
export function getDetailsToolbarContent(): EditorUiElement[] {
|
||||
return [
|
||||
new EditorButton(detailsEditLabel),
|
||||
new EditorButton(detailsToggle),
|
||||
new EditorButton(detailsUnwrap),
|
||||
];
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user