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: '', action: () => ''});
+ const html = button._getOriginalModel().getDOMElement().outerHTML;
+ expect(html).toContain('TestLabel');
+ expect(html).toContain('');
+ })
+ });
+
+ 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;
}
};