UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

195 lines (169 loc) 6.23 kB
/** * @fileoverview Char class */ import { _w, isEdge } from '../../../helper/env'; import { addClass, removeClass, hasClass } from '../../../helper/dom/domUtils'; /** * @description character count, character limit, etc. management class */ class Char { #$; #frameContext; #frameOptions; /** * @constructor * @param {SunEditor.Kernel} kernel */ constructor(kernel) { this.#$ = kernel.$; this.#frameContext = this.#$.frameContext; this.#frameOptions = this.#$.frameOptions; } /** * @description Returns `false` if char count is greater than "frameOptions.get('charCounter_max')" when "html" is added to the current editor. * @param {Node|string} html Element node or String. * @returns {boolean} * @example * const canInsert = editor.$.char.check('<strong>new text</strong>'); */ check(html) { const maxCharCount = this.#frameOptions.get('charCounter_max'); if (maxCharCount) { const length = this.getLength(typeof html === 'string' ? html : this.#frameOptions.get('charCounter_type') === 'byte-html' && html.nodeType === 1 ? /** @type {HTMLElement} */ (html).outerHTML : html.textContent); if (length > 0 && length + this.getLength() > maxCharCount) { CounterBlink(this.#frameContext.get('charWrapper')); return false; } } return true; } /** * @description Get the [content]'s number of characters or binary data size. (frameOptions.get('charCounter_type')) * - If [content] is `undefined`, get the current editor's number of characters or binary data size. * @param {string} [content] Content to count. (defalut: this.#frameContext.get('wysiwyg')) * @returns {number} * @example * const currentLength = editor.$.char.getLength(); * const textLength = editor.$.char.getLength('Hello World'); */ getLength(content) { if (typeof content !== 'string') { content = this.#frameOptions.get('charCounter_type') === 'byte-html' ? this.#frameContext.get('wysiwyg').innerHTML : this.#frameContext.get('wysiwyg').textContent; } return /byte/.test(this.#frameOptions.get('charCounter_type')) ? this.getByteLength(content) : content.length; } /** * @descriptionGets Get the length in bytes of a string. * @param {string} text String text * @returns {number} * @example * const bytes = editor.$.char.getByteLength('Hello 世界'); // 12 */ getByteLength(text) { if (!text || !text.toString) return 0; text = text.toString(); let cr, cl; if (isEdge) { cl = decodeURIComponent(encodeURIComponent(text)).length; cr = 0; if (encodeURIComponent(text).match(/(%0A|%0D)/gi) !== null) { cr = encodeURIComponent(text).match(/(%0A|%0D)/gi).length; } return cl + cr; } else { cl = new TextEncoder().encode(text).length; cr = 0; if (encodeURIComponent(text).match(/(%0A|%0D)/gi) !== null) { cr = encodeURIComponent(text).match(/(%0A|%0D)/gi).length; } return cl + cr; } } /** * @description Get the number of words in the content. * - If [content] is `undefined`, get the current editor's word count. * @param {string} [content] Content to count. (default: wysiwyg textContent) * @returns {number} * const currentWords = editor.$.char.getWordCount(); * const textWords = editor.$.char.getWordCount('Hello World'); */ getWordCount(content) { if (typeof content !== 'string') { content = this.#frameContext.get('wysiwyg').innerText; } const trimmed = content.trim(); if (!trimmed) return 0; return trimmed.split(/\s+/).length; } /** * @description Set the char count and word count to counter element textContent. * @param {?SunEditor.FrameContext} [fc] Frame context */ display(fc) { const ctx = fc || this.#frameContext; const charCounter = ctx.get('charCounter'); const wordCounter = ctx.get('wordCounter'); if (charCounter || wordCounter) { // Defer count update — DOM content may still be mutating from the current input/paste action _w.setTimeout(() => { if (charCounter) charCounter.textContent = String(this.getLength()); if (wordCounter) wordCounter.textContent = String(this.getWordCount()); }, 0); } } /** * @description Returns `false` if char count is greater than "frameOptions.get('charCounter_max')" when "inputText" is added to the current editor. * - If the current number of characters is greater than "charCounter_max", the excess characters are removed. * And call the char.display() * @param {string} inputText Text added. * @param {boolean} _fromInputEvent Whether the test is triggered from an input event. * @returns {boolean} * @example * if (!editor.$.char.test(inputData, true)) { * e.preventDefault(); * } */ test(inputText, _fromInputEvent) { let nextCharCount = 0; if (inputText) nextCharCount = this.getLength(inputText); this.display(); const maxCharCount = this.#frameOptions.get('charCounter_max'); if (maxCharCount > 0) { let over = false; const count = this.getLength(); if (count > maxCharCount) { over = true; if (nextCharCount > 0 && _fromInputEvent) { this.#$.selection.init(); const range = this.#$.selection.getRange(); const endOff = range.endOffset - 1; const text = this.#$.selection.getNode().textContent; const slicePosition = range.endOffset - 1; // (count - maxCharCount); this.#$.selection.getNode().textContent = text.slice(0, slicePosition < 0 ? 0 : slicePosition) + text.slice(range.endOffset, text.length); this.#$.selection.setRange(range.endContainer, endOff, range.endContainer, endOff); } } else if (count + nextCharCount > maxCharCount) { over = true; } if (over) { CounterBlink(this.#frameContext.get('charWrapper')); if (nextCharCount > 0) return false; } } return true; } } /** * @description The character counter blinks. * @param {Element} charWrapper this.#frameContext.get('charWrapper') */ function CounterBlink(charWrapper) { if (charWrapper && !hasClass(charWrapper, 'se-blink')) { addClass(charWrapper, 'se-blink'); // Remove blink CSS class after animation completes (600ms matches the CSS animation duration) _w.setTimeout(() => { removeClass(charWrapper, 'se-blink'); }, 600); } } export default Char;