From ebceba0afe50ad4c1d10d1542c24e189857371a0 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 4 Dec 2025 21:13:17 +0000 Subject: [PATCH] Lexical API: Started working on docs format and jest testing --- dev/docs/wysiwyg-js-api.md | 91 ++++++++++++++- package-lock.json | 1 - .../wysiwyg/api/__tests__/api-test-utils.ts | 13 +++ resources/js/wysiwyg/api/__tests__/ui.test.ts | 109 ++++++++++++++++++ resources/js/wysiwyg/api/ui.ts | 16 +-- .../lexical/core/__tests__/utils/index.ts | 2 +- 6 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 resources/js/wysiwyg/api/__tests__/api-test-utils.ts create mode 100644 resources/js/wysiwyg/api/__tests__/ui.test.ts diff --git a/dev/docs/wysiwyg-js-api.md b/dev/docs/wysiwyg-js-api.md index 7fd91a5e6..dec09b1a8 100644 --- a/dev/docs/wysiwyg-js-api.md +++ b/dev/docs/wysiwyg-js-api.md @@ -5,4 +5,93 @@ TODO - Create JS events and add to the js public events doc. TODO - Document the JS API. -TODO - Add testing coverage \ No newline at end of file +TODO - Add testing coverage + +**Warning: This API is currently in development and may change without notice.** + +This document covers the JavaScript API for the (newer Lexical-based) WYSIWYG editor. +This API is custom-built, and designed to abstract the internals of the editor away +to provide a stable interface for performing common customizations. + +Only the methods and properties documented here are guaranteed to be stable **once this API +is out of initial development**. +Other elements may be accessible but are not designed to be used directly, and therefore may change +without notice. +Stable parts of the API may still change where needed, but such changes would be noted as part of BookStack update advisories. + +## Overview + +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. + +Each of these modules, and the relevant types used within, can be found detailed below. + +--- + +## UI Module + +This module provides all actions related to the UI of the editor, like buttons and toolbars. + +### Methods + +#### createButton(options) + +Creates a new button which can be used by other methods. +This takes an option object with the following properties: + +- `label` - string, optional - Used for the button text if no icon provided, or the button tooltip if an icon is provided. +- `icon` - string, optional - The icon to use for the button. Expected to be an SVG string. +- `action` - callback, required - The action to perform when the button is clicked. + +The function returns an [EditorApiButton](#editorapibutton) object. + +**Example** + +```javascript +const button = api.ui.createButton({ + label: 'Warn', + icon: '...', + action: () => { + window.alert('You clicked the button!'); + } +}); +``` + +### getMainToolbarSections() + +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. + +**Example** + +```javascript +const sections = api.ui.getMainToolbarSections(); +if (sections.length > 0) { + sections[0].addButton(button); +} +``` + +### Types + +These are types which may be provided from UI module methods. +The methods on these types are documented using standard TypeScript notation. + +#### EditorApiButton + +Represents a button created via the `createButton` method. +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 + +Represents a section of the main editor toolbar, which contains a set of buttons. +This has the following methods: + +- `getLabel(): string` - Gets the 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. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 44ebf7eab..b85d1f5e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "bookstack", "dependencies": { "@codemirror/commands": "^6.10.0", "@codemirror/lang-css": "^6.3.1", diff --git a/resources/js/wysiwyg/api/__tests__/api-test-utils.ts b/resources/js/wysiwyg/api/__tests__/api-test-utils.ts new file mode 100644 index 000000000..beff8504f --- /dev/null +++ b/resources/js/wysiwyg/api/__tests__/api-test-utils.ts @@ -0,0 +1,13 @@ +import {createTestContext} from "lexical/__tests__/utils"; +import {EditorApi} from "../api"; +import {EditorUiContext} from "../../ui/framework/core"; + + +/** + * Create an instance of the EditorApi and EditorUiContext. + */ +export function createEditorApiInstance(): { api: EditorApi; context: EditorUiContext } { + const context = createTestContext(); + const api = new EditorApi(context); + return {api, context}; +} \ No newline at end of file diff --git a/resources/js/wysiwyg/api/__tests__/ui.test.ts b/resources/js/wysiwyg/api/__tests__/ui.test.ts new file mode 100644 index 000000000..6cd80895b --- /dev/null +++ b/resources/js/wysiwyg/api/__tests__/ui.test.ts @@ -0,0 +1,109 @@ +import {createEditorApiInstance} from "./api-test-utils"; +import {EditorApiButton, EditorApiToolbarSection} from "../ui"; +import {getMainEditorFullToolbar} from "../../ui/defaults/toolbars"; +import {EditorContainerUiElement} from "../../ui/framework/core"; +import {EditorOverflowContainer} from "../../ui/framework/blocks/overflow-container"; + + +describe('Editor API: UI Module', () => { + + describe('createButton()', () => { + it('should return a button', () => { + const {api} = createEditorApiInstance(); + const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => ''}); + expect(button).toBeInstanceOf(EditorApiButton); + }); + + it('should only need action to be required', () => { + const {api} = createEditorApiInstance(); + const button = api.ui.createButton({action: () => ''}); + expect(button).toBeInstanceOf(EditorApiButton); + }); + + it('should pass the label and icon to the button', () => { + const {api} = createEditorApiInstance(); + const button = api.ui.createButton({label: 'TestLabel', icon: 'cat', action: () => ''}); + const html = button._getOriginalModel().getDOMElement().outerHTML; + expect(html).toContain('TestLabel'); + expect(html).toContain('cat'); + }) + }); + + describe('EditorApiButton', () => { + + describe('setActive()', () => { + it('should update the active state of the button', () => { + const {api} = createEditorApiInstance(); + const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => ''}); + + button.setActive(true); + expect(button._getOriginalModel().isActive()).toBe(true); + + button.setActive(false); + expect(button._getOriginalModel().isActive()).toBe(false); + }) + }); + + it('should call the provided action on click', () => { + const {api} = createEditorApiInstance(); + let count = 0; + const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => { + count++; + }}); + + const dom = button._getOriginalModel().getDOMElement(); + dom.click(); + dom.click(); + expect(count).toBe(2); + }); + + }); + + describe('getMainToolbarSections()', () => { + it('should return an array of toolbar sections', () => { + const {api, context} = createEditorApiInstance(); + context.manager.setToolbar(getMainEditorFullToolbar(context)); + + const sections = api.ui.getMainToolbarSections(); + expect(Array.isArray(sections)).toBe(true); + + 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]; + expect(section.getLabel()).toBe('section-a'); + }) + }); + + describe('addButton()', () => { + it('should add a button to the section', () => { + const {api, context} = createEditorApiInstance(); + const toolbar = testToolbar(); + context.manager.setToolbar(toolbar); + const section = api.ui.getMainToolbarSections()[0]; + + const button = api.ui.createButton({label: 'TestButtonText!', action: () => ''}); + section.addButton(button); + + const toolbarRendered = toolbar.getDOMElement().innerHTML; + expect(toolbarRendered).toContain('TestButtonText!'); + }); + }); + + }); + + function testToolbar(): EditorContainerUiElement { + return new EditorContainerUiElement([ + new EditorOverflowContainer('section-a', 1, []), + new EditorOverflowContainer('section-b', 1, []), + ]); + } + +}); \ No newline at end of file diff --git a/resources/js/wysiwyg/api/ui.ts b/resources/js/wysiwyg/api/ui.ts index faad5d316..cf559269f 100644 --- a/resources/js/wysiwyg/api/ui.ts +++ b/resources/js/wysiwyg/api/ui.ts @@ -5,11 +5,11 @@ import {EditorOverflowContainer} from "../ui/framework/blocks/overflow-container type EditorApiButtonOptions = { label?: string; icon?: string; - onClick: () => void; + action: () => void; }; -class EditorApiButton { - #button: EditorButton; +export class EditorApiButton { + readonly #button: EditorButton; #isActive: boolean = false; constructor(options: EditorApiButtonOptions, context: EditorUiContext) { @@ -17,7 +17,7 @@ class EditorApiButton { label: options.label || '', icon: options.icon || '', action: () => { - options.onClick(); + options.action(); }, isActive: () => this.#isActive, }); @@ -34,8 +34,8 @@ class EditorApiButton { } } -class EditorApiToolbarSection { - #section: EditorOverflowContainer; +export class EditorApiToolbarSection { + readonly #section: EditorOverflowContainer; label: string; constructor(section: EditorOverflowContainer) { @@ -55,7 +55,7 @@ class EditorApiToolbarSection { export class EditorApiUiModule { - #context: EditorUiContext; + readonly #context: EditorUiContext; constructor(context: EditorUiContext) { this.#context = context; @@ -65,7 +65,7 @@ export class EditorApiUiModule { return new EditorApiButton(options, this.#context); } - getToolbarSections(): EditorApiToolbarSection[] { + getMainToolbarSections(): EditorApiToolbarSection[] { const toolbar = this.#context.manager.getToolbar(); if (!toolbar) { return []; diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index 00c5ec796..4f08f35ed 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -504,7 +504,7 @@ export function createTestContext(): EditorUiContext { options: {}, scrollDOM: scrollWrap, translate(text: string): string { - return ""; + return text; } };