first commit
This commit is contained in:
18
assets/editor/editor.html
Normal file
18
assets/editor/editor.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<link rel="stylesheet" type="text/css" href="normalize.css">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link rel="stylesheet" type="text/css" href="platform_style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="editor" contenteditable="true"></div>
|
||||
<script type="text/javascript" src="interact.min.js"></script>
|
||||
<script type="text/javascript" src="rich_text_editor.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
23
assets/editor/index.html
Normal file
23
assets/editor/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>Spoon-Knife</title>
|
||||
<LINK href="styles.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<img src="forkit.gif" id="octocat" alt="" />
|
||||
|
||||
<!-- Feel free to change this text here -->
|
||||
<p>
|
||||
Fork me? Fork you, @octocat!
|
||||
</p>
|
||||
<p>
|
||||
Sean made a change
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
6
assets/editor/interact.min.js
vendored
Normal file
6
assets/editor/interact.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
412
assets/editor/normalize.css
vendored
Normal file
412
assets/editor/normalize.css
vendored
Normal file
@@ -0,0 +1,412 @@
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: roboto; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
8
assets/editor/platform_style.css
Normal file
8
assets/editor/platform_style.css
Normal file
@@ -0,0 +1,8 @@
|
||||
body {
|
||||
font-family: serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
699
assets/editor/rich_text_editor.js
Normal file
699
assets/editor/rich_text_editor.js
Normal file
@@ -0,0 +1,699 @@
|
||||
|
||||
// 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();
|
||||
106
assets/editor/style.css
Normal file
106
assets/editor/style.css
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (C) 2017 Wasabeef
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@charset "UTF-8";
|
||||
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: scroll;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
min-height:100%;
|
||||
}
|
||||
|
||||
#editor {
|
||||
display: table-cell;
|
||||
outline: 0px solid transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#editor[placeholder]:empty:not(:focus):before {
|
||||
content: attr(placeholder);
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
-webkit-margin-before: 0em;
|
||||
}
|
||||
|
||||
|
||||
// Thanks to Jonathan B (https://stackoverflow.com/a/23501094) for figuring that out
|
||||
|
||||
sup {
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
sub {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
img {
|
||||
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* change this when changing the class name of resizableImageClass in rich_text_editor.js */
|
||||
img.resizable {
|
||||
|
||||
border: 1px solid gray;
|
||||
|
||||
touch-action: none;
|
||||
|
||||
/* This is suggested by the doc, but I'm not sure that it is required when there is no large borders around the image. */
|
||||
/* This makes things *much* easier */
|
||||
/* box-sizing: border-box;*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
.rotate90deg {
|
||||
-webkit-transform: rotate(90deg);
|
||||
-moz-transform: rotate(90deg);
|
||||
-o-transform: rotate(90deg);
|
||||
-ms-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.rotate180deg {
|
||||
-webkit-transform: rotate(180deg);
|
||||
-moz-transform: rotate(180deg);
|
||||
-o-transform: rotate(180deg);
|
||||
-ms-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.rotate270deg {
|
||||
-webkit-transform: rotate(270deg);
|
||||
-moz-transform: rotate(270deg);
|
||||
-o-transform: rotate(270deg);
|
||||
-ms-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
Reference in New Issue
Block a user