mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
b5cc0a8e38
Due to manual handling & wrapping of non-block content in details block not taking hr elements into account. For #3963
316 lines
10 KiB
JavaScript
316 lines
10 KiB
JavaScript
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 {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';
|
|
|
|
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: '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 = {
|
|
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'},
|
|
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'}},
|
|
};
|
|
|
|
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') {
|
|
/** @type {EntitySelectorPopup} * */
|
|
const selector = window.$components.first('entity-selector-popup');
|
|
selector.show(entity => {
|
|
callback(entity.link, {
|
|
text: entity.name,
|
|
title: entity.name,
|
|
});
|
|
});
|
|
}
|
|
|
|
if (meta.filetype === 'image') {
|
|
// Show image manager
|
|
/** @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 gatherPlugins(options) {
|
|
const plugins = [
|
|
'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());
|
|
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));
|
|
}
|
|
|
|
/**
|
|
* Fetch custom HTML head content from the parent page head into the editor.
|
|
*/
|
|
function fetchCustomHeadContent() {
|
|
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 '';
|
|
}
|
|
return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
|
|
}
|
|
|
|
/**
|
|
* Setup a serializer filter for <br> tags to ensure they're not rendered
|
|
* within code blocks and that we use newlines there instead.
|
|
* @param {Editor} editor
|
|
*/
|
|
function setupBrFilter(editor) {
|
|
editor.serializer.addNodeFilter('br', nodes => {
|
|
for (const node of nodes) {
|
|
if (node.parent && node.parent.name === 'code') {
|
|
const newline = window.tinymce.html.Node.create('#text');
|
|
newline.value = '\n';
|
|
node.replace(newline);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {WysiwygConfigOptions} options
|
|
* @return {function(Editor)}
|
|
*/
|
|
function getSetupCallback(options) {
|
|
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);
|
|
listenForDragAndPaste(editor, options);
|
|
|
|
editor.on('init', () => {
|
|
editorChange();
|
|
scrollToQueryString(editor);
|
|
window.editor = editor;
|
|
registerShortcuts(editor);
|
|
});
|
|
|
|
editor.on('PreInit', () => {
|
|
setupBrFilter(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
|
|
*/
|
|
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 build(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 ? 'tinymce-5-dark' : 'tinymce-5',
|
|
body_class: 'page-content',
|
|
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,
|
|
paste_data_images: false,
|
|
extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',
|
|
automatic_uploads: false,
|
|
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),
|
|
contextmenu: false,
|
|
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',
|
|
color_map: colorMap,
|
|
file_picker_callback: filePickerCallback,
|
|
paste_preprocess(plugin, args) {
|
|
const {content} = args;
|
|
if (content.indexOf('<img src="file://') !== -1) {
|
|
args.content = '';
|
|
}
|
|
},
|
|
init_instance_callback(editor) {
|
|
const head = editor.getDoc().querySelector('head');
|
|
head.innerHTML += fetchCustomHeadContent();
|
|
},
|
|
setup(editor) {
|
|
registerCustomIcons(editor);
|
|
registerAdditionalToolbars(editor);
|
|
getSetupCallback(options)(editor);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} WysiwygConfigOptions
|
|
* @property {Element} containerElement
|
|
* @property {string} language
|
|
* @property {boolean} darkMode
|
|
* @property {string} textDirection
|
|
* @property {string} drawioUrl
|
|
* @property {int} pageId
|
|
* @property {Object} translations
|
|
* @property {Object} translationMap
|
|
*/
|