UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

176 lines (155 loc) 5.57 kB
import { numbers } from '../../helper'; /** * @typedef {Object} StoreState * @property {*} rootKey - Current root frame key. * @property {boolean} hasFocus - Whether the editor has focus. * @property {number} tabSize - Tab character space count. * @property {number} indentSize - `block` indent margin size (px). * @property {number} codeIndentSize - Code view indent space count. * @property {Array<string>} currentNodes - Selection path tag names (for navigation bar). * @property {Array<string>} currentNodesMap - Active command/style names from selection path. * @property {number} initViewportHeight - Viewport height at initialization. * @property {number} currentViewportHeight - Current visual viewport height. * @property {boolean} controlActive - Whether a controller or component is currently active, used to manage `blur`/`focus` behavior. * @property {(fc: SunEditor.FrameContext) => boolean} isScrollable - Whether the frame content is scrollable (derived from `height`/`maxHeight` options). * @property {?Node} _lastSelectionNode - Last selection node processed by `selectionState.update()` (cache for dedup). * @property {?Range} _range - Cached selection range. * @property {boolean} _mousedown - Whether `mousedown` is pressed. * @property {boolean} _preventBlur - Suppress `blur` event handling. * @property {boolean} _preventFocus - Suppress `focus` event handling. */ /** * @typedef {Object} StoreMode - Toolbar display mode flags (immutable after init). * @property {boolean} isClassic - Whether the toolbar is in classic (top-fixed) mode. * @property {boolean} isInline - Whether the toolbar is in `inline` mode (appears above the editor on focus). * @property {boolean} isBalloon - Whether the toolbar is in `balloon` mode (appears on text selection). * @property {boolean} isBalloonAlways - Whether the toolbar is in `balloon-always` mode (always visible as floating). * @property {boolean} isSubBalloon - Whether the sub-toolbar is in `balloon` mode. * @property {boolean} isSubBalloonAlways - Whether the sub-toolbar is in `balloon-always` mode. * @property {boolean} isBottom - Whether the toolbar is placed at the bottom of the editor (`classic:bottom`, `inline:bottom`). */ /** * @description Central runtime state management for the editor. * - Does not store DOM references (kept in `frameContext`). * - Does not store configuration values (kept in `options`). * - Only manages runtime state. */ class Store { /** @type {StoreState} */ #state; #subscribers = new Map(); /** * @param {import('../section/constructor').ConstructorReturnType} product - Constructor product */ constructor(product) { const options = product.options; const mode = options.get('mode'); const subMode = options.get('_subMode'); /** * @internal * @description If `true`, initialize all indexes of image, video information * @type {boolean} */ this._editorInitFinished = false; /** @type {StoreMode} */ this.mode = { isClassic: /classic/i.test(mode), isInline: /inline/i.test(mode), isBalloon: /balloon/i.test(mode), isBalloonAlways: /balloon-always/i.test(mode), isSubBalloon: /balloon/i.test(subMode), isSubBalloonAlways: /balloon-always/i.test(subMode), isBottom: !!options.get('_toolbar_bottom'), }; this.#state = { rootKey: product.rootId, hasFocus: false, tabSize: 4, indentSize: 25, codeIndentSize: 2, currentNodes: [], currentNodesMap: [], initViewportHeight: 0, currentViewportHeight: 0, controlActive: false, _lastSelectionNode: null, isScrollable: (fc) => { const fo = fc.get('options'); const height = fo.get('height'); const maxHeight = fo.get('maxHeight'); if (height !== 'auto') { return true; } if (!maxHeight) { return false; } // height === 'auto' && maxHeight return fc.get('wysiwyg').offsetHeight >= numbers.get(maxHeight); }, _range: null, _mousedown: false, _preventBlur: false, _preventFocus: false, }; } /** * @description Get state value (supports underscore notation) * @template {keyof StoreState} K * @param {K} key * @returns {StoreState[K]} */ get(key) { return this.#state[key]; } /** * @description Set state value and notify subscribers * @template {keyof StoreState} K * @param {K} key * @param {StoreState[K]} value - Value to set */ set(key, value) { const oldValue = this.#state[key]; this.#state[key] = value; // Notify subscribers this.#notify(key, value, oldValue); } /** * @description Subscribe to state changes * @template {keyof StoreState} K * @param {K} path - Path to subscribe * @param {(newValue: StoreState[K], oldValue: StoreState[K]) => void} callback * @returns {() => void} Unsubscribe function */ subscribe(path, callback) { if (!this.#subscribers.has(path)) { this.#subscribers.set(path, new Set()); } this.#subscribers.get(path).add(callback); return () => this.#subscribers.get(path).delete(callback); } /** * @param {keyof StoreState} path * @param {*} newValue * @param {*} oldValue */ #notify(path, newValue, oldValue) { const subscribers = this.#subscribers.get(path); if (!subscribers) return; for (const cb of subscribers) { try { cb(newValue, oldValue); } catch (e) { console.error(`[Store] Subscriber error for "${path}":`, e); } } } // Internal API _reset() { // Reset to initial state } _destroy() { this.#subscribers.clear(); this.#state = null; } } export default Store;