/*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))),
// http://stackoverflow.com/a/11752084/569101
isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),
// https://github.com/jashkenas/underscore
keyCode: {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
ESCAPE: 27,
SPACE: 32,
DELETE: 46
},
/**
* Returns true if it's metaKey on Mac, or ctrlKey on non-Mac.
* See #591
*/
isMetaCtrlKey: function (event) {
if ((this.isMac && event.metaKey) || (!this.isMac && event.ctrlKey)) {
return true;
}
return false;
},
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, checkEquality) {
if (!parent || !child) {
return false;
}
if (checkEquality && parent === child) {
return true;
}
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) {
if (!current) {
return false;
}
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);
},
/**
* Set target to blank on the given el element
*
* TODO: not sure if this should be here
*
* When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link
* instead of the created link itself (as it is for Chrome for example), so we retrieve all "a" children to grab the good one by
* using `anchorUrl` to ensure that we are adding target="_blank" on the good one.
* This isn't a bulletproof solution anyway ..
*/
setTargetBlank: function (el, anchorUrl) {
var i, url = anchorUrl || false;
if (el.tagName.toLowerCase() === 'a') {
el.target = '_blank';
} else {
el = el.getElementsByTagName('a');
for (i = 0; i < el.length; i += 1) {
if (false === url || url === el[i].attributes.href.value) {
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);
},
/* splitDOMTree
*
* Given a root element some descendant element, split the root element
* into its own element containing the descendant element and all elements
* on the left or right side of the descendant ('right' is default)
*
* example:
*
*
* / | \
*
* / \ / \ / \
* 1 2 3 4 5 6
*
* If I wanted to split this tree given the
as the root and "4" as the leaf
* the result would be (the prime ' marks indicates nodes that are created as clones):
*
* SPLITTING OFF 'RIGHT' TREE SPLITTING OFF 'LEFT' TREE
*
*
'
'
* / \ / \ / \ |
* '
* / \ | | / \ /\ /\ /\
* 1 2 3 4 5 6 1 2 3 4 5 6
*
* The above example represents splitting off the 'right' or 'left' part of a tree, where
* the
' would be returned as an element not appended to the DOM, and the
* would remain in place where it was
*
*/
splitOffDOMTree: function (rootNode, leafNode, splitLeft) {
var splitOnNode = leafNode,
createdNode = null,
splitRight = !splitLeft;
// loop until we hit the root
while (splitOnNode !== rootNode) {
var currParent = splitOnNode.parentNode,
newParent = currParent.cloneNode(false),
targetNode = (splitRight ? splitOnNode : currParent.firstChild),
appendLast;
// Create a new parent element which is a clone of the current parent
if (createdNode) {
if (splitRight) {
// If we're splitting right, add previous created element before siblings
newParent.appendChild(createdNode);
} else {
// If we're splitting left, add previous created element last
appendLast = createdNode;
}
}
createdNode = newParent;
while (targetNode) {
var sibling = targetNode.nextSibling;
// Special handling for the 'splitNode'
if (targetNode === splitOnNode) {
if (!targetNode.hasChildNodes()) {
targetNode.parentNode.removeChild(targetNode);
} else {
// For the node we're splitting on, if it has children, we need to clone it
// and not just move it
targetNode = targetNode.cloneNode(false);
}
// If the resulting split node has content, add it
if (targetNode.textContent) {
createdNode.appendChild(targetNode);
}
targetNode = (splitRight ? sibling : null);
} else {
// For general case, just remove the element and only
// add it to the split tree if it contains something
targetNode.parentNode.removeChild(targetNode);
if (targetNode.hasChildNodes() || targetNode.textContent) {
createdNode.appendChild(targetNode);
}
targetNode = sibling;
}
}
// If we had an element we wanted to append at the end, do that now
if (appendLast) {
createdNode.appendChild(appendLast);
}
splitOnNode = currParent;
}
return createdNode;
},
moveTextRangeIntoElement: function (startNode, endNode, newElement) {
if (!startNode || !endNode) {
return false;
}
var rootNode = this.findCommonRoot(startNode, endNode);
if (!rootNode) {
return false;
}
if (endNode === startNode) {
var temp = startNode.parentNode,
sibling = startNode.nextSibling;
temp.removeChild(startNode);
newElement.appendChild(startNode);
if (sibling) {
temp.insertBefore(newElement, sibling);
} else {
temp.appendChild(newElement);
}
return newElement.hasChildNodes();
}
// create rootChildren array which includes all the children
// we care about
var rootChildren = [],
firstChild,
lastChild,
nextNode;
for (var i = 0; i < rootNode.childNodes.length; i++) {
nextNode = rootNode.childNodes[i];
if (!firstChild) {
if (Util.isDescendant(nextNode, startNode, true)) {
firstChild = nextNode;
}
} else {
if (this.isDescendant(nextNode, endNode, true)) {
lastChild = nextNode;
break;
} else {
rootChildren.push(nextNode);
}
}
}
var afterLast = lastChild.nextSibling,
fragment = document.createDocumentFragment();
// build up fragment on startNode side of tree
if (firstChild === startNode) {
firstChild.parentNode.removeChild(firstChild);
fragment.appendChild(firstChild);
} else {
fragment.appendChild(this.splitOffDOMTree(firstChild, startNode));
}
// add any elements between firstChild & lastChild
rootChildren.forEach(function (element) {
element.parentNode.removeChild(element);
fragment.appendChild(element);
});
// build up fragment on endNode side of the tree
if (lastChild === endNode) {
lastChild.parentNode.removeChild(lastChild);
fragment.appendChild(lastChild);
} else {
fragment.appendChild(this.splitOffDOMTree(lastChild, endNode, true));
}
// Add fragment into passed in element
newElement.appendChild(fragment);
if (lastChild.parentNode === rootNode) {
// If last child is in the root, insert newElement in front of it
rootNode.insertBefore(newElement, lastChild);
} else if (afterLast) {
// If last child was removed, but it had a sibling, insert in front of it
rootNode.insertBefore(newElement, afterLast);
} else {
// lastChild was removed and was the last actual element just append
rootNode.appendChild(newElement);
}
return newElement.hasChildNodes();
},
/* based on http://stackoverflow.com/a/6183069 */
depthOfNode: function (inNode) {
var theDepth = 0,
node = inNode;
while (node.parentNode !== null) {
node = node.parentNode;
theDepth++;
}
return theDepth;
},
findCommonRoot: function (inNode1, inNode2) {
var depth1 = this.depthOfNode(inNode1),
depth2 = this.depthOfNode(inNode2),
node1 = inNode1,
node2 = inNode2;
while (depth1 !== depth2) {
if (depth1 > depth2) {
node1 = node1.parentNode;
depth1 -= 1;
} else {
node2 = node2.parentNode;
depth2 -= 1;
}
}
while (node1 !== node2) {
node1 = node1.parentNode;
node2 = node2.parentNode;
}
return node1;
},
/* END - based on http://stackoverflow.com/a/6183069 */
ensureUrlHasProtocol: function (url) {
if (url.indexOf('://') === -1) {
return 'http://' + url;
}
return url;
},
warn: function () {
if (window.console !== undefined && typeof window.console.warn === 'function') {
window.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);
}
}, this);
},
getClosestTag: function (el, tag) { // get the closest parent
return Util.traverseUp(el, function (element) {
return element.tagName.toLowerCase() === tag.toLowerCase();
});
},
unwrap: function (el, doc) {
var fragment = doc.createDocumentFragment(),
nodes = Array.prototype.slice.call(el.childNodes);
// cast nodeList to array since appending child
// to a different node will alter length of el.childNodes
for (var i = 0; i < nodes.length; i++) {
fragment.appendChild(nodes[i]);
}
if (fragment.childNodes.length) {
el.parentNode.replaceChild(fragment, el);
} else {
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:
// ,