diff --git a/resources/icons/editor/fullscreen.svg b/resources/icons/editor/fullscreen.svg new file mode 100644 index 000000000..3cca3097a --- /dev/null +++ b/resources/icons/editor/fullscreen.svg @@ -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> \ No newline at end of file diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index 98732dab7..bdcdd5c51 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -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); }); } diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index 9f2f1645a..3ca01835f 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -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); } diff --git a/resources/js/wysiwyg/ui/defaults/button-definitions.ts b/resources/js/wysiwyg/ui/defaults/button-definitions.ts index cebf25807..4a45ef75d 100644 --- a/resources/js/wysiwyg/ui/defaults/button-definitions.ts +++ b/resources/js/wysiwyg/ui/defaults/button-definitions.ts @@ -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'); + } }; \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/buttons.ts b/resources/js/wysiwyg/ui/framework/buttons.ts index 7e8df076a..f74201ff7 100644 --- a/resources/js/wysiwyg/ui/framework/buttons.ts +++ b/resources/js/wysiwyg/ui/framework/buttons.ts @@ -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); } diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts index 2972c9821..465765caa 100644 --- a/resources/js/wysiwyg/ui/framework/core.ts +++ b/resources/js/wysiwyg/ui/framework/core.ts @@ -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, diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index 4ca90a12a..3c2ad8926 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -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); } } diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 1f07fe710..3501ed557 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -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, diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index bb3b436b9..550c798c2 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -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({ diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 2fcf4edb3..79cbc73e8 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -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; diff --git a/resources/views/pages/parts/wysiwyg-editor.blade.php b/resources/views/pages/parts/wysiwyg-editor.blade.php index 5cd60bbc6..8fc0dc55a 100644 --- a/resources/views/pages/parts/wysiwyg-editor.blade.php +++ b/resources/views/pages/parts/wysiwyg-editor.blade.php @@ -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"--}}