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:
@@ -6,3 +6,92 @@ TODO - Create JS events and add to the js public events doc.
|
|||||||
TODO - Document the JS API.
|
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
1
package-lock.json
generated
@@ -4,7 +4,6 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bookstack",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "^6.10.0",
|
"@codemirror/commands": "^6.10.0",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
|||||||
13
resources/js/wysiwyg/api/__tests__/api-test-utils.ts
Normal file
13
resources/js/wysiwyg/api/__tests__/api-test-utils.ts
Normal 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};
|
||||||
|
}
|
||||||
109
resources/js/wysiwyg/api/__tests__/ui.test.ts
Normal file
109
resources/js/wysiwyg/api/__tests__/ui.test.ts
Normal 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, []),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -5,11 +5,11 @@ import {EditorOverflowContainer} from "../ui/framework/blocks/overflow-container
|
|||||||
type EditorApiButtonOptions = {
|
type EditorApiButtonOptions = {
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
onClick: () => void;
|
action: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditorApiButton {
|
export class EditorApiButton {
|
||||||
#button: EditorButton;
|
readonly #button: EditorButton;
|
||||||
#isActive: boolean = false;
|
#isActive: boolean = false;
|
||||||
|
|
||||||
constructor(options: EditorApiButtonOptions, context: EditorUiContext) {
|
constructor(options: EditorApiButtonOptions, context: EditorUiContext) {
|
||||||
@@ -17,7 +17,7 @@ class EditorApiButton {
|
|||||||
label: options.label || '',
|
label: options.label || '',
|
||||||
icon: options.icon || '',
|
icon: options.icon || '',
|
||||||
action: () => {
|
action: () => {
|
||||||
options.onClick();
|
options.action();
|
||||||
},
|
},
|
||||||
isActive: () => this.#isActive,
|
isActive: () => this.#isActive,
|
||||||
});
|
});
|
||||||
@@ -34,8 +34,8 @@ class EditorApiButton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditorApiToolbarSection {
|
export class EditorApiToolbarSection {
|
||||||
#section: EditorOverflowContainer;
|
readonly #section: EditorOverflowContainer;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
constructor(section: EditorOverflowContainer) {
|
constructor(section: EditorOverflowContainer) {
|
||||||
@@ -55,7 +55,7 @@ class EditorApiToolbarSection {
|
|||||||
|
|
||||||
|
|
||||||
export class EditorApiUiModule {
|
export class EditorApiUiModule {
|
||||||
#context: EditorUiContext;
|
readonly #context: EditorUiContext;
|
||||||
|
|
||||||
constructor(context: EditorUiContext) {
|
constructor(context: EditorUiContext) {
|
||||||
this.#context = context;
|
this.#context = context;
|
||||||
@@ -65,7 +65,7 @@ export class EditorApiUiModule {
|
|||||||
return new EditorApiButton(options, this.#context);
|
return new EditorApiButton(options, this.#context);
|
||||||
}
|
}
|
||||||
|
|
||||||
getToolbarSections(): EditorApiToolbarSection[] {
|
getMainToolbarSections(): EditorApiToolbarSection[] {
|
||||||
const toolbar = this.#context.manager.getToolbar();
|
const toolbar = this.#context.manager.getToolbar();
|
||||||
if (!toolbar) {
|
if (!toolbar) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ export function createTestContext(): EditorUiContext {
|
|||||||
options: {},
|
options: {},
|
||||||
scrollDOM: scrollWrap,
|
scrollDOM: scrollWrap,
|
||||||
translate(text: string): string {
|
translate(text: string): string {
|
||||||
return "";
|
return text;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user