Skip to content

New WYSIWYG editor for comments & descriptions #5676

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/Util/HtmlDescriptionFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use DOMAttr;
use DOMElement;
use DOMNamedNodeMap;
use DOMNode;

/**
Expand All @@ -25,6 +24,7 @@ class HtmlDescriptionFilter
'ul' => [],
'li' => [],
'strong' => [],
'span' => [],
'em' => [],
'br' => [],
];
Expand Down Expand Up @@ -59,7 +59,6 @@ protected static function filterElement(DOMElement $element): void
return;
}

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

foreach ($element->childNodes as $child) {
$childNodes = [...$element->childNodes];
foreach ($childNodes as $child) {
if ($child instanceof DOMElement) {
static::filterElement($child);
}
Expand Down
34 changes: 16 additions & 18 deletions resources/js/components/page-comment.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Component} from './component';
import {getLoading, htmlToDom} from '../services/dom';
import {buildForInput} from '../wysiwyg-tinymce/config';
import {PageCommentReference} from "./page-comment-reference";
import {HttpError} from "../services/http";
import {SimpleWysiwygEditorInterface} from "../wysiwyg";
import {el} from "../wysiwyg/utils/dom";

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

protected wysiwygEditor: any = null;
protected wysiwygLanguage!: string;
protected wysiwygEditor: SimpleWysiwygEditorInterface|null = null;
protected wysiwygTextDirection!: string;

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

// Editor reference and text options
this.wysiwygLanguage = this.$opts.wysiwygLanguage;
this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;

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

protected startEdit() : void {
protected async startEdit(): Promise<void> {
this.toggleEditMode(true);

if (this.wysiwygEditor) {
this.wysiwygEditor.focus();
return;
}

const config = buildForInput({
language: this.wysiwygLanguage,
containerElement: this.input,
type WysiwygModule = typeof import('../wysiwyg');
const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;
const editorContent = this.input.value;
const container = el('div', {class: 'comment-editor-container'});
this.input.parentElement?.appendChild(container);
this.input.hidden = true;

this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, editorContent, {
darkMode: document.documentElement.classList.contains('dark-mode'),
textDirection: this.wysiwygTextDirection,
drawioUrl: '',
pageId: 0,
translations: {},
translationMap: (window as unknown as Record<string, Object>).editor_translations,
textDirection: this.$opts.textDirection,
translations: (window as unknown as Record<string, Object>).editor_translations,
});

(window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
this.wysiwygEditor = editors[0];
setTimeout(() => this.wysiwygEditor.focus(), 50);
});
this.wysiwygEditor.focus();
}

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

const reqData = {
html: this.wysiwygEditor.getContent(),
html: await this.wysiwygEditor?.getContentAsHtml() || '',
};

try {
Expand Down
33 changes: 15 additions & 18 deletions resources/js/components/page-comments.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Component} from './component';
import {getLoading, htmlToDom} from '../services/dom';
import {buildForInput} from '../wysiwyg-tinymce/config';
import {Tabs} from "./tabs";
import {PageCommentReference} from "./page-comment-reference";
import {scrollAndHighlightElement} from "../services/util";
import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment";
import {el} from "../wysiwyg/utils/dom";
import {SimpleWysiwygEditorInterface} from "../wysiwyg";

export class PageComments extends Component {

Expand All @@ -28,9 +29,8 @@ export class PageComments extends Component {
private hideFormButton!: HTMLElement;
private removeReplyToButton!: HTMLElement;
private removeReferenceButton!: HTMLElement;
private wysiwygLanguage!: string;
private wysiwygTextDirection!: string;
private wysiwygEditor: any = null;
private wysiwygEditor: SimpleWysiwygEditorInterface|null = null;
private createdText!: string;
private countText!: string;
private archivedCountText!: string;
Expand Down Expand Up @@ -63,7 +63,6 @@ export class PageComments extends Component {
this.removeReferenceButton = this.$refs.removeReferenceButton;

// WYSIWYG options
this.wysiwygLanguage = this.$opts.wysiwygLanguage;
this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;

// Translations
Expand Down Expand Up @@ -107,7 +106,7 @@ export class PageComments extends Component {
}
}

protected saveComment(event: SubmitEvent): void {
protected async saveComment(event: SubmitEvent): Promise<void> {
event.preventDefault();
event.stopPropagation();

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

const reqData = {
html: this.wysiwygEditor.getContent(),
html: (await this.wysiwygEditor?.getContentAsHtml()) || '',
parent_id: this.parentId || null,
content_ref: this.contentReference,
};
Expand Down Expand Up @@ -189,27 +188,25 @@ export class PageComments extends Component {
this.addButtonContainer.toggleAttribute('hidden', false);
}

protected loadEditor(): void {
protected async loadEditor(): Promise<void> {
if (this.wysiwygEditor) {
this.wysiwygEditor.focus();
return;
}

const config = buildForInput({
language: this.wysiwygLanguage,
containerElement: this.formInput,
type WysiwygModule = typeof import('../wysiwyg');
const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;
const container = el('div', {class: 'comment-editor-container'});
this.formInput.parentElement?.appendChild(container);
this.formInput.hidden = true;

this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, '<p></p>', {
darkMode: document.documentElement.classList.contains('dark-mode'),
textDirection: this.wysiwygTextDirection,
drawioUrl: '',
pageId: 0,
translations: {},
translationMap: (window as unknown as Record<string, Object>).editor_translations,
translations: (window as unknown as Record<string, Object>).editor_translations,
});

(window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
this.wysiwygEditor = editors[0];
setTimeout(() => this.wysiwygEditor.focus(), 50);
});
this.wysiwygEditor.focus();
}

protected removeEditor(): void {
Expand Down
23 changes: 0 additions & 23 deletions resources/js/components/wysiwyg-input.js

This file was deleted.

32 changes: 32 additions & 0 deletions resources/js/components/wysiwyg-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {Component} from './component';
import {el} from "../wysiwyg/utils/dom";
import {SimpleWysiwygEditorInterface} from "../wysiwyg";

export class WysiwygInput extends Component {
private elem!: HTMLTextAreaElement;
private wysiwygEditor!: SimpleWysiwygEditorInterface;
private textDirection!: string;

async setup() {
this.elem = this.$el as HTMLTextAreaElement;
this.textDirection = this.$opts.textDirection;

type WysiwygModule = typeof import('../wysiwyg');
const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;
const container = el('div', {class: 'basic-editor-container'});
this.elem.parentElement?.appendChild(container);
this.elem.hidden = true;

this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, this.elem.value, {
darkMode: document.documentElement.classList.contains('dark-mode'),
textDirection: this.textDirection,
translations: (window as unknown as Record<string, Object>).editor_translations,
});

this.wysiwygEditor.onChange(() => {
this.wysiwygEditor.getContentAsHtml().then(html => {
this.elem.value = html;
});
});
}
}
48 changes: 0 additions & 48 deletions resources/js/wysiwyg-tinymce/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,54 +310,6 @@ export function buildForEditor(options) {
};
}

/**
* @param {WysiwygConfigOptions} options
* @return {RawEditorOptions}
*/
export function buildForInput(options) {
// Set language
window.tinymce.addI18n(options.language, options.translationMap);

// BookStack Version
const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];

// Return config object
return {
width: '100%',
height: '185px',
target: options.containerElement,
cache_suffix: `?version=${version}`,
content_css: [
window.baseUrl('/dist/styles.css'),
],
branding: false,
skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
body_class: 'wysiwyg-input',
browser_spellcheck: true,
relative_urls: false,
language: options.language,
directionality: options.textDirection,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
end_container_on_empty_block: true,
remove_trailing_brs: false,
statusbar: false,
menubar: false,
plugins: 'link autolink lists',
contextmenu: false,
toolbar: 'bold italic link bullist numlist',
content_style: getContentStyle(options),
file_picker_types: 'file',
valid_elements: 'p,a[href|title|target],ol,ul,li,strong,em,br',
file_picker_callback: filePickerCallback,
init_instance_callback(editor) {
addCustomHeadContent(editor.getDoc());

editor.contentDocument.documentElement.classList.toggle('dark-mode', options.darkMode);
},
};
}

/**
* @typedef {Object} WysiwygConfigOptions
* @property {Element} containerElement
Expand Down
Loading