diff --git a/dev/docs/javascript-public-events.md b/dev/docs/javascript-public-events.md index 85a4caad4..e9ea014ef 100644 --- a/dev/docs/javascript-public-events.md +++ b/dev/docs/javascript-public-events.md @@ -134,6 +134,47 @@ window.addEventListener('editor-tinymce::setup', event => { }); ``` +### `editor-wysiwyg::post-init` + +This is called after the (new custom-built Lexical-based) WYSIWYG editor has been initialised. + +#### Event Data + +- `usage` - A string label to identify the usage type of the WYSIWYG editor in BookStack. +- `api` - An instance to the WYSIWYG editor API, as documented in the [WYSIWYG JavaScript API file](./wysiwyg-js-api.md). + +##### Example + +The below example shows how you'd use this API to create a button, with that button added to the main toolbar of the page editor, which inserts bold "Hello!" text on press: + +
+Show Example + +```javascript +window.addEventListener('editor-wysiwyg::post-init', event => { + const {usage, api} = event.detail; + // Check that it's the page editor which is being loaded + if (usage !== 'page-editor') { + return; + } + + // Create a custom button which inserts bold hello text on press + const button = api.ui.createButton({ + label: 'Greet', + action: () => { + api.content.insertHtml(`Hello!`); + } + }); + + // Add the button to the start of the first section within the main toolbar + const toolbar = api.ui.getMainToolbar(); + if (toolbar) { + toolbar.getSections()[0]?.addButton(button, 0); + } +}); +``` +
+ ### `library-cm6::configure-theme` This event is called whenever a CodeMirror instance is loaded, as a method to configure the theme used by CodeMirror. This applies to all CodeMirror instances including in-page code blocks, editors using in BookStack settings, and the Page markdown editor. @@ -319,41 +360,3 @@ window.addEventListener('library-cm6::post-init', event => { }); ``` - -### `editor-wysiwyg::post-init` - -This is called after the (new custom-built Lexical-based) WYSIWYG editor has been initialised. - -#### Event Data - -- `usage` - A string label to identify the usage type of the WYSIWYG editor in BookStack. -- `api` - An instance to the WYSIWYG editor API, as documented in the [WYSIWYG JavaScript API file](./wysiwyg-js-api.md). - -##### Example - -The below shows how you'd use this API to create a button, with that button added to the toolbar of the page editor, which inserts bold hello text on press: - -
-Show Example - -```javascript -window.addEventListener('editor-wysiwyg::post-init', event => { - const {usage, api} = event.detail; - // Check that it's the page editor being loaded. - if (usage !== 'page-editor') { - return; - } - - // Create a custom button which inserts bold hello text on press. - const button = api.ui.createButton({ - label: 'Greet', - action: () => { - api.content.insertHtml(`Hello!`); - } - }); - - // Add the button to the start of the first section within the main toolbar. - api.ui.getMainToolbarSections()[0]?.addButton(button, 0); -}); -``` -
\ No newline at end of file diff --git a/dev/docs/wysiwyg-js-api.md b/dev/docs/wysiwyg-js-api.md index 7286f943f..394f2f15f 100644 --- a/dev/docs/wysiwyg-js-api.md +++ b/dev/docs/wysiwyg-js-api.md @@ -1,9 +1,9 @@ # WYSIWYG JavaScript API -TODO - Create JS events and add to the js public events doc. - **Warning: This API is currently in development and may change without notice.** +Feedback is very much welcomed via this issue: https://github.com/BookStackApp/BookStack/issues/5937 + This document covers the JavaScript API for the (newer Lexical-based) WYSIWYG editor. This API is built and designed to abstract the internals of the editor away to provide a stable interface for performing common customizations. @@ -19,8 +19,8 @@ Stable parts of the API may still change where needed, but such changes would be The API is provided as an object, which itself provides a number of modules via its properties: -- `ui` - Provides all actions related to the UI of the editor, like buttons and toolbars. -- `content` - Provides all actions related to the live user content being edited upon. +- `ui` - Provides methods related to the UI of the editor, like buttons and toolbars. +- `content` - Provides methods related to the live user content being edited upon. Each of these modules, and the relevant types used within, are documented in detail below. @@ -28,7 +28,7 @@ Each of these modules, and the relevant types used within, are documented in det ## UI Module -This module provides all actions related to the UI of the editor, like buttons and toolbars. +This module provides methods related to the UI of the editor, like buttons and toolbars. ### Methods @@ -55,17 +55,16 @@ const button = api.ui.createButton({ }); ``` -### getMainToolbarSections() +### getMainToolbar() -Get the sections of the main editor toolbar. These are those which contain groups of buttons -with overflow control. - -The function returns an array of [EditorToolbarSection](#editortoolbarsection) objects. +Get the main editor toolbar. This is typically the toolbar at the top of the editor. +The function returns an [EditorApiToolbar](#editorapitoolbar) object, or null if no toolbar is found. **Example** ```javascript -const sections = api.ui.getMainToolbarSections(); +const toolbar = api.ui.getMainToolbar(); +const sections = toolbar?.getSections() || []; if (sections.length > 0) { sections[0].addButton(button); } @@ -83,20 +82,27 @@ This has the following methods: - `setActive(isActive: boolean): void` - Sets whether the button should be in an active state or not (typically active buttons appear as pressed). -#### EditorToolbarSection +#### EditorApiToolbar + +Represents a toolbar within the editor. This is a bar typically containing sets of buttons. +This has the following methods: + +- `getSections(): EditorApiToolbarSection[]` - Provides the main [EditorApiToolbarSections](#editorapitoolbarsection) contained within this toolbar. + +#### EditorApiToolbarSection Represents a section of the main editor toolbar, which contains a set of buttons. This has the following methods: - `getLabel(): string` - Provides the string label of the section. - `addButton(button: EditorApiButton, targetIndex: number = -1): void` - Adds a button to the section. - - By default, this will append the button, although a target index can be provided to insert the button at a specific position. + - By default, this will append the button, although a target index can be provided to insert at a specific position. --- ## Content Module -This module provides all actions related to the live user content being edited within the editor. +This module provides methods related to the live user content being edited within the editor. ### Methods diff --git a/resources/js/wysiwyg/api/__tests__/ui.test.ts b/resources/js/wysiwyg/api/__tests__/ui.test.ts index 6cd80895b..80045bb48 100644 --- a/resources/js/wysiwyg/api/__tests__/ui.test.ts +++ b/resources/js/wysiwyg/api/__tests__/ui.test.ts @@ -1,5 +1,5 @@ import {createEditorApiInstance} from "./api-test-utils"; -import {EditorApiButton, EditorApiToolbarSection} from "../ui"; +import {EditorApiButton, EditorApiToolbar, EditorApiToolbarSection} from "../ui"; import {getMainEditorFullToolbar} from "../../ui/defaults/toolbars"; import {EditorContainerUiElement} from "../../ui/framework/core"; import {EditorOverflowContainer} from "../../ui/framework/blocks/overflow-container"; @@ -59,25 +59,39 @@ describe('Editor API: UI Module', () => { }); - describe('getMainToolbarSections()', () => { - it('should return an array of toolbar sections', () => { + describe('getMainToolbar()', () => { + it('should return the main editor toolbar', () => { const {api, context} = createEditorApiInstance(); context.manager.setToolbar(getMainEditorFullToolbar(context)); - const sections = api.ui.getMainToolbarSections(); - expect(Array.isArray(sections)).toBe(true); + const toolbar = api.ui.getMainToolbar(); - expect(sections[0]).toBeInstanceOf(EditorApiToolbarSection); + expect(toolbar).toBeInstanceOf(EditorApiToolbar); }); }); + describe('EditorApiToolbar', () => { + describe('getSections()', () => { + it('should return the sections of the toolbar', () => { + const {api, context} = createEditorApiInstance(); + context.manager.setToolbar(testToolbar()); + const toolbar = api.ui.getMainToolbar(); + + const sections = toolbar?.getSections() || []; + + expect(sections.length).toBe(2); + expect(sections[0]).toBeInstanceOf(EditorApiToolbarSection); + }) + }) + }) + describe('EditorApiToolbarSection', () => { describe('getLabel()', () => { it('should return the label of the section', () => { const {api, context} = createEditorApiInstance(); context.manager.setToolbar(testToolbar()); - const section = api.ui.getMainToolbarSections()[0]; + const section = api.ui.getMainToolbar()?.getSections()[0] as EditorApiToolbarSection; expect(section.getLabel()).toBe('section-a'); }) }); @@ -87,7 +101,7 @@ describe('Editor API: UI Module', () => { const {api, context} = createEditorApiInstance(); const toolbar = testToolbar(); context.manager.setToolbar(toolbar); - const section = api.ui.getMainToolbarSections()[0]; + const section = api.ui.getMainToolbar()?.getSections()[0] as EditorApiToolbarSection; const button = api.ui.createButton({label: 'TestButtonText!', action: () => ''}); section.addButton(button); diff --git a/resources/js/wysiwyg/api/ui.ts b/resources/js/wysiwyg/api/ui.ts index cf559269f..c5b822a46 100644 --- a/resources/js/wysiwyg/api/ui.ts +++ b/resources/js/wysiwyg/api/ui.ts @@ -1,5 +1,5 @@ import {EditorButton} from "../ui/framework/buttons"; -import {EditorUiContext} from "../ui/framework/core"; +import {EditorContainerUiElement, EditorUiContext} from "../ui/framework/core"; import {EditorOverflowContainer} from "../ui/framework/blocks/overflow-container"; type EditorApiButtonOptions = { @@ -34,13 +34,26 @@ export class EditorApiButton { } } +export class EditorApiToolbar { + readonly #toolbar: EditorContainerUiElement; + + constructor(toolbar: EditorContainerUiElement) { + this.#toolbar = toolbar; + } + + getSections(): EditorApiToolbarSection[] { + const sections = this.#toolbar.getChildren(); + return sections.filter(section => { + return section instanceof EditorOverflowContainer; + }).map(section => new EditorApiToolbarSection(section)); + } +} + export class EditorApiToolbarSection { readonly #section: EditorOverflowContainer; - label: string; constructor(section: EditorOverflowContainer) { this.#section = section; - this.label = section.getLabel(); } getLabel(): string { @@ -65,15 +78,12 @@ export class EditorApiUiModule { return new EditorApiButton(options, this.#context); } - getMainToolbarSections(): EditorApiToolbarSection[] { + getMainToolbar(): EditorApiToolbar|null { const toolbar = this.#context.manager.getToolbar(); if (!toolbar) { - return []; + return null; } - const sections = toolbar.getChildren(); - return sections.filter(section => { - return section instanceof EditorOverflowContainer; - }).map(section => new EditorApiToolbarSection(section)); + return new EditorApiToolbar(toolbar); } } \ No newline at end of file