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