UNPKG

jodit

Version:

Jodit is awesome and usefully wysiwyg editor with filebrowser

504 lines (457 loc) 12.6 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Licensed under GNU General Public License version 2 or later or a commercial license or MIT; * For GPL see LICENSE-GPL.txt in the project root for license information. * For MIT see LICENSE-MIT.txt in the project root for license information. * For commercial licenses see https://xdsoft.net/jodit/commercial/ * Copyright (c) 2013-2019 Valeriy Chupurnov. All rights reserved. https://xdsoft.net */ import { Config } from '../Config'; import * as consts from '../constants'; import { IS_INLINE } from '../constants'; import { Dom } from '../modules/Dom'; import { cleanFromWord, debounce, normalizeNode, trim } from '../modules/helpers/'; import { Select } from '../modules/Selection'; import { IDictionary, IJodit } from '../types'; /** * @property {object} cleanHTML {@link cleanHtml|cleanHtml}'s options * @property {boolean} cleanHTML.cleanOnPaste=true clean pasted html * @property {boolean} cleanHTML.replaceNBSP=true Replace &amp;nbsp; toWYSIWYG plain space * @property {boolean} cleanHTML.allowTags=false The allowTags option defines which elements will remain in the * edited text when the editor saves. You can use this toWYSIWYG limit the returned HTML toWYSIWYG a subset. * @example * ```javascript * var jodit = new Jodit('#editor', { * cleanHTML: { * cleanOnPaste: false * } * }); * ``` * @example * ```javascript * var editor = Jodit('#editor', { * cleanHTML: { * allowTags: 'p,a[href],table,tr,td, img[src=1.png]' // allow only <p>,<a>,<table>,<tr>,<td>,<img> tags and * for <a> allow only `href` attribute and <img> allow only `src` atrribute == '1.png' * } * }); * editor.value = 'Sorry! <strong>Goodby</strong>\ * <span>mr.</span> <a style="color:red" href="http://xdsoft.net">Freeman</a>'; * console.log(editor.value); //Sorry! <a href="http://xdsoft.net">Freeman</a> * ``` * * @example * ```javascript * var editor = Jodit('#editor', { * cleanHTML: { * allowTags: { * p: true, * a: { * href: true * }, * table: true, * tr: true, * td: true, * img: { * src: '1.png' * } * } * } * }); * ``` */ declare module '../Config' { interface Config { cleanHTML: { timeout: number; replaceNBSP: boolean; cleanOnPaste: boolean; fillEmptyParagraph: boolean; removeEmptyElements: boolean; replaceOldTags: { [key: string]: string } | false; allowTags: false | string | { [key: string]: string }; denyTags: false | string | { [key: string]: string }; }; } } Config.prototype.cleanHTML = { timeout: 300, removeEmptyElements: true, fillEmptyParagraph: true, replaceNBSP: true, cleanOnPaste: true, replaceOldTags: { i: 'em', b: 'strong' }, allowTags: false, denyTags: false }; Config.prototype.controls.eraser = { command: 'removeFormat', tooltip: 'Clear Formatting' }; /** * Clean HTML after removeFormat and insertHorizontalRule command */ export function cleanHtml(editor: IJodit) { // TODO compare this functionality and plugin paste.ts if (editor.options.cleanHTML.cleanOnPaste) { editor.events.on('processPaste', (event: Event, html: string) => { return cleanFromWord(html); }); } const attributesReg = /([^\[]*)\[([^\]]+)]/, seperator = /[\s]*,[\s]*/, attrReg = /^(.*)[\s]*=[\s]*(.*)$/; const getHash = ( tags: false | string | IDictionary<string> ): IDictionary | false => { const tagsHash: IDictionary = {}; if (typeof tags === 'string') { tags.split(seperator).map((elm: string) => { elm = trim(elm); const attr: RegExpExecArray | null = attributesReg.exec(elm), allowAttributes: IDictionary<string | boolean> = {}, attributeMap = (attrName: string) => { attrName = trim(attrName); const val: string[] | null = attrReg.exec(attrName); if (val) { allowAttributes[val[1]] = val[2]; } else { allowAttributes[attrName] = true; } }; if (attr) { const attr2: string[] = attr[2].split(seperator); if (attr[1]) { attr2.forEach(attributeMap); tagsHash[attr[1].toUpperCase()] = allowAttributes; } } else { tagsHash[elm.toUpperCase()] = true; } }); return tagsHash; } if (tags) { Object.keys(tags).forEach((tagName: string) => { tagsHash[tagName.toUpperCase()] = tags[tagName]; }); return tagsHash; } return false; }; let current: Node | false; const allowTagsHash: IDictionary | false = getHash( editor.options.cleanHTML.allowTags ), denyTagsHash: IDictionary | false = getHash( editor.options.cleanHTML.denyTags ); const hasNotEmptyTextSibling = (node: Node, next = false): boolean => { let prev: Node | null = next ? node.nextSibling : node.previousSibling; while (prev) { if ( prev.nodeType === Node.ELEMENT_NODE || !Dom.isEmptyTextNode(prev) ) { return true; } prev = next ? prev.nextSibling : prev.previousSibling; } return false; }; const isRemovableNode = (node: Node): boolean => { if ( node.nodeType !== Node.TEXT_NODE && ((allowTagsHash && !allowTagsHash[node.nodeName]) || (denyTagsHash && denyTagsHash[node.nodeName])) ) { return true; } // remove extra br if ( current && node.nodeName === 'BR' && hasNotEmptyTextSibling(node) && !hasNotEmptyTextSibling(node, true) && Dom.up( node, node => Dom.isBlock(node, editor.editorWindow), editor.editor ) !== Dom.up( current, node => Dom.isBlock(node, editor.editorWindow), editor.editor ) ) { return true; } return ( editor.options.cleanHTML.removeEmptyElements && current !== false && node.nodeType === Node.ELEMENT_NODE && node.nodeName.match(IS_INLINE) !== null && !editor.selection.isMarker(node) && trim((node as Element).innerHTML).length === 0 && !Dom.isOrContains(node, current) ); }; editor.events .on( 'change afterSetMode afterInit mousedown keydown', debounce(() => { if ( !editor.isDestructed && editor.isEditorMode() && editor.selection ) { current = editor.selection.current(); let node: Node | null = null, work: boolean = false, i: number = 0; const remove: Node[] = [], replaceOldTags: { [key: string]: string } | false = editor.options.cleanHTML.replaceOldTags; if (replaceOldTags && current) { const tags: string = Object.keys(replaceOldTags).join( '|' ); if (editor.selection.isCollapsed()) { const oldParent: Node | false = Dom.closest( current, tags, editor.editor ); if (oldParent) { const selInfo = editor.selection.save(), tagName: string = replaceOldTags[ oldParent.nodeName.toLowerCase() ] || replaceOldTags[oldParent.nodeName]; Dom.replace( oldParent as HTMLElement, tagName, true, false, editor.editorDocument ); editor.selection.restore(selInfo); } } } const checkNode = ( nodeElm: Element | Node | null ): void => { if (nodeElm) { if (isRemovableNode(nodeElm)) { remove.push(nodeElm); return checkNode(nodeElm.nextSibling); } if ( editor.options.cleanHTML.fillEmptyParagraph && Dom.isBlock(nodeElm, editor.editorWindow) && Dom.isEmpty( nodeElm, /^(img|svg|canvas|input|textarea|form|br)$/ ) ) { const br: HTMLBRElement = editor.create.inside.element( 'br' ); nodeElm.appendChild(br); } if ( allowTagsHash && allowTagsHash[nodeElm.nodeName] !== true ) { const attributes: NamedNodeMap = (nodeElm as Element) .attributes; if (attributes && attributes.length) { const removeAttrs: string[] = []; for (i = 0; i < attributes.length; i += 1) { if ( !allowTagsHash[nodeElm.nodeName][ attributes[i].name ] || (allowTagsHash[nodeElm.nodeName][ attributes[i].name ] !== true && allowTagsHash[nodeElm.nodeName][ attributes[i].name ] !== attributes[i].value) ) { removeAttrs.push( attributes[i].name ); } } if (removeAttrs.length) { work = true; } removeAttrs.forEach((attr: string) => { (nodeElm as Element).removeAttribute( attr ); }); } } checkNode(nodeElm.firstChild); checkNode(nodeElm.nextSibling); } }; if (editor.editor.firstChild) { node = editor.editor.firstChild as Element; } checkNode(node); remove.forEach(Dom.safeRemove); if (remove.length || work) { editor.events && editor.events.fire('syncho'); } } }, editor.options.cleanHTML.timeout) ) // remove invisible chars if node has another chars .on('keyup', () => { if (editor.options.readonly) { return; } const currentNode: false | Node = editor.selection.current(); if (currentNode) { const currentParagraph: Node | false = Dom.up( currentNode, node => Dom.isBlock(node, editor.editorWindow), editor.editor ); if (currentParagraph) { Dom.all(currentParagraph, node => { if (node && node.nodeType === Node.TEXT_NODE) { if ( node.nodeValue !== null && consts.INVISIBLE_SPACE_REG_EXP.test( node.nodeValue ) && node.nodeValue.replace( consts.INVISIBLE_SPACE_REG_EXP, '' ).length !== 0 ) { node.nodeValue = node.nodeValue.replace( consts.INVISIBLE_SPACE_REG_EXP, '' ); if ( node === currentNode && editor.selection.isCollapsed() ) { editor.selection.setCursorAfter(node); } } } }); } } }) .on('afterCommand', (command: string) => { const sel: Select = editor.selection; let hr: HTMLHRElement | null, node: Node | null; switch (command.toLowerCase()) { case 'inserthorizontalrule': hr = editor.editor.querySelector('hr[id=null]'); if (hr) { node = Dom.next( hr, node => Dom.isBlock(node, editor.editorWindow), editor.editor, false ) as Node | null; if (!node) { node = editor.create.inside.element( editor.options.enter ); if (node) { Dom.after(hr, node as HTMLElement); } } sel.setCursorIn(node); } break; case 'removeformat': node = sel.current() as Node; const clean: (elm: Node) => false | void = (elm: Node) => { switch (elm.nodeType) { case Node.ELEMENT_NODE: Dom.each(elm, clean); if (elm.nodeName === 'FONT') { Dom.unwrap(elm); } else { // clean some "style" attributes in selected range [].slice .call((elm as Element).attributes) .forEach((attr: Attr) => { if ( [ 'src', 'href', 'rel', 'content' ].indexOf( attr.name.toLowerCase() ) === -1 ) { (elm as Element).removeAttribute( attr.name ); } }); normalizeNode(elm); } break; case Node.TEXT_NODE: if ( editor.options.cleanHTML.replaceNBSP && elm.nodeType === Node.TEXT_NODE && elm.nodeValue !== null && elm.nodeValue.match(consts.SPACE_REG_EXP) ) { elm.nodeValue = elm.nodeValue.replace( consts.SPACE_REG_EXP, ' ' ); } break; default: Dom.safeRemove(elm); } }; if (!sel.isCollapsed()) { editor.selection.eachSelection( (currentNode: Node): false | void => { clean(currentNode); } ); } else { while ( node && node.nodeType !== Node.ELEMENT_NODE && node !== editor.editor ) { clean(node); if (node) { node = node.parentNode; } } } break; } }); }