ESLINT: Started inital pass at addressing issues

This commit is contained in:
Dan Brown 2023-04-19 10:46:13 +01:00
parent e711290d8b
commit 0519e58fbf
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
18 changed files with 586 additions and 554 deletions

View File

@ -58,6 +58,7 @@
"es2021": true "es2021": true
}, },
"extends": "airbnb-base", "extends": "airbnb-base",
"ignorePatterns": ["resources/**/*-stub.js"],
"overrides": [ "overrides": [
], ],
"parserOptions": { "parserOptions": {
@ -76,6 +77,28 @@
"anonymous": "never", "anonymous": "never",
"named": "never", "named": "never",
"asyncArrow": "always" "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
}] }]
} }
} }

View File

@ -1,19 +1,20 @@
import events from './services/events'; import events from './services/events';
import httpInstance from './services/http'; import * as httpInstance from './services/http';
import Translations from './services/translations'; import Translations from './services/translations';
import * as components from './services/components'; import * as components from './services/components';
import * as componentMap from './components'; import * as componentMap from './components';
// Url retrieval function // 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'); let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
if (basePath[basePath.length - 1] === '/') basePath = basePath.slice(0, basePath.length - 1); if (basePath[basePath.length - 1] === '/') basePath = basePath.slice(0, basePath.length - 1);
if (path[0] === '/') path = path.slice(1); if (targetPath[0] === '/') targetPath = targetPath.slice(1);
return `${basePath}/${path}`; 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 version = document.querySelector('link[href*="/dist/styles.css?version="]').href.split('?version=').pop();
const importPath = window.baseUrl(`dist/${moduleName}.js?version=${version}`); const importPath = window.baseUrl(`dist/${moduleName}.js?version=${version}`);
return import(importPath); return import(importPath);

View File

@ -1,6 +1,6 @@
import {provideKeyBindings} from './shortcuts'; import {provideKeyBindings} from './shortcuts';
import {debounce} from '../services/util'; import {debounce} from '../services/util';
import Clipboard from '../services/clipboard'; import {Clipboard} from '../services/clipboard';
/** /**
* Initiate the codemirror instance for the markdown editor. * Initiate the codemirror instance for the markdown editor.

View File

@ -66,5 +66,3 @@ export async function copyTextToClipboard(text) {
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(tempInput); document.body.removeChild(tempInput);
} }
export default Clipboard;

View File

@ -1,55 +1,100 @@
/** /**
* Perform a HTTP GET request. * @typedef FormattedResponse
* Can easily pass query parameters as the second parameter. * @property {Headers} headers
* @param {String} url * @property {Response} original
* @param {Object} params * @property {Object|String} data
* @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} * @property {Boolean} redirected
* @property {Number} status
* @property {string} statusText
* @property {string} url
*/ */
async function get(url, params = {}) {
return request(url, { /**
method: 'GET', * Get the content from a fetch response.
params, * Checks the content-type header to determine the format.
}); * @param {Response} response
* @returns {Promise<Object|String>}
*/
async function getResponseContent(response) {
if (response.status === 204) {
return null;
}
const responseContentType = response.headers.get('Content-Type') || '';
const subType = responseContentType.split(';')[0].split('/').pop();
if (subType === 'javascript' || subType === 'json') {
return response.json();
}
return response.text();
}
export class HttpError extends Error {
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 POST request. * Create a new HTTP request, setting the required CSRF information
* to communicate with the back-end. Parses & formats the response.
* @param {String} url * @param {String} url
* @param {Object} data * @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 post(url, data = null) { async function request(url, options = {}) {
return dataRequest('POST', url, data); let requestUrl = url;
}
/** if (!requestUrl.startsWith('http')) {
* Perform a HTTP PUT request. requestUrl = window.baseUrl(requestUrl);
* @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);
}
/** if (options.params) {
* Perform a HTTP PATCH request. const urlObj = new URL(requestUrl);
* @param {String} url for (const paramName of Object.keys(options.params)) {
* @param {Object} data const value = options.params[paramName];
* @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} if (typeof value !== 'undefined' && value !== null) {
*/ urlObj.searchParams.set(paramName, value);
async function patch(url, data = null) { }
return dataRequest('PATCH', url, data); }
} requestUrl = urlObj.toString();
}
/** const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
* Perform a HTTP DELETE request. const requestOptions = {...options, credentials: 'same-origin'};
* @param {String} url requestOptions.headers = {
* @param {Object} data ...requestOptions.headers || {},
* @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} baseURL: window.baseUrl(''),
*/ 'X-CSRF-TOKEN': csrfToken,
async function performDelete(url, data = null) { };
return dataRequest('DELETE', url, data);
const response = await fetch(requestUrl, requestOptions);
const content = await getResponseContent(response);
const returnData = {
data: content,
headers: response.headers,
redirected: response.redirected,
status: response.status,
statusText: response.statusText,
url: response.url,
original: response,
};
if (!response.ok) {
throw new HttpError(response, content);
}
return returnData;
} }
/** /**
@ -58,7 +103,7 @@ async function performDelete(url, data = null) {
* @param {String} method * @param {String} method
* @param {String} url * @param {String} url
* @param {Object} data * @param {Object} data
* @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} * @returns {Promise<FormattedResponse>}
*/ */
async function dataRequest(method, url, data = null) { async function dataRequest(method, url, data = null) {
const options = { const options = {
@ -87,96 +132,57 @@ async function dataRequest(method, url, data = null) {
} }
/** /**
* Create a new HTTP request, setting the required CSRF information * Perform a HTTP GET request.
* to communicate with the back-end. Parses & formats the response. * Can easily pass query parameters as the second parameter.
* @param {String} url * @param {String} url
* @param {Object} options * @param {Object} params
* @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 = {}) { export async function get(url, params = {}) {
if (!url.startsWith('http')) { return request(url, {
url = window.baseUrl(url); method: 'GET',
} params,
});
if (options.params) {
const urlObj = new URL(url);
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();
}
const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
options = {...options, credentials: 'same-origin'};
options.headers = {
...options.headers || {},
baseURL: window.baseUrl(''),
'X-CSRF-TOKEN': csrfToken,
};
const response = await fetch(url, options);
const content = await getResponseContent(response);
const returnData = {
data: content,
headers: response.headers,
redirected: response.redirected,
status: response.status,
statusText: response.statusText,
url: response.url,
original: response,
};
if (!response.ok) {
throw new HttpError(response, content);
}
return returnData;
} }
/** /**
* Get the content from a fetch response. * Perform a HTTP POST request.
* Checks the content-type header to determine the format. * @param {String} url
* @param {Response} response * @param {Object} data
* @returns {Promise<Object|String>} * @returns {Promise<FormattedResponse>}
*/ */
async function getResponseContent(response) { export async function post(url, data = null) {
if (response.status === 204) { return dataRequest('POST', url, data);
return null;
}
const responseContentType = response.headers.get('Content-Type') || '';
const subType = responseContentType.split(';')[0].split('/').pop();
if (subType === 'javascript' || subType === 'json') {
return await response.json();
}
return await response.text();
} }
class HttpError extends Error { /**
* Perform a HTTP PUT request.
constructor(response, content) { * @param {String} url
super(response.statusText); * @param {Object} data
this.data = content; * @returns {Promise<FormattedResponse>}
this.headers = response.headers; */
this.redirected = response.redirected; export async function put(url, data = null) {
this.status = response.status; return dataRequest('PUT', url, data);
this.statusText = response.statusText;
this.url = response.url;
this.original = response;
}
} }
export default { /**
get, * Perform a HTTP PATCH request.
post, * @param {String} url
put, * @param {Object} data
patch, * @returns {Promise<FormattedResponse>}
delete: performDelete, */
HttpError, 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 {performDelete as delete};

View File

@ -11,10 +11,9 @@
*/ */
export function debounce(func, wait, immediate) { export function debounce(func, wait, immediate) {
let timeout; let timeout;
return function() { return function debouncedWrapper(...args) {
const context = this; const const context = this;
args = arguments; const later = function debouncedTimeout() {
const later = function() {
timeout = null; timeout = null;
if (!immediate) func.apply(context, args); if (!immediate) func.apply(context, args);
}; };
@ -67,6 +66,7 @@ export function escapeHtml(unsafe) {
* @returns {string} * @returns {string}
*/ */
export function uniqueId() { export function uniqueId() {
// eslint-disable-next-line no-bitwise
const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`); return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
} }

View File

@ -13,7 +13,7 @@ import {getPlugin as getAboutPlugin} from './plugins-about';
import {getPlugin as getDetailsPlugin} from './plugins-details'; import {getPlugin as getDetailsPlugin} from './plugins-details';
import {getPlugin as getTasklistPlugin} from './plugins-tasklist'; import {getPlugin as getTasklistPlugin} from './plugins-tasklist';
const style_formats = [ const styleFormats = [
{title: 'Large Header', format: 'h2', preview: 'color: blue;'}, {title: 'Large Header', format: 'h2', preview: 'color: blue;'},
{title: 'Medium Header', format: 'h3'}, {title: 'Medium Header', format: 'h3'},
{title: 'Small Header', format: 'h4'}, {title: 'Small Header', format: 'h4'},
@ -43,7 +43,7 @@ const formats = {
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}, calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
}; };
const color_map = [ const colorMap = [
'#BFEDD2', '', '#BFEDD2', '',
'#FBEEB8', '', '#FBEEB8', '',
'#F8CAC6', '', '#F8CAC6', '',
@ -72,7 +72,7 @@ const color_map = [
'#ffffff', '', '#ffffff', '',
]; ];
function file_picker_callback(callback, value, meta) { function filePickerCallback(callback, value, meta) {
// field_name, url, type, win // field_name, url, type, win
if (meta.filetype === 'file') { if (meta.filetype === 'file') {
/** @type {EntitySelectorPopup} * */ /** @type {EntitySelectorPopup} * */
@ -119,12 +119,12 @@ function gatherPlugins(options) {
options.textDirection === 'rtl' ? 'directionality' : '', options.textDirection === 'rtl' ? 'directionality' : '',
]; ];
window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options)); window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options)); window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options)); window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
window.tinymce.PluginManager.add('about', getAboutPlugin(options)); window.tinymce.PluginManager.add('about', getAboutPlugin());
window.tinymce.PluginManager.add('details', getDetailsPlugin(options)); window.tinymce.PluginManager.add('details', getDetailsPlugin());
window.tinymce.PluginManager.add('tasklist', getTasklistPlugin(options)); window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
if (options.drawioUrl) { if (options.drawioUrl) {
window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options)); window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
@ -156,7 +156,7 @@ function setupBrFilter(editor) {
editor.serializer.addNodeFilter('br', nodes => { editor.serializer.addNodeFilter('br', nodes => {
for (const node of nodes) { for (const node of nodes) {
if (node.parent && node.parent.name === 'code') { 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'; newline.value = '\n';
node.replace(newline); node.replace(newline);
} }
@ -169,7 +169,14 @@ function setupBrFilter(editor) {
* @return {function(Editor)} * @return {function(Editor)}
*/ */
function getSetupCallback(options) { 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); editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
listenForCommonEvents(editor); listenForCommonEvents(editor);
listenForDragAndPaste(editor, options); listenForDragAndPaste(editor, options);
@ -185,13 +192,6 @@ function getSetupCallback(options) {
setupBrFilter(editor); setupBrFilter(editor);
}); });
function editorChange() {
if (options.darkMode) {
editor.contentDocument.documentElement.classList.add('dark-mode');
}
window.$events.emit('editor-html-change', '');
}
// Custom handler hook // Custom handler hook
window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor}); window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
@ -274,7 +274,7 @@ export function build(options) {
contextmenu: false, contextmenu: false,
toolbar: getPrimaryToolbar(options), toolbar: getPrimaryToolbar(options),
content_style: getContentStyle(options), content_style: getContentStyle(options),
style_formats, style_formats: styleFormats,
style_formats_merge: false, style_formats_merge: false,
media_alt_source: false, media_alt_source: false,
media_poster: false, media_poster: false,
@ -282,8 +282,8 @@ export function build(options) {
table_style_by_css: true, table_style_by_css: true,
table_use_colgroups: true, table_use_colgroups: true,
file_picker_types: 'file image', file_picker_types: 'file image',
color_map, color_map: colorMap,
file_picker_callback, file_picker_callback: filePickerCallback,
paste_preprocess(plugin, args) { paste_preprocess(plugin, args) {
const {content} = args; const {content} = args;
if (content.indexOf('<img src="file://') !== -1) { if (content.indexOf('<img src="file://') !== -1) {
@ -296,7 +296,7 @@ export function build(options) {
}, },
setup(editor) { setup(editor) {
registerCustomIcons(editor); registerCustomIcons(editor);
registerAdditionalToolbars(editor, options); registerAdditionalToolbars(editor);
getSetupCallback(options)(editor); getSetupCallback(options)(editor);
}, },
}; };

View File

@ -1,4 +1,4 @@
import Clipboard from '../services/clipboard'; import {Clipboard} from '../services/clipboard';
let wrap; let wrap;
let draggedContentEditable; let draggedContentEditable;
@ -7,6 +7,25 @@ function hasTextContent(node) {
return node && !!(node.textContent || node.innerText); 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. * Handle pasting images from clipboard.
* @param {Editor} editor * @param {Editor} editor
@ -43,36 +62,16 @@ function paste(editor, options, event) {
}).catch(err => { }).catch(err => {
editor.dom.remove(id); editor.dom.remove(id);
window.$events.emit('error', options.translations.imageUploadErrorText); window.$events.emit('error', options.translations.imageUploadErrorText);
console.log(err); console.error(err);
}); });
}, 10); }, 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 {Editor} editor
* @param {WysiwygConfigOptions} options
*/ */
function dragStart(editor, options) { function dragStart(editor) {
const node = editor.selection.getNode(); const node = editor.selection.getNode();
if (node.nodeName === 'IMG') { if (node.nodeName === 'IMG') {
@ -96,7 +95,11 @@ function dragStart(editor, options) {
*/ */
function drop(editor, options, event) { function drop(editor, options, event) {
const {dom} = editor; 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 // Template insertion
const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template'); const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');
@ -151,7 +154,7 @@ function drop(editor, options, event) {
* @param {WysiwygConfigOptions} options * @param {WysiwygConfigOptions} options
*/ */
export function listenForDragAndPaste(editor, 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('drop', event => drop(editor, options, event));
editor.on('paste', event => paste(editor, options, event)); editor.on('paste', event => paste(editor, options, event));
} }

View File

@ -116,7 +116,9 @@ function defineCodeBlockCustomElement(editor) {
const container = this.shadowRoot.querySelector('.CodeMirrorContainer'); const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
const renderEditor = Code => { const renderEditor = Code => {
this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage()); 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 => { window.importVersioned('code').then(Code => {
@ -143,9 +145,8 @@ function defineCodeBlockCustomElement(editor) {
/** /**
* @param {Editor} editor * @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.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.ui.registry.addButton('codeeditor', {
@ -183,7 +184,7 @@ function register(editor, url) {
} }
}); });
editor.on('dblclick', event => { editor.on('dblclick', () => {
const selectedNode = editor.selection.getNode(); const selectedNode = editor.selection.getNode();
if (elemIsCodeBlock(selectedNode)) { if (elemIsCodeBlock(selectedNode)) {
showPopupForCodeBlock(editor, selectedNode); showPopupForCodeBlock(editor, selectedNode);
@ -193,7 +194,7 @@ function register(editor, url) {
editor.on('PreInit', () => { editor.on('PreInit', () => {
editor.parser.addNodeFilter('pre', elms => { editor.parser.addNodeFilter('pre', elms => {
for (const el of 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', contenteditable: 'false',
}); });
@ -234,9 +235,8 @@ function register(editor, url) {
} }
/** /**
* @param {WysiwygConfigOptions} options
* @return {register} * @return {register}
*/ */
export function getPlugin(options) { export function getPlugin() {
return register; return register;
} }

View File

@ -32,12 +32,6 @@ function showDrawingManager(mceEditor, selectedNode = null) {
}, 'drawio'); }, 'drawio');
} }
function showDrawingEditor(mceEditor, selectedNode = null) {
pageEditor = mceEditor;
currentNode = selectedNode;
DrawIO.show(options.drawioUrl, drawingInit, updateContent);
}
async function updateContent(pngData) { async function updateContent(pngData) {
const id = `image-${Math.random().toString(16).slice(2)}`; const id = `image-${Math.random().toString(16).slice(2)}`;
const loadingImage = window.baseUrl('/loading.gif'); const loadingImage = window.baseUrl('/loading.gif');
@ -48,7 +42,7 @@ async function updateContent(pngData) {
} else { } else {
window.$events.emit('error', options.translations.imageUploadErrorText); window.$events.emit('error', options.translations.imageUploadErrorText);
} }
console.log(error); console.error(error);
}; };
// Handle updating an existing image // Handle updating an existing image
@ -92,6 +86,66 @@ function drawingInit() {
return DrawIO.load(drawingId); 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 * @param {WysiwygConfigOptions} providedOptions
@ -99,54 +153,5 @@ function drawingInit() {
*/ */
export function getPlugin(providedOptions) { export function getPlugin(providedOptions) {
options = providedOptions; options = providedOptions;
return function(editor, url) { return register;
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');
}
});
});
};
} }

View File

@ -1,8 +1,7 @@
/** /**
* @param {Editor} editor * @param {Editor} editor
* @param {String} url
*/ */
function register(editor, url) { function register(editor) {
const aboutDialog = { const aboutDialog = {
title: 'About the WYSIWYG Editor', title: 'About the WYSIWYG Editor',
url: window.baseUrl('/help/wysiwyg'), url: window.baseUrl('/help/wysiwyg'),
@ -12,15 +11,14 @@ function register(editor, url) {
icon: 'help', icon: 'help',
tooltip: 'About the editor', tooltip: 'About the editor',
onAction() { onAction() {
tinymce.activeEditor.windowManager.openUrl(aboutDialog); window.tinymce.activeEditor.windowManager.openUrl(aboutDialog);
}, },
}); });
} }
/** /**
* @param {WysiwygConfigOptions} options
* @return {register} * @return {register}
*/ */
export function getPlugin(options) { export function getPlugin() {
return register; return register;
} }

View File

@ -1,8 +1,7 @@
/** /**
* @param {Editor} editor * @param {Editor} editor
* @param {String} url
*/ */
function register(editor, url) { function register(editor) {
editor.addCommand('InsertHorizontalRule', () => { editor.addCommand('InsertHorizontalRule', () => {
const hrElem = document.createElement('hr'); const hrElem = document.createElement('hr');
const cNode = editor.selection.getNode(); const cNode = editor.selection.getNode();
@ -20,9 +19,8 @@ function register(editor, url) {
} }
/** /**
* @param {WysiwygConfigOptions} options
* @return {register} * @return {register}
*/ */
export function getPlugin(options) { export function getPlugin() {
return register; return register;
} }

View File

@ -1,10 +1,178 @@
/**
* @param {Editor} editor
* @param {String} url
*/
import {blockElementTypes} from './util'; import {blockElementTypes} from './util';
function register(editor, url) { /**
* @param {Editor} editor
*/
function getSelectedDetailsBlock(editor) {
return editor.selection.getNode().closest('details');
}
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;
});
}
/**
* @param {Editor} editor
*/
function detailsDialog(editor) {
return {
title: 'Edit collapsible block',
body: {
type: 'panel',
items: [
{
type: 'input',
name: 'summary',
label: 'Toggle label',
},
],
},
buttons: [
{
type: 'cancel',
text: 'Cancel',
},
{
type: 'submit',
text: 'Save',
primary: true,
},
],
onSubmit(api) {
const {summary} = api.getData();
setSummary(editor, summary);
api.close();
},
};
}
/**
* @param {Element} element
*/
function getSummaryTextFromDetails(element) {
const summary = element.querySelector('summary');
if (!summary) {
return '';
}
return summary.textContent;
}
/**
* @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
*/
function unwrapDetailsInSelection(editor) {
const details = editor.selection.getNode().closest('details');
const selectionBm = editor.selection.getBookmark();
if (details) {
const elements = details.querySelectorAll('details > *:not(summary, doc-root), doc-root > *');
editor.undoManager.transact(() => {
for (const element of elements) {
details.parentNode.insertBefore(element, details);
}
details.remove();
});
}
editor.focus();
editor.selection.moveToBookmark(selectionBm);
}
/**
* @param {tinymce.html.Node} detailsEl
*/
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;
}
}
if (madeUnwrap) {
unwrapDetailsEditable(detailsEl);
}
}
/**
* @param {tinymce.html.Node} detailsEl
*/
function ensureDetailsWrappedInEditable(detailsEl) {
unwrapDetailsEditable(detailsEl);
detailsEl.attr('contenteditable', 'false');
const rootWrap = window.tinymce.html.Node.create('doc-root', {contenteditable: 'true'});
let previousBlockWrap = null;
for (const child of detailsEl.children()) {
if (child.name === 'summary') continue;
const isBlock = blockElementTypes.includes(child.name);
if (!isBlock) {
if (!previousBlockWrap) {
previousBlockWrap = window.tinymce.html.Node.create('p');
rootWrap.append(previousBlockWrap);
}
previousBlockWrap.append(child);
} else {
rootWrap.append(child);
previousBlockWrap = null;
}
}
detailsEl.append(rootWrap);
}
/**
* @param {Editor} editor
*/
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);
}
});
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('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('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.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>');
@ -89,178 +257,8 @@ function register(editor, url) {
} }
/** /**
* @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
*/
function getSelectedDetailsBlock(editor) {
return editor.selection.getNode().closest('details');
}
/**
* @param {Element} element
*/
function getSummaryTextFromDetails(element) {
const summary = element.querySelector('summary');
if (!summary) {
return '';
}
return summary.textContent;
}
/**
* @param {Editor} editor
*/
function detailsDialog(editor) {
return {
title: 'Edit collapsible block',
body: {
type: 'panel',
items: [
{
type: 'input',
name: 'summary',
label: 'Toggle label',
},
],
},
buttons: [
{
type: 'cancel',
text: 'Cancel',
},
{
type: 'submit',
text: 'Save',
primary: true,
},
],
onSubmit(api) {
const {summary} = api.getData();
setSummary(editor, summary);
api.close();
},
};
}
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;
});
}
/**
* @param {Editor} editor
*/
function unwrapDetailsInSelection(editor) {
const details = editor.selection.getNode().closest('details');
const selectionBm = editor.selection.getBookmark();
if (details) {
const elements = details.querySelectorAll('details > *:not(summary, doc-root), doc-root > *');
editor.undoManager.transact(() => {
for (const element of elements) {
details.parentNode.insertBefore(element, details);
}
details.remove();
});
}
editor.focus();
editor.selection.moveToBookmark(selectionBm);
}
/**
* @param {Editor} editor
*/
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);
}
});
editor.serializer.addNodeFilter('doc-root', elms => {
for (const el of elms) {
el.unwrap();
}
});
}
/**
* @param {tinymce.html.Node} detailsEl
*/
function ensureDetailsWrappedInEditable(detailsEl) {
unwrapDetailsEditable(detailsEl);
detailsEl.attr('contenteditable', 'false');
const rootWrap = tinymce.html.Node.create('doc-root', {contenteditable: 'true'});
let previousBlockWrap = null;
for (const child of detailsEl.children()) {
if (child.name === 'summary') continue;
const isBlock = blockElementTypes.includes(child.name);
if (!isBlock) {
if (!previousBlockWrap) {
previousBlockWrap = tinymce.html.Node.create('p');
rootWrap.append(previousBlockWrap);
}
previousBlockWrap.append(child);
} else {
rootWrap.append(child);
previousBlockWrap = null;
}
}
detailsEl.append(rootWrap);
}
/**
* @param {tinymce.html.Node} detailsEl
*/
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;
}
}
if (madeUnwrap) {
unwrapDetailsEditable(detailsEl);
}
}
/**
* @param {WysiwygConfigOptions} options
* @return {register} * @return {register}
*/ */
export function getPlugin(options) { export function getPlugin() {
return register; return register;
} }

View File

@ -1,8 +1,7 @@
/** /**
* @param {Editor} editor * @param {Editor} editor
* @param {String} url
*/ */
function register(editor, url) { function register(editor) {
// Custom Image picker button // Custom Image picker button
editor.ui.registry.addButton('imagemanager-insert', { editor.ui.registry.addButton('imagemanager-insert', {
title: 'Insert image', title: 'Insert image',
@ -23,9 +22,8 @@ function register(editor, url) {
} }
/** /**
* @param {WysiwygConfigOptions} options
* @return {register} * @return {register}
*/ */
export function getPlugin(options) { export function getPlugin() {
return register; return register;
} }

View File

@ -1,96 +1,3 @@
/**
* @param {Editor} editor
* @param {String} url
*/
function register(editor, url) {
// 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', {
tooltip: 'Task list',
icon: 'tasklist',
active: false,
onAction(api) {
if (api.isActive()) {
editor.execCommand('RemoveList');
} else {
editor.execCommand('InsertUnorderedList', null, {
'list-item-attributes': {
class: 'task-list-item',
},
'list-style-type': 'tasklist',
});
}
},
onSetup(api) {
editor.on('NodeChange', event => {
const parentListEl = event.parents.find(el => el.nodeName === 'LI');
const inList = parentListEl && parentListEl.classList.contains('task-list-item');
api.setActive(Boolean(inList));
});
},
});
// 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) {
editor.on('NodeChange', event => {
const parentList = event.parents.find(el => el.nodeName === 'LI');
const inTaskList = parentList && parentList.classList.contains('task-list-item');
const inUlList = parentList && parentList.parentNode.nodeName === 'UL';
api.setActive(Boolean(inUlList && !inTaskList));
});
};
existingBullListButton.onAction = function() {
// 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.
if (elementWithinTaskList(editor.selection.getNode())) {
editor.execCommand('InsertOrderedList', null, {
'list-item-attributes': {class: null},
});
}
editor.execCommand('InsertUnorderedList', null, {
'list-item-attributes': {class: null},
});
};
// Tweak existing number list to not allow classes on child items
const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;
existingNumListButton.onAction = function() {
editor.execCommand('InsertOrderedList', null, {
'list-item-attributes': {class: null},
});
};
// Setup filters on pre-init
editor.on('PreInit', () => {
editor.parser.addNodeFilter('li', nodes => {
for (const node of nodes) {
if (node.attributes.map.class === 'task-list-item') {
parseTaskListNode(node);
}
}
});
editor.serializer.addNodeFilter('li', nodes => {
for (const node of nodes) {
if (node.attributes.map.class === 'task-list-item') {
serializeTaskListNode(node);
}
}
});
});
// Handle checkbox click in editor
editor.on('click', event => {
const clickedEl = event.target;
if (clickedEl.nodeName === 'LI' && clickedEl.classList.contains('task-list-item')) {
handleTaskListItemClick(event, clickedEl, editor);
event.preventDefault();
}
});
}
/** /**
* @param {Element} element * @param {Element} element
* @return {boolean} * @return {boolean}
@ -108,9 +15,9 @@ function elementWithinTaskList(element) {
function handleTaskListItemClick(event, clickedEl, editor) { function handleTaskListItemClick(event, clickedEl, editor) {
const bounds = clickedEl.getBoundingClientRect(); const bounds = clickedEl.getBoundingClientRect();
const withinBounds = event.clientX <= bounds.right const withinBounds = event.clientX <= bounds.right
&& event.clientX >= bounds.left && event.clientX >= bounds.left
&& event.clientY >= bounds.top && event.clientY >= bounds.top
&& event.clientY <= bounds.bottom; && event.clientY <= bounds.bottom;
// Outside of the task list item bounds mean we're probably clicking the pseudo-element. // Outside of the task list item bounds mean we're probably clicking the pseudo-element.
if (!withinBounds) { if (!withinBounds) {
@ -156,15 +63,111 @@ function serializeTaskListNode(node) {
} }
// Create & insert checkbox input element // Create & insert checkbox input element
const checkbox = tinymce.html.Node.create('input', inputAttrs); const checkbox = window.tinymce.html.Node.create('input', inputAttrs);
checkbox.shortEnded = true; checkbox.shortEnded = true;
node.firstChild ? node.insert(checkbox, node.firstChild, true) : node.append(checkbox);
if (node.firstChild) {
node.insert(checkbox, node.firstChild, true);
} else {
node.append(checkbox);
}
}
/**
* @param {Editor} editor
*/
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', {
tooltip: 'Task list',
icon: 'tasklist',
active: false,
onAction(api) {
if (api.isActive()) {
editor.execCommand('RemoveList');
} else {
editor.execCommand('InsertUnorderedList', null, {
'list-item-attributes': {
class: 'task-list-item',
},
'list-style-type': 'tasklist',
});
}
},
onSetup(api) {
editor.on('NodeChange', event => {
const parentListEl = event.parents.find(el => el.nodeName === 'LI');
const inList = parentListEl && parentListEl.classList.contains('task-list-item');
api.setActive(Boolean(inList));
});
},
});
// 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 customBullListOnSetup(api) {
editor.on('NodeChange', event => {
const parentList = event.parents.find(el => el.nodeName === 'LI');
const inTaskList = parentList && parentList.classList.contains('task-list-item');
const inUlList = parentList && parentList.parentNode.nodeName === 'UL';
api.setActive(Boolean(inUlList && !inTaskList));
});
};
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.
if (elementWithinTaskList(editor.selection.getNode())) {
editor.execCommand('InsertOrderedList', null, {
'list-item-attributes': {class: null},
});
}
editor.execCommand('InsertUnorderedList', null, {
'list-item-attributes': {class: null},
});
};
// Tweak existing number list to not allow classes on child items
const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;
existingNumListButton.onAction = function customNumListButtonOnAction() {
editor.execCommand('InsertOrderedList', null, {
'list-item-attributes': {class: null},
});
};
// Setup filters on pre-init
editor.on('PreInit', () => {
editor.parser.addNodeFilter('li', nodes => {
for (const node of nodes) {
if (node.attributes.map.class === 'task-list-item') {
parseTaskListNode(node);
}
}
});
editor.serializer.addNodeFilter('li', nodes => {
for (const node of nodes) {
if (node.attributes.map.class === 'task-list-item') {
serializeTaskListNode(node);
}
}
});
});
// Handle checkbox click in editor
editor.on('click', event => {
const clickedEl = event.target;
if (clickedEl.nodeName === 'LI' && clickedEl.classList.contains('task-list-item')) {
handleTaskListItemClick(event, clickedEl, editor);
event.preventDefault();
}
});
} }
/** /**
* @param {WysiwygConfigOptions} options
* @return {register} * @return {register}
*/ */
export function getPlugin(options) { export function getPlugin() {
return register; return register;
} }

View File

@ -1,16 +1,3 @@
/**
* 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 {Editor} editor
* @param {String} scrollId * @param {String} scrollId
@ -27,3 +14,16 @@ function scrollToText(editor, scrollId) {
editor.selection.collapse(false); editor.selection.collapse(false);
editor.focus(); 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);
}
}

View File

@ -35,7 +35,9 @@ export function register(editor) {
const callout = selectedNode ? selectedNode.closest('.callout') : null; const callout = selectedNode ? selectedNode.closest('.callout') : null;
const formats = ['info', 'success', 'warning', 'danger']; 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 newFormatIndex = (currentFormatIndex + 1) % formats.length;
const newFormat = formats[newFormatIndex]; const newFormat = formats[newFormatIndex];

View File

@ -70,9 +70,8 @@ function registerImageContextToolbar(editor) {
/** /**
* @param {Editor} editor * @param {Editor} editor
* @param {WysiwygConfigOptions} options
*/ */
export function registerAdditionalToolbars(editor, options) { export function registerAdditionalToolbars(editor) {
registerPrimaryToolbarGroups(editor); registerPrimaryToolbarGroups(editor);
registerLinkContextToolbar(editor); registerLinkContextToolbar(editor);
registerImageContextToolbar(editor); registerImageContextToolbar(editor);