@ithaka/bonsai
Version:
ITHAKA core styling
331 lines (295 loc) • 9.82 kB
JavaScript
"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;
}
}