Skip to content

Commit ebceba0

Browse files
committed
Lexical API: Started working on docs format and jest testing
1 parent 9d732d8 commit ebceba0

File tree

6 files changed

+221
-11
lines changed

6 files changed

+221
-11
lines changed

dev/docs/wysiwyg-js-api.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,93 @@ TODO - Create JS events and add to the js public events doc.
55

66
TODO - Document the JS API.
77

8-
TODO - Add testing coverage
8+
TODO - Add testing coverage
9+
10+
**Warning: This API is currently in development and may change without notice.**
11+
12+
This document covers the JavaScript API for the (newer Lexical-based) WYSIWYG editor.
13+
This API is custom-built, and designed to abstract the internals of the editor away
14+
to provide a stable interface for performing common customizations.
15+
16+
Only the methods and properties documented here are guaranteed to be stable **once this API
17+
is out of initial development**.
18+
Other elements may be accessible but are not designed to be used directly, and therefore may change
19+
without notice.
20+
Stable parts of the API may still change where needed, but such changes would be noted as part of BookStack update advisories.
21+
22+
## Overview
23+
24+
The API is provided as an object, which itself provides a number of modules
25+
via its properties:
26+
27+
- `ui` - Provides all actions related to the UI of the editor, like buttons and toolbars.
28+
29+
Each of these modules, and the relevant types used within, can be found detailed below.
30+
31+
---
32+
33+
## UI Module
34+
35+
This module provides all actions related to the UI of the editor, like buttons and toolbars.
36+
37+
### Methods
38+
39+
#### createButton(options)
40+
41+
Creates a new button which can be used by other methods.
42+
This takes an option object with the following properties:
43+
44+
- `label` - string, optional - Used for the button text if no icon provided, or the button tooltip if an icon is provided.
45+
- `icon` - string, optional - The icon to use for the button. Expected to be an SVG string.
46+
- `action` - callback, required - The action to perform when the button is clicked.
47+
48+
The function returns an [EditorApiButton](#editorapibutton) object.
49+
50+
**Example**
51+
52+
```javascript
53+
const button = api.ui.createButton({
54+
label: 'Warn',
55+
icon: '<svg>...</svg>',
56+
action: () => {
57+
window.alert('You clicked the button!');
58+
}
59+
});
60+
```
61+
62+
### getMainToolbarSections()
63+
64+
Get the sections of the main editor toolbar. These are those which contain groups of buttons
65+
with overflow control.
66+
67+
The function returns an array of [EditorToolbarSection](#editortoolbarsection) objects.
68+
69+
**Example**
70+
71+
```javascript
72+
const sections = api.ui.getMainToolbarSections();
73+
if (sections.length > 0) {
74+
sections[0].addButton(button);
75+
}
76+
```
77+
78+
### Types
79+
80+
These are types which may be provided from UI module methods.
81+
The methods on these types are documented using standard TypeScript notation.
82+
83+
#### EditorApiButton
84+
85+
Represents a button created via the `createButton` method.
86+
This has the following methods:
87+
88+
- `setActive(isActive: boolean): void` - Sets whether the button should be in an active state or not (typically active buttons appear as pressed).
89+
90+
#### EditorToolbarSection
91+
92+
Represents a section of the main editor toolbar, which contains a set of buttons.
93+
This has the following methods:
94+
95+
- `getLabel(): string` - Gets the label of the section.
96+
- `addButton(button: EditorApiButton, targetIndex: number = -1): void` - Adds a button to the section.
97+
- By default, this will append the button, although a target index can be provided to insert the button at a specific position.

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {createTestContext} from "lexical/__tests__/utils";
2+
import {EditorApi} from "../api";
3+
import {EditorUiContext} from "../../ui/framework/core";
4+
5+
6+
/**
7+
* Create an instance of the EditorApi and EditorUiContext.
8+
*/
9+
export function createEditorApiInstance(): { api: EditorApi; context: EditorUiContext } {
10+
const context = createTestContext();
11+
const api = new EditorApi(context);
12+
return {api, context};
13+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {createEditorApiInstance} from "./api-test-utils";
2+
import {EditorApiButton, EditorApiToolbarSection} from "../ui";
3+
import {getMainEditorFullToolbar} from "../../ui/defaults/toolbars";
4+
import {EditorContainerUiElement} from "../../ui/framework/core";
5+
import {EditorOverflowContainer} from "../../ui/framework/blocks/overflow-container";
6+
7+
8+
describe('Editor API: UI Module', () => {
9+
10+
describe('createButton()', () => {
11+
it('should return a button', () => {
12+
const {api} = createEditorApiInstance();
13+
const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => ''});
14+
expect(button).toBeInstanceOf(EditorApiButton);
15+
});
16+
17+
it('should only need action to be required', () => {
18+
const {api} = createEditorApiInstance();
19+
const button = api.ui.createButton({action: () => ''});
20+
expect(button).toBeInstanceOf(EditorApiButton);
21+
});
22+
23+
it('should pass the label and icon to the button', () => {
24+
const {api} = createEditorApiInstance();
25+
const button = api.ui.createButton({label: 'TestLabel', icon: '<svg>cat</svg>', action: () => ''});
26+
const html = button._getOriginalModel().getDOMElement().outerHTML;
27+
expect(html).toContain('TestLabel');
28+
expect(html).toContain('<svg>cat</svg>');
29+
})
30+
});
31+
32+
describe('EditorApiButton', () => {
33+
34+
describe('setActive()', () => {
35+
it('should update the active state of the button', () => {
36+
const {api} = createEditorApiInstance();
37+
const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => ''});
38+
39+
button.setActive(true);
40+
expect(button._getOriginalModel().isActive()).toBe(true);
41+
42+
button.setActive(false);
43+
expect(button._getOriginalModel().isActive()).toBe(false);
44+
})
45+
});
46+
47+
it('should call the provided action on click', () => {
48+
const {api} = createEditorApiInstance();
49+
let count = 0;
50+
const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => {
51+
count++;
52+
}});
53+
54+
const dom = button._getOriginalModel().getDOMElement();
55+
dom.click();
56+
dom.click();
57+
expect(count).toBe(2);
58+
});
59+
60+
});
61+
62+
describe('getMainToolbarSections()', () => {
63+
it('should return an array of toolbar sections', () => {
64+
const {api, context} = createEditorApiInstance();
65+
context.manager.setToolbar(getMainEditorFullToolbar(context));
66+
67+
const sections = api.ui.getMainToolbarSections();
68+
expect(Array.isArray(sections)).toBe(true);
69+
70+
expect(sections[0]).toBeInstanceOf(EditorApiToolbarSection);
71+
});
72+
});
73+
74+
describe('EditorApiToolbarSection', () => {
75+
76+
describe('getLabel()', () => {
77+
it('should return the label of the section', () => {
78+
const {api, context} = createEditorApiInstance();
79+
context.manager.setToolbar(testToolbar());
80+
const section = api.ui.getMainToolbarSections()[0];
81+
expect(section.getLabel()).toBe('section-a');
82+
})
83+
});
84+
85+
describe('addButton()', () => {
86+
it('should add a button to the section', () => {
87+
const {api, context} = createEditorApiInstance();
88+
const toolbar = testToolbar();
89+
context.manager.setToolbar(toolbar);
90+
const section = api.ui.getMainToolbarSections()[0];
91+
92+
const button = api.ui.createButton({label: 'TestButtonText!', action: () => ''});
93+
section.addButton(button);
94+
95+
const toolbarRendered = toolbar.getDOMElement().innerHTML;
96+
expect(toolbarRendered).toContain('TestButtonText!');
97+
});
98+
});
99+
100+
});
101+
102+
function testToolbar(): EditorContainerUiElement {
103+
return new EditorContainerUiElement([
104+
new EditorOverflowContainer('section-a', 1, []),
105+
new EditorOverflowContainer('section-b', 1, []),
106+
]);
107+
}
108+
109+
});

resources/js/wysiwyg/api/ui.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ import {EditorOverflowContainer} from "../ui/framework/blocks/overflow-container
55
type EditorApiButtonOptions = {
66
label?: string;
77
icon?: string;
8-
onClick: () => void;
8+
action: () => void;
99
};
1010

11-
class EditorApiButton {
12-
#button: EditorButton;
11+
export class EditorApiButton {
12+
readonly #button: EditorButton;
1313
#isActive: boolean = false;
1414

1515
constructor(options: EditorApiButtonOptions, context: EditorUiContext) {
1616
this.#button = new EditorButton({
1717
label: options.label || '',
1818
icon: options.icon || '',
1919
action: () => {
20-
options.onClick();
20+
options.action();
2121
},
2222
isActive: () => this.#isActive,
2323
});
@@ -34,8 +34,8 @@ class EditorApiButton {
3434
}
3535
}
3636

37-
class EditorApiToolbarSection {
38-
#section: EditorOverflowContainer;
37+
export class EditorApiToolbarSection {
38+
readonly #section: EditorOverflowContainer;
3939
label: string;
4040

4141
constructor(section: EditorOverflowContainer) {
@@ -55,7 +55,7 @@ class EditorApiToolbarSection {
5555

5656

5757
export class EditorApiUiModule {
58-
#context: EditorUiContext;
58+
readonly #context: EditorUiContext;
5959

6060
constructor(context: EditorUiContext) {
6161
this.#context = context;
@@ -65,7 +65,7 @@ export class EditorApiUiModule {
6565
return new EditorApiButton(options, this.#context);
6666
}
6767

68-
getToolbarSections(): EditorApiToolbarSection[] {
68+
getMainToolbarSections(): EditorApiToolbarSection[] {
6969
const toolbar = this.#context.manager.getToolbar();
7070
if (!toolbar) {
7171
return [];

resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ export function createTestContext(): EditorUiContext {
504504
options: {},
505505
scrollDOM: scrollWrap,
506506
translate(text: string): string {
507-
return "";
507+
return text;
508508
}
509509
};
510510

0 commit comments

Comments
 (0)