UNPKG

sp-editor

Version:

SpEditor is a HTML5 rich text editor in smartphone browsers

502 lines (501 loc) 17.8 kB
/*! * @sp-editor/editor version 1.0.0 * Author: Capricorncd * Released under the MIT License * Repository: https://github.com/capricorncd/zx-editor * Copyright © 2018-present, Capricorncd/ Xing Zhong. */ var W = Object.defineProperty; var V = (e, r, t) => r in e ? W(e, r, { enumerable: !0, configurable: !0, writable: !0, value: t }) : e[r] = t; var f = (e, r, t) => (V(e, typeof r != "symbol" ? r + "" : r, t), t); class F { constructor() { this._events = {}; } /** * @method on(eventName, fn) * `on` is used to add a callback function that's going to be executed when the event is triggered. * @param eventName `string` custom event name. * @param fn `Function` callback function. * @returns `EventEmitter` */ on(r, t) { return !r || !t || typeof t != "function" ? this : (this._events[r] || (this._events[r] = []), this._events[r].push(t), this); } /** * @method once(eventName, fn) * `once` add a one-time listener. * @param eventName `string` custom event name. * @param fn `Function` callback function. * @returns `EventEmitter` */ once(r, t) { const n = (...s) => { t.apply(this, s), this.off(r, n); }; return this.on(r, n); } /** * @method emit(eventName, arg1, arg2, ..., argN) * `emit` is used to trigger an event. * @param eventName `string` * @param args `any` * @returns `EventEmitter` */ emit(r, ...t) { const n = this._events[r]; if (!n) return this; for (let s = 0; s < n.length; s++) try { n[s].apply(this, t); } catch (i) { this.emit("error", i, "emit"); } return this; } /** * @method off(eventName, fn) * remove an event listener from an event. * @param eventName `string` custom event name. * @param fn? `Function` callback function. When `fn` is not a function, all monitoring functions of `eventName` will be removed. * @returns `EventEmitter` */ off(r, t) { if (!this._events[r]) return this; const n = this._events[r]; if (typeof t == "function") { const s = n.findIndex((i) => i === t); s >= 0 && n.splice(s, 1); } else this._events[r].length = 0; return this._removeEmpty(r), this; } /** * _removeEmpty(eventName) * remove empty event list * @param eventName `string` * @private * @returns `void` */ _removeEmpty(r) { this._events[r].length || delete this._events[r]; } /** * @method destroyEventEmitter() * remove all listeners for an event. * @returns `void` */ removeAllListeners() { Object.keys(this._events).forEach((r) => this.off(r)); } } /*! * zx-sml version 0.7.5 * Author: Capricorncd <capricorncd@qq.com> * Repository: https://github.com/capricorncd/zx-sml * Released on: 2023-05-17 20:41:58 (GMT+0900) */ var K = Object.defineProperty, w = Object.getOwnPropertySymbols, q = Object.prototype.hasOwnProperty, G = Object.prototype.propertyIsEnumerable, O = (e, r, t) => r in e ? K(e, r, { enumerable: !0, configurable: !0, writable: !0, value: t }) : e[r] = t, b = (e, r) => { for (var t in r || (r = {})) q.call(r, t) && O(e, t, r[t]); if (w) for (var t of w(r)) G.call(r, t) && O(e, t, r[t]); return e; }; function J(e) { return Array.isArray(e); } function P(e) { return typeof e == "object" && e !== null && !J(e); } function R(e = "", r = "-") { return e.replace(/[A-Z]/g, (t, n) => `${n > 0 ? r : ""}${t.toLowerCase()}`); } function I(e = "", r = !1) { const t = e.replace(/[-_\s](\w)/g, (n, s) => s.toUpperCase()); return r ? t.replace(/^\w/, (n) => n.toUpperCase()) : t; } function p(e, r = 0) { return Array.prototype.slice.call(e, r); } function B(e = {}, r = !1) { const t = r ? I : R, n = {}; for (const [s, i] of Object.entries(e)) n[t(s)] = P(i) ? B(i, r) : i; return n; } function Q(e, r = document) { return e ? e instanceof HTMLElement ? e : r.querySelector(e) : null; } function h(e, r = {}, t) { const n = document.createElement(e); for (const [s, i] of Object.entries(r)) n.setAttribute(R(s), s === "style" && P(i) ? v(i) : String(i)); return t && (Array.isArray(t) || (t = [t]), t.forEach((s) => { if (typeof s == "string") { const i = h("div"); i.innerHTML = s, n.append(...i.childNodes); } else n.append(s); })), n; } function v(...e) { const r = e.reduce((n, s) => b(b({}, n), B(s)), {}), t = []; for (const [n, s] of Object.entries(r)) s === "" || typeof s > "u" || s === null || t.push(`${n}:${s}`); return t.join(";"); } const A = (e, r = "style") => e ? (e.getAttribute(r) || "").split(/\s?;\s?/).reduce((n, s) => { const [i, o] = s.split(/\s?:\s?/); return i && (n[I(i)] = o), n; }, {}) : {}, X = (e) => document.createTextNode(e), Y = "sp-editor__editor", _ = "is-empty", D = "SECTION", Z = "BR", U = [D, "H1", "H2", "H3", "H4", "H5", "BLOCKQUOTE", "UL", "OL"]; function m(e, r, t) { return e.replace( new RegExp("(^<" + r + ")|(" + r + ">$)", "gi"), (n) => n.replace(new RegExp(r, "i"), t.toLowerCase()) ); } function z(e) { return e.replace(/<li[^>]*>(.+)<\/li>/gi, "$1"); } function g(e) { const r = typeof e == "string" ? e : e.nodeName; return /^UL|OL$/i.test(r); } function x(e) { if (!e) return !1; const r = p(e.childNodes); return r.length > 0 && r.every((t) => t.nodeName === "BR"); } function E(e) { return typeof e != "string" && e.nodeName && (e = e.outerHTML), typeof e == "string" && /^<(\w+)[^>]*>.*<\/\1>$/.test(e.replace(/\n/g, "")); } function k(e) { return ["PICTURE", "VIDEO", "AUDIO", "CANVAS"].includes(e.nodeName); } function ee(e) { return ["IMG"].includes(e.nodeName) || k(e); } function j(e) { if (ee(e)) return !0; for (let r = 0; r < e.children.length; r++) if (j(e.children[r])) return !0; return !1; } const te = (e, r) => { const t = { minHeight: e.minHeight, // placeholder "--placeholder": JSON.stringify(e.placeholder), "--placeholder-color": e.placeholderColor, "--line-height": e.lineHeight, // paragraphTailSpacing "--paragraph-spacing": e.paragraphTailSpacing, "--padding-bottom": e.paddingBottom, // 用户自定义样式优先 ...e.styles }; e.caretColor && (t.caretColor = e.caretColor), e.textColor && (t.color = e.textColor); const n = { class: `${Y} ${_}`, style: v(t) }; return e.editable && (n.contenteditable = "true"), h("div", n, r); }, M = (e, r) => { var l, a, C, y, L, T, $, H, S; if (!e) return null; const t = e.nodeName, n = r.toUpperCase(); if (t === n) return null; const s = h(r), i = e.parentElement; let o; if (t === "LI" && g(i)) { if (s.innerHTML = m(e.outerHTML, t, n), o = s.firstChild, i.childElementCount > 1) if (i.firstElementChild === e) (l = i.parentElement) == null || l.insertBefore(o, i); else if (i.lastElementChild === e) { const d = (a = i.parentElement) == null ? void 0 : a.nextElementSibling; d ? (C = d.parentElement) == null || C.insertBefore(o, d) : (y = i.parentElement) == null || y.append(o); } else { const d = p(i.children), c = h(i.nodeName); let u = d.shift(); for (; u && u !== e; ) c.append(u), u = d.shift(); (L = i.parentElement) == null || L.insertBefore(c, i), (T = i.parentElement) == null || T.insertBefore(o, i), i.removeChild(e); } else ($ = i.parentElement) == null || $.insertBefore(o, i), (H = i.parentElement) == null || H.removeChild(i); return o; } if (/UL|OL/.test(n)) { const d = e.previousElementSibling, c = e.nextElementSibling; if (d && g(d)) { if (s.innerHTML = m(e.outerHTML, t, "li"), o = s.firstChild, d.append(o), i == null || i.removeChild(e), c && c.nodeName === d.nodeName) { const u = p(c.children); d.append(...u), (S = c.parentElement) == null || S.removeChild(c); } } else c && g(c) ? (s.innerHTML = m(e.outerHTML, t, "li"), o = s.firstChild, c.insertBefore(o, c.firstElementChild), i == null || i.removeChild(e)) : (o = s, s.innerHTML = m(e.outerHTML, t, "li"), i == null || i.replaceChild(o, e)); } else s.innerHTML = z(m(e.outerHTML, t, n)), o = s.firstChild, i == null || i.replaceChild(o, e); return o; }, N = (e) => { var r; !((r = e.innerText) != null && r.trim()) && !j(e) ? e.classList.add(_) : e.classList.remove(_); }; function ne(e, r, t = !1) { var n; for (; e && r !== e; ) { if (!t && e.nodeName === "LI" && ((n = e.parentElement) == null ? void 0 : n.parentElement) === r || e.parentElement === r) return e; e = e.parentElement; } return r.lastElementChild; } const re = { // 内容是否可以被编辑 editable: !0, // 编辑器输入内容绝对定位 // editor min height minHeight: "50vh", paddingBottom: "50vh", // style placeholder: "请在此输入内容..", placeholderColor: "#999", lineHeight: 1.5, childNodeName: D, allowedNodeNames: U, // paragraph tail spacing, default 10px paragraphTailSpacing: "10px", caretColor: "", textColor: "#333333", // 自定义粘贴处理函数 customPasteHandler: void 0, insertTextToNewParagraph: !1 }; class se extends F { constructor(t) { super(); /** * @property version * 获取版本号 * @returns `string` */ f(this, "version"); // 参数 f(this, "options"); // 编辑器内容区域HTML元素 f(this, "$editor"); // 内容元素事件处理函数 f(this, "_eventHandler"); // 内容中允许使用的元素标签 f(this, "allowedNodeNames"); f(this, "blankLine"); f(this, "range"); f(this, "_pasteHandler"); const n = typeof t.container == "string" ? Q(t.container) : t.container; if (!n) throw new Error(`Can't found '${t.container}' Node in document!`); this.version = "1.0.0", this.options = { ...re, ...t }, this.allowedNodeNames = (this.options.allowedNodeNames || U).map((i) => i.toUpperCase()); const s = this.options.childNodeName.toUpperCase(); this.options.childNodeName = s, this.blankLine = `<${s}><br></${s}>`, this.allowedNodeNames.includes(s) || this.allowedNodeNames.push(s), this.$editor = te(this.options, this.blankLine), n.append(this.$editor), this.range = new Range(), this.range.setStart(this.$editor.children[0], 0), this._eventHandler = (i) => { var l; const o = i.type; if (o === "click") { const a = (l = window.getSelection()) == null ? void 0 : l.getRangeAt(0); a && (this.range = a); } else o === "blur" && this._verifyChild(); this.emit(o === "input" ? "change" : o, i), N(this.$editor); }, this._pasteHandler = (i) => { var l; if (typeof this.options.customPasteHandler == "function") return this.options.customPasteHandler(i); i.preventDefault(); const o = (l = i.clipboardData) == null ? void 0 : l.getData("text"); this.insert(o); }, this._initEvents(); } /** * init events * @private */ _initEvents() { this.$editor.addEventListener("focus", this._eventHandler), this.$editor.addEventListener("blur", this._eventHandler), this.$editor.addEventListener("input", this._eventHandler), this.$editor.addEventListener("click", this._eventHandler), this.$editor.addEventListener("paste", this._pasteHandler); } setRangeWithNode(t) { const n = new Range(); return n.setStart(t === this.$editor ? this.$editor.childNodes[this.$editor.childNodes.length - 1] : t, 0), this.range = n, n; } /** * @method use(plugin, parentElement) * extension, 扩展插件 * @param plugin `EditorPlugin` * @param parentElement? `HTMLElement` */ use(t, n) { typeof t.install == "function" && t.install(this, n); } /** * @method setHtml(html) * 设置编辑器内容,会覆盖之前内容 * set html to the content element * @param html `string` */ setHtml(t) { this.$editor.innerHTML = this.blankLine, this.insert(t, !0), N(this.$editor); } /** * @method getHtml(retainLastBlankLines) * 获取编辑器中的HTML代码,会自动去除结尾处的空行。 * get html string from content element. * @param retainLastBlankLines? `boolean` Retain last blank lines, If `true` the last `<section><br></section>` not will be removed. * @return `string` */ getHtml(t) { const n = this.$editor.innerHTML; if (t) return n; const s = this.options.childNodeName; return n.replace(new RegExp(`(<${s}><br\\s?\\/?><\\/${s}>)+$`, "i"), ""); } /** * @method insert(input, toNewParagraph) * 向编辑器中插入内容/HTML代码/元素等 * insert html or element to content element * @param input `string | HTMLElement` * @param toNewParagraph? `boolean` Insert `text` in a new paragraph, only `textNode` is valid. Defaults to `false`. */ insert(t, n = !1) { if (!t) return; const { childNodeName: s, insertTextToNewParagraph: i } = this.options; if (t instanceof HTMLElement) this._insertEl(t); else { const o = h("div", {}, t), l = p(o.childNodes); if (!n && !i && l.every((a) => a.nodeType === Node.TEXT_NODE)) return this._insertText(t); l.forEach((a) => { a.nodeType === Node.ELEMENT_NODE ? a.nodeName === Z ? this._insertEl(h(s, {}, "<br/>")) : this._insertEl(a) : a.textContent && this._insertEl(h(s, {}, a.textContent)); }); } this._verifyChild(), this._dispatchChange(), N(this.$editor); } /** * insert element to content element * @param input * @private */ _insertEl(t) { const n = this.getCursorElement(); x(n) ? E(t.outerHTML) ? this.$editor.insertBefore(t, n) : n === this.$editor.children[this.$editor.children.length - 1] ? this.$editor.insertBefore(h(this.options.childNodeName, {}, t), n) : (n.innerHTML = "", n.append(t)) : (E(t.outerHTML) || (t = h(this.options.childNodeName, {}, t)), n.nextElementSibling ? this.$editor.insertBefore(t, n.nextElementSibling) : this.$editor.append(t)), this.setRangeWithNode(t); } /** * insert text into editor * @param input * @returns */ _insertText(t) { if (!t) return; const n = window.getSelection(); if (!(n == null ? void 0 : n.rangeCount)) return this.insert(t, !0); n.deleteFromDocument(); const i = X(t); n.getRangeAt(0).insertNode(i), n.setPosition(i, t.length), this.range = n.getRangeAt(0), n.collapseToEnd(); } /** * 验证编辑器的子元素是否为允许使用的元素,并检查其最后一段是否为空行,非空行则插入。 * Verify that the editor's child element is an allowed elements, and check if it's last child is a blank line, if not, insert a new blank line * @private */ _verifyChild() { const t = this.options.childNodeName; let n, s, i = !1; const o = this.$editor.childNodes; for (let l = 0; l < o.length; l++) s = null, n = o[l], i = n === this.range.commonAncestorContainer, n.nodeType === Node.ELEMENT_NODE && E(n) && !k(n) ? this.allowedNodeNames.includes(n.nodeName) || (s = M(n, t)) : (s = h(t, {}, n.cloneNode(!0)), this.$editor.replaceChild(s, n)), i && s && this.setRangeWithNode(s); x(this.$editor.lastElementChild) || this.$editor.append(h(t, {}, "<br>")); } /** * @method changeNodeName(nodeName) * 修改光标所在元素的标签 * Replace the tag of the element under the cursor * @param nodeName? `string` allowed element names, `UL`, `SECTION` etc. If `undefined`, use the default `options.childNodeName`. * @return `boolean` */ changeNodeName(t) { if (t = (t || this.options.childNodeName).toUpperCase(), !this.allowedNodeNames.includes(t)) return !1; const n = this.getCursorElement(), s = M(n, t); return s ? (this.setRangeWithNode(s), this._dispatchChange(), !0) : !1; } /** * @method changeStyles(styles, value) * 修改光标所在元素的样式 * Change the style of the element where the cursor is located * @param styles? `CSSProperties | string` When it's `undefined` or null, all styles will be removed. * @param value? `any` */ changeStyles(t, n) { const s = this.getCursorElement(!0); if (s) { const i = A(s); if (t) { const o = typeof t == "string" ? { [t]: n } : t; s.setAttribute("style", v(i, o)); } else { if (!Object.keys(i).length) return; s.removeAttribute("style"); } this._dispatchChange(); } } /** * 分派事件 */ _dispatchChange() { this.$editor.dispatchEvent(new InputEvent("input")); } /** * @method getStyles() * 获取光标所在的元素的`style`对象 * Get the `style` object of the element where the cursor is located * @return `CSSProperties` */ getStyles() { return A(this.getCursorElement()); } /** * @method getCursorElement(isOnlyEditorChild) * 获取光标所在的元素 * Get the element where the cursor is located * @param isOnlyEditorChild? `boolean` Must be a child element of editor `HTMLElement`. For example: when it is `false`, the `li` element is returned in `ul/ol`, and when it is `true`, the `ul/ol` element is returned. * @return `HTMLElement` */ getCursorElement(t = !1) { return ne(this.range.commonAncestorContainer, this.$editor, t); } /** * @method destroy() * 销毁事件 * destroy events */ destroy() { this.$editor.removeEventListener("focus", this._eventHandler), this.$editor.removeEventListener("blur", this._eventHandler), this.$editor.removeEventListener("input", this._eventHandler), this.$editor.removeEventListener("paste", this._pasteHandler), this.removeAllListeners(); } } export { U as ALLOWED_NODE_NAMES, se as Editor };