From: Dan Brown Date: Wed, 19 Apr 2023 09:46:13 +0000 (+0100) Subject: ESLINT: Started inital pass at addressing issues X-Git-Tag: v23.05~1^2~24^2~2 X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/0519e58fbf4e0da0ee42e1954dd159755ee1718c ESLINT: Started inital pass at addressing issues --- diff --git a/package.json b/package.json index 20f96ff48..264180f81 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "es2021": true }, "extends": "airbnb-base", + "ignorePatterns": ["resources/**/*-stub.js"], "overrides": [ ], "parserOptions": { @@ -76,6 +77,28 @@ "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 }] } } diff --git a/resources/js/app.js b/resources/js/app.js index ccf54b33d..86c8d0802 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,19 +1,20 @@ 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); diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index 67b7c68ec..b615e666b 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -1,6 +1,6 @@ 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. diff --git a/resources/js/services/clipboard.js b/resources/js/services/clipboard.js index ecdbecf53..02db29be0 100644 --- a/resources/js/services/clipboard.js +++ b/resources/js/services/clipboard.js @@ -66,5 +66,3 @@ export async function copyTextToClipboard(text) { document.execCommand('copy'); document.body.removeChild(tempInput); } - -export default Clipboard; diff --git a/resources/js/services/http.js b/resources/js/services/http.js index 9f1b5deac..d0d33e317 100644 --- a/resources/js/services/http.js +++ b/resources/js/services/http.js @@ -1,89 +1,48 @@ /** - * 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} */ -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); } /** @@ -91,33 +50,35 @@ async function dataRequest(method, url, data = null) { * 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} */ 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, @@ -137,46 +98,91 @@ async function request(url, options = {}) { } /** - * Get the content from a fetch response. - * Checks the content-type header to determine the format. - * @param {Response} response - * @returns {Promise} + * 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} */ -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} + */ +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} + */ +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} + */ +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} + */ +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} + */ +async function performDelete(url, data = null) { + return dataRequest('DELETE', url, data); } -export default { - get, - post, - put, - patch, - delete: performDelete, - HttpError, -}; +export {performDelete as delete}; diff --git a/resources/js/services/util.js b/resources/js/services/util.js index df2b31336..dd97d81aa 100644 --- a/resources/js/services/util.js +++ b/resources/js/services/util.js @@ -11,10 +11,9 @@ */ 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); }; @@ -67,6 +66,7 @@ export function escapeHtml(unsafe) { * @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()}`); } diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index 89efdb8a0..a0e7156ee 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -13,7 +13,7 @@ import {getPlugin as getAboutPlugin} from './plugins-about'; 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'}, @@ -43,7 +43,7 @@ const formats = { calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}, }; -const color_map = [ +const colorMap = [ '#BFEDD2', '', '#FBEEB8', '', '#F8CAC6', '', @@ -72,7 +72,7 @@ const color_map = [ '#ffffff', '', ]; -function file_picker_callback(callback, value, meta) { +function filePickerCallback(callback, value, meta) { // field_name, url, type, win if (meta.filetype === 'file') { /** @type {EntitySelectorPopup} * */ @@ -119,12 +119,12 @@ function gatherPlugins(options) { 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)); @@ -156,7 +156,7 @@ function setupBrFilter(editor) { 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); } @@ -169,7 +169,14 @@ function setupBrFilter(editor) { * @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); @@ -185,13 +192,6 @@ function getSetupCallback(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}); @@ -274,7 +274,7 @@ export function build(options) { 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, @@ -282,8 +282,8 @@ export function build(options) { 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(''); 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(); if (elemIsCodeBlock(selectedNode)) { showPopupForCodeBlock(editor, selectedNode); @@ -193,7 +194,7 @@ function register(editor, url) { 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', }); @@ -234,9 +235,8 @@ function register(editor, url) { } /** - * @param {WysiwygConfigOptions} options * @return {register} */ -export function getPlugin(options) { +export function getPlugin() { return register; } diff --git a/resources/js/wysiwyg/plugin-drawio.js b/resources/js/wysiwyg/plugin-drawio.js index 3d1125841..7827efd62 100644 --- a/resources/js/wysiwyg/plugin-drawio.js +++ b/resources/js/wysiwyg/plugin-drawio.js @@ -32,12 +32,6 @@ function showDrawingManager(mceEditor, selectedNode = null) { }, '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'); @@ -48,7 +42,7 @@ async function updateContent(pngData) { } else { window.$events.emit('error', options.translations.imageUploadErrorText); } - console.log(error); + console.error(error); }; // Handle updating an existing image @@ -92,6 +86,66 @@ function drawingInit() { 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', ``); + + 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 @@ -99,54 +153,5 @@ function drawingInit() { */ 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', ``); - - 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; } diff --git a/resources/js/wysiwyg/plugins-about.js b/resources/js/wysiwyg/plugins-about.js index a9c0a9e64..096b4f968 100644 --- a/resources/js/wysiwyg/plugins-about.js +++ b/resources/js/wysiwyg/plugins-about.js @@ -1,8 +1,7 @@ /** * @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'), @@ -12,15 +11,14 @@ function register(editor, url) { 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; } diff --git a/resources/js/wysiwyg/plugins-customhr.js b/resources/js/wysiwyg/plugins-customhr.js index 6aa1620da..f5da947f2 100644 --- a/resources/js/wysiwyg/plugins-customhr.js +++ b/resources/js/wysiwyg/plugins-customhr.js @@ -1,8 +1,7 @@ /** * @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(); @@ -20,9 +19,8 @@ function register(editor, url) { } /** - * @param {WysiwygConfigOptions} options * @return {register} */ -export function getPlugin(options) { +export function getPlugin() { return register; } diff --git a/resources/js/wysiwyg/plugins-details.js b/resources/js/wysiwyg/plugins-details.js index 5da6680ed..c4a6d927d 100644 --- a/resources/js/wysiwyg/plugins-details.js +++ b/resources/js/wysiwyg/plugins-details.js @@ -1,102 +1,5 @@ -/** - * @param {Editor} editor - * @param {String} url - */ import {blockElementTypes} from './util'; -function register(editor, url) { - editor.ui.registry.addIcon('details', ''); - editor.ui.registry.addIcon('togglefold', ''); - editor.ui.registry.addIcon('togglelabel', ''); - - 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 = '


'; - } - - 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 */ @@ -104,15 +7,18 @@ 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; +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; + }); } /** @@ -150,18 +56,24 @@ function detailsDialog(editor) { }; } -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)}); } /** @@ -187,27 +99,21 @@ function unwrapDetailsInSelection(editor) { } /** - * @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); + } } /** @@ -217,7 +123,7 @@ function ensureDetailsWrappedInEditable(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()) { @@ -226,7 +132,7 @@ function ensureDetailsWrappedInEditable(detailsEl) { if (!isBlock) { if (!previousBlockWrap) { - previousBlockWrap = tinymce.html.Node.create('p'); + previousBlockWrap = window.tinymce.html.Node.create('p'); rootWrap.append(previousBlockWrap); } previousBlockWrap.append(child); @@ -240,27 +146,119 @@ function ensureDetailsWrappedInEditable(detailsEl) { } /** - * @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', ''); + editor.ui.registry.addIcon('togglefold', ''); + editor.ui.registry.addIcon('togglelabel', ''); + + 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 = '


'; + } + + 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; } diff --git a/resources/js/wysiwyg/plugins-imagemanager.js b/resources/js/wysiwyg/plugins-imagemanager.js index e7dd126cb..37b5bfafd 100644 --- a/resources/js/wysiwyg/plugins-imagemanager.js +++ b/resources/js/wysiwyg/plugins-imagemanager.js @@ -1,8 +1,7 @@ /** * @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', @@ -23,9 +22,8 @@ function register(editor, url) { } /** - * @param {WysiwygConfigOptions} options * @return {register} */ -export function getPlugin(options) { +export function getPlugin() { return register; } diff --git a/resources/js/wysiwyg/plugins-tasklist.js b/resources/js/wysiwyg/plugins-tasklist.js index cf69287f1..191f83649 100644 --- a/resources/js/wysiwyg/plugins-tasklist.js +++ b/resources/js/wysiwyg/plugins-tasklist.js @@ -1,8 +1,82 @@ +/** + * @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', ''); editor.ui.registry.addToggleButton('tasklist', { @@ -33,7 +107,7 @@ function register(editor, url) { // 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'); @@ -41,7 +115,7 @@ function register(editor, url) { 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. @@ -57,7 +131,7 @@ function register(editor, url) { }; // 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}, }); @@ -92,79 +166,8 @@ function register(editor, url) { } /** - * @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; } diff --git a/resources/js/wysiwyg/scrolling.js b/resources/js/wysiwyg/scrolling.js index faeb837a4..92f8f1583 100644 --- a/resources/js/wysiwyg/scrolling.js +++ b/resources/js/wysiwyg/scrolling.js @@ -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 {String} scrollId @@ -27,3 +14,16 @@ function scrollToText(editor, 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); + } +} diff --git a/resources/js/wysiwyg/shortcuts.js b/resources/js/wysiwyg/shortcuts.js index b624b23a2..1c20df9c5 100644 --- a/resources/js/wysiwyg/shortcuts.js +++ b/resources/js/wysiwyg/shortcuts.js @@ -35,7 +35,9 @@ export function register(editor) { 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]; diff --git a/resources/js/wysiwyg/toolbars.js b/resources/js/wysiwyg/toolbars.js index 9acb24c1d..4663ad132 100644 --- a/resources/js/wysiwyg/toolbars.js +++ b/resources/js/wysiwyg/toolbars.js @@ -70,9 +70,8 @@ function registerImageContextToolbar(editor) { /** * @param {Editor} editor - * @param {WysiwygConfigOptions} options */ -export function registerAdditionalToolbars(editor, options) { +export function registerAdditionalToolbars(editor) { registerPrimaryToolbarGroups(editor); registerLinkContextToolbar(editor); registerImageContextToolbar(editor);