{I"
class:ETI"ProcessedAsset; FI"logical_path; TI"medium-editor.js; FI"
pathname; TI"W/Users/richardadams/github/type_station/vendor/assets/javascripts/medium-editor.js; FI"content_type; TI"application/javascript; TI"
mtime; Tl+L{TI"length; Ti]I"digest; TI"%0c9cbd06d19178f55ff463b9f2936d0d; FI"source; TI"]function MediumEditor(elements, options) {
'use strict';
return this.init(elements, options);
}
if (typeof module === 'object') {
module.exports = MediumEditor;
// AMD support
} else if (typeof define === 'function' && define.amd) {
define(function () {
'use strict';
return MediumEditor;
});
}
(function (window, document) {
'use strict';
function extend(b, a) {
var prop;
if (b === undefined) {
return a;
}
for (prop in a) {
if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
b[prop] = a[prop];
}
}
return b;
}
// https://github.com/jashkenas/underscore
var now = Date.now || function () {
return new Date().getTime();
};
// https://github.com/jashkenas/underscore
function throttle(func, wait) {
var THROTTLE_INTERVAL = 50,
context,
args,
result,
timeout = null,
previous = 0,
later;
if (!wait && wait !== 0) {
wait = THROTTLE_INTERVAL;
}
later = function () {
previous = now();
timeout = null;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
};
return function () {
var currNow = now(),
remaining = wait - (currNow - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
timeout = null;
previous = currNow;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
}
function isDescendant(parent, child) {
var node = child.parentNode;
while (node !== null) {
if (node === parent) {
return true;
}
node = node.parentNode;
}
return false;
}
// 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
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;
}
// http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element
// by Tim Down
function saveSelection() {
var i,
len,
ranges,
sel = this.options.contentWindow.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
ranges = [];
for (i = 0, len = sel.rangeCount; i < len; i += 1) {
ranges.push(sel.getRangeAt(i));
}
return ranges;
}
return null;
}
function restoreSelection(savedSel) {
var i,
len,
sel = this.options.contentWindow.getSelection();
if (savedSel) {
sel.removeAllRanges();
for (i = 0, len = savedSel.length; i < len; i += 1) {
sel.addRange(savedSel[i]);
}
}
}
// http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
// by You
function getSelectionStart() {
var node = this.options.ownerDocument.getSelection().anchorNode,
startNode = (node && node.nodeType === 3 ? node.parentNode : node);
return startNode;
}
// http://stackoverflow.com/questions/4176923/html-of-selected-text
// by Tim Down
function getSelectionHtml() {
var i,
html = '',
sel,
len,
container;
if (this.options.contentWindow.getSelection !== undefined) {
sel = this.options.contentWindow.getSelection();
if (sel.rangeCount) {
container = this.options.ownerDocument.createElement('div');
for (i = 0, len = sel.rangeCount; i < len; i += 1) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
} else if (this.options.ownerDocument.selection !== undefined) {
if (this.options.ownerDocument.selection.type === 'Text') {
html = this.options.ownerDocument.selection.createRange().htmlText;
}
}
return html;
}
/**
* Find the caret position within an element irrespective of any inline tags it may contain.
*
* @param {DOMElement} An element containing the cursor to find offsets relative to.
* @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
* @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
*/
function getCaretOffsets(element, range) {
var preCaretRange, postCaretRange;
if (!range) {
range = window.getSelection().getRangeAt(0);
}
preCaretRange = range.cloneRange();
postCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
postCaretRange.selectNodeContents(element);
postCaretRange.setStart(range.endContainer, range.endOffset);
return {
left: preCaretRange.toString().length,
right: postCaretRange.toString().length
};
}
// https://github.com/jashkenas/underscore
function isElement(obj) {
return !!(obj && obj.nodeType === 1);
}
// http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
function insertHTMLCommand(doc, html) {
var selection, range, el, fragment, node, lastNode;
if (doc.queryCommandSupported('insertHTML')) {
try {
return doc.execCommand('insertHTML', false, html);
} catch (ignore) {}
}
selection = window.getSelection();
if (selection.getRangeAt && selection.rangeCount) {
range = selection.getRangeAt(0);
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);
}
}
}
MediumEditor.prototype = {
defaults: {
allowMultiParagraphSelection: true,
anchorInputPlaceholder: 'Paste or type a link',
anchorInputCheckboxLabel: 'Open in new window',
anchorPreviewHideDelay: 500,
buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
buttonLabels: false,
checkLinkFormat: false,
cleanPastedHTML: false,
delay: 0,
diffLeft: 0,
diffTop: -10,
disableReturn: false,
disableDoubleReturn: false,
disableToolbar: false,
disableEditing: false,
disableAnchorForm: false,
disablePlaceholders: false,
elementsContainer: false,
standardizeSelectionStart: false,
contentWindow: window,
ownerDocument: document,
firstHeader: 'h3',
forcePlainText: true,
placeholder: 'Type your text',
secondHeader: 'h4',
targetBlank: false,
anchorTarget: false,
anchorButton: false,
anchorButtonClass: 'btn',
extensions: {},
activeButtonClass: 'medium-editor-button-active',
firstButtonClass: 'medium-editor-button-first',
lastButtonClass: 'medium-editor-button-last'
},
// 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))),
init: function (elements, options) {
var uniqueId = 1;
this.options = extend(options, this.defaults);
this.setElementSelection(elements);
if (this.elements.length === 0) {
return;
}
this.parentElements = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'];
if (!this.options.elementsContainer) {
this.options.elementsContainer = this.options.ownerDocument.body;
}
while (this.options.elementsContainer.querySelector('#medium-editor-toolbar-' + uniqueId)) {
uniqueId = uniqueId + 1;
}
this.id = uniqueId;
return this.setup();
},
setup: function () {
this.events = [];
this.isActive = true;
this.initThrottledMethods()
.initElements()
.bindSelect()
.bindPaste()
.setPlaceholders()
.bindElementActions()
.bindWindowActions();
//.passInstance();
},
on: function (target, event, listener, useCapture) {
target.addEventListener(event, listener, useCapture);
this.events.push([target, event, listener, useCapture]);
},
off: function (target, event, listener, useCapture) {
var index = this.indexOfListener(target, event, listener, useCapture),
e;
if (index !== -1) {
e = this.events.splice(index, 1)[0];
e[0].removeEventListener(e[1], e[2], e[3]);
}
},
indexOfListener: function (target, event, listener, useCapture) {
var i, n, item;
for (i = 0, n = this.events.length; i < n; i = i + 1) {
item = this.events[i];
if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
return i;
}
}
return -1;
},
delay: function (fn) {
var self = this;
setTimeout(function () {
if (self.isActive) {
fn();
}
}, this.options.delay);
},
removeAllEvents: function () {
var e = this.events.pop();
while (e) {
e[0].removeEventListener(e[1], e[2], e[3]);
e = this.events.pop();
}
},
initThrottledMethods: function () {
var self = this;
// handleResize 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.handleResize = throttle(function () {
if (self.isActive) {
self.positionToolbarIfShown();
}
});
// handleBlur is throttled because:
// - This method could be called many times due to the type of event handlers that are calling it
// - We want a slight delay so that other events in the stack can run, some of which may
// prevent the toolbar from being hidden (via this.keepToolbarAlive).
this.handleBlur = throttle(function () {
if (self.isActive && !self.keepToolbarAlive) {
self.hideToolbarActions();
}
});
return this;
},
initElements: function () {
var i,
addToolbar = false;
for (i = 0; i < this.elements.length; i += 1) {
if (!this.options.disableEditing && !this.elements[i].getAttribute('data-disable-editing')) {
this.elements[i].setAttribute('contentEditable', true);
}
if (!this.elements[i].getAttribute('data-placeholder')) {
this.elements[i].setAttribute('data-placeholder', this.options.placeholder);
}
this.elements[i].setAttribute('data-medium-element', true);
this.elements[i].setAttribute('role', 'textbox');
this.elements[i].setAttribute('aria-multiline', true);
this.bindParagraphCreation(i);
if (!this.options.disableToolbar && !this.elements[i].getAttribute('data-disable-toolbar')) {
addToolbar = true;
}
}
// Init toolbar
if (addToolbar) {
this.passInstance()
.callExtensions('init')
.initToolbar()
.bindButtons()
.bindAnchorForm()
.bindAnchorPreview();
}
return this;
},
setElementSelection: function (selector) {
if (!selector) {
selector = [];
}
// If string, use as query selector
if (typeof selector === 'string') {
selector = this.options.ownerDocument.querySelectorAll(selector);
}
// If element, put into array
if (isElement(selector)) {
selector = [selector];
}
// Convert NodeList (or other array like object) into an array
this.elements = Array.prototype.slice.apply(selector);
},
bindBlur: function () {
var self = this,
blurFunction = function (e) {
var isDescendantOfEditorElements = false,
i;
for (i = 0; i < self.elements.length; i += 1) {
if (isDescendant(self.elements[i], e.target)) {
isDescendantOfEditorElements = true;
break;
}
}
// If it's not part of the editor, or the toolbar
if (e.target !== self.toolbar
&& self.elements.indexOf(e.target) === -1
&& !isDescendantOfEditorElements
&& !isDescendant(self.toolbar, e.target)
&& !isDescendant(self.anchorPreview, e.target)) {
// Activate the placeholder
if (!self.options.disablePlaceholders) {
self.placeholderWrapper(e, self.elements[0]);
}
// Hide the toolbar after a small delay so we can prevent this on toolbar click
self.handleBlur();
}
};
// Hide the toolbar when focusing outside of the editor.
this.on(this.options.ownerDocument.body, 'click', blurFunction, true);
this.on(this.options.ownerDocument.body, 'focus', blurFunction, true);
return this;
},
bindClick: function (i) {
var self = this;
this.on(this.elements[i], 'click', function () {
if (!self.options.disablePlaceholders) {
// Remove placeholder
this.classList.remove('medium-editor-placeholder');
}
if (self.options.staticToolbar) {
self.setToolbarPosition();
}
});
return this;
},
/**
* This handles blur and keypress events on elements
* Including Placeholders, and tooldbar hiding on blur
*/
bindElementActions: function () {
var i;
for (i = 0; i < this.elements.length; i += 1) {
if (!this.options.disablePlaceholders) {
// Active all of the placeholders
this.activatePlaceholder(this.elements[i]);
}
// Bind the return and tab keypress events
this.bindReturn(i)
.bindKeydown(i)
.bindBlur()
.bindClick(i);
}
return this;
},
// Two functions to handle placeholders
activatePlaceholder: function (el) {
if (!(el.querySelector('img')) &&
!(el.querySelector('blockquote')) &&
el.textContent.replace(/^\s+|\s+$/g, '') === '') {
el.classList.add('medium-editor-placeholder');
}
},
placeholderWrapper: function (evt, el) {
el = el || evt.target;
el.classList.remove('medium-editor-placeholder');
if (evt.type !== 'keypress') {
this.activatePlaceholder(el);
}
},
serialize: function () {
var i,
elementid,
content = {};
for (i = 0; i < this.elements.length; i += 1) {
elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
content[elementid] = {
value: this.elements[i].innerHTML.trim()
};
}
return content;
},
/**
* Helper function to call a method with a number of parameters on all registered extensions.
* The function assures that the function exists before calling.
*
* @param {string} funcName name of the function to call
* @param [args] arguments passed into funcName
*/
callExtensions: function (funcName) {
if (arguments.length < 1) {
return;
}
var args = Array.prototype.slice.call(arguments, 1),
ext,
name;
for (name in this.options.extensions) {
if (this.options.extensions.hasOwnProperty(name)) {
ext = this.options.extensions[name];
if (ext[funcName] !== undefined) {
ext[funcName].apply(ext, args);
}
}
}
return this;
},
/**
* Pass current Medium Editor instance to all extensions
* if extension constructor has 'parent' attribute set to 'true'
*
*/
passInstance: function () {
var self = this,
ext,
name;
for (name in self.options.extensions) {
if (self.options.extensions.hasOwnProperty(name)) {
ext = self.options.extensions[name];
if (ext.parent) {
ext.base = self;
}
}
}
return self;
},
bindParagraphCreation: function (index) {
var self = this;
this.on(this.elements[index], 'keypress', function (e) {
var node,
tagName;
if (e.which === 32) {
node = getSelectionStart.call(self);
tagName = node.tagName.toLowerCase();
if (tagName === 'a') {
self.options.ownerDocument.execCommand('unlink', false, null);
}
}
});
this.on(this.elements[index], 'keyup', function (e) {
var node = getSelectionStart.call(self),
tagName,
editorElement;
if (node && node.getAttribute('data-medium-element') && node.children.length === 0 && !(self.options.disableReturn || node.getAttribute('data-disable-return'))) {
self.options.ownerDocument.execCommand('formatBlock', false, 'p');
}
if (e.which === 13) {
node = getSelectionStart.call(self);
tagName = node.tagName.toLowerCase();
editorElement = self.getSelectionElement();
if (!(self.options.disableReturn || editorElement.getAttribute('data-disable-return')) &&
tagName !== 'li' && !self.isListItemChild(node)) {
if (!e.shiftKey) {
// paragraph creation should not be forced within a header tag
if (!/h\d/.test(tagName)) {
self.options.ownerDocument.execCommand('formatBlock', false, 'p');
}
}
if (tagName === 'a') {
self.options.ownerDocument.execCommand('unlink', false, null);
}
}
}
});
return this;
},
isListItemChild: function (node) {
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;
},
bindReturn: function (index) {
var self = this;
this.on(this.elements[index], 'keypress', function (e) {
if (e.which === 13) {
if (self.options.disableReturn || this.getAttribute('data-disable-return')) {
e.preventDefault();
} else if (self.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
var node = getSelectionStart.call(self);
if (node && node.textContent === '\n') {
e.preventDefault();
}
}
}
});
return this;
},
bindKeydown: function (index) {
var self = this;
this.on(this.elements[index], 'keydown', function (e) {
if (e.which === 9) {
// Override tab only for pre nodes
var tag = getSelectionStart.call(self).tagName.toLowerCase();
if (tag === 'pre') {
e.preventDefault();
self.options.ownerDocument.execCommand('insertHtml', null, ' ');
}
// Tab to indent list structures!
if (tag === 'li') {
e.preventDefault();
// If Shift is down, outdent, otherwise indent
if (e.shiftKey) {
self.options.ownerDocument.execCommand('outdent', e);
} else {
self.options.ownerDocument.execCommand('indent', e);
}
}
} else if (e.which === 8 || e.which === 46 || e.which === 13) {
// Bind keys which can create or destroy a block element: backspace, delete, return
self.onBlockModifier(e);
}
});
return this;
},
onBlockModifier: function (e) {
var range, sel, p, node = getSelectionStart.call(this),
tagName = node.tagName.toLowerCase(),
isEmpty = /^(\s+|
)?$/i,
isHeader = /h\d/i;
// backspace or return
if ((e.which === 8 || e.which === 13)
&& node.previousElementSibling
// in a header
&& isHeader.test(tagName)
// at the very end of the block
&& getCaretOffsets(node).left === 0) {
if (e.which === 8 && isEmpty.test(node.previousElementSibling.innerHTML)) {
// backspacing the begining of a header into an empty previous element will
// change the tagName of the current node to prevent one
// instead delete previous node and cancel the event.
node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
e.preventDefault();
} else if (e.which === 13) {
// hitting return in the begining of a header will create empty header elements before the current one
// instead, make "
[CURSOR]
' + self.htmlEntities(paragraphs[p]) + '
'; } } insertHTMLCommand(self.options.ownerDocument, html); } else { html = self.htmlEntities(e.clipboardData.getData(dataFormatPlain)); insertHTMLCommand(self.options.ownerDocument, html); } } }; for (i = 0; i < this.elements.length; i += 1) { this.on(this.elements[i], 'paste', this.pasteWrapper); } return this; }, setPlaceholders: function () { if (!this.options.disablePlaceholders && this.elements && this.elements.length) { this.elements.forEach(function (el) { this.activatePlaceholder(el); this.on(el, 'blur', this.placeholderWrapper.bind(this)); this.on(el, 'keypress', this.placeholderWrapper.bind(this)); }.bind(this)); } return this; }, cleanPaste: function (text) { /*jslint regexp: true*/ /* jslint does not allow character negation, because the negation will not match any unicode characters. In the regexes in this block, negation is used specifically to match the end of an html tag, and in fact unicode characters *should* be allowed. */ var i, elList, workEl, el = this.getSelectionElement(), multiline = /]*docs-internal-guid[^>]*>/gi), ""],
[new RegExp(/<\/b>( ' + elList.join(' ') + '
]*>)?$/gi), ""],
// un-html spaces and newlines inserted by OS X
[new RegExp(/\s+<\/span>/g), ' '],
[new RegExp(/
/g), '
'],
// replace google docs italics+bold with a span to be replaced once the html is inserted
[new RegExp(/]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), ''],
// replace google docs italics with a span to be replaced once the html is inserted
[new RegExp(/]*font-style:italic[^>]*>/gi), ''],
//[replace google docs bolds with a span to be replaced once the html is inserted
[new RegExp(/]*font-weight:bold[^>]*>/gi), ''],
// replace manually entered b/i/a tags with real ones
[new RegExp(/<(\/?)(i|b|a)>/gi), '<$1$2>'],
// replace manually a tags with real ones, converting smart-quotes from google docs
[new RegExp(/<a\s+href=("|”|“|“|”)([^&]+)("|”|“|“|”)>/gi), '']
];
/*jslint regexp: false*/
for (i = 0; i < replacements.length; i += 1) {
text = text.replace(replacements[i][0], replacements[i][1]);
}
if (multiline) {
// double br's aren't converted to p tags, but we want paragraphs.
elList = text.split('
');
this.pasteHTML('