/**
* Refer to <span color = "yellow">{@link ve.Component}</span> for methods or fields inherited from this Component's parent such as `.options.attributes` or `.element`.
*
* Multiline rich text editor used as a word processor.
* - Functional binding: <span color=00ffff>veWYSIWYG</span>().
*
* ##### Constructor:
* - `arg0_value`: {@link string} - The HTML contents of the current text editor.
* - `arg1_options`: {@link Object}
*
* ##### Instance:
* - `.v`: {@link string} - The current HTML contents, same as `arg0_value`.
*
* ##### Methods:
* - <span color=00ffff>{@link ve.WYSIWYG.handleEvents|handleEvents}</span>() - Initialises event handlers for the present component.
* - <span color=00ffff>{@link ve.WYSIWYG.getWYSIWYGFromFields|getWYSIWYGFromFields}</span>(arg0_wysiwyg_el:{@link HTMLElement}) | {@link string} - Fetches the internal .innerHTML value, resolving conflicts between visual/code views.
* - <span color=00ffff>{@link ve.WYSIWYG.initWYSIWYG|initWYSIWYG}</span>() - Internal helper function. Initialises the component.
* -
* - <span color=00ffff>{@link ve.WYSIWYG.addParagraphTag|addParagraphTag}</span>(arg0_e:{@link KeyboardEvent})
* - <span color=00ffff>{@link ve.WYSIWYG.parentTagActive|parentTagActive}</span>(arg0_el:{@link HTMLElement}) | {@link boolean}
* - <span color=00ffff>{@link ve.WYSIWYG.selectionChange|selectionChange}</span>(arg0_e:{@link Event}, arg1_buttons:{@link Array}<{@link HTMLElement}>, arg2_editor:{@link HTMLElement}) | {@link boolean} - Fires an internal selection change event.
* - <span color=00ffff>{@link ve.WYSIWYG.pasteEvent|pasteEvent}</span>(arg0_e:{@link ClipboardEvent})
*
* ##### Static Methods:
* - <span color=00ffff>{@link ve.WYSIWYG.execCodeAction|execCodeAction}</span>(arg0_button_el:{@link HTMLElement}, arg1_editor_el:{@link HTMLElement}, arg2_visual_view_el:{@link HTMLElement}, arg3_html_view_el:{@link HTMLElement})
* - <span color=00ffff>{@link ve.WYSIWYG.execDefaultAction|execDefaultAction}</span>(arg0_action:{@link string})
* - <span color=00ffff>{@link ve.WYSIWYG.execLinkAction|execLinkAction}</span>(arg0_modal_el:{@link HTMLElement})
* - <span color=00ffff>{@link ve.WYSIWYG.restoreSelection|restoreSelection}</span>(arg0_saved_selection:{@link Selection})
* - <span color=00ffff>{@link ve.WYSIWYG.saveSelection|saveSelection}</span>() | {@link Range}
*
* @augments ve.Component
* @augments {@link ve.Component}
* @memberof ve.Component
* @type {ve.WYSIWYG}
*/
ve.WYSIWYG = class extends ve.Component {
static demo_value = `This is an immediate test for WYSIWYG editors being ported.`;
static demo_options = {
onchange: (e) => {
console.log(`ve.WYSIWYG:`, e);
}
};
constructor (arg0_value, arg1_options) {
//Convert from parameters
let value = arg0_value;
let options = (arg1_options) ? arg1_options : {};
super(options);
//Initialise options
options.attributes = (options.attributes) ? options.attributes : {};
//Declare local instance variables
this.element = document.createElement("span");
let html_string = [];
//Div header
html_string.push(`<div class = "header" id = "name"></div>`);
html_string.push(`<div class = "wysiwyg-editor-container">`);
html_string.push(`<div id = "wysiwyg-editor" class = "wysiwyg-editor">`);
//Editor toolbar
{
html_string.push(`<div class = "toolbar">`);
//FIRST LINE
html_string.push(`<div class = "line">`);
//First box: Bold, Italic, Underline, Strikethrough
html_string.push(`<div class = "box">`);
//Bold
html_string.push(`<span class = "editor-button icon small" data-action = "bold" data-tag-name = "b" title = "Bold"><icon>format_bold</icon></span>`);
//Italic
html_string.push(`<span class = "editor-button icon small" data-action = "italic" data-tag-name = "i" title = "Italic"><icon>format_italic</icon></span>`);
//Underline
html_string.push(`<span class = "editor-button icon small" data-action = "underline" data-tag-name = "u" title = "Underline"><icon>format_underlined</icon></span>`);
//Strikethrough
html_string.push(`<span class = "editor-button icon small" data-action = "strikeThrough" data-tag-name = "strike" title = "Strikethrough"><icon>strikethrough_s</icon></span>`);
html_string.push(`</div>`);
//Second box: Alignment, Lists, Indents, Hr
html_string.push(`<div class = "box">`);
html_string.push(`<span class = "editor-button icon has-submenu">`);
//Menu icon
html_string.push(`<icon class = "has-submenu-icon">format_align_left</icon><icon class = "has-submenu-chevron">keyboard_arrow_down</icon>`);
//1. Submenu
html_string.push(`<div class = "submenu">`);
//Align left
html_string.push(`<span class = "editor-button icon" data-action = "justifyLeft" data-style = "textAlign:left" title = "Align Left"><icon>format_align_left</icon></span>`);
//Align centre
html_string.push(`<span class = "editor-button icon" data-action = "justifyCenter" data-style = "textAlign:center" title = "Align Centre"><icon>format_align_center</icon></span>`);
//Align right
html_string.push(`<span class = "editor-button icon" data-action = "justifyRight" data-style = "textAlign:right" title = "Align Right"><icon>format_align_right</icon></span>`);
//Align justify
html_string.push(`<span class = "editor-button icon" data-action = "formatBlock" data-style = "textAlign:justify" title = "Justify"><icon>format_align_justify</icon></span>`);
html_string.push(`</div>`);
html_string.push(`</span>`);
//Insert ordered list
html_string.push(`<span class = "editor-button icon" data-action = "insertOrderedList" data-tag-name = "ol" title = "Insert ordered list"><icon>format_list_numbered</icon></span>`);
//Insert unordered list
html_string.push(`<span class = "editor-button icon" data-action = "insertUnorderedList" data-tag-name = "ul" title = "Insert unordered list"><icon>format_list_bulleted</icon></span>`);
//Indent
html_string.push(`<span class = "editor-button icon" data-action = "indent" title = "Indent"><icon>format_indent_increase</icon></span>`);
//Outdent
html_string.push(`<span class = "editor-button icon" data-action = "outdent" title = "Outdent" data-required-tag = "li"><icon>format_indent_decrease</icon></span>`);
html_string.push(`</div>`);
html_string.push(`</div>`);
//SECOND LINE
html_string.push(`<div class = "line">`);
//Third box: Undo, clear formatting
html_string.push(`<div class = "box">`);
//Undo
html_string.push(`<span class = "editor-button icon small" data-action = "undo" title = "Undo"><icon>undo</icon></span>`);
//Remove formatting
html_string.push(`<span class = "editor-button icon small" data-action = "removeFormat" title = "Remove format"><icon>format_clear</icon></span>`);
html_string.push(`</div>`);
//Fourth box: Add link, remove link
html_string.push(`<div class = "box">`);
//Insert Link
html_string.push(`<span class = "editor-button icon small" data-action = "createLink" title = "Insert Link"><icon>add_link</icon></span>`);
//Unlink
html_string.push(`<span class = "editor-button icon small" data-action = "unlink" data-tag-name = "a" title = "Unlink"><icon>link_off</icon></span>`);
html_string.push(`</div>`);
//Fifth box: Show HTML
html_string.push(`<div class = "box">`);
//Show HTML code
html_string.push(`<icon class = "editor-button icon" data-action = "toggle-view" title = "Show HTML Code"><icon>code</icon></span>`);
html_string.push(`</div>`);
html_string.push(`</div>`);
html_string.push(`</div>`);
}
//Content area
html_string.push(`<div class = "content-area">`);
html_string.push(`<br><div class = "visual-view" contenteditable></div>`);
html_string.push(`<textarea class = "html-view"></textarea>`);
html_string.push(`</div>`);
//Modal for hyperlinks
html_string.push(`<div class = "modal">`);
html_string.push(`<div class = "modal-bg"></div>`);
html_string.push(`<div class = "modal-wrapper">`);
html_string.push(`<div class = "close">x</div>`);
html_string.push(`<div class = "modal-content" id = "modal-create-link">`);
html_string.push(`<h3>Insert Link</h3>`);
html_string.push(`<input type = "text" id = "link-value" placeholder = "Link (example: https://google.com/)">`);
html_string.push(`<div class = "row">`);
html_string.push(`<input type = "checkbox" id = "new-tab"`);
html_string.push(`<label for = "new-tab">Open in New Tab?</label>`);
html_string.push(`</div>`);
html_string.push(`<button class = "done">Done</button>`);
html_string.push(`</div>`);
html_string.push(`</div>`);
html_string.push(`</div>`);
html_string.push(`</div>`);
html_string.push(`</div>`);
//Set .innerHTML; this.v
this.element.innerHTML = html_string.join("");
this.options = options;
this.handleEvents();
this.initWYSIWYG();
this.from_binding_fire_silently = true;
this.v = value;
this.from_binding_fire_silently = false;
}
/**
* Returns the innerHTML of the present Component's input value.
* - Accessor of: {@link ve.WYSIWYG}
*
* @returns {string}
*/
get v () {
//Return statement
return this.getWYSIWYGFromFields(this.element);
}
/**
* Sets the HTML value for the present Component as a string.
* - Accessor of: {@link ve.WYSIWYG}
*
* @param {string} arg0_value
*/
set v (arg0_value) {
//Convert from parameters
let value = (arg0_value) ? arg0_value : "";
//Set element .html-view, .visual-view content
this.element.querySelector(`.html-view`).value = value;
this.element.querySelector(`.visual-view`).innerHTML = value;
this.fireFromBinding();
}
/**
* Initialises event handlers for the present Component.
* - Method of: {@link ve.WYSIWYG}
*/
handleEvents () {
//Declare local instance variables
let html_view_el = this.element.querySelector(`.html-view`);
let visual_view_el = this.element.querySelector(`.visual-view`);
//Add change handlers
html_view_el.addEventListener("input", (e) => {
this.fireToBinding();
});
visual_view_el.addEventListener("input", (e) => {
this.fireToBinding();
});
}
//Internal helper functions
/**
* Adds a paragraph tag on newline.
* - Method of: {@link ve.WYSIWYG}
*
* @param {KeyboardEvent} arg0_e
*/
addParagraphTag (arg0_e) {
//Convert from parameters
let e = arg0_e;
//Check if keyCode was Enter
if (e.keyCode === "13") {
//Guard clause; Don't add a p tag on list item
if (window.getSelection().anchorNode.parentNode.tagName === "LI") return;
//Otherwise, add a p tag
document.execCommand("formatBlock", false, "p");
}
}
/**
* Fetches the internal .innerHTML value, resolving any conflicts between the visual view and the code view.
* - Method of: {@link ve.WYSIWYG}
*
* @param {HTMLElement} arg0_wysiwyg_el
*
* @returns {string}
*/
getWYSIWYGFromFields (arg0_wysiwyg_el) {
//Convert from parameters
let wysiwyg_el = arg0_wysiwyg_el;
//Declare local instance variables
let html_content_el = wysiwyg_el.querySelector(`.html-view`);
let visual_content_el = wysiwyg_el.querySelector(`.visual-view`);
//Return statement
return (html_content_el.innerHTML.length > visual_content_el.innerHTML.length) ?
html_content_el.innerHTML : visual_content_el.innerHTML;
}
/**
* Initialises the present WYSIWYG Component.
* - Method of: {@link ve.WYSIWYG}
*/
initWYSIWYG () {
//Declare local instance variables
let editor = this.element.querySelector(`.wysiwyg-editor`);
let modal = editor.getElementsByClassName("modal")[0];
let toolbar = editor.getElementsByClassName("toolbar")[0];
let buttons = toolbar.querySelectorAll(`.editor-button:not(.has-submenu)`);
let content_area = editor.getElementsByClassName("content-area")[0];
let visual_view = content_area.getElementsByClassName(`visual-view`)[0];
let html_view = content_area.getElementsByClassName(`html-view`)[0];
//Add active tag event
document.addEventListener("selectionchange", (e) => {
this.selectionChange(e, buttons, editor);
});
//Add paste event
visual_view.addEventListener("paste", this.pasteEvent);
//Add paragraph tag on newline
content_area.addEventListener("keypress", this.addParagraphTag);
//Add toolbar button actions
for (let i = 0; i < buttons.length; i++) {
let local_button = buttons[i];
local_button.addEventListener("click", function (e) {
let action = this.dataset.action;
//execCommand handler
switch (action) {
case "toggle-view":
ve.WYSIWYG.execCodeAction(this, editor, visual_view, html_view);
break;
case "createLink":
ve.WYSIWYG.execLinkAction(modal);
break;
default:
ve.WYSIWYG.execDefaultAction(action);
}
});
}
}
/**
* Check if the parent tag of an element was active.
* - Method of: {@link ve.WYSIWYG}
*
* @param {HTMLElement} arg0_el
*
* @returns {boolean}
*/
parentTagActive (arg0_el) {
//Convert from parameters
let element = arg0_el;
//Guard clause for visual view
if (!element || !element.classList || element.classList.contains("visual-view"))
return false;
//Declare local instance variables
let tag_name = element.tagName.toLowerCase();
let text_align = element.style.textAlign;
let toolbar_button;
//Active by tag name
toolbar_button = document.querySelectorAll(`.toolbar .editor-button[data-tag-name="${tag_name}"]`)[0];
//Active by text-align
toolbar_button = document.querySelectorAll(`.toolbar .editor-button[data-style="textAlign:${text_align}"]`)[0];
//Set toolbar_button to being active if toolbar_button is defined
if (toolbar_button)
toolbar_button.classList.add("active");
//Return statement
return this.parentTagActive(element.parentNode);
}
/**
* Internal helper function for monitoring selection changes.
* - Method of: {@link ve.WYSIWYG}
*
* @param {Event} arg0_e
* @param {HTMLElement[]} arg1_buttons
* @param {HTMLElement} arg2_editor
*
* @returns {boolean}
*/
selectionChange (arg0_e, arg1_buttons, arg2_editor) {
//Convert from parameters
let e = arg0_e;
let buttons = arg1_buttons;
let editor = arg2_editor;
//Declare local instance variables
for (let i = 0; i < buttons.length; i++) {
let local_button = buttons[i];
//Don't remove active class on code toggle button
if (local_button.dataset.action === "toggle-view") continue;
local_button.classList.remove("active");
}
try {
if (!childOf(window.getSelection().anchorNode.parentNode, editor))
//Return statement; guard clause
return false;
this.parentTagActive(window.getSelection().anchorNode.parentNode);
} catch {}
}
/**
* pasteEvent() - Handles paste event by removing all HTML tags.
* - Method of: {@link ve.WYSIWYG}
*
* @param {ClipboardEvent} arg0_e
*/
pasteEvent (arg0_e) {
//Convert from parameters
let e = arg0_e;
//Declare local instance variables
let text = (e.originalEvent || e).clipboardData.getData("text/plain");
e.preventDefault();
document.execCommand("insertHTML", false, text);
}
//Static helper methods
/**
* Executes a code action and changes the formatting within the {@link ve.WYSIWYG} component.
* - Static method of: {@link ve.WYSIWYG}
*
* @param {HTMLElement} arg0_button_el - The button_el that determines the code action to execute.
* @param {HTMLElement} arg1_editor_el
* @param {HTMLElement} arg2_visual_view_el
* @param {HTMLElement} arg3_html_view_el
*/
static execCodeAction (arg0_button_el, arg1_editor_el, arg2_visual_view_el, arg3_html_view_el) {
//Convert from parameters
let button_el = arg0_button_el;
let visual_view = arg2_visual_view_el;
let html_view = arg3_html_view_el;
//Toggle visual/HTML view depending on current state
if (button_el.classList.contains("active")) { //Show visual view
visual_view.innerHTML = html_view.innerHTML = html_view.value;
html_view.style.display = "none";
visual_view.style.display = "block";
button_el.classList.remove("active");
} else { //Show HTML view
html_view.innerText = visual_view.innerHTML;
visual_view.style.display = "none";
html_view.style.display = "block";
button_el.classList.add("active");
}
}
/**
* Executes a default action given its string via {@link document.execCommand}.
* - Static method of: {@link ve.WYSIWYG}
*
* @param {string} arg0_action
*/
static execDefaultAction (arg0_action) {
//Convert from parameters
let action = arg0_action;
//Invoke execCommand
document.execCommand(action, false);
}
/**
* Adds a link to the current {@link ve.WYSIWYG} editor being mentioned.
* - Static method of: {@link ve.WYSIWYG}
*
* @param {HTMLElement} arg0_modal_el
*/
static execLinkAction (arg0_modal_el) {
//Convert from parameters
let modal = arg0_modal_el;
//Declare local instance variables
let close = modal.querySelectorAll(".close")[0];
let selection = ve.WYSIWYG.saveSelection();
let submit = modal.querySelectorAll("button.done")[0];
//Set modal to visible
modal.style.display = "block";
//Add link once done button is active
submit.addEventListener("click", (e) => {
e.preventDefault();
let new_tab_checkbox = modal.querySelectorAll(`#new-tab`)[0];
let link_input = modal.querySelectorAll(`#link-value`)[0];
let link_value = link_input.value;
let new_tab = new_tab_checkbox.checked;
//Restore selection
ve.WYSIWYG.restoreSelection(selection);
//Handle selection
if (window.getSelection().toString()) {
let local_a = document.createElement("a");
local_a.href = link_value;
if (new_tab)
local_a.target = "_blank";
window.getSelection().getRangeAt(0).surroundContents(local_a);
}
//Hide modal, deregister modal events
modal.style.display = "none";
link_input.value = "";
submit.removeEventListener("click", arguments.callee); //[WIP] - Deprecate callee at a later date
close.removeEventListener("click", arguments.callee);
});
//Close modal on close button click
close.addEventListener("click", (e) => {
e.preventDefault();
let link_input = modal.querySelectorAll("#link-value")[0];
//Hide modal, deregister modal events
modal.style.display = "none";
link_input.value = "";
submit.removeEventListener("click", arguments.callee);
close.removeEventListener("click", arguments.callee);
});
}
/**
* Restores a saved selection for the current {@link ve.WYSIWYG} component.
* - Static method of: {@link ve.WYSIWYG}
*
* @param {Selection} arg0_saved_selection
*/
static restoreSelection (arg0_saved_selection) {
//Convert from parameters
let saved_selection = arg0_saved_selection;
//Restore selection
if (saved_selection)
if (window.getSelection) {
let selection = window.getSelection();
selection.removeAllRanges();
//Populate selection ranges
for (let i = 0, length = saved_selection.length; i < length; i++)
selection.addRange(saved_selection[i]);
} else if (document.selection && saved_selection.select) {
saved_selection.select();
}
}
/**
* Saves the current selection for later restoration by {@link ve.WYSIWYG.restoreSelection}().
* - Static method of: {@link ve.WYSIWYG}
*
* @returns {Range}
*/
static saveSelection () {
if (window.getSelection) {
let selection = window.getSelection();
if (selection.getRangeAt && selection.rangeCount) {
let ranges = [];
//Iterate over selection.rangeCount to populate ranges
for (let i = 0, length = selection.rangeCount; i < length; i++)
ranges.push(selection.getRangeAt(i));
//Return statement
return ranges;
}
} else if (document.selection && document.selection.createRange) {
//Return statement
return document.selection.createRange();
}
}
};
//Functional binding
/**
* @returns {ve.WYSIWYG}
*/
veWYSIWYG = function () {
//Return statement
return new ve.WYSIWYG(...arguments);
};