Skip to content

Commit 4b9e604

Browse files
authored
Merge pull request #5676 from BookStackApp/lexical_comments
New WYSIWYG editor for comments & descriptions
2 parents d279b08 + a37d0c5 commit 4b9e604

File tree

26 files changed

+388
-313
lines changed

26 files changed

+388
-313
lines changed

app/Util/HtmlDescriptionFilter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use DOMAttr;
66
use DOMElement;
7-
use DOMNamedNodeMap;
87
use DOMNode;
98

109
/**
@@ -25,6 +24,7 @@ class HtmlDescriptionFilter
2524
'ul' => [],
2625
'li' => [],
2726
'strong' => [],
27+
'span' => [],
2828
'em' => [],
2929
'br' => [],
3030
];
@@ -59,7 +59,6 @@ protected static function filterElement(DOMElement $element): void
5959
return;
6060
}
6161

62-
/** @var DOMNamedNodeMap $attrs */
6362
$attrs = $element->attributes;
6463
for ($i = $attrs->length - 1; $i >= 0; $i--) {
6564
/** @var DOMAttr $attr */
@@ -70,7 +69,8 @@ protected static function filterElement(DOMElement $element): void
7069
}
7170
}
7271

73-
foreach ($element->childNodes as $child) {
72+
$childNodes = [...$element->childNodes];
73+
foreach ($childNodes as $child) {
7474
if ($child instanceof DOMElement) {
7575
static::filterElement($child);
7676
}

resources/js/components/page-comment.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {Component} from './component';
22
import {getLoading, htmlToDom} from '../services/dom';
3-
import {buildForInput} from '../wysiwyg-tinymce/config';
43
import {PageCommentReference} from "./page-comment-reference";
54
import {HttpError} from "../services/http";
5+
import {SimpleWysiwygEditorInterface} from "../wysiwyg";
6+
import {el} from "../wysiwyg/utils/dom";
67

78
export interface PageCommentReplyEventData {
89
id: string; // ID of comment being replied to
@@ -21,8 +22,7 @@ export class PageComment extends Component {
2122
protected updatedText!: string;
2223
protected archiveText!: string;
2324

24-
protected wysiwygEditor: any = null;
25-
protected wysiwygLanguage!: string;
25+
protected wysiwygEditor: SimpleWysiwygEditorInterface|null = null;
2626
protected wysiwygTextDirection!: string;
2727

2828
protected container!: HTMLElement;
@@ -44,7 +44,6 @@ export class PageComment extends Component {
4444
this.archiveText = this.$opts.archiveText;
4545

4646
// Editor reference and text options
47-
this.wysiwygLanguage = this.$opts.wysiwygLanguage;
4847
this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
4948

5049
// Element references
@@ -90,29 +89,28 @@ export class PageComment extends Component {
9089
this.form.toggleAttribute('hidden', !show);
9190
}
9291

93-
protected startEdit() : void {
92+
protected async startEdit(): Promise<void> {
9493
this.toggleEditMode(true);
9594

9695
if (this.wysiwygEditor) {
9796
this.wysiwygEditor.focus();
9897
return;
9998
}
10099

101-
const config = buildForInput({
102-
language: this.wysiwygLanguage,
103-
containerElement: this.input,
100+
type WysiwygModule = typeof import('../wysiwyg');
101+
const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;
102+
const editorContent = this.input.value;
103+
const container = el('div', {class: 'comment-editor-container'});
104+
this.input.parentElement?.appendChild(container);
105+
this.input.hidden = true;
106+
107+
this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, editorContent, {
104108
darkMode: document.documentElement.classList.contains('dark-mode'),
105-
textDirection: this.wysiwygTextDirection,
106-
drawioUrl: '',
107-
pageId: 0,
108-
translations: {},
109-
translationMap: (window as unknown as Record<string, Object>).editor_translations,
109+
textDirection: this.$opts.textDirection,
110+
translations: (window as unknown as Record<string, Object>).editor_translations,
110111
});
111112

112-
(window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
113-
this.wysiwygEditor = editors[0];
114-
setTimeout(() => this.wysiwygEditor.focus(), 50);
115-
});
113+
this.wysiwygEditor.focus();
116114
}
117115

118116
protected async update(event: Event): Promise<void> {
@@ -121,7 +119,7 @@ export class PageComment extends Component {
121119
this.form.toggleAttribute('hidden', true);
122120

123121
const reqData = {
124-
html: this.wysiwygEditor.getContent(),
122+
html: await this.wysiwygEditor?.getContentAsHtml() || '',
125123
};
126124

127125
try {

resources/js/components/page-comments.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {Component} from './component';
22
import {getLoading, htmlToDom} from '../services/dom';
3-
import {buildForInput} from '../wysiwyg-tinymce/config';
43
import {Tabs} from "./tabs";
54
import {PageCommentReference} from "./page-comment-reference";
65
import {scrollAndHighlightElement} from "../services/util";
76
import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment";
7+
import {el} from "../wysiwyg/utils/dom";
8+
import {SimpleWysiwygEditorInterface} from "../wysiwyg";
89

910
export class PageComments extends Component {
1011

@@ -28,9 +29,8 @@ export class PageComments extends Component {
2829
private hideFormButton!: HTMLElement;
2930
private removeReplyToButton!: HTMLElement;
3031
private removeReferenceButton!: HTMLElement;
31-
private wysiwygLanguage!: string;
3232
private wysiwygTextDirection!: string;
33-
private wysiwygEditor: any = null;
33+
private wysiwygEditor: SimpleWysiwygEditorInterface|null = null;
3434
private createdText!: string;
3535
private countText!: string;
3636
private archivedCountText!: string;
@@ -63,7 +63,6 @@ export class PageComments extends Component {
6363
this.removeReferenceButton = this.$refs.removeReferenceButton;
6464

6565
// WYSIWYG options
66-
this.wysiwygLanguage = this.$opts.wysiwygLanguage;
6766
this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
6867

6968
// Translations
@@ -107,7 +106,7 @@ export class PageComments extends Component {
107106
}
108107
}
109108

110-
protected saveComment(event: SubmitEvent): void {
109+
protected async saveComment(event: SubmitEvent): Promise<void> {
111110
event.preventDefault();
112111
event.stopPropagation();
113112

@@ -117,7 +116,7 @@ export class PageComments extends Component {
117116
this.form.toggleAttribute('hidden', true);
118117

119118
const reqData = {
120-
html: this.wysiwygEditor.getContent(),
119+
html: (await this.wysiwygEditor?.getContentAsHtml()) || '',
121120
parent_id: this.parentId || null,
122121
content_ref: this.contentReference,
123122
};
@@ -189,27 +188,25 @@ export class PageComments extends Component {
189188
this.addButtonContainer.toggleAttribute('hidden', false);
190189
}
191190

192-
protected loadEditor(): void {
191+
protected async loadEditor(): Promise<void> {
193192
if (this.wysiwygEditor) {
194193
this.wysiwygEditor.focus();
195194
return;
196195
}
197196

198-
const config = buildForInput({
199-
language: this.wysiwygLanguage,
200-
containerElement: this.formInput,
197+
type WysiwygModule = typeof import('../wysiwyg');
198+
const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;
199+
const container = el('div', {class: 'comment-editor-container'});
200+
this.formInput.parentElement?.appendChild(container);
201+
this.formInput.hidden = true;
202+
203+
this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, '<p></p>', {
201204
darkMode: document.documentElement.classList.contains('dark-mode'),
202205
textDirection: this.wysiwygTextDirection,
203-
drawioUrl: '',
204-
pageId: 0,
205-
translations: {},
206-
translationMap: (window as unknown as Record<string, Object>).editor_translations,
206+
translations: (window as unknown as Record<string, Object>).editor_translations,
207207
});
208208

209-
(window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
210-
this.wysiwygEditor = editors[0];
211-
setTimeout(() => this.wysiwygEditor.focus(), 50);
212-
});
209+
this.wysiwygEditor.focus();
213210
}
214211

215212
protected removeEditor(): void {

resources/js/components/wysiwyg-input.js

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {Component} from './component';
2+
import {el} from "../wysiwyg/utils/dom";
3+
import {SimpleWysiwygEditorInterface} from "../wysiwyg";
4+
5+
export class WysiwygInput extends Component {
6+
private elem!: HTMLTextAreaElement;
7+
private wysiwygEditor!: SimpleWysiwygEditorInterface;
8+
private textDirection!: string;
9+
10+
async setup() {
11+
this.elem = this.$el as HTMLTextAreaElement;
12+
this.textDirection = this.$opts.textDirection;
13+
14+
type WysiwygModule = typeof import('../wysiwyg');
15+
const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;
16+
const container = el('div', {class: 'basic-editor-container'});
17+
this.elem.parentElement?.appendChild(container);
18+
this.elem.hidden = true;
19+
20+
this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, this.elem.value, {
21+
darkMode: document.documentElement.classList.contains('dark-mode'),
22+
textDirection: this.textDirection,
23+
translations: (window as unknown as Record<string, Object>).editor_translations,
24+
});
25+
26+
this.wysiwygEditor.onChange(() => {
27+
this.wysiwygEditor.getContentAsHtml().then(html => {
28+
this.elem.value = html;
29+
});
30+
});
31+
}
32+
}

resources/js/wysiwyg-tinymce/config.js

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -310,54 +310,6 @@ export function buildForEditor(options) {
310310
};
311311
}
312312

313-
/**
314-
* @param {WysiwygConfigOptions} options
315-
* @return {RawEditorOptions}
316-
*/
317-
export function buildForInput(options) {
318-
// Set language
319-
window.tinymce.addI18n(options.language, options.translationMap);
320-
321-
// BookStack Version
322-
const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
323-
324-
// Return config object
325-
return {
326-
width: '100%',
327-
height: '185px',
328-
target: options.containerElement,
329-
cache_suffix: `?version=${version}`,
330-
content_css: [
331-
window.baseUrl('/dist/styles.css'),
332-
],
333-
branding: false,
334-
skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
335-
body_class: 'wysiwyg-input',
336-
browser_spellcheck: true,
337-
relative_urls: false,
338-
language: options.language,
339-
directionality: options.textDirection,
340-
remove_script_host: false,
341-
document_base_url: window.baseUrl('/'),
342-
end_container_on_empty_block: true,
343-
remove_trailing_brs: false,
344-
statusbar: false,
345-
menubar: false,
346-
plugins: 'link autolink lists',
347-
contextmenu: false,
348-
toolbar: 'bold italic link bullist numlist',
349-
content_style: getContentStyle(options),
350-
file_picker_types: 'file',
351-
valid_elements: 'p,a[href|title|target],ol,ul,li,strong,em,br',
352-
file_picker_callback: filePickerCallback,
353-
init_instance_callback(editor) {
354-
addCustomHeadContent(editor.getDoc());
355-
356-
editor.contentDocument.documentElement.classList.toggle('dark-mode', options.darkMode);
357-
},
358-
};
359-
}
360-
361313
/**
362314
* @typedef {Object} WysiwygConfigOptions
363315
* @property {Element} containerElement

0 commit comments

Comments
 (0)