/*global self, document, DOMException */
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
// Full polyfill for browsers with no classList support
if (!("classList" in document.createElement("_"))) {
(function (view) {
"use strict";
if (!('Element' in view)) return;
var
classListProp = "classList"
, protoProp = "prototype"
, elemCtrProto = view.Element[protoProp]
, objCtr = Object
, strTrim = String[protoProp].trim || function () {
return this.replace(/^\s+|\s+$/g, "");
}
, arrIndexOf = Array[protoProp].indexOf || function (item) {
var
i = 0
, len = this.length
;
for (; i < len; i++) {
if (i in this && this[i] === item) {
return i;
}
}
return -1;
}
// Vendors: please allow content code to instantiate DOMExceptions
, DOMEx = function (type, message) {
this.name = type;
this.code = DOMException[type];
this.message = message;
}
, checkTokenAndGetIndex = function (classList, token) {
if (token === "") {
throw new DOMEx(
"SYNTAX_ERR"
, "An invalid or illegal string was specified"
);
}
if (/\s/.test(token)) {
throw new DOMEx(
"INVALID_CHARACTER_ERR"
, "String contains an invalid character"
);
}
return arrIndexOf.call(classList, token);
}
, ClassList = function (elem) {
var
trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
, i = 0
, len = classes.length
;
for (; i < len; i++) {
this.push(classes[i]);
}
this._updateClassName = function () {
elem.setAttribute("class", this.toString());
};
}
, classListProto = ClassList[protoProp] = []
, classListGetter = function () {
return new ClassList(this);
}
;
// Most DOMException implementations don't allow calling DOMException's toString()
// on non-DOMExceptions. Error's toString() is sufficient here.
DOMEx[protoProp] = Error[protoProp];
classListProto.item = function (i) {
return this[i] || null;
};
classListProto.contains = function (token) {
token += "";
return checkTokenAndGetIndex(this, token) !== -1;
};
classListProto.add = function () {
var
tokens = arguments
, i = 0
, l = tokens.length
, token
, updated = false
;
do {
token = tokens[i] + "";
if (checkTokenAndGetIndex(this, token) === -1) {
this.push(token);
updated = true;
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.remove = function () {
var
tokens = arguments
, i = 0
, l = tokens.length
, token
, updated = false
, index
;
do {
token = tokens[i] + "";
index = checkTokenAndGetIndex(this, token);
while (index !== -1) {
this.splice(index, 1);
updated = true;
index = checkTokenAndGetIndex(this, token);
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.toggle = function (token, force) {
token += "";
var
result = this.contains(token)
, method = result ?
force !== true && "remove"
:
force !== false && "add"
;
if (method) {
this[method](token);
}
if (force === true || force === false) {
return force;
} else {
return !result;
}
};
classListProto.toString = function () {
return this.join(" ");
};
if (objCtr.defineProperty) {
var classListPropDesc = {
get: classListGetter
, enumerable: true
, configurable: true
};
try {
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
} catch (ex) { // IE 8 doesn't support enumerable:true
if (ex.number === -0x7FF5EC54) {
classListPropDesc.enumerable = false;
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
}
}
} else if (objCtr[protoProp].__defineGetter__) {
elemCtrProto.__defineGetter__(classListProp, classListGetter);
}
}(self));
}
/* Blob.js
* A Blob implementation.
* 2014-07-24
*
* By Eli Grey, http://eligrey.com
* By Devin Samarin, https://github.com/dsamarin
* License: X11/MIT
* See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
*/
/*global self, unescape */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
(function (view) {
"use strict";
view.URL = view.URL || view.webkitURL;
if (view.Blob && view.URL) {
try {
new Blob;
return;
} catch (e) {}
}
// Internally we use a BlobBuilder implementation to base Blob off of
// in order to support older browsers that only have BlobBuilder
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
var
get_class = function(object) {
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
}
, FakeBlobBuilder = function BlobBuilder() {
this.data = [];
}
, FakeBlob = function Blob(data, type, encoding) {
this.data = data;
this.size = data.length;
this.type = type;
this.encoding = encoding;
}
, FBB_proto = FakeBlobBuilder.prototype
, FB_proto = FakeBlob.prototype
, FileReaderSync = view.FileReaderSync
, FileException = function(type) {
this.code = this[this.name = type];
}
, file_ex_codes = (
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
).split(" ")
, file_ex_code = file_ex_codes.length
, real_URL = view.URL || view.webkitURL || view
, real_create_object_URL = real_URL.createObjectURL
, real_revoke_object_URL = real_URL.revokeObjectURL
, URL = real_URL
, btoa = view.btoa
, atob = view.atob
, ArrayBuffer = view.ArrayBuffer
, Uint8Array = view.Uint8Array
, origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
;
FakeBlob.fake = FB_proto.fake = true;
while (file_ex_code--) {
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
}
// Polyfill URL
if (!real_URL.createObjectURL) {
URL = view.URL = function(uri) {
var
uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
, uri_origin
;
uri_info.href = uri;
if (!("origin" in uri_info)) {
if (uri_info.protocol.toLowerCase() === "data:") {
uri_info.origin = null;
} else {
uri_origin = uri.match(origin);
uri_info.origin = uri_origin && uri_origin[1];
}
}
return uri_info;
};
}
URL.createObjectURL = function(blob) {
var
type = blob.type
, data_URI_header
;
if (type === null) {
type = "application/octet-stream";
}
if (blob instanceof FakeBlob) {
data_URI_header = "data:" + type;
if (blob.encoding === "base64") {
return data_URI_header + ";base64," + blob.data;
} else if (blob.encoding === "URI") {
return data_URI_header + "," + decodeURIComponent(blob.data);
} if (btoa) {
return data_URI_header + ";base64," + btoa(blob.data);
} else {
return data_URI_header + "," + encodeURIComponent(blob.data);
}
} else if (real_create_object_URL) {
return real_create_object_URL.call(real_URL, blob);
}
};
URL.revokeObjectURL = function(object_URL) {
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
real_revoke_object_URL.call(real_URL, object_URL);
}
};
FBB_proto.append = function(data/*, endings*/) {
var bb = this.data;
// decode data to a binary string
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
var
str = ""
, buf = new Uint8Array(data)
, i = 0
, buf_len = buf.length
;
for (; i < buf_len; i++) {
str += String.fromCharCode(buf[i]);
}
bb.push(str);
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
if (FileReaderSync) {
var fr = new FileReaderSync;
bb.push(fr.readAsBinaryString(data));
} else {
// async FileReader won't work as BlobBuilder is sync
throw new FileException("NOT_READABLE_ERR");
}
} else if (data instanceof FakeBlob) {
if (data.encoding === "base64" && atob) {
bb.push(atob(data.data));
} else if (data.encoding === "URI") {
bb.push(decodeURIComponent(data.data));
} else if (data.encoding === "raw") {
bb.push(data.data);
}
} else {
if (typeof data !== "string") {
data += ""; // convert unsupported types to strings
}
// decode UTF-16 to binary string
bb.push(unescape(encodeURIComponent(data)));
}
};
FBB_proto.getBlob = function(type) {
if (!arguments.length) {
type = null;
}
return new FakeBlob(this.data.join(""), type, "raw");
};
FBB_proto.toString = function() {
return "[object BlobBuilder]";
};
FB_proto.slice = function(start, end, type) {
var args = arguments.length;
if (args < 3) {
type = null;
}
return new FakeBlob(
this.data.slice(start, args > 1 ? end : this.data.length)
, type
, this.encoding
);
};
FB_proto.toString = function() {
return "[object Blob]";
};
FB_proto.close = function() {
this.size = 0;
delete this.data;
};
return FakeBlobBuilder;
}(view));
view.Blob = function(blobParts, options) {
var type = options ? (options.type || "") : "";
var builder = new BlobBuilder();
if (blobParts) {
for (var i = 0, len = blobParts.length; i < len; i++) {
if (Uint8Array && blobParts[i] instanceof Uint8Array) {
builder.append(blobParts[i].buffer);
}
else {
builder.append(blobParts[i]);
}
}
}
var blob = builder.getBlob(type);
if (!blob.slice && blob.webkitSlice) {
blob.slice = blob.webkitSlice;
}
return blob;
};
var getPrototypeOf = Object.getPrototypeOf || function(object) {
return object.__proto__;
};
view.Blob.prototype = getPrototypeOf(new view.Blob());
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));
(function (root, factory) {
'use strict';
if (typeof module === 'object') {
module.exports = factory;
} else if (typeof define === 'function' && define.amd) {
define(function () {
return factory;
});
} else {
root.MediumEditor = factory;
}
}(this, function () {
'use strict';
var Util;
(function (window) {
'use strict';
// Params: Array, Boolean, Object
function getProp(parts, create, context) {
if (!context) {
context = window;
}
try {
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
if (!(p in context)) {
if (create) {
context[p] = {};
} else {
return;
}
}
context = context[p];
}
return context;
} catch (e) {
// "p in context" throws an exception when context is a number, boolean, etc. rather than an object,
// so in that corner case just return undefined (by having no return statement)
}
}
function copyInto(overwrite, dest) {
var prop,
sources = Array.prototype.slice.call(arguments, 2);
dest = dest || {};
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
if (source) {
for (prop in source) {
if (source.hasOwnProperty(prop) &&
typeof source[prop] !== 'undefined' &&
(overwrite || dest.hasOwnProperty(prop) === false)) {
dest[prop] = source[prop];
}
}
}
}
return dest;
}
Util = {
// http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
// by rg89
isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
// https://github.com/jashkenas/underscore
keyCode: {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
ESCAPE: 27,
SPACE: 32,
DELETE: 46
},
parentElements: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'],
extend: function extend(/* dest, source1, source2, ...*/) {
var args = [true].concat(Array.prototype.slice.call(arguments));
return copyInto.apply(this, args);
},
defaults: function defaults(/*dest, source1, source2, ...*/) {
var args = [false].concat(Array.prototype.slice.call(arguments));
return copyInto.apply(this, args);
},
derives: function derives(base, derived) {
var origPrototype = derived.prototype;
function Proto() { }
Proto.prototype = base.prototype;
derived.prototype = new Proto();
derived.prototype.constructor = base;
derived.prototype = copyInto(false, derived.prototype, origPrototype);
return derived;
},
// Find the next node in the DOM tree that represents any text that is being
// displayed directly next to the targetNode (passed as an argument)
// Text that appears directly next to the current node can be:
// - A sibling text node
// - A descendant of a sibling element
// - A sibling text node of an ancestor
// - A descendant of a sibling element of an ancestor
findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
var pastTarget = false,
nextNode,
nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
// Use a native NodeIterator to iterate over all the text nodes that are descendants
// of the rootNode. Once past the targetNode, choose the first non-empty text node
nextNode = nodeIterator.nextNode();
while (nextNode) {
if (nextNode === targetNode) {
pastTarget = true;
} else if (pastTarget) {
if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
break;
}
}
nextNode = nodeIterator.nextNode();
}
return nextNode;
},
isDescendant: function isDescendant(parent, child) {
if (!parent || !child) {
return false;
}
var node = child.parentNode;
while (node !== null) {
if (node === parent) {
return true;
}
node = node.parentNode;
}
return false;
},
// https://github.com/jashkenas/underscore
isElement: function isElement(obj) {
return !!(obj && obj.nodeType === 1);
},
now: Date.now,
// https://github.com/jashkenas/underscore
throttle: function (func, wait) {
var THROTTLE_INTERVAL = 50,
context,
args,
result,
timeout = null,
previous = 0,
later = function () {
previous = Date.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
};
if (!wait && wait !== 0) {
wait = THROTTLE_INTERVAL;
}
return function () {
var now = Date.now(),
remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
},
traverseUp: function (current, testElementFunction) {
do {
if (current.nodeType === 1) {
if (testElementFunction(current)) {
return current;
}
// do not traverse upwards past the nearest containing editor
if (current.getAttribute('data-medium-element')) {
return false;
}
}
current = current.parentNode;
} while (current);
return false;
},
htmlEntities: function (str) {
// converts special characters (like <) into their escaped/encoded values (like <).
// This allows you to show to display the string without the browser reading it as HTML.
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
},
// http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
insertHTMLCommand: function (doc, html) {
var selection, range, el, fragment, node, lastNode, toReplace;
if (doc.queryCommandSupported('insertHTML')) {
try {
return doc.execCommand('insertHTML', false, html);
} catch (ignore) {}
}
selection = doc.defaultView.getSelection();
if (selection.getRangeAt && selection.rangeCount) {
range = selection.getRangeAt(0);
toReplace = range.commonAncestorContainer;
// Ensure range covers maximum amount of nodes as possible
// By moving up the DOM and selecting ancestors whose only child is the range
if ((toReplace.nodeType === 3 && toReplace.nodeValue === range.toString()) ||
(toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {
while (toReplace.parentNode &&
toReplace.parentNode.childNodes.length === 1 &&
!toReplace.parentNode.getAttribute('data-medium-element')) {
toReplace = toReplace.parentNode;
}
range.selectNode(toReplace);
}
range.deleteContents();
el = doc.createElement("div");
el.innerHTML = html;
fragment = doc.createDocumentFragment();
while (el.firstChild) {
node = el.firstChild;
lastNode = fragment.appendChild(node);
}
range.insertNode(fragment);
// Preserve the selection:
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
}
},
getSelectionRange: function (ownerDocument) {
var selection = ownerDocument.getSelection();
if (selection.rangeCount === 0) {
return null;
}
return selection.getRangeAt(0);
},
// http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
// by You
getSelectionStart: function (ownerDocument) {
var node = ownerDocument.getSelection().anchorNode,
startNode = (node && node.nodeType === 3 ? node.parentNode : node);
return startNode;
},
getSelectionData: function (el) {
var tagName;
if (el && el.tagName) {
tagName = el.tagName.toLowerCase();
}
while (el && this.parentElements.indexOf(tagName) === -1) {
el = el.parentNode;
if (el && el.tagName) {
tagName = el.tagName.toLowerCase();
}
}
return {
el: el,
tagName: tagName
};
},
execFormatBlock: function (doc, tagName) {
var selectionData = this.getSelectionData(this.getSelectionStart(doc));
// FF handles blockquote differently on formatBlock
// allowing nesting, we need to use outdent
// https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
if (tagName === 'blockquote' && selectionData.el &&
selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') {
return doc.execCommand('outdent', false, null);
}
if (selectionData.tagName === tagName) {
tagName = 'p';
}
// When IE we need to add <> to heading elements and
// blockquote needs to be called as indent
// http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
// http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
if (this.isIE) {
if (tagName === 'blockquote') {
return doc.execCommand('indent', false, tagName);
}
tagName = '<' + tagName + '>';
}
return doc.execCommand('formatBlock', false, tagName);
},
// TODO: not sure if this should be here
setTargetBlank: function (el) {
var i;
if (el.tagName.toLowerCase() === 'a') {
el.target = '_blank';
} else {
el = el.getElementsByTagName('a');
for (i = 0; i < el.length; i += 1) {
el[i].target = '_blank';
}
}
},
addClassToAnchors: function (el, buttonClass) {
var classes = buttonClass.split(' '),
i,
j;
if (el.tagName.toLowerCase() === 'a') {
for (j = 0; j < classes.length; j += 1) {
el.classList.add(classes[j]);
}
} else {
el = el.getElementsByTagName('a');
for (i = 0; i < el.length; i += 1) {
for (j = 0; j < classes.length; j += 1) {
el[i].classList.add(classes[j]);
}
}
}
},
isListItem: function (node) {
if (!node) {
return false;
}
if (node.tagName.toLowerCase() === 'li') {
return true;
}
var parentNode = node.parentNode,
tagName = parentNode.tagName.toLowerCase();
while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
if (tagName === 'li') {
return true;
}
parentNode = parentNode.parentNode;
if (parentNode && parentNode.tagName) {
tagName = parentNode.tagName.toLowerCase();
} else {
return false;
}
}
return false;
},
cleanListDOM: function (element) {
if (element.tagName.toLowerCase() === 'li') {
var list = element.parentElement;
if (list.parentElement.tagName.toLowerCase() === 'p') { // yes we need to clean up
this.unwrapElement(list.parentElement);
}
}
},
unwrapElement: function (element) {
var parent = element.parentNode,
current = element.firstChild,
next;
do {
next = current.nextSibling;
parent.insertBefore(current, element);
current = next;
} while (current);
parent.removeChild(element);
},
warn: function(){
if(window.console !== undefined){
console.warn.apply(console, arguments);
}
},
deprecated: function(oldName, newName, version){
// simple deprecation warning mechanism.
var m = oldName + " is deprecated, please use " + newName + " instead.";
if(version){
m += " Will be removed in " + version;
}
Util.warn(m);
},
deprecatedMethod: function (oldName, newName, args, version) {
// run the replacement and warn when someone calls a deprecated method
Util.deprecated(oldName, newName, version);
if (typeof this[newName] === 'function') {
this[newName].apply(this, args);
}
},
cleanupAttrs: function (el, attrs) {
attrs.forEach(function (attr) {
el.removeAttribute(attr);
});
},
cleanupTags: function (el, tags) {
tags.forEach(function (tag) {
if (el.tagName.toLowerCase() === tag) {
el.parentNode.removeChild(el);
}
});
},
setObject: function(name, value, context){
// summary:
// Set a property from a dot-separated string, such as "A.B.C"
var parts = name.split("."), p = parts.pop(), obj = getProp(parts, true, context);
return obj && p ? (obj[p] = value) : undefined; // Object
},
getObject: function(name, create, context){
// summary:
// Get a property from a dot-separated string, such as "A.B.C"
return getProp(name ? name.split(".") : [], create, context); // Object
}
};
}(window));
var ButtonsData;
(function(){
'use strict';
ButtonsData = {
'bold': {
name: 'bold',
action: 'bold',
aria: 'bold',
tagNames: ['b', 'strong'],
style: {
prop: 'font-weight',
value: '700|bold'
},
useQueryState: true,
contentDefault: 'B',
contentFA: '',
key: 'b'
},
'italic': {
name: 'italic',
action: 'italic',
aria: 'italic',
tagNames: ['i', 'em'],
style: {
prop: 'font-style',
value: 'italic'
},
useQueryState: true,
contentDefault: 'I',
contentFA: '',
key: 'i'
},
'underline': {
name: 'underline',
action: 'underline',
aria: 'underline',
tagNames: ['u'],
style: {
prop: 'text-decoration',
value: 'underline'
},
useQueryState: true,
contentDefault: 'U',
contentFA: '',
key: 'u'
},
'strikethrough': {
name: 'strikethrough',
action: 'strikethrough',
aria: 'strike through',
tagNames: ['strike'],
style: {
prop: 'text-decoration',
value: 'line-through'
},
useQueryState: true,
contentDefault: 'A',
contentFA: ''
},
'superscript': {
name: 'superscript',
action: 'superscript',
aria: 'superscript',
tagNames: ['sup'],
/* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript
https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
// useQueryState: true
contentDefault: 'x1',
contentFA: ''
},
'subscript': {
name: 'subscript',
action: 'subscript',
aria: 'subscript',
tagNames: ['sub'],
/* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript
https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
// useQueryState: true
contentDefault: 'x1',
contentFA: ''
},
'image': {
name: 'image',
action: 'image',
aria: 'image',
tagNames: ['img'],
contentDefault: 'image',
contentFA: ''
},
'quote': {
name: 'quote',
action: 'append-blockquote',
aria: 'blockquote',
tagNames: ['blockquote'],
contentDefault: '“',
contentFA: ''
},
'orderedlist': {
name: 'orderedlist',
action: 'insertorderedlist',
aria: 'ordered list',
tagNames: ['ol'],
useQueryState: true,
contentDefault: '1.',
contentFA: ''
},
'unorderedlist': {
name: 'unorderedlist',
action: 'insertunorderedlist',
aria: 'unordered list',
tagNames: ['ul'],
useQueryState: true,
contentDefault: '•',
contentFA: ''
},
'pre': {
name: 'pre',
action: 'append-pre',
aria: 'preformatted text',
tagNames: ['pre'],
contentDefault: '0101',
contentFA: ''
},
'indent': {
name: 'indent',
action: 'indent',
aria: 'indent',
tagNames: [],
contentDefault: '→',
contentFA: ''
},
'outdent': {
name: 'outdent',
action: 'outdent',
aria: 'outdent',
tagNames: [],
contentDefault: '←',
contentFA: ''
},
'justifyCenter': {
name: 'justifyCenter',
action: 'justifyCenter',
aria: 'center justify',
tagNames: [],
style: {
prop: 'text-align',
value: 'center'
},
contentDefault: 'C',
contentFA: ''
},
'justifyFull': {
name: 'justifyFull',
action: 'justifyFull',
aria: 'full justify',
tagNames: [],
style: {
prop: 'text-align',
value: 'justify'
},
contentDefault: 'J',
contentFA: ''
},
'justifyLeft': {
name: 'justifyLeft',
action: 'justifyLeft',
aria: 'left justify',
tagNames: [],
style: {
prop: 'text-align',
value: 'left'
},
contentDefault: 'L',
contentFA: ''
},
'justifyRight': {
name: 'justifyRight',
action: 'justifyRight',
aria: 'right justify',
tagNames: [],
style: {
prop: 'text-align',
value: 'right'
},
contentDefault: 'R',
contentFA: ''
},
'header1': {
name: 'header1',
action: function (options) {
return 'append-' + options.firstHeader;
},
aria: function (options) {
return options.firstHeader;
},
tagNames: function (options) {
return [options.firstHeader];
},
contentDefault: 'H1'
},
'header2': {
name: 'header2',
action: function (options) {
return 'append-' + options.secondHeader;
},
aria: function (options) {
return options.secondHeader;
},
tagNames: function (options) {
return [options.secondHeader];
},
contentDefault: 'H2'
},
// Known inline elements that are not removed, or not removed consistantly across browsers:
// ,
[new RegExp(/<\/?o:[a-z]*>/gi), ''] ]; } /*jslint regexp: false*/ /* Paste Options: * * forcePlainText: Forces pasting as plain text. Default: true * cleanPastedHtml: cleans pasted content from different sources, like google docs etc. Default: false * cleanReplacements: custom pairs (2 element arrays) of RegExp and replacement text to use during paste when * __forcePlainText__ or __cleanPastedHtml__ are `true` OR when calling `cleanPaste(text)` * helper method. Default: [] * cleanAttrs: list of attributes to remove when ... default: ['class', 'style', 'dir'] * cleanTags: list of element tag names to remove... default: ['meta'] * * ----- internal options needed from base ----- * disableReturn * targetBlank * contentWindow * ownerDocument */ PasteHandler = function (instance, options) { this.base = instance; this.options = options; if (this.options.forcePlainText || this.options.cleanPastedHTML) { this.base.subscribe('editablePaste', this.handlePaste.bind(this)); } }; PasteHandler.prototype = { handlePaste: function (event, element) { var paragraphs, html = '', p, dataFormatHTML = 'text/html', dataFormatPlain = 'text/plain'; if (this.options.contentWindow.clipboardData && event.clipboardData === undefined) { event.clipboardData = this.options.contentWindow.clipboardData; // If window.clipboardData exists, but event.clipboardData doesn't exist, // we're probably in IE. IE only has two possibilities for clipboard // data format: 'Text' and 'URL'. // // Of the two, we want 'Text': dataFormatHTML = 'Text'; dataFormatPlain = 'Text'; } if (event.clipboardData && event.clipboardData.getData && !event.defaultPrevented) { event.preventDefault(); if (this.options.cleanPastedHTML && event.clipboardData.getData(dataFormatHTML)) { return this.cleanPaste(event.clipboardData.getData(dataFormatHTML)); } if (!(this.options.disableReturn || element.getAttribute('data-disable-return'))) { paragraphs = event.clipboardData.getData(dataFormatPlain).split(/[\r\n]/g); for (p = 0; p < paragraphs.length; p += 1) { if (paragraphs[p] !== '') { html += '
' + Util.htmlEntities(paragraphs[p]) + '
'; } } Util.insertHTMLCommand(this.options.ownerDocument, html); } else { html = Util.htmlEntities(event.clipboardData.getData(dataFormatPlain)); Util.insertHTMLCommand(this.options.ownerDocument, html); } } }, cleanPaste: function (text) { var i, elList, workEl, el = Selection.getSelectionElement(this.options.contentWindow), multiline = /
');
this.pasteHTML('
' + elList.join('
') + '
'); try { this.options.ownerDocument.execCommand('insertText', false, "\n"); } catch (ignore) { } // block element cleanup elList = el.querySelectorAll('a,p,div,br'); for (i = 0; i < elList.length; i += 1) { workEl = elList[i]; // Microsoft Word replaces some spaces with newlines. // While newlines between block elements are meaningless, newlines within // elements are sometimes actually spaces. workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' '); switch (workEl.tagName.toLowerCase()) { case 'a': if (this.options.targetBlank) { Util.setTargetBlank(workEl); } break; case 'p': case 'div': this.filterCommonBlocks(workEl); break; case 'br': this.filterLineBreak(workEl); break; } } } else { this.pasteHTML(text); } }, pasteHTML: function (html, options) { options = Util.defaults({}, options, this.options); var elList, workEl, i, fragmentBody, pasteBlock = this.options.ownerDocument.createDocumentFragment(); pasteBlock.appendChild(this.options.ownerDocument.createElement('body')); fragmentBody = pasteBlock.querySelector('body'); fragmentBody.innerHTML = html; this.cleanupSpans(fragmentBody); elList = fragmentBody.querySelectorAll('*'); for (i = 0; i < elList.length; i += 1) { workEl = elList[i]; Util.cleanupAttrs(workEl, options.cleanAttrs); Util.cleanupTags(workEl, options.cleanTags); } Util.insertHTMLCommand(this.options.ownerDocument, fragmentBody.innerHTML.replace(/ /g, ' ')); }, isCommonBlock: function (el) { return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div')); }, filterCommonBlocks: function (el) { if (/^\s*$/.test(el.textContent) && el.parentNode) { el.parentNode.removeChild(el); } }, filterLineBreak: function (el) { if (this.isCommonBlock(el.previousElementSibling)) { // remove stray br's following common block elements this.removeWithParent(el); } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) { // remove br's just inside open or close tags of a div/p this.removeWithParent(el); } else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') { // and br's that are the only child of elements other than div/p this.removeWithParent(el); } }, // remove an element, including its parent, if it is the only element within its parent removeWithParent: function (el) { if (el && el.parentNode) { if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) { el.parentNode.parentNode.removeChild(el.parentNode); } else { el.parentNode.removeChild(el); } } }, cleanupSpans: function (container_el) { var i, el, new_el, spans = container_el.querySelectorAll('.replace-with'), isCEF = function (el) { return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false'); }; for (i = 0; i < spans.length; i += 1) { el = spans[i]; new_el = this.options.ownerDocument.createElement(el.classList.contains('bold') ? 'b' : 'i'); if (el.classList.contains('bold') && el.classList.contains('italic')) { // add an i tag as well if this has both italics and bold new_el.innerHTML = '' + el.innerHTML + ''; } else { new_el.innerHTML = el.innerHTML; } el.parentNode.replaceChild(new_el, el); } spans = container_el.querySelectorAll('span'); for (i = 0; i < spans.length; i += 1) { el = spans[i]; // bail if span is in contenteditable = false if (Util.traverseUp(el, isCEF)) { return false; } // remove empty spans, replace others with their contents if (/^\s*$/.test()) { el.parentNode.removeChild(el); } else { el.parentNode.replaceChild(this.options.ownerDocument.createTextNode(el.textContent), el); } } } }; }()); var AnchorExtension; (function () { 'use strict'; function AnchorDerived() { this.parent = true; this.options = { name: 'anchor', action: 'createLink', aria: 'link', tagNames: ['a'], contentDefault: '#', contentFA: '' }; this.name = 'anchor'; this.hasForm = true; } AnchorDerived.prototype = { // Button and Extension handling // labels for the anchor-edit form buttons formSaveLabel: '✓', formCloseLabel: '×', // Called when the button the toolbar is clicked // Overrides DefaultButton.handleClick handleClick: function (evt) { evt.preventDefault(); evt.stopPropagation(); var selectedParentElement = Selection.getSelectedParentElement(Util.getSelectionRange(this.base.options.ownerDocument)); if (selectedParentElement.tagName && selectedParentElement.tagName.toLowerCase() === 'a') { return this.base.execAction('unlink'); } if (!this.isDisplayed()) { this.showForm(); } return false; }, // Called by medium-editor to append form to the toolbar getForm: function () { if (!this.form) { this.form = this.createForm(); } return this.form; }, getTemplate: function () { var template = [ '' ]; template.push( '', this.base.options.buttonLabels === 'fontawesome' ? '' : this.formSaveLabel, '' ); template.push('', this.base.options.buttonLabels === 'fontawesome' ? '' : this.formCloseLabel, ''); // both of these options are slightly moot with the ability to // override the various form buildup/serialize functions. if (this.base.options.anchorTarget) { // fixme: ideally, this options.anchorInputCheckboxLabel would be a formLabel too, // figure out how to deprecate? also consider `fa-` icon default implcations. template.push( '', '' ); } if (this.base.options.anchorButton) { // fixme: expose this `Button` text as a formLabel property, too // and provide similar access to a `fa-` icon default. template.push( '', '' ); } return template.join(""); }, // Used by medium-editor when the default toolbar is to be displayed isDisplayed: function () { return this.getForm().style.display === 'block'; }, hideForm: function () { this.getForm().style.display = 'none'; this.getInput().value = ''; }, showForm: function (link_value) { var input = this.getInput(); this.base.saveSelection(); this.base.hideToolbarDefaultActions(); this.getForm().style.display = 'block'; this.base.setToolbarPosition(); input.value = link_value || ''; input.focus(); }, // Called by core when tearing down medium-editor (deactivate) deactivate: function () { if (!this.form) { return false; } if (this.form.parentNode) { this.form.parentNode.removeChild(this.form); } delete this.form; }, // core methods getFormOpts: function () { // no notion of private functions? wanted `_getFormOpts` var targetCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-target'), buttonCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-button'), opts = { url: this.getInput().value }; if (this.base.options.checkLinkFormat) { opts.url = this.checkLinkFormat(opts.url); } if (targetCheckbox && targetCheckbox.checked) { opts.target = "_blank"; } else { opts.target = "_self"; } if (buttonCheckbox && buttonCheckbox.checked) { opts.buttonClass = this.base.options.anchorButtonClass; } return opts; }, doFormSave: function () { var opts = this.getFormOpts(); this.completeFormSave(opts); }, completeFormSave: function (opts) { this.base.restoreSelection(); this.base.createLink(opts); this.base.checkSelection(); }, checkLinkFormat: function (value) { var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/; return (re.test(value) ? '' : 'http://') + value; }, doFormCancel: function () { this.base.restoreSelection(); this.base.checkSelection(); }, // form creation and event handling attachFormEvents: function (form) { var close = form.querySelector(".medium-editor-toolbar-close"), save = form.querySelector(".medium-editor-toolbar-save"), input = form.querySelector(".medium-editor-toolbar-input"); // Handle clicks on the form itself this.base.on(form, 'click', this.handleFormClick.bind(this)); // Handle typing in the textbox this.base.on(input, 'keyup', this.handleTextboxKeyup.bind(this)); // Handle close button clicks this.base.on(close, 'click', this.handleCloseClick.bind(this)); // Handle save button clicks (capture) this.base.on(save, 'click', this.handleSaveClick.bind(this), true); }, createForm: function () { var doc = this.base.options.ownerDocument, form = doc.createElement('div'); // Anchor Form (div) form.className = 'medium-editor-toolbar-form'; form.id = 'medium-editor-toolbar-form-anchor-' + this.base.id; form.innerHTML = this.getTemplate(); this.attachFormEvents(form); return form; }, getInput: function () { return this.getForm().querySelector('input.medium-editor-toolbar-input'); }, handleTextboxKeyup: function (event) { // For ENTER -> create the anchor if (event.keyCode === Util.keyCode.ENTER) { event.preventDefault(); this.doFormSave(); return; } // For ESCAPE -> close the form if (event.keyCode === Util.keyCode.ESCAPE) { event.preventDefault(); this.doFormCancel(); } }, handleFormClick: function (event) { // make sure not to hide form when clicking inside the form event.stopPropagation(); }, handleSaveClick: function (event) { // Clicking Save -> create the anchor event.preventDefault(); this.doFormSave(); }, handleCloseClick: function (event) { // Click Close -> close the form event.preventDefault(); this.doFormCancel(); } }; AnchorExtension = Util.derives(DefaultButton, AnchorDerived); }()); var AnchorPreview; (function () { 'use strict'; AnchorPreview = function () { this.parent = true; this.name = 'anchor-preview'; }; AnchorPreview.prototype = { // the default selector to locate where to // put the activeAnchor value in the preview previewValueSelector: 'i', init: function (instance) { this.base = instance; this.anchorPreview = this.createPreview(); this.base.options.elementsContainer.appendChild(this.anchorPreview); this.attachToEditables(); }, getPreviewElement: function () { return this.anchorPreview; }, createPreview: function () { var el = this.base.options.ownerDocument.createElement('div'); el.id = 'medium-editor-anchor-preview-' + this.base.id; el.className = 'medium-editor-anchor-preview'; el.innerHTML = this.getTemplate(); this.base.on(el, 'click', this.handleClick.bind(this)); return el; }, getTemplate: function () { return ' '; }, deactivate: function () { if (this.anchorPreview) { if (this.anchorPreview.parentNode) { this.anchorPreview.parentNode.removeChild(this.anchorPreview); } delete this.anchorPreview; } }, hidePreview: function () { this.anchorPreview.classList.remove('medium-editor-anchor-preview-active'); this.activeAnchor = null; }, showPreview: function (anchorEl) { if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') || anchorEl.getAttribute('data-disable-preview')) { return true; } if (this.previewValueSelector) { this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value; } this.anchorPreview.classList.add('medium-toolbar-arrow-over'); this.anchorPreview.classList.remove('medium-toolbar-arrow-under'); if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) { this.anchorPreview.classList.add('medium-editor-anchor-preview-active'); } this.activeAnchor = anchorEl; this.positionPreview(); this.attachPreviewHandlers(); return this; }, positionPreview: function () { var buttonHeight = 40, boundary = this.activeAnchor.getBoundingClientRect(), middleBoundary = (boundary.left + boundary.right) / 2, halfOffsetWidth, defaultLeft; halfOffsetWidth = this.anchorPreview.offsetWidth / 2; defaultLeft = this.base.options.diffLeft - halfOffsetWidth; this.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - this.base.options.diffTop + this.base.options.contentWindow.pageYOffset - this.anchorPreview.offsetHeight) + 'px'; if (middleBoundary < halfOffsetWidth) { this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px'; } else if ((this.base.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) { this.anchorPreview.style.left = this.base.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px'; } else { this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px'; } }, attachToEditables: function () { this.base.subscribe('editableMouseover', this.handleEditableMouseover.bind(this)); }, handleClick: function () { var range, sel, anchorExtension = this.base.getExtensionByName('anchor'), activeAnchor = this.activeAnchor; if (anchorExtension && activeAnchor) { range = this.base.options.ownerDocument.createRange(); range.selectNodeContents(this.activeAnchor); sel = this.base.options.contentWindow.getSelection(); sel.removeAllRanges(); sel.addRange(range); // Using setTimeout + options.delay because: // We may actually be displaying the anchor form, which should be controlled by options.delay this.base.delay(function () { if (activeAnchor) { anchorExtension.showForm(activeAnchor.attributes.href.value); activeAnchor = null; } }.bind(this)); } this.hidePreview(); }, handleAnchorMouseout: function () { this.anchorToPreview = null; this.base.off(this.activeAnchor, 'mouseout', this.instance_handleAnchorMouseout); this.instance_handleAnchorMouseout = null; }, handleEditableMouseover: function (event) { if (event.target && event.target.tagName.toLowerCase() === 'a') { // Detect empty href attributes // The browser will make href="" or href="#top" // into absolute urls when accessed as event.targed.href, so check the html if (!/href=["']\S+["']/.test(event.target.outerHTML) || /href=["']#\S+["']/.test(event.target.outerHTML)) { return true; } // only show when hovering on anchors if (this.base.toolbar && this.base.toolbar.isDisplayed()) { // only show when toolbar is not present return true; } // detach handler for other anchor in case we hovered multiple anchors quickly if (this.activeAnchor && this.activeAnchor !== event.target) { this.detachPreviewHandlers(); } this.anchorToPreview = event.target; this.instance_handleAnchorMouseout = this.handleAnchorMouseout.bind(this); this.base.on(this.anchorToPreview, 'mouseout', this.instance_handleAnchorMouseout); // Using setTimeout + options.delay because: // - We're going to show the anchor preview according to the configured delay // if the mouse has not left the anchor tag in that time this.base.delay(function () { if (this.anchorToPreview) { //this.activeAnchor = this.anchorToPreview; this.showPreview(this.anchorToPreview); } }.bind(this)); } }, handlePreviewMouseover: function () { this.lastOver = (new Date()).getTime(); this.hovering = true; }, handlePreviewMouseout: function (event) { if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) { this.hovering = false; } }, updatePreview: function () { if (this.hovering) { return true; } var durr = (new Date()).getTime() - this.lastOver; if (durr > this.base.options.anchorPreviewHideDelay) { // hide the preview 1/2 second after mouse leaves the link this.detachPreviewHandlers(); } }, detachPreviewHandlers: function () { // cleanup clearInterval(this.interval_timer); if (this.instance_handlePreviewMouseover) { this.base.off(this.anchorPreview, 'mouseover', this.instance_handlePreviewMouseover); this.base.off(this.anchorPreview, 'mouseout', this.instance_handlePreviewMouseout); if (this.activeAnchor) { this.base.off(this.activeAnchor, 'mouseover', this.instance_handlePreviewMouseover); this.base.off(this.activeAnchor, 'mouseout', this.instance_handlePreviewMouseout); } } this.hidePreview(); this.hovering = this.instance_handlePreviewMouseover = this.instance_handlePreviewMouseout = null; }, // TODO: break up method and extract out handlers attachPreviewHandlers: function () { this.lastOver = (new Date()).getTime(); this.hovering = true; this.instance_handlePreviewMouseover = this.handlePreviewMouseover.bind(this); this.instance_handlePreviewMouseout = this.handlePreviewMouseout.bind(this); this.interval_timer = setInterval(this.updatePreview.bind(this), 200); this.base.on(this.anchorPreview, 'mouseover', this.instance_handlePreviewMouseover); this.base.on(this.anchorPreview, 'mouseout', this.instance_handlePreviewMouseout); this.base.on(this.activeAnchor, 'mouseover', this.instance_handlePreviewMouseover); this.base.on(this.activeAnchor, 'mouseout', this.instance_handlePreviewMouseout); } }; }()); var Toolbar; (function () { 'use strict'; Toolbar = function Toolbar(instance) { this.base = instance; this.options = instance.options; this.initThrottledMethods(); }; Toolbar.prototype = { // Toolbar creation/deletion createToolbar: function () { var toolbar = this.base.options.ownerDocument.createElement('div'); toolbar.id = 'medium-editor-toolbar-' + this.base.id; toolbar.className = 'medium-editor-toolbar'; if (this.options.staticToolbar) { toolbar.className += " static-toolbar"; } else { toolbar.className += " stalker-toolbar"; } toolbar.appendChild(this.createToolbarButtons()); // Add any forms that extensions may have this.base.commands.forEach(function (extension) { if (extension.hasForm) { toolbar.appendChild(extension.getForm()); } }); this.attachEventHandlers(); this.base.subscribe('externalInteraction', this.handleBlur.bind(this)); return toolbar; }, createToolbarButtons: function () { var ul = this.base.options.ownerDocument.createElement('ul'), li, btn, buttons; ul.id = 'medium-editor-toolbar-actions' + this.base.id; ul.className = 'medium-editor-toolbar-actions clearfix'; ul.style.display = 'block'; this.base.commands.forEach(function (extension) { if (typeof extension.getButton === 'function') { btn = extension.getButton(this.base); li = this.base.options.ownerDocument.createElement('li'); if (Util.isElement(btn)) { li.appendChild(btn); } else { li.innerHTML = btn; } ul.appendChild(li); } }.bind(this)); buttons = ul.querySelectorAll('button'); if (buttons.length > 0) { buttons[0].classList.add(this.options.firstButtonClass); buttons[buttons.length - 1].classList.add(this.options.lastButtonClass); } return ul; }, deactivate: function () { if (this.toolbar) { if (this.toolbar.parentNode) { this.toolbar.parentNode.removeChild(this.toolbar); } delete this.toolbar; } }, // Toolbar accessors getToolbarElement: function () { if (!this.toolbar) { this.toolbar = this.createToolbar(); } return this.toolbar; }, getToolbarActionsElement: function () { return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions'); }, // Toolbar event handlers initThrottledMethods: function () { // throttledPositionToolbar is throttled because: // - It will be called when the browser is resizing, which can fire many times very quickly // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits this.throttledPositionToolbar = Util.throttle(function () { if (this.base.isActive) { this.positionToolbarIfShown(); } }.bind(this)); }, attachEventHandlers: function () { this.base.on(this.options.ownerDocument.documentElement, 'mousedown', this.handleDocumentMousedown.bind(this)); // Handle mouseup on document for updating the selection in the toolbar this.base.on(this.options.ownerDocument.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this)); // Add a scroll event for sticky toolbar if (this.options.staticToolbar && this.options.stickyToolbar) { // On scroll (capture), re-position the toolbar this.base.on(this.options.contentWindow, 'scroll', this.handleWindowScroll.bind(this), true); } // On resize, re-position the toolbar this.base.on(this.options.contentWindow, 'resize', this.handleWindowResize.bind(this)); // Handlers for each contentedtiable element this.base.elements.forEach(function (element) { // Attach click handler to each contenteditable element this.base.on(element, 'click', this.handleEditableClick.bind(this)); // Attach keyup handler to each contenteditable element this.base.on(element, 'keyup', this.handleEditableKeyup.bind(this)); // Attach blur handler to each contenteditable element this.base.on(element, 'blur', this.handleEditableBlur.bind(this)); }, this); }, handleWindowScroll: function () { this.positionToolbarIfShown(); }, handleWindowResize: function () { this.throttledPositionToolbar(); }, handleDocumentMousedown: function (event) { this.lastMousedownTarget = event.target; }, handleDocumentMouseup: function (event) { this.lastMousedownTarget = null; // Do not trigger checkState when mouseup fires over the toolbar if (event && event.target && Util.isDescendant(this.getToolbarElement(), event.target)) { return false; } this.checkState(); }, handleEditableClick: function () { // Delay the call to checkState to handle bug where selection is empty // immediately after clicking inside a pre-existing selection setTimeout(function () { this.checkState(); }.bind(this), 0); }, handleEditableKeyup: function () { this.checkState(); }, handleEditableBlur: function (event) { var isRelatedTargetOwnedByThisEditor = false, relatedTarget = (event && event.relatedTarget) ? event.relatedTarget : this.lastMousedownTarget; // Do not trigger checkState when blurring the editable area and clicking into the toolbar if (Util.isDescendant(this.getToolbarElement(), relatedTarget)) { return false; } if (relatedTarget) { // Remove all selections before checking state. This is necessary to avoid issues with // standardizeSelectionStart 'canceling' the blur event by moving the selection (in Chrome only). // In Safari, when you click on a non-button element outside of the contenteditable, the selection // is already nulled out by the browser at this point, but remained set in Chrome, Firefox, and IE11. // This change will effectively normalize all browsers' behavior to be the same as Safari. this.base.elements.forEach(function (el) { isRelatedTargetOwnedByThisEditor = isRelatedTargetOwnedByThisEditor || Util.isDescendant(el, relatedTarget) || relatedTarget === el; }, this); // We only remove all the ranges if the user clicked outside the contenteditables managed by this // medium-editor instance. Otherwise keep the ranges if they are set, we need the range to be present // for various things done by the toolbar to work. if (!isRelatedTargetOwnedByThisEditor) { this.options.contentWindow.getSelection().removeAllRanges(); } } this.checkState(); }, handleBlur: function () { // Delay the call to hideToolbar to handle bug with multiple editors on the page at once setTimeout(function () { this.hideToolbar(); }.bind(this), 0); }, // Hiding/showing toolbar isDisplayed: function () { return this.getToolbarElement().classList.contains('medium-editor-toolbar-active'); }, showToolbar: function () { if (!this.isDisplayed()) { this.getToolbarElement().classList.add('medium-editor-toolbar-active'); if (typeof this.options.onShowToolbar === 'function') { this.options.onShowToolbar(); } } }, hideToolbar: function () { if (this.isDisplayed()) { this.base.commands.forEach(function (extension) { if (typeof extension.onHide === 'function') { extension.onHide(); } }); this.getToolbarElement().classList.remove('medium-editor-toolbar-active'); if (typeof this.options.onHideToolbar === 'function') { this.options.onHideToolbar(); } } }, isToolbarDefaultActionsDisplayed: function () { return this.getToolbarActionsElement().style.display === 'block'; }, hideToolbarDefaultActions: function () { if (this.isToolbarDefaultActionsDisplayed()) { this.getToolbarActionsElement().style.display = 'none'; } }, showToolbarDefaultActions: function () { this.hideExtensionForms(); if (!this.isToolbarDefaultActionsDisplayed()) { this.getToolbarActionsElement().style.display = 'block'; } // Using setTimeout + options.delay because: // We will actually be displaying the toolbar, which should be controlled by options.delay this.base.delay(function () { this.showToolbar(); }.bind(this)); }, hideExtensionForms: function () { // Hide all extension forms this.base.commands.forEach(function (extension) { if (extension.hasForm && extension.isDisplayed()) { extension.hideForm(); } }); }, // Responding to changes in user selection // Checks for existance of multiple block elements in the current selection multipleBlockElementsSelected: function () { /*jslint regexp: true*/ var selectionHtml = Selection.getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''), hasMultiParagraphs = selectionHtml.match(/<(p|h[1-6]|blockquote)[^>]*>/g); /*jslint regexp: false*/ return !!hasMultiParagraphs && hasMultiParagraphs.length > 1; }, // TODO: selection and selectionRange should be properties of the // Selection object checkSelectionElement: function (newSelection, selectionElement) { var i, adjacentNode, offset = 0, newRange, selectionRange = newSelection.getRangeAt(0); /* * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start * will be at the very end of an element. In other browsers, the selectionRange start * would instead be at the very beginning of an element that actually has content. * example: * foobar * * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning * of the 'bar' span. However, there are cases where firefox will have the selectionRange start * at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any * properties on the 'bar' span, they won't be reflected accurately in the toolbar * (ie 'Bold' button wouldn't be active) * * So, for cases where the selectionRange start is at the end of an element/node, find the next * adjacent text node that actually has content in it, and move the selectionRange start there. */ if (this.options.standardizeSelectionStart && selectionRange.startContainer.nodeValue && (selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) { adjacentNode = Util.findAdjacentTextNodeWithContent(Selection.getSelectionElement(this.options.contentWindow), selectionRange.startContainer, this.options.ownerDocument); if (adjacentNode) { offset = 0; while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) { offset = offset + 1; } newRange = this.options.ownerDocument.createRange(); newRange.setStart(adjacentNode, offset); newRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); newSelection.removeAllRanges(); newSelection.addRange(newRange); selectionRange = newRange; } } for (i = 0; i < this.base.elements.length; i += 1) { if (this.base.elements[i] === selectionElement) { this.showAndUpdateToolbar(); return; } } if (!this.options.staticToolbar) { this.hideToolbar(); } }, checkState: function () { var newSelection, selectionElement; if (!this.base.preventSelectionUpdates) { newSelection = this.options.contentWindow.getSelection(); if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') || (this.options.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected()) || Selection.selectionInContentEditableFalse(this.options.contentWindow)) { if (!this.options.staticToolbar) { this.hideToolbar(); } else { this.showAndUpdateToolbar(); } } else { selectionElement = Selection.getSelectionElement(this.options.contentWindow); if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) { if (!this.options.staticToolbar) { this.hideToolbar(); } } else { this.checkSelectionElement(newSelection, selectionElement); } } } }, // Updating the toolbar showAndUpdateToolbar: function () { this.setToolbarButtonStates(); this.showToolbarDefaultActions(); this.setToolbarPosition(); }, setToolbarButtonStates: function () { this.base.commands.forEach(function (extension) { if (typeof extension.isActive === 'function') { extension.setInactive(); } }.bind(this)); this.checkActiveButtons(); }, checkActiveButtons: function () { var manualStateChecks = [], queryState = null, selectionRange = Util.getSelectionRange(this.options.ownerDocument), parentNode, updateExtensionState = function (extension) { if (typeof extension.checkState === 'function') { extension.checkState(parentNode); } else if (typeof extension.isActive === 'function' && typeof extension.isAlreadyApplied === 'function') { if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) { extension.setActive(); } } }; if (!selectionRange) { return; } parentNode = Selection.getSelectedParentElement(selectionRange); // Loop through all commands this.base.commands.forEach(function (command) { // For those commands where we can use document.queryCommandState(), do so if (typeof command.queryCommandState === 'function') { queryState = command.queryCommandState(); // If queryCommandState returns a valid value, we can trust the browser // and don't need to do our manual checks if (queryState !== null) { if (queryState) { command.setActive(); } return; } } // We can't use queryCommandState for this command, so add to manualStateChecks manualStateChecks.push(command); }); // Climb up the DOM and do manual checks for whether a certain command is currently enabled for this node while (parentNode.tagName !== undefined && Util.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) { manualStateChecks.forEach(updateExtensionState); // we can abort the search upwards if we leave the contentEditable element if (this.base.elements.indexOf(parentNode) !== -1) { break; } parentNode = parentNode.parentNode; } }, // Positioning toolbar positionToolbarIfShown: function () { if (this.isDisplayed()) { this.setToolbarPosition(); } }, setToolbarPosition: function () { var container = Selection.getSelectionElement(this.options.contentWindow), selection = this.options.contentWindow.getSelection(), anchorPreview; // If there isn't a valid selection, bail if (!container || !this.options.contentWindow.getSelection().focusNode) { return this; } // If the container isn't part of this medium-editor instance, bail if (this.base.elements.indexOf(container) === -1) { return this; } if (this.options.staticToolbar) { this.showToolbar(); this.positionStaticToolbar(container); } else if (!selection.isCollapsed) { this.showToolbar(); this.positionToolbar(selection); } anchorPreview = this.base.getExtensionByName('anchor-preview'); if (anchorPreview && typeof anchorPreview.hidePreview === 'function') { anchorPreview.hidePreview(); } }, positionStaticToolbar: function (container) { // position the toolbar at left 0, so we can get the real width of the toolbar this.getToolbarElement().style.left = '0'; // document.documentElement for IE 9 var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop, windowWidth = this.options.contentWindow.innerWidth, toolbarElement = this.getToolbarElement(), containerRect = container.getBoundingClientRect(), containerTop = containerRect.top + scrollTop, containerCenter = (containerRect.left + (containerRect.width / 2)), toolbarHeight = toolbarElement.offsetHeight, toolbarWidth = toolbarElement.offsetWidth, halfOffsetWidth = toolbarWidth / 2, targetLeft; if (this.options.stickyToolbar) { // If it's beyond the height of the editor, position it at the bottom of the editor if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) { toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px'; toolbarElement.classList.remove('sticky-toolbar'); // Stick the toolbar to the top of the window } else if (scrollTop > (containerTop - toolbarHeight)) { toolbarElement.classList.add('sticky-toolbar'); toolbarElement.style.top = "0px"; // Normal static toolbar position } else { toolbarElement.classList.remove('sticky-toolbar'); toolbarElement.style.top = containerTop - toolbarHeight + "px"; } } else { toolbarElement.style.top = containerTop - toolbarHeight + "px"; } if (this.options.toolbarAlign === 'left') { targetLeft = containerRect.left; } else if (this.options.toolbarAlign === 'center') { targetLeft = containerCenter - halfOffsetWidth; } else if (this.options.toolbarAlign === 'right') { targetLeft = containerRect.right - toolbarWidth; } if (targetLeft < 0) { targetLeft = 0; } else if ((targetLeft + toolbarWidth) > windowWidth) { targetLeft = windowWidth - toolbarWidth; } toolbarElement.style.left = targetLeft + 'px'; }, positionToolbar: function (selection) { // position the toolbar at left 0, so we can get the real width of the toolbar this.getToolbarElement().style.left = '0'; var windowWidth = this.options.contentWindow.innerWidth, range = selection.getRangeAt(0), boundary = range.getBoundingClientRect(), middleBoundary = (boundary.left + boundary.right) / 2, toolbarElement = this.getToolbarElement(), toolbarHeight = toolbarElement.offsetHeight, toolbarWidth = toolbarElement.offsetWidth, halfOffsetWidth = toolbarWidth / 2, buttonHeight = 50, defaultLeft = this.options.diffLeft - halfOffsetWidth; if (boundary.top < buttonHeight) { toolbarElement.classList.add('medium-toolbar-arrow-over'); toolbarElement.classList.remove('medium-toolbar-arrow-under'); toolbarElement.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px'; } else { toolbarElement.classList.add('medium-toolbar-arrow-under'); toolbarElement.classList.remove('medium-toolbar-arrow-over'); toolbarElement.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px'; } if (middleBoundary < halfOffsetWidth) { toolbarElement.style.left = defaultLeft + halfOffsetWidth + 'px'; } else if ((windowWidth - middleBoundary) < halfOffsetWidth) { toolbarElement.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px'; } else { toolbarElement.style.left = defaultLeft + middleBoundary + 'px'; } } }; }()); var Placeholders; (function () { 'use strict'; Placeholders = function (instance) { this.base = instance; this.initPlaceholders(); this.attachEventHandlers(); }; Placeholders.prototype = { initPlaceholders: function () { this.base.elements.forEach(function (el) { this.updatePlaceholder(el); }, this); }, showPlaceholder: function (el) { if (el) { el.classList.add('medium-editor-placeholder'); } }, hidePlaceholder: function (el) { if (el) { el.classList.remove('medium-editor-placeholder'); } }, updatePlaceholder: function (el) { if (!(el.querySelector('img')) && !(el.querySelector('blockquote')) && el.textContent.replace(/^\s+|\s+$/g, '') === '') { this.showPlaceholder(el); } else { this.hidePlaceholder(el); } }, attachEventHandlers: function () { // Custom events this.base.subscribe('externalInteraction', this.handleExternalInteraction.bind(this)); // Check placeholder on blur this.base.subscribe('editableBlur', this.handleBlur.bind(this)); // Events where we always hide the placeholder this.base.subscribe('editableClick', this.handleHidePlaceholderEvent.bind(this)); this.base.subscribe('editableKeypress', this.handleHidePlaceholderEvent.bind(this)); this.base.subscribe('editablePaste', this.handleHidePlaceholderEvent.bind(this)); }, handleHidePlaceholderEvent: function (event, element) { // Events where we hide the placeholder this.hidePlaceholder(element); }, handleBlur: function (event, element) { // Update placeholder for element that lost focus this.updatePlaceholder(element); }, handleExternalInteraction: function () { // Update all placeholders this.initPlaceholders(); } }; }()); var extensionDefaults; (function(){ // for now this is empty because nothing interally uses an Extension default. // as they are converted, provide them here. extensionDefaults = { }; })(); function MediumEditor(elements, options) { 'use strict'; return this.init(elements, options); } (function () { 'use strict'; // Event handlers that shouldn't be exposed externally function handleDisabledEnterKeydown(event, element) { if (this.options.disableReturn || element.getAttribute('data-disable-return')) { event.preventDefault(); } else if (this.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) { var node = Util.getSelectionStart(this.options.ownerDocument); if (node && node.textContent.trim() === '') { event.preventDefault(); } } } function handleTabKeydown(event) { // Override tab only for pre nodes var node = Util.getSelectionStart(this.options.ownerDocument), tag = node && node.tagName.toLowerCase(); if (tag === 'pre') { event.preventDefault(); Util.insertHTMLCommand(this.options.ownerDocument, ' '); } // Tab to indent list structures! if (Util.isListItem(node)) { event.preventDefault(); // If Shift is down, outdent, otherwise indent if (event.shiftKey) { this.options.ownerDocument.execCommand('outdent', false, null); } else { this.options.ownerDocument.execCommand('indent', false, null); } } } function handleBlockDeleteKeydowns(event) { var range, sel, p, node = Util.getSelectionStart(this.options.ownerDocument), tagName = node.tagName.toLowerCase(), isEmpty = /^(\s+|[CURSOR]