mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-14 23:20:13 +08:00
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:
parent
b1c489090e
commit
c2ecbf071f
1
resources/icons/editor/fullscreen.svg
Normal file
1
resources/icons/editor/fullscreen.svg
Normal 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 |
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user