{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+Λ·TI"length; Til+I"digest; TI"%ea2a38de46e65ed7e195f5dd61580e8f; FI"source; TI"l+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 debounce(func, wait) {
var DEBOUNCE_INTERVAL = 50,
timeout,
args,
context,
timestamp,
result,
later;
if (!wait && wait !== 0) {
wait = DEBOUNCE_INTERVAL;
}
later = function() {
var last = now() - timestamp;
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = now();
if (!timeout) {
timeout = setTimeout(later, wait);
}
return result;
};
}
function isDescendant(parent, child) {
var node = child.parentNode;
while (node !== null) {
if (node === parent) {
return true;
}
node = node.parentNode;
}
return false;
}
// 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;
}
// 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')) {
return doc.execCommand('insertHTML', false, html);
}
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',
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,
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 = document.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.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.events.indexOf([target, event, listener, useCapture]),
e;
if(index !== -1) {
e = this.events.splice(index, 1);
e[0].removeEventListener(e[1], e[2], e[3]);
}
},
removeAllEvents: function() {
var e = this.events.pop();
while(e) {
e[0].removeEventListener(e[1], e[2], e[3]);
e = this.events.pop();
}
},
initElements: function () {
this.updateElementList();
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.bindParagraphCreation(i);
if (!this.options.disableToolbar && !this.elements[i].getAttribute('data-disable-toolbar')) {
addToolbar = true;
}
}
// Init toolbar
if (addToolbar) {
this.initToolbar()
.bindButtons()
.bindAnchorForm()
.bindAnchorPreview();
}
return this;
},
setElementSelection: function (selector) {
this.elementSelection = selector;
this.updateElementList();
},
updateElementList: function () {
this.elements = typeof this.elementSelection === 'string' ? this.options.ownerDocument.querySelectorAll(this.elementSelection) : this.elementSelection;
if (this.elements.nodeType === 1) {
this.elements = [this.elements];
}
},
// handleBlur is debounced 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).
handleBlur: debounce(function() {
if ( !this.keepToolbarAlive ) {
// this.hideToolbarActions();
}
}),
bindBlur: function(i) {
var self = this,
blurFunction = function(e){
// If it's not part of the editor, or the toolbar
if ( e.target !== self.toolbar
&& e.target !== self.elements[0]
&& !isDescendant(self.elements[0], e.target)
&& !isDescendant(self.toolbar, e.target)
&& !isDescendant(self.anchorPreview, e.target)) {
// Activate the placeholder
if (!self.options.disablePlaceholders) {
self.placeholderWrapper(self.elements[0], e);
}
// 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(document.body, 'click', blurFunction, true);
this.on(document.body, 'focus', blurFunction, true);
return this;
},
bindKeypress: function(i) {
if (this.options.disablePlaceholders) {
return this;
}
var self = this;
// Set up the keypress events
this.on(this.elements[i], 'keypress', function(event){
self.placeholderWrapper(this,event);
});
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)
.bindTab(i)
.bindBlur(i)
.bindClick(i)
.bindKeypress(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 (el, e) {
el.classList.remove('medium-editor-placeholder');
if (e.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);
}
}
}
},
/**
* 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') {
document.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'))) {
document.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) {
document.execCommand('formatBlock', false, 'p');
}
if (tagName === 'a') {
document.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;
},
bindTab: 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();
document.execCommand('insertHtml', null, ' ');
}
// Tab to indent list structures!
if (tag === 'li') {
e.preventDefault();
// If Shift is down, outdent, otherwise indent
if (e.shiftKey) {
document.execCommand('outdent', e);
} else {
document.execCommand('indent', e);
}
}
}
});
return this;
},
buttonTemplate: function (btnType) {
var buttonLabels = this.getButtonLabels(this.options.buttonLabels),
buttonTemplates = {
'bold': '',
'italic': '',
'underline': '',
'strikethrough': '',
'superscript': '',
'subscript': '',
'anchor': '',
'image': '',
'header1': '',
'header2': '',
'quote': '',
'orderedlist': '',
'unorderedlist': '',
'pre': '',
'indent': '',
'outdent': '',
'justifyCenter': '',
'justifyFull': '',
'justifyLeft': '',
'justifyRight': ''
};
return buttonTemplates[btnType] || false;
},
// TODO: break method
getButtonLabels: function (buttonLabelType) {
var customButtonLabels,
attrname,
buttonLabels = {
'bold': 'B',
'italic': 'I',
'underline': 'U',
'strikethrough': 'A',
'superscript': 'x1',
'subscript': 'x1',
'anchor': '#',
'image': 'image',
'header1': 'H1',
'header2': 'H2',
'quote': '“',
'orderedlist': '1.',
'unorderedlist': '•',
'pre': '0101',
'indent': '→',
'outdent': '←',
'justifyCenter': 'C',
'justifyFull': 'J',
'justifyLeft': 'L',
'justifyRight': 'R'
};
if (buttonLabelType === 'fontawesome') {
customButtonLabels = {
'bold': '',
'italic': '',
'underline': '',
'strikethrough': '',
'superscript': '',
'subscript': '',
'anchor': '',
'image': '',
'quote': '',
'orderedlist': '',
'unorderedlist': '',
'pre': '',
'indent': '',
'outdent': '',
'justifyCenter': '',
'justifyFull': '',
'justifyLeft': '',
'justifyRight': ''
};
} else if (typeof buttonLabelType === 'object') {
customButtonLabels = buttonLabelType;
}
if (typeof customButtonLabels === 'object') {
for (attrname in customButtonLabels) {
if (customButtonLabels.hasOwnProperty(attrname)) {
buttonLabels[attrname] = customButtonLabels[attrname];
}
}
}
return buttonLabels;
},
initToolbar: function () {
if (this.toolbar) {
return this;
}
this.toolbar = this.createToolbar();
this.keepToolbarAlive = false;
this.toolbarActions = this.toolbar.querySelector('.medium-editor-toolbar-actions');
this.anchorPreview = this.createAnchorPreview();
if (!this.options.disableAnchorForm) {
this.anchorForm = this.toolbar.querySelector('.medium-editor-toolbar-form-anchor');
this.anchorInput = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-input');
this.anchorTarget = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-target');
this.anchorButton = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-button');
}
return this;
},
createToolbar: function () {
var toolbar = document.createElement('div');
toolbar.id = 'medium-editor-toolbar-' + this.id;
toolbar.className = 'medium-editor-toolbar';
if ( this.options.staticToolbar ) {
toolbar.className += " static-toolbar";
} else {
toolbar.className += " stalker-toolbar";
}
toolbar.appendChild(this.toolbarButtons());
if (!this.options.disableAnchorForm) {
toolbar.appendChild(this.toolbarFormAnchor());
}
this.options.elementsContainer.appendChild(toolbar);
return toolbar;
},
//TODO: actionTemplate
toolbarButtons: function () {
var btns = this.options.buttons,
ul = document.createElement('ul'),
li,
i,
btn,
ext;
ul.id = 'medium-editor-toolbar-actions' + this.id;
ul.className = 'medium-editor-toolbar-actions clearfix';
for (i = 0; i < btns.length; i += 1) {
if (this.options.extensions.hasOwnProperty(btns[i])) {
ext = this.options.extensions[btns[i]];
btn = ext.getButton !== undefined ? ext.getButton() : null;
} else {
btn = this.buttonTemplate(btns[i]);
}
if (btn) {
li = document.createElement('li');
if (isElement(btn)) {
li.appendChild(btn);
} else {
li.innerHTML = btn;
}
ul.appendChild(li);
}
}
return ul;
},
toolbarFormAnchor: function () {
var anchor = document.createElement('div'),
input = document.createElement('input'),
target_label = document.createElement('label'),
target = document.createElement('input'),
button_label = document.createElement('label'),
button = document.createElement('input'),
close = document.createElement('a'),
save = document.createElement('a');
close.setAttribute('href', '#');
close.className = 'medium-editor-toobar-anchor-close';
close.innerHTML = '×';
save.setAttribute('href', '#');
save.className = 'medium-editor-toobar-anchor-save';
save.innerHTML = '✓';
input.setAttribute('type', 'text');
input.className = 'medium-editor-toolbar-anchor-input';
input.setAttribute('placeholder', this.options.anchorInputPlaceholder);
target.setAttribute('type', 'checkbox');
target.className = 'medium-editor-toolbar-anchor-target';
target_label.innerHTML = "Open in New Window?";
target_label.insertBefore(target, target_label.firstChild);
button.setAttribute('type', 'checkbox');
button.className = 'medium-editor-toolbar-anchor-button';
button_label.innerHTML = "Button";
button_label.insertBefore(button, button_label.firstChild);
anchor.className = 'medium-editor-toolbar-form-anchor';
anchor.id = 'medium-editor-toolbar-form-anchor-' + this.id;
anchor.appendChild(input);
anchor.appendChild(save);
anchor.appendChild(close);
if (this.options.anchorTarget) {
anchor.appendChild(target_label);
}
if (this.options.anchorButton) {
anchor.appendChild(button_label);
}
return anchor;
},
bindSelect: function () {
var self = this,
i;
this.checkSelectionWrapper = function (e) {
// Do not close the toolbar when bluring the editable area and clicking into the anchor form
if (!self.options.disableAnchorForm && e && self.clickingIntoArchorForm(e)) {
return false;
}
self.checkSelection();
};
this.on(document.documentElement, 'mouseup', this.checkSelectionWrapper);
for (i = 0; i < this.elements.length; i += 1) {
this.on(this.elements[i], 'keyup', this.checkSelectionWrapper);
this.on(this.elements[i], 'blur', this.checkSelectionWrapper);
this.on(this.elements[i], 'click', this.checkSelectionWrapper);
}
return this;
},
checkSelection: function () {
var newSelection,
selectionElement;
if (this.keepToolbarAlive !== true && !this.options.disableToolbar) {
newSelection = this.options.contentWindow.getSelection();
if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') ||
(this.options.allowMultiParagraphSelection === false && this.hasMultiParagraphs()) ||
this.selectionInContentEditableFalse()) {
if ( !this.options.staticToolbar ) {
this.hideToolbarActions();
} else if (this.anchorForm && this.anchorForm.style.display === 'block') {
this.setToolbarButtonStates();
this.showToolbarActions();
}
} else {
selectionElement = this.getSelectionElement();
if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
if ( !this.options.staticToolbar ) {
this.hideToolbarActions();
}
} else {
this.checkSelectionElement(newSelection, selectionElement);
}
}
}
return this;
},
clickingIntoArchorForm: function (e) {
var self = this;
if (e.type && e.type.toLowerCase() === 'blur' && e.relatedTarget && e.relatedTarget === self.anchorInput) {
return true;
}
return false;
},
hasMultiParagraphs: function () {
var selectionHtml = getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
hasMultiParagraphs = selectionHtml.match(/<(p|h[0-6]|blockquote)>([\s\S]*?)<\/(p|h[0-6]|blockquote)>/g);
return (hasMultiParagraphs ? hasMultiParagraphs.length : 0);
},
checkSelectionElement: function (newSelection, selectionElement) {
var i;
this.selection = newSelection;
this.selectionRange = this.selection.getRangeAt(0);
for (i = 0; i < this.elements.length; i += 1) {
if (this.elements[i] === selectionElement) {
this.setToolbarButtonStates()
.setToolbarPosition()
.showToolbarActions();
return;
}
}
if ( !this.options.staticToolbar ) {
this.hideToolbarActions();
}
},
findMatchingSelectionParent: function(testElementFunction) {
var selection = this.options.contentWindow.getSelection(), range, current;
if (selection.rangeCount === 0) {
return false;
}
range = selection.getRangeAt(0);
current = range.commonAncestorContainer;
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;
},
getSelectionElement: function () {
return this.findMatchingSelectionParent(function(el) {
return el.getAttribute('data-medium-element');
});
},
selectionInContentEditableFalse: function () {
return this.findMatchingSelectionParent(function(el) {
return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
});
},
setToolbarPosition: function () {
// document.documentElement for IE 9
var scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop,
container = this.elements[0],
containerRect = container.getBoundingClientRect(),
containerTop = containerRect.top + scrollTop,
buttonHeight = 50,
selection = window.getSelection(),
range,
boundary,
middleBoundary,
defaultLeft = (this.options.diffLeft) - (this.toolbar.offsetWidth / 2),
halfOffsetWidth = this.toolbar.offsetWidth / 2;
if ( selection.focusNode === null ) {
return this;
}
this.showToolbar();
if ( this.options.staticToolbar ) {
if ( this.options.stickyToolbar ) {
// If it's beyond the height of the editor, position it at the bottom of the editor
if ( scrollTop > (containerTop + this.elements[0].offsetHeight - this.toolbar.offsetHeight)) {
this.toolbar.style.top = (containerTop + this.elements[0].offsetHeight) + 'px';
}
// Stick the toolbar to the top of the window
else if ( scrollTop > (containerTop - this.toolbar.offsetHeight) ) {
this.toolbar.classList.add('sticky-toolbar');
this.toolbar.style.top = "0px";
}
// Normal static toolbar position
else {
this.toolbar.classList.remove('sticky-toolbar');
this.toolbar.style.top = containerTop - this.toolbar.offsetHeight + "px";
}
} else {
this.toolbar.style.top = containerTop - this.toolbar.offsetHeight + "px";
}
this.toolbar.style.left = containerRect.left + "px";
} else if (!selection.isCollapsed) {
range = selection.getRangeAt(0);
boundary = range.getBoundingClientRect();
middleBoundary = (boundary.left + boundary.right) / 2;
if (boundary.top < buttonHeight) {
this.toolbar.classList.add('medium-toolbar-arrow-over');
this.toolbar.classList.remove('medium-toolbar-arrow-under');
this.toolbar.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - this.toolbar.offsetHeight + 'px';
} else {
this.toolbar.classList.add('medium-toolbar-arrow-under');
this.toolbar.classList.remove('medium-toolbar-arrow-over');
this.toolbar.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - this.toolbar.offsetHeight + 'px';
}
if (middleBoundary < halfOffsetWidth) {
this.toolbar.style.left = defaultLeft + halfOffsetWidth + 'px';
} else if ((this.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
this.toolbar.style.left = this.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
} else {
this.toolbar.style.left = defaultLeft + middleBoundary + 'px';
}
}
this.hideAnchorPreview();
return this;
},
setToolbarButtonStates: function () {
var buttons = this.toolbarActions.querySelectorAll('button'),
i;
for (i = 0; i < buttons.length; i += 1) {
buttons[i].classList.remove(this.options.activeButtonClass);
}
this.checkActiveButtons();
return this;
},
checkActiveButtons: function () {
var elements = Array.prototype.slice.call(this.elements),
parentNode = this.getSelectedParentElement();
while (parentNode.tagName !== undefined && this.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
this.activateButton(parentNode.tagName.toLowerCase());
this.callExtensions('checkState', parentNode);
// we can abort the search upwards if we leave the contentEditable element
if (elements.indexOf(parentNode) !== -1) {
break;
}
parentNode = parentNode.parentNode;
}
},
activateButton: function (tag) {
var el = this.toolbar.querySelector('[data-element="' + tag + '"]');
if (el !== null && el.className.indexOf(this.options.activeButtonClass) === -1) {
el.className += ' ' + this.options.activeButtonClass;
}
},
bindButtons: function () {
var buttons = this.toolbar.querySelectorAll('button'),
i,
self = this,
triggerAction = function (e) {
e.preventDefault();
e.stopPropagation();
if (self.selection === undefined) {
self.checkSelection();
}
if (this.className.indexOf(self.options.activeButtonClass) > -1) {
this.classList.remove(self.options.activeButtonClass);
} else {
this.className += ' ' + self.options.activeButtonClass;
}
if (this.hasAttribute('data-action')) {
self.execAction(this.getAttribute('data-action'), e);
}
};
for (i = 0; i < buttons.length; i += 1) {
this.on(buttons[i], 'click', triggerAction);
}
this.setFirstAndLastItems(buttons);
return this;
},
setFirstAndLastItems: function (buttons) {
if (buttons.length > 0) {
buttons[0].className += ' ' + this.options.firstButtonClass;
buttons[buttons.length - 1].className += ' ' + this.options.lastButtonClass;
}
return this;
},
execAction: function (action, e) {
if (action.indexOf('append-') > -1) {
this.execFormatBlock(action.replace('append-', ''));
this.setToolbarPosition();
this.setToolbarButtonStates();
} else if (action === 'anchor') {
if (!this.options.disableAnchorForm) {
this.triggerAnchorAction(e);
}
} else if (action === 'image') {
this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
} else {
this.options.ownerDocument.execCommand(action, false, null);
this.setToolbarPosition();
}
},
// http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
rangeSelectsSingleNode: function (range) {
var startNode = range.startContainer;
return startNode === range.endContainer &&
startNode.hasChildNodes() &&
range.endOffset === range.startOffset + 1;
},
getSelectedParentElement: function () {
var selectedParentElement = null,
range = this.selectionRange;
if (this.rangeSelectsSingleNode(range)) {
selectedParentElement = range.startContainer.childNodes[range.startOffset];
} else if (range.startContainer.nodeType === 3) {
selectedParentElement = range.startContainer.parentNode;
} else {
selectedParentElement = range.startContainer;
}
return selectedParentElement;
},
triggerAnchorAction: function () {
var selectedParentElement = this.getSelectedParentElement();
if (selectedParentElement.tagName &&
selectedParentElement.tagName.toLowerCase() === 'a') {
this.options.ownerDocument.execCommand('unlink', false, null);
} else if (this.anchorForm) {
if (this.anchorForm.style.display === 'block') {
this.showToolbarActions();
} else {
this.showAnchorForm();
}
}
return this;
},
execFormatBlock: function (el) {
var selectionData = this.getSelectionData(this.selection.anchorNode);
// 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 (el === 'blockquote' && selectionData.el &&
selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') {
return this.options.ownerDocument.execCommand('outdent', false, null);
}
if (selectionData.tagName === el) {
el = '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 (el === 'blockquote') {
return this.options.ownerDocument.execCommand('indent', false, el);
}
el = '<' + el + '>';
}
return this.options.ownerDocument.execCommand('formatBlock', false, el);
},
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
};
},
getFirstChild: function (el) {
var firstChild = el.firstChild;
while (firstChild !== null && firstChild.nodeType !== 1) {
firstChild = firstChild.nextSibling;
}
return firstChild;
},
isToolbarShown: function() {
return this.toolbar && this.toolbar.classList.contains('medium-editor-toolbar-active');
},
showToolbar: function() {
if (this.toolbar && !this.isToolbarShown()) {
this.toolbar.classList.add('medium-editor-toolbar-active');
}
},
hideToolbar: function() {
if (this.isToolbarShown()) {
this.toolbar.classList.remove('medium-editor-toolbar-active');
if (this.onHideToolbar) {
this.onHideToolbar();
}
}
},
hideToolbarActions: function () {
this.keepToolbarAlive = false;
this.hideToolbar();
},
showToolbarActions: function () {
var self = this;
if (this.anchorForm) {
this.anchorForm.style.display = 'none';
}
this.toolbarActions.style.display = 'block';
this.keepToolbarAlive = false;
// Using setTimeout + options.delay because:
// We will actually be displaying the toolbar, which should be controlled by options.delay
setTimeout(function () {
self.showToolbar();
}, this.options.delay);
},
saveSelection: function() {
this.savedSelection = saveSelection.call(this);
},
restoreSelection: function() {
restoreSelection.call(this, this.savedSelection);
},
showAnchorForm: function (link_value) {
if (!this.anchorForm) {
return;
}
this.toolbarActions.style.display = 'none';
this.saveSelection();
this.anchorForm.style.display = 'block';
this.setToolbarPosition();
this.keepToolbarAlive = true;
this.anchorInput.focus();
this.anchorInput.value = link_value || '';
},
bindAnchorForm: function () {
if (!this.anchorForm) {
return this;
}
var linkCancel = this.anchorForm.querySelector('a.medium-editor-toobar-anchor-close'),
linkSave = this.anchorForm.querySelector('a.medium-editor-toobar-anchor-save'),
self = this;
this.on(this.anchorForm, 'click', function (e) {
e.stopPropagation();
self.keepToolbarAlive = true;
});
this.on(this.anchorInput, 'keyup', function (e) {
var button = null,
target;
if (e.keyCode === 13) {
e.preventDefault();
if (self.options.anchorTarget && self.anchorTarget.checked) {
target = "_blank";
}
else {
target = "_self";
}
if (self.options.anchorButton && self.anchorButton.checked) {
button = self.options.anchorButtonClass;
}
self.createLink(this, target, button);
}
else if (e.keyCode === 27) {
e.preventDefault();
self.showToolbarActions();
restoreSelection.call(self, self.savedSelection);
}
});
this.on(linkSave, 'click', function(e) {
var button = null,
target;
e.preventDefault();
if ( self.options.anchorTarget && self.anchorTarget.checked) {
target = "_blank";
}
else {
target = "_self";
}
if (self.options.anchorButton && self.anchorButton.checked) {
button = self.options.anchorButtonClass;
}
self.createLink(self.anchorInput, target, button);
}, true);
this.on(this.anchorInput, 'click', function (e) {
// make sure not to hide form when cliking into the input
e.stopPropagation();
self.keepToolbarAlive = true;
});
// Hide the anchor form when focusing outside of it.
this.on(this.options.ownerDocument.body, 'click', function (e) {
if (e.target !== self.anchorForm && !isDescendant(self.anchorForm, e.target) && !isDescendant(self.toolbarActions, e.target)) {
self.keepToolbarAlive = false;
self.checkSelection();
}
}, true);
this.on(this.options.ownerDocument.body, 'focus', function (e) {
if (e.target !== self.anchorForm && !isDescendant(self.anchorForm, e.target) && !isDescendant(self.toolbarActions, e.target)) {
self.keepToolbarAlive = false;
self.checkSelection();
}
}, true);
this.on(linkCancel, 'click', function (e) {
e.preventDefault();
self.showToolbarActions();
restoreSelection.call(self, self.savedSelection);
});
return this;
},
hideAnchorPreview: function () {
this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
},
// TODO: break method
showAnchorPreview: function (anchorEl) {
if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')
|| anchorEl.getAttribute('data-disable-preview')) {
return true;
}
var self = this,
buttonHeight = 40,
boundary = anchorEl.getBoundingClientRect(),
middleBoundary = (boundary.left + boundary.right) / 2,
halfOffsetWidth,
defaultLeft;
self.anchorPreview.querySelector('i').textContent = anchorEl.href;
halfOffsetWidth = self.anchorPreview.offsetWidth / 2;
defaultLeft = self.options.diffLeft - halfOffsetWidth;
self.observeAnchorPreview(anchorEl);
self.anchorPreview.classList.add('medium-toolbar-arrow-over');
self.anchorPreview.classList.remove('medium-toolbar-arrow-under');
self.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - self.options.diffTop + this.options.contentWindow.pageYOffset - self.anchorPreview.offsetHeight) + 'px';
if (middleBoundary < halfOffsetWidth) {
self.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
} else if ((this.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
self.anchorPreview.style.left = this.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
} else {
self.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
}
if (this.anchorPreview && !this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
}
return this;
},
// TODO: break method
observeAnchorPreview: function (anchorEl) {
var self = this,
lastOver = (new Date()).getTime(),
over = true,
stamp = function () {
lastOver = (new Date()).getTime();
over = true;
},
unstamp = function (e) {
if (!e.relatedTarget || !/anchor-preview/.test(e.relatedTarget.className)) {
over = false;
}
},
interval_timer = setInterval(function () {
if (over) {
return true;
}
var durr = (new Date()).getTime() - lastOver;
if (durr > self.options.anchorPreviewHideDelay) {
// hide the preview 1/2 second after mouse leaves the link
self.hideAnchorPreview();
// cleanup
clearInterval(interval_timer);
self.off(self.anchorPreview, 'mouseover', stamp);
self.off(self.anchorPreview, 'mouseout', unstamp);
self.off(anchorEl, 'mouseover', stamp);
self.off(anchorEl, 'mouseout', unstamp);
}
}, 200);
this.on(self.anchorPreview, 'mouseover', stamp);
this.on(self.anchorPreview, 'mouseout', unstamp);
this.on(anchorEl, 'mouseover', stamp);
this.on(anchorEl, 'mouseout', unstamp);
},
createAnchorPreview: function () {
var self = this,
anchorPreview = this.options.ownerDocument.createElement('div');
anchorPreview.id = 'medium-editor-anchor-preview-' + this.id;
anchorPreview.className = 'medium-editor-anchor-preview';
anchorPreview.innerHTML = this.anchorPreviewTemplate();
this.options.elementsContainer.appendChild(anchorPreview);
this.on(anchorPreview, 'click', function () {
self.anchorPreviewClickHandler();
});
return anchorPreview;
},
anchorPreviewTemplate: function () {
return '
' + 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) { return this; } var i, activatePlaceholder = function (el) { if (!(el.querySelector('img')) && !(el.querySelector('blockquote')) && el.textContent.replace(/^\s+|\s+$/g, '') === '') { el.classList.add('medium-editor-placeholder'); } }, placeholderWrapper = function (e) { this.classList.remove('medium-editor-placeholder'); if (e.type !== 'keypress') { activatePlaceholder(this); } }; for (i = 0; i < this.elements.length; i += 1) { activatePlaceholder(this.elements[i]); this.on(this.elements[i], 'blur', placeholderWrapper); this.on(this.elements[i], 'keypress', placeholderWrapper); } 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('