UNPKG

@ithaka/bonsai

Version:
331 lines (295 loc) 9.82 kB
"use strict"; import Quill from "quill"; import uuidv4 from "uuid/v4"; const FONT_SIZE_BINDINGS = [ {"key": "1", "size": "small"}, {"key": "2", "size": false}, {"key": "3", "size": "large"}, {"key": "4", "size": "huge"}, ]; const LIST_BINDINGS = [ {"key": "O", "value": "ordered"}, {"key": "U", "value": "bullet"}, ]; /** * BonsaiTextEditor is a single editor instance with a toolbar and save button. * * @class * @name BonsaiTextEditor * * @example * <div id="editor" data-text-editor data-editor-placeholder="I'm custom placeholder text"></div> * * <script> * BonsaiTextEditor.editorFactory() * * $("#editor").on("save", (event, data, html) => { * alert("Saving editor content." + html); * }); * </script> */ export class BonsaiTextEditor { /** * Creates all the elements tagged with `data-text-editor` on the page * * @Returns * Array of BonsaiTextEditorElements * * @static */ static editorFactory() { let editors = []; $("[data-text-editor]").each(($index, element) => { editors.push(new BonsaiTextEditor(element)); }); return editors; } /** * Get editor by ID from array of constructed editors * * @static */ static getEditorById(editors, editorId) { let selectedEditor = undefined; for (let editor of editors) { if (editor._getEditorId() === editorId) { selectedEditor = editor; break; } } return selectedEditor; } /** * @constructor * * @param element to build the text editor around */ constructor(element) { this.$editor = $(element); let placeholderText = this.$editor.data("editorPlaceholder") || "Type something"; this.uuid = uuidv4(); const options = { placeholder: placeholderText, modules: { toolbar: `[data-quill-toolbar="${this.uuid}"]` } }; this._attachHtmlElements(); this._attachHelpButton(this.$editor.data("helpLink")); this._attachCancelLink(); this._initializeSaveTrigger(); this.quill = new Quill(element, options); this._addSelectBoxHack(); this._defaultHide(); this._addKeyboardBindings(); } /** * Creates the toolbar and save button for the text editor * * @private */ _attachHtmlElements() { const toolbarHtml = ` <div data-quill-toolbar="${this.uuid}"> <span data-size-placeholder="${this.uuid}" class="size-placeholder" aria-hidden="true">Normal</span> <select class="ql-size" aria-label="select font size"> <option value="small">Small</option> <option selected>Normal</option> <option value="large">Large</option> <option value="huge">Extra Large</option> </select> <span class="ql-formats"> <span class="vertical-rule-full-height pvxs"></span> <button class="ql-bold" aria-label="bold"><i class="icon-bold icon-small" aria-hidden="true"></i></button> <button class="ql-italic" aria-label="italic"><i class="icon-italic icon-small" aria-hidden="true"></i></button> <button class="ql-underline" aria-label="underline"><i class="icon-underline icon-small" aria-hidden="true"></i></button> <span class="vertical-rule-full-height pvxs"></span> <button class="ql-list" value="ordered" aria-label="ordered list"><i class="icon-list-ol icon-small" aria-hidden="true"></i></button> <button class="ql-list" value="bullet" aria-label="unordered list"><i class="icon-list-ul icon-small" aria-hidden="true"></i></button> <span class="vertical-rule-full-height pvxs"></span> <button class="ql-link" aria-label="link"><i class="icon-link icon-small" aria-hidden="true"></i></button> </span> </div>`, saveButtonHtml = `<button data-quill-save="${this.uuid}" class="button save-button mts">Save</button>`; this.$editor.before(toolbarHtml); this.$editor.after(saveButtonHtml); this.$toolbar = $(`[data-quill-toolbar="${this.uuid}"]`); this.$saveButton = $(`[data-quill-save="${this.uuid}"]`); } _attachHelpButton(helpLink) { if(!helpLink) { return; } this.$helpButton = $(`<a class="help-button button" tabindex="0" href="javascript:void(0);" aria-label="Keyboard shortcut documentation. This link opens in a new tab."><i class="icon-question icon-small" aria-hidden="true"></i></a>`); this.$toolbar.append(this.$helpButton); this.$helpButton.on("click", () => { window.open(helpLink); }); } _attachCancelLink() { this.$cancelLink = $(`<a class="cancel-link mls" tabindex="0" href="javascript:void(0);">Cancel</a>`); this.$cancelLink.on("click", () => { this.$editor.trigger("cancel.bonsai-text-editor"); }); this.$saveButton.after(this.$cancelLink); } /** * Hides the editor, toolbar and save button if the editor has `data-editor-hidden` element * * @private */ _defaultHide() { if (this.$editor.data("editorHidden") !== undefined) { this.toggle(); } } /** * Captures the click event on the save button and retriggers an event on the element * * @private */ _initializeSaveTrigger() { this.$saveButton.on("click", () => { const data = this.quill.getContents(), html = this.$editor.find(".ql-editor").html(), sanitizedHtml = this._sanitizeHtml(html); this.$editor.trigger("save.bonsai-text-editor", [data, sanitizedHtml]); }); } /** * Hack to "sanitize" the html by removing any occurrences of the <span> quill uses to save the cursor * position leading to random unicode unicode characters getting saved to the database. See https://github.com/quilljs/quill/issues/1682 * @private */ _sanitizeHtml(html) { if (html) { return html.replace(/<span class="ql-cursor">.*<\/span>/g, ""); } } /** * Adds a hack where we show/hide the span that fakes an empty select box when the editor loses focus. * * @private */ _addSelectBoxHack() { let $sizePlaceholder = $(`[data-size-placeholder="${this.uuid}"]`); this.quill.on("editor-change", (eventName, ...args) => { if(eventName === "selection-change") { if (this.quill.hasFocus()) { $sizePlaceholder.addClass("hide"); } else { $sizePlaceholder.removeClass("hide"); } } }); } /** * Adds keyboard bindings * * @private */ _addKeyboardBindings() { if(this.quill.keyboard) { this._addFontSizeBindings(); this._addListBindings(); this._addLinkBinding(); } } /** * Add font size keyboard bindings * * @private */ _addFontSizeBindings() { FONT_SIZE_BINDINGS.forEach((entry) => { this.quill.keyboard.addBinding({ key: entry["key"], shortKey: true, altKey: true }, (range, context) => { if (range.length > 0) { this.quill.formatText(range, {"size": entry["size"]}); } else { this.quill.format("size", entry["size"]); } }); }); } /** * Add list keyboard bindings * * @private */ _addListBindings() { LIST_BINDINGS.forEach((entry) => { this.quill.keyboard.addBinding({ key: entry["key"], shortKey: true, altKey: true }, (range, context) => { if (context.format.list === entry["value"]) { this.quill.format("list", false); } else { this.quill.formatLine(range, 1, "list", entry["value"]); } }); }); } /** * Add link keyboard bindings * * @private */ _addLinkBinding() { this.quill.keyboard.addBinding({ key: "L", shortKey: true, altKey: true }, (range, context) => { if (context.format.link) { this.quill.format("link", false); } else { let result = window.prompt("Enter link URL:"); if (result) { this.quill.formatText(range, {"link": result}); } } }); } /** * Get ID of editor * * @returns Editor ID * * @private */ _getEditorId() { return this.$editor[0].id; } /** * Inserts html into the quill editor * * @param editorHtml */ insertEditorContent(editorHtml) { this.$editor.find(".ql-editor").html(editorHtml); } /** * Show or hide the editor elements depending on the current state * */ toggle() { this.$editor.toggle(); this.$toolbar.toggle(); this.$saveButton.toggle(); this.$cancelLink.toggle(); } /** * Get ID of editor * * @returns Editor ID */ getId() { return this.$editor.uuid; } }