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() {
this.elem = this.$el;
this.editArea = this.$refs.editArea;
this.editContainer = this.$refs.editContainer;
this.editContent = this.$refs.editContent;
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 {setEditorContentFromHtml} from "./actions";
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 = {
namespace: 'BookStackPageEditor',
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);
editor.setRootElement(editArea);
@ -37,7 +42,7 @@ export function createPageEditorInstance(editArea: HTMLElement) {
registerTableResizer(editor, editArea),
);
setEditorContentFromHtml(editor, startingHtml);
setEditorContentFromHtml(editor, htmlContent);
const debugView = document.getElementById('lexical-debug');
editor.registerUpdateListener(({editorState}) => {
@ -47,24 +52,5 @@ export function createPageEditorInstance(editArea: HTMLElement) {
}
});
buildEditorUI(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');
// });
buildEditorUI(container, editArea, editor);
}

View File

@ -51,6 +51,7 @@ import imageIcon from "@icons/editor/image.svg"
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"
import detailsIcon from "@icons/editor/details.svg"
import sourceIcon from "@icons/editor/source-view.svg"
import fullscreenIcon from "@icons/editor/fullscreen.svg"
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
export const undo: EditorButtonDefinition = {
@ -206,7 +207,7 @@ function buildListButton(label: string, type: ListType, icon: string): EditorBut
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
const selection = $getSelection();
if (this.isActive(selection)) {
if (this.isActive(selection, context)) {
removeList(context.editor);
} else {
insertList(context.editor, type);
@ -374,4 +375,18 @@ export const source: EditorButtonDefinition = {
isActive() {
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 {
action: (context: EditorUiContext) => void;
isActive: (selection: BaseSelection|null) => boolean;
action: (context: EditorUiContext, button: EditorButton) => void;
isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
setup?: (context: EditorUiContext, button: EditorButton) => void;
}
@ -68,11 +68,16 @@ export class EditorButton extends EditorUiElement {
}
protected onClick() {
this.definition.action(this.getContext());
this.definition.action(this.getContext(), this);
}
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);
}

View File

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

View File

@ -79,7 +79,7 @@ export class EditorUIManager {
this.toolbar = toolbar;
toolbar.setContext(this.getContext());
this.getContext().editorDOM.before(toolbar.getDOMElement());
this.getContext().containerDOM.prepend(toolbar.getDOMElement());
}
registerContextToolbar(key: string, definition: EditorContextToolbarDefinition) {
@ -97,6 +97,13 @@ export class EditorUIManager {
// console.log('selection update', update.selection);
}
triggerStateRefresh(): void {
this.triggerStateUpdate({
editor: this.getContext().editor,
selection: this.getContext().lastSelection,
});
}
protected updateContextToolbars(update: EditorUiStateUpdate): void {
for (const toolbar of this.activeContextToolbars) {
toolbar.empty();
@ -133,7 +140,7 @@ export class EditorUIManager {
toolbar.setContext(this.getContext());
this.activeContextToolbars.push(toolbar);
this.getContext().editorDOM.after(toolbar.getDOMElement());
this.getContext().containerDOM.append(toolbar.getDOMElement());
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 {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 context: EditorUiContext = {
editor,
containerDOM: container,
editorDOM: element,
manager,
translate: (text: string): string => text,

View File

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

View File

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

View File

@ -6,48 +6,49 @@
option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
class="">
<div class="editor-container">
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
<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>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p>
<h2>List below this h2 header</h2>
<ul>
<li>Hello</li>
</ul>
<details>
<summary>Collapsible details/summary block</summary>
<p>Inner text here</p>
<h4>Inner Header</h4>
<p>More text <strong>with bold in</strong> it</p>
</details>
<p class="callout info">
Hello there, this is an info callout
</p>
<h3>Table</h3>
<table>
<thead>
<tr>
<th>Cell A</th>
<th>Cell B</th>
<th>Cell C</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell D</td>
<td>Cell E</td>
<td>Cell F</td>
</tr>
</tbody>
</table>
</div>
<div class="editor-container" refs="wysiwyg-editor@edit-container">
</div>
<script type="text/html" refs="wysiwyg-editor@edit-content">
<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>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p>
<h2>List below this h2 header</h2>
<ul>
<li>Hello</li>
</ul>
<details>
<summary>Collapsible details/summary block</summary>
<p>Inner text here</p>
<h4>Inner Header</h4>
<p>More text <strong>with bold in</strong> it</p>
</details>
<p class="callout info">
Hello there, this is an info callout
</p>
<h3>Table</h3>
<table>
<thead>
<tr>
<th>Cell A</th>
<th>Cell B</th>
<th>Cell C</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell D</td>
<td>Cell E</td>
<td>Cell F</td>
</tr>
</tbody>
</table>
</script>
<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>
{{-- <textarea id="html-editor" name="html" rows="5"--}}