suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
289 lines (256 loc) • 7.85 kB
JavaScript
import { dom, env } from '../../helper';
const { _w, _d } = env;
/**
* @typedef {import('../schema/frameContext').FrameContextStore} ConfigFrameContextStore
* @typedef {import('../schema/context').ContextStore} ConfigContextStore
*/
/**
* @typedef {Object} ContextMap
* @property {(k: keyof ConfigContextStore) => HTMLElement|null} get - Get a DOM element from the context by key.
* @property {(k: keyof ConfigContextStore, v: HTMLElement) => void} set - Set a DOM element in the context by key.
* @property {(k: keyof ConfigContextStore) => boolean} has - Check if a key exists in the context.
* @property {(k: keyof ConfigContextStore) => boolean} delete - Delete a key from the context.
* @property {() => Object<keyof ConfigContextStore, HTMLElement|null>} [getAll] - Get all DOM elements in the context as an object.
* @property {number} size - Get context size
* @property {() => void} clear - Clear all elements in the context.
*/
/**
* @typedef {Object} FrameContextMap
* @property {<K extends keyof ConfigFrameContextStore>(k: K) => ConfigFrameContextStore[K]} get - Get a DOM element from the context by key.
* @property {<K extends keyof ConfigFrameContextStore>(k: K, v: ConfigFrameContextStore[K]) => void} set - Set a DOM element in the context by key.
* @property {<K extends keyof ConfigFrameContextStore>(k: K) => boolean} has - Check if a key exists in the context.
* @property {<K extends keyof ConfigFrameContextStore>(k: K) => boolean} delete - Delete a key from the context.
* @property {() => Object<keyof ConfigFrameContextStore, *>} [getAll] - Get all DOM elements in the context as an object.
* @property {(newMap: *) => void} [reset] - Reset the context with a new Map.
* @property {number} size - Get context size
* @property {() => void} clear - Clear all elements in the context.
*/
/**
* @description Provides Map-based access to editor DOM contexts (global and per-frame).
*/
export default class ContextProvider {
/**
* @description Frame root map
*/
#frameRoots;
/**
* @description Utility object that manages the editor's runtime context.
* Provides methods to get, set, and inspect internal context.
*/
#contextMap;
/**
* @description Utility object that manages the editor's runtime [frame] context.
* Provides methods to get, set, and inspect internal context.
* @type {FrameContextMap}
*/
#frameContextMap;
/**
* @constructor
* @param {import('../section/constructor').ConstructorReturnType} product
*/
constructor(product) {
this.#frameRoots = product.frameRoots;
this.#contextMap = this.#CreateContextMap(product.context);
this.#frameContextMap = this.#CreateFrameContextMap({ value: new Map() });
/**
* @description Frame root key array
* @type {Array<*>}
*/
this.rootKeys = product.rootKeys;
/**
* @description Controllers carrier
* @type {HTMLElement}
*/
this.carrierWrapper = product.carrierWrapper;
/**
* @description Default icons object
* @type {Object<string, string>}
*/
this.icons = product.icons;
/**
* @description loaded language
* @type {Object<string, *>}
*/
this.lang = product.lang;
/**
* @description Closest ShadowRoot to editor if found
* @type {ShadowRoot & { getSelection?: () => Selection }} - Chromium-based browsers (Chrome, Edge, etc.) has a getSelection method on the ShadowRoot
*/
this.shadowRoot = null;
}
get frameRoots() {
return this.#frameRoots;
}
get context() {
return this.#contextMap;
}
get frameContext() {
return this.#frameContextMap;
}
init() {
this.applyToRoots((e) => {
this.#setEditorParams(e);
});
}
/**
* @param {SunEditor.FrameContext} rt Root target[key] FrameContext
*/
reset(rt) {
this.#frameContextMap.reset(rt);
}
/**
* @description Execute a function by traversing all root targets.
* @param {(frameContext: SunEditor.FrameContext, rootKey: string|null, frameRoots: Map<string|null, SunEditor.FrameContext>) => void} f Callback function
*/
applyToRoots(f) {
this.#frameRoots.forEach(f);
}
/**
* @description Set the FrameContext parameters and options
* @param {SunEditor.FrameContext} e - Frame context object
*/
#setEditorParams(e) {
const frameOptions = e.get('options');
e.set('wwComputedStyle', _w.getComputedStyle(e.get('wysiwyg')));
if (!frameOptions.get('iframe') && typeof ShadowRoot === 'function') {
let child = e.get('wysiwygFrame');
while (child) {
if (child.shadowRoot) {
this.shadowRoot = child.shadowRoot;
break;
} else if (child instanceof ShadowRoot) {
this.shadowRoot = child;
break;
}
child = /** @type {SunEditor.WysiwygFrame} */ (child.parentNode);
}
}
// init, validate
if (frameOptions.get('iframe')) {
e.set('_ww', e.get('wysiwygFrame').contentWindow);
e.set('_wd', e.get('wysiwygFrame').contentDocument);
e.set('wysiwyg', e.get('_wd').body);
// e.get('wysiwyg').className += ' ' + options.get('_editableClass');
if (frameOptions.get('_defaultStyles').editor) e.get('wysiwyg').style.cssText = frameOptions.get('_defaultStyles').editor;
if (frameOptions.get('height') === 'auto') e.set('_iframeAuto', e.get('_wd').body);
} else {
e.set('_ww', _w);
e.set('_wd', _d);
}
// wisywig attributes
const attr = frameOptions.get('editableFrameAttributes');
for (const k in attr) {
e.get('wysiwyg').setAttribute(k, attr[k]);
}
}
/**
* @description Creates a utility wrapper for editor base options.
* - Provides get, set, has, getAll with internal Map support.
* @param {*} _context - Origin options object
* @returns {ContextMap}
*/
#CreateContextMap(_context) {
const store = _context;
return {
get(k) {
return store.get(k);
},
set(k, v) {
return store.set(k, v);
},
has(k) {
return store.has(k);
},
delete(k) {
return store.delete(k);
},
getAll() {
return Object.fromEntries(store.entries());
},
get size() {
return store.size;
},
clear() {
store.clear();
},
};
}
/**
* @description Creates a utility wrapper for editor base options.
* - Provides get, set, has, getAll with internal Map support.
* @param {{value: Map}} _frameContext - The editor instance
* @returns {FrameContextMap}
*/
#CreateFrameContextMap(_frameContext) {
let store = _frameContext.value;
return {
/**
* @template {keyof ConfigFrameContextStore} K
* @param {K} k
* @returns {ConfigFrameContextStore[K]}
*/
get(k) {
return store.get(k);
},
/**
* @template {keyof ConfigFrameContextStore} K
* @param {K} k
* @param {ConfigFrameContextStore[K]} v
*/
set(k, v) {
return store.set(k, v);
},
/**
* @template {keyof ConfigFrameContextStore} K
* @param {K} k
* @returns {boolean}
*/
has(k) {
return store.has(k);
},
/**
* @template {keyof ConfigFrameContextStore} K
* @param {K} k
* @returns {boolean}
*/
delete(k) {
return store.delete(k);
},
getAll() {
return Object.fromEntries(store.entries());
},
/** @param {*} newMap */
reset(newMap) {
store = _frameContext.value = newMap;
},
get size() {
return store.size;
},
clear() {
store.clear();
},
};
}
_destroy() {
/** clear frame roots */
this.applyToRoots((e) => {
// destroy documentType instance
const docType = e.get('documentType');
if (docType) {
docType._destroy();
}
dom.utils.removeItem(e.get('topArea'));
e.get('options').clear();
e.clear();
});
this.#contextMap.clear();
this.#frameContextMap.clear();
this.#frameRoots.clear();
dom.utils.removeItem(this.carrierWrapper);
this.rootKeys = null;
this.carrierWrapper = null;
this.icons = null;
this.lang = null;
this.shadowRoot = null;
}
}