UNPKG

jodit

Version:

Jodit is an awesome and useful wysiwyg editor with filebrowser

1,019 lines (1,017 loc) 32.4 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Released under MIT see LICENSE.txt in the project root for license information. * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net */ /** * Editor options. These can be configured upon the creation of the editor. * ```javascript * const editor = Jodit.make('#editor', { * toolbar: true, * buttons: ['bold', 'italic', 'underline'] * // other options * // ... * }); * ``` * @packageDocumentation * @module config */ import * as consts from "./core/constants.js"; import { globalDocument, globalWindow, INSERT_AS_HTML } from "./core/constants.js"; let ConfigPrototype = {}; /** * Default Editor's Configuration. * * This class holds all default option values for the Jodit editor. * It uses a **private constructor** and a **lazy singleton** pattern — the single instance * is created on the first access to {@link Config.defaultOptions} (also available as `Jodit.defaultOptions`). * * ## How options are resolved * * When you create an editor with `Jodit.make('#editor', userOptions)`, the library * calls {@link ConfigProto}(userOptions, Config.defaultOptions). `ConfigProto` does * **not** deep-clone the defaults. Instead it creates a new object whose JavaScript * prototype is `Config.defaultOptions`: * * ``` * userOptions ──[[Prototype]]──► Config.defaultOptions * ``` * * Any key present in `userOptions` shadows the default; * any key **not** present falls through to `Config.defaultOptions` via the prototype chain. * Nested plain objects are recursively prototyped in the same way, so partial overrides * of nested options work automatically: * * ```js * // Only override `dialogWidth`; all other `image.*` defaults are still available * Jodit.make('#editor', { * image: { dialogWidth: 500 } * }); * ``` * * ## How plugins extend the config * * Each plugin adds its own defaults by assigning to `Config.prototype` and augmenting * the TypeScript type with `declare module`: * * ```ts * // 1. Type augmentation (compile-time) * declare module 'jodit/config' { * interface Config { * toolbarSticky: boolean; * } * } * * // 2. Runtime default * Config.prototype.toolbarSticky = true; * ``` * * Because the constructor runs `Object.assign(this, ConfigPrototype)` (where * `ConfigPrototype` is captured as `Config.prototype` after the class definition), * all prototype-level values — including those added by plugins — are materialized * as own properties on the singleton. This means `Config.defaultOptions` always * contains every registered option as an own, enumerable property. * * ## Changing global defaults * * You can modify `Jodit.defaultOptions` **before** creating editors to change * defaults globally: * * ```js * Jodit.defaultOptions.language = 'de'; * Jodit.defaultOptions.theme = 'dark'; * * // Both editors inherit the new defaults * Jodit.make('#editor1'); * Jodit.make('#editor2'); * ``` * * ## `Jodit.atom` — preventing deep merge * * By default, `ConfigProto` deep-merges nested plain objects and arrays. * Wrap a value with `Jodit.atom(value)` to make it **atomic** — it will completely * replace the default instead of being merged: * * ```js * Jodit.make('#editor', { * controls: { * fontsize: { * // Replace the entire list rather than merging with the default one * list: Jodit.atom([8, 9, 10]) * } * } * }); * ``` * * `Jodit.atom` calls {@link markAsAtomic}, which sets a non-enumerable * `isAtom` flag on the object. `ConfigProto` checks this flag and skips * recursive merging when it is present. Note: top-level arrays (depth 0) * are always treated as atomic — they replace rather than merge. * * @see {@link ConfigProto} for the full merge algorithm * @see {@link markAsAtomic} / {@link isAtom} for the atom marker implementation */ class Config { constructor() { /** * When enabled, the editor caches the results of expensive computations (e.g. toolbar rebuilds) * to improve performance. Disable for debugging or when options change frequently at runtime. */ this.cache = true; /** * Timeout of all asynchronous methods */ this.defaultTimeout = 100; /** * Prefix used for CSS class names and local-storage keys to avoid collisions * when multiple editor instances or applications share the same page. */ this.namespace = ''; /** * Editor loads completely without plugins. Useful when debugging your own plugin. */ this.safeMode = false; /** * Editor's width * * ```javascript * Jodit.make('.editor', { * width: '100%', * }) * ``` * ```javascript * Jodit.make('.editor', { * width: 600, // equivalent for '600px' * }) * ``` * ```javascript * Jodit.make('.editor', { * width: 'auto', // autosize * }) * ``` */ this.width = 'auto'; /** * Editor's height * * ```javascript * Jodit.make('.editor', { * height: '100%', * }) * ``` * ```javascript * Jodit.make('.editor', { * height: 600, // equivalent for '600px' * }) * ``` * ```javascript * Jodit.make('.editor', { * height: 'auto', // default - autosize * }) * ``` */ this.height = 'auto'; /** * List of plugins that will be initialized in safe mode. * * ```js * Jodit.make('#editor', { * safeMode: true, * safePluginsList: ['about'], * extraPlugins: ['yourPluginDev'] * }); * ``` */ this.safePluginsList = [ 'about', 'enter', 'backspace', 'size', 'bold', 'hotkeys' ]; /** * Reserved for the paid version of the editor */ this.license = ''; /** * The name of the preset that will be used to initialize the editor. * The list of available presets can be found here Jodit.defaultOptions.presets * ```javascript * Jodit.make('.editor', { * preset: 'inline' * }); * ``` */ this.preset = 'custom'; /** * Dictionary of named configuration presets. Each key is a preset name and the value * is a partial options object that will be merged into the editor config when * {@link Config.preset} matches the key. * * ```javascript * // Use a built-in preset * Jodit.make('#editor', { * preset: 'inline' * }); * ``` * * ```javascript * // Define and use a custom preset * Jodit.defaultOptions.presets.myCompact = { * toolbarButtonSize: 'small', * showCharsCounter: false, * showWordsCounter: false, * showXPathInStatusbar: false * }; * * Jodit.make('#editor', { * preset: 'myCompact' * }); * ``` */ this.presets = { inline: { inline: true, toolbar: false, toolbarInline: true, toolbarInlineForSelection: true, showXPathInStatusbar: false, showCharsCounter: false, showWordsCounter: false, showPlaceholder: false } }; /** * The Document object the editor operates within. Defaults to the current `document`. * Override when the editor is created inside an iframe or a different browsing context. */ this.ownerDocument = globalDocument; /** * Allows you to specify the window in which the editor will be created. Default - window * This is necessary if you are creating the editor inside an iframe but the code is running in the parent window */ this.ownerWindow = globalWindow; /** * Shadow root if Jodit was created in it * * ```html * <div id="editor"></div> * ``` * * ```js * const app = document.getElementById('editor'); * app.attachShadow({ mode: 'open' }); * const root = app.shadowRoot; * * root.innerHTML = ` * <link rel="stylesheet" href="./build/jodit.css"/> * <h1>Jodit example in Shadow DOM</h1> * <div id="edit"></div> * `; * * const editor = Jodit.make(root.getElementById('edit'), { * globalFullSize: false, * shadowRoot: root * }); * editor.value = '<p>start</p>'; * ``` */ this.shadowRoot = null; /** * Base CSS `z-index` for the editor UI (toolbar, popups, dialogs). * Set to a higher value when other page elements overlap the editor. * `0` means no explicit z-index is applied. */ this.zIndex = 0; /** * Change the read-only state of the editor */ this.readonly = false; /** * Change the disabled state of the editor */ this.disabled = false; /** * In readOnly mode, some buttons can still be useful, for example, the button to view source code or print */ this.activeButtonsInReadOnly = [ 'source', 'fullsize', 'print', 'about', 'dots', 'selectall' ]; /** * When the editor is in read-only mode, some commands can still be executed: * ```javascript * const editor = Jodit.make('.editor', { * allowCommandsInReadOnly: ['selectall', 'preview', 'print'] * readonly: true * }); * editor.execCommand('selectall');// will be selected all content * editor.execCommand('delete');// but content will not be deleted * ``` */ this.allowCommandsInReadOnly = ['selectall', 'preview', 'print']; /** * Size of icons in the toolbar (can be "small", "middle", "large") * * ```javascript * const editor = Jodit.make(".dark_editor", { * toolbarButtonSize: "small" * }); * ``` */ this.toolbarButtonSize = 'middle'; /** * Allow navigation in the toolbar of the editor by Tab key */ this.allowTabNavigation = false; /** * When enabled, the editor renders without its own container chrome (toolbar, borders, statusbar). * The editable area becomes the element itself. Typically combined with * `toolbarInline: true` so a floating toolbar appears on selection. */ this.inline = false; /** * Theme (can be "dark") * * ```javascript * const editor = Jodit.make(".dark_editor", { * theme: "dark" * }); * ``` */ this.theme = 'default'; /** * if set true, then the current mode is saved in a cookie, and is restored after a reload of the page */ this.saveModeInStorage = false; /** * Class name that can be appended to the editable area * * @see {@link Config.iframeCSSLinks} * @see {@link Config.iframeStyle} * * ```javascript * Jodit.make('#editor', { * editorClassName: 'some_my_class' * }); * ``` * ```html * <style> * .some_my_class p{ * line-height: 16px; * } * </style> * ``` */ this.editorClassName = false; /** * Class name that can be appended to the main editor container * * ```javascript * const jodit = Jodit.make('#editor', { * className: 'some_my_class' * }); * * console.log(jodit.container.classList.contains('some_my_class')); // true * ``` * ```html * <style> * .some_my_class { * max-width: 600px; * margin: 0 auto; * } * </style> * ``` */ this.className = false; /** * The internal styles of the editable area. They are intended to change * not the appearance of the editor, but to change the appearance of the content. * * ```javascript * Jodit.make('#editor', { * style: { * font: '12px Arial', * color: '#0c0c0c' * } * }); * ``` */ this.style = false; /** * Inline CSS styles applied to the outer editor container element. * Use this to style the editor wrapper (borders, background, etc.) without affecting content. * * ```javascript * Jodit.make('#editor', { * containerStyle: { * border: '1px solid #ccc', * background: '#f9f9f9' * } * }); * ``` */ this.containerStyle = false; /** * Dictionary of variable values in css, a complete list can be found here * https://github.com/xdan/jodit/blob/main/src/styles/variables.less#L25 * * ```js * const editor = Jodit.make('#editor', { * styleValues: { * 'color-text': 'red', * colorBorder: 'black', * 'color-panel': 'blue' * } * }); * ``` */ this.styleValues = {}; /** * When enabled, the editor dispatches a native `change` event on the original * `<textarea>` element whenever the content changes, so standard DOM listeners work. * * ```javascript * const editor = Jodit.make('#editor'); * document.getElementById('editor').addEventListener('change', function () { * console.log(this.value); * }) * ``` */ this.triggerChangeEvent = true; /** * The writing direction of the language which is used to create editor content. Allowed values are: '' * (an empty string) – Indicates that content direction will be the same as either the editor UI direction or * the page element direction. 'ltr' – Indicates a Left-To-Right text direction (like in English). * 'rtl' – Indicates a Right-To-Left text direction (like in Arabic). * * ```javascript * Jodit.make('.editor', { * direction: 'rtl' * }) * ``` */ this.direction = ''; /** * Language by default. if `auto` language set by document.documentElement.lang || * (navigator.language && navigator.language.substr(0, 2)) || * (navigator.browserLanguage && navigator.browserLanguage.substr(0, 2)) || 'en' * * ```html * <!-- include in you page lang file --> * <script src="jodit/lang/de.js"></script> * <script> * var editor = Jodit.make('.editor', { * language: 'de' * }); * </script> * ``` */ this.language = 'auto'; /** * if true all Lang.i18n(key) return `{key}` * * ```html * <script> * var editor = Jodit.make('.editor', { * debugLanguage: true * }); * * console.log(editor.i18n("Test")); // {Test} * </script> * ``` */ this.debugLanguage = false; /** * Collection of language pack data `{en: {'Type something': 'Type something', ...}}` * * ```javascript * const editor = Jodit.make('#editor', { * language: 'ru', * i18n: { * ru: { * 'Type something': 'Начните что-либо вводить' * } * } * }); * console.log(editor.i18n('Type something')) //Начните что-либо вводить * ``` */ this.i18n = false; /** * The tabindex global attribute is an integer indicating if the element can take * input focus (is focusable), if it should participate to sequential keyboard navigation, * and if so, at what position. It can take several values */ this.tabIndex = -1; /** * Boolean, whether the toolbar should be shown. * Alternatively, a valid css-selector-string to use an element as toolbar container. */ this.toolbar = true; /** * Boolean, whether the statusbar should be shown. */ this.statusbar = true; /** * Show tooltip after mouse enter on the button */ this.showTooltip = true; /** * Delay before show tooltip */ this.showTooltipDelay = 200; /** * Instead of creating a custom tooltip, use the browser's native title tooltips */ this.useNativeTooltip = false; /** * How pasted content is inserted into the editor by default. * Possible values: `insert_as_html`, `insert_as_text`, `insert_only_text`, `insert_clear_html`. */ this.defaultActionOnPaste = INSERT_AS_HTML; // TODO // autosave: false, // false or url // autosaveCallback: false, // function // interval: 60, // seconds // TODO /** * Element that will be created when you press Enter */ this.enter = consts.PARAGRAPH; /** * When this option is enabled, the editor's content will be placed in an iframe and isolated from the rest of the page. * * ```javascript * Jodit.make('#editor', { * iframe: true, * iframeStyle: 'html{margin: 0px;}body{padding:10px;background:transparent;color:#000;position:relative;z-index:2;\ * user-select:auto;margin:0px;overflow:hidden;}body:after{content:"";clear:both;display:block}'; * }); * ``` */ this.iframe = false; /** * Allow editing the entire HTML document(html, head) * \> Works together with the iframe option. * * ```js * const editor = Jodit.make('#editor', { * iframe: true, * editHTMLDocumentMode: true * }); * editor.value = '<!DOCTYPE html><html lang="en" style="overflow-y:hidden">' + * '<head><title>Jodit Editor</title></head>' + * '<body spellcheck="false"><p>Some text</p><p> a </p></body>' + * '</html>'; * ``` */ this.editHTMLDocumentMode = false; /** * Use when you need to insert new block element * use enter option if not set */ this.enterBlock = this.enter !== 'br' ? this.enter : consts.PARAGRAPH; /** * Jodit.MODE_WYSIWYG The HTML editor allows you to write like MSWord, * Jodit.MODE_SOURCE syntax highlighting source editor * * ```javascript * var editor = Jodit.make('#editor', { * defaultMode: Jodit.MODE_SPLIT * }); * console.log(editor.getRealMode()) * ``` */ this.defaultMode = consts.MODE_WYSIWYG; /** * When enabled, the editor displays both the WYSIWYG view and the source-code view side by side. */ this.useSplitMode = false; /** * The colors in HEX representation to select a color for the background and for the text in colorpicker * * ```javascript * Jodit.make('#editor', { * colors: ['#ff0000', '#00ff00', '#0000ff'] * }) * ``` */ this.colors = { greyscale: [ '#000000', '#434343', '#666666', '#999999', '#B7B7B7', '#CCCCCC', '#D9D9D9', '#EFEFEF', '#F3F3F3', '#FFFFFF' ], palette: [ '#980000', '#FF0000', '#FF9900', '#FFFF00', '#00F0F0', '#00FFFF', '#4A86E8', '#0000FF', '#9900FF', '#FF00FF' ], full: [ '#E6B8AF', '#F4CCCC', '#FCE5CD', '#FFF2CC', '#D9EAD3', '#D0E0E3', '#C9DAF8', '#CFE2F3', '#D9D2E9', '#EAD1DC', '#DD7E6B', '#EA9999', '#F9CB9C', '#FFE599', '#B6D7A8', '#A2C4C9', '#A4C2F4', '#9FC5E8', '#B4A7D6', '#D5A6BD', '#CC4125', '#E06666', '#F6B26B', '#FFD966', '#93C47D', '#76A5AF', '#6D9EEB', '#6FA8DC', '#8E7CC3', '#C27BA0', '#A61C00', '#CC0000', '#E69138', '#F1C232', '#6AA84F', '#45818E', '#3C78D8', '#3D85C6', '#674EA7', '#A64D79', '#85200C', '#990000', '#B45F06', '#BF9000', '#38761D', '#134F5C', '#1155CC', '#0B5394', '#351C75', '#733554', '#5B0F00', '#660000', '#783F04', '#7F6000', '#274E13', '#0C343D', '#1C4587', '#073763', '#20124D', '#4C1130' ] }; /** * The default tab color picker * * ```javascript * Jodit.make('#editor2', { * colorPickerDefaultTab: 'color' * }) * ``` */ this.colorPickerDefaultTab = 'background'; /** * Default width (in pixels) applied to images inserted into the editor */ this.imageDefaultWidth = 300; /** * Do not display these buttons that are on the list * * ```javascript * Jodit.make('#editor2', { * removeButtons: ['hr', 'source'] * }); * ``` */ this.removeButtons = []; /** * Do not init these plugins * * ```typescript * var editor = Jodit.make('.editor', { * disablePlugins: 'table,iframe' * }); * //or * var editor = Jodit.make('.editor', { * disablePlugins: ['table', 'iframe'] * }); * ``` */ this.disablePlugins = []; /** * Init and download extra plugins * * ```typescript * var editor = Jodit.make('.editor', { * extraPlugins: ['emoji'] * }); * ``` * It will try load %SCRIPT_PATH%/plugins/emoji/emoji.js and after load will try init it */ this.extraPlugins = []; /** * Additional buttons appended to the {@link Config.buttons} list */ this.extraButtons = []; /** * By default, you can only install an icon from the Jodit suite. * You can add your icon to the set using the `Jodit.modules.Icon.set (name, svg Code)` method. * But for a declarative declaration, you can use this option. * * ```js * Jodit.modules.Icon.set('someIcon', '<svg><path.../></svg>'); * const editor = Jodit.make({ * extraButtons: [{ * name: 'someButton', * icon: 'someIcon' * }] * }); * ``` * * ```js * const editor = Jodit.make({ * extraIcons: { * someIcon: '<svg><path.../></svg>' * }, * extraButtons: [{ * name: 'someButton', * icon: 'someIcon' * }] * }); * ``` * * ```js * const editor = Jodit.make({ * extraButtons: [{ * name: 'someButton', * icon: '<svg><path.../></svg>' * }] * }); * ``` */ this.extraIcons = {}; /** * Default attributes for created inside editor elements * * ```js * const editor2 = Jodit.make('#editor', { * createAttributes: { * div: { * class: 'test' * }, * ul: function (ul) { * ul.classList.add('ui-test'); * } * } * }); * * const div2 = editor2.createInside.div(); * expect(div2.className).equals('test'); * * const ul = editor2.createInside.element('ul'); * expect(ul.className).equals('ui-test'); * ``` * Or JSX in React * * ```jsx * import React, {useState, useRef} from 'react'; * import JoditEditor from "jodit-react"; * * const config = { * createAttributes: { * div: { * class: 'align-center' * } * } * }; * * <JoditEditor config={config}/> * ``` */ this.createAttributes = { table: { style: 'border-collapse:collapse;width: 100%;' } }; /** * The width of the editor, accepted as the biggest. Used to the responsive version of the editor */ this.sizeLG = 900; /** * The width of the editor, accepted as the medium. Used to the responsive version of the editor */ this.sizeMD = 700; /** * The width of the editor, accepted as the small. Used to the responsive version of the editor */ this.sizeSM = 400; /** * The list of buttons that appear in the editor's toolbar on large places (≥ options.sizeLG). * Note - this is not the width of the device, the width of the editor * * ```javascript * Jodit.make('#editor', { * buttons: ['bold', 'italic', 'source'], * buttonsMD: ['bold', 'italic'], * buttonsXS: ['bold', 'fullsize'], * }); * ``` * * ```javascript * Jodit.make('#editor2', { * buttons: [{ * name: 'empty', * icon: 'source', * exec: function (editor) { * const dialog = new Jodit.modules.Dialog({}), * text = editor.c.element('textarea'); * * dialog.setHeader('Source code'); * dialog.setContent(text); * dialog.setSize(400, 300); * * Jodit.modules.Helpers.css(elm, { * width: '100%', * height: '100%' * }) * dialog.open(); * } * }] * }); * ``` * * ```javascript * Jodit.make('#editor2', { * buttons: Jodit.defaultOptions.buttons.concat([{ * name: 'listsss', * iconURL: 'stuf/dummy.png', * list: { * h1: 'insert Header 1', * h2: 'insert Header 2', * clear: 'Empty editor', * }, * exec: ({originalEvent, control, btn}) => { * var key = control.args[0], * value = control.args[1]; * if (key === 'clear') { * this.val(''); * return; * } * this.s.insertNode(this.c.element(key, '')); * this.message.info('Was inserted ' + value); * }, * template: function (key, value) { * return '<div>' + value + '</div>'; * } * }); * ``` */ this.buttons = [ { group: 'font-style', buttons: [] }, { group: 'list', buttons: [] }, { group: 'font', buttons: [] }, '---', { group: 'script', buttons: [] }, { group: 'media', buttons: [] }, '\n', { group: 'state', buttons: [] }, { group: 'clipboard', buttons: [] }, { group: 'insert', buttons: [] }, { group: 'indent', buttons: [] }, { group: 'color', buttons: [] }, { group: 'form', buttons: [] }, '---', { group: 'history', buttons: [] }, { group: 'search', buttons: [] }, { group: 'source', buttons: [] }, { group: 'other', buttons: [] }, { group: 'info', buttons: [] } ]; /** * Some events are called when the editor is initialized, for example, the `afterInit` event. * So this code won't work: * ```javascript * const editor = Jodit.make('#editor'); * editor.events.on('afterInit', () => console.log('afterInit')); * ``` * You need to do this: * ```javascript * Jodit.make('#editor', { * events: { * afterInit: () => console.log('afterInit') * } * }); * ``` * The option can use any Jodit events, for example: * ```javascript * const editor = Jodit.make('#editor', { * events: { * hello: (name) => console.log('Hello', name) * } * }); * editor.e.fire('hello', 'Mike'); * ``` */ this.events = {}; /** * Buttons in toolbar without SVG - only texts */ this.textIcons = false; /** * Element for dialog container */ this.popupRoot = null; /** * shows a INPUT[type=color] to open the browser color picker, on the right bottom of widget color picker */ this.showBrowserColorPicker = true; Object.assign(this, ConfigPrototype); } static get defaultOptions() { if (!Config.__defaultOptions) { Config.__defaultOptions = new Config(); } return Config.__defaultOptions; } } ConfigPrototype = Config.prototype; Config.prototype.controls = {}; export { Config };