-import {register as registerShortcuts} from "./shortcuts";
-import {listen as listenForCommonEvents} from "./common-events";
-import {scrollToQueryString, fixScrollForMobile} from "./scrolling";
-import {listenForDragAndPaste} from "./drop-paste-handling";
-
-import {getPlugin as getCodeeditorPlugin} from "./plugin-codeeditor";
-import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
-import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
-import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
-
-const style_formats = [
- {title: "Large Header", format: "h2", preview: 'color: blue;'},
- {title: "Medium Header", format: "h3"},
- {title: "Small Header", format: "h4"},
- {title: "Tiny Header", format: "h5"},
- {title: "Paragraph", format: "p", exact: true, classes: ''},
- {title: "Blockquote", format: "blockquote"},
- {title: "Inline Code", inline: "code"},
+import {register as registerShortcuts} from './shortcuts';
+import {listen as listenForCommonEvents} from './common-events';
+import {scrollToQueryString} from './scrolling';
+import {listenForDragAndPaste} from './drop-paste-handling';
+import {getPrimaryToolbar, registerAdditionalToolbars} from './toolbars';
+import {registerCustomIcons} from './icons';
+import {setupFilters} from './filters';
+
+import {getPlugin as getCodeeditorPlugin} from './plugin-codeeditor';
+import {getPlugin as getDrawioPlugin} from './plugin-drawio';
+import {getPlugin as getCustomhrPlugin} from './plugins-customhr';
+import {getPlugin as getImagemanagerPlugin} from './plugins-imagemanager';
+import {getPlugin as getAboutPlugin} from './plugins-about';
+import {getPlugin as getDetailsPlugin} from './plugins-details';
+import {getPlugin as getTasklistPlugin} from './plugins-tasklist';
+import {handleEmbedAlignmentChanges} from './fixes';
+
+const styleFormats = [
+ {title: 'Large Header', format: 'h2', preview: 'color: blue;'},
+ {title: 'Medium Header', format: 'h3'},
+ {title: 'Small Header', format: 'h4'},
+ {title: 'Tiny Header', format: 'h5'},
{
- title: "Callouts", items: [
- {title: "Information", format: 'calloutinfo'},
- {title: "Success", format: 'calloutsuccess'},
- {title: "Warning", format: 'calloutwarning'},
- {title: "Danger", format: 'calloutdanger'}
- ]
+ title: 'Paragraph', format: 'p', exact: true, classes: '',
+ },
+ {title: 'Blockquote', format: 'blockquote'},
+ {
+ title: 'Callouts',
+ items: [
+ {title: 'Information', format: 'calloutinfo'},
+ {title: 'Success', format: 'calloutsuccess'},
+ {title: 'Warning', format: 'calloutwarning'},
+ {title: 'Danger', format: 'calloutdanger'},
+ ],
},
];
const formats = {
- codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
- alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
- aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
- alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
+ alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-left'},
+ aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-center'},
+ alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-right'},
calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
- calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
+ calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
};
-function file_picker_callback(callback, value, meta) {
+const colorMap = [
+ '#BFEDD2', '',
+ '#FBEEB8', '',
+ '#F8CAC6', '',
+ '#ECCAFA', '',
+ '#C2E0F4', '',
+
+ '#2DC26B', '',
+ '#F1C40F', '',
+ '#E03E2D', '',
+ '#B96AD9', '',
+ '#3598DB', '',
+
+ '#169179', '',
+ '#E67E23', '',
+ '#BA372A', '',
+ '#843FA1', '',
+ '#236FA1', '',
+
+ '#ECF0F1', '',
+ '#CED4D9', '',
+ '#95A5A6', '',
+ '#7E8C8D', '',
+ '#34495E', '',
+
+ '#000000', '',
+ '#ffffff', '',
+];
+function filePickerCallback(callback, value, meta) {
// field_name, url, type, win
if (meta.filetype === 'file') {
- window.EntitySelectorPopup.show(entity => {
+ /** @type {EntitySelectorPopup} * */
+ const selector = window.$components.first('entity-selector-popup');
+ const selectionText = this.selection.getContent({format: 'text'}).trim();
+ selector.show(entity => {
callback(entity.link, {
text: entity.name,
title: entity.name,
});
+ }, {
+ initialValue: selectionText,
+ searchEndpoint: '/search/entity-selector',
+ entityTypes: 'page,book,chapter,bookshelf',
+ entityPermission: 'view',
});
}
if (meta.filetype === 'image') {
// Show image manager
- window.ImageManager.show(function (image) {
+ /** @type {ImageManager} * */
+ const imageManager = window.$components.first('image-manager');
+ imageManager.show(image => {
callback(image.url, {alt: image.name});
}, 'gallery');
}
-
-}
-
-/**
- * @param {WysiwygConfigOptions} options
- * @return {string}
- */
-function buildToolbar(options) {
- const textDirPlugins = options.textDirection === 'rtl' ? 'ltr rtl' : '';
-
- const toolbar = [
- 'undo redo',
- 'styleselect',
- 'bold italic underline strikethrough superscript subscript',
- 'forecolor backcolor',
- 'alignleft aligncenter alignright alignjustify',
- 'bullist numlist outdent indent',
- textDirPlugins,
- 'table imagemanager-insert link hr codeeditor drawio media',
- 'removeformat code ${textDirPlugins} fullscreen'
- ];
-
- return toolbar.filter(row => Boolean(row)).join(' | ');
}
/**
* @param {WysiwygConfigOptions} options
- * @return {string}
+ * @return {string[]}
*/
function gatherPlugins(options) {
const plugins = [
- "image",
- "imagetools",
- "table",
- "paste",
- "link",
- "autolink",
- "fullscreen",
- "code",
- "customhr",
- "autosave",
- "lists",
- "codeeditor",
- "media",
- "imagemanager",
+ 'image',
+ 'table',
+ 'link',
+ 'autolink',
+ 'fullscreen',
+ 'code',
+ 'customhr',
+ 'autosave',
+ 'lists',
+ 'codeeditor',
+ 'media',
+ 'imagemanager',
+ 'about',
+ 'details',
+ 'tasklist',
options.textDirection === 'rtl' ? 'directionality' : '',
];
- window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
- window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
- window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
+ window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
+ window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
+ window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
+ window.tinymce.PluginManager.add('about', getAboutPlugin());
+ window.tinymce.PluginManager.add('details', getDetailsPlugin());
+ window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
if (options.drawioUrl) {
window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
plugins.push('drawio');
}
- return plugins.filter(plugin => Boolean(plugin)).join(' ');
+ return plugins.filter(plugin => Boolean(plugin));
}
/**
- * Load custom HTML head content from the settings into the editor.
- * TODO: We should be able to get this from current parent page?
- * @param {Editor} editor
+ * Fetch custom HTML head content nodes from the outer page head
+ * and add them to the given editor document.
+ * @param {Document} editorDoc
*/
-function loadCustomHeadContent(editor) {
- window.$http.get(window.baseUrl('/custom-head-content')).then(resp => {
- if (!resp.data) return;
- let head = editor.getDoc().querySelector('head');
- head.innerHTML += resp.data;
- });
+function addCustomHeadContent(editorDoc) {
+ const headContentLines = document.head.innerHTML.split('\n');
+ const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
+ const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
+ if (startLineIndex === -1 || endLineIndex === -1) {
+ return;
+ }
+
+ const customHeadHtml = headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
+ const el = editorDoc.createElement('div');
+ el.innerHTML = customHeadHtml;
+
+ editorDoc.head.append(...el.children);
}
/**
* @return {function(Editor)}
*/
function getSetupCallback(options) {
- return function(editor) {
+ return function setupCallback(editor) {
+ function editorChange() {
+ if (options.darkMode) {
+ editor.contentDocument.documentElement.classList.add('dark-mode');
+ }
+ window.$events.emit('editor-html-change', '');
+ }
+
editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
listenForCommonEvents(editor);
- registerShortcuts(editor);
listenForDragAndPaste(editor, options);
editor.on('init', () => {
editorChange();
scrollToQueryString(editor);
- fixScrollForMobile(editor);
window.editor = editor;
+ registerShortcuts(editor);
});
- function editorChange() {
- const content = editor.getContent();
- if (options.darkMode) {
- editor.contentDocument.documentElement.classList.add('dark-mode');
- }
- window.$events.emit('editor-html-change', content);
- }
-
- // TODO - Update to standardise across both editors
- // Use events within listenForBookStackEditorEvents instead (Different event signature)
- window.$events.listen('editor-html-update', html => {
- editor.setContent(html);
- editor.selection.select(editor.getBody(), true);
- editor.selection.collapse(false);
- editorChange(html);
+ editor.on('PreInit', () => {
+ setupFilters(editor);
});
+ handleEmbedAlignmentChanges(editor);
+
// Custom handler hook
window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
- }
+
+ // Inline code format button
+ editor.ui.registry.addButton('inlinecode', {
+ tooltip: 'Inline code',
+ icon: 'sourcecode',
+ onAction() {
+ editor.execCommand('mceToggleFormat', false, 'code');
+ },
+ });
+ };
}
/**
* @param {WysiwygConfigOptions} options
- * @return {Object}
*/
-export function build(options) {
+function getContentStyle(options) {
+ return `
+html, body, html.dark-mode {
+ background: ${options.darkMode ? '#222' : '#fff'};
+}
+body {
+ padding-left: 15px !important;
+ padding-right: 15px !important;
+ height: initial !important;
+ margin:0!important;
+ margin-left: auto! important;
+ margin-right: auto !important;
+ overflow-y: hidden !important;
+}`.trim().replace('\n', '');
+}
+/**
+ * @param {WysiwygConfigOptions} options
+ * @return {Object}
+ */
+export function buildForEditor(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: '100%',
selector: '#html-editor',
+ cache_suffix: `?version=${version}`,
content_css: [
window.baseUrl('/dist/styles.css'),
],
branding: false,
- skin: options.darkMode ? 'oxide-dark' : 'oxide',
+ skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
body_class: 'page-content',
browser_spellcheck: true,
relative_urls: false,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
end_container_on_empty_block: true,
+ remove_trailing_brs: false,
statusbar: false,
menubar: false,
paste_data_images: false,
- extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
+ extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',
automatic_uploads: false,
- valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
+ custom_elements: 'doc-root,code-block',
+ valid_children: [
+ '-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]',
+ '+div[pre|img]',
+ '-doc-root[doc-root|#text]',
+ '-li[details]',
+ '+code-block[pre]',
+ '+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div|hr]',
+ ].join(','),
plugins: gatherPlugins(options),
- imagetools_toolbar: 'imageoptions',
contextmenu: false,
- toolbar: buildToolbar(options),
- content_style: `html, body, html.dark-mode {background: ${options.darkMode ? '#222' : '#fff'};} body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}`,
- style_formats,
+ toolbar: getPrimaryToolbar(options),
+ content_style: getContentStyle(options),
+ style_formats: styleFormats,
style_formats_merge: false,
media_alt_source: false,
media_poster: false,
formats,
+ table_style_by_css: true,
+ table_use_colgroups: true,
file_picker_types: 'file image',
- file_picker_callback,
+ color_map: colorMap,
+ file_picker_callback: filePickerCallback,
paste_preprocess(plugin, args) {
- let content = args.content;
+ const {content} = args;
if (content.indexOf('<img src="file://') !== -1) {
args.content = '';
}
},
init_instance_callback(editor) {
- loadCustomHeadContent(editor);
+ addCustomHeadContent(editor.getDoc());
+ },
+ setup(editor) {
+ registerCustomIcons(editor);
+ registerAdditionalToolbars(editor);
+ getSetupCallback(options)(editor);
+ },
+ };
+}
+
+/**
+ * @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],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);
},
- setup: getSetupCallback(options),
};
}
* @property {int} pageId
* @property {Object} translations
* @property {Object} translationMap
- */
\ No newline at end of file
+ */