mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
ESLINT: Started inital pass at addressing issues
This commit is contained in:
parent
e711290d8b
commit
0519e58fbf
23
package.json
23
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
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -66,5 +66,3 @@ export async function copyTextToClipboard(text) {
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(tempInput);
|
||||
}
|
||||
|
||||
export default Clipboard;
|
||||
|
@ -1,55 +1,100 @@
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the content from a fetch response.
|
||||
* Checks the content-type header to determine the format.
|
||||
* @param {Response} response
|
||||
* @returns {Promise<Object|String>}
|
||||
*/
|
||||
async function 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 {Object} data
|
||||
* @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
|
||||
* @param {Object} options
|
||||
* @returns {Promise<FormattedResponse>}
|
||||
*/
|
||||
async function post(url, data = null) {
|
||||
return dataRequest('POST', url, data);
|
||||
}
|
||||
async function request(url, options = {}) {
|
||||
let requestUrl = url;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
if (!requestUrl.startsWith('http')) {
|
||||
requestUrl = window.baseUrl(requestUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (options.params) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
requestUrl = urlObj.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
|
||||
const requestOptions = {...options, credentials: 'same-origin'};
|
||||
requestOptions.headers = {
|
||||
...requestOptions.headers || {},
|
||||
baseURL: window.baseUrl(''),
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
};
|
||||
|
||||
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} url
|
||||
* @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) {
|
||||
const options = {
|
||||
@ -87,96 +132,57 @@ async function dataRequest(method, url, data = null) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new HTTP request, setting the required CSRF information
|
||||
* to communicate with the back-end. Parses & formats the response.
|
||||
* Perform a HTTP GET request.
|
||||
* Can easily pass query parameters as the second parameter.
|
||||
* @param {String} url
|
||||
* @param {Object} options
|
||||
* @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
|
||||
* @param {Object} params
|
||||
* @returns {Promise<FormattedResponse>}
|
||||
*/
|
||||
async function request(url, options = {}) {
|
||||
if (!url.startsWith('http')) {
|
||||
url = window.baseUrl(url);
|
||||
}
|
||||
|
||||
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;
|
||||
export async function get(url, params = {}) {
|
||||
return request(url, {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content from a fetch response.
|
||||
* Checks the content-type header to determine the format.
|
||||
* @param {Response} response
|
||||
* @returns {Promise<Object|String>}
|
||||
* Perform a HTTP POST request.
|
||||
* @param {String} url
|
||||
* @param {Object} data
|
||||
* @returns {Promise<FormattedResponse>}
|
||||
*/
|
||||
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 await response.json();
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
export async function post(url, data = null) {
|
||||
return dataRequest('POST', url, data);
|
||||
}
|
||||
|
||||
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 PUT request.
|
||||
* @param {String} url
|
||||
* @param {Object} data
|
||||
* @returns {Promise<FormattedResponse>}
|
||||
*/
|
||||
export async function put(url, data = null) {
|
||||
return dataRequest('PUT', url, data);
|
||||
}
|
||||
|
||||
export default {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
patch,
|
||||
delete: performDelete,
|
||||
HttpError,
|
||||
};
|
||||
/**
|
||||
* Perform a HTTP PATCH request.
|
||||
* @param {String} url
|
||||
* @param {Object} data
|
||||
* @returns {Promise<FormattedResponse>}
|
||||
*/
|
||||
export async function patch(url, data = null) {
|
||||
return dataRequest('PATCH', url, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a HTTP DELETE request.
|
||||
* @param {String} url
|
||||
* @param {Object} data
|
||||
* @returns {Promise<FormattedResponse>}
|
||||
*/
|
||||
async function performDelete(url, data = null) {
|
||||
return dataRequest('DELETE', url, data);
|
||||
}
|
||||
|
||||
export {performDelete as delete};
|
||||
|
@ -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()}`);
|
||||
}
|
||||
|
@ -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('<img src="file://') !== -1) {
|
||||
@ -296,7 +296,7 @@ export function build(options) {
|
||||
},
|
||||
setup(editor) {
|
||||
registerCustomIcons(editor);
|
||||
registerAdditionalToolbars(editor, options);
|
||||
registerAdditionalToolbars(editor);
|
||||
getSetupCallback(options)(editor);
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Clipboard from '../services/clipboard';
|
||||
import {Clipboard} from '../services/clipboard';
|
||||
|
||||
let wrap;
|
||||
let draggedContentEditable;
|
||||
@ -7,6 +7,25 @@ function hasTextContent(node) {
|
||||
return node && !!(node.textContent || node.innerText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an image file to the server
|
||||
* @param {File} file
|
||||
* @param {int} pageId
|
||||
*/
|
||||
async function uploadImageFile(file, pageId) {
|
||||
if (file === null || file.type.indexOf('image') !== 0) {
|
||||
throw new Error('Not an image file');
|
||||
}
|
||||
|
||||
const remoteFilename = file.name || `image-${Date.now()}.png`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, remoteFilename);
|
||||
formData.append('uploaded_to', pageId);
|
||||
|
||||
const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pasting images from clipboard.
|
||||
* @param {Editor} editor
|
||||
@ -43,36 +62,16 @@ function paste(editor, options, event) {
|
||||
}).catch(err => {
|
||||
editor.dom.remove(id);
|
||||
window.$events.emit('error', options.translations.imageUploadErrorText);
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an image file to the server
|
||||
* @param {File} file
|
||||
* @param {int} pageId
|
||||
*/
|
||||
async function uploadImageFile(file, pageId) {
|
||||
if (file === null || file.type.indexOf('image') !== 0) {
|
||||
throw new Error('Not an image file');
|
||||
}
|
||||
|
||||
const remoteFilename = file.name || `image-${Date.now()}.png`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, remoteFilename);
|
||||
formData.append('uploaded_to', pageId);
|
||||
|
||||
const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Editor} editor
|
||||
* @param {WysiwygConfigOptions} options
|
||||
*/
|
||||
function dragStart(editor, options) {
|
||||
function dragStart(editor) {
|
||||
const node = editor.selection.getNode();
|
||||
|
||||
if (node.nodeName === 'IMG') {
|
||||
@ -96,7 +95,11 @@ function dragStart(editor, options) {
|
||||
*/
|
||||
function drop(editor, options, event) {
|
||||
const {dom} = editor;
|
||||
const rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
|
||||
const rng = window.tinymce.dom.RangeUtils.getCaretRangeFromPoint(
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
editor.getDoc(),
|
||||
);
|
||||
|
||||
// Template insertion
|
||||
const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');
|
||||
@ -151,7 +154,7 @@ function drop(editor, options, event) {
|
||||
* @param {WysiwygConfigOptions} options
|
||||
*/
|
||||
export function listenForDragAndPaste(editor, options) {
|
||||
editor.on('dragstart', () => dragStart(editor, options));
|
||||
editor.on('dragstart', () => dragStart(editor));
|
||||
editor.on('drop', event => drop(editor, options, event));
|
||||
editor.on('paste', event => paste(editor, options, event));
|
||||
}
|
||||
|
@ -116,7 +116,9 @@ function defineCodeBlockCustomElement(editor) {
|
||||
const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
|
||||
const renderEditor = Code => {
|
||||
this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());
|
||||
setTimeout(() => this.style.height = null, 12);
|
||||
setTimeout(() => {
|
||||
this.style.height = null;
|
||||
}, 12);
|
||||
};
|
||||
|
||||
window.importVersioned('code').then(Code => {
|
||||
@ -143,9 +145,8 @@ function defineCodeBlockCustomElement(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.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;
|
||||
}
|
||||
|
@ -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', `<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
|
||||
@ -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', `<svg width="24" height="24" fill="${options.darkMode ? '#BBB' : '#000000'}" xmlns="http://www.w3.org/2000/svg"><path d="M20.716 7.639V2.845h-4.794v1.598h-7.99V2.845H3.138v4.794h1.598v7.99H3.138v4.794h4.794v-1.598h7.99v1.598h4.794v-4.794h-1.598v-7.99zM4.736 4.443h1.598V6.04H4.736zm1.598 14.382H4.736v-1.598h1.598zm9.588-1.598h-7.99v-1.598H6.334v-7.99h1.598V6.04h7.99v1.598h1.598v7.99h-1.598zm3.196 1.598H17.52v-1.598h1.598zM17.52 6.04V4.443h1.598V6.04zm-4.21 7.19h-2.79l-.582 1.599H8.643l2.717-7.191h1.119l2.724 7.19h-1.302zm-2.43-1.006h2.086l-1.039-3.06z"/></svg>`);
|
||||
|
||||
editor.ui.registry.addSplitButton('drawio', {
|
||||
tooltip: 'Insert/edit drawing',
|
||||
icon: 'diagram',
|
||||
onAction() {
|
||||
editor.execCommand('drawio');
|
||||
// Hack to de-focus the tinymce editor toolbar
|
||||
window.document.body.dispatchEvent(new Event('mousedown', {bubbles: true}));
|
||||
},
|
||||
fetch(callback) {
|
||||
callback([
|
||||
{
|
||||
type: 'choiceitem',
|
||||
text: 'Drawing manager',
|
||||
value: 'drawing-manager',
|
||||
},
|
||||
]);
|
||||
},
|
||||
onItemAction(api, value) {
|
||||
if (value === 'drawing-manager') {
|
||||
const selectedNode = editor.selection.getNode();
|
||||
showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
editor.on('dblclick', event => {
|
||||
const selectedNode = editor.selection.getNode();
|
||||
if (!isDrawing(selectedNode)) return;
|
||||
showDrawingEditor(editor, selectedNode);
|
||||
});
|
||||
|
||||
editor.on('SetContent', () => {
|
||||
const drawings = editor.dom.select('body > div[drawio-diagram]');
|
||||
if (!drawings.length) return;
|
||||
|
||||
editor.undoManager.transact(() => {
|
||||
for (const drawing of drawings) {
|
||||
drawing.setAttribute('contenteditable', 'false');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return register;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,10 +1,178 @@
|
||||
/**
|
||||
* @param {Editor} editor
|
||||
* @param {String} url
|
||||
*/
|
||||
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('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>');
|
||||
@ -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}
|
||||
*/
|
||||
export function getPlugin(options) {
|
||||
export function getPlugin() {
|
||||
return register;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
* @return {boolean}
|
||||
@ -108,9 +15,9 @@ function elementWithinTaskList(element) {
|
||||
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;
|
||||
&& 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) {
|
||||
@ -156,15 +63,111 @@ function serializeTaskListNode(node) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
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}
|
||||
*/
|
||||
export function getPlugin(options) {
|
||||
export function getPlugin() {
|
||||
return register;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user