Lexical: Added tracked container, added fullscreen action

Changed how the editor is loaded in, so it now creates its own DOM, and
content is passed via creation function, to be better self-contained.
This commit is contained in:
Dan Brown 2024-07-01 10:44:23 +01:00
parent b1c489090e
commit c2ecbf071f
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
11 changed files with 108 additions and 74 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-200h80v120h120v80H120Zm520 0v-80h120v-120h80v200H640ZM120-640v-200h200v80H200v120h-80Zm640 0v-120H640v-80h200v200h-80Z"/></svg>

After

Width:  |  Height:  |  Size: 211 B

View File

@ -4,10 +4,12 @@ export class WysiwygEditor extends Component {
setup() { setup() {
this.elem = this.$el; this.elem = this.$el;
this.editArea = this.$refs.editArea; this.editContainer = this.$refs.editContainer;
this.editContent = this.$refs.editContent;
window.importVersioned('wysiwyg').then(wysiwyg => { window.importVersioned('wysiwyg').then(wysiwyg => {
wysiwyg.createPageEditorInstance(this.editArea); const editorContent = this.editContent.textContent;
wysiwyg.createPageEditorInstance(this.editContainer, editorContent);
}); });
} }

View File

@ -6,8 +6,9 @@ import {getNodesForPageEditor} from './nodes';
import {buildEditorUI} from "./ui"; import {buildEditorUI} from "./ui";
import {setEditorContentFromHtml} from "./actions"; import {setEditorContentFromHtml} from "./actions";
import {registerTableResizer} from "./ui/framework/helpers/table-resizer"; import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
import {el} from "./helpers";
export function createPageEditorInstance(editArea: HTMLElement) { export function createPageEditorInstance(container: HTMLElement, htmlContent: string) {
const config: CreateEditorArgs = { const config: CreateEditorArgs = {
namespace: 'BookStackPageEditor', namespace: 'BookStackPageEditor',
nodes: getNodesForPageEditor(), nodes: getNodesForPageEditor(),
@ -26,7 +27,11 @@ export function createPageEditorInstance(editArea: HTMLElement) {
} }
}; };
const startingHtml = editArea.innerHTML; const editArea = el('div', {
contenteditable: 'true',
});
container.append(editArea);
container.classList.add('editor-container');
const editor = createEditor(config); const editor = createEditor(config);
editor.setRootElement(editArea); editor.setRootElement(editArea);
@ -37,7 +42,7 @@ export function createPageEditorInstance(editArea: HTMLElement) {
registerTableResizer(editor, editArea), registerTableResizer(editor, editArea),
); );
setEditorContentFromHtml(editor, startingHtml); setEditorContentFromHtml(editor, htmlContent);
const debugView = document.getElementById('lexical-debug'); const debugView = document.getElementById('lexical-debug');
editor.registerUpdateListener(({editorState}) => { editor.registerUpdateListener(({editorState}) => {
@ -47,24 +52,5 @@ export function createPageEditorInstance(editArea: HTMLElement) {
} }
}); });
buildEditorUI(editArea, editor); buildEditorUI(container, editArea, editor);
// Example of creating, registering and using a custom command
// const SET_BLOCK_CALLOUT_COMMAND = createCommand();
// editor.registerCommand(SET_BLOCK_CALLOUT_COMMAND, (category: CalloutCategory = 'info') => {
// const selection = $getSelection();
// const blockElement = $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]);
// if ($isCalloutNode(blockElement)) {
// $setBlocksType(selection, $createParagraphNode);
// } else {
// $setBlocksType(selection, () => $createCalloutNode(category));
// }
// return true;
// }, COMMAND_PRIORITY_LOW);
//
// const button = document.getElementById('lexical-button');
// button.addEventListener('click', event => {
// editor.dispatchCommand(SET_BLOCK_CALLOUT_COMMAND, 'info');
// });
} }

View File

@ -51,6 +51,7 @@ import imageIcon from "@icons/editor/image.svg"
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg" import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"
import detailsIcon from "@icons/editor/details.svg" import detailsIcon from "@icons/editor/details.svg"
import sourceIcon from "@icons/editor/source-view.svg" import sourceIcon from "@icons/editor/source-view.svg"
import fullscreenIcon from "@icons/editor/fullscreen.svg"
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule"; import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
export const undo: EditorButtonDefinition = { export const undo: EditorButtonDefinition = {
@ -206,7 +207,7 @@ function buildListButton(label: string, type: ListType, icon: string): EditorBut
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
const selection = $getSelection(); const selection = $getSelection();
if (this.isActive(selection)) { if (this.isActive(selection, context)) {
removeList(context.editor); removeList(context.editor);
} else { } else {
insertList(context.editor, type); insertList(context.editor, type);
@ -375,3 +376,17 @@ export const source: EditorButtonDefinition = {
return false; return false;
} }
}; };
export const fullscreen: EditorButtonDefinition = {
label: 'Fullscreen',
icon: fullscreenIcon,
async action(context: EditorUiContext, button: EditorButton) {
const isFullScreen = context.containerDOM.classList.contains('fullscreen');
context.containerDOM.classList.toggle('fullscreen', !isFullScreen);
(context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen);
button.setActiveState(!isFullScreen);
},
isActive(selection, context: EditorUiContext) {
return context.containerDOM.classList.contains('fullscreen');
}
};

View File

@ -8,8 +8,8 @@ export interface EditorBasicButtonDefinition {
} }
export interface EditorButtonDefinition extends EditorBasicButtonDefinition { export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
action: (context: EditorUiContext) => void; action: (context: EditorUiContext, button: EditorButton) => void;
isActive: (selection: BaseSelection|null) => boolean; isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
setup?: (context: EditorUiContext, button: EditorButton) => void; setup?: (context: EditorUiContext, button: EditorButton) => void;
} }
@ -68,11 +68,16 @@ export class EditorButton extends EditorUiElement {
} }
protected onClick() { protected onClick() {
this.definition.action(this.getContext()); this.definition.action(this.getContext(), this);
} }
updateActiveState(selection: BaseSelection|null) { updateActiveState(selection: BaseSelection|null) {
this.active = this.definition.isActive(selection); const isActive = this.definition.isActive(selection, this.getContext());
this.setActiveState(isActive);
}
setActiveState(active: boolean) {
this.active = active;
this.dom?.classList.toggle('editor-button-active', this.active); this.dom?.classList.toggle('editor-button-active', this.active);
} }

View File

@ -10,6 +10,7 @@ export type EditorUiStateUpdate = {
export type EditorUiContext = { export type EditorUiContext = {
editor: LexicalEditor, editor: LexicalEditor,
editorDOM: HTMLElement, editorDOM: HTMLElement,
containerDOM: HTMLElement,
translate: (text: string) => string, translate: (text: string) => string,
manager: EditorUIManager, manager: EditorUIManager,
lastSelection: BaseSelection|null, lastSelection: BaseSelection|null,

View File

@ -79,7 +79,7 @@ export class EditorUIManager {
this.toolbar = toolbar; this.toolbar = toolbar;
toolbar.setContext(this.getContext()); toolbar.setContext(this.getContext());
this.getContext().editorDOM.before(toolbar.getDOMElement()); this.getContext().containerDOM.prepend(toolbar.getDOMElement());
} }
registerContextToolbar(key: string, definition: EditorContextToolbarDefinition) { registerContextToolbar(key: string, definition: EditorContextToolbarDefinition) {
@ -97,6 +97,13 @@ export class EditorUIManager {
// console.log('selection update', update.selection); // console.log('selection update', update.selection);
} }
triggerStateRefresh(): void {
this.triggerStateUpdate({
editor: this.getContext().editor,
selection: this.getContext().lastSelection,
});
}
protected updateContextToolbars(update: EditorUiStateUpdate): void { protected updateContextToolbars(update: EditorUiStateUpdate): void {
for (const toolbar of this.activeContextToolbars) { for (const toolbar of this.activeContextToolbars) {
toolbar.empty(); toolbar.empty();
@ -133,7 +140,7 @@ export class EditorUIManager {
toolbar.setContext(this.getContext()); toolbar.setContext(this.getContext());
this.activeContextToolbars.push(toolbar); this.activeContextToolbars.push(toolbar);
this.getContext().editorDOM.after(toolbar.getDOMElement()); this.getContext().containerDOM.append(toolbar.getDOMElement());
toolbar.attachTo(target); toolbar.attachTo(target);
} }
} }

View File

@ -5,10 +5,11 @@ import {image as imageFormDefinition, link as linkFormDefinition, source as sour
import {ImageDecorator} from "./decorators/image"; import {ImageDecorator} from "./decorators/image";
import {EditorUiContext} from "./framework/core"; import {EditorUiContext} from "./framework/core";
export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) { export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor) {
const manager = new EditorUIManager(); const manager = new EditorUIManager();
const context: EditorUiContext = { const context: EditorUiContext = {
editor, editor,
containerDOM: container,
editorDOM: element, editorDOM: element,
manager, manager,
translate: (text: string): string => text, translate: (text: string): string => text,

View File

@ -1,7 +1,7 @@
import {EditorButton} from "./framework/buttons"; import {EditorButton} from "./framework/buttons";
import { import {
blockquote, bold, bulletList, clearFormating, code, blockquote, bold, bulletList, clearFormating, code,
dangerCallout, details, dangerCallout, details, fullscreen,
h2, h3, h4, h5, highlightColor, horizontalRule, image, h2, h3, h4, h5, highlightColor, horizontalRule, image,
infoCallout, italic, link, numberList, paragraph, infoCallout, italic, link, numberList, paragraph,
redo, source, strikethrough, subscript, redo, source, strikethrough, subscript,
@ -73,6 +73,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
// Meta elements // Meta elements
new EditorButton(source), new EditorButton(source),
new EditorButton(fullscreen),
// Test // Test
new EditorButton({ new EditorButton({

View File

@ -4,11 +4,25 @@
} }
// Main UI elements // Main UI elements
.editor-container {
background-color: #FFF;
position: relative;
&.fullscreen {
z-index: 500;
}
}
.editor-toolbar-main { .editor-toolbar-main {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
body.editor-is-fullscreen {
overflow: hidden;
.edit-area {
z-index: 20;
}
}
// Buttons // Buttons
.editor-button { .editor-button {
border: 1px solid #DDD; border: 1px solid #DDD;

View File

@ -6,8 +6,10 @@
option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}" option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
class=""> class="">
<div class="editor-container"> <div class="editor-container" refs="wysiwyg-editor@edit-container">
<div refs="wysiwyg-editor@edit-area" contenteditable="true"> </div>
<script type="text/html" refs="wysiwyg-editor@edit-content">
<p id="Content!">Some <strong>content</strong> here</p> <p id="Content!">Some <strong>content</strong> here</p>
<p>Content with image in, before text. <img src="https://bookstack.local/bookstack/uploads/images/gallery/2022-03/scaled-1680-/cats-image-2400x1000-2.jpg" width="200" alt="Sleepy meow"> After text.</p> <p>Content with image in, before text. <img src="https://bookstack.local/bookstack/uploads/images/gallery/2022-03/scaled-1680-/cats-image-2400x1000-2.jpg" width="200" alt="Sleepy meow"> After text.</p>
<p>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p> <p>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p>
@ -45,8 +47,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </script>
</div>
<div id="lexical-debug" style="white-space: pre-wrap; font-size: 12px; height: 200px; overflow-y: scroll; background-color: #000; padding: 1rem; border-radius: 4px; color: #FFF;"></div> <div id="lexical-debug" style="white-space: pre-wrap; font-size: 12px; height: 200px; overflow-y: scroll; background-color: #000; padding: 1rem; border-radius: 4px; color: #FFF;"></div>