700 lines
22 KiB
JavaScript
700 lines
22 KiB
JavaScript
|
||
// also edit class name in style.css when changing this.
|
||
const resizableImageClass = "resizable";
|
||
|
||
const EditorDefaultHtml = "<p></p>";
|
||
|
||
|
||
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() == "<p><br></p>") { // SwiftKey, when deleting all entered text, inserts a pure "<br>" therefore check for <p>​</p> 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, '<br />'); // 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('ÿþ<<br>') !== -1) && previousHtml.indexOf('ÿþ<') === -1) {
|
||
this._textField.innerHTML = this._getHtml().replace('ÿþ<', pastedData).replace('ÿþ<<br>', 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', '<h'+heading+'>');
|
||
},
|
||
|
||
setFormattingToParagraph: function() {
|
||
this._executeCommand('formatBlock', '<p>');
|
||
},
|
||
|
||
setPreformat: function() {
|
||
this._executeCommand('formatBlock', '<pre>');
|
||
},
|
||
|
||
setBlockQuote: function() {
|
||
this._executeCommand('formatBlock', '<blockquote>');
|
||
},
|
||
|
||
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("<a href='"+url+"'>"+title+"</a>");
|
||
}
|
||
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 = '<input type="checkbox" name="'+ text +'" value="'+ text +'" onclick="editor._checkboxClicked(this)"/> ';
|
||
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();
|