UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

425 lines (371 loc) 13.6 kB
import { PluginCommand, PluginDropdown, PluginInput } from '../../interfaces'; import { dom, numbers, keyCodeMap } from '../../helper'; void PluginCommand; void PluginDropdown; const DEFAULT_UNIT_MAP = { text: { default: '16px', list: [ { title: 'XX-Small', size: '8px', }, { title: 'X-Small', size: '10px', }, { title: 'Small', size: '13px', }, { title: 'Medium', size: '16px', }, { title: 'Large', size: '18px', }, { title: 'X-Large', size: '24px', }, { title: 'XX-Large', size: '32px', }, ], }, px: { default: 13, inc: 1, min: 8, max: 72, list: [8, 10, 13, 15, 18, 20, 22, 26, 28, 36, 48, 72], }, pt: { default: 10, inc: 1, min: 6, max: 72, list: [6, 8, 10, 12, 14, 18, 22, 26, 32], }, em: { default: 1, inc: 0.1, min: 0.5, max: 5, list: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3], }, rem: { default: 1, inc: 0.1, min: 0.5, max: 5, list: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3], }, vw: { inc: 0.1, min: 0.5, max: 10, list: [2, 3.5, 4, 4.5, 6, 8], }, vh: { default: 1.5, inc: 0.1, min: 0.5, max: 10, list: [1, 1.5, 2, 2.5, 3, 3.5, 4], }, '%': { default: 100, inc: 1, min: 50, max: 200, list: [50, 70, 90, 100, 120, 140, 160, 180, 200], }, }; /** * @typedef {Object} FontSizePluginOptions * @property {string} [sizeUnit='px'] - The unit for the font size. * - Accepted values include: `'px'`, `'pt'`, `'em'`, `'rem'`, `'vw'`, `'vh'`, `'%'` or `'text'`. * - If `'text'` is used, a text-based font size list is applied. * @property {boolean} [showDefaultSizeLabel=true] - Determines whether the default size label is displayed in the dropdown menu. * @property {boolean} [showIncDecControls] - When `true`, displays increase and decrease buttons for font size adjustments. * - Defaults to `false`. Always `false` when `sizeUnit` is `'text'` (ignored). * @property {boolean} [disableInput] - When `true`, disables the direct font size input box. * - Defaults to `true` when `sizeUnit` is `'text'`, otherwise `false`. * @property {Object<string, {default: number, inc: number, min: number, max: number, list: Array<number>}>} [unitMap={}] - Override or extend the default unit mapping for font sizes. * Each key is a unit name (e.g., `'px'`, `'em'`). `default`: initial size, `inc`: step for inc/dec buttons, `min`/`max`: range limits, `list`: dropdown values. * When `sizeUnit` is `'text'`, list items use `{title: string, size: string}` instead of numbers. * ```js * { unitMap: { px: { default: 16, inc: 1, min: 8, max: 72, list: [8, 12, 16, 20, 24, 32, 48] } } } * ``` */ /** * @class * @implements {PluginCommand} * @implements {PluginDropdown} * @description FontSize Plugin * - This plugin enables users to modify the font size of selected text within the editor. * - It supports various measurement units (e.g., 'px', 'pt', 'em', 'rem', 'vw', 'vh', '%') and * - provides multiple interfaces: dropdown menus, direct input, and optional increment/decrement buttons. */ class FontSize extends PluginInput { static key = 'fontSize'; static className = 'se-btn-select se-btn-input se-btn-tool-font-size'; #disableInput; /** * @constructor * @param {SunEditor.Kernel} kernel - The Kernel instance * @param {FontSizePluginOptions} pluginOptions - Configuration options for the FontSize plugin. */ constructor(kernel, pluginOptions) { super(kernel); // create HTML this.unitMap = { ...DEFAULT_UNIT_MAP, ...(pluginOptions.unitMap || {}) }; this.sizeUnit = /text/.test(pluginOptions.sizeUnit) ? '' : pluginOptions.sizeUnit || this.$.options.get('fontSizeUnits')[0]; const unitMap = this.unitMap[this.sizeUnit || 'text']; const menu = CreateHTML(this.$, unitMap, this.sizeUnit, pluginOptions.showDefaultSizeLabel); // plugin basic properties const showIncDec = this.sizeUnit ? (pluginOptions.showIncDecControls ?? false) : false; const disableInput = this.sizeUnit ? (pluginOptions.disableInput ?? false) : true; this.title = this.$.lang.fontSize; this.inner = disableInput && !showIncDec ? false : disableInput ? `<span class="se-txt se-not-arrow-text __se__font_size">${this.$.lang.fontSize}</span>` : `<input type="text" class="__se__font_size se-not-arrow-text" placeholder="${this.$.lang.fontSize}" />`; // increase, decrease buttons if (showIncDec) { this.beforeItem = dom.utils.createElement( 'button', { class: 'se-btn se-tooltip se-sub-btn', type: 'button', 'data-command': FontSize.key, 'data-type': 'command', 'data-value': 'dec' }, `${this.$.icons.minus}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.decrease}</span></span>`, ); this.afterItem = dom.utils.createElement( 'button', { class: 'se-btn se-tooltip se-sub-btn', type: 'button', 'data-command': FontSize.key, 'data-type': 'command', 'data-value': 'inc' }, `${this.$.icons.plus}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.increase}</span></span>`, ); } else if (!disableInput) { this.afterItem = dom.utils.createElement( 'button', { class: 'se-btn se-tooltip se-sub-arrow-btn', type: 'button', 'data-command': FontSize.key, 'data-type': 'dropdown' }, `${this.$.icons.arrow_down}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.fontSize}</span></span>`, ); this.$.menu.initDropdownTarget({ key: FontSize.key, type: 'dropdown' }, menu); } else if (disableInput && !showIncDec) { this.replaceButton = dom.utils.createElement( 'button', { class: 'se-btn se-tooltip se-btn-select se-btn-tool-font-size', type: 'button', 'data-command': FontSize.key, 'data-type': 'dropdown' }, `<span class="se-txt __se__font_size">${this.$.lang.fontSize}</span>${this.$.icons.arrow_down}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.fontSize}</span></span>`, ); this.$.menu.initDropdownTarget({ key: FontSize.key, type: 'dropdown' }, menu); } // members this.currentSize = ''; this.sizeList = menu.querySelectorAll('li button'); this.hasInputFocus = false; this.isInputActive = false; // input target event this.#disableInput = disableInput; // init this.$.menu.initDropdownTarget(FontSize, menu); } /** * @hook Editor.EventManager * @type {SunEditor.Hook.Event.Active} */ active(element, target) { if (!dom.utils.hasClass(target, '__se__font_size')) return false; let fontSize = ''; if (!element) { this.#setSize(target, this.#getDefaultSize()); } else if (this.$.format.isLine(element)) { return undefined; } else if ((fontSize = dom.utils.getStyle(element, 'fontSize'))) { this.#setSize(target, fontSize); return true; } return false; } /** * @override * @type {PluginInput['toolbarInputKeyDown']} */ toolbarInputKeyDown({ target, event }) { const keyCode = event.code; if (this.#disableInput || keyCodeMap.isSpace(keyCode)) { event.preventDefault(); return; } if (!/^(ArrowUp|ArrowDown|Enter)$/.test(keyCode)) return; const { value, unit } = this.#getSize(target); if (!value) return; const numValue = numbers.get(value); const unitMap = this.unitMap[unit]; let changeValue = numValue; switch (keyCode) { case 'ArrowUp': //up changeValue += unitMap.inc; if (changeValue > unitMap.max) changeValue = numValue; break; case 'ArrowDown': //down changeValue -= unitMap.inc; if (changeValue < unitMap.min) changeValue = numValue; } event.preventDefault(); try { this.isInputActive = true; const size = this.#setSize(target, changeValue + unit); if (this.#disableInput) return; const newNode = dom.utils.createElement('SPAN', { style: 'font-size: ' + size + ';' }); this.$.inline.apply(newNode, { stylesToModify: ['font-size'], nodesToRemove: null, strictRemove: null }); if (!keyCodeMap.isEnter(keyCode)) target.focus(); } finally { this.isInputActive = false; } } /** * @override * @type {PluginInput['toolbarInputChange']} */ toolbarInputChange({ target, value: changeValue, event }) { if (this.#disableInput) return; try { this.isInputActive = true; // eslint-disable-next-line prefer-const let { value, unit } = this.#getSize(changeValue); const { max, min } = this.unitMap[unit]; value = value > max ? max : value < min ? min : value; const newNode = dom.utils.createElement('SPAN', { style: 'font-size: ' + this.#setSize(target, value + unit) + ';' }); this.$.inline.apply(newNode, { stylesToModify: ['font-size'], nodesToRemove: null, strictRemove: null }); } finally { this.isInputActive = false; event.preventDefault(); this.$.focusManager.focus(); } } /** * @imple Dropdown * @type {PluginDropdown['on']} */ on(target) { const { value, unit } = this.#getSize(target); const currentSize = value + unit; if (currentSize === this.currentSize) return; const sizeList = this.sizeList; for (let i = 0, len = sizeList.length; i < len; i++) { if (currentSize === sizeList[i].getAttribute('data-value')) { dom.utils.addClass(sizeList[i], 'active'); } else { dom.utils.removeClass(sizeList[i], 'active'); } } this.currentSize = currentSize; } /** * @imple Command * @type {PluginCommand['action']} */ action(target) { const commandValue = target.getAttribute('data-command'); if (commandValue === FontSize.key) { const { value, unit } = this.#getSize(target); let newSize = numbers.get(value) + (target.getAttribute('data-value') === 'inc' ? 1 : -1); const { min, max } = this.unitMap[unit]; newSize = newSize < min ? min : newSize > max ? max : newSize; const newNode = dom.utils.createElement('SPAN', { style: 'font-size: ' + newSize + unit + ';' }); this.$.inline.apply(newNode, { stylesToModify: ['font-size'], nodesToRemove: null, strictRemove: null }); } else if (commandValue) { const newNode = dom.utils.createElement('SPAN', { style: 'font-size: ' + commandValue + ';' }); this.$.inline.apply(newNode, { stylesToModify: ['font-size'], nodesToRemove: null, strictRemove: null }); } else { this.$.inline.apply(null, { stylesToModify: ['font-size'], nodesToRemove: ['span'], strictRemove: true }); } this.$.menu.dropdownOff(); } /** * @description Retrieves the default font size of the editor. * @returns {string} - The computed font size from the editor. */ #getDefaultSize() { return this.$.frameContext.get('wwComputedStyle').fontSize; } /** * @description Extracts the font size and unit from the given element or input value. * @param {string|Element} target - The target input or element. * @returns {{ unit: string, value: number|string }} - An object containing: * - `unit` (string): The detected font size unit. * - `value` (number|string): The numeric font size value or text-based size. */ #getSize(target) { target = typeof target === 'string' ? target : target.parentElement.querySelector('.__se__font_size'); if (!target) return { unit: this.sizeUnit, value: this.sizeUnit ? 0 : '', }; const size = typeof target === 'string' ? target : dom.check.isInputElement(target) ? target.value : target.textContent; const splitValue = this.sizeUnit ? size.split(/(\d+)/) : [size, '']; let unit = (splitValue.pop() || '').trim().toLowerCase(); unit = this.$.options.get('fontSizeUnits').includes(unit) ? unit : this.sizeUnit; const tempValue = splitValue.pop(); const value = unit ? Number(tempValue) : tempValue; return { unit, value, }; } /** * @description Sets the font size in the toolbar input field or button label. * @param {HTMLElement} target - The target element in the toolbar. * @param {string|number} value - The font size value. * @returns {string|number} - The applied font size. */ #setSize(target, value) { target = target.parentElement.querySelector('.__se__font_size'); if (!target) return 0; if (dom.check.isInputElement(target)) { return (target.value = String(value)); } else { return (target.textContent = String(this.sizeUnit ? value : this.unitMap.text.list.find((v) => v.size === value)?.title || value)); } } } /** * @param {SunEditor.Deps} $ - Kernel dependencies * @param {{list: Array<number|{title: string, size: string}>, default?: number|string}} unitMap - Font size unit map * @param {string} sizeUnit - Size unit string * @param {boolean} showDefaultSizeLabel - Whether to show default size label * @returns {HTMLElement} */ function CreateHTML({ lang }, unitMap, sizeUnit, showDefaultSizeLabel) { const sizeList = unitMap.list; const defaultSize = unitMap.default; const defaultLang = showDefaultSizeLabel ? lang.default : ''; let list = /*html*/ ` <div class="se-list-inner"> <ul class="se-list-basic">`; for (let i = 0, len = sizeList.length, size, t, v, d, l; i < len; i++) { size = sizeList[i]; if (typeof size === 'object') { t = size.title; v = size.size; } else { t = v = size + sizeUnit; } d = defaultSize === v ? ' default_value' : ''; l = d ? defaultLang || t : t; list += /*html*/ ` <li> <button type="button" class="se-btn se-btn-list${d}" data-command="${v}" data-value="${t}" title="${l}" aria-label="${l}" style="font-size:${v};">${l}</button> </li>`; } list += /*html*/ ` </ul> </div>`; return dom.utils.createElement('DIV', { class: 'se-dropdown se-list-layer se-list-font-size' }, list); } export default FontSize;