/** * EditorContent.ts * * Released under LGPL License. * Copyright (c) 1999-2018 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ import { Editor } from 'tinymce/core/api/Editor'; import Node from 'tinymce/core/api/html/Node'; import Tools from 'tinymce/core/api/util/Tools'; import TrimHtml from 'tinymce/core/dom/TrimHtml'; import Serializer from 'tinymce/core/api/html/Serializer'; import * as FilterNode from './html/FilterNode'; import { Option, Fun } from '@ephox/katamari'; import Zwsp from 'tinymce/core/text/Zwsp'; import Settings from 'tinymce/core/api/Settings'; const defaultFormat = 'html'; export type Content = string | Node; export interface SetContentArgs { format?: string; set?: boolean; content?: string; no_events?: boolean; } export interface GetContentArgs { format?: string; get?: boolean; content?: string; getInner?: boolean; no_events?: boolean; } const isTreeNode = (content: any): content is Node => content instanceof Node; const setContentString = (editor: Editor, body: HTMLElement, content: string, args: SetContentArgs): string => { let forcedRootBlockName, padd; // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content // It will also be impossible to place the caret in the editor unless there is a BR element present if (content.length === 0 || /^\s+$/.test(content)) { padd = '
'; // Todo: There is a lot more root elements that need special padding // so separate this and add all of them at some point. if (body.nodeName === 'TABLE') { content = '' + padd + ''; } else if (/^(UL|OL)$/.test(body.nodeName)) { content = '
  • ' + padd + '
  • '; } forcedRootBlockName = Settings.getForcedRootBlock(editor); // Check if forcedRootBlock is configured and that the block is a valid child of the body if (forcedRootBlockName && editor.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) { content = padd; content = editor.dom.createHTML(forcedRootBlockName, editor.settings.forced_root_block_attrs, content); } else if (!content) { // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret content = '
    '; } editor.dom.setHTML(body, content); editor.fire('SetContent', args); } else { if (args.format !== 'raw') { content = Serializer({ validate: editor.validate }, editor.schema).serialize( editor.parser.parse(content, { isRootContent: true, insert: true }) ); } args.content = Tools.trim(content); editor.dom.setHTML(body, args.content); if (!args.no_events) { editor.fire('SetContent', args); } } return args.content as string; }; const setContentTree = (editor: Editor, body: HTMLElement, content: Node, args: SetContentArgs): Node => { FilterNode.filter(editor.parser.getNodeFilters(), editor.parser.getAttributeFilters(), content); const html = Serializer({ validate: editor.validate }, editor.schema).serialize(content); args.content = Tools.trim(html); editor.dom.setHTML(body, args.content); if (!args.no_events) { editor.fire('SetContent', args); } return content; }; const trimEmptyContents = (editor: Editor, html: string): string => { const blockName = Settings.getForcedRootBlock(editor); const emptyRegExp = new RegExp(`^(<${blockName}[^>]*>( | |\\s|\u00a0|
    |)<\\/${blockName}>[\r\n]*|
    [\r\n]*)$`); return html.replace(emptyRegExp, ''); }; const getContentFromBody = (editor, args, body) => { let content; args.format = args.format ? args.format : defaultFormat; args.get = true; args.getInner = true; if (!args.no_events) { editor.fire('BeforeGetContent', args); } if (args.format === 'raw') { content = Tools.trim(TrimHtml.trimExternal(editor.serializer, body.innerHTML)); } else if (args.format === 'text') { content = Zwsp.trim(body.innerText || body.textContent); } else if (args.format === 'tree') { return editor.serializer.serialize(body, args); } else { content = trimEmptyContents(editor, editor.serializer.serialize(body, args)); } if (args.format !== 'text') { args.content = Tools.trim(content); } else { args.content = content; } if (!args.no_events) { editor.fire('GetContent', args); } return args.content; }; const setContent = (editor: Editor, content: Content, args: SetContentArgs = {}): Content => { args.format = args.format ? args.format : defaultFormat; args.set = true; args.content = isTreeNode(content) ? '' : content; if (!isTreeNode(content) && !args.no_events) { editor.fire('BeforeSetContent', args); content = args.content; } return Option.from(editor.getBody()) .fold( Fun.constant(content), (body) => isTreeNode(content) ? setContentTree(editor, body, content, args) : setContentString(editor, body, content, args) ); }; const getContent = (editor: Editor, args: GetContentArgs = {}): Content => { return Option.from(editor.getBody()) .fold( Fun.constant(args.format === 'tree' ? new Node('body', 11) : ''), (body) => getContentFromBody(editor, args, body) ); }; export { setContent, getContent };