spacedeck-open/public/javascripts/medium.patched.js

1862 lines
45 KiB
JavaScript
Raw Permalink Normal View History

2017-04-07 01:29:05 +02:00
/*
* Medium.js
*
* Copyright 2013, Jacob Kelley - http://jakiestfu.com/
* Released under the MIT Licence
* http://opensource.org/licenses/MIT
*
* Github: http://github.com/jakiestfu/Medium.js/
* Version: 1.0
*/
(function (w, d) {
'use strict';
var Medium = (function () {
var trim = function (string) {
return string.replace(/^\s+|\s+$/g, '');
},
arrayContains = function (array, variable) {
var i = array.length;
while (i--) {
if (array[i] === variable) {
return true;
}
}
return false;
},
//two modes, wild (native) or domesticated (rangy + undo.js)
rangy = w['rangy'] || null,
undo = w['Undo'] || null,
wild = (!rangy || !undo),
domesticated = (!wild),
key = w.Key = {
'backspace': 8,
'tab': 9,
'enter': 13,
'shift': 16,
'ctrl': 17,
'alt': 18,
'pause': 19,
'capsLock': 20,
'escape': 27,
'pageUp': 33,
'pageDown': 34,
'end': 35,
'home': 36,
'leftArrow': 37,
'upArrow': 38,
'rightArrow': 39,
'downArrow': 40,
'insert': 45,
'delete': 46,
'0': 48,
'1': 49,
'2': 50,
'3': 51,
'4': 52,
'5': 53,
'6': 54,
'7': 55,
'8': 56,
'9': 57,
'a': 65,
'b': 66,
'c': 67,
'd': 68,
'e': 69,
'f': 70,
'g': 71,
'h': 72,
'i': 73,
'j': 74,
'k': 75,
'l': 76,
'm': 77,
'n': 78,
'o': 79,
'p': 80,
'q': 81,
'r': 82,
's': 83,
't': 84,
'u': 85,
'v': 86,
'w': 87,
'x': 88,
'y': 89,
'z': 90,
'leftWindow': 91,
'rightWindowKey': 92,
'select': 93,
'numpad0': 96,
'numpad1': 97,
'numpad2': 98,
'numpad3': 99,
'numpad4': 100,
'numpad5': 101,
'numpad6': 102,
'numpad7': 103,
'numpad8': 104,
'numpad9': 105,
'multiply': 106,
'add': 107,
'subtract': 109,
'decimalPoint': 110,
'divide': 111,
'f1': 112,
'f2': 113,
'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123,
'numLock': 144,
'scrollLock': 145,
'semiColon': 186,
'equalSign': 187,
'comma': 188,
'dash': 189,
'period': 190,
'forwardSlash': 191,
'graveAccent': 192,
'openBracket': 219,
'backSlash': 220,
'closeBraket': 221,
'singleQuote': 222
},
/**
* Medium.js - Taking control of content editable
* @constructor
* @param {Object} [userSettings] user options
*/
Medium = function (userSettings) {
var medium = this,
action = new Medium.Action(),
cache = new Medium.Cache(),
cursor = new Medium.Cursor(),
html = new Medium.HtmlAssistant(),
utils = new Medium.Utilities(),
selection = new Medium.Selection(),
intercept = {
focus: function (e) {
e = e || w.event;
Medium.activeElement = el;
},
blur: function (e) {
e = e || w.event;
if (Medium.activeElement === el) {
Medium.activeElement = null;
}
html.placeholders();
},
down: function (e) {
e = e || w.event;
var keepEvent = true;
//in Chrome it sends out this event before every regular event, not sure why
if (e.keyCode === 229) return;
utils.isCommand(e, function () {
cache.cmd = true;
}, function () {
cache.cmd = false;
});
utils.isShift(e, function () {
cache.shift = true;
}, function () {
cache.shift = false;
});
utils.isModifier(e, function (cmd) {
if (cache.cmd) {
if (( (settings.mode === Medium.inlineMode) || (settings.mode === Medium.partialMode) ) && cmd !== "paste") {
utils.preventDefaultEvent(e);
return;
}
var cmdType = typeof cmd,
fn = null;
if (cmdType === "function") {
fn = cmd;
} else {
fn = intercept.command[cmd];
}
keepEvent = fn.call(medium, e);
if (keepEvent === false) {
utils.preventDefaultEvent(e);
utils.stopPropagation(e);
}
}
});
if (settings.maxLength !== -1) {
var len = html.text().length,
hasSelection = false,
selection = w.getSelection();
if (selection) {
hasSelection = !selection.isCollapsed;
}
if (len >= settings.maxLength && !utils.isSpecial(e) && !utils.isNavigational(e) && !hasSelection) {
return utils.preventDefaultEvent(e);
}
}
switch (e.keyCode) {
case key['enter']:
intercept.enterKey(e);
break;
case key['backspace']:
case key['delete']:
intercept.backspaceOrDeleteKey(e);
break;
}
return keepEvent;
},
up: function (e) {
e = e || w.event;
utils.isCommand(e, function () {
cache.cmd = false;
}, function () {
cache.cmd = true;
});
html.clean();
html.placeholders();
//here we have a key context, so if you need to create your own object within a specific context it is doable
var keyContext;
if (
settings.keyContext !== null
&& ( keyContext = settings.keyContext[e.keyCode] )
) {
var el = cursor.parent();
if (el) {
keyContext.call(medium, e, el);
}
}
action.preserveElementFocus();
},
command: {
bold: function (e) {
utils.preventDefaultEvent(e);
// IE uses strong instead of b
(new Medium.Element(medium, 'bold'))
.setClean(false)
.invoke(settings.beforeInvokeElement);
},
underline: function (e) {
utils.preventDefaultEvent(e);
(new Medium.Element(medium, 'underline'))
.setClean(false)
.invoke(settings.beforeInvokeElement);
},
italicize: function (e) {
utils.preventDefaultEvent(e);
(new Medium.Element(medium, 'italic'))
.setClean(false)
.invoke(settings.beforeInvokeElement);
},
quote: function (e) {
},
paste: function (e) {
medium.makeUndoable();
if (settings.pasteAsText) {
var sel = utils.selection.saveSelection();
utils.pasteHook(function (text) {
utils.selection.restoreSelection(sel);
text = text.replace(/\n/g, '<br>');
(new Medium.Html(medium, text))
.setClean(false)
.insert(settings.beforeInsertHtml, true);
html.clean();
html.placeholders();
});
} else {
html.clean();
html.placeholders();
}
}
},
enterKey: function (e) {
if (settings.mode === Medium.inlineMode) {
return utils.preventDefaultEvent(e);
}
if (!cache.shift) {
var focusedElement = html.atCaret() || {},
children = el.children,
lastChild = focusedElement === el.lastChild ? el.lastChild : null,
makeHR,
secondToLast,
paragraph;
if (
lastChild
&& lastChild !== el.firstChild
&& settings.autoHR
&& settings.mode !== 'partial'
&& settings.tags.horizontalRule
) {
utils.preventDefaultEvent(e);
makeHR =
html.text(lastChild) === ""
&& lastChild.nodeName.toLowerCase() === settings.tags.paragraph;
if (makeHR && children.length >= 2) {
secondToLast = children[children.length - 2];
if (secondToLast.nodeName.toLowerCase() === settings.tags.horizontalRule) {
makeHR = false;
}
}
if (makeHR) {
html.addTag(settings.tags.horizontalRule, false, true, focusedElement);
focusedElement = focusedElement.nextSibling;
}
if ((paragraph = html.addTag(settings.tags.paragraph, true, null, focusedElement)) !== null) {
paragraph.innerHTML = '';
cursor.set(0, paragraph);
}
}
}
return true;
},
backspaceOrDeleteKey: function (e) {
if (el.lastChild === null) return;
var lastChild = el.lastChild,
beforeLastChild = lastChild.previousSibling;
if (
lastChild
&& settings.tags.horizontalRule
&& lastChild.nodeName.toLocaleLowerCase() === settings.tags.horizontalRule
) {
el.removeChild(lastChild);
} else if (
lastChild
&& beforeLastChild
&& utils.html.text(lastChild).length < 1
&& beforeLastChild.nodeName.toLowerCase() === settings.tags.horizontalRule
&& lastChild.nodeName.toLowerCase() === settings.tags.paragraph
) {
el.removeChild(lastChild);
el.removeChild(beforeLastChild);
}
}
},
defaultSettings = {
element: null,
modifier: 'auto',
placeholder: "",
autofocus: false,
autoHR: true,
mode: Medium.richMode,
maxLength: -1,
modifiers: {
'b': 'bold',
'i': 'italicize',
'u': 'underline',
'v': 'paste'
},
tags: {
'break': 'br',
'horizontalRule': 'hr',
'paragraph': 'p',
'outerLevel': ['pre', 'blockquote', 'figure'],
'innerLevel': ['a', 'b', 'u', 'i', 'img', 'strong']
},
cssClasses: {
editor: 'Medium',
pasteHook: 'Medium-paste-hook',
placeholder: 'Medium-placeholder',
clear: 'Medium-clear'
},
attributes: {
remove: ['style', 'class']
},
pasteAsText: true,
beforeInvokeElement: function () {
//this = Medium.Element
},
beforeInsertHtml: function () {
//this = Medium.Html
},
beforeAddTag: function (tag, shouldFocus, isEditable, afterElement) {
},
keyContext: null,
pasteEventHandler: function (e) {
e = e || w.event;
medium.makeUndoable();
var length = medium.value().length,
totalLength;
if (settings.pasteAsText) {
utils.preventDefaultEvent(e);
var
sel = utils.selection.saveSelection(),
text = prompt(Medium.Messages.pastHere) || '';
if (text.length > 0) {
el.focus();
Medium.activeElement = el;
utils.selection.restoreSelection(sel);
//encode the text first
text = html.encodeHtml(text);
//cut down it's length
totalLength = text.length + length;
if (settings.maxLength > 0 && totalLength > settings.maxLength) {
text = text.substring(0, settings.maxLength - length);
}
if (settings.mode !== Medium.inlineMode) {
text = text.replace(/\n/g, '<br>');
}
(new Medium.Html(medium, text))
.setClean(false)
.insert(settings.beforeInsertHtml, true);
html.clean();
html.placeholders();
return false;
}
} else {
setTimeout(function () {
html.clean();
html.placeholders();
}, 20);
}
}
},
settings = utils.deepExtend(defaultSettings, userSettings),
el,
newVal,
i,
bridge = {};
for (i in defaultSettings) {
// Override defaults with data-attributes
if (
typeof defaultSettings[i] !== 'object'
&& defaultSettings.hasOwnProperty(i)
&& settings.element.getAttribute('data-medium-' + key)
) {
newVal = settings.element.getAttribute('data-medium-' + key);
if (newVal.toLowerCase() === "false" || newVal.toLowerCase() === "true") {
newVal = newVal.toLowerCase() === "true";
}
settings[i] = newVal;
}
}
if (settings.modifiers) {
for (i in settings.modifiers) {
if (typeof(key[i]) !== 'undefined') {
settings.modifiers[key[i]] = settings.modifiers[i];
}
}
}
if (settings.keyContext) {
for (i in settings.keyContext) {
if (typeof(key[i]) !== 'undefined') {
settings.keyContext[key[i]] = settings.keyContext[i];
}
}
}
// Extend Settings
el = settings.element;
// Editable
el.contentEditable = true;
el.className
+= (' ' + settings.cssClasses.editor)
+ (' ' + settings.cssClasses.editor + '-' + settings.mode);
settings.tags = (settings.tags || {});
if (settings.tags.outerLevel) {
settings.tags.outerLevel = settings.tags.outerLevel.concat([settings.tags.paragraph, settings.tags.horizontalRule]);
}
this.settings = settings;
this.element = el;
this.intercept = intercept;
this.action = action;
this.cache = cache;
this.cursor = cursor;
this.html = html;
this.utils = utils;
this.selection = selection;
bridge.element = el;
bridge.medium = this;
bridge.settings = settings;
bridge.action = action;
bridge.cache = cache;
bridge.cursor = cursor;
bridge.html = html;
bridge.intercept = intercept;
bridge.utils = utils;
bridge.selection = selection;
action.setBridge(bridge);
cache.setBridge(bridge);
cursor.setBridge(bridge);
html.setBridge(bridge);
utils.setBridge(bridge);
selection.setBridge(bridge);
// Initialize editor
html.clean();
html.placeholders();
action.preserveElementFocus();
// Capture Events
action.listen();
if (wild) {
this.makeUndoable = function () {
};
} else {
this.dirty = false;
this.undoable = new Medium.Undoable(this);
this.undo = this.undoable.undo;
this.redo = this.undoable.redo;
this.makeUndoable = this.undoable.makeUndoable;
}
el.medium = this;
// Set as initialized
cache.initialized = true;
};
Medium.prototype = {
/**
*
* @param {String|Object} html
* @param {Function} [callback]
* @returns {Medium}
*/
insertHtml: function (html, callback) {
var result = (new Medium.Html(this, html))
.insert(this.settings.beforeInsertHtml);
this.utils.triggerEvent(this.element, "change");
if (callback) {
callback.apply(result);
}
return this;
},
/**
*
* @param {String} tagName
* @param {Object} [attributes]
* @returns {Medium}
*/
invokeElement: function (tagName, attributes) {
var settings = this.settings,
attributes = attributes || {},
remove = attributes.remove || [];
switch (settings.mode) {
case Medium.inlineMode:
case Medium.partialMode:
return this;
default:
}
//invoke works off class, so if it isn't there, we just add it
if (remove.length > 0) {
if (!arrayContains(settings, 'class')) {
remove.push('class');
}
}
(new Medium.Element(this, tagName, attributes))
.invoke(this.settings.beforeInvokeElement);
this.utils.triggerEvent(this.element, "change");
return this;
},
/**
* @returns {string}
*/
behavior: function () {
return (wild ? 'wild' : 'domesticated');
},
/**
*
* @param value
* @returns {Medium}
*/
value: function (value) {
if (typeof value !== 'undefined') {
this.element.innerHTML = value;
this.html.clean();
this.html.placeholders();
} else {
return this.element.innerHTML;
}
return this;
},
/**
* Focus on element
* @returns {Medium}
*/
focus: function () {
var el = this.element;
el.focus();
return this;
},
/**
* Select all text
* @returns {Medium}
*/
select: function () {
var el = this.element,
range,
selection;
el.focus();
if (d.body.createTextRange) {
range = d.body.createTextRange();
range.moveToElementText(el);
range.select();
} else if (w.getSelection) {
selection = w.getSelection();
range = d.createRange();
range.selectNodeContents(el);
selection.removeAllRanges();
selection.addRange(range);
}
return this;
},
isActive: function () {
return (Medium.activeElement === this.element);
},
destroy: function () {
var el = this.element,
intercept = this.intercept,
settings = this.settings,
placeholder = this.placeholder || null;
if (placeholder !== null && placeholder.setup) {
//remove placeholder
placeholder.parentNode.removeChild(placeholder);
delete el.placeHolderActive;
}
//remove contenteditable
el.removeAttribute('contenteditable');
//remove classes
el.className = trim(el.className
.replace(settings.cssClasses.editor, '')
.replace(settings.cssClasses.clear, '')
.replace(settings.cssClasses.editor + '-' + settings.mode, ''));
//remove events
this.utils
.removeEvent(el, 'keyup', intercept.up)
.removeEvent(el, 'keydown', intercept.down)
.removeEvent(el, 'focus', intercept.focus)
.removeEvent(el, 'blur', intercept.focus)
.removeEvent(el, 'paste', settings.pasteEventHandler);
},
// Clears the element and restores the placeholder
clear: function () {
this.element.innerHTML = '';
this.html.placeholders();
}
};
/**
* @param {Medium} medium
* @param {String} tagName
* @param {Object} [attributes]
* @constructor
*/
Medium.Element = function (medium, tagName, attributes) {
this.medium = medium;
this.element = medium.settings.element;
if (wild) {
this.tagName = tagName;
} else {
switch (tagName.toLowerCase()) {
case 'bold':
this.tagName = 'b';
break;
case 'italic':
this.tagName = 'i';
break;
case 'underline':
this.tagName = 'u';
break;
default:
this.tagName = tagName;
}
}
this.attributes = attributes || {};
this.clean = true;
};
/**
* @constructor
* @param {Medium} medium
* @param {String|HtmlElement} html
*/
Medium.Html = function (medium, html) {
this.medium = medium;
this.element = medium.settings.element;
this.html = html;
this.clean = true;
};
/**
*
* @constructor
*/
Medium.Injector = function () {
};
if (wild) {
Medium.Element.prototype = {
/**
* @methodOf Medium.Element
* @param {Function} [fn]
*/
invoke: function (fn) {
if (Medium.activeElement === this.element) {
if (fn) {
fn.apply(this);
}
d.execCommand(this.tagName, false);
}
},
setClean: function () {
return this;
}
};
Medium.Injector.prototype = {
/**
* @methodOf Medium.Injector
* @param {String|HtmlElement} htmlRaw
* @param {Boolean} [selectInserted]
* @returns {null}
*/
inject: function (htmlRaw, selectInserted) {
this.insertHTML(htmlRaw, selectInserted);
return null;
}
};
/**
*
* @constructor
*/
Medium.Undoable = function () {
};
}
//if medium is domesticated (ie, not wild)
else {
rangy.rangePrototype.insertNodeAtEnd = function (node) {
var range = this.cloneRange();
range.collapse(false);
range.insertNode(node);
range.detach();
this.setEndAfter(node);
};
Medium.Element.prototype = {
/**
* @methodOf Medium.Element
* @param {Function} [fn]
*/
invoke: function (fn) {
if (Medium.activeElement === this.element) {
if (fn) {
fn.apply(this);
}
var
attr = this.attributes,
tagName = this.tagName.toLowerCase(),
applier,
cl;
if (attr.className !== undefined) {
cl = (attr.className.split[' '] || [attr.className]).shift();
delete attr.className;
} else {
cl = 'medium-' + tagName;
}
applier = rangy.createClassApplier(cl, {
elementTagName: tagName,
elementAttributes: this.attributes
});
this.medium.makeUndoable();
applier.toggleSelection(w);
if (this.clean) {
//cleanup
this.medium.html.clean();
this.medium.html.placeholders();
}
}
},
/**
*
* @param {Boolean} clean
* @returns {Medium.Element}
*/
setClean: function (clean) {
this.clean = clean;
return this;
}
};
Medium.Injector.prototype = {
/**
* @methodOf Medium.Injector
* @param {String|HtmlElement} htmlRaw
* @returns {HtmlElement}
*/
inject: function (htmlRaw) {
var html, isConverted = false;
if (typeof htmlRaw === 'string') {
var htmlConverter = d.createElement('div');
htmlConverter.innerHTML = htmlRaw;
html = htmlConverter.childNodes;
isConverted = true;
} else {
html = htmlRaw;
}
this.insertHTML('<span id="wedge"></span>');
var wedge = d.getElementById('wedge'),
parent = wedge.parentNode,
i = 0;
wedge.removeAttribute('id');
if (isConverted) {
while (i < html.length) {
parent.insertBefore(html[i], wedge);
}
} else {
parent.insertBefore(html, wedge);
}
parent.removeChild(wedge);
wedge = null;
return html;
}
};
/**
* @param {Medium} medium
* @constructor
*/
Medium.Undoable = function (medium) {
var me = this,
element = medium.settings.element,
utils = medium.utils,
addEvent = utils.addEvent,
startValue = element.innerHTML,
timer,
stack = new Undo.Stack(),
EditCommand = Undo.Command.extend({
constructor: function (oldValue, newValue) {
this.oldValue = oldValue;
this.newValue = newValue;
},
execute: function () {
},
undo: function () {
element.innerHTML = this.oldValue;
medium.canUndo = stack.canUndo();
medium.canRedo = stack.canRedo();
medium.dirty = stack.dirty();
},
redo: function () {
element.innerHTML = this.newValue;
medium.canUndo = stack.canUndo();
medium.canRedo = stack.canRedo();
medium.dirty = stack.dirty();
}
}),
makeUndoable = function () {
var newValue = element.innerHTML;
// ignore meta key presses
if (newValue != startValue) {
if (!me.movingThroughStack) {
// this could try and make a diff instead of storing snapshots
stack.execute(new EditCommand(startValue, newValue));
startValue = newValue;
medium.dirty = stack.dirty();
}
utils.triggerEvent(medium.settings.element, "change");
}
};
this.medium = medium;
this.timer = timer;
this.stack = stack;
this.makeUndoable = makeUndoable;
this.EditCommand = EditCommand;
this.movingThroughStack = false;
addEvent(element, 'keyup', function (e) {
if (e.ctrlKey || e.keyCode === key.z) {
utils.preventDefaultEvent(e);
return;
}
// a way too simple algorithm in place of single-character undo
clearTimeout(timer);
timer = setTimeout(function () {
makeUndoable();
}, 250);
});
addEvent(element, 'keydown', function (e) {
if (!e.ctrlKey || e.keyCode !== key.z) {
me.movingThroughStack = false;
return true;
}
utils.preventDefaultEvent(e);
me.movingThroughStack = true;
if (e.shiftKey) {
stack.canRedo() && stack.redo()
} else {
stack.canUndo() && stack.undo();
}
});
};
}
//Thank you Tim Down (super uber genius): http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
Medium.Injector.prototype.insertHTML = function (html, selectPastedContent) {
var sel, range;
if (w.getSelection) {
// IE9 and non-IE
sel = w.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
// Range.createContextualFragment() would be useful here but is
// only relatively recently standardized and is not supported in
// some browsers (IE9, for one)
var el = d.createElement("div");
el.innerHTML = html;
var frag = d.createDocumentFragment(), node, lastNode;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
var firstNode = frag.firstChild;
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
if (selectPastedContent) {
range.setStartBefore(firstNode);
} else {
range.collapse(true);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
} else if ((sel = d.selection) && sel.type != "Control") {
// IE < 9
var originalRange = sel.createRange();
originalRange.collapse(true);
sel.createRange().pasteHTML(html);
if (selectPastedContent) {
range = sel.createRange();
range.setEndPoint("StartToStart", originalRange);
range.select();
}
}
};
Medium.Html.prototype = {
setBridge: function (bridge) {
for (var i in bridge) {
this[i] = bridge[i];
}
},
/**
* @methodOf Medium.Html
* @param {Function} [fn]
* @param {Boolean} [selectInserted]
* @returns {HtmlElement}
*/
insert: function (fn, selectInserted) {
if (Medium.activeElement === this.element) {
if (fn) {
fn.apply(this);
}
var inserted = this.injector.inject(this.html, selectInserted);
if (this.clean) {
//cleanup
this.medium.html.clean();
this.medium.html.placeholders();
}
this.medium.makeUndoable();
return inserted;
} else {
return null;
}
},
/**
* @attributeOf {Medium.Injector} Medium.Html
*/
injector: new Medium.Injector(),
/**
* @methodOf Medium.Html
* @param clean
* @returns {Medium.Html}
*/
setClean: function (clean) {
this.clean = clean;
return this;
}
};
Medium.Utilities = function () {
};
Medium.Utilities.prototype = {
setBridge: function (bridge) {
for (var i in bridge) {
this[i] = bridge[i];
}
},
/*
* Keyboard Interface events
*/
isCommand: function (e, fnTrue, fnFalse) {
var s = this.settings;
if ((s.modifier === 'ctrl' && e.ctrlKey ) ||
(s.modifier === 'cmd' && e.metaKey ) ||
(s.modifier === 'auto' && (e.ctrlKey || e.metaKey) )
) {
return fnTrue.call();
} else {
return fnFalse.call();
}
},
isShift: function (e, fnTrue, fnFalse) {
if (e.shiftKey) {
return fnTrue.call();
} else {
return fnFalse.call();
}
},
isModifier: function (e, fn) {
var cmd = this.settings.modifiers[e.keyCode];
if (cmd) {
return fn.call(null, cmd);
}
return false;
},
special: (function () {
var special = {};
special[key['backspace']] = true;
special[key['shift']] = true;
special[key['ctrl']] = true;
special[key['alt']] = true;
special[key['delete']] = true;
special[key['cmd']] = true;
return special;
})(),
isSpecial: function (e) {
if (this.cache.cmd) {
return true;
}
return typeof this.special[e.keyCode] !== 'undefined';
},
navigational: (function () {
var navigational = {};
navigational[key['upArrow']] = true;
navigational[key['downArrow']] = true;
navigational[key['leftArrow']] = true;
navigational[key['rightArrow']] = true;
return navigational;
})(),
isNavigational: function (e) {
return typeof this.navigational[e.keyCode] !== 'undefined';
},
/*
* Handle Events
*/
addEvent: function addEvent(element, eventName, func) {
if (element.addEventListener) {
element.addEventListener(eventName, func, false);
} else if (element.attachEvent) {
element.attachEvent("on" + eventName, func);
} else {
element['on' + eventName] = func;
}
return this;
},
removeEvent: function removeEvent(element, eventName, func) {
if (element.removeEventListener) {
element.removeEventListener(eventName, func, false);
} else if (element.detachEvent) {
element.detachEvent("on" + eventName, func);
} else {
element['on' + eventName] = null;
}
return this;
},
preventDefaultEvent: function (e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
return this;
},
stopPropagation: function (e) {
e = e || window.event;
e.cancelBubble = true;
if (e.stopPropagation !== undefined) {
e.stopPropagation();
}
},
triggerEvent: function (element, eventName) {
var e;
if (d.createEvent) {
e = d.createEvent("HTMLEvents");
e.initEvent(eventName, true, true);
e.eventName = eventName;
element.dispatchEvent(e);
} else {
e = d.createEventObject();
element.fireEvent("on" + eventName, e);
}
return this;
},
deepExtend: function (destination, source) {
for (var property in source) {
if (
source[property]
&& source[property].constructor
&& source[property].constructor === Object
) {
destination[property] = destination[property] || {};
this.deepExtend(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
},
/*
* This is a Paste Hook. When the user pastes
* content, this ultimately converts it into
* plain text before inserting the data.
*/
pasteHook: function (fn) {
var textarea = d.createElement('textarea'),
el = this.element,
existingValue,
existingLength,
overallLength,
s = this.settings,
medium = this.medium,
html = this.html;
textarea.className = s.cssClasses.pasteHook;
el.parentNode.appendChild(textarea);
textarea.focus();
if (!wild) {
medium.makeUndoable();
}
setTimeout(function () {
el.focus();
if (s.maxLength > 0) {
existingValue = html.text(el);
existingLength = existingValue.length;
overallLength = existingLength + textarea.value.length;
if (overallLength > existingLength) {
textarea.value = textarea.value.substring(0, s.maxLength - existingLength);
}
}
fn(textarea.value);
html.deleteNode(textarea);
}, 2);
},
setupContents: function () {
var el = this.element,
children = el.children,
childNodes = el.childNodes,
initialParagraph;
if (
!this.settings.tags.paragraph
|| children.length > 0
|| this.settings.mode === Medium.inlineMode
) {
return;
}
//has content, but no children
if (childNodes.length > 0) {
initialParagraph = d.createElement(this.settings.tags.paragraph);
if (el.innerHTML.match('^[&]nbsp[;]')) {
el.innerHTML = el.innerHTML.substring(6, el.innerHTML.length - 1);
}
initialParagraph.innerHTML = el.innerHTML;
el.innerHTML = '';
el.appendChild(initialParagraph);
this.cursor.set(initialParagraph.innerHTML.length, initialParagraph);
} else {
initialParagraph = d.createElement(this.settings.tags.paragraph);
initialParagraph.innerHTML = '&nbsp;';
el.appendChild(initialParagraph);
}
},
traverseAll: function (element, options, depth) {
var children = element.childNodes,
length = children.length,
i = 0,
node,
depth = depth || 1;
options = options || {};
if (length > 0) {
for (; i < length; i++) {
node = children[i];
switch (node.nodeType) {
case 1:
this.traverseAll(node, options, depth + 1);
if (options.element !== undefined) options.element(node, i, depth, element);
break;
case 3:
if (options.fragment !== undefined) options.fragment(node, i, depth, element);
}
//length may change
length = children.length;
//if length did change, and we are at the last item, this causes infinite recursion, so if we are at the last item, then stop to prevent this
if (node === element.lastChild) {
i = length;
}
}
}
}
};
/*
* Handle Selection Logic
*/
Medium.Selection = function () {
};
Medium.Selection.prototype = {
setBridge: function (bridge) {
for (var i in bridge) {
this[i] = bridge[i];
}
},
saveSelection: function () {
if (w.getSelection) {
var sel = w.getSelection();
if (sel.rangeCount > 0) {
return sel.getRangeAt(0);
}
} else if (d.selection && d.selection.createRange) { // IE
return d.selection.createRange();
}
return null;
},
restoreSelection: function (range) {
if (range) {
if (w.getSelection) {
var sel = w.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (d.selection && range.select) { // IE
range.select();
}
}
}
};
/*
* Handle Cursor Logic
*/
Medium.Cursor = function () {
};
Medium.Cursor.prototype = {
setBridge: function (bridge) {
for (var i in bridge) {
this[i] = bridge[i];
}
},
set: function (pos, el) {
var range,
html = this.html;
if (d.createRange) {
var selection = w.getSelection(),
lastChild = html.lastChild(),
length = html.text(lastChild).length - 1,
toModify = el ? el : lastChild,
theLength = ((typeof pos !== 'undefined') && (pos !== null) ? pos : length);
range = d.createRange();
try{
range.setStart(toModify, theLength);
}
catch(e){};
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
} else {
range = d.body.createTextRange();
range.moveToElementText(el);
range.collapse(false);
range.select();
}
},
parent: function () {
var target = null, range;
if (w.getSelection) {
range = w.getSelection().getRangeAt(0);
target = range.commonAncestorContainer;
target = (target.nodeType === 1
? target
: target.parentNode
);
}
else if (d.selection) {
target = d.selection.createRange().parentElement();
}
if (target.tagName == 'SPAN') {
target = target.parentNode;
}
return target;
},
caretToBeginning: function (el) {
this.set(0, el);
},
caretToEnd: function (el) {
this.set(this.html.text(el).length, el);
}
};
/*
* HTML Abstractions
*/
Medium.HtmlAssistant = function () {
};
Medium.HtmlAssistant.prototype = {
setBridge: function (bridge) {
for (var i in bridge) {
this[i] = bridge[i];
}
},
encodeHtml: function (html) {
return d.createElement('a').appendChild(
d.createTextNode(html)).parentNode.innerHTML;
},
text: function (node, val) {
node = node || this.settings.element;
if (val) {
if ((node.textContent) && (typeof (node.textContent) != "undefined")) {
node.textContent = val;
} else {
node.innerText = val;
}
}
else if (node.innerText) {
return trim(node.innerText);
}
else if (node.textContent) {
return trim(node.textContent);
}
//document fragment
else if (node.data) {
return trim(node.data);
}
//for good measure
return '';
},
changeTag: function (oldNode, newTag) {
var newNode = d.createElement(newTag),
node,
nextNode;
node = oldNode.firstChild;
while (node) {
nextNode = node.nextSibling;
newNode.appendChild(node);
node = nextNode;
}
oldNode.parentNode.insertBefore(newNode, oldNode);
oldNode.parentNode.removeChild(oldNode);
return newNode;
},
deleteNode: function (el) {
el.parentNode.removeChild(el);
},
placeholders: function () {
//in IE8, just gracefully degrade to no placeholders
if (!w.getComputedStyle) return;
var that = this,
s = this.settings,
placeholder = this.medium.placeholder || (this.medium.placeholder = d.createElement('div')),
el = s.element,
style = placeholder.style,
elStyle = w.getComputedStyle(el, null),
qStyle = function (prop) {
return elStyle.getPropertyValue(prop)
},
utils = this.utils,
text = utils.html.text(el),
cursor = this.cursor,
childCount = el.children.length;
el.placeholder = placeholder;
// Empty Editor
if (text.length < 1 && childCount < 2) {
if (el.placeHolderActive) return;
if (!el.innerHTML.match('<' + s.tags.paragraph)) {
el.innerHTML = '';
}
// We need to add placeholders
if (s.placeholder.length > 0) {
if (!placeholder.setup) {
placeholder.setup = true;
//background & background color
style.background = qStyle('background');
style.backgroundColor = qStyle('background-color');
//text size & text color
style.fontSize = qStyle('font-size');
style.color = elStyle.color;
//begin box-model
//margin
style.marginTop = qStyle('margin-top');
style.marginBottom = qStyle('margin-bottom');
style.marginLeft = qStyle('margin-left');
style.marginRight = qStyle('margin-right');
//padding
style.paddingTop = qStyle('padding-top');
style.paddingBottom = qStyle('padding-bottom');
style.paddingLeft = qStyle('padding-left');
style.paddingRight = qStyle('padding-right');
//border
style.borderTopWidth = qStyle('border-top-width');
style.borderTopColor = qStyle('border-top-color');
style.borderTopStyle = qStyle('border-top-style');
style.borderBottomWidth = qStyle('border-bottom-width');
style.borderBottomColor = qStyle('border-bottom-color');
style.borderBottomStyle = qStyle('border-bottom-style');
style.borderLeftWidth = qStyle('border-left-width');
style.borderLeftColor = qStyle('border-left-color');
style.borderLeftStyle = qStyle('border-left-style');
style.borderRightWidth = qStyle('border-right-width');
style.borderRightColor = qStyle('border-right-color');
style.borderRightStyle = qStyle('border-right-style');
//end box model
//element setup
placeholder.className = s.cssClasses.placeholder + ' ' + s.cssClasses.placeholder + '-' + s.mode;
placeholder.innerHTML = '<div>' + s.placeholder + '</div>';
el.parentNode.insertBefore(placeholder, el);
}
el.className += ' ' + s.cssClasses.clear;
style.display = '';
// Add base P tag and do auto focus, give it a min height if el has one
style.minHeight = el.clientHeight + 'px';
style.minWidth = el.clientWidth + 'px';
if (s.mode !== Medium.inlineMode) {
utils.setupContents();
if (childCount === 0 && el.firstChild) {
cursor.set(0, el.firstChild);
}
}
}
el.placeHolderActive = true;
} else if (el.placeHolderActive) {
el.placeHolderActive = false;
style.display = 'none';
el.className = trim(el.className.replace(s.cssClasses.clear, ''));
utils.setupContents();
}
},
/**
* Cleans element
* @param {HtmlElement} [el] default is settings.element
*/
clean: function (el) {
/*
* Deletes invalid nodes
* Removes Attributes
*/
var s = this.settings,
placeholderClass = s.cssClasses.placeholder,
attributesToRemove = (s.attributes || {}).remove || [],
tags = s.tags || {},
onlyOuter = tags.outerLevel || null,
onlyInner = tags.innerLevel || null,
outerSwitch = {},
innerSwitch = {},
paragraphTag = (tags.paragraph || '').toUpperCase(),
html = this.html,
attr,
text,
j;
el = el || s.element;
if (onlyOuter !== null) {
for (j = 0; j < onlyOuter.length; j++) {
outerSwitch[onlyOuter[j].toUpperCase()] = true;
}
}
if (onlyInner !== null) {
for (j = 0; j < onlyInner.length; j++) {
innerSwitch[onlyInner[j].toUpperCase()] = true;
}
}
this.utils.traverseAll(el, {
element: function (child, i, depth, parent) {
var nodeName = child.nodeName,
shouldDelete = true;
// Remove attributes
for (j = 0; j < attributesToRemove.length; j++) {
attr = attributesToRemove[j];
if (child.hasAttribute(attr)) {
if (child.getAttribute(attr) !== placeholderClass) {
child.removeAttribute(attr);
}
}
}
if (onlyOuter === null && onlyInner === null) {
return;
}
if (depth === 1 && outerSwitch[nodeName] !== undefined) {
shouldDelete = false;
} else if (depth > 1 && innerSwitch[nodeName] !== undefined) {
shouldDelete = false;
}
// Convert tags or delete
if (shouldDelete) {
if (w.getComputedStyle(child, null).getPropertyValue('display') === 'block') {
if (paragraphTag.length > 0 && paragraphTag !== nodeName) {
html.changeTag(child, paragraphTag);
}
if (depth > 1) {
while (parent.childNodes.length > i) {
parent.parentNode.insertBefore(parent.lastChild, parent.nextSibling);
}
}
} else {
switch (nodeName) {
case 'BR':
if (child === child.parentNode.lastChild) {
if (child === child.parentNode.firstChild) {
break;
}
text = document.createTextNode("");
text.innerHTML = '&nbsp';
parent.insertBefore(text, child);
break;
}
default:
while (child.firstChild !== null) {
parent.insertBefore(child.firstChild, child);
}
html.deleteNode(child);
break;
}
}
}
}
});
},
lastChild: function () {
return this.element.lastChild;
},
addTag: function (tag, shouldFocus, isEditable, afterElement) {
if (!this.settings.beforeAddTag(tag, shouldFocus, isEditable, afterElement)) {
var newEl = d.createElement(tag),
toFocus;
if (typeof isEditable !== "undefined" && isEditable === false) {
newEl.contentEditable = false;
}
if (newEl.innerHTML.length == 0) {
newEl.innerHTML = ' ';
}
if (afterElement && afterElement.nextSibling) {
afterElement.parentNode.insertBefore(newEl, afterElement.nextSibling);
toFocus = afterElement.nextSibling;
} else {
this.settings.element.appendChild(newEl);
toFocus = this.html.lastChild();
}
if (shouldFocus) {
this.cache.focusedElement = toFocus;
this.cursor.set(0, toFocus);
}
return newEl;
}
return null;
},
baseAtCaret: function () {
if (!this.medium.isActive()) return null;
var sel = w.getSelection ? w.getSelection() : document.selection;
if (sel.rangeCount) {
var selRange = sel.getRangeAt(0),
container = selRange.endContainer;
switch (container.nodeType) {
case 3:
if (container.data && container.data.length != selRange.endOffset) return false;
break;
}
return container;
}
return null;
},
atCaret: function () {
var container = this.baseAtCaret() || {},
el = this.element;
if (container === false) return null;
while (container && container.parentNode !== el) {
container = container.parentNode;
}
if (container && container.nodeType == 1) {
return container;
}
return null;
}
};
Medium.Action = function () {
};
Medium.Action.prototype = {
setBridge: function (bridge) {
for (var i in bridge) {
this[i] = bridge[i];
}
},
listen: function () {
var el = this.element,
intercept = this.intercept;
this.utils
.addEvent(el, 'keyup', intercept.up)
.addEvent(el, 'keydown', intercept.down)
.addEvent(el, 'focus', intercept.focus)
.addEvent(el, 'blur', intercept.blur)
.addEvent(el, 'paste', this.settings.pasteEventHandler);
},
preserveElementFocus: function () {
// Fetch node that has focus
var anchorNode = w.getSelection ? w.getSelection().anchorNode : d.activeElement;
if (anchorNode) {
var cache = this.medium.cache,
s = this.settings,
cur = anchorNode.parentNode,
children = s.element.children,
diff = cur !== cache.focusedElement,
elementIndex = 0,
i;
// anchorNode is our target if element is empty
if (cur === s.element) {
cur = anchorNode;
}
// Find our child index
for (i = 0; i < children.length; i++) {
if (cur === children[i]) {
elementIndex = i;
break;
}
}
// Focused element is different
if (diff) {
cache.focusedElement = cur;
cache.focusedElementIndex = elementIndex;
}
}
}
};
Medium.Cache = function () {
this.initialized = false;
this.cmd = false;
this.focusedElement = null
};
Medium.Cache.prototype = {
setBridge: function (bridge) {
for (var i in bridge) {
this[i] = bridge[i];
}
}
};
//Modes;
Medium.inlineMode = 'inline';
Medium.partialMode = 'partial';
Medium.richMode = 'rich';
Medium.Messages = {
pastHere: 'Paste Here'
};
return Medium;
}());
if (typeof define === 'function' && define['amd']) {
define(function () { return Medium; });
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = Medium;
} else if (typeof this !== 'undefined') {
this.Medium = Medium;
}
}).call(this, window, document);