']
];
}
/*jslint regexp: false*/
pasteHandler = {
handlePaste: function (element, evt, options) {
var paragraphs,
html = '',
p,
dataFormatHTML = 'text/html',
dataFormatPlain = 'text/plain';
element.classList.remove('medium-editor-placeholder');
if (!options.forcePlainText && !options.cleanPastedHTML) {
return element;
}
if (options.contentWindow.clipboardData && evt.clipboardData === undefined) {
evt.clipboardData = options.contentWindow.clipboardData;
// If window.clipboardData exists, but e.clipboardData doesn't exist,
// we're probably in IE. IE only has two possibilities for clipboard
// data format: 'Text' and 'URL'.
//
// Of the two, we want 'Text':
dataFormatHTML = 'Text';
dataFormatPlain = 'Text';
}
if (evt.clipboardData && evt.clipboardData.getData && !evt.defaultPrevented) {
evt.preventDefault();
if (options.cleanPastedHTML && evt.clipboardData.getData(dataFormatHTML)) {
return this.cleanPaste(evt.clipboardData.getData(dataFormatHTML), options);
}
if (!(options.disableReturn || element.getAttribute('data-disable-return'))) {
paragraphs = evt.clipboardData.getData(dataFormatPlain).split(/[\r\n]/g);
for (p = 0; p < paragraphs.length; p += 1) {
if (paragraphs[p] !== '') {
html += '' + mediumEditorUtil.htmlEntities(paragraphs[p]) + '
';
}
}
mediumEditorUtil.insertHTMLCommand(options.ownerDocument, html);
} else {
html = mediumEditorUtil.htmlEntities(evt.clipboardData.getData(dataFormatPlain));
mediumEditorUtil.insertHTMLCommand(options.ownerDocument, html);
}
}
},
cleanPaste: function (text, options) {
var i, elList, workEl,
el = meSelection.getSelectionElement(options.contentWindow),
multiline = /
');
this.pasteHTML('
' + elList.join('
') + '
', options.ownerDocument);
options.ownerDocument.execCommand('insertText', false, "\n");
// block element cleanup
elList = el.querySelectorAll('a,p,div,br');
for (i = 0; i < elList.length; i += 1) {
workEl = elList[i];
switch (workEl.tagName.toLowerCase()) {
case 'a':
if (options.targetBlank) {
mediumEditorUtil.setTargetBlank(workEl);
}
break;
case 'p':
case 'div':
this.filterCommonBlocks(workEl);
break;
case 'br':
this.filterLineBreak(workEl);
break;
}
}
} else {
this.pasteHTML(text, options.ownerDocument);
}
},
pasteHTML: function (html, ownerDocument) {
var elList, workEl, i, fragmentBody, pasteBlock = ownerDocument.createDocumentFragment();
pasteBlock.appendChild(ownerDocument.createElement('body'));
fragmentBody = pasteBlock.querySelector('body');
fragmentBody.innerHTML = html;
this.cleanupSpans(fragmentBody, ownerDocument);
elList = fragmentBody.querySelectorAll('*');
for (i = 0; i < elList.length; i += 1) {
workEl = elList[i];
// delete ugly attributes
workEl.removeAttribute('class');
workEl.removeAttribute('style');
workEl.removeAttribute('dir');
if (workEl.tagName.toLowerCase() === 'meta') {
workEl.parentNode.removeChild(workEl);
}
}
mediumEditorUtil.insertHTMLCommand(ownerDocument, fragmentBody.innerHTML.replace(/ /g, ' '));
},
isCommonBlock: function (el) {
return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
},
filterCommonBlocks: function (el) {
if (/^\s*$/.test(el.textContent)) {
el.parentNode.removeChild(el);
}
},
filterLineBreak: function (el) {
if (this.isCommonBlock(el.previousElementSibling)) {
// remove stray br's following common block elements
el.parentNode.removeChild(el);
} else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
// remove br's just inside open or close tags of a div/p
el.parentNode.removeChild(el);
} else if (el.parentNode.childElementCount === 1) {
// and br's that are the only child of a div/p
this.removeWithParent(el);
}
},
// remove an element, including its parent, if it is the only element within its parent
removeWithParent: function (el) {
if (el && el.parentNode) {
if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
el.parentNode.parentNode.removeChild(el.parentNode);
} else {
el.parentNode.removeChild(el.parentNode);
}
}
},
cleanupSpans: function (container_el, ownerDocument) {
var i,
el,
new_el,
spans = container_el.querySelectorAll('.replace-with'),
isCEF = function (el) {
return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
};
for (i = 0; i < spans.length; i += 1) {
el = spans[i];
new_el = ownerDocument.createElement(el.classList.contains('bold') ? 'b' : 'i');
if (el.classList.contains('bold') && el.classList.contains('italic')) {
// add an i tag as well if this has both italics and bold
new_el.innerHTML = '' + el.innerHTML + '';
} else {
new_el.innerHTML = el.innerHTML;
}
el.parentNode.replaceChild(new_el, el);
}
spans = container_el.querySelectorAll('span');
for (i = 0; i < spans.length; i += 1) {
el = spans[i];
// bail if span is in contenteditable = false
if (mediumEditorUtil.traverseUp(el, isCEF)) {
return false;
}
// remove empty spans, replace others with their contents
if (/^\s*$/.test()) {
el.parentNode.removeChild(el);
} else {
el.parentNode.replaceChild(ownerDocument.createTextNode(el.textContent), el);
}
}
}
};
}(window, document));
var AnchorExtension;
(function (window, document) {
'use strict';
AnchorExtension = function (instance) {
this.base = instance;
};
AnchorExtension.prototype = {
getForm: function () {
if (!this.anchorForm) {
this.anchorForm = this.createForm();
}
return this.anchorForm;
},
getInput: function () {
return this.getForm().querySelector('input.medium-editor-toolbar-input');
},
deactivate: function () {
if (!this.anchorForm) {
return false;
}
if (this.anchorForm.parentNode) {
this.anchorForm.parentNode.removeChild(this.anchorForm);
}
delete this.anchorForm;
},
doLinkCreation: function () {
var button = null,
target,
targetCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-target'),
buttonCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-button');
if (targetCheckbox && targetCheckbox.checked) {
target = "_blank";
} else {
target = "_self";
}
if (buttonCheckbox && buttonCheckbox.checked) {
button = this.base.options.anchorButtonClass;
}
this.base.createLink(this.getInput(), target, button);
},
doFormCancel: function () {
this.base.showToolbarActions();
this.base.restoreSelection();
},
handleOutsideInteraction: function (event) {
if (event.target !== this.getForm() &&
!mediumEditorUtil.isDescendant(this.getForm(), event.target) &&
!mediumEditorUtil.isDescendant(this.base.toolbarActions, event.target)) {
this.base.keepToolbarAlive = false;
this.base.checkSelection();
}
},
createForm: function () {
var doc = this.base.options.ownerDocument,
form = doc.createElement('div'),
input = doc.createElement('input'),
close = doc.createElement('a'),
save = doc.createElement('a'),
target,
target_label,
button,
button_label;
// Anchor Form (div)
form.className = 'medium-editor-toolbar-form';
form.id = 'medium-editor-toolbar-form-anchor-' + this.base.id;
// Handle clicks on the form itself
this.base.on(form, 'click', function (event) {
event.stopPropagation();
this.base.keepToolbarAlive = true;
}.bind(this));
// Add url textbox
input.setAttribute('type', 'text');
input.className = 'medium-editor-toolbar-input';
input.setAttribute('placeholder', this.base.options.anchorInputPlaceholder);
form.appendChild(input);
// Handle typing in the textbox
this.base.on(input, 'keyup', function (event) {
// For ENTER -> create the anchor
if (event.keyCode === mediumEditorUtil.keyCode.ENTER) {
event.preventDefault();
this.doLinkCreation();
return;
}
// For ESCAPE -> close the form
if (event.keyCode === mediumEditorUtil.keyCode.ESCAPE) {
event.preventDefault();
this.doFormCancel();
}
}.bind(this));
// Handle clicks into the textbox
this.base.on(input, 'click', function (event) {
// make sure not to hide form when cliking into the input
event.stopPropagation();
this.base.keepToolbarAlive = true;
}.bind(this));
// Add save buton
save.setAttribute('href', '#');
save.className = 'medium-editor-toobar-save';
save.innerHTML = '✓';
form.appendChild(save);
// Handle save button clicks (capture)
this.base.on(save, 'click', function (event) {
// Clicking Save -> create the anchor
event.preventDefault();
this.doLinkCreation();
}.bind(this), true);
// Add close button
close.setAttribute('href', '#');
close.className = 'medium-editor-toobar-close';
close.innerHTML = '×';
form.appendChild(close);
// Handle close button clicks
this.base.on(close, 'click', function (event) {
// Click Close -> close the form
event.preventDefault();
this.doFormCancel();
}.bind(this));
// (Optional) Add 'open in new window' checkbox
if (this.base.options.anchorTarget) {
target = doc.createElement('input');
target.setAttribute('type', 'checkbox');
target.className = 'medium-editor-toolbar-anchor-target';
target_label = doc.createElement('label');
target_label.innerHTML = this.base.options.anchorInputCheckboxLabel;
target_label.insertBefore(target, target_label.firstChild);
form.appendChild(target_label);
}
// (Optional) Add 'add button class to anchor' checkbox
if (this.base.options.anchorButton) {
button = doc.createElement('input');
button.setAttribute('type', 'checkbox');
button.className = 'medium-editor-toolbar-anchor-button';
button_label = doc.createElement('label');
button_label.innerHTML = "Button";
button_label.insertBefore(button, button_label.firstChild);
form.appendChild(button_label);
}
// Handle click (capture) & focus (capture) outside of the form
this.base.on(doc.body, 'click', this.handleOutsideInteraction.bind(this), true);
this.base.on(doc.body, 'focus', this.handleOutsideInteraction.bind(this), true);
return form;
},
focus: function (value) {
var input = this.getInput();
input.focus();
input.value = value || '';
},
hideForm: function () {
this.getForm().style.display = 'none';
},
showForm: function () {
this.getForm().style.display = 'block';
},
isDisplayed: function () {
return this.getForm().style.display === 'block';
},
isClickIntoForm: function (event) {
return (event &&
event.type &&
event.type.toLowerCase() === 'blur' &&
event.relatedTarget &&
event.relatedTarget === this.getInput());
}
};
}(window, document));
function MediumEditor(elements, options) {
'use strict';
return this.init(elements, options);
}
(function () {
'use strict';
MediumEditor.statics = {
ButtonsData: ButtonsData,
DefaultButton: DefaultButton,
AnchorExtension: AnchorExtension
};
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,
imageDragging: true,
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'
},
init: function (elements, options) {
var uniqueId = 1;
this.options = mediumEditorUtil.extend(options, this.defaults);
this.setElementSelection(elements);
if (this.elements.length === 0) {
return;
}
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()
.initCommands()
.initElements()
.bindSelect()
.bindDragDrop()
.bindPaste()
.setPlaceholders()
.bindElementActions()
.bindWindowActions();
},
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 = mediumEditorUtil.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 = mediumEditorUtil.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.initToolbar()
.bindButtons()
.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 (mediumEditorUtil.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,
selection = self.options.contentWindow.getSelection(),
selRange = selection.isCollapsed ?
null :
meSelection.getSelectedParentElement(selection.getRangeAt(0)),
i;
// This control was introduced also to avoid the toolbar
// to disapper when selecting from right to left and
// the selection ends at the beginning of the text.
for (i = 0; i < self.elements.length; i += 1) {
if (mediumEditorUtil.isDescendant(self.elements[i], e.target)
|| mediumEditorUtil.isDescendant(self.elements[i], selRange)) {
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
&& !mediumEditorUtil.isDescendant(self.toolbar, e.target)
&& !mediumEditorUtil.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)
.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;
},
initExtension: function (extension, name) {
if (extension.parent) {
extension.base = this;
}
if (typeof extension.init === 'function') {
extension.init(this);
}
if (!extension.name) {
extension.name = name;
}
return extension;
},
initCommands: function () {
var buttons = this.options.buttons,
extensions = this.options.extensions,
ext,
name;
this.commands = [];
buttons.forEach(function (buttonName) {
if (extensions[buttonName]) {
ext = this.initExtension(extensions[buttonName], buttonName);
this.commands.push(ext);
} else if (ButtonsData.hasOwnProperty(buttonName)) {
ext = new DefaultButton(ButtonsData[buttonName], this);
this.commands.push(ext);
}
}.bind(this));
for (name in extensions) {
if (extensions.hasOwnProperty(name) && buttons.indexOf(name) === -1) {
ext = this.initExtension(extensions[name], name);
}
}
return this;
},
/**
* 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;
},
bindParagraphCreation: function (index) {
var self = this;
this.on(this.elements[index], 'keypress', function (e) {
var node,
tagName;
if (e.which === mediumEditorUtil.keyCode.SPACE) {
node = meSelection.getSelectionStart(self.options.ownerDocument);
tagName = node.tagName.toLowerCase();
if (tagName === 'a') {
self.options.ownerDocument.execCommand('unlink', false, null);
}
}
});
this.on(this.elements[index], 'keyup', function (e) {
var node = meSelection.getSelectionStart(self.options.ownerDocument),
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 === mediumEditorUtil.keyCode.ENTER) {
node = meSelection.getSelectionStart(self.options.ownerDocument);
tagName = node.tagName.toLowerCase();
editorElement = meSelection.getSelectionElement(self.options.contentWindow);
if (!(self.options.disableReturn || editorElement.getAttribute('data-disable-return')) &&
tagName !== 'li' && !mediumEditorUtil.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;
},
bindReturn: function (index) {
var self = this;
this.on(this.elements[index], 'keypress', function (e) {
if (e.which === mediumEditorUtil.keyCode.ENTER) {
if (self.options.disableReturn || this.getAttribute('data-disable-return')) {
e.preventDefault();
} else if (self.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
var node = meSelection.getSelectionStart(self.options.contentWindow);
if (node && node.textContent.trim() === '') {
e.preventDefault();
}
}
}
});
return this;
},
bindKeydown: function (index) {
var self = this;
this.on(this.elements[index], 'keydown', function (e) {
var node, tag, key;
if (e.which === mediumEditorUtil.keyCode.TAB) {
// Override tab only for pre nodes
node = meSelection.getSelectionStart(self.options.ownerDocument);
tag = node && node.tagName.toLowerCase();
if (tag === 'pre') {
e.preventDefault();
self.options.ownerDocument.execCommand('insertHtml', null, ' ');
}
// Tab to indent list structures!
if (tag === 'li' || mediumEditorUtil.isListItemChild(node)) {
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 === mediumEditorUtil.keyCode.BACKSPACE || e.which === mediumEditorUtil.keyCode.DELETE || e.which === mediumEditorUtil.keyCode.ENTER) {
// Bind keys which can create or destroy a block element: backspace, delete, return
self.onBlockModifier(e);
} else if (e.ctrlKey) {
key = String.fromCharCode(e.which || e.keyCode).toLowerCase();
self.commands.forEach(function (extension) {
if (extension.options.key && extension.options.key === key) {
extension.handleClick(e);
}
});
}
});
return this;
},
onBlockModifier: function (e) {
var range, sel, p, node = meSelection.getSelectionStart(this.options.ownerDocument),
tagName = node.tagName.toLowerCase(),
isEmpty = /^(\s+|
)?$/i,
isHeader = /h\d/i;
if ((e.which === mediumEditorUtil.keyCode.BACKSPACE || e.which === mediumEditorUtil.keyCode.ENTER)
&& node.previousElementSibling
// in a header
&& isHeader.test(tagName)
// at the very end of the block
&& meSelection.getCaretOffsets(node).left === 0) {
if (e.which === mediumEditorUtil.keyCode.BACKSPACE && 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 === mediumEditorUtil.keyCode.ENTER) {
// hitting return in the begining of a header will create empty header elements before the current one
// instead, make "
" element, which are what happens if you hit return in an empty paragraph
p = this.options.ownerDocument.createElement('p');
p.innerHTML = '
';
node.previousElementSibling.parentNode.insertBefore(p, node);
e.preventDefault();
}
} else if (e.which === mediumEditorUtil.keyCode.DELETE
&& node.nextElementSibling
&& node.previousElementSibling
// not in a header
&& !isHeader.test(tagName)
// in an empty tag
&& isEmpty.test(node.innerHTML)
// when the next tag *is* a header
&& isHeader.test(node.nextElementSibling.tagName)) {
// hitting delete in an empty element preceding a header, ex:
// [CURSOR]
Header
// Will cause the h1 to become a paragraph.
// Instead, delete the paragraph node and move the cursor to the begining of the h1
// remove node and move cursor to start of header
range = document.createRange();
sel = window.getSelection();
range.setStart(node.nextElementSibling, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
node.previousElementSibling.parentNode.removeChild(node);
e.preventDefault();
}
},
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();
this.addExtensionForms();
return this;
},
createToolbar: function () {
var toolbar = this.options.ownerDocument.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) {
this.anchorExtension = new AnchorExtension(this);
toolbar.appendChild(this.anchorExtension.getForm());
}
this.options.elementsContainer.appendChild(toolbar);
return toolbar;
},
//TODO: actionTemplate
toolbarButtons: function () {
var ul = this.options.ownerDocument.createElement('ul'),
li,
btn;
ul.id = 'medium-editor-toolbar-actions' + this.id;
ul.className = 'medium-editor-toolbar-actions clearfix';
this.commands.forEach(function (extension) {
if (typeof extension.getButton === 'function') {
btn = extension.getButton(this);
li = this.options.ownerDocument.createElement('li');
if (mediumEditorUtil.isElement(btn)) {
li.appendChild(btn);
} else {
li.innerHTML = btn;
}
ul.appendChild(li);
}
}.bind(this));
return ul;
},
addExtensionForms: function () {
var form,
id;
this.commands.forEach(function (extension) {
if (extension.hasForm) {
form = (typeof extension.getForm === 'function') ? extension.getForm() : null;
}
if (form) {
id = 'medium-editor-toolbar-form-' + extension.name + '-' + this.id;
form.className += ' medium-editor-toolbar-form';
form.id = id;
this.toolbar.appendChild(form);
}
}.bind(this));
},
bindSelect: function () {
var self = this,
i,
timeoutHelper;
this.checkSelectionWrapper = function (e) {
// Do not close the toolbar when bluring the editable area and clicking into the anchor form
if (e && this.anchorExtension && this.anchorExtension.isClickIntoForm(e)) {
return false;
}
self.checkSelection();
};
timeoutHelper = function (event) {
setTimeout(function () {
this.checkSelectionWrapper(event);
}.bind(this), 0);
}.bind(this);
this.on(this.options.ownerDocument.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', timeoutHelper);
}
return this;
},
// http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
insertHTML: function insertHTML(html) {
var selection, range, el, fragment, node, lastNode;
if (this.options.ownerDocument.queryCommandSupported('insertHTML')) {
try {
return this.options.ownerDocument.execCommand('insertHTML', false, html);
} catch (ignore) {}
}
selection = window.getSelection();
if (selection.getRangeAt && selection.rangeCount) {
range = selection.getRangeAt(0);
range.deleteContents();
el = this.options.ownerDocument.createElement("div");
el.innerHTML = html;
fragment = this.options.ownerDocument.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);
}
}
},
bindDragDrop: function () {
var self = this, i, className, onDrag, onDrop, element;
if (!self.options.imageDragging) {
return this;
}
className = 'medium-editor-dragover';
onDrag = function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
if (e.type === "dragover") {
this.classList.add(className);
} else {
this.classList.remove(className);
}
};
onDrop = function (e) {
var files;
e.preventDefault();
e.stopPropagation();
files = Array.prototype.slice.call(e.dataTransfer.files, 0);
files.some(function (file) {
if (file.type.match("image")) {
var fileReader, id;
fileReader = new FileReader();
fileReader.readAsDataURL(file);
id = 'medium-img-' + (+new Date());
mediumEditorUtil.insertHTMLCommand(self.options.ownerDocument, '');
fileReader.onload = function () {
var img = document.getElementById(id);
if (img) {
img.removeAttribute('id');
img.removeAttribute('class');
img.src = fileReader.result;
}
};
}
});
this.classList.remove(className);
};
for (i = 0; i < this.elements.length; i += 1) {
element = this.elements[i];
this.on(element, 'dragover', onDrag);
this.on(element, 'dragleave', onDrag);
this.on(element, 'drop', onDrop);
}
return this;
},
stopSelectionUpdates: function () {
this.preventSelectionUpdates = true;
},
startSelectionUpdates: function () {
this.preventSelectionUpdates = false;
},
checkSelection: function () {
var newSelection,
selectionElement;
if (!this.preventSelectionUpdates &&
this.keepToolbarAlive !== true &&
!this.options.disableToolbar) {
newSelection = this.options.contentWindow.getSelection();
if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') ||
(this.options.allowMultiParagraphSelection === false && this.hasMultiParagraphs()) ||
meSelection.selectionInContentEditableFalse(this.options.contentWindow)) {
if (!this.options.staticToolbar) {
this.hideToolbarActions();
} else if (this.anchorExtension && this.anchorExtension.isDisplayed()) {
this.setToolbarButtonStates();
this.showToolbarActions();
}
} else {
selectionElement = meSelection.getSelectionElement(this.options.contentWindow);
if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
if (!this.options.staticToolbar) {
this.hideToolbarActions();
}
} else {
this.checkSelectionElement(newSelection, selectionElement);
}
}
}
return this;
},
hasMultiParagraphs: function () {
var selectionHtml = meSelection.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,
adjacentNode,
offset = 0,
newRange;
this.selection = newSelection;
this.selectionRange = this.selection.getRangeAt(0);
/*
* In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
* will be at the very end of an element. In other browsers, the selectionRange start
* would instead be at the very beginning of an element that actually has content.
* example:
* foobar
*
* If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
* of the 'bar' span. However, there are cases where firefox will have the selectionRange start
* at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any
* properties on the 'bar' span, they won't be reflected accurately in the toolbar
* (ie 'Bold' button wouldn't be active)
*
* So, for cases where the selectionRange start is at the end of an element/node, find the next
* adjacent text node that actually has content in it, and move the selectionRange start there.
*/
if (this.options.standardizeSelectionStart &&
this.selectionRange.startContainer.nodeValue &&
(this.selectionRange.startOffset === this.selectionRange.startContainer.nodeValue.length)) {
adjacentNode = mediumEditorUtil.findAdjacentTextNodeWithContent(meSelection.getSelectionElement(this.options.contentWindow), this.selectionRange.startContainer, this.options.ownerDocument);
if (adjacentNode) {
offset = 0;
while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
offset = offset + 1;
}
newRange = this.options.ownerDocument.createRange();
newRange.setStart(adjacentNode, offset);
newRange.setEnd(this.selectionRange.endContainer, this.selectionRange.endOffset);
this.selection.removeAllRanges();
this.selection.addRange(newRange);
this.selectionRange = newRange;
}
}
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();
}
},
setToolbarPosition: function () {
// document.documentElement for IE 9
var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
container = this.elements[0],
containerRect = container.getBoundingClientRect(),
containerTop = containerRect.top + scrollTop,
buttonHeight = 50,
selection = this.options.contentWindow.getSelection(),
range,
boundary,
middleBoundary,
defaultLeft = (this.options.diffLeft) - (this.toolbar.offsetWidth / 2),
halfOffsetWidth = this.toolbar.offsetWidth / 2,
containerCenter = (containerRect.left + (containerRect.width / 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";
}
if (this.options.toolbarAlign) {
if (this.options.toolbarAlign === 'left') {
this.toolbar.style.left = containerRect.left + "px";
} else if (this.options.toolbarAlign === 'center') {
this.toolbar.style.left = (containerCenter - halfOffsetWidth) + "px";
} else {
this.toolbar.style.left = (containerRect.right - this.toolbar.offsetWidth) + "px";
}
} else {
this.toolbar.style.left = (containerCenter - halfOffsetWidth) + "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 () {
this.commands.forEach(function (extension) {
if (typeof extension.deactivate === 'function') {
extension.deactivate();
}
}.bind(this));
this.checkActiveButtons();
return this;
},
checkActiveButtons: function () {
var elements = Array.prototype.slice.call(this.elements),
manualStateChecks = [],
queryState = null,
parentNode = meSelection.getSelectedParentElement(this.selectionRange),
checkExtension = function (extension) {
if (typeof extension.checkState === 'function') {
extension.checkState(parentNode);
} else if (typeof extension.isActive === 'function') {
if (!extension.isActive() && extension.shouldActivate(parentNode)) {
extension.activate();
}
}
};
// Loop through all commands
this.commands.forEach(function (command) {
// For those commands where we can use document.queryCommandState(), do so
if (typeof command.queryCommandState === 'function') {
queryState = command.queryCommandState();
// If queryCommandState returns a valid value, we can trust the browser
// and don't need to do our manual checks
if (queryState !== null) {
if (queryState) {
command.activate();
}
return;
}
}
// We can't use queryCommandState for this command, so add to manualStateChecks
manualStateChecks.push(command);
});
// Climb up the DOM and do manual checks for whether a certain command is currently enabled for this node
while (parentNode.tagName !== undefined && mediumEditorUtil.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
this.activateButton(parentNode.tagName.toLowerCase());
manualStateChecks.forEach(checkExtension.bind(this));
// 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.classList.contains(this.options.activeButtonClass)) {
el.classList.add(this.options.activeButtonClass);
}
},
bindButtons: function () {
this.setFirstAndLastItems(this.toolbar.querySelectorAll('button'));
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) {
/*jslint regexp: true*/
var fullAction = /^full-(.+)$/gi,
appendAction = /^append-(.+)$/gi,
justifyAction = /^justify(left|center|right|full)$/gi,
match;
/*jslint regexp: false*/
// Actions starting with 'full-' should be applied to to the entire contents of the editable element
// (ie full-bold, full-append-pre, etc.)
match = fullAction.exec(action);
if (match) {
// Store the current selection to be restored after applying the action
this.saveSelection();
// Select all of the contents before calling the action
this.selectAllContents();
this.execAction(match[1], e);
// Restore the previous selection
this.restoreSelection();
return;
}
// Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
// type of block element (ie append-blockquote, append-h1, append-pre, etc.)
match = appendAction.exec(action);
if (match) {
this.execFormatBlock(match[1]);
this.setToolbarPosition();
this.setToolbarButtonStates();
return;
}
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();
// Manually update the toolbar for text-alignment actions
if (justifyAction.test(action)) {
this.setToolbarButtonStates();
}
}
},
// Method to show an extension's form
// TO DO: Improve this
showForm: function (formId, e) {
this.toolbarActions.style.display = 'none';
this.saveSelection();
var form = document.getElementById(formId);
form.style.display = 'block';
this.setToolbarPosition();
this.keepToolbarAlive = true;
},
// Method to show an extension's form
// TO DO: Improve this
hideForm: function (form, e) {
var el = document.getElementById(form.id);
el.style.display = 'none';
this.showToolbarActions();
this.setToolbarPosition();
this.restoreSelection();
},
// TODO: move these two methods to selection.js
// 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) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
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 = meSelection.getSelectedParentElement(this.selectionRange);
if (selectedParentElement.tagName &&
selectedParentElement.tagName.toLowerCase() === 'a') {
this.options.ownerDocument.execCommand('unlink', false, null);
} else if (this.anchorExtension) {
if (this.anchorExtension.isDisplayed()) {
this.showToolbarActions();
} else {
this.showAnchorForm();
}
}
return this;
},
execFormatBlock: function (el) {
var selectionData = meSelection.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 (mediumEditorUtil.isIE) {
if (el === 'blockquote') {
return this.options.ownerDocument.execCommand('indent', false, el);
}
el = '<' + el + '>';
}
return this.options.ownerDocument.execCommand('formatBlock', false, el);
},
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');
if (this.onShowToolbar) {
this.onShowToolbar();
}
}
},
hideToolbar: function () {
if (this.isToolbarShown()) {
this.toolbar.classList.remove('medium-editor-toolbar-active');
// TODO: this should be an option?
if (this.onHideToolbar) {
this.onHideToolbar();
}
}
},
hideToolbarActions: function () {
this.commands.forEach(function (extension) {
if (extension.onHide && typeof extension.onHide === 'function') {
extension.onHide();
}
});
this.keepToolbarAlive = false;
this.hideToolbar();
},
showToolbarActions: function () {
var self = this;
if (this.anchorExtension) {
this.anchorExtension.hideForm();
}
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
this.delay(function () {
self.showToolbar();
});
},
selectAllContents: function () {
var range = this.options.ownerDocument.createRange(),
sel = this.options.contentWindow.getSelection(),
currNode = meSelection.getSelectionElement(this.options.contentWindow);
if (currNode) {
// Move to the lowest descendant node that still selects all of the contents
while (currNode.children.length === 1) {
currNode = currNode.children[0];
}
range.selectNodeContents(currNode);
sel.removeAllRanges();
sel.addRange(range);
}
},
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
// Tim Down
// TODO: move to selection.js and clean up old methods there
saveSelection: function () {
this.selectionState = null;
var selection = this.options.contentWindow.getSelection(),
range,
preSelectionRange,
start,
editableElementIndex = -1;
if (selection.rangeCount > 0) {
range = selection.getRangeAt(0);
preSelectionRange = range.cloneRange();
// Find element current selection is inside
this.elements.forEach(function (el, index) {
if (el === range.startContainer || mediumEditorUtil.isDescendant(el, range.startContainer)) {
editableElementIndex = index;
return false;
}
});
if (editableElementIndex > -1) {
preSelectionRange.selectNodeContents(this.elements[editableElementIndex]);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
start = preSelectionRange.toString().length;
this.selectionState = {
start: start,
end: start + range.toString().length,
editableElementIndex: editableElementIndex
};
}
}
},
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
// Tim Down
// TODO: move to selection.js and clean up old methods there
restoreSelection: function () {
if (!this.selectionState) {
return;
}
var editableElement = this.elements[this.selectionState.editableElementIndex],
charIndex = 0,
range = this.options.ownerDocument.createRange(),
nodeStack = [editableElement],
node,
foundStart = false,
stop = false,
i,
sel,
nextCharIndex;
range.setStart(editableElement, 0);
range.collapse(true);
node = nodeStack.pop();
while (!stop && node) {
if (node.nodeType === 3) {
nextCharIndex = charIndex + node.length;
if (!foundStart && this.selectionState.start >= charIndex && this.selectionState.start <= nextCharIndex) {
range.setStart(node, this.selectionState.start - charIndex);
foundStart = true;
}
if (foundStart && this.selectionState.end >= charIndex && this.selectionState.end <= nextCharIndex) {
range.setEnd(node, this.selectionState.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
i = node.childNodes.length - 1;
while (i >= 0) {
nodeStack.push(node.childNodes[i]);
i -= 1;
}
}
if (!stop) {
node = nodeStack.pop();
}
}
sel = this.options.contentWindow.getSelection();
sel.removeAllRanges();
sel.addRange(range);
},
showAnchorForm: function (link_value) {
if (!this.anchorExtension) {
return;
}
this.toolbarActions.style.display = 'none';
this.saveSelection();
this.anchorExtension.showForm();
this.setToolbarPosition();
this.keepToolbarAlive = true;
this.anchorExtension.focus(link_value);
},
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.attributes.href.value;
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 '' +
' ' +
'
';
},
anchorPreviewClickHandler: function (e) {
if (!this.options.disableAnchorForm && this.activeAnchor) {
var self = this,
range = this.options.ownerDocument.createRange(),
sel = this.options.contentWindow.getSelection();
range.selectNodeContents(self.activeAnchor);
sel.removeAllRanges();
sel.addRange(range);
// Using setTimeout + options.delay because:
// We may actually be displaying the anchor form, which should be controlled by options.delay
this.delay(function () {
if (self.activeAnchor) {
self.showAnchorForm(self.activeAnchor.attributes.href.value);
}
self.keepToolbarAlive = false;
});
}
this.hideAnchorPreview();
},
editorAnchorObserver: function (e) {
var self = this,
overAnchor = true,
leaveAnchor = function () {
// mark the anchor as no longer hovered, and stop listening
overAnchor = false;
self.off(self.activeAnchor, 'mouseout', leaveAnchor);
};
if (e.target && e.target.tagName.toLowerCase() === 'a') {
// Detect empty href attributes
// The browser will make href="" or href="#top"
// into absolute urls when accessed as e.targed.href, so check the html
if (!/href=["']\S+["']/.test(e.target.outerHTML) || /href=["']#\S+["']/.test(e.target.outerHTML)) {
return true;
}
// only show when hovering on anchors
if (this.isToolbarShown()) {
// only show when toolbar is not present
return true;
}
this.activeAnchor = e.target;
this.on(this.activeAnchor, 'mouseout', leaveAnchor);
// Using setTimeout + options.delay because:
// - We're going to show the anchor preview according to the configured delay
// if the mouse has not left the anchor tag in that time
this.delay(function () {
if (overAnchor) {
self.showAnchorPreview(e.target);
}
});
}
},
bindAnchorPreview: function (index) {
var i, self = this;
this.editorAnchorObserverWrapper = function (e) {
self.editorAnchorObserver(e);
};
for (i = 0; i < this.elements.length; i += 1) {
this.on(this.elements[i], 'mouseover', this.editorAnchorObserverWrapper);
}
return this;
},
checkLinkFormat: function (value) {
var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
return (re.test(value) ? '' : 'http://') + value;
},
setButtonClass: function (buttonClass) {
var el = meSelection.getSelectionStart(this.options.ownerDocument),
classes = buttonClass.split(' '),
i,
j;
if (el.tagName.toLowerCase() === 'a') {
for (j = 0; j < classes.length; j += 1) {
el.classList.add(classes[j]);
}
} else {
el = el.getElementsByTagName('a');
for (i = 0; i < el.length; i += 1) {
for (j = 0; j < classes.length; j += 1) {
el[i].classList.add(classes[j]);
}
}
}
},
createLink: function (input, target, buttonClass) {
var i, event;
this.createLinkInternal(input.value, target, buttonClass);
if (this.options.targetBlank || target === "_blank" || buttonClass) {
event = this.options.ownerDocument.createEvent("HTMLEvents");
event.initEvent("input", true, true, this.options.contentWindow);
for (i = 0; i < this.elements.length; i += 1) {
this.elements[i].dispatchEvent(event);
}
}
this.checkSelection();
this.showToolbarActions();
input.value = '';
},
createLinkInternal: function (url, target, buttonClass) {
if (!url || url.trim().length === 0) {
this.hideToolbarActions();
return;
}
this.restoreSelection();
if (this.options.checkLinkFormat) {
url = this.checkLinkFormat(url);
}
this.options.ownerDocument.execCommand('createLink', false, url);
if (this.options.targetBlank || target === "_blank") {
mediumEditorUtil.setTargetBlank(meSelection.getSelectionStart(this.options.ownerDocument));
}
if (buttonClass) {
this.setButtonClass(buttonClass);
}
},
positionToolbarIfShown: function () {
if (this.isToolbarShown()) {
this.setToolbarPosition();
}
},
bindWindowActions: function () {
var self = this;
// Add a scroll event for sticky toolbar
if (this.options.staticToolbar && this.options.stickyToolbar) {
// On scroll, re-position the toolbar
this.on(this.options.contentWindow, 'scroll', function () {
self.positionToolbarIfShown();
}, true);
}
this.on(this.options.contentWindow, 'resize', function () {
self.handleResize();
});
this.bindBlur();
return this;
},
activate: function () {
if (this.isActive) {
return;
}
this.setup();
},
// TODO: break method
deactivate: function () {
var i;
if (!this.isActive) {
return;
}
this.isActive = false;
if (this.toolbar !== undefined) {
this.options.elementsContainer.removeChild(this.anchorPreview);
this.options.elementsContainer.removeChild(this.toolbar);
delete this.toolbar;
delete this.anchorPreview;
}
for (i = 0; i < this.elements.length; i += 1) {
this.elements[i].removeAttribute('contentEditable');
this.elements[i].removeAttribute('data-medium-element');
}
this.commands.forEach(function (extension) {
if (typeof extension.deactivate === 'function') {
extension.deactivate();
}
}.bind(this));
if (this.anchorExtension) {
this.anchorExtension.deactivate();
}
this.removeAllEvents();
},
bindPaste: function () {
var i, self = this;
this.pasteWrapper = function (e) {
pasteHandler.handlePaste(this, e, self.options);
};
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) {
pasteHandler.cleanPaste(text, this.options);
},
pasteHTML: function (html) {
pasteHandler.pasteHTML(html, this.options.ownerDocument);
}
};
}());
return MediumEditor;
}()));