1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-12-11 19:57:23 +03:00

Lexical API: Started working on docs format and jest testing

This commit is contained in:
Dan Brown
2025-12-04 21:13:17 +00:00
parent 9d732d8dd8
commit ebceba0afe
6 changed files with 221 additions and 11 deletions

View File

@@ -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
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: '<svg>...</svg>',
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.

1
package-lock.json generated
View File

@@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
"name": "bookstack",
"dependencies": {
"@codemirror/commands": "^6.10.0",
"@codemirror/lang-css": "^6.3.1",

View File

@@ -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};
}

View File

@@ -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: '<svg>cat</svg>', action: () => ''});
const html = button._getOriginalModel().getDOMElement().outerHTML;
expect(html).toContain('TestLabel');
expect(html).toContain('<svg>cat</svg>');
})
});
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, []),
]);
}
});

View File

@@ -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 [];

View File

@@ -504,7 +504,7 @@ export function createTestContext(): EditorUiContext {
options: {},
scrollDOM: scrollWrap,
translate(text: string): string {
return "";
return text;
}
};