/*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'; 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, K: 75 // K keycode, and not k }, /** * 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; }, /** * Returns true if the key associated to the event is inside keys array * * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484 * @see : http://stackoverflow.com/q/4471582/569101 */ isKey: function (event, keys) { var keyCode = this.getKeyCode(event); // it's not an array let's just compare strings! if (false === Array.isArray(keys)) { return keyCode === keys; } if (-1 === keys.indexOf(keyCode)) { return false; } return true; }, getKeyCode: function (event) { var keyCode = event.which; // getting the key code from event if (null === keyCode) { keyCode = event.charCode !== null ? event.charCode : event.keyCode; } return keyCode; }, blockContainerElementNames: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'], emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'], 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); }, /* * Create a link around the provided text nodes which must be adjacent to each other and all be * descendants of the same closest block container. If the preconditions are not met, unexpected * behavior will result. */ createLink: function (document, textNodes, href, target) { var anchor = document.createElement('a'); Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor); anchor.setAttribute('href', href); if (target) { anchor.setAttribute('target', target); } return anchor; }, /* * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the * textContent of the provided element argument, modify the DOM inside element to ensure that the text * identified by the provided match can be returned as text nodes that contain exactly that text, without * any additional text at the beginning or end of the returned array of adjacent text nodes. * * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are * not affected in any way. */ findOrCreateMatchingTextNodes: function (document, element, match) { var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false), matchedNodes = [], currentTextIndex = 0, startReached = false, currentNode = null, newNode = null; while ((currentNode = treeWalker.nextNode()) !== null) { if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) { startReached = true; newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex); } if (startReached) { Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex); } if (startReached && currentTextIndex === match.end) { break; // Found the node(s) corresponding to the link. Break out and move on to the next. } else if (startReached && currentTextIndex > (match.end + 1)) { throw new Error('PerformLinking overshot the target!'); // should never happen... } if (startReached) { matchedNodes.push(newNode || currentNode); } currentTextIndex += currentNode.nodeValue.length; if (newNode !== null) { currentTextIndex += newNode.nodeValue.length; // Skip the newNode as we'll already have pushed it to the matches treeWalker.nextNode(); } newNode = null; } return matchedNodes; }, /* * Given the provided text node and text coordinates, split the text node if needed to make it align * precisely with the coordinates. * * This function is intended to be called from Util.findOrCreateMatchingTextNodes. */ splitStartNodeIfNeeded: function (currentNode, matchStartIndex, currentTextIndex) { if (matchStartIndex !== currentTextIndex) { return currentNode.splitText(matchStartIndex - currentTextIndex); } return null; }, /* * Given the provided text node and text coordinates, split the text node if needed to make it align * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded, * if that function has been called on the same currentNode. * * This function is intended to be called from Util.findOrCreateMatchingTextNodes. */ splitEndNodeIfNeeded: function (currentNode, newNode, matchEndIndex, currentTextIndex) { var textIndexOfEndOfFarthestNode, endSplitPoint; textIndexOfEndOfFarthestNode = currentTextIndex + (newNode || currentNode).nodeValue.length + (newNode ? currentNode.nodeValue.length : 0) - 1; endSplitPoint = (newNode || currentNode).nodeValue.length - (textIndexOfEndOfFarthestNode + 1 - matchEndIndex); if (textIndexOfEndOfFarthestNode >= matchEndIndex && currentTextIndex !== textIndexOfEndOfFarthestNode && endSplitPoint !== 0) { (newNode || currentNode).splitText(endSplitPoint); } }, // 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); }, // 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 (Util.isMediumEditorElement(current)) { 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 && !Util.isMediumEditorElement(toReplace.parentNode)) { 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); } } }, execFormatBlock: function (doc, tagName) { // Get the top level block element that contains the selection var blockContainer = Util.getTopBlockContainer(Selection.getSelectionStart(doc)); // Special handling for blockquote if (tagName === 'blockquote') { if (blockContainer) { var childNodes = Array.prototype.slice.call(blockContainer.childNodes); // Check if the blockquote has a block element as a child (nested blocks) if (childNodes.some(function (childNode) { return Util.isBlockContainer(childNode); })) { // 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 return doc.execCommand('outdent', false, null); } } // When IE blockquote needs to be called as indent // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777 if (this.isIE) { return doc.execCommand('indent', false, tagName); } } // If the blockContainer is already the element type being passed in // treat it as 'undo' formatting and just convert it to a

if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) { tagName = 'p'; } // When IE we need to add <> to heading elements // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie if (this.isIE) { 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.nodeName.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.nodeName.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.nodeName.toLowerCase() === 'li') { return true; } var parentNode = node.parentNode, tagName = parentNode.nodeName.toLowerCase(); while (!this.isBlockContainer(parentNode) && tagName !== 'div') { if (tagName === 'li') { return true; } parentNode = parentNode.parentNode; if (parentNode) { tagName = parentNode.nodeName.toLowerCase(); } else { return false; } } return false; }, cleanListDOM: function (ownerDocument, element) { if (element.nodeName.toLowerCase() !== 'li') { return; } var list = element.parentElement; if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up this.unwrap(list.parentElement, ownerDocument); // move cursor at the end of the text inside the list // for some unknown reason, the cursor is moved to end of the "visual" line Selection.moveCursor(ownerDocument, element.firstChild, element.firstChild.textContent.length); } }, /* 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 (this.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 = rootNode.ownerDocument.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 */ isElementAtBeginningOfBlock: function (node) { var textVal, sibling; while (!this.isBlockContainer(node) && !this.isMediumEditorElement(node)) { sibling = node; while (sibling = sibling.previousSibling) { textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent; if (textVal.length > 0) { return false; } } node = node.parentNode; } return true; }, isMediumEditorElement: function (element) { return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element'); }, getContainerEditorElement: function (element) { return Util.traverseUp(element, function (node) { return Util.isMediumEditorElement(node); }); }, isBlockContainer: function (element) { return element && element.nodeType !== 3 && this.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1; }, getClosestBlockContainer: function (node) { return Util.traverseUp(node, function (node) { return Util.isBlockContainer(node); }); }, getTopBlockContainer: function (element) { var topBlock = element; this.traverseUp(element, function (el) { if (Util.isBlockContainer(el)) { topBlock = el; } return false; }); return topBlock; }, getFirstSelectableLeafNode: function (element) { while (element && element.firstChild) { element = element.firstChild; } // We don't want to set the selection to an element that can't have children, this messes up Gecko. element = this.traverseUp(element, function (el) { return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1; }); // Selecting at the beginning of a table doesn't work in PhantomJS. if (element.nodeName.toLowerCase() === 'table') { var firstCell = element.querySelector('th, td'); if (firstCell) { element = firstCell; } } return element; }, getFirstTextNode: function (element) { if (element.nodeType === 3) { return element; } for (var i = 0; i < element.childNodes.length; i++) { var textNode = this.getFirstTextNode(element.childNodes[i]); if (textNode !== null) { return textNode; } } return null; }, 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(window.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.nodeName.toLowerCase() === tag) { el.parentNode.removeChild(el); } }, this); }, // get the closest parent getClosestTag: function (el, tag) { return this.traverseUp(el, function (element) { return element.nodeName.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); } } }; }(window)); var buttonDefaults; (function () { 'use strict'; buttonDefaults = { 'bold': { name: 'bold', action: 'bold', aria: 'bold', tagNames: ['b', 'strong'], style: { prop: 'font-weight', value: '700|bold' }, useQueryState: true, contentDefault: 'B', contentFA: '' }, 'italic': { name: 'italic', action: 'italic', aria: 'italic', tagNames: ['i', 'em'], style: { prop: 'font-style', value: 'italic' }, useQueryState: true, contentDefault: 'I', contentFA: '' }, 'underline': { name: 'underline', action: 'underline', aria: 'underline', tagNames: ['u'], style: { prop: 'text-decoration', value: 'underline' }, useQueryState: true, contentDefault: 'U', contentFA: '' }, '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: '' }, '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: '' }, '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: '' }, // Known inline elements that are not removed, or not removed consistantly across browsers: // ,