// also edit class name in style.css when changing this. const resizableImageClass = "resizable"; const EditorDefaultHtml = "

"; var editor = { _textField: document.getElementById('editor'), _htmlSetByApplication: null, _currentSelection: { "startContainer": 0, "startOffset": 0, "endContainer": 0, "endOffset": 0 }, _useWindowLocationForEditorStateChangedCallback: false, _imageMinWidth: 100, _imageMinHeight: 50, _isImageResizingEnabled: true, init: function() { document.addEventListener("selectionchange", function() { editor._backupRange(); editor._handleTextEntered(); // in newly selected area different commands may be activated / deactivated }); this._textField.addEventListener("keydown", function(e) { var BACKSPACE = 8; var M = 77; if(e.which == BACKSPACE) { if(editor._textField.innerText.length == 1) { // prevent that first paragraph gets deleted e.preventDefault(); return false; } } else if(e.which == M && e.ctrlKey) { // TODO: what is Ctrl + M actually good for? e.preventDefault(); // but be aware in this way also (JavaFX) application won't be able to use Ctrl + M return false; } }); this._textField.addEventListener("keyup", function(e) { if(e.altKey || e.ctrlKey) { // some key combinations activate commands like CTRL + B setBold() -> update editor state so that UI is aware of this editor._updateEditorState(); } }); this._textField.addEventListener("paste", function(e) { editor._handlePaste(e); }); this._ensureEditorInsertsParagraphWhenPressingEnter(); this._initDragImageToResize(); this._updateEditorState(); }, _ensureEditorInsertsParagraphWhenPressingEnter: function() { // see https://stackoverflow.com/a/36373967 this._executeCommand("DefaultParagraphSeparator", "p"); this._textField.innerHTML = ""; // clear previous content var newElement = document.createElement("p"); newElement.innerHTML = "​"; this._textField.appendChild(newElement); var selection=document.getSelection(); var range=document.createRange(); range.setStart(newElement.firstChild, 1); selection.removeAllRanges(); selection.addRange(range); }, _initDragImageToResize: function() { var angle = 0; interact.addDocument(window.document, { events: { passive: false }, }); interact('img.' + resizableImageClass) .draggable({ onmove: window.dragMoveListener, restrict: { restriction: 'parent', elementRect: { top: 0, left: 0, bottom: 1, right: 1 } }, }) .resizable({ // resize from right or bottom edges: { top: true, left: true, right: true, bottom: true}, // keep the edges inside the parent restrictEdges: { outer: 'parent', endOnly: true, }, // minimum size restrictSize: { min: { width: this._imageMinWidth, height: this._imageMinHeight }, }, inertia: true, preserveAspectRatio: true, }) .gesturable({ onmove: function (event) { var target = event.target; angle += event.da; if(Math.abs(90 - (angle % 360)) < 10){ angle = 90;} if(Math.abs(180 - (angle % 360)) < 10){ angle = 180;} if(Math.abs(270 - (angle % 360)) < 10){ angle = 270;} if(Math.abs(angle % 360) < 10){ angle = 0;} target.style.webkitTransform = target.style.transform = 'rotate(' + angle + 'deg)'; } }) .on('resizemove', function (event) { var target = event.target, x = (parseFloat(target.getAttribute('data-x')) || 0), y = (parseFloat(target.getAttribute('data-y')) || 0); // update the element's style target.style.width = event.rect.width + 'px'; target.style.height = event.rect.height + 'px'; target.width = event.rect.width + 'px'; target.height = event.rect.height + 'px'; target.setAttribute('data-x', x); target.setAttribute('data-y', y); }); }, _handleTextEntered: function() { if(this._getHtml() == "


") { // SwiftKey, when deleting all entered text, inserts a pure "
" therefore check for

​​

doesn't work anymore this._ensureEditorInsertsParagraphWhenPressingEnter(); } this._updateEditorState(); }, _handlePaste: function(event) { var clipboardData = event.clipboardData || window.clipboardData; var pastedData = clipboardData.getData('text/html') || clipboardData.getData('text').replace(/(?:\r\n|\r|\n)/g, '
'); // replace new lines // TODO: may use 'text/plain' instead of 'text' this._waitTillPastedDataInserted(event, pastedData); }, _waitTillPastedDataInserted: function(event, pastedData) { var previousHtml = this._getHtml(); setTimeout(function () { // on paste event inserted text is not inserted yet -> wait for till text has been inserted editor._waitTillPastedTextInserted(previousHtml, 10, pastedData); // max 10 tries, after that we give up to prevent endless loops }, 100); }, _waitTillPastedTextInserted: function(previousHtml, iteration, pastedData) { var hasBeenInserted = this._getHtml() != previousHtml; if(hasBeenInserted || ! iteration) { // there seems to be a bug (on Linux only?) when pasting data e.g. from Firefox: then only '' gets inserted if((this._getHtml().indexOf('​ÿþ<') !== -1 || this._getHtml().indexOf('ÿþ<
') !== -1) && previousHtml.indexOf('​​ÿþ<') === -1) { this._textField.innerHTML = this._getHtml().replace('​ÿþ<', pastedData).replace('ÿþ<
', pastedData); // TODO: set caret to end of pasted data } this._updateEditorState(); } else { setTimeout(function () { // wait for till pasted data has been inserted editor._waitTillPastedTextInserted(pastedText, iteration - 1); }, 100); } }, _getHtml: function() { return this._textField.innerHTML; }, _getHtmlWithoutInternalModifications: function() { var clonedHtml = this._textField.cloneNode(true); this._removeResizeImageClasses(clonedHtml); return clonedHtml.innerHTML; }, getEncodedHtml: function() { return encodeURIComponent(this._getHtmlWithoutInternalModifications()); }, setHtml: function(html, baseUrl) { if(baseUrl) { this._setBaseUrl(baseUrl); } if(html.length != 0) { var decodedHtml = this._decodeHtml(html); this._textField.innerHTML = decodedHtml; this._htmlSetByApplication = decodedHtml; if(this._isImageResizingEnabled) { this.makeImagesResizeable(); } } else { this._ensureEditorInsertsParagraphWhenPressingEnter(); this._htmlSetByApplication = null; } this.didHtmlChange = false; }, _decodeHtml: function(html) { return decodeURIComponent(html.replace(/\+/g, '%20')); }, _setBaseUrl: function(baseUrl) { var baseElements = document.head.getElementsByTagName('base'); var baseElement = null; if(baseElements.length > 0) { baseElement = baseElements[0]; } else { var baseElement = document.createElement('base'); document.head.appendChild(baseElement); // don't know why but append() is not available } baseElement.setAttribute('href', baseUrl); baseElement.setAttribute('target', '_blank'); }, useWindowLocationForEditorStateChangedCallback: function() { this._useWindowLocationForEditorStateChangedCallback = true; }, makeImagesResizeable: function() { this._isImageResizingEnabled = true; var images = document.getElementsByTagName("img"); for(var i = 0; i < images.length; i++) { this._addClass(images[i], resizableImageClass); } }, disableImageResizing: function() { this._isImageResizingEnabled = false; this._removeResizeImageClasses(document); }, _removeResizeImageClasses: function(document) { var images = document.getElementsByTagName("img"); for(var i = 0; i < images.length; i++) { this._removeClass(images[i], resizableImageClass); } }, _hasClass: function(element, className) { return !!element.className.match(new RegExp('(\\s|^)' + className +'(\\s|$)')); }, _addClass: function(element, className) { if (this._hasClass(element, className) == false) { element.className += " " + className; } }, _removeClass: function(element, className) { if (this._hasClass(element, className)) { element.classList.remove(className); var classAttributeValue = element.getAttribute('class'); if (!!! classAttributeValue) { // remove class attribute if no class is left to restore original html element.removeAttribute('class'); } } }, /* Text Commands */ undo: function() { this._executeCommand('undo', null); }, redo: function() { this._executeCommand('redo', null); }, setBold: function() { this._executeCommand('bold', null); }, setItalic: function() { this._executeCommand('italic', null); }, setUnderline: function() { this._executeCommand('underline', null); }, setSubscript: function() { this._executeCommand('subscript', null); }, setSuperscript: function() { this._executeCommand('superscript', null); }, setStrikeThrough: function() { this._executeCommand('strikeThrough', null); }, setTextColor: function(color) { this._executeStyleCommand('foreColor', color); }, setTextBackgroundColor: function(color) { if(color == 'rgba(0, 0, 0, 0)') { // resetting backColor does not work with any color value (whether #00000000 nor rgba(0, 0, 0, 0)), we have to pass 'inherit'. Thanks to https://stackoverflow.com/a/7071465 for pointing this out to me this._executeStyleCommand('backColor', 'inherit'); } else { this._executeStyleCommand('backColor', color); } }, setFontName: function(fontName) { this._executeCommand("fontName", fontName); }, setFontSize: function(fontSize) { this._executeCommand("fontSize", fontSize); }, setHeading: function(heading) { this._executeCommand('formatBlock', ''); }, setFormattingToParagraph: function() { this._executeCommand('formatBlock', '

'); }, setPreformat: function() { this._executeCommand('formatBlock', '

');
    },

    setBlockQuote: function() {
        this._executeCommand('formatBlock', '
'); }, removeFormat: function() { this._executeCommand('removeFormat', null); }, setJustifyLeft: function() { this._executeCommand('justifyLeft', null); }, setJustifyCenter: function() { this._executeCommand('justifyCenter', null); }, setJustifyRight: function() { this._executeCommand('justifyRight', null); }, setJustifyFull: function() { this._executeCommand('justifyFull', null); }, setIndent: function() { this._executeCommand('indent', null); }, setOutdent: function() { this._executeCommand('outdent', null); }, insertBulletList: function() { this._executeCommand('insertUnorderedList', null); }, insertNumberedList: function() { this._executeCommand('insertOrderedList', null); }, /* Insert elements */ insertLink: function(url, title) { this._restoreRange(); var sel = document.getSelection(); if (sel.toString().length == 0) { this._insertHtml(""+title+""); } else if (sel.rangeCount) { var el = document.createElement("a"); el.setAttribute("href", url); el.setAttribute("title", title); var range = sel.getRangeAt(0).cloneRange(); range.surroundContents(el); sel.removeAllRanges(); sel.addRange(range); this._updateEditorState(); } }, insertImage: function(url, alt, width, height, rotation) { var imageElement = document.createElement('img'); imageElement.setAttribute('src', url); if(alt) { imageElement.setAttribute('alt', alt); } if(width) { imageElement.setAttribute('width', width); } if(height) { imageElement.setAttribute('height', height); } if(this._isImageResizingEnabled) { imageElement.setAttribute('class', resizableImageClass); } if(rotation) { this._setImageRotation(imageElement, rotation); } this._insertHtml(imageElement.outerHTML); }, _setImageRotation: function(imageElement, rotation) { if(rotation == 90) { this._addClass(imageElement, 'rotate90deg'); } else if(rotation == 180) { this._addClass(imageElement, 'rotate180deg'); } else if(rotation == 270) { this._addClass(imageElement, 'rotate270deg'); } }, insertCheckbox: function(text) { var editor = this; var html = '  '; this._insertHtml(html); }, _checkboxClicked: function(clickedCheckbox) { // incredible, checked attribute doesn't get set in html, see issue https://github.com/dankito/RichTextEditor/issues/24 if (clickedCheckbox.checked) { clickedCheckbox.setAttribute('checked', 'checked'); } else { clickedCheckbox.removeAttribute('checked'); } this._updateEditorState(); }, insertHtml: function(encodedHtml) { var html = this._decodeHtml(encodedHtml); this._insertHtml(html); }, _insertHtml: function(html) { this._backupRange(); this._restoreRange(); document.execCommand('insertHTML', false, html); if(this._isImageResizingEnabled) { this.makeImagesResizeable(); } this._updateEditorState(); }, /* Editor default settings */ setBaseTextColor: function(color) { this._textField.style.color = color; }, setBaseFontFamily: function(fontFamily) { this._textField.style.fontFamily = fontFamily; }, setBaseFontSize: function(size) { this._textField.style.fontSize = size; }, setPadding: function(left, top, right, bottom) { this._textField.style.paddingLeft = left; this._textField.style.paddingTop = top; this._textField.style.paddingRight = right; this._textField.style.paddingBottom = bottom; }, // TODO: is this one ever user? setBackgroundColor: function(color) { document.body.style.backgroundColor = color; }, setBackgroundImage: function(image) { this._textField.style.backgroundImage = image; }, setWidth: function(size) { this._textField.style.minWidth = size; // TODO: why did i use minWidth here but height (not minHeight) below? }, setHeight: function(size) { this._textField.style.height = size; }, setTextAlign: function(align) { this._textField.style.textAlign = align; }, setVerticalAlign: function(align) { this._textField.style.verticalAlign = align; }, setPlaceholder: function(placeholder) { this._textField.setAttribute("placeholder", placeholder); }, setInputEnabled: function(inputEnabled) { this._textField.contentEditable = String(inputEnabled); if(inputEnabled) { // TODO: may interferes with _isImageResizingEnabled this.makeImagesResizeable(); } else { this.disableImageResizing(); } }, focus: function() { var range = document.createRange(); range.selectNodeContents(this._textField); range.collapse(false); var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); this._textField.focus(); }, blurFocus: function() { this._textField.blur(); }, _executeStyleCommand: function(command, parameter) { this._executeCommand("styleWithCSS", null, true); this._executeCommand(command, parameter); this._executeCommand("styleWithCSS", null, false); }, _executeCommand: function(command, parameter) { document.execCommand(command, false, parameter); this._updateEditorState(); }, _updateEditorState: function() { var html = this._getHtmlWithoutInternalModifications(); var didHtmlChange = (this._htmlSetByApplication != null && this._htmlSetByApplication != html) || // html set by application changed (this._htmlSetByApplication == null && html != EditorDefaultHtml); // or if html not set by application: default html changed if (typeof editorCallback !== 'undefined') { // in most applications like in the JavaFX app changing window.location.href doesn't work -> tell them via callback that editor state changed editorCallback.updateEditorState(didHtmlChange) // these applications determine editor state manually } else if (this._useWindowLocationForEditorStateChangedCallback) { // Android can handle changes to windows.location -> communicate editor changes via a self defined protocol name var commandStates = this._determineCommandStates(); var editorState = { 'didHtmlChange': didHtmlChange, 'html': html, // TODO: remove in upcoming versions 'commandStates': commandStates }; window.location.href = "editor-state-changed-callback://" + encodeURIComponent(JSON.stringify(editorState)); } }, _determineCommandStates: function() { var commandStates = {}; this._determineStateForCommand('undo', commandStates); this._determineStateForCommand('redo', commandStates); this._determineStateForCommand('bold', commandStates); this._determineStateForCommand('italic', commandStates); this._determineStateForCommand('underline', commandStates); this._determineStateForCommand('subscript', commandStates); this._determineStateForCommand('superscript', commandStates); this._determineStateForCommand('strikeThrough', commandStates); this._determineStateForCommand('foreColor', commandStates); this._determineStateForCommand('backColor', commandStates); this._determineStateForCommand('fontName', commandStates); this._determineStateForCommand('fontSize', commandStates); this._determineStateForCommand('formatBlock', commandStates); this._determineStateForCommand('removeFormat', commandStates); this._determineStateForCommand('justifyLeft', commandStates); this._determineStateForCommand('justifyCenter', commandStates); this._determineStateForCommand('justifyRight', commandStates); this._determineStateForCommand('justifyFull', commandStates); this._determineStateForCommand('indent', commandStates); this._determineStateForCommand('outdent', commandStates); this._determineStateForCommand('insertUnorderedList', commandStates); this._determineStateForCommand('insertOrderedList', commandStates); this._determineStateForCommand('insertHorizontalRule', commandStates); this._determineStateForCommand('insertHTML', commandStates); return commandStates; }, _determineStateForCommand: function(command, commandStates) { commandStates[command.toUpperCase()] = { 'executable': document.queryCommandEnabled(command), 'value': document.queryCommandValue(command) } }, _backupRange: function(){ var selection = window.getSelection(); if(selection.rangeCount > 0) { var range = selection.getRangeAt(0); this._currentSelection = { "startContainer": range.startContainer, "startOffset": range.startOffset, "endContainer": range.endContainer, "endOffset": range.endOffset }; } }, _restoreRange: function(){ var selection = window.getSelection(); selection.removeAllRanges(); var range = document.createRange(); range.setStart(this._currentSelection.startContainer, this._currentSelection.startOffset); range.setEnd(this._currentSelection.endContainer, this._currentSelection.endOffset); selection.addRange(range); }, } editor.init();