UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

487 lines (438 loc) 15.1 kB
import { dom, converter } from '../../helper'; import { InitOptions, CreateStatusbar } from '../section/constructor'; import { OPTION_FRAME_FIXED_FLAG, OPTION_FIXED_FLAG } from '../schema/options'; import { UpdateStatusbarContext } from '../schema/frameContext'; /** * @typedef {import('../schema/options').ProcessedBaseOptions} ConfigAllBaseOptions * @typedef {import('../schema/options').ProcessedFrameOptions} ConfigAllFrameOptions */ /** * @typedef {Object} BaseOptionsMap * - A Map containing all processed editor base options. * - This Map contains all keys from {@link ConfigAllBaseOptions}, where: * - Keys are option names (string) * - Values depend on the specific option (see {@link ConfigAllBaseOptions} for details) * * @property {<K extends keyof ConfigAllBaseOptions>(k: K) => ConfigAllBaseOptions[K]} get - Retrieves the value of a specific option. * @property {<K extends keyof ConfigAllBaseOptions>(k: K, v: ConfigAllBaseOptions[K]) => void} set - Sets the value of a specific option. * @property {<K extends keyof ConfigAllBaseOptions>(k: K) => boolean} has - Checks if a specific option exists. * @property {() => Object<keyof ConfigAllBaseOptions, *>} getAll - Retrieves all options as an object. * @property {(options: Map<*, *>) => void} setMany - Sets multiple options at once. * @property {(newMap: SunEditor.InitOptions) => void} reset - Replaces all options with a new Map. * @property {() => number} size - Get option size * @property {() => void} clear - Clears all stored options. * @property {() => IterableIterator<[keyof ConfigAllBaseOptions, *]>} entries - Returns an iterator of [key, value] pairs. * @property {() => IterableIterator<keyof ConfigAllBaseOptions>} keys - Returns an iterator of keys. * @property {() => IterableIterator<*>} values - Returns an iterator of values. * @property {(callbackfn: (value: *, key: string, map: Map<string, *>) => void) => void} forEach - Executes a function for each entry. */ /** * @typedef {Object} FrameOptionsMap * - A Map containing all processed frame-level options. * - This Map contains all keys from {@link ConfigAllFrameOptions}, where: * - Keys are option names (string) * - Values depend on the specific option (see {@link ConfigAllFrameOptions} for details) * * @property {<K extends keyof ConfigAllFrameOptions>(k: K) => ConfigAllFrameOptions[K]} get - Retrieves the value of a specific option. * @property {<K extends keyof ConfigAllFrameOptions>(k: K, v: ConfigAllFrameOptions[K]) => void} set - Sets the value of a specific option. * @property {<K extends keyof ConfigAllFrameOptions>(k: K) => boolean} has - Checks if a specific option exists. * @property {() => Object<keyof ConfigAllFrameOptions, *>} getAll - Retrieves all options as an object. * @property {(options: Map<*, *>) => void} setMany - Sets multiple options at once. * @property {(newMap: SunEditor.FrameOptions) => void} reset - Replaces all options with a new Map. * @property {() => number} size - Get option size * @property {() => void} clear - Clears all stored options. * @property {() => IterableIterator<[keyof ConfigAllFrameOptions, *]>} entries - Returns an iterator of [key, value] pairs. * @property {() => IterableIterator<keyof ConfigAllFrameOptions>} keys - Returns an iterator of keys. * @property {() => IterableIterator<*>} values - Returns an iterator of values. * @property {(callbackfn: (value: *, key: string, map: Map<string, *>) => void) => void} forEach - Executes a function for each entry. */ /** * @description Provides Map-based access to editor options (base and per-frame). */ export default class OptionProvider { #kernel; /** * @description Origin options * @type {SunEditor.InitOptions} */ #originOptions; /** * @description Utility object that manages the editor's runtime options. * Provides methods to get, set, and inspect internal editor options. * @type {BaseOptionsMap} */ #optionsMap; /** * @description Utility object that manages the editor's runtime [frame] options. * Provides methods to get, set, and inspect internal [frame] options. * @type {FrameOptionsMap} */ #frameOptionsMap; /** * @constructor * @param {SunEditor.Kernel} kernel * @param {import('../section/constructor').ConstructorReturnType} product */ constructor(kernel, product, options) { this.#kernel = kernel; this.#originOptions = options; this.#optionsMap = this.#CreateOptionsMap({ value: product.options }); this.#frameOptionsMap = this.#CreateFrameOptionsMap({ value: new Map() }); } /** * @return {BaseOptionsMap} */ get options() { return this.#optionsMap; } get frameOptions() { return this.#frameOptionsMap; } /** * @description Add or reset option property (Editor is reloaded) * @param {SunEditor.InitOptions} newOptions Options */ reset(newOptions) { const { $ } = this.#kernel; // use kernel const frameRoots = $.frameRoots; const context = $.context; const eventManager = $.eventManager; const format = $.format; const html = $.html; const char = $.char; const viewer = $.viewer; const plugins = $.plugins; const history = $.history; const ui = $.ui; const eventOrchestrator = this.#kernel._eventOrchestrator; viewer.codeView(false); viewer.showBlocks(false); const rootDiff = new Map(); const newRoots = []; const newRootKeys = new Map(); // frame roots const nRoot = {}; for (const k in newOptions) { if (OPTION_FRAME_FIXED_FLAG[k] === undefined) continue; nRoot[k] = newOptions[k]; delete newOptions[k]; } for (const rootKey of frameRoots.keys()) { newOptions[rootKey || ''] = { ...nRoot, ...newOptions[rootKey || ''] }; } // check reoption validation const newOptionKeys = Object.keys(newOptions); this.#CheckResetKeys(newOptionKeys, plugins, ''); if (newOptionKeys.length === 0) return; if (frameRoots.size === 1) { newOptionKeys.unshift(null); } // option merge const _originOptions = [this.#originOptions, newOptions].reduce((init, option) => { for (const key in option) { if (frameRoots.has(key || null)) { this.#RestoreFrameOptions(key, option, frameRoots, rootDiff, newRootKeys, newRoots); } else { init[key] = option[key]; } } return init; }, /** @type {SunEditor.InitOptions} */ ({})); // init options const options = this.#optionsMap; const newO = InitOptions(_originOptions, newRoots, plugins); const newOptionMap = newO.o; const newFrameMap = newO.frameMap; /** --------- [root start] --------- */ for (let i = 0, len = newOptionKeys.length, k; i < len; i++) { k = /** @type {keyof ConfigAllBaseOptions} */ (newOptionKeys[i] || null); if (newRootKeys.has(k)) { const diff = rootDiff.get(k); const fc = frameRoots.get(k); const originOptions = fc.get('options'); const newRootOptions = newFrameMap.get(k); // --- merge frame options --- for (const [mk, mv] of newRootOptions.entries()) { originOptions.set(mk, mv); } // statusbar-changed if (diff.has('statusbar-changed')) { // statusbar dom.utils.removeItem(fc.get('statusbar')); if (newRootOptions.get('statusbar')) { const statusbar = CreateStatusbar(newRootOptions, null).statusbar; fc.get('container').appendChild(statusbar); UpdateStatusbarContext(statusbar, fc); eventOrchestrator.__addStatusbarEvent(fc, newRootOptions); } else { eventManager.removeEvent(originOptions.get('__statusbarEvent')); newRootOptions.set('__statusbarEvent', null); UpdateStatusbarContext(null, fc); } // charCounter if (fc.get('statusbar')) { char.display(fc); } } // iframe's options if (diff.has('iframe_attributes')) { const frame = fc.get('wysiwygFrame'); const originAttr = originOptions.get('iframe_attributes'); const newAttr = newRootOptions.get('iframe_attributes'); for (const origin_k in originAttr) frame.removeAttribute(origin_k); for (const new_k in newAttr) frame.setAttribute(new_k, newAttr[new_k]); } if (diff.has('iframe_cssFileName')) { const docHead = fc.get('_wd').head; const links = docHead.getElementsByTagName('link'); while (links[0]) docHead.removeChild(links[0]); const parseDocument = new DOMParser().parseFromString(converter._setIframeStyleLinks(newRootOptions.get('iframe_cssFileName')), 'text/html'); const newLinks = parseDocument.head.children; const sTag = docHead.querySelector('style'); while (newLinks[0]) docHead.insertBefore(newLinks[0], sTag); } if (diff.has('placeholder')) { fc.get('placeholder').textContent = newRootOptions.get('placeholder'); } // frame styles ui.setEditorStyle(newRootOptions.get('editorStyle'), fc); // frame attributes const frame = fc.get('wysiwyg'); const originAttr = originOptions.get('editableFrameAttributes'); const newAttr = newRootOptions.get('editableFrameAttributes'); for (const origin_k in originAttr) frame.removeAttribute(origin_k); for (const new_k in newAttr) frame.setAttribute(new_k, newAttr[new_k]); continue; } /** --------- [root end] --------- */ /** Options that require a function call */ switch (k) { case 'theme': { ui.setTheme(newOptionMap.get('theme')); break; } case 'events': { const events = newOptionMap.get('events'); for (const name in events) { eventManager.events[name] = events[name]; } break; } case 'autoStyleify': { html.__resetAutoStyleify(newOptionMap.get('autoStyleify')); break; } case 'textDirection': { ui.setDir(newOptionMap.get('textDirection') === 'rtl' ? 'rtl' : 'ltr'); break; } case 'historyStackDelayTime': { history.resetDelayTime(newOptionMap.get('historyStackDelayTime')); break; } case 'defaultLineBreakFormat': { format.__resetBrLineBreak(newOptionMap.get('defaultLineBreakFormat')); } } } // --- set options --- options.setMany(newOptionMap); /** apply options */ // _origin this.#originOptions = _originOptions; // --- [toolbar] --- const toolbar = context.get('toolbar_main'); // width if (/inline|balloon/i.test(options.get('mode')) && newOptionKeys.includes('toolbar_width')) { toolbar.style.width = options.get('toolbar_width'); } // hide if (options.get('toolbar_hide')) { toolbar.style.display = 'none'; } else { toolbar.style.display = ''; } // shortcuts hint if (options.get('shortcutsHint')) { dom.utils.removeClass(toolbar, 'se-shortcut-hide'); } else { dom.utils.addClass(toolbar, 'se-shortcut-hide'); } } /** * @description Add or reset frame option property (Editor is reloaded) * @param {SunEditor.FrameOptions} newOptions Options */ resetFrame(newOptions) { this.#frameOptionsMap.reset(newOptions); } #RestoreFrameOptions(key, option, frameRoots, rootDiff, newRootKeys, newRoots) { const nro = option[key]; const newKeys = Object.keys(nro); this.#CheckResetKeys(newKeys, null, key + '.'); if (newKeys.length === 0) return false; const rootKey = key || null; rootDiff.set(rootKey, new Map()); const o = frameRoots.get(rootKey).get('options').get('_origin'); const no = {}; const hasOwn = Object.prototype.hasOwnProperty; for (const rk in nro) { if (!hasOwn.call(OPTION_FRAME_FIXED_FLAG, rk)) continue; const roV = nro[rk]; if (!newKeys.includes(rk) || o[rk] === roV) continue; rootDiff.get(rootKey).set(this.#GetResetDiffKey(rk), true); no[rk] = roV; } const newO = { ...o, ...no }; newRootKeys.set(rootKey, new Map(Object.entries(newO))); newRoots.push({ key: rootKey, options: newO }); } #GetResetDiffKey(key) { if (/^statusbar|^charCounter|^wordCounter/.test(key)) return 'statusbar-changed'; return key; } #CheckResetKeys(keys, plugins, root) { for (let i = 0, len = keys.length, k; i < len; i++) { k = keys[i]; if (OPTION_FIXED_FLAG[k] === 'fixed' || OPTION_FRAME_FIXED_FLAG[k] === 'fixed' || (plugins && plugins[k])) { console.warn(`[SUNEDITOR.warn.resetOptions] The "[${root + k}]" option cannot be changed after the editor is created.`); keys.splice(i--, 1); len--; } } } /** * @description Creates a utility wrapper for editor base options. * - Provides get, set, has, getAll, and setMany methods with internal Map support. * @param {*} _options - Origin options object * @returns {BaseOptionsMap} */ #CreateOptionsMap(_options) { let store = _options.value; return { /** * @template {keyof ConfigAllBaseOptions} K * @param {K} k * @returns {ConfigAllBaseOptions[K]} */ get(k) { return store.get(k); }, /** * @template {keyof ConfigAllBaseOptions} K * @param {K} k * @param {ConfigAllBaseOptions[K]} v */ set(k, v) { return store.set(k, v); }, /** * @template {keyof ConfigAllBaseOptions} K * @param {K} k * @returns {boolean} */ has(k) { return store.has(k); }, getAll() { return Object.fromEntries(store.entries()); }, /** @param {Map<*, *>} obj */ setMany(obj) { obj.forEach((v, k) => store.set(k, v)); }, /** @param {SunEditor.InitOptions} newMap */ reset(newMap) { store = _options.value = newMap; }, size() { return store.size; }, clear() { store.clear(); }, entries() { return store.entries(); }, keys() { return store.keys(); }, values() { return store.values(); }, forEach(callbackfn) { store.forEach(callbackfn); }, }; } /** * @description Creates a utility wrapper for editor frame options. * Provides get, set, has, getAll, and setMany methods with internal Map support. * @param {*} _options - Origin options object * @returns {FrameOptionsMap} */ #CreateFrameOptionsMap(_options) { let store = _options.value; return { /** * @template {keyof ConfigAllFrameOptions} K * @param {K} k * @returns {ConfigAllFrameOptions[K]} */ get(k) { return store.get(k); }, /** * @template {keyof ConfigAllFrameOptions} K * @param {K} k * @param {ConfigAllFrameOptions[K]} v */ set(k, v) { return store.set(k, v); }, /** * @template {keyof ConfigAllFrameOptions} K * @param {K} k * @returns {boolean} */ has(k) { return store.has(k); }, getAll() { return Object.fromEntries(store.entries()); }, /** @param {Map<*, *>} obj */ setMany(obj) { obj.forEach((v, k) => store.set(k, v)); }, /** @param {SunEditor.FrameOptions} newMap */ reset(newMap) { store = _options.value = newMap; }, size() { return store.size; }, clear() { store.clear(); }, entries() { return store.entries(); }, keys() { return store.keys(); }, values() { return store.values(); }, forEach(callbackfn) { store.forEach(callbackfn); }, }; } _destroy() { this.#originOptions = null; this.#optionsMap.clear(); this.#frameOptionsMap.clear(); } }