"es2021": true
},
"extends": "airbnb-base",
+ "ignorePatterns": ["resources/**/*-stub.js"],
"overrides": [
],
"parserOptions": {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
+ }],
+ "import/prefer-default-export": "off",
+ "no-plusplus": ["error", {
+ "allowForLoopAfterthoughts": true
+ }],
+ "arrow-body-style": "off",
+ "no-restricted-syntax": "off",
+ "no-continue": "off",
+ "no-console": ["warn", {
+ "allow": ["error"]
+ }],
+ "max-len": ["error", {
+ "code": 110,
+ "tabWidth": 4,
+ "ignoreUrls": true,
+ "ignoreComments": false,
+ "ignoreRegExpLiterals": true,
+ "ignoreStrings": true,
+ "ignoreTemplateLiterals": true
+ }],
+ "no-param-reassign": ["error", {
+ "props": false
}]
}
}
import events from './services/events';
-import httpInstance from './services/http';
+import * as httpInstance from './services/http';
import Translations from './services/translations';
import * as components from './services/components';
import * as componentMap from './components';
// Url retrieval function
-window.baseUrl = function(path) {
+window.baseUrl = function baseUrl(path) {
+ let targetPath = path;
let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
if (basePath[basePath.length - 1] === '/') basePath = basePath.slice(0, basePath.length - 1);
- if (path[0] === '/') path = path.slice(1);
- return `${basePath}/${path}`;
+ if (targetPath[0] === '/') targetPath = targetPath.slice(1);
+ return `${basePath}/${targetPath}`;
};
-window.importVersioned = function(moduleName) {
+window.importVersioned = function importVersioned(moduleName) {
const version = document.querySelector('link[href*="/dist/styles.css?version="]').href.split('?version=').pop();
const importPath = window.baseUrl(`dist/${moduleName}.js?version=${version}`);
return import(importPath);
import {provideKeyBindings} from './shortcuts';
import {debounce} from '../services/util';
-import Clipboard from '../services/clipboard';
+import {Clipboard} from '../services/clipboard';
/**
* Initiate the codemirror instance for the markdown editor.
document.execCommand('copy');
document.body.removeChild(tempInput);
}
-
-export default Clipboard;
/**
- * Perform a HTTP GET request.
- * Can easily pass query parameters as the second parameter.
- * @param {String} url
- * @param {Object} params
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @typedef FormattedResponse
+ * @property {Headers} headers
+ * @property {Response} original
+ * @property {Object|String} data
+ * @property {Boolean} redirected
+ * @property {Number} status
+ * @property {string} statusText
+ * @property {string} url
*/
-async function get(url, params = {}) {
- return request(url, {
- method: 'GET',
- params,
- });
-}
/**
- * Perform a HTTP POST request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * Get the content from a fetch response.
+ * Checks the content-type header to determine the format.
+ * @param {Response} response
+ * @returns {Promise<Object|String>}
*/
-async function post(url, data = null) {
- return dataRequest('POST', url, data);
-}
+async function getResponseContent(response) {
+ if (response.status === 204) {
+ return null;
+ }
-/**
- * Perform a HTTP PUT request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
- */
-async function put(url, data = null) {
- return dataRequest('PUT', url, data);
-}
+ const responseContentType = response.headers.get('Content-Type') || '';
+ const subType = responseContentType.split(';')[0].split('/').pop();
-/**
- * Perform a HTTP PATCH request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
- */
-async function patch(url, data = null) {
- return dataRequest('PATCH', url, data);
-}
+ if (subType === 'javascript' || subType === 'json') {
+ return response.json();
+ }
-/**
- * Perform a HTTP DELETE request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
- */
-async function performDelete(url, data = null) {
- return dataRequest('DELETE', url, data);
+ return response.text();
}
-/**
- * Perform a HTTP request to the back-end that includes data in the body.
- * Parses the body to JSON if an object, setting the correct headers.
- * @param {String} method
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
- */
-async function dataRequest(method, url, data = null) {
- const options = {
- method,
- body: data,
- };
+export class HttpError extends Error {
- // Send data as JSON if a plain object
- if (typeof data === 'object' && !(data instanceof FormData)) {
- options.headers = {
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest',
- };
- options.body = JSON.stringify(data);
- }
-
- // Ensure FormData instances are sent over POST
- // Since Laravel does not read multipart/form-data from other types
- // of request. Hence the addition of the magic _method value.
- if (data instanceof FormData && method !== 'post') {
- data.append('_method', method);
- options.method = 'post';
+ constructor(response, content) {
+ super(response.statusText);
+ this.data = content;
+ this.headers = response.headers;
+ this.redirected = response.redirected;
+ this.status = response.status;
+ this.statusText = response.statusText;
+ this.url = response.url;
+ this.original = response;
}
- return request(url, options);
}
/**
* to communicate with the back-end. Parses & formats the response.
* @param {String} url
* @param {Object} options
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @returns {Promise<FormattedResponse>}
*/
async function request(url, options = {}) {
- if (!url.startsWith('http')) {
- url = window.baseUrl(url);
+ let requestUrl = url;
+
+ if (!requestUrl.startsWith('http')) {
+ requestUrl = window.baseUrl(requestUrl);
}
if (options.params) {
- const urlObj = new URL(url);
+ const urlObj = new URL(requestUrl);
for (const paramName of Object.keys(options.params)) {
const value = options.params[paramName];
if (typeof value !== 'undefined' && value !== null) {
urlObj.searchParams.set(paramName, value);
}
}
- url = urlObj.toString();
+ requestUrl = urlObj.toString();
}
const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
- options = {...options, credentials: 'same-origin'};
- options.headers = {
- ...options.headers || {},
+ const requestOptions = {...options, credentials: 'same-origin'};
+ requestOptions.headers = {
+ ...requestOptions.headers || {},
baseURL: window.baseUrl(''),
'X-CSRF-TOKEN': csrfToken,
};
- const response = await fetch(url, options);
+ const response = await fetch(requestUrl, requestOptions);
const content = await getResponseContent(response);
const returnData = {
data: content,
}
/**
- * Get the content from a fetch response.
- * Checks the content-type header to determine the format.
- * @param {Response} response
- * @returns {Promise<Object|String>}
+ * Perform a HTTP request to the back-end that includes data in the body.
+ * Parses the body to JSON if an object, setting the correct headers.
+ * @param {String} method
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
*/
-async function getResponseContent(response) {
- if (response.status === 204) {
- return null;
- }
+async function dataRequest(method, url, data = null) {
+ const options = {
+ method,
+ body: data,
+ };
- const responseContentType = response.headers.get('Content-Type') || '';
- const subType = responseContentType.split(';')[0].split('/').pop();
+ // Send data as JSON if a plain object
+ if (typeof data === 'object' && !(data instanceof FormData)) {
+ options.headers = {
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest',
+ };
+ options.body = JSON.stringify(data);
+ }
- if (subType === 'javascript' || subType === 'json') {
- return await response.json();
+ // Ensure FormData instances are sent over POST
+ // Since Laravel does not read multipart/form-data from other types
+ // of request. Hence the addition of the magic _method value.
+ if (data instanceof FormData && method !== 'post') {
+ data.append('_method', method);
+ options.method = 'post';
}
- return await response.text();
+ return request(url, options);
+}
+
+/**
+ * Perform a HTTP GET request.
+ * Can easily pass query parameters as the second parameter.
+ * @param {String} url
+ * @param {Object} params
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function get(url, params = {}) {
+ return request(url, {
+ method: 'GET',
+ params,
+ });
+}
+
+/**
+ * Perform a HTTP POST request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function post(url, data = null) {
+ return dataRequest('POST', url, data);
}
-class HttpError extends Error {
+/**
+ * Perform a HTTP PUT request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function put(url, data = null) {
+ return dataRequest('PUT', url, data);
+}
- constructor(response, content) {
- super(response.statusText);
- this.data = content;
- this.headers = response.headers;
- this.redirected = response.redirected;
- this.status = response.status;
- this.statusText = response.statusText;
- this.url = response.url;
- this.original = response;
- }
+/**
+ * Perform a HTTP PATCH request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function patch(url, data = null) {
+ return dataRequest('PATCH', url, data);
+}
+/**
+ * Perform a HTTP DELETE request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+async function performDelete(url, data = null) {
+ return dataRequest('DELETE', url, data);
}
-export default {
- get,
- post,
- put,
- patch,
- delete: performDelete,
- HttpError,
-};
+export {performDelete as delete};
*/
export function debounce(func, wait, immediate) {
let timeout;
- return function() {
- const context = this; const
- args = arguments;
- const later = function() {
+ return function debouncedWrapper(...args) {
+ const context = this;
+ const later = function debouncedTimeout() {
timeout = null;
if (!immediate) func.apply(context, args);
};
* @returns {string}
*/
export function uniqueId() {
+ // eslint-disable-next-line no-bitwise
const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
}
import {getPlugin as getDetailsPlugin} from './plugins-details';
import {getPlugin as getTasklistPlugin} from './plugins-tasklist';
-const style_formats = [
+const styleFormats = [
{title: 'Large Header', format: 'h2', preview: 'color: blue;'},
{title: 'Medium Header', format: 'h3'},
{title: 'Small Header', format: 'h4'},
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
};
-const color_map = [
+const colorMap = [
'#BFEDD2', '',
'#FBEEB8', '',
'#F8CAC6', '',
'#ffffff', '',
];
-function file_picker_callback(callback, value, meta) {
+function filePickerCallback(callback, value, meta) {
// field_name, url, type, win
if (meta.filetype === 'file') {
/** @type {EntitySelectorPopup} * */
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('about', getAboutPlugin(options));
- window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
- window.tinymce.PluginManager.add('tasklist', getTasklistPlugin(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));
editor.serializer.addNodeFilter('br', nodes => {
for (const node of nodes) {
if (node.parent && node.parent.name === 'code') {
- const newline = tinymce.html.Node.create('#text');
+ const newline = window.tinymce.html.Node.create('#text');
newline.value = '\n';
node.replace(newline);
}
* @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);
listenForDragAndPaste(editor, options);
setupBrFilter(editor);
});
- function editorChange() {
- if (options.darkMode) {
- editor.contentDocument.documentElement.classList.add('dark-mode');
- }
- window.$events.emit('editor-html-change', '');
- }
-
// Custom handler hook
window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
contextmenu: false,
toolbar: getPrimaryToolbar(options),
content_style: getContentStyle(options),
- style_formats,
+ style_formats: styleFormats,
style_formats_merge: false,
media_alt_source: false,
media_poster: false,
table_style_by_css: true,
table_use_colgroups: true,
file_picker_types: 'file image',
- color_map,
- file_picker_callback,
+ color_map: colorMap,
+ file_picker_callback: filePickerCallback,
paste_preprocess(plugin, args) {
const {content} = args;
if (content.indexOf('<img src="file://') !== -1) {
},
setup(editor) {
registerCustomIcons(editor);
- registerAdditionalToolbars(editor, options);
+ registerAdditionalToolbars(editor);
getSetupCallback(options)(editor);
},
};
-import Clipboard from '../services/clipboard';
+import {Clipboard} from '../services/clipboard';
let wrap;
let draggedContentEditable;
return node && !!(node.textContent || node.innerText);
}
+/**
+ * Upload an image file to the server
+ * @param {File} file
+ * @param {int} pageId
+ */
+async function uploadImageFile(file, pageId) {
+ if (file === null || file.type.indexOf('image') !== 0) {
+ throw new Error('Not an image file');
+ }
+
+ const remoteFilename = file.name || `image-${Date.now()}.png`;
+ const formData = new FormData();
+ formData.append('file', file, remoteFilename);
+ formData.append('uploaded_to', pageId);
+
+ const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
+ return resp.data;
+}
+
/**
* Handle pasting images from clipboard.
* @param {Editor} editor
}).catch(err => {
editor.dom.remove(id);
window.$events.emit('error', options.translations.imageUploadErrorText);
- console.log(err);
+ console.error(err);
});
}, 10);
}
}
-/**
- * Upload an image file to the server
- * @param {File} file
- * @param {int} pageId
- */
-async function uploadImageFile(file, pageId) {
- if (file === null || file.type.indexOf('image') !== 0) {
- throw new Error('Not an image file');
- }
-
- const remoteFilename = file.name || `image-${Date.now()}.png`;
- const formData = new FormData();
- formData.append('file', file, remoteFilename);
- formData.append('uploaded_to', pageId);
-
- const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
- return resp.data;
-}
-
/**
* @param {Editor} editor
- * @param {WysiwygConfigOptions} options
*/
-function dragStart(editor, options) {
+function dragStart(editor) {
const node = editor.selection.getNode();
if (node.nodeName === 'IMG') {
*/
function drop(editor, options, event) {
const {dom} = editor;
- const rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
+ const rng = window.tinymce.dom.RangeUtils.getCaretRangeFromPoint(
+ event.clientX,
+ event.clientY,
+ editor.getDoc(),
+ );
// Template insertion
const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');
* @param {WysiwygConfigOptions} options
*/
export function listenForDragAndPaste(editor, options) {
- editor.on('dragstart', () => dragStart(editor, options));
+ editor.on('dragstart', () => dragStart(editor));
editor.on('drop', event => drop(editor, options, event));
editor.on('paste', event => paste(editor, options, event));
}
const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
const renderEditor = Code => {
this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());
- setTimeout(() => this.style.height = null, 12);
+ setTimeout(() => {
+ this.style.height = null;
+ }, 12);
};
window.importVersioned('code').then(Code => {
/**
* @param {Editor} editor
- * @param {String} url
*/
-function register(editor, url) {
+function register(editor) {
editor.ui.registry.addIcon('codeblock', '<svg width="24" height="24"><path d="M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm1 2v14h14V5Z"/><path d="M11.103 15.423c.277.277.277.738 0 .922a.692.692 0 0 1-1.106 0l-4.057-3.78a.738.738 0 0 1 0-1.107l4.057-3.872c.276-.277.83-.277 1.106 0a.724.724 0 0 1 0 1.014L7.6 12.012ZM12.897 8.577c-.245-.312-.2-.675.08-.955.28-.281.727-.27 1.027.033l4.057 3.78a.738.738 0 0 1 0 1.107l-4.057 3.872c-.277.277-.83.277-1.107 0a.724.724 0 0 1 0-1.014l3.504-3.412z"/></svg>');
editor.ui.registry.addButton('codeeditor', {
}
});
- editor.on('dblclick', event => {
+ editor.on('dblclick', () => {
const selectedNode = editor.selection.getNode();
if (elemIsCodeBlock(selectedNode)) {
showPopupForCodeBlock(editor, selectedNode);
editor.on('PreInit', () => {
editor.parser.addNodeFilter('pre', elms => {
for (const el of elms) {
- const wrapper = tinymce.html.Node.create('code-block', {
+ const wrapper = window.tinymce.html.Node.create('code-block', {
contenteditable: 'false',
});
}
/**
- * @param {WysiwygConfigOptions} options
* @return {register}
*/
-export function getPlugin(options) {
+export function getPlugin() {
return register;
}
}, 'drawio');
}
-function showDrawingEditor(mceEditor, selectedNode = null) {
- pageEditor = mceEditor;
- currentNode = selectedNode;
- DrawIO.show(options.drawioUrl, drawingInit, updateContent);
-}
-
async function updateContent(pngData) {
const id = `image-${Math.random().toString(16).slice(2)}`;
const loadingImage = window.baseUrl('/loading.gif');
} else {
window.$events.emit('error', options.translations.imageUploadErrorText);
}
- console.log(error);
+ console.error(error);
};
// Handle updating an existing image
return DrawIO.load(drawingId);
}
+function showDrawingEditor(mceEditor, selectedNode = null) {
+ pageEditor = mceEditor;
+ currentNode = selectedNode;
+ DrawIO.show(options.drawioUrl, drawingInit, updateContent);
+}
+
+/**
+ * @param {Editor} editor
+ */
+function register(editor) {
+ editor.addCommand('drawio', () => {
+ const selectedNode = editor.selection.getNode();
+ showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null);
+ });
+
+ editor.ui.registry.addIcon('diagram', `<svg width="24" height="24" fill="${options.darkMode ? '#BBB' : '#000000'}" xmlns="http://www.w3.org/2000/svg"><path d="M20.716 7.639V2.845h-4.794v1.598h-7.99V2.845H3.138v4.794h1.598v7.99H3.138v4.794h4.794v-1.598h7.99v1.598h4.794v-4.794h-1.598v-7.99zM4.736 4.443h1.598V6.04H4.736zm1.598 14.382H4.736v-1.598h1.598zm9.588-1.598h-7.99v-1.598H6.334v-7.99h1.598V6.04h7.99v1.598h1.598v7.99h-1.598zm3.196 1.598H17.52v-1.598h1.598zM17.52 6.04V4.443h1.598V6.04zm-4.21 7.19h-2.79l-.582 1.599H8.643l2.717-7.191h1.119l2.724 7.19h-1.302zm-2.43-1.006h2.086l-1.039-3.06z"/></svg>`);
+
+ editor.ui.registry.addSplitButton('drawio', {
+ tooltip: 'Insert/edit drawing',
+ icon: 'diagram',
+ onAction() {
+ editor.execCommand('drawio');
+ // Hack to de-focus the tinymce editor toolbar
+ window.document.body.dispatchEvent(new Event('mousedown', {bubbles: true}));
+ },
+ fetch(callback) {
+ callback([
+ {
+ type: 'choiceitem',
+ text: 'Drawing manager',
+ value: 'drawing-manager',
+ },
+ ]);
+ },
+ onItemAction(api, value) {
+ if (value === 'drawing-manager') {
+ const selectedNode = editor.selection.getNode();
+ showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);
+ }
+ },
+ });
+
+ editor.on('dblclick', () => {
+ const selectedNode = editor.selection.getNode();
+ if (!isDrawing(selectedNode)) return;
+ showDrawingEditor(editor, selectedNode);
+ });
+
+ editor.on('SetContent', () => {
+ const drawings = editor.dom.select('body > div[drawio-diagram]');
+ if (!drawings.length) return;
+
+ editor.undoManager.transact(() => {
+ for (const drawing of drawings) {
+ drawing.setAttribute('contenteditable', 'false');
+ }
+ });
+ });
+}
+
/**
*
* @param {WysiwygConfigOptions} providedOptions
*/
export function getPlugin(providedOptions) {
options = providedOptions;
- return function(editor, url) {
- editor.addCommand('drawio', () => {
- const selectedNode = editor.selection.getNode();
- showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null);
- });
-
- editor.ui.registry.addIcon('diagram', `<svg width="24" height="24" fill="${options.darkMode ? '#BBB' : '#000000'}" xmlns="http://www.w3.org/2000/svg"><path d="M20.716 7.639V2.845h-4.794v1.598h-7.99V2.845H3.138v4.794h1.598v7.99H3.138v4.794h4.794v-1.598h7.99v1.598h4.794v-4.794h-1.598v-7.99zM4.736 4.443h1.598V6.04H4.736zm1.598 14.382H4.736v-1.598h1.598zm9.588-1.598h-7.99v-1.598H6.334v-7.99h1.598V6.04h7.99v1.598h1.598v7.99h-1.598zm3.196 1.598H17.52v-1.598h1.598zM17.52 6.04V4.443h1.598V6.04zm-4.21 7.19h-2.79l-.582 1.599H8.643l2.717-7.191h1.119l2.724 7.19h-1.302zm-2.43-1.006h2.086l-1.039-3.06z"/></svg>`);
-
- editor.ui.registry.addSplitButton('drawio', {
- tooltip: 'Insert/edit drawing',
- icon: 'diagram',
- onAction() {
- editor.execCommand('drawio');
- // Hack to de-focus the tinymce editor toolbar
- window.document.body.dispatchEvent(new Event('mousedown', {bubbles: true}));
- },
- fetch(callback) {
- callback([
- {
- type: 'choiceitem',
- text: 'Drawing manager',
- value: 'drawing-manager',
- },
- ]);
- },
- onItemAction(api, value) {
- if (value === 'drawing-manager') {
- const selectedNode = editor.selection.getNode();
- showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);
- }
- },
- });
-
- editor.on('dblclick', event => {
- const selectedNode = editor.selection.getNode();
- if (!isDrawing(selectedNode)) return;
- showDrawingEditor(editor, selectedNode);
- });
-
- editor.on('SetContent', () => {
- const drawings = editor.dom.select('body > div[drawio-diagram]');
- if (!drawings.length) return;
-
- editor.undoManager.transact(() => {
- for (const drawing of drawings) {
- drawing.setAttribute('contenteditable', 'false');
- }
- });
- });
- };
+ return register;
}
/**
* @param {Editor} editor
- * @param {String} url
*/
-function register(editor, url) {
+function register(editor) {
const aboutDialog = {
title: 'About the WYSIWYG Editor',
url: window.baseUrl('/help/wysiwyg'),
icon: 'help',
tooltip: 'About the editor',
onAction() {
- tinymce.activeEditor.windowManager.openUrl(aboutDialog);
+ window.tinymce.activeEditor.windowManager.openUrl(aboutDialog);
},
});
}
/**
- * @param {WysiwygConfigOptions} options
* @return {register}
*/
-export function getPlugin(options) {
+export function getPlugin() {
return register;
}
/**
* @param {Editor} editor
- * @param {String} url
*/
-function register(editor, url) {
+function register(editor) {
editor.addCommand('InsertHorizontalRule', () => {
const hrElem = document.createElement('hr');
const cNode = editor.selection.getNode();
}
/**
- * @param {WysiwygConfigOptions} options
* @return {register}
*/
-export function getPlugin(options) {
+export function getPlugin() {
return register;
}
-/**
- * @param {Editor} editor
- * @param {String} url
- */
import {blockElementTypes} from './util';
-function register(editor, url) {
- editor.ui.registry.addIcon('details', '<svg width="24" height="24"><path d="M8.2 9a.5.5 0 0 0-.4.8l4 5.6a.5.5 0 0 0 .8 0l4-5.6a.5.5 0 0 0-.4-.8ZM20.122 18.151h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7zM20.122 3.042h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7z"/></svg>');
- editor.ui.registry.addIcon('togglefold', '<svg height="24" width="24"><path d="M8.12 19.3c.39.39 1.02.39 1.41 0L12 16.83l2.47 2.47c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-3.17-3.17c-.39-.39-1.02-.39-1.41 0l-3.17 3.17c-.4.38-.4 1.02-.01 1.41zm7.76-14.6c-.39-.39-1.02-.39-1.41 0L12 7.17 9.53 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.03 0 1.42l3.17 3.17c.39.39 1.02.39 1.41 0l3.17-3.17c.4-.39.4-1.03.01-1.42z"/></svg>');
- editor.ui.registry.addIcon('togglelabel', '<svg height="18" width="18" viewBox="0 0 24 24"><path d="M21.41,11.41l-8.83-8.83C12.21,2.21,11.7,2,11.17,2H4C2.9,2,2,2.9,2,4v7.17c0,0.53,0.21,1.04,0.59,1.41l8.83,8.83 c0.78,0.78,2.05,0.78,2.83,0l7.17-7.17C22.2,13.46,22.2,12.2,21.41,11.41z M6.5,8C5.67,8,5,7.33,5,6.5S5.67,5,6.5,5S8,5.67,8,6.5 S7.33,8,6.5,8z"/></svg>');
-
- editor.ui.registry.addButton('details', {
- icon: 'details',
- tooltip: 'Insert collapsible block',
- onAction() {
- editor.execCommand('InsertDetailsBlock');
- },
- });
-
- editor.ui.registry.addButton('removedetails', {
- icon: 'table-delete-table',
- tooltip: 'Unwrap',
- onAction() {
- unwrapDetailsInSelection(editor);
- },
- });
-
- editor.ui.registry.addButton('editdetials', {
- icon: 'togglelabel',
- tooltip: 'Edit label',
- onAction() {
- showDetailLabelEditWindow(editor);
- },
- });
-
- editor.on('dblclick', event => {
- if (!getSelectedDetailsBlock(editor) || event.target.closest('doc-root')) return;
- showDetailLabelEditWindow(editor);
- });
-
- editor.ui.registry.addButton('toggledetails', {
- icon: 'togglefold',
- tooltip: 'Toggle open/closed',
- onAction() {
- const details = getSelectedDetailsBlock(editor);
- details.toggleAttribute('open');
- editor.focus();
- },
- });
-
- editor.addCommand('InsertDetailsBlock', () => {
- let content = editor.selection.getContent({format: 'html'});
- const details = document.createElement('details');
- const summary = document.createElement('summary');
- const id = `details-${Date.now()}`;
- details.setAttribute('data-id', id);
- details.appendChild(summary);
-
- if (!content) {
- content = '<p><br></p>';
- }
-
- details.innerHTML += content;
- editor.insertContent(details.outerHTML);
- editor.focus();
-
- const domDetails = editor.dom.select(`[data-id="${id}"]`)[0] || null;
- if (domDetails) {
- const firstChild = domDetails.querySelector('doc-root > *');
- if (firstChild) {
- firstChild.focus();
- }
- domDetails.removeAttribute('data-id');
- }
- });
-
- editor.ui.registry.addContextToolbar('details', {
- predicate(node) {
- return node.nodeName.toLowerCase() === 'details';
- },
- items: 'editdetials toggledetails removedetails',
- position: 'node',
- scope: 'node',
- });
-
- editor.on('PreInit', () => {
- setupElementFilters(editor);
- });
-}
-
-/**
- * @param {Editor} editor
- */
-function showDetailLabelEditWindow(editor) {
- const details = getSelectedDetailsBlock(editor);
- const dialog = editor.windowManager.open(detailsDialog(editor));
- dialog.setData({summary: getSummaryTextFromDetails(details)});
-}
-
/**
* @param {Editor} editor
*/
return editor.selection.getNode().closest('details');
}
-/**
- * @param {Element} element
- */
-function getSummaryTextFromDetails(element) {
- const summary = element.querySelector('summary');
- if (!summary) {
- return '';
- }
- return summary.textContent;
+function setSummary(editor, summaryContent) {
+ const details = getSelectedDetailsBlock(editor);
+ if (!details) return;
+
+ editor.undoManager.transact(() => {
+ let summary = details.querySelector('summary');
+ if (!summary) {
+ summary = document.createElement('summary');
+ details.prepend(summary);
+ }
+ summary.textContent = summaryContent;
+ });
}
/**
};
}
-function setSummary(editor, summaryContent) {
- const details = getSelectedDetailsBlock(editor);
- if (!details) return;
+/**
+ * @param {Element} element
+ */
+function getSummaryTextFromDetails(element) {
+ const summary = element.querySelector('summary');
+ if (!summary) {
+ return '';
+ }
+ return summary.textContent;
+}
- editor.undoManager.transact(() => {
- let summary = details.querySelector('summary');
- if (!summary) {
- summary = document.createElement('summary');
- details.prepend(summary);
- }
- summary.textContent = summaryContent;
- });
+/**
+ * @param {Editor} editor
+ */
+function showDetailLabelEditWindow(editor) {
+ const details = getSelectedDetailsBlock(editor);
+ const dialog = editor.windowManager.open(detailsDialog(editor));
+ dialog.setData({summary: getSummaryTextFromDetails(details)});
}
/**
}
/**
- * @param {Editor} editor
+ * @param {tinymce.html.Node} detailsEl
*/
-function setupElementFilters(editor) {
- editor.parser.addNodeFilter('details', elms => {
- for (const el of elms) {
- ensureDetailsWrappedInEditable(el);
- }
- });
-
- editor.serializer.addNodeFilter('details', elms => {
- for (const el of elms) {
- unwrapDetailsEditable(el);
- el.attr('open', null);
+function unwrapDetailsEditable(detailsEl) {
+ detailsEl.attr('contenteditable', null);
+ let madeUnwrap = false;
+ for (const child of detailsEl.children()) {
+ if (child.name === 'doc-root') {
+ child.unwrap();
+ madeUnwrap = true;
}
- });
+ }
- editor.serializer.addNodeFilter('doc-root', elms => {
- for (const el of elms) {
- el.unwrap();
- }
- });
+ if (madeUnwrap) {
+ unwrapDetailsEditable(detailsEl);
+ }
}
/**
unwrapDetailsEditable(detailsEl);
detailsEl.attr('contenteditable', 'false');
- const rootWrap = tinymce.html.Node.create('doc-root', {contenteditable: 'true'});
+ const rootWrap = window.tinymce.html.Node.create('doc-root', {contenteditable: 'true'});
let previousBlockWrap = null;
for (const child of detailsEl.children()) {
if (!isBlock) {
if (!previousBlockWrap) {
- previousBlockWrap = tinymce.html.Node.create('p');
+ previousBlockWrap = window.tinymce.html.Node.create('p');
rootWrap.append(previousBlockWrap);
}
previousBlockWrap.append(child);
}
/**
- * @param {tinymce.html.Node} detailsEl
+ * @param {Editor} editor
*/
-function unwrapDetailsEditable(detailsEl) {
- detailsEl.attr('contenteditable', null);
- let madeUnwrap = false;
- for (const child of detailsEl.children()) {
- if (child.name === 'doc-root') {
- child.unwrap();
- madeUnwrap = true;
+function setupElementFilters(editor) {
+ editor.parser.addNodeFilter('details', elms => {
+ for (const el of elms) {
+ ensureDetailsWrappedInEditable(el);
}
- }
+ });
- if (madeUnwrap) {
- unwrapDetailsEditable(detailsEl);
- }
+ editor.serializer.addNodeFilter('details', elms => {
+ for (const el of elms) {
+ unwrapDetailsEditable(el);
+ el.attr('open', null);
+ }
+ });
+
+ editor.serializer.addNodeFilter('doc-root', elms => {
+ for (const el of elms) {
+ el.unwrap();
+ }
+ });
+}
+
+/**
+ * @param {Editor} editor
+ */
+function register(editor) {
+ editor.ui.registry.addIcon('details', '<svg width="24" height="24"><path d="M8.2 9a.5.5 0 0 0-.4.8l4 5.6a.5.5 0 0 0 .8 0l4-5.6a.5.5 0 0 0-.4-.8ZM20.122 18.151h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7zM20.122 3.042h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7z"/></svg>');
+ editor.ui.registry.addIcon('togglefold', '<svg height="24" width="24"><path d="M8.12 19.3c.39.39 1.02.39 1.41 0L12 16.83l2.47 2.47c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-3.17-3.17c-.39-.39-1.02-.39-1.41 0l-3.17 3.17c-.4.38-.4 1.02-.01 1.41zm7.76-14.6c-.39-.39-1.02-.39-1.41 0L12 7.17 9.53 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.03 0 1.42l3.17 3.17c.39.39 1.02.39 1.41 0l3.17-3.17c.4-.39.4-1.03.01-1.42z"/></svg>');
+ editor.ui.registry.addIcon('togglelabel', '<svg height="18" width="18" viewBox="0 0 24 24"><path d="M21.41,11.41l-8.83-8.83C12.21,2.21,11.7,2,11.17,2H4C2.9,2,2,2.9,2,4v7.17c0,0.53,0.21,1.04,0.59,1.41l8.83,8.83 c0.78,0.78,2.05,0.78,2.83,0l7.17-7.17C22.2,13.46,22.2,12.2,21.41,11.41z M6.5,8C5.67,8,5,7.33,5,6.5S5.67,5,6.5,5S8,5.67,8,6.5 S7.33,8,6.5,8z"/></svg>');
+
+ editor.ui.registry.addButton('details', {
+ icon: 'details',
+ tooltip: 'Insert collapsible block',
+ onAction() {
+ editor.execCommand('InsertDetailsBlock');
+ },
+ });
+
+ editor.ui.registry.addButton('removedetails', {
+ icon: 'table-delete-table',
+ tooltip: 'Unwrap',
+ onAction() {
+ unwrapDetailsInSelection(editor);
+ },
+ });
+
+ editor.ui.registry.addButton('editdetials', {
+ icon: 'togglelabel',
+ tooltip: 'Edit label',
+ onAction() {
+ showDetailLabelEditWindow(editor);
+ },
+ });
+
+ editor.on('dblclick', event => {
+ if (!getSelectedDetailsBlock(editor) || event.target.closest('doc-root')) return;
+ showDetailLabelEditWindow(editor);
+ });
+
+ editor.ui.registry.addButton('toggledetails', {
+ icon: 'togglefold',
+ tooltip: 'Toggle open/closed',
+ onAction() {
+ const details = getSelectedDetailsBlock(editor);
+ details.toggleAttribute('open');
+ editor.focus();
+ },
+ });
+
+ editor.addCommand('InsertDetailsBlock', () => {
+ let content = editor.selection.getContent({format: 'html'});
+ const details = document.createElement('details');
+ const summary = document.createElement('summary');
+ const id = `details-${Date.now()}`;
+ details.setAttribute('data-id', id);
+ details.appendChild(summary);
+
+ if (!content) {
+ content = '<p><br></p>';
+ }
+
+ details.innerHTML += content;
+ editor.insertContent(details.outerHTML);
+ editor.focus();
+
+ const domDetails = editor.dom.select(`[data-id="${id}"]`)[0] || null;
+ if (domDetails) {
+ const firstChild = domDetails.querySelector('doc-root > *');
+ if (firstChild) {
+ firstChild.focus();
+ }
+ domDetails.removeAttribute('data-id');
+ }
+ });
+
+ editor.ui.registry.addContextToolbar('details', {
+ predicate(node) {
+ return node.nodeName.toLowerCase() === 'details';
+ },
+ items: 'editdetials toggledetails removedetails',
+ position: 'node',
+ scope: 'node',
+ });
+
+ editor.on('PreInit', () => {
+ setupElementFilters(editor);
+ });
}
/**
- * @param {WysiwygConfigOptions} options
* @return {register}
*/
-export function getPlugin(options) {
+export function getPlugin() {
return register;
}
/**
* @param {Editor} editor
- * @param {String} url
*/
-function register(editor, url) {
+function register(editor) {
// Custom Image picker button
editor.ui.registry.addButton('imagemanager-insert', {
title: 'Insert image',
}
/**
- * @param {WysiwygConfigOptions} options
* @return {register}
*/
-export function getPlugin(options) {
+export function getPlugin() {
return register;
}
+/**
+ * @param {Element} element
+ * @return {boolean}
+ */
+function elementWithinTaskList(element) {
+ const listEl = element.closest('li');
+ return listEl && listEl.parentNode.nodeName === 'UL' && listEl.classList.contains('task-list-item');
+}
+
+/**
+ * @param {MouseEvent} event
+ * @param {Element} clickedEl
+ * @param {Editor} editor
+ */
+function handleTaskListItemClick(event, clickedEl, editor) {
+ const bounds = clickedEl.getBoundingClientRect();
+ const withinBounds = event.clientX <= bounds.right
+ && event.clientX >= bounds.left
+ && event.clientY >= bounds.top
+ && event.clientY <= bounds.bottom;
+
+ // Outside of the task list item bounds mean we're probably clicking the pseudo-element.
+ if (!withinBounds) {
+ editor.undoManager.transact(() => {
+ if (clickedEl.hasAttribute('checked')) {
+ clickedEl.removeAttribute('checked');
+ } else {
+ clickedEl.setAttribute('checked', 'checked');
+ }
+ });
+ }
+}
+
+/**
+ * @param {AstNode} node
+ */
+function parseTaskListNode(node) {
+ // Force task list item class
+ node.attr('class', 'task-list-item');
+
+ // Copy checkbox status and remove checkbox within editor
+ for (const child of node.children()) {
+ if (child.name === 'input') {
+ if (child.attr('checked') === 'checked') {
+ node.attr('checked', 'checked');
+ }
+ child.remove();
+ }
+ }
+}
+
+/**
+ * @param {AstNode} node
+ */
+function serializeTaskListNode(node) {
+ // Get checked status and clean it from list node
+ const isChecked = node.attr('checked') === 'checked';
+ node.attr('checked', null);
+
+ const inputAttrs = {type: 'checkbox', disabled: 'disabled'};
+ if (isChecked) {
+ inputAttrs.checked = 'checked';
+ }
+
+ // Create & insert checkbox input element
+ const checkbox = window.tinymce.html.Node.create('input', inputAttrs);
+ checkbox.shortEnded = true;
+
+ if (node.firstChild) {
+ node.insert(checkbox, node.firstChild, true);
+ } else {
+ node.append(checkbox);
+ }
+}
+
/**
* @param {Editor} editor
- * @param {String} url
*/
-function register(editor, url) {
+function register(editor) {
// Tasklist UI buttons
editor.ui.registry.addIcon('tasklist', '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22,8c0-0.55-0.45-1-1-1h-7c-0.55,0-1,0.45-1,1s0.45,1,1,1h7C21.55,9,22,8.55,22,8z M13,16c0,0.55,0.45,1,1,1h7 c0.55,0,1-0.45,1-1c0-0.55-0.45-1-1-1h-7C13.45,15,13,15.45,13,16z M10.47,4.63c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25 c-0.39,0.39-1.02,0.39-1.42,0L2.7,8.16c-0.39-0.39-0.39-1.02,0-1.41c0.39-0.39,1.02-0.39,1.41,0l1.42,1.42l3.54-3.54 C9.45,4.25,10.09,4.25,10.47,4.63z M10.48,12.64c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25c-0.39,0.39-1.02,0.39-1.42,0L2.7,16.16 c-0.39-0.39-0.39-1.02,0-1.41s1.02-0.39,1.41,0l1.42,1.42l3.54-3.54C9.45,12.25,10.09,12.25,10.48,12.64L10.48,12.64z"/></svg>');
editor.ui.registry.addToggleButton('tasklist', {
// Tweak existing bullet list button active state to not be active
// when we're in a task list.
const existingBullListButton = editor.ui.registry.getAll().buttons.bullist;
- existingBullListButton.onSetup = function(api) {
+ existingBullListButton.onSetup = function customBullListOnSetup(api) {
editor.on('NodeChange', event => {
const parentList = event.parents.find(el => el.nodeName === 'LI');
const inTaskList = parentList && parentList.classList.contains('task-list-item');
api.setActive(Boolean(inUlList && !inTaskList));
});
};
- existingBullListButton.onAction = function() {
+ existingBullListButton.onAction = function customBullListOnAction() {
// Cheeky hack to prevent list toggle action treating tasklists as normal
// unordered lists which would unwrap the list on toggle from tasklist to bullet list.
// Instead we quickly jump through an ordered list first if we're within a tasklist.
};
// Tweak existing number list to not allow classes on child items
const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;
- existingNumListButton.onAction = function() {
+ existingNumListButton.onAction = function customNumListButtonOnAction() {
editor.execCommand('InsertOrderedList', null, {
'list-item-attributes': {class: null},
});
}
/**
- * @param {Element} element
- * @return {boolean}
- */
-function elementWithinTaskList(element) {
- const listEl = element.closest('li');
- return listEl && listEl.parentNode.nodeName === 'UL' && listEl.classList.contains('task-list-item');
-}
-
-/**
- * @param {MouseEvent} event
- * @param {Element} clickedEl
- * @param {Editor} editor
- */
-function handleTaskListItemClick(event, clickedEl, editor) {
- const bounds = clickedEl.getBoundingClientRect();
- const withinBounds = event.clientX <= bounds.right
- && event.clientX >= bounds.left
- && event.clientY >= bounds.top
- && event.clientY <= bounds.bottom;
-
- // Outside of the task list item bounds mean we're probably clicking the pseudo-element.
- if (!withinBounds) {
- editor.undoManager.transact(() => {
- if (clickedEl.hasAttribute('checked')) {
- clickedEl.removeAttribute('checked');
- } else {
- clickedEl.setAttribute('checked', 'checked');
- }
- });
- }
-}
-
-/**
- * @param {AstNode} node
- */
-function parseTaskListNode(node) {
- // Force task list item class
- node.attr('class', 'task-list-item');
-
- // Copy checkbox status and remove checkbox within editor
- for (const child of node.children()) {
- if (child.name === 'input') {
- if (child.attr('checked') === 'checked') {
- node.attr('checked', 'checked');
- }
- child.remove();
- }
- }
-}
-
-/**
- * @param {AstNode} node
- */
-function serializeTaskListNode(node) {
- // Get checked status and clean it from list node
- const isChecked = node.attr('checked') === 'checked';
- node.attr('checked', null);
-
- const inputAttrs = {type: 'checkbox', disabled: 'disabled'};
- if (isChecked) {
- inputAttrs.checked = 'checked';
- }
-
- // Create & insert checkbox input element
- const checkbox = tinymce.html.Node.create('input', inputAttrs);
- checkbox.shortEnded = true;
- node.firstChild ? node.insert(checkbox, node.firstChild, true) : node.append(checkbox);
-}
-
-/**
- * @param {WysiwygConfigOptions} options
* @return {register}
*/
-export function getPlugin(options) {
+export function getPlugin() {
return register;
}
-/**
- * Scroll to a section dictated by the current URL query string, if present.
- * Used when directly editing a specific section of the page.
- * @param {Editor} editor
- */
-export function scrollToQueryString(editor) {
- const queryParams = (new URL(window.location)).searchParams;
- const scrollId = queryParams.get('content-id');
- if (scrollId) {
- scrollToText(editor, scrollId);
- }
-}
-
/**
* @param {Editor} editor
* @param {String} scrollId
editor.selection.collapse(false);
editor.focus();
}
+
+/**
+ * Scroll to a section dictated by the current URL query string, if present.
+ * Used when directly editing a specific section of the page.
+ * @param {Editor} editor
+ */
+export function scrollToQueryString(editor) {
+ const queryParams = (new URL(window.location)).searchParams;
+ const scrollId = queryParams.get('content-id');
+ if (scrollId) {
+ scrollToText(editor, scrollId);
+ }
+}
const callout = selectedNode ? selectedNode.closest('.callout') : null;
const formats = ['info', 'success', 'warning', 'danger'];
- const currentFormatIndex = formats.findIndex(format => callout && callout.classList.contains(format));
+ const currentFormatIndex = formats.findIndex(format => {
+ return callout && callout.classList.contains(format);
+ });
const newFormatIndex = (currentFormatIndex + 1) % formats.length;
const newFormat = formats[newFormatIndex];
/**
* @param {Editor} editor
- * @param {WysiwygConfigOptions} options
*/
-export function registerAdditionalToolbars(editor, options) {
+export function registerAdditionalToolbars(editor) {
registerPrimaryToolbarGroups(editor);
registerLinkContextToolbar(editor);
registerImageContextToolbar(editor);