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:
@@ -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
1
package-lock.json
generated
@@ -4,7 +4,6 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bookstack",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
"@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 = {
|
||||
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 [];
|
||||
|
||||
@@ -504,7 +504,7 @@ export function createTestContext(): EditorUiContext {
|
||||
options: {},
|
||||
scrollDOM: scrollWrap,
|
||||
translate(text: string): string {
|
||||
return "";
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user