diff --git a/Gruntfile.js b/Gruntfile.js
index d3ad948f..94fda317 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -84,12 +84,12 @@ module.exports = function(grunt) {
},
tinyice: {
files: {
- 'dist/ice_editor_plugin.js': 'lib/tinymce/jscripts/tiny_mce/plugins/ice/editor_plugin.js'
+ 'dist/ice_editor_plugin.js': 'lib/tinymce/js/tinymce/plugins/ice/plugin.min.js'
}
},
tinysr: {
files: {
- 'dist/sr_editor_plugin.js': 'lib/tinymce/jscripts/tiny_mce/plugins/icesearchreplace/editor_plugin.js'
+ 'dist/sr_editor_plugin.js': 'lib/tinymce/js/tinymce/plugins/icesearchreplace/plugin.min.js'
}
}
},
@@ -139,7 +139,7 @@ module.exports = function(grunt) {
});
var cpTinyDir = function(dir) {
- grunt.file.recurse('lib/tinymce/jscripts/tiny_mce/plugins/' + dir + '/', function(abspath, rootdir, subdir, filename) {
+ grunt.file.recurse('lib/tinymce/js/tinymce/plugins/' + dir + '/', function(abspath, rootdir, subdir, filename) {
grunt.file.copy(rootdir + '/' + (subdir ? subdir + '/' : '') + filename,'dist/tinymce/plugins/' + dir + '/' + (subdir ? subdir + '/' : '') + '/' + filename);
});
};
diff --git a/demo/ice/bookmark.js b/demo/ice/bookmark.js
new file mode 100644
index 00000000..73690efe
--- /dev/null
+++ b/demo/ice/bookmark.js
@@ -0,0 +1,166 @@
+(function() {
+
+ var exports = this, Bookmark;
+
+ Bookmark = function(env, range, keepOldBookmarks) {
+
+ this.env = env;
+ this.element = env.element;
+ this.selection = this.env.selection;
+
+ // Remove all bookmarks?
+ if (!keepOldBookmarks) {
+ this.removeBookmarks(this.element);
+ }
+
+ var currRange = range || this.selection.getRangeAt(0);
+ range = currRange.cloneRange();
+ var startContainer = range.startContainer;
+ var endContainer = range.endContainer;
+ var startOffset = range.startOffset;
+ var endOffset = range.endOffset;
+ var tmp;
+
+ // Collapse to the end of range.
+ range.collapse(false);
+
+ var endBookmark = this.env.document.createElement('span');
+ endBookmark.style.display = 'none';
+ ice.dom.setHtml(endBookmark, ' ');
+ ice.dom.addClass(endBookmark, 'iceBookmark iceBookmark_end');
+ endBookmark.setAttribute('iceBookmark', 'end');
+ range.insertNode(endBookmark);
+ if(!ice.dom.isChildOf(endBookmark, this.element)) {
+ this.element.appendChild(endBookmark);
+ }
+
+ // Move the range to where it was before.
+ range.setStart(startContainer, startOffset);
+ range.collapse(true);
+
+ // Create the start bookmark.
+ var startBookmark = this.env.document.createElement('span');
+ startBookmark.style.display = 'none';
+ ice.dom.addClass(startBookmark, 'iceBookmark iceBookmark_start');
+ ice.dom.setHtml(startBookmark, ' ');
+ startBookmark.setAttribute('iceBookmark', 'start');
+ try {
+ range.insertNode(startBookmark);
+
+ // Make sure start and end are in correct position.
+ if (startBookmark.previousSibling === endBookmark) {
+ // Reverse..
+ tmp = startBookmark;
+ startBookmark = endBookmark;
+ endBookmark = tmp;
+ }
+ } catch (e) {
+ // NS_ERROR_UNEXPECTED: I believe this is a Firefox bug.
+ // It seems like if the range is collapsed and the text node is empty
+ // (i.e. length = 0) then Firefox tries to split the node for no reason and fails...
+ ice.dom.insertBefore(endBookmark, startBookmark);
+ }
+
+ if (ice.dom.isChildOf(startBookmark, this.element) === false) {
+ if (this.element.firstChild) {
+ ice.dom.insertBefore(this.element.firstChild, startBookmark);
+ } else {
+ // Should not happen...
+ this.element.appendChild(startBookmark);
+ }
+ }
+
+ if (!endBookmark.previousSibling) {
+ tmp = this.env.document.createTextNode('');
+ ice.dom.insertBefore(endBookmark, tmp);
+ }
+
+ // The original range object must be changed.
+ if (!startBookmark.nextSibling) {
+ tmp = this.env.document.createTextNode('');
+ ice.dom.insertAfter(startBookmark, tmp);
+ }
+
+ currRange.setStart(startBookmark.nextSibling, 0);
+ currRange.setEnd(endBookmark.previousSibling, (endBookmark.previousSibling.length || 0));
+
+ this.start = startBookmark;
+ this.end = endBookmark;
+ };
+
+ Bookmark.prototype = {
+
+ selectBookmark: function() {
+ var range = this.selection.getRangeAt(0);
+ var startPos = null;
+ var endPos = null;
+ var startOffset = 0;
+ var endOffset = null;
+ if (this.start.nextSibling === this.end || ice.dom.getElementsBetween(this.start, this.end).length === 0) {
+ // Bookmark is collapsed.
+ if (this.end.nextSibling) {
+ startPos = ice.dom.getFirstChild(this.end.nextSibling);
+ } else if (this.start.previousSibling) {
+ startPos = ice.dom.getFirstChild(this.start.previousSibling);
+ if (startPos.nodeType === ice.dom.TEXT_NODE) {
+ startOffset = startPos.length;
+ }
+ } else {
+ // Create a text node in parent.
+ this.end.parentNode.appendChild(this.env.document.createTextNode(''));
+ startPos = ice.dom.getFirstChild(this.end.nextSibling);
+ }
+ } else {
+ if (this.start.nextSibling) {
+ startPos = ice.dom.getFirstChild(this.start.nextSibling);
+ } else {
+ if (!this.start.previousSibling) {
+ var tmp = this.env.document.createTextNode('');
+ ice.dom.insertBefore(this.start, tmp);
+ }
+
+ startPos = ice.dom.getLastChild(this.start.previousSibling);
+ startOffset = startPos.length;
+ }
+
+ if (this.end.previousSibling) {
+ endPos = ice.dom.getLastChild(this.end.previousSibling);
+ } else {
+ endPos = ice.dom.getFirstChild(this.end.nextSibling || this.end);
+ endOffset = 0;
+ }
+ }
+
+ ice.dom.remove([this.start, this.end]);
+
+ if (endPos === null) {
+ range.setEnd(startPos, startOffset);
+ range.collapse(false);
+ } else {
+ range.setStart(startPos, startOffset);
+ if (endOffset === null) {
+ endOffset = (endPos.length || 0);
+ }
+ range.setEnd(endPos, endOffset);
+ }
+
+ try {
+ this.selection.addRange(range);
+ } catch (e) {
+ // IE may throw exception for hidden elements..
+ }
+ },
+
+ getBookmark: function(parent, type) {
+ var elem = ice.dom.getClass('iceBookmark_' + type, parent)[0];
+ return elem;
+ },
+
+ removeBookmarks: function(elem) {
+ ice.dom.remove(ice.dom.getClass('iceBookmark', elem, 'span'));
+ }
+ };
+
+ exports.Bookmark = Bookmark;
+
+}).call(this.ice);
\ No newline at end of file
diff --git a/demo/ice/dom.js b/demo/ice/dom.js
new file mode 100644
index 00000000..c1d4eb2f
--- /dev/null
+++ b/demo/ice/dom.js
@@ -0,0 +1,998 @@
+(function () {
+ var exports = this,
+ dom = {};
+
+ dom.DOM_VK_DELETE = 8;
+ dom.DOM_VK_LEFT = 37;
+ dom.DOM_VK_UP = 38;
+ dom.DOM_VK_RIGHT = 39;
+ dom.DOM_VK_DOWN = 40;
+ dom.DOM_VK_ENTER = 13;
+ dom.ELEMENT_NODE = 1;
+ dom.ATTRIBUTE_NODE = 2;
+ dom.TEXT_NODE = 3;
+ dom.CDATA_SECTION_NODE = 4;
+ dom.ENTITY_REFERENCE_NODE = 5;
+ dom.ENTITY_NODE = 6;
+ dom.PROCESSING_INSTRUCTION_NODE = 7;
+ dom.COMMENT_NODE = 8;
+ dom.DOCUMENT_NODE = 9;
+ dom.DOCUMENT_TYPE_NODE = 10;
+ dom.DOCUMENT_FRAGMENT_NODE = 11;
+ dom.NOTATION_NODE = 12;
+ dom.CHARACTER_UNIT = 'character';
+ dom.WORD_UNIT = 'word';
+ dom.BREAK_ELEMENT = 'br';
+ dom.CONTENT_STUB_ELEMENTS = ['img', 'hr', 'iframe', 'param', 'link', 'meta', 'input', 'frame', 'col', 'base', 'area'];
+ dom.BLOCK_ELEMENTS = ['p', 'div', 'pre', 'ul', 'ol', 'li', 'table', 'tbody', 'td', 'th', 'fieldset', 'form', 'blockquote', 'dl', 'dt', 'dd', 'dir', 'center', 'address', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
+ dom.TEXT_CONTAINER_ELEMENTS = ['p', 'div', 'pre', 'li', 'td', 'th', 'blockquote', 'dt', 'dd', 'center', 'address', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
+
+ dom.STUB_ELEMENTS = dom.CONTENT_STUB_ELEMENTS.slice();
+ dom.STUB_ELEMENTS.push(dom.BREAK_ELEMENT);
+
+ dom.getKeyChar = function (e) {
+ return String.fromCharCode(e.which);
+ };
+ dom.getClass = function (className, startElement, tagName) {
+ if (!startElement) {
+ startElement = document.body;
+ }
+ className = '.' + className.split(' ').join('.');
+ if (tagName) {
+ className = tagName + className;
+ }
+ return jQuery.makeArray(jQuery(startElement).find(className));
+ };
+ dom.getId = function (id, startElement) {
+ if (!startElement) {
+ startElement = document;
+ }
+ element = startElement.getElementById(id);
+ return element;
+ };
+ dom.getTag = function (tagName, startElement) {
+ if (!startElement) {
+ startElement = document;
+ }
+ return jQuery.makeArray(jQuery(startElement).find(tagName));
+ };
+ dom.getElementWidth = function (element) {
+ return element.offsetWidth;
+ };
+ dom.getElementHeight = function (element) {
+ return element.offsetHeight;
+ };
+ dom.getElementDimensions = function (element) {
+ var result = {
+ 'width': dom.getElementWidth(element),
+ 'height': dom.getElementHeight(element)
+ };
+ return result;
+ };
+ dom.trim = function (string) {
+ return jQuery.trim(string);
+ };
+ dom.empty = function (element) {
+ if (element) {
+ return jQuery(element).empty();
+ }
+ };
+ dom.remove = function (element) {
+ if (element) {
+ return jQuery(element).remove();
+ }
+ };
+ dom.prepend = function (parent, elem) {
+ jQuery(parent).prepend(elem);
+ };
+ dom.append = function (parent, elem) {
+ jQuery(parent).append(elem);
+ };
+ dom.insertBefore = function (before, elem) {
+ jQuery(before).before(elem);
+ };
+ dom.insertAfter = function (after, elem) {
+ jQuery(after).after(elem);
+ };
+ dom.getHtml = function (element) {
+ return jQuery(element).html();
+ };
+ dom.setHtml = function (element, content) {
+ if (element) {
+ jQuery(element).html(content);
+ }
+ };
+ // Remove whitespace/newlines between nested block elements
+ // that are supported by ice.
+ // For example the following element with innerHTML:
+ //
+ // Will be converted to the following:
+ //
+ dom.removeWhitespace = function(element) {
+ jQuery(element).contents().filter(function() {
+ // Ice supports UL and OL, so recurse in these blocks to
+ // make sure that spaces don't exist between inner LI.
+ if (this.nodeType != ice.dom.TEXT_NODE && this.nodeName == 'UL' || this.nodeName == 'OL') {
+ dom.removeWhitespace(this);
+ return false;
+ } else if (this.nodeType != ice.dom.TEXT_NODE) {
+ return false;
+ } else {
+ return !/\S/.test(this.nodeValue);
+ }
+ }).remove();
+ };
+ dom.contents = function (el) {
+ return jQuery.makeArray(jQuery(el).contents());
+ };
+ /**
+ * Returns the inner contents of `el` as a DocumentFragment.
+ */
+ dom.extractContent = function (el) {
+ var frag = document.createDocumentFragment(),
+ child;
+ while ((child = el.firstChild)) {
+ frag.appendChild(child);
+ }
+ return frag;
+ };
+
+ /**
+ * Returns this `node` or the first parent tracking node that matches the given `selector`.
+ */
+ dom.getNode = function (node, selector) {
+ return dom.is(node, selector) ? node : dom.parents(node, selector)[0] || null;
+ };
+
+ dom.getParents = function (elements, filter, stopEl) {
+ var res = jQuery(elements).parents(filter);
+ var ln = res.length;
+ var ar = [];
+ for (var i = 0; i < ln; i++) {
+ if (res[i] === stopEl) {
+ break;
+ }
+ ar.push(res[i]);
+ }
+ return ar;
+ };
+ dom.hasBlockChildren = function (parent) {
+ var c = parent.childNodes.length;
+ for (var i = 0; i < c; i++) {
+ if (parent.childNodes[i].nodeType === dom.ELEMENT_NODE) {
+ if (dom.isBlockElement(parent.childNodes[i]) === true) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+ dom.removeTag = function (element, selector) {
+ jQuery(element).find(selector).replaceWith(function () {
+ return jQuery(this).contents();
+ });
+ return element;
+ };
+ dom.stripEnclosingTags = function (content, allowedTags) {
+ var c = jQuery(content);
+ c.find('*').not(allowedTags).replaceWith(function () {
+ var ret = jQuery();
+ var $this;
+ try{
+ $this = jQuery(this);
+ ret = $this.contents();
+ } catch(e){}
+
+ // Handling jQuery bug (which may be fixed in the official release later)
+ // http://bugs.jquery.com/ticket/13401
+ if(ret.length === 0){
+ $this.remove();
+ }
+ return ret;
+ });
+ return c[0];
+ };
+ dom.getSiblings = function (element, dir, elementNodesOnly, stopElem) {
+ if (elementNodesOnly === true) {
+ if (dir === 'prev') {
+ return jQuery(element).prevAll();
+ } else {
+ return jQuery(element).nextAll();
+ }
+ } else {
+ var elems = [];
+ if (dir === 'prev') {
+ while (element.previousSibling) {
+ element = element.previousSibling;
+ if (element === stopElem) {
+ break;
+ }
+ elems.push(element);
+ }
+ } else {
+ while (element.nextSibling) {
+ element = element.nextSibling;
+ if (element === stopElem) {
+ break;
+ }
+ elems.push(element);
+ }
+ }
+ return elems;
+ }
+ };
+ dom.getNodeTextContent = function (node) {
+ return jQuery(node).text();
+ };
+ dom.getNodeStubContent = function (node) {
+ return jQuery(node).find(dom.CONTENT_STUB_ELEMENTS.join(', '));
+ };
+ dom.hasNoTextOrStubContent = function (node) {
+ if (dom.getNodeTextContent(node).length > 0) return false;
+ if (jQuery(node).find(dom.CONTENT_STUB_ELEMENTS.join(', ')).length > 0) return false;
+ return true;
+ };
+ dom.getNodeCharacterLength = function (node) {
+ return dom.getNodeTextContent(node).length + jQuery(node).find(dom.STUB_ELEMENTS.join(', ')).length;
+ };
+ dom.setNodeTextContent = function (node, txt) {
+ return jQuery(node).text(txt);
+ };
+ dom.getTagName = function (node) {
+ return node.tagName && node.tagName.toLowerCase() || null;
+ };
+ dom.getIframeDocument = function (iframe) {
+ var doc = null;
+ if (iframe.contentDocument) {
+ doc = iframe.contentDocument;
+ } else if (iframe.contentWindow) {
+ doc = iframe.contentWindow.document;
+ } else if (iframe.document) {
+ doc = iframe.document;
+ }
+ return doc;
+ };
+ dom.isBlockElement = function (element) {
+ return dom.BLOCK_ELEMENTS.lastIndexOf(element.nodeName.toLowerCase()) != -1;
+ };
+ dom.isStubElement = function (element) {
+ return dom.STUB_ELEMENTS.lastIndexOf(element.nodeName.toLowerCase()) != -1;
+ };
+ dom.removeBRFromChild = function (node) {
+ if (node && node.hasChildNodes()) {
+ for(var z=0; z < node.childNodes.length ; z++) {
+ var child = node.childNodes[z];
+ if (child && (ice.dom.BREAK_ELEMENT == ice.dom.getTagName(child))) {
+ child.parentNode.removeChild(child);
+ }
+ }
+ }
+ };
+ dom.isChildOf = function (el, parent) {
+ try {
+ while (el && el.parentNode) {
+ if (el.parentNode === parent) {
+ return true;
+ }
+ el = el.parentNode;
+ }
+ } catch (e) {}
+ return false;
+ };
+ dom.isChildOfTagName = function (el, name) {
+ try {
+ while (el && el.parentNode) {
+ if (el.parentNode && el.parentNode.tagName && el.parentNode.tagName.toLowerCase() === name) {
+ return el.parentNode;
+ }
+ el = el.parentNode;
+ }
+ } catch (e) {}
+ return false;
+ };
+
+
+ dom.isChildOfTagNames = function (el, names) {
+ try {
+ while (el && el.parentNode) {
+ if (el.parentNode && el.parentNode.tagName) {
+ tagName = el.parentNode.tagName.toLowerCase();
+ for (var i = 0; i < names.length; i++) {
+ if (tagName === names[i]) {
+ return el.parentNode;
+ }
+ }
+ }
+ el = el.parentNode;
+ }
+ } catch (e) {}
+ return null;
+ };
+
+ dom.isChildOfClassName = function (el, name) {
+ try {
+ while (el && el.parentNode) {
+ if (jQuery(el.parentNode).hasClass(name)) return el.parentNode;
+ el = el.parentNode;
+ }
+ } catch (e) {}
+ return null;
+ };
+ dom.cloneNode = function (elems, cloneEvents) {
+ if (cloneEvents === undefined) {
+ cloneEvents = true;
+ }
+ return jQuery(elems).clone(cloneEvents);
+ };
+
+ dom.bind = function (element, event, callback) {
+ return jQuery(element).bind(event, callback);
+ };
+
+ dom.unbind = function (element, event, callback) {
+ return jQuery(element).unbind(event, callback);
+ };
+
+ dom.attr = function (elements, key, val) {
+ if (val) return jQuery(elements).attr(key, val);
+ else return jQuery(elements).attr(key);
+ };
+ dom.replaceWith = function (node, replacement) {
+ return jQuery(node).replaceWith(replacement);
+ };
+ dom.removeAttr = function (elements, name) {
+ jQuery(elements).removeAttr(name);
+ };
+ dom.getElementsBetween = function (fromElem, toElem) {
+ var elements = [];
+ if (fromElem === toElem) {
+ return elements;
+ }
+ if (dom.isChildOf(toElem, fromElem) === true) {
+ var fElemLen = fromElem.childNodes.length;
+ for (var i = 0; i < fElemLen; i++) {
+ if (fromElem.childNodes[i] === toElem) {
+ break;
+ } else if (dom.isChildOf(toElem, fromElem.childNodes[i]) === true) {
+ return dom.arrayMerge(elements, dom.getElementsBetween(fromElem.childNodes[i], toElem));
+ } else {
+ elements.push(fromElem.childNodes[i]);
+ }
+ }
+ return elements;
+ }
+ var startEl = fromElem.nextSibling;
+ while (startEl) {
+ if (dom.isChildOf(toElem, startEl) === true) {
+ elements = dom.arrayMerge(elements, dom.getElementsBetween(startEl, toElem));
+ return elements;
+ } else if (startEl === toElem) {
+ return elements;
+ } else {
+ elements.push(startEl);
+ startEl = startEl.nextSibling;
+ }
+ }
+ var fromParents = dom.getParents(fromElem);
+ var toParents = dom.getParents(toElem);
+ var parentElems = dom.arrayDiff(fromParents, toParents, true);
+ var pElemLen = parentElems.length;
+ for (var j = 0; j < (pElemLen - 1); j++) {
+ elements = dom.arrayMerge(elements, dom.getSiblings(parentElems[j], 'next'));
+ }
+ var lastParent = parentElems[(parentElems.length - 1)];
+ elements = dom.arrayMerge(elements, dom.getElementsBetween(lastParent, toElem));
+ return elements;
+ };
+ dom.getCommonAncestor = function (a, b) {
+ var node = a;
+ while (node) {
+ if (dom.isChildOf(b, node) === true) {
+ return node;
+ }
+ node = node.parentNode;
+ }
+ return null;
+ };
+ dom.getNextNode = function (node, container) {
+ if (node) {
+ while (node.parentNode) {
+ if (node === container) {
+ return null;
+ }
+
+ if (node.nextSibling) {
+ // if next sibling is an empty text node, look further
+ if (node.nextSibling.nodeType === dom.TEXT_NODE && node.nextSibling.length === 0) {
+ node = node.nextSibling;
+ continue;
+ }
+
+ return dom.getFirstChild(node.nextSibling);
+ }
+ node = node.parentNode;
+ }
+ }
+ return null;
+ };
+ dom.getNextContentNode = function (node, container) {
+ if (node) {
+ while (node.parentNode) {
+ if (node === container) {
+ return null;
+ }
+
+ if (node.nextSibling && dom.canContainTextElement(dom.getBlockParent(node))) {
+ // if next sibling is an empty text node, look further
+ if (node.nextSibling.nodeType === dom.TEXT_NODE && node.nextSibling.length === 0) {
+ node = node.nextSibling;
+ continue;
+ }
+
+ return node.nextSibling;
+ } else if (node.nextElementSibling) {
+ return node.nextElementSibling;
+ }
+
+ node = node.parentNode;
+ }
+ }
+ return null;
+ };
+
+
+ dom.getPrevNode = function (node, container) {
+ if (node) {
+ while (node.parentNode) {
+ if (node === container) {
+ return null;
+ }
+
+ if (node.previousSibling) {
+ // if previous sibling is an empty text node, look further
+ if (node.previousSibling.nodeType === dom.TEXT_NODE && node.previousSibling.length === 0) {
+ node = node.previousSibling;
+ continue;
+ }
+
+ return dom.getLastChild(node.previousSibling);
+ }
+ node = node.parentNode;
+ }
+ }
+ return null;
+ };
+ dom.getPrevContentNode = function (node, container) {
+ if (node) {
+ while (node.parentNode) {
+ if (node === container) {
+ return null;
+ }
+ if (node.previousSibling && dom.canContainTextElement(dom.getBlockParent(node))) {
+
+ // if previous sibling is an empty text node, look further
+ if (node.previousSibling.nodeType === dom.TEXT_NODE && node.previousSibling.length === 0) {
+ node = node.previousSibling;
+
+ continue;
+ }
+ return node.previousSibling;
+ } else if (node.previousElementSibling) {
+ return node.previousElementSibling;
+ }
+
+ node = node.parentNode;
+ }
+ }
+ return null;
+ };
+
+ dom.canContainTextElement = function (element) {
+ if (element && element.nodeName) {
+ return dom.TEXT_CONTAINER_ELEMENTS.lastIndexOf(element.nodeName.toLowerCase()) != -1;
+ } else {
+ return false;
+ }
+ };
+
+ dom.getFirstChild = function (node) {
+ if (node.firstChild) {
+ if (node.firstChild.nodeType === dom.ELEMENT_NODE) {
+ return dom.getFirstChild(node.firstChild);
+ } else {
+ return node.firstChild;
+ }
+ }
+ return node;
+ };
+ dom.getLastChild = function (node) {
+ if (node.lastChild) {
+ if (node.lastChild.nodeType === dom.ELEMENT_NODE) {
+ return dom.getLastChild(node.lastChild);
+ } else {
+ return node.lastChild;
+ }
+ }
+ return node;
+ };
+ dom.removeEmptyNodes = function (parent, callback) {
+ var elems = jQuery(parent).find(':empty');
+ var i = elems.length;
+ while (i > 0) {
+ i--;
+ if (dom.isStubElement(elems[i]) === false) {
+ if (!callback || callback.call(this, elems[i]) !== false) {
+ dom.remove(elems[i]);
+ }
+ }
+ }
+ };
+ dom.create = function (html) {
+ return jQuery(html)[0];
+ };
+ dom.find = function (parent, exp) {
+ return jQuery(parent).find(exp);
+ };
+ dom.children = function (parent, exp) {
+ return jQuery(parent).children(exp);
+ };
+ dom.parent = function (child, exp) {
+ return jQuery(child).parent(exp)[0];
+ };
+ dom.parents = function (child, exp) {
+ return jQuery(child).parents(exp);
+ };
+ dom.is = function (node, exp) {
+ return jQuery(node).is(exp);
+ };
+ dom.extend = function (deep, target, object1, object2) {
+ return jQuery.extend.apply(this, arguments);
+ };
+ dom.walk = function (elem, callback, lvl) {
+ if (!elem) {
+ return;
+ }
+ if (!lvl) {
+ lvl = 0;
+ }
+ var retVal = callback.call(this, elem, lvl);
+ if (retVal === false) {
+ return;
+ }
+ if (elem.childNodes && elem.childNodes.length > 0) {
+ dom.walk(elem.firstChild, callback, (lvl + 1));
+ } else if (elem.nextSibling) {
+ dom.walk(elem.nextSibling, callback, lvl);
+ } else if (elem.parentNode && elem.parentNode.nextSibling) {
+ dom.walk(elem.parentNode.nextSibling, callback, (lvl - 1));
+ }
+ };
+ dom.revWalk = function (elem, callback) {
+ if (!elem) {
+ return;
+ }
+ var retVal = callback.call(this, elem);
+ if (retVal === false) {
+ return;
+ }
+ if (elem.childNodes && elem.childNodes.length > 0) {
+ dom.walk(elem.lastChild, callback);
+ } else if (elem.previousSibling) {
+ dom.walk(elem.previousSibling, callback);
+ } else if (elem.parentNode && elem.parentNode.previousSibling) {
+ dom.walk(elem.parentNode.previousSibling, callback);
+ }
+ };
+ dom.setStyle = function (element, property, value) {
+ if (element) {
+ jQuery(element).css(property, value);
+ }
+ };
+ dom.getStyle = function (element, property) {
+ return jQuery(element).css(property);
+ };
+ dom.hasClass = function (element, className) {
+ return jQuery(element).hasClass(className);
+ };
+ dom.addClass = function (element, classNames) {
+ jQuery(element).addClass(classNames);
+ };
+ dom.removeClass = function (element, classNames) {
+ jQuery(element).removeClass(classNames);
+ };
+ dom.preventDefault = function (e) {
+ e.preventDefault();
+ dom.stopPropagation(e);
+ };
+ dom.stopPropagation = function (e) {
+ e.stopPropagation();
+ };
+ dom.noInclusionInherits = function (child, parent) {
+ if (parent instanceof String || typeof parent === 'string') {
+ parent = window[parent];
+ }
+ if (child instanceof String || typeof child === 'string') {
+ child = window[child];
+ }
+ var above = function () {};
+ if (dom.isset(parent) === true) {
+ for (value in parent.prototype) {
+ if (child.prototype[value]) {
+ above.prototype[value] = parent.prototype[value];
+ continue;
+ }
+ child.prototype[value] = parent.prototype[value];
+ }
+ }
+ if (child.prototype) {
+ above.prototype.constructor = parent;
+ child.prototype['super'] = new above();
+ }
+ };
+
+ dom.each = function (val, callback) {
+ jQuery.each(val, function (i, el) {
+ callback.call(this, i, el);
+ });
+ };
+
+ dom.foreach = function (value, cb) {
+ if (value instanceof Array || value instanceof NodeList || typeof value.length != 'undefined' && typeof value.item != 'undefined') {
+ var len = value.length;
+ for (var i = 0; i < len; i++) {
+ var res = cb.call(this, i, value[i]);
+ if (res === false) {
+ break;
+ }
+ }
+ } else {
+ for (var id in value) {
+ if (value.hasOwnProperty(id) === true) {
+ var res = cb.call(this, id);
+ if (res === false) {
+ break;
+ }
+ }
+ }
+ }
+ };
+ dom.isBlank = function (value) {
+ if (!value || /^\s*$/.test(value)) {
+ return true;
+ }
+ return false;
+ };
+ dom.isFn = function (f) {
+ if (typeof f === 'function') {
+ return true;
+ }
+ return false;
+ };
+ dom.isObj = function (v) {
+ if (v !== null && typeof v === 'object') {
+ return true;
+ }
+ return false;
+ };
+ dom.isset = function (v) {
+ if (typeof v !== 'undefined' && v !== null) {
+ return true;
+ }
+ return false;
+ };
+ dom.isArray = function (v) {
+ return jQuery.isArray(v);
+ };
+ dom.isNumeric = function (str) {
+ var result = str.match(/^\d+$/);
+ if (result !== null) {
+ return true;
+ }
+ return false;
+ };
+ dom.getUniqueId = function () {
+ var timestamp = (new Date()).getTime();
+ var random = Math.ceil(Math.random() * 1000000);
+ var id = timestamp + '' + random;
+ return id.substr(5, 18).replace(/,/, '');
+ };
+ dom.inArray = function (needle, haystack) {
+ var hln = haystack.length;
+ for (var i = 0; i < hln; i++) {
+ if (needle === haystack[i]) {
+ return true;
+ }
+ }
+ return false;
+ };
+ dom.arrayDiff = function (array1, array2, firstOnly) {
+ var al = array1.length;
+ var res = [];
+ for (var i = 0; i < al; i++) {
+ if (dom.inArray(array1[i], array2) === false) {
+ res.push(array1[i]);
+ }
+ }
+ if (firstOnly !== true) {
+ al = array2.length;
+ for (var i = 0; i < al; i++) {
+ if (dom.inArray(array2[i], array1) === false) {
+ res.push(array2[i]);
+ }
+ }
+ }
+ return res;
+ };
+ dom.arrayMerge = function (array1, array2) {
+ var c = array2.length;
+ for (var i = 0; i < c; i++) {
+ array1.push(array2[i]);
+ }
+ return array1;
+ };
+ /**
+ * Removes allowedTags from the given content html string. If allowedTags is a string, then it
+ * is expected to be a selector; otherwise, it is expected to be array of string tag names.
+ */
+ dom.stripTags = function (content, allowedTags) {
+ if (typeof allowedTags === "string") {
+ var c = jQuery('' + content + '
');
+ c.find('*').not(allowedTags).remove();
+ return c.html();
+ } else {
+ var match;
+ var re = new RegExp(/<\/?(\w+)((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)\/?>/gim);
+ var resCont = content;
+ while ((match = re.exec(content)) != null) {
+ if (dom.isset(allowedTags) === false || dom.inArray(match[1], allowedTags) !== true) {
+ resCont = resCont.replace(match[0], '');
+ }
+ }
+ return resCont;
+ }
+ };
+ dom.browser = function () {
+ var result = {};
+ result.version = jQuery.browser.version;
+ if (jQuery.browser.mozilla === true) {
+ result.type = 'mozilla';
+ } else if (jQuery.browser.msie === true) {
+ result.type = 'msie';
+ } else if (jQuery.browser.opera === true) {
+ result.type = 'opera';
+ } else if (jQuery.browser.webkit === true) {
+ result.type = 'webkit';
+ }
+ return result;
+ };
+ dom.getBrowserType = function () {
+ if (this._browserType === null) {
+ var tests = ['msie', 'firefox', 'chrome', 'safari'];
+ var tln = tests.length;
+ for (var i = 0; i < tln; i++) {
+ var r = new RegExp(tests[i], 'i');
+ if (r.test(navigator.userAgent) === true) {
+ this._browserType = tests[i];
+ return this._browserType;
+ }
+ }
+
+ this._browserType = 'other';
+ }
+ return this._browserType;
+ };
+ dom.getWebkitType = function(){
+ if(dom.browser().type !== "webkit") {
+ console.log("Not a webkit!");
+ return false;
+ }
+ var isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
+ if(isSafari) return "safari";
+ return "chrome";
+ };
+ dom.isBrowser = function (browser) {
+ return (dom.browser().type === browser);
+ };
+
+ dom.getBlockParent = function (node, container) {
+ if (dom.isBlockElement(node) === true) {
+ return node;
+ }
+ if (node) {
+ while (node.parentNode) {
+ node = node.parentNode;
+ if (node === container) {
+ return null;
+ }
+
+ if (dom.isBlockElement(node) === true) {
+ return node;
+ }
+ }
+ }
+ return null;
+ };
+ dom.findNodeParent = function (node, selector, container) {
+ if (node) {
+ while (node.parentNode) {
+ if (node === container) {
+ return null;
+ }
+
+ if (dom.is(node, selector) === true) {
+ return node;
+ }
+ node = node.parentNode;
+ }
+ }
+ return null;
+ };
+ dom.onBlockBoundary = function (leftContainer, rightContainer, blockEls) {
+ if (!leftContainer || !rightContainer) return false
+ var bleft = dom.isChildOfTagNames(leftContainer, blockEls) || dom.is(leftContainer, blockEls.join(', ')) && leftContainer || null;
+ var bright = dom.isChildOfTagNames(rightContainer, blockEls) || dom.is(rightContainer, blockEls.join(', ')) && rightContainer || null;
+ return (bleft !== bright);
+ };
+
+ dom.isOnBlockBoundary = function (leftContainer, rightContainer, container) {
+ if (!leftContainer || !rightContainer) return false
+ var bleft = dom.getBlockParent(leftContainer, container) || dom.isBlockElement(leftContainer, container) && leftContainer || null;
+ var bright = dom.getBlockParent(rightContainer, container) || dom.isBlockElement(rightContainer, container) && rightContainer || null;
+ return (bleft !== bright);
+ };
+
+ dom.mergeContainers = function (node, mergeToNode) {
+ if (!node || !mergeToNode) return false;
+
+ if (node.nodeType === dom.TEXT_NODE || dom.isStubElement(node)) {
+ // Move only this node.
+ mergeToNode.appendChild(node);
+ } else if (node.nodeType === dom.ELEMENT_NODE) {
+ // Move all the child nodes to the new parent.
+ while (node.firstChild) {
+ mergeToNode.appendChild(node.firstChild);
+ }
+
+ dom.remove(node);
+ }
+ return true;
+ };
+
+ dom.mergeBlockWithSibling = function (range, block, next) {
+ var siblingBlock = next ? jQuery(block).next().get(0) : jQuery(block).prev().get(0); // block['nextSibling'] : block['previousSibling'];
+ if (next) dom.mergeContainers(siblingBlock, block);
+ else dom.mergeContainers(block, siblingBlock);
+ range.collapse(true);
+ return true;
+ };
+
+ dom.date = function (format, timestamp, tsIso8601) {
+ if (timestamp === null && tsIso8601) {
+ timestamp = dom.tsIso8601ToTimestamp(tsIso8601);
+ if (!timestamp) {
+ return;
+ }
+ }
+ var date = new Date(timestamp);
+ var formats = format.split('');
+ var fc = formats.length;
+ var dateStr = '';
+ for (var i = 0; i < fc; i++) {
+ var r = '';
+ var f = formats[i];
+ switch (f) {
+ case 'D':
+ case 'l':
+ var names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ r = names[date.getDay()];
+ if (f === 'D') {
+ r = r.substring(0, 3);
+ }
+ break;
+ case 'F':
+ case 'm':
+ r = date.getMonth() + 1;
+ if (r < 10) r = '0' + r;
+ break;
+ case 'M':
+ months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+ r = months[date.getMonth()];
+ if (f === 'M') {
+ r = r.substring(0, 3);
+ }
+ break;
+ case 'd':
+ r = date.getDate();
+ break;
+ case 'S':
+ r = dom.getOrdinalSuffix(date.getDate());
+ break;
+ case 'Y':
+ r = date.getFullYear();
+ break;
+ case 'y':
+ r = date.getFullYear();
+ r = r.toString().substring(2);
+ break;
+ case 'H':
+ r = date.getHours();
+ break;
+ case 'h':
+ r = date.getHours();
+ if (r === 0) {
+ r = 12;
+ } else if (r > 12) {
+ r -= 12;
+ }
+ break;
+ case 'i':
+ r = dom.addNumberPadding(date.getMinutes());
+ break;
+ case 'a':
+ r = 'am';
+ if (date.getHours() >= 12) {
+ r = 'pm';
+ }
+ break;
+ default:
+ r = f;
+ break;
+ }
+ dateStr += r;
+ }
+ return dateStr;
+ };
+ dom.getOrdinalSuffix = function (number) {
+ var suffix = '';
+ var tmp = (number % 100);
+ if (tmp >= 4 && tmp <= 20) {
+ suffix = 'th';
+ } else {
+ switch (number % 10) {
+ case 1:
+ suffix = 'st';
+ break;
+ case 2:
+ suffix = 'nd';
+ break;
+ case 3:
+ suffix = 'rd';
+ break;
+ default:
+ suffix = 'th';
+ break;
+ }
+ }
+ return suffix;
+ };
+ dom.addNumberPadding = function (number) {
+ if (number < 10) {
+ number = '0' + number;
+ }
+ return number;
+ };
+ dom.tsIso8601ToTimestamp = function (tsIso8601) {
+ var regexp = /(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d)(?:[T ](\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(?:Z|(?:([-+])(\d\d)(?::?(\d\d))?)?)?)?)?)?/;
+ var d = tsIso8601.match(new RegExp(regexp));
+ if (d) {
+ var date = new Date();
+ date.setDate(d[3]);
+ date.setFullYear(d[1]);
+ date.setMonth(d[2] - 1);
+ date.setHours(d[4]);
+ date.setMinutes(d[5]);
+ date.setSeconds(d[6]);
+ var offset = (d[9] * 60);
+ if (d[8] === '+') {
+ offset *= -1;
+ }
+ offset -= date.getTimezoneOffset();
+ var timestamp = (date.getTime() + (offset * 60 * 1000));
+ return timestamp;
+ }
+ return null;
+ };
+
+ exports.dom = dom;
+
+}).call(this.ice);
diff --git a/demo/ice/ice.js b/demo/ice/ice.js
new file mode 100644
index 00000000..e22be15b
--- /dev/null
+++ b/demo/ice/ice.js
@@ -0,0 +1,1691 @@
+(function () {
+
+ var exports = this,
+ defaults, InlineChangeEditor;
+
+ defaults = {
+ // ice node attribute names:
+ changeIdAttribute: 'data-cid',
+ userIdAttribute: 'data-userid',
+ userNameAttribute: 'data-username',
+ timeAttribute: 'data-time',
+
+ // Prepended to `changeType.alias` for classname uniqueness, if needed
+ attrValuePrefix: '',
+
+ // Block element tagname, which wrap text and other inline nodes in `this.element`
+ blockEl: 'p',
+
+ // All permitted block element tagnames
+ blockEls: ['p', 'ol', 'ul', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'],
+
+ // Unique style prefix, prepended to a digit, incremented for each encountered user, and stored
+ // in ice node class attributes - cts1, cts2, cts3, ...
+ stylePrefix: 'cts',
+ currentUser: {
+ id: null,
+ name: null
+ },
+
+ // Default change types are insert and delete. Plugins or outside apps should extend this
+ // if they want to manage new change types. The changeType name is used as a primary
+ // reference for ice nodes; the `alias`, is dropped in the class attribute and is the
+ // primary method of identifying ice nodes; and `tag` is used for construction only.
+ // Invoking `this.getCleanContent()` will remove all delete type nodes and remove the tags
+ // for the other types, leaving the html content in place.
+ changeTypes: {
+ insertType: {
+ tag: 'span',
+ alias: 'ins',
+ action: 'Inserted'
+ },
+ deleteType: {
+ tag: 'span',
+ alias: 'del',
+ action: 'Deleted'
+ }
+ },
+
+ // If `true`, setup event listeners on `this.element` and handle events - good option for a basic
+ // setup without a text editor. Otherwise, when set to `false`, events need to be manually passed
+ // to `handleEvent`, which is good for a text editor with an event callback handler, like tinymce.
+ handleEvents: false,
+
+ // Sets this.element with the contentEditable element
+ contentEditable: true,
+
+ // Switch for toggling track changes on/off - when `false` events will be ignored.
+ isTracking: true,
+
+ // NOT IMPLEMENTED - Selector for elements that will not get track changes
+ noTrack: '.ice-no-track',
+
+ // Selector for elements to avoid - move range before or after - similar handling to deletes
+ avoid: '.ice-avoid',
+
+ // Switch for whether paragraph breaks should be removed when the user is deleting over a
+ // paragraph break while changes are tracked.
+ mergeBlocks: true
+ };
+
+ InlineChangeEditor = function (options) {
+
+ // Data structure for modelling changes in the element according to the following model:
+ // [changeid] => {`type`, `time`, `userid`, `username`}
+ this._changes = {};
+
+ options || (options = {});
+ if (!options.element) throw Error("`options.element` must be defined for ice construction.");
+
+ ice.dom.extend(true, this, defaults, options);
+
+ this.pluginsManager = new ice.IcePluginManager(this);
+ if (options.plugins) this.pluginsManager.usePlugins('ice-init', options.plugins);
+ };
+
+ InlineChangeEditor.prototype = {
+ // Tracks all of the styles for users according to the following model:
+ // [userId] => styleId; where style is "this.stylePrefix" + "this.uniqueStyleIndex"
+ _userStyles: {},
+ _styles: {},
+
+ // Incremented for each new user and appended to they style prefix, and dropped in the
+ // ice node class attribute.
+ _uniqueStyleIndex: 0,
+
+ _browserType: null,
+
+ // One change may create multiple ice nodes, so this keeps track of the current batch id.
+ _batchChangeid: null,
+
+ // Incremented for each new change, dropped in the changeIdAttribute.
+ _uniqueIDIndex: 1,
+
+ // Temporary bookmark tags for deletes, when delete placeholding is active.
+ _delBookmark: 'tempdel',
+ isPlaceHoldingDeletes: false,
+
+ /**
+ * Turns on change tracking - sets up events, if needed, and initializes the environment,
+ * range, and editor.
+ */
+ startTracking: function () {
+ this.element.setAttribute('contentEditable', this.contentEditable);
+
+ // If we are handling events setup the delegate to handle various events on `this.element`.
+ if (this.handleEvents) {
+ var self = this;
+ ice.dom.bind(self.element, 'keyup.ice keydown.ice keypress.ice mousedown.ice mouseup.ice', function (e) {
+ return self.handleEvent(e);
+ });
+ }
+
+ this.initializeEnvironment();
+ this.initializeEditor();
+ this.findTrackTags();
+ this.initializeRange();
+
+ this.pluginsManager.fireEnabled(this.element);
+ return this;
+ },
+
+ /**
+ * Removes contenteditability and stops event handling.
+ */
+ stopTracking: function () {
+ this.element.setAttribute('contentEditable', !this.contentEditable);
+
+ // If we are handling events setup the delegate to handle various events on `this.element`.
+ if (this.handleEvents) {
+ var self = this;
+ ice.dom.unbind(self.element, 'keyup.ice keydown.ice keypress.ice mousedown.ice mouseup.ice');
+ }
+
+ this.pluginsManager.fireDisabled(this.element);
+ return this;
+ },
+
+ /**
+ * Initializes the `env` object with pointers to key objects of the page.
+ */
+ initializeEnvironment: function () {
+ this.env || (this.env = {});
+ this.env.element = this.element;
+ this.env.document = this.element.ownerDocument;
+ this.env.window = this.env.document.defaultView || this.env.document.parentWindow || window;
+ this.env.frame = this.env.window.frameElement;
+ this.env.selection = this.selection = new ice.Selection(this.env);
+ // Hack for using custom tags in IE 8/7
+ this.env.document.createElement(this.changeTypes.insertType.tag);
+ this.env.document.createElement(this.changeTypes.deleteType.tag);
+ },
+
+ /**
+ * Initializes the internal range object and sets focus to the editing element.
+ */
+ initializeRange: function () {
+ var range = this.selection.createRange();
+ range.setStart(ice.dom.find(this.element, this.blockEls.join(', '))[0], 0);
+ range.collapse(true);
+ this.selection.addRange(range);
+ if (this.env.frame) this.env.frame.contentWindow.focus();
+ else this.element.focus();
+ },
+
+ /**
+ * Initializes the content in the editor - cleans non-block nodes found between blocks.
+ */
+ initializeEditor: function () {
+ // Clean the element html body - add an empty block if there is no body, or remove any
+ // content between elements.
+ var body = this.env.document.createElement('div');
+ if (this.element.childNodes.length) {
+ body.innerHTML = this.element.innerHTML;
+ ice.dom.removeWhitespace(body);
+ if (body.innerHTML === '') body.appendChild(ice.dom.create('<' + this.blockEl + ' >
' + this.blockEl + '>'));
+ } else {
+ body.appendChild(ice.dom.create('<' + this.blockEl + ' >
' + this.blockEl + '>'));
+ }
+ if (this.element.innerHTML != body.innerHTML) {
+ this.element.innerHTML = body.innerHTML;
+ }
+
+ },
+
+ /*
+ * Updates the list of changes to include all track tags found inside the element.
+ */
+ findTrackTags: function () {
+
+ // Grab class for each changeType
+ var self = this, changeTypeClasses = [];
+ for (var changeType in this.changeTypes) {
+ changeTypeClasses.push(this._getIceNodeClass(changeType));
+ }
+
+ ice.dom.each(ice.dom.find(this.element, '.' + changeTypeClasses.join(', .')), function (i, el) {
+ var styleIndex = 0;
+ var ctnType = '';
+ var classList = el.className.split(' ');
+ for (var i = 0; i < classList.length; i++) {
+ var styleReg = new RegExp(self.stylePrefix + '-(\\d+)').exec(classList[i]);
+ if (styleReg) styleIndex = styleReg[1];
+ var ctnReg = new RegExp('(' + changeTypeClasses.join('|') + ')').exec(classList[i]);
+ if (ctnReg) ctnType = self._getChangeTypeFromAlias(ctnReg[1]);
+ }
+ var userid = ice.dom.attr(el, self.userIdAttribute);
+ self.setUserStyle(userid, Number(styleIndex));
+ var changeid = ice.dom.attr(el, self.changeIdAttribute);
+ self._changes[changeid] = {
+ type: ctnType,
+ userid: userid,
+ username: ice.dom.attr(el, self.userNameAttribute),
+ time: ice.dom.attr(el, self.timeAttribute)
+ };
+ });
+ },
+
+ /**
+ * Turn on change tracking and event handling.
+ */
+ enableChangeTracking: function () {
+ this.isTracking = true;
+ this.pluginsManager.fireEnabled(this.element);
+ },
+
+ /**
+ * Turn off change tracking and event handling.
+ */
+ disableChangeTracking: function () {
+ this.isTracking = false;
+ this.pluginsManager.fireDisabled(this.element);
+ },
+
+ /**
+ * Set the user to be tracked. A user object has the following properties:
+ * {`id`, `name`}
+ */
+ setCurrentUser: function (user) {
+ this.currentUser = user;
+ },
+
+ /**
+ * If tracking is on, handles event e when it is one of the following types:
+ * mouseup, mousedown, keypress, keydown, and keyup. Each event type is
+ * propagated to all of the plugins. Prevents default handling if the event
+ * was fully handled.
+ */
+ handleEvent: function (e) {
+ if (!this.isTracking) return;
+ if (e.type == 'mouseup') {
+ var self = this;
+ setTimeout(function () {
+ self.mouseUp(e);
+ }, 200);
+ } else if (e.type == 'mousedown') {
+ return this.mouseDown(e);
+ } else if (e.type == 'keypress') {
+ var needsToBubble = this.keyPress(e);
+ if (!needsToBubble) e.preventDefault();
+ return needsToBubble;
+ } else if (e.type == 'keydown') {
+ var needsToBubble = this.keyDown(e);
+ if (!needsToBubble) e.preventDefault();
+ return needsToBubble;
+ } else if (e.type == 'keyup') {
+ this.pluginsManager.fireCaretUpdated();
+ }
+ },
+ visible: function(el) {
+ if(el.nodeType === ice.dom.TEXT_NODE) el = el.parentNode;
+ var rect = el.getBoundingClientRect();
+ return ( rect.top > 0 && rect.left > 0);
+ },
+
+ /**
+ * Returns a tracking tag for the given `changeType`, with the optional `childNode` appended.
+ */
+ createIceNode: function (changeType, childNode) {
+ var node = this.env.document.createElement(this.changeTypes[changeType].tag);
+ ice.dom.addClass(node, this._getIceNodeClass(changeType));
+
+ node.appendChild(childNode ? childNode : this.env.document.createTextNode(''));
+ this.addChange(this.changeTypes[changeType].alias, [node]);
+
+ this.pluginsManager.fireNodeCreated(node, {
+ 'action': this.changeTypes[changeType].action
+ });
+ return node;
+ },
+
+ /**
+ * Inserts the given string/node into the given range with tracking tags, collapsing (deleting)
+ * the range first if needed. If range is undefined, then the range from the Selection object
+ * is used. If the range is in a parent delete node, then the range is positioned after the delete.
+ */
+ insert: function (node, range) {
+ // If the node is not defined, then we need to insert an
+ // invisible space and force propagation to the browser.
+ var isPropagating = !node;
+ node || (node = '\uFEFF');
+
+ if (range) this.selection.addRange(range);
+ else range = this.getCurrentRange();
+
+ if (typeof node === "string") node = document.createTextNode(node);
+
+ // If we have any nodes selected, then we want to delete them before inserting the new text.
+ if (!range.collapsed) {
+ this.deleteContents();
+ // Update the range
+ range = this.getCurrentRange();
+ if (range.startContainer === range.endContainer && this.element === range.startContainer) {
+ // The whole editable element is selected. Need to remove everything and init its contents.
+ ice.dom.empty(this.element);
+ var firstSelectable = range.getLastSelectableChild(this.element);
+ range.setStartAfter(firstSelectable);
+ range.collapse(true);
+ }
+ }
+ // If we are in a non-tracking/void element, move the range to the end/outside.
+ this._moveRangeToValidTrackingPos(range);
+
+ var changeid = this.startBatchChange();
+ // Send a dummy node to be inserted, if node is undefined
+ this._insertNode(node, range, isPropagating);
+ this.pluginsManager.fireNodeInserted(node, range);
+ this.endBatchChange(changeid);
+ return isPropagating;
+ },
+
+ /**
+ * This command will drop placeholders in place of delete tags in the element
+ * body and store references in the `_deletes` array to the original delete nodes.
+ *
+ * A placeholder tag is of the following structure:
+ *
+ * Where [NUM] is the referenced allocation in the `_deletes` array where the
+ * original delete node is stored.
+ */
+ placeholdDeletes: function () {
+ var self = this;
+ if (this.isPlaceholdingDeletes) {
+ this.revertDeletePlaceholders();
+ }
+ this.isPlaceholdingDeletes = true;
+ this._deletes = [];
+ var deleteSelector = '.' + this._getIceNodeClass('deleteType');
+ ice.dom.each(ice.dom.find(this.element, deleteSelector), function (i, el) {
+ self._deletes.push(ice.dom.cloneNode(el));
+ ice.dom.replaceWith(el, '<' + self._delBookmark + ' data-allocation="' + (self._deletes.length - 1) + '"/>');
+ });
+ return true;
+ },
+
+ /**
+ * Replaces all delete placeholders in the element body with the referenced
+ * delete nodes in the `_deletes` array.
+ *
+ * A placeholder tag is of the following structure:
+ *
+ * Where [NUM] is the referenced allocation in the `_deletes` array where the
+ * original delete node is stored.
+ */
+ revertDeletePlaceholders: function () {
+ var self = this;
+ if (!this.isPlaceholdingDeletes) {
+ return false;
+ }
+ ice.dom.each(this._deletes, function (i, el) {
+ ice.dom.find(self.element, self._delBookmark + '[data-allocation=' + i + ']').replaceWith(el);
+ });
+ this.isPlaceholdingDeletes = false;
+ return true;
+ },
+ /**
+ * Deletes the contents in the given range or the range from the Selection object. If the range
+ * is not collapsed, then a selection delete is handled; otherwise, it deletes one character
+ * to the left or right if the right parameter is false or true, respectively.
+ *
+ * @return true if deletion was handled.
+ */
+ deleteContents: function (right, range) {
+ var prevent = true;
+ var browser = ice.dom.browser();
+
+ if (range) {
+ this.selection.addRange(range);
+ } else {
+ range = this.getCurrentRange();
+ }
+
+ var changeid = this.startBatchChange(this.changeTypes['deleteType'].alias);
+ if (range.collapsed === false) {
+ if(this._currentUserIceNode(range.startContainer.parentNode)){
+ this._deleteSelection(range);
+ } else {
+ this._deleteSelection(range);
+ if(browser["type"] === "mozilla"){
+ if(range.startContainer.parentNode.previousSibling){
+ range.setEnd(range.startContainer.parentNode.previousSibling, 0);
+ range.moveEnd(ice.dom.CHARACTER_UNIT, ice.dom.getNodeCharacterLength(range.endContainer));
+ } else {
+ range.setEndAfter(range.startContainer.parentNode);
+ }
+ range.collapse(false);
+ } else {
+ if(!this.visible(range.endContainer)){
+ range.setEnd(range.endContainer, range.endOffset - 1);
+ range.collapse(false);
+ }
+ }
+ }
+ } else {
+ if (right) {
+ // RIGHT DELETE
+ if(browser["type"] === "mozilla"){
+ prevent = this._deleteRight(range);
+ // Handling track change show/hide
+ if(!this.visible(range.endContainer)){
+ if(range.endContainer.parentNode.nextSibling){
+// range.setEnd(range.endContainer.parentNode.nextSibling, 0);
+ range.setEndBefore(range.endContainer.parentNode.nextSibling);
+ } else {
+ range.setEndAfter(range.endContainer);
+ }
+ range.collapse(false);
+ }
+ }
+ else {
+ // Calibrate Cursor before deleting
+ if(range.endOffset === ice.dom.getNodeCharacterLength(range.endContainer)){
+ var next = range.startContainer.nextSibling;
+ if (ice.dom.is(next, '.' + this._getIceNodeClass('deleteType'))) {
+ while(next){
+ if (ice.dom.is(next, '.' + this._getIceNodeClass('deleteType'))) {
+ next = next.nextSibling;
+ continue;
+ }
+ range.setStart(next, 0);
+ range.collapse(true);
+ break;
+ }
+ }
+ }
+
+ // Delete
+ prevent = this._deleteRight(range);
+
+ // Calibrate Cursor after deleting
+ if(!this.visible(range.endContainer)){
+ if (ice.dom.is(range.endContainer.parentNode, '.' + this._getIceNodeClass('insertType') + ', .' + this._getIceNodeClass('deleteType'))) {
+// range.setStart(range.endContainer.parentNode.nextSibling, 0);
+ range.setStartAfter(range.endContainer.parentNode);
+ range.collapse(true);
+ }
+ }
+ }
+ }
+ else {
+ // LEFT DELETE
+ if(browser["type"] === "mozilla"){
+ prevent = this._deleteLeft(range);
+ // Handling track change show/hide
+ if(!this.visible(range.startContainer)){
+ if(range.startContainer.parentNode.previousSibling){
+ range.setEnd(range.startContainer.parentNode.previousSibling, 0);
+ } else {
+ range.setEnd(range.startContainer.parentNode, 0);
+ }
+ range.moveEnd(ice.dom.CHARACTER_UNIT, ice.dom.getNodeCharacterLength(range.endContainer));
+ range.collapse(false);
+ }
+ }
+ else {
+ if(!this.visible(range.startContainer)){
+ if(range.endOffset === ice.dom.getNodeCharacterLength(range.endContainer)){
+ var prev = range.startContainer.previousSibling;
+ if (ice.dom.is(prev, '.' + this._getIceNodeClass('deleteType'))) {
+ while(prev){
+ if (ice.dom.is(prev, '.' + this._getIceNodeClass('deleteType'))) {
+ prev = prev.prevSibling;
+ continue;
+ }
+ range.setEndBefore(prev.nextSibling, 0);
+ range.collapse(false);
+ break;
+ }
+ }
+ }
+ }
+ prevent = this._deleteLeft(range);
+ }
+ }
+ }
+
+ this.selection.addRange(range);
+ this.endBatchChange(changeid);
+ return prevent;
+ },
+
+ /**
+ * Returns the changes - a hash of objects with the following properties:
+ * [changeid] => {`type`, `time`, `userid`, `username`}
+ */
+ getChanges: function () {
+ return this._changes;
+ },
+
+ /**
+ * Returns an array with the user ids who made the changes
+ */
+ getChangeUserids: function () {
+ var result = [];
+ var keys = Object.keys(this._changes);
+
+ for (var key in keys)
+ result.push(this._changes[keys[key]].userid);
+
+ return result.sort().filter(function (el, i, a) {
+ if (i == a.indexOf(el)) return 1;
+ return 0;
+ });
+ },
+
+ /**
+ * Returns the html contents for the tracked element.
+ */
+ getElementContent: function () {
+ return this.element.innerHTML;
+ },
+
+ /**
+ * Returns the html contents, without tracking tags, for `this.element` or
+ * the optional `body` param which can be of either type string or node.
+ * Delete tags, and their html content, are completely removed; all other
+ * change type tags are removed, leaving the html content in place. After
+ * cleaning, the optional `callback` is executed, which should further
+ * modify and return the element body.
+ *
+ * prepare gets run before the body is cleaned by ice.
+ */
+ getCleanContent: function (body, callback, prepare) {
+ var classList = '';
+ var self = this;
+ ice.dom.each(this.changeTypes, function (type, i) {
+ if (type != 'deleteType') {
+ if (i > 0) classList += ',';
+ classList += '.' + self._getIceNodeClass(type);
+ }
+ });
+ if (body) {
+ if (typeof body === 'string') body = ice.dom.create('' + body + '
');
+ else body = ice.dom.cloneNode(body, false)[0];
+ } else {
+ body = ice.dom.cloneNode(this.element, false)[0];
+ }
+ body = prepare ? prepare.call(this, body) : body;
+ var changes = ice.dom.find(body, classList);
+ ice.dom.each(changes, function (el, i) {
+ ice.dom.replaceWith(this, ice.dom.contents(this));
+ });
+ var deletes = ice.dom.find(body, '.' + this._getIceNodeClass('deleteType'));
+ ice.dom.remove(deletes);
+
+ body = callback ? callback.call(this, body) : body;
+
+ return body.innerHTML;
+ },
+
+ /**
+ * Accepts all changes in the element body - removes delete nodes, and removes outer
+ * insert tags keeping the inner content in place.
+ */
+ acceptAll: function () {
+ this.element.innerHTML = this.getCleanContent();
+ },
+
+ /**
+ * Rejects all changes in the element body - removes insert nodes, and removes outer
+ * delete tags keeping the inner content in place.*
+ */
+ rejectAll: function () {
+ var insSel = '.' + this._getIceNodeClass('insertType');
+ var delSel = '.' + this._getIceNodeClass('deleteType');
+
+ ice.dom.remove(ice.dom.find(this.element, insSel));
+ ice.dom.each(ice.dom.find(this.element, delSel), function (i, el) {
+ ice.dom.replaceWith(el, ice.dom.contents(el));
+ });
+ },
+
+ /**
+ * Accepts the change at the given, or first tracking parent node of, `node`. If
+ * `node` is undefined then the startContainer of the current collapsed range will be used.
+ * In the case of insert, inner content will be used to replace the containing tag; and in
+ * the case of delete, the node will be removed.
+ */
+ acceptChange: function (node) {
+ this.acceptRejectChange(node, true);
+ },
+
+ /**
+ * Rejects the change at the given, or first tracking parent node of, `node`. If
+ * `node` is undefined then the startContainer of the current collapsed range will be used.
+ * In the case of delete, inner content will be used to replace the containing tag; and in
+ * the case of insert, the node will be removed.
+ */
+ rejectChange: function (node) {
+ this.acceptRejectChange(node, false);
+ },
+
+ /**
+ * Handles accepting or rejecting tracking changes
+ */
+ acceptRejectChange: function (node, isAccept) {
+ var delSel, insSel, selector, removeSel, replaceSel, trackNode, changes, dom = ice.dom;
+
+ if (!node) {
+ var range = this.getCurrentRange();
+ if (!range.collapsed) return;
+ else node = range.startContainer;
+ }
+
+ delSel = removeSel = '.' + this._getIceNodeClass('deleteType');
+ insSel = replaceSel = '.' + this._getIceNodeClass('insertType');
+ selector = delSel + ',' + insSel;
+ trackNode = dom.getNode(node, selector);
+ // Some changes are done in batches so there may be other tracking
+ // nodes with the same `changeIdAttribute` batch number.
+ changes = dom.find(this.element, '[' + this.changeIdAttribute + '=' + dom.attr(trackNode, this.changeIdAttribute) + ']');
+
+ if (!isAccept) {
+ removeSel = insSel;
+ replaceSel = delSel;
+ }
+
+ if (ice.dom.is(trackNode, replaceSel)) {
+ dom.each(changes, function (i, node) {
+ dom.replaceWith(node, ice.dom.contents(node));
+ });
+ } else if (dom.is(trackNode, removeSel)) {
+ dom.remove(changes);
+ }
+ },
+
+ /**
+ * Returns true if the given `node`, or the current collapsed range is in a tracking
+ * node; otherwise, false.
+ */
+ isInsideChange: function (node) {
+ var selector = '.' + this._getIceNodeClass('insertType') + ', .' + this._getIceNodeClass('deleteType');
+ if (!node) {
+ range = this.getCurrentRange();
+ if (!range.collapsed) return false;
+ else node = range.startContainer;
+ }
+ return !!ice.dom.getNode(node, selector);
+ },
+
+ /**
+ * Add a new change tracking typeName with the given tag and alias.
+ */
+ addChangeType: function (typeName, tag, alias, action) {
+ var changeType = {
+ tag: tag,
+ alias: alias
+ };
+
+ if (action) changeType.action = action;
+
+ this.changeTypes[typeName] = changeType;
+ },
+
+ /**
+ * Returns this `node` or the first parent tracking node with the given `changeType`.
+ */
+ getIceNode: function (node, changeType) {
+ var selector = '.' + this._getIceNodeClass(changeType);
+ return ice.dom.getNode(node, selector);
+ },
+
+ /**
+ * Sets the given `range` to the first position, to the right, where it is outside of
+ * void elements.
+ */
+ _moveRangeToValidTrackingPos: function (range) {
+ var onEdge = false;
+ var voidEl = this._getVoidElement(range.endContainer);
+ while (voidEl) {
+ // Move end of range to position it inside of any potential adjacent containers
+ // E.G.: test|text -> test|text
+ try {
+ range.moveEnd(ice.dom.CHARACTER_UNIT, 1);
+ range.moveEnd(ice.dom.CHARACTER_UNIT, -1);
+ } catch (e) {
+ // Moving outside of the element and nothing is left on the page
+ onEdge = true;
+ }
+ if (onEdge || ice.dom.onBlockBoundary(range.endContainer, range.startContainer, this.blockEls)) {
+ range.setStartAfter(voidEl);
+ range.collapse(true);
+ break;
+ }
+ voidEl = this._getVoidElement(range.endContainer);
+ if (voidEl) {
+ range.setEnd(range.endContainer, 0);
+ range.moveEnd(ice.dom.CHARACTER_UNIT, ice.dom.getNodeCharacterLength(range.endContainer));
+ range.collapse();
+ } else {
+ range.setStart(range.endContainer, 0);
+ range.collapse(true);
+ }
+ }
+ },
+
+ /**
+ * Returns the given `node` or the first parent node that matches against the list of no track elements.
+ */
+ _getNoTrackElement: function (node) {
+ var noTrackSelector = this._getNoTrackSelector();
+ var parent = ice.dom.is(node, noTrackSelector) ? node : (ice.dom.parents(node, noTrackSelector)[0] || null);
+ return parent;
+ },
+
+ /**
+ * Returns a selector for not tracking changes
+ */
+ _getNoTrackSelector: function () {
+ return this.noTrack;
+ },
+
+ /**
+ * Returns the given `node` or the first parent node that matches against the list of void elements.
+ */
+ _getVoidElement: function (node) {
+ var voidSelector = this._getVoidElSelector();
+ return ice.dom.is(node, voidSelector) ? node : (ice.dom.parents(node, voidSelector)[0] || null);
+ },
+
+ /**
+ * Returns a combined selector for delete and void elements.
+ */
+ _getVoidElSelector: function () {
+ return '.' + this._getIceNodeClass('deleteType') + ',' + this.avoid;
+ },
+
+ /**
+ * Returns true if node has a user id attribute that matches the current user id.
+ */
+ _currentUserIceNode: function (node) {
+ return ice.dom.attr(node, this.userIdAttribute) == this.currentUser.id;
+ },
+
+ /**
+ * With the given alias, searches the changeTypes objects and returns the
+ * associated key for the alias.
+ */
+ _getChangeTypeFromAlias: function (alias) {
+ var type, ctnType = null;
+ for (type in this.changeTypes) {
+ if (this.changeTypes.hasOwnProperty(type)) {
+ if (this.changeTypes[type].alias == alias) {
+ ctnType = type;
+ }
+ }
+ }
+
+ return ctnType;
+ },
+
+ _getIceNodeClass: function (changeType) {
+ return this.attrValuePrefix + this.changeTypes[changeType].alias;
+ },
+
+ getUserStyle: function (userid) {
+ var styleIndex = null;
+ if (this._userStyles[userid]) styleIndex = this._userStyles[userid];
+ else styleIndex = this.setUserStyle(userid, this.getNewStyleId());
+ return styleIndex;
+ },
+
+ setUserStyle: function (userid, styleIndex) {
+ var style = this.stylePrefix + '-' + styleIndex;
+ if (!this._styles[styleIndex]) this._styles[styleIndex] = true;
+ return this._userStyles[userid] = style;
+ },
+
+ getNewStyleId: function () {
+ var id = ++this._uniqueStyleIndex;
+ if (this._styles[id]) {
+ // Dupe.. create another..
+ return this.getNewStyleId();
+ } else {
+ this._styles[id] = true;
+ return id;
+ }
+ },
+
+ addChange: function (ctnType, ctNodes) {
+ var changeid = this._batchChangeid || this.getNewChangeId();
+ if (!this._changes[changeid]) {
+ // Create the change object.
+ this._changes[changeid] = {
+ type: this._getChangeTypeFromAlias(ctnType),
+ time: (new Date()).getTime(),
+ userid: this.currentUser.id,
+ username: this.currentUser.name
+ };
+ }
+ var self = this;
+ ice.dom.foreach(ctNodes, function (i) {
+ self.addNodeToChange(changeid, ctNodes[i]);
+ });
+
+ return changeid;
+ },
+
+ /**
+ * Adds tracking attributes from the change with changeid to the ctNode.
+ * @param changeid Id of an existing change.
+ * @param ctNode The element to add for the change.
+ */
+ addNodeToChange: function (changeid, ctNode) {
+ if (this._batchChangeid !== null) changeid = this._batchChangeid;
+
+ var change = this.getChange(changeid);
+
+ if (!ctNode.getAttribute(this.changeIdAttribute)) ctNode.setAttribute(this.changeIdAttribute, changeid);
+
+ if (!ctNode.getAttribute(this.userIdAttribute)) ctNode.setAttribute(this.userIdAttribute, change.userid);
+
+ if (!ctNode.getAttribute(this.userNameAttribute)) ctNode.setAttribute(this.userNameAttribute, change.username);
+
+ if (!ctNode.getAttribute(this.timeAttribute)) ctNode.setAttribute(this.timeAttribute, change.time);
+
+ if (!ice.dom.hasClass(ctNode, this._getIceNodeClass(change.type))) ice.dom.addClass(ctNode, this._getIceNodeClass(change.type));
+
+ var style = this.getUserStyle(change.userid);
+ if (!ice.dom.hasClass(ctNode, style)) ice.dom.addClass(ctNode, style);
+ },
+
+ getChange: function (changeid) {
+ var change = null;
+ if (this._changes[changeid]) {
+ change = this._changes[changeid];
+ }
+ return change;
+ },
+
+ getNewChangeId: function () {
+ var id = ++this._uniqueIDIndex;
+ if (this._changes[id]) {
+ // Dupe.. create another..
+ id = this.getNewChangeId();
+ }
+ return id;
+ },
+
+ startBatchChange: function () {
+ this._batchChangeid = this.getNewChangeId();
+ return this._batchChangeid;
+ },
+
+ endBatchChange: function (changeid) {
+ if (changeid !== this._batchChangeid) return;
+ this._batchChangeid = null;
+ },
+
+ getCurrentRange: function () {
+ return this.selection.getRangeAt(0);
+ },
+
+ _insertNode: function (node, range, insertingDummy) {
+ var origNode = node;
+ if (!ice.dom.isBlockElement(range.startContainer) && !ice.dom.canContainTextElement(ice.dom.getBlockParent(range.startContainer, this.element)) && range.startContainer.previousSibling) {
+ range.setStart(range.startContainer.previousSibling, 0);
+
+ }
+ var startContainer = range.startContainer;
+ var parentBlock = ice.dom.isBlockElement(range.startContainer) && range.startContainer || ice.dom.getBlockParent(range.startContainer, this.element) || null;
+ if (parentBlock === this.element) {
+ var firstPar = document.createElement(this.blockEl);
+ parentBlock.appendChild(firstPar);
+ range.setStart(firstPar, 0);
+ range.collapse();
+ return this._insertNode(node, range, insertingDummy);
+ }
+ if (ice.dom.hasNoTextOrStubContent(parentBlock)) {
+ ice.dom.empty(parentBlock);
+ ice.dom.append(parentBlock, '
');
+ range.setStart(parentBlock, 0);
+ }
+
+ var ctNode = this.getIceNode(range.startContainer, 'insertType');
+ var inCurrentUserInsert = this._currentUserIceNode(ctNode);
+
+ // Do nothing, let this bubble-up to insertion handler.
+ if (insertingDummy && inCurrentUserInsert) return;
+ // If we aren't in an insert node which belongs to the current user, then create a new ins node
+ else if (!inCurrentUserInsert) node = this.createIceNode('insertType', node);
+
+ range.insertNode(node);
+ range.setEnd(node, 1);
+
+ if (insertingDummy) {
+ // Create a selection of the dummy character we inserted
+ // which will be removed after it bubbles up to the final handler.
+ range.setStart(node, 0);
+ } else {
+ range.collapse();
+ }
+
+ this.selection.addRange(range);
+ },
+
+ _handleVoidEl: function(el, range) {
+ // If `el` is or is in a void element, but not a delete
+ // then collapse the `range` and return `true`.
+ var voidEl = this._getVoidElement(el);
+ if (voidEl && !this.getIceNode(voidEl, 'deleteType')) {
+ range.collapse(true);
+ return true;
+ }
+ return false;
+ },
+
+ _deleteSelection: function (range) {
+
+ // Bookmark the range and get elements between.
+ var bookmark = new ice.Bookmark(this.env, range),
+ elements = ice.dom.getElementsBetween(bookmark.start, bookmark.end),
+ b1 = ice.dom.parents(range.startContainer, this.blockEls.join(', '))[0],
+ b2 = ice.dom.parents(range.endContainer, this.blockEls.join(', '))[0],
+ betweenBlocks = new Array();
+
+ for (var i = 0; i < elements.length; i++) {
+ var elem = elements[i];
+ if (ice.dom.isBlockElement(elem)) {
+ betweenBlocks.push(elem);
+ if (!ice.dom.canContainTextElement(elem)) {
+ // Ignore containers that are not supposed to contain text. Check children instead.
+ for (var k = 0; k < elem.childNodes.length; k++) {
+ elements.push(elem.childNodes[k]);
+ }
+ continue;
+ }
+ }
+ // Ignore empty space nodes
+ if (elem.nodeType === ice.dom.TEXT_NODE && ice.dom.getNodeTextContent(elem).length === 0) continue;
+
+ if (!this._getVoidElement(elem)) {
+ // If the element is not a text or stub node, go deeper and check the children.
+ if (elem.nodeType !== ice.dom.TEXT_NODE) {
+ // Browsers like to insert breaks into empty paragraphs - remove them
+ if (ice.dom.BREAK_ELEMENT == ice.dom.getTagName(elem)) {
+ continue;
+ }
+
+ if (ice.dom.isStubElement(elem)) {
+ this._addNodeTracking(elem, false, true);
+ continue;
+ }
+ if (ice.dom.hasNoTextOrStubContent(elem)) {
+ ice.dom.remove(elem);
+ }
+
+ for (j = 0; j < elem.childNodes.length; j++) {
+ var child = elem.childNodes[j];
+ elements.push(child);
+ }
+ continue;
+ }
+ var parentBlock = ice.dom.getBlockParent(elem);
+ this._addNodeTracking(elem, false, true, true);
+ if (ice.dom.hasNoTextOrStubContent(parentBlock)) {
+ ice.dom.remove(parentBlock);
+ }
+ }
+ }
+
+ if (this.mergeBlocks && b1 !== b2) {
+ while (betweenBlocks.length)
+ ice.dom.mergeContainers(betweenBlocks.shift(), b1);
+ ice.dom.removeBRFromChild(b2);
+ ice.dom.removeBRFromChild(b1);
+ ice.dom.mergeContainers(b2, b1);
+ }
+
+ bookmark.selectBookmark();
+// range.collapse(false);
+ range.collapse(true);
+ },
+
+ // Delete
+ _deleteRight: function (range) {
+
+ var parentBlock = ice.dom.isBlockElement(range.startContainer) && range.startContainer || ice.dom.getBlockParent(range.startContainer, this.element) || null,
+ isEmptyBlock = parentBlock ? (ice.dom.hasNoTextOrStubContent(parentBlock)) : false,
+ nextBlock = parentBlock && ice.dom.getNextContentNode(parentBlock, this.element),
+ nextBlockIsEmpty = nextBlock ? (ice.dom.hasNoTextOrStubContent(nextBlock)) : false,
+ initialContainer = range.endContainer,
+ initialOffset = range.endOffset,
+ commonAncestor = range.commonAncestorContainer,
+ nextContainer, returnValue;
+
+ // If the current block is empty then let the browser handle the delete/event.
+ if (isEmptyBlock) return false;
+
+ // Some bugs in Firefox and Webkit make the caret disappear out of text nodes, so we try to put them back in.
+ if (commonAncestor.nodeType !== ice.dom.TEXT_NODE) {
+
+ // If placed at the beginning of a container that cannot contain text, such as an ul element, place the caret at the beginning of the first item.
+ if (initialOffset === 0 && ice.dom.isBlockElement(commonAncestor) && (!ice.dom.canContainTextElement(commonAncestor))) {
+ var firstItem = commonAncestor.firstElementChild;
+ if (firstItem) {
+ range.setStart(firstItem, 0);
+ range.collapse();
+ return this._deleteRight(range);
+ }
+ }
+
+ if (commonAncestor.childNodes.length > initialOffset) {
+ var tempTextContainer = document.createTextNode(' ');
+ commonAncestor.insertBefore(tempTextContainer, commonAncestor.childNodes[initialOffset]);
+ range.setStart(tempTextContainer, 1);
+ range.collapse(true);
+ returnValue = this._deleteRight(range);
+ ice.dom.remove(tempTextContainer);
+ return returnValue;
+ } else {
+ nextContainer = ice.dom.getNextContentNode(commonAncestor, this.element);
+ range.setEnd(nextContainer, 0);
+ range.collapse();
+ return this._deleteRight(range);
+ }
+ }
+
+ // Move range to position the cursor on the inside of any adjacent container that it is going
+ // to potentially delete into or after a stub element. E.G.: test|text -> test|text or
+ // text1 |
text2 -> text1
| text2
+
+ // Merge blocks: If mergeBlocks is enabled, merge the previous and current block.
+ range.moveEnd(ice.dom.CHARACTER_UNIT, 1);
+ range.moveEnd(ice.dom.CHARACTER_UNIT, -1);
+
+ // Handle cases of the caret is at the end of a container or placed directly in a block element
+ if (initialOffset === initialContainer.data.length && (!ice.dom.hasNoTextOrStubContent(initialContainer))) {
+ nextContainer = ice.dom.getNextNode(initialContainer, this.element);
+
+ // If the next container is outside of ICE then do nothing.
+ if (!nextContainer) {
+ range.selectNodeContents(initialContainer);
+ range.collapse();
+ return false;
+ }
+
+ // If the next container is
element find the next node
+ if (ice.dom.BREAK_ELEMENT == ice.dom.getTagName(nextContainer)) {
+ nextContainer = ice.dom.getNextNode(nextContainer, this.element);
+ }
+ // If the next container is a text node, look at the parent node instead.
+ if (nextContainer.nodeType === ice.dom.TEXT_NODE) {
+ nextContainer = nextContainer.parentNode;
+ }
+
+ // If the next container is non-editable, enclose it with a delete ice node and add an empty text node after it to position the caret.
+ if (!nextContainer.isContentEditable) {
+ returnValue = this._addNodeTracking(nextContainer, false, false);
+ var emptySpaceNode = document.createTextNode('');
+ nextContainer.parentNode.insertBefore(emptySpaceNode, nextContainer.nextSibling);
+ range.selectNode(emptySpaceNode);
+ range.collapse(true);
+ return returnValue;
+ }
+
+ if (this._handleVoidEl(nextContainer, range)) return true;
+
+ // If the caret was placed directly before a stub element, enclose the element with a delete ice node.
+ if (ice.dom.isChildOf(nextContainer, parentBlock) && ice.dom.isStubElement(nextContainer)) {
+ return this._addNodeTracking(nextContainer, range, false);
+ }
+
+ }
+
+ if (this._handleVoidEl(nextContainer, range)) return true;
+
+ // If we are deleting into a no tracking containiner, then remove the content
+ if (this._getNoTrackElement(range.endContainer.parentElement)) {
+ range.deleteContents();
+ return false;
+ }
+
+ if (ice.dom.isOnBlockBoundary(range.startContainer, range.endContainer, this.element)) {
+ if (this.mergeBlocks && ice.dom.is(ice.dom.getBlockParent(nextContainer, this.element), this.blockEl)) {
+ // Since the range is moved by character, it may have passed through empty blocks.
+ // text {RANGE.START}
{RANGE.END} text
+ if (nextBlock !== ice.dom.getBlockParent(range.endContainer, this.element)) {
+ range.setEnd(nextBlock, 0);
+ }
+ // The browsers like to auto-insert breaks into empty paragraphs - remove them.
+ var elements = ice.dom.getElementsBetween(range.startContainer, range.endContainer);
+ for (var i = 0; i < elements.length; i++) {
+ ice.dom.remove(elements[i]);
+ }
+ var startContainer = range.startContainer;
+ var endContainer = range.endContainer;
+ ice.dom.remove(ice.dom.find(startContainer, 'br'));
+ ice.dom.remove(ice.dom.find(endContainer, 'br'));
+ return ice.dom.mergeBlockWithSibling(range, ice.dom.getBlockParent(range.endContainer, this.element) || parentBlock);
+ } else {
+ // If the next block is empty, remove the next block.
+ if (nextBlockIsEmpty) {
+ ice.dom.remove(nextBlock);
+ range.collapse(true);
+ return true;
+ }
+
+ // Place the caret at the start of the next block.
+ range.setStart(nextBlock, 0);
+ range.collapse(true);
+ return true;
+ }
+ }
+
+ var entireTextNode = range.endContainer;
+ var deletedCharacter = entireTextNode.splitText(range.endOffset);
+ var remainingTextNode = deletedCharacter.splitText(1);
+
+ return this._addNodeTracking(deletedCharacter, range, false);
+
+ },
+
+ // Backspace
+ _deleteLeft: function (range) {
+
+ var parentBlock = ice.dom.isBlockElement(range.startContainer) && range.startContainer || ice.dom.getBlockParent(range.startContainer, this.element) || null,
+ isEmptyBlock = parentBlock ? ice.dom.hasNoTextOrStubContent(parentBlock) : false,
+ prevBlock = parentBlock && ice.dom.getPrevContentNode(parentBlock, this.element), // || ice.dom.getBlockParent(parentBlock, this.element) || null,
+ prevBlockIsEmpty = prevBlock ? ice.dom.hasNoTextOrStubContent(prevBlock) : false,
+ initialContainer = range.startContainer,
+ initialOffset = range.startOffset,
+ commonAncestor = range.commonAncestorContainer,
+ lastSelectable, prevContainer;
+ // If the current block is empty, then let the browser handle the key/event.
+ if (isEmptyBlock) return false;
+
+ // Handle cases of the caret is at the start of a container or outside a text node
+ if (initialOffset === 0 || commonAncestor.nodeType !== ice.dom.TEXT_NODE) {
+ // If placed at the end of a container that cannot contain text, such as an ul element, place the caret at the end of the last item.
+ if (ice.dom.isBlockElement(commonAncestor) && (!ice.dom.canContainTextElement(commonAncestor))) {
+ if (initialOffset === 0) {
+ var firstItem = commonAncestor.firstElementChild;
+ if (firstItem) {
+ range.setStart(firstItem, 0);
+ range.collapse();
+ return this._deleteLeft(range);
+ }
+
+ } else {
+ var lastItem = commonAncestor.lastElementChild;
+ if (lastItem) {
+
+ lastSelectable = range.getLastSelectableChild(lastItem);
+ if (lastSelectable) {
+ range.setStart(lastSelectable, lastSelectable.data.length);
+ range.collapse();
+ return this._deleteLeft(range);
+ }
+ }
+ }
+ }
+ if (initialOffset === 0) {
+ prevContainer = ice.dom.getPrevContentNode(initialContainer, this.element);
+ } else {
+ var newOffset = initialOffset;
+ var style;
+// while(newOffset > 0){
+// prevContainer = commonAncestor.childNodes[--newOffset];
+// if(!ice.dom.hasClass(prevContainer, "del")) break;
+// prevContainer = null;
+// }
+ prevContainer = commonAncestor.childNodes[initialOffset-1];
+ }
+
+ // If the previous container is outside of ICE then do nothing.
+ if (!prevContainer) {
+ return false;
+ }
+ // Firefox finds an ice node wrapped around an image instead of the image itself sometimes, so we make sure to look at the image instead.
+ if (ice.dom.is(prevContainer, '.' + this._getIceNodeClass('insertType') + ', .' + this._getIceNodeClass('deleteType')) && prevContainer.childNodes.length > 0 && prevContainer.lastChild) {
+ prevContainer = prevContainer.lastChild;
+ }
+
+ // If the previous container is a text node, look at the parent node instead.
+ if (prevContainer.nodeType === ice.dom.TEXT_NODE) {
+ prevContainer = prevContainer.parentNode;
+ }
+
+ // If the previous container is non-editable, enclose it with a delete ice node and add an empty text node before it to position the caret.
+ if (!prevContainer.isContentEditable) {
+ var returnValue = this._addNodeTracking(prevContainer, false, true);
+ var emptySpaceNode = document.createTextNode('');
+ prevContainer.parentNode.insertBefore(emptySpaceNode, prevContainer);
+ range.selectNode(emptySpaceNode);
+ range.collapse(true);
+ return returnValue;
+ }
+
+ if (this._handleVoidEl(prevContainer, range)) return true;
+
+ // If the caret was placed directly after a stub element, enclose the element with a delete ice node.
+ if (ice.dom.isStubElement(prevContainer) && ice.dom.isChildOf(prevContainer, parentBlock) || !prevContainer.isContentEditable) {
+ return this._addNodeTracking(prevContainer, range, true);
+ }
+
+ // If the previous container is a stub element between blocks
+ // then just delete and leave the range/cursor in place.
+ if (ice.dom.isStubElement(prevContainer)) {
+ ice.dom.remove(prevContainer);
+ range.collapse(true);
+ return false;
+ }
+
+ if (prevContainer !== parentBlock && !ice.dom.isChildOf(prevContainer, parentBlock)) {
+
+ if (!ice.dom.canContainTextElement(prevContainer)) {
+ prevContainer = prevContainer.lastElementChild;
+ }
+ // Before putting the caret into the last selectable child, lets see if the last element is a stub element. If it is, we need to put the caret there manually.
+ if (prevContainer.lastChild && prevContainer.lastChild.nodeType !== ice.dom.TEXT_NODE && ice.dom.isStubElement(prevContainer.lastChild) && prevContainer.lastChild.tagName !== 'BR') {
+ range.setStartAfter(prevContainer.lastChild);
+ range.collapse(true);
+ return true;
+ }
+ // Find the last selectable part of the prevContainer. If it exists, put the caret there.
+ lastSelectable = range.getLastSelectableChild(prevContainer);
+
+ if (lastSelectable && !ice.dom.isOnBlockBoundary(range.startContainer, lastSelectable, this.element)) {
+ range.selectNodeContents(lastSelectable);
+ range.collapse();
+ return true;
+ }
+ }
+ }
+
+ // Firefox: If an image is at the start of the paragraph and the user has just deleted the image using backspace, an empty text node is created in the delete node before
+ // the image, but the caret is placed with the image. We move the caret to the empty text node and execute deleteFromLeft again.
+ if (initialOffset === 1 && !ice.dom.isBlockElement(commonAncestor) && range.startContainer.childNodes.length > 1 && range.startContainer.childNodes[0].nodeType === ice.dom.TEXT_NODE && range.startContainer.childNodes[0].data.length === 0) {
+ range.setStart(range.startContainer, 0);
+ return this._deleteLeft(range);
+ }
+
+ // Move range to position the cursor on the inside of any adjacent container that it is going
+ // to potentially delete into or before a stub element. E.G.: text| test -> text| test or
+ // text1
| text2 -> text1 |
text2
+ range.moveStart(ice.dom.CHARACTER_UNIT, -1);
+ range.moveStart(ice.dom.CHARACTER_UNIT, 1);
+
+ // If we are deleting into a no tracking containiner, then remove the content
+ if (this._getNoTrackElement(range.startContainer.parentElement)) {
+ range.deleteContents();
+ return false;
+ }
+
+ // Handles cases in which the caret is at the start of the block.
+ if (ice.dom.isOnBlockBoundary(range.startContainer, range.endContainer, this.element)) {
+
+ // If the previous block is empty, remove the previous block.
+ if (prevBlockIsEmpty) {
+ ice.dom.remove(prevBlock);
+ range.collapse();
+ return true;
+ }
+
+ // Merge blocks: If mergeBlocks is enabled, merge the previous and current block.
+ if (this.mergeBlocks && ice.dom.is(ice.dom.getBlockParent(prevContainer, this.element), this.blockEl)) {
+ // Since the range is moved by character, it may have passed through empty blocks.
+ // text {RANGE.START}
{RANGE.END} text
+ if (prevBlock !== ice.dom.getBlockParent(range.startContainer, this.element)) {
+ range.setStart(prevBlock, prevBlock.childNodes.length);
+ }
+ // The browsers like to auto-insert breaks into empty paragraphs - remove them.
+ var elements = ice.dom.getElementsBetween(range.startContainer, range.endContainer)
+ for (var i = 0; i < elements.length; i++) {
+ ice.dom.remove(elements[i]);
+ }
+ var startContainer = range.startContainer;
+ var endContainer = range.endContainer;
+ ice.dom.remove(ice.dom.find(startContainer, 'br'));
+ ice.dom.remove(ice.dom.find(endContainer, 'br'));
+ return ice.dom.mergeBlockWithSibling(range, ice.dom.getBlockParent(range.endContainer, this.element) || parentBlock);
+ }
+
+ // If the previous Block ends with a stub element, set the caret behind it.
+ if (prevBlock && prevBlock.lastChild && ice.dom.isStubElement(prevBlock.lastChild)) {
+ range.setStartAfter(prevBlock.lastChild);
+ range.collapse(true);
+ return true;
+ }
+
+ // Place the caret at the end of the previous block.
+ lastSelectable = range.getLastSelectableChild(prevBlock);
+ if (lastSelectable) {
+ range.setStart(lastSelectable, lastSelectable.data.length);
+ range.collapse(true);
+ } else if (prevBlock) {
+ range.setStart(prevBlock, prevBlock.childNodes.length);
+ range.collapse(true);
+ }
+
+ return true;
+ }
+
+ var entireTextNode = range.startContainer;
+ var deletedCharacter = entireTextNode.splitText(range.startOffset - 1);
+ var remainingTextNode = deletedCharacter.splitText(1);
+
+ return this._addNodeTracking(deletedCharacter, range, true);
+
+ },
+
+ // Marks text and other nodes for deletion
+ _addNodeTracking: function (contentNode, range, moveLeft) {
+
+ var contentAddNode = this.getIceNode(contentNode, 'insertType');
+
+ if (contentAddNode && this._currentUserIceNode(contentAddNode)) {
+ if (range && moveLeft) {
+ range.selectNode(contentNode);
+ }
+ contentNode.parentNode.removeChild(contentNode);
+ var cleanNode = ice.dom.cloneNode(contentAddNode);
+ ice.dom.remove(ice.dom.find(cleanNode, '.iceBookmark'));
+ // Remove a potential empty tracking container
+ if (contentAddNode !== null && (ice.dom.hasNoTextOrStubContent(cleanNode[0]))) {
+ var newstart = this.env.document.createTextNode('');
+ ice.dom.insertBefore(contentAddNode, newstart);
+ if (range) {
+ range.setStart(newstart, 0);
+ range.collapse(true);
+ }
+ ice.dom.replaceWith(contentAddNode, ice.dom.contents(contentAddNode));
+ }
+
+ return true;
+
+ } else if (range && this.getIceNode(contentNode, 'deleteType')) {
+ // It if the contentNode a text node, unite it with text nodes before and after it.
+ contentNode.normalize();
+
+ var found = false;
+ if (moveLeft) {
+ // Move to the left until there is valid sibling.
+ var previousSibling = ice.dom.getPrevContentNode(contentNode, this.element);
+ while (!found) {
+ ctNode = this.getIceNode(previousSibling, 'deleteType');
+ if (!ctNode) {
+ found = true;
+ } else {
+ previousSibling = ice.dom.getPrevContentNode(previousSibling, this.element);
+ }
+ }
+ if (previousSibling) {
+ var lastSelectable = range.getLastSelectableChild(previousSibling);
+ if (lastSelectable) {
+ previousSibling = lastSelectable;
+ }
+ range.setStart(previousSibling, ice.dom.getNodeCharacterLength(previousSibling));
+ range.collapse(true);
+ }
+ return true;
+ } else {
+ // Move the range to the right until there is valid sibling.
+
+ var nextSibling = ice.dom.getNextContentNode(contentNode, this.element);
+ while (!found) {
+ ctNode = this.getIceNode(nextSibling, 'deleteType');
+ if (!ctNode) {
+ found = true;
+ } else {
+ nextSibling = ice.dom.getNextContentNode(nextSibling, this.element);
+ }
+ }
+
+ if (nextSibling) {
+ range.selectNodeContents(nextSibling);
+ range.collapse(true);
+ }
+ return true;
+ }
+
+ }
+ // Webkit likes to insert empty text nodes next to elements. We remove them here.
+ if (contentNode.previousSibling && contentNode.previousSibling.nodeType === ice.dom.TEXT_NODE && contentNode.previousSibling.length === 0) {
+ contentNode.parentNode.removeChild(contentNode.previousSibling);
+ }
+ if (contentNode.nextSibling && contentNode.nextSibling.nodeType === ice.dom.TEXT_NODE && contentNode.nextSibling.length === 0) {
+ contentNode.parentNode.removeChild(contentNode.nextSibling);
+ }
+ var prevDelNode = this.getIceNode(contentNode.previousSibling, 'deleteType');
+ var nextDelNode = this.getIceNode(contentNode.nextSibling, 'deleteType');
+ var ctNode;
+
+ if (prevDelNode && this._currentUserIceNode(prevDelNode)) {
+ ctNode = prevDelNode;
+ ctNode.appendChild(contentNode);
+ if (nextDelNode && this._currentUserIceNode(nextDelNode)) {
+ var nextDelContents = ice.dom.extractContent(nextDelNode);
+ ice.dom.append(ctNode, nextDelContents);
+ nextDelNode.parentNode.removeChild(nextDelNode);
+ }
+ } else if (nextDelNode && this._currentUserIceNode(nextDelNode)) {
+ ctNode = nextDelNode;
+ ctNode.insertBefore(contentNode, ctNode.firstChild);
+ } else {
+ ctNode = this.createIceNode('deleteType');
+ contentNode.parentNode.insertBefore(ctNode, contentNode);
+ ctNode.appendChild(contentNode);
+ }
+
+ if (range) {
+ if (ice.dom.isStubElement(contentNode)) {
+ range.selectNode(contentNode);
+ } else {
+ range.selectNodeContents(contentNode);
+ }
+ if (moveLeft) {
+ range.collapse(true);
+ } else {
+ range.collapse();
+ }
+ contentNode.normalize();
+ }
+ return true;
+
+ },
+
+
+ /**
+ * Handles arrow, delete key events, and others.
+ *
+ * @param {event} e The event object.
+ * return {void|boolean} Returns false if default event needs to be blocked.
+ */
+ _handleAncillaryKey: function (e) {
+ var key = e.keyCode ? e.keyCode : e.which;
+ var browser = ice.dom.browser();
+ var preventDefault = true;
+ var shiftKey = e.shiftKey;
+ var self = this;
+ var range = self.getCurrentRange();
+ switch (key) {
+ case ice.dom.DOM_VK_DELETE:
+ preventDefault = this.deleteContents();
+ this.pluginsManager.fireKeyPressed(e);
+ break;
+ case 46:
+ // Key 46 is the DELETE key.
+ preventDefault = this.deleteContents(true);
+ this.pluginsManager.fireKeyPressed(e);
+ break;
+
+ /************************************************************************************/
+ /** BEGIN: Handling of caret movements inside hidden .ins/.del elements on Firefox **/
+ /** *Fix for carets getting stuck in .del elements when track changes are hidden **/
+ case ice.dom.DOM_VK_DOWN:
+ case ice.dom.DOM_VK_UP:
+ case ice.dom.DOM_VK_LEFT:
+ this.pluginsManager.fireCaretPositioned();
+ if (browser["type"] === "mozilla") {
+ if (!this.visible(range.startContainer)) {
+ // if Previous sibling exists in the paragraph, jump to the previous sibling
+ if(range.startContainer.parentNode.previousSibling) {
+ // When moving left and moving into a hidden element, skip it and go to the previousSibling
+ range.setEnd(range.startContainer.parentNode.previousSibling, 0);
+ range.moveEnd(ice.dom.CHARACTER_UNIT, ice.dom.getNodeCharacterLength(range.endContainer));
+ range.collapse(false);
+ }
+ // if Previous sibling doesn't exist, get out of the hidden zone by moving to the right
+ else {
+ range.setEnd(range.startContainer.parentNode.nextSibling, 0);
+ range.collapse(false);
+ }
+ }
+ }
+ preventDefault = false;
+ break;
+ case ice.dom.DOM_VK_RIGHT:
+ this.pluginsManager.fireCaretPositioned();
+ if (browser["type"] === "mozilla") {
+ if (!this.visible(range.startContainer)) {
+ if(range.startContainer.parentNode.nextSibling) {
+ // When moving right and moving into a hidden element, skip it and go to the nextSibling
+ range.setStart(range.startContainer.parentNode.nextSibling,0);
+ range.collapse(true);
+ }
+ }
+ }
+ preventDefault = false;
+ break;
+ /** END: Handling of caret movements inside hidden .ins/.del elements ***************/
+ /************************************************************************************/
+
+ case 32:
+ preventDefault = true;
+ var range = this.getCurrentRange();
+ this._moveRangeToValidTrackingPos(range, range.startContainer);
+ this.insert('\u00A0' , range);
+ break;
+ default:
+ // Ignore key.
+ preventDefault = false;
+ break;
+ } //end switch
+
+ if (preventDefault === true) {
+ ice.dom.preventDefault(e);
+ return false;
+ }
+ return true;
+ },
+
+ keyDown: function (e) {
+ if (!this.pluginsManager.fireKeyDown(e)) {
+ ice.dom.preventDefault(e);
+ return false;
+ }
+ var preventDefault = false;
+
+ if (this._handleSpecialKey(e) === false) {
+ if (ice.dom.isBrowser('msie') !== true) {
+ this._preventKeyPress = true;
+ }
+
+ return false;
+ } else if ((e.ctrlKey === true || e.metaKey === true) && (ice.dom.isBrowser('msie') === true || ice.dom.isBrowser('chrome') === true)) {
+ // IE does not fire keyPress event if ctrl is also pressed.
+ // E.g. CTRL + B (Bold) will not fire keyPress so this.plugins
+ // needs to be notified here for IE.
+ if (!this.pluginsManager.fireKeyPressed(e)) {
+ return false;
+ }
+ }
+ switch (e.keyCode) {
+ case 27:
+ // ESC
+ break;
+ default:
+ // If not Firefox then check if event is special arrow key etc.
+ // Firefox will handle this in keyPress event.
+ if (/Firefox/.test(navigator.userAgent) !== true) {
+ preventDefault = !(this._handleAncillaryKey(e));
+ }
+ break;
+ }
+
+ if (preventDefault) {
+ ice.dom.preventDefault(e);
+ return false;
+ }
+ return true;
+ },
+
+ keyPress: function (e) {
+ if (this._preventKeyPress === true) {
+ this._preventKeyPress = false;
+ return;
+ }
+
+ if (!this.pluginsManager.fireKeyPress(e)) return false;
+
+ var c = null;
+ if (e.which == null) {
+ // IE.
+ c = String.fromCharCode(e.keyCode);
+ } else if (e.which > 0) {
+ c = String.fromCharCode(e.which);
+ }
+
+ // Inside a br - most likely in a placeholder of a new block - delete before handling.
+ var range = this.getCurrentRange();
+ var br = ice.dom.parents(range.startContainer, 'br')[0] || null;
+ if (br) {
+ range.moveToNextEl(br);
+ br.parentNode.removeChild(br);
+ }
+
+ // Ice will ignore the keyPress event if CMD or CTRL key is also pressed
+ if (c !== null && e.ctrlKey !== true && e.metaKey !== true) {
+ var key = e.keyCode ? e.keyCode : e.which;
+ switch (key) {
+ case ice.dom.DOM_VK_DELETE:
+ // Handle delete key for Firefox.
+ return this._handleAncillaryKey(e);
+ case ice.dom.DOM_VK_ENTER:
+ return this._handleEnter();
+ case 32:
+ return this._handleAncillaryKey(e);
+ default:
+ // If we are in a deletion, move the range to the end/outside.
+ this._moveRangeToValidTrackingPos(range, range.startContainer);
+ return this.insert();
+ }
+ }
+
+ return this._handleAncillaryKey(e);
+ },
+
+ _handleEnter: function () {
+ var range = this.getCurrentRange();
+ if (!range.collapsed) this.deleteContents();
+ return true;
+ },
+
+ _handleSpecialKey: function (e) {
+ var keyCode = e.which;
+ if (keyCode === null) {
+ // IE.
+ keyCode = e.keyCode;
+ }
+
+ var preventDefault = false;
+ switch (keyCode) {
+ case 65:
+ // Check for CTRL/CMD + A (select all).
+ if (e.ctrlKey === true || e.metaKey === true) {
+ preventDefault = true;
+ var range = this.getCurrentRange();
+
+ if (ice.dom.isBrowser('msie') === true) {
+ var selStart = this.env.document.createTextNode('');
+ var selEnd = this.env.document.createTextNode('');
+
+ if (this.element.firstChild) {
+ ice.dom.insertBefore(this.element.firstChild, selStart);
+ } else {
+ this.element.appendChild(selStart);
+ }
+
+ this.element.appendChild(selEnd);
+
+ range.setStart(selStart, 0);
+ range.setEnd(selEnd, 0);
+ } else {
+ range.setStart(range.getFirstSelectableChild(this.element), 0);
+ var lastSelectable = range.getLastSelectableChild(this.element);
+ range.setEnd(lastSelectable, lastSelectable.length);
+ } //end if
+
+ this.selection.addRange(range);
+ } //end if
+ break;
+
+ default:
+ // Not a special key.
+ break;
+ } //end switch
+
+ if (preventDefault === true) {
+ ice.dom.preventDefault(e);
+ return false;
+ }
+
+ return true;
+ },
+
+ mouseUp: function (e, target) {
+ if (!this.pluginsManager.fireClicked(e)) return false;
+ this.pluginsManager.fireSelectionChanged(this.getCurrentRange());
+ },
+
+ mouseDown: function (e, target) {
+ if (!this.pluginsManager.fireMouseDown(e)) return false;
+ this.pluginsManager.fireCaretUpdated();
+ }
+ };
+
+ exports.ice = this.ice || {};
+ exports.ice.InlineChangeEditor = InlineChangeEditor;
+
+}).call(this);
diff --git a/demo/ice/icePlugin.js b/demo/ice/icePlugin.js
new file mode 100644
index 00000000..4f1ca9bd
--- /dev/null
+++ b/demo/ice/icePlugin.js
@@ -0,0 +1,39 @@
+(function() {
+
+var exports = this;
+
+var IcePlugin = function(ice_instance) {
+ this._ice = ice_instance;
+};
+
+IcePlugin.prototype = {
+
+ start: function() {},
+ clicked: function(e) {
+ return true;
+ },
+ mouseDown: function(e) {
+ return true;
+ },
+ keyDown: function(e) {
+ return true;
+ },
+ keyPress: function(e) {
+ return true;
+ },
+ selectionChanged: function(range) {},
+ setEnabled: function(enabled) {},
+ setDisabled: function(enabled) {},
+ caretUpdated: function() {},
+ nodeInserted: function(node, range) {},
+ nodeCreated: function(node, options) {},
+ caretPositioned: function() {},
+ remove: function() {
+ this._ice.removeKeyPressListener(this);
+ },
+ setSettings: function(settings) {}
+};
+
+exports.IcePlugin = IcePlugin;
+
+}).call(this.ice);
diff --git a/demo/ice/icePluginManager.js b/demo/ice/icePluginManager.js
new file mode 100644
index 00000000..24730158
--- /dev/null
+++ b/demo/ice/icePluginManager.js
@@ -0,0 +1,336 @@
+(function() {
+
+var exports = this;
+
+var IcePluginManager = function(ice_instance) {
+
+ this.plugins = {},
+ this.pluginConstructors = {},
+ this.keyPressListeners = {},
+ this.activePlugin = null,
+ this.pluginSets = {},
+ this.activePluginSet = null,
+
+ this._ice = ice_instance;
+};
+
+IcePluginManager.prototype = {
+
+ getPluginNames: function() {
+ var plugins = [];
+ for (var name in this.plugins) {
+ plugins.push(name);
+ }
+ return plugins;
+ },
+
+ addPluginObject: function(pluginName, pluginObj) {
+ this.plugins[pluginName] = pluginObj;
+ },
+
+ addPlugin: function(name, pluginConstructor) {
+ if (typeof pluginConstructor !== 'function') {
+ throw Error('IcePluginException: plugin must be a constructor function');
+ }
+
+ if (ice.dom.isset(this.pluginConstructors[name]) === false) {
+ this.pluginConstructors[name] = pluginConstructor;
+ }
+ },
+
+ loadPlugins: function(plugins, callback) {
+ if (plugins.length === 0) {
+ callback.call(this);
+ } else {
+ var plugin = plugins.shift();
+ if (typeof plugin === 'object') {
+ plugin = plugin.name;
+ }
+
+ if (ice.dom.isset(ice._plugin[plugin]) === true) {
+ this.addPlugin(plugin, ice._plugin[plugin]);
+ this.loadPlugins(plugins, callback);
+ } else {
+ throw new Error('plugin was not included in the page: ' + plugin);
+ }
+ }
+ },
+
+ _enableSet: function(name) {
+ this.activePluginSet = name;
+ var pSetLen = this.pluginSets[name].length;
+ for (var i = 0; i < pSetLen; i++) {
+ var plugin = this.pluginSets[name][i];
+ var pluginName = '';
+ if (typeof plugin === 'object') {
+ pluginName = plugin.name;
+ } else {
+ pluginName = plugin;
+ }
+
+ var pluginConstructor = this.pluginConstructors[pluginName];
+ if (pluginConstructor) {
+ var pluginObj = new pluginConstructor(this._ice);
+ this.plugins[pluginName] = pluginObj;
+
+ if (ice.dom.isset(plugin.settings) === true) {
+ pluginObj.setSettings(plugin.settings);
+ }
+
+ pluginObj.start();
+ }
+ }
+ },
+
+ setActivePlugin: function(name) {
+ this.activePlugin = name;
+ },
+
+ getActivePlugin: function() {
+ return this.activePlugin;
+ },
+
+ _getPluginName: function(pluginConstructor) {
+ var fn = pluginConstructor.toString();
+ var start = 'function '.length;
+ var name = fn.substr(start, (fn.indexOf('(') - start));
+ return name;
+ },
+
+ /**
+ * Removes specified plugin.
+ */
+ removePlugin: function(plugin) {
+ if (this.plugins[plugin]) {
+ // Call the remove fn of the plugin incase it needs to do cleanup.
+ this.plugins[plugin].remove();
+ }
+ },
+
+ /**
+ * Returns the plugin object for specified plugin name.
+ */
+ getPlugin: function(name) {
+ return this.plugins[name];
+
+ },
+
+ /**
+ * Add a new set of plugins.
+ */
+ usePlugins: function(name, plugins, callback) {
+ var self = this;
+ if (ice.dom.isset(plugins) === true) {
+ this.pluginSets[name] = plugins;
+ } else {
+ this.pluginSets[name] = [];
+ }
+ var clone = this.pluginSets[name].concat([]);
+ this.loadPlugins(clone, function() {
+ self._enableSet(name);
+ if(callback) callback.call(this);
+ });
+ },
+
+ disablePlugin: function(name) {
+ this.plugins[name].disable();
+ },
+
+ isPluginElement: function(element) {
+ for (var i in this.plugins) {
+ if (this.plugins[i].isPluginElement) {
+ if (this.plugins[i].isPluginElement(element) === true) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+ fireKeyPressed: function(e) {
+ if (this._fireKeyPressFns(e, 'all_keys') === false) {
+ return false;
+ }
+
+ var eKeys = [];
+ if (e.ctrlKey === true || e.metaKey === true) {
+ eKeys.push('ctrl');
+ }
+
+ if (e.shiftKey === true) {
+ eKeys.push('shift');
+ }
+
+ if (e.altKey === true) {
+ eKeys.push('alt');
+ }
+
+ switch (e.keyCode) {
+ case 13:
+ eKeys.push('enter');
+ break;
+
+ case ice.dom.DOM_VK_LEFT:
+ eKeys.push('left');
+ break;
+
+ case ice.dom.DOM_VK_RIGHT:
+ eKeys.push('right');
+ break;
+
+ case ice.dom.DOM_VK_UP:
+ eKeys.push('up');
+ break;
+
+ case ice.dom.DOM_VK_DOWN:
+ eKeys.push('down');
+ break;
+
+ case 9:
+ eKeys.push('tab');
+ break;
+
+ case ice.dom.DOM_VK_DELETE:
+ eKeys.push('delete');
+ break;
+
+ default:
+ var code;
+ if (e.keyCode) {
+ code = e.keyCode;
+ } else if (e.which) {
+ code = e.which;
+ }
+
+ // Other characters (a-z0-9..).
+ if (code) {
+ eKeys.push(String.fromCharCode(code).toLowerCase());
+ }
+ break;
+ }//end switch
+
+ var eKeysStr = eKeys.sort().join('+');
+
+ return this._fireKeyPressFns(e, eKeysStr);
+
+ },
+
+ _fireKeyPressFns: function(e, eKeysStr) {
+ if (this.keyPressListeners[eKeysStr]) {
+ var ln = this.keyPressListeners[eKeysStr].length;
+ for (var i = 0; i < ln; i++) {
+ var listener = this.keyPressListeners[eKeysStr][i];
+ var eventFn = listener.fn;
+ var plugin = listener.plugin;
+ var data = listener.data;
+
+ if (eventFn) {
+ if (ice.dom.isFn(eventFn) === true) {
+ if (eventFn.call(plugin, e, data) === true) {
+ ice.dom.preventDefault(e);
+ return false;
+ }
+ } else if (plugin[eventFn] && plugin[eventFn].call(plugin, e, data) === true) {
+ ice.dom.preventDefault(e);
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ },
+
+ fireSelectionChanged: function(range) {
+ for (var i in this.plugins) {
+ this.plugins[i].selectionChanged(range);
+ }
+ },
+
+ fireNodeInserted: function(node, range) {
+ for (var i in this.plugins) {
+ if (this.plugins[i].nodeInserted(node, range) === false) {
+ return false;
+ }
+ }
+ },
+
+ fireNodeCreated: function(node, option) {
+ for (var i in this.plugins) {
+ if (this.plugins[i].nodeCreated(node, option) === false) {
+ return false;
+ }
+ }
+ },
+
+ fireCaretPositioned: function() {
+ for (var i in this.plugins) {
+ this.plugins[i].caretPositioned()
+ }
+ },
+
+ fireClicked: function(e) {
+ var val = true;
+ for (var i in this.plugins) {
+ if (this.plugins[i].clicked(e) === false) {
+ val = false;
+ }
+ }
+ return val;
+ },
+
+ fireMouseDown: function(e) {
+ var val = true;
+ for (var i in this.plugins) {
+ if (this.plugins[i].mouseDown(e) === false) {
+ val = false;
+ }
+ }
+ return val;
+ },
+
+ fireKeyDown: function(e) {
+ var val = true;
+ for (var i in this.plugins) {
+ if (this.plugins[i].keyDown(e) === false) {
+ val = false;
+ }
+ }
+ return val;
+ },
+
+ fireKeyPress: function(e) {
+ var val = true;
+ for (var i in this.plugins) {
+ if (this.plugins[i].keyPress(e) === false) {
+ val = false;
+ }
+ }
+ return val;
+ },
+
+ fireEnabled: function(enabled) {
+ for (var i in this.plugins) {
+ this.plugins[i].setEnabled(enabled);
+ }
+ },
+
+ fireDisabled: function(disabled) {
+ for (var i in this.plugins) {
+ this.plugins[i].setDisabled(disabled);
+ }
+ },
+
+ fireCaretUpdated: function() {
+ for (var i in this.plugins) {
+ if (this.plugins[i].caretUpdated) {
+ this.plugins[i].caretUpdated();
+ }
+ }
+ }
+};
+
+exports._plugin = {};
+exports.IcePluginManager = IcePluginManager;
+
+}).call(this.ice);
diff --git a/demo/ice/plugins/IceAddTitlePlugin/IceAddTitlePlugin.js b/demo/ice/plugins/IceAddTitlePlugin/IceAddTitlePlugin.js
new file mode 100644
index 00000000..94955a8e
--- /dev/null
+++ b/demo/ice/plugins/IceAddTitlePlugin/IceAddTitlePlugin.js
@@ -0,0 +1,19 @@
+(function() {
+
+var exports = this, IceAddTitlePlugin;
+
+IceAddTitlePlugin = function(ice_instance) {
+ this._ice = ice_instance;
+};
+
+IceAddTitlePlugin.prototype = {
+ nodeCreated: function(node, option) {
+ node.setAttribute('title', (option.action || 'Modified') + ' by ' + node.getAttribute(this._ice.userNameAttribute)
+ + ' - ' + ice.dom.date('m/d/Y h:ia', parseInt(node.getAttribute(this._ice.timeAttribute))));
+ }
+};
+
+ice.dom.noInclusionInherits(IceAddTitlePlugin, ice.IcePlugin);
+exports._plugin.IceAddTitlePlugin = IceAddTitlePlugin;
+
+}).call(this.ice);
diff --git a/demo/ice/plugins/IceCopyPastePlugin/IceCopyPastePlugin.js b/demo/ice/plugins/IceCopyPastePlugin/IceCopyPastePlugin.js
new file mode 100644
index 00000000..d33b5fe7
--- /dev/null
+++ b/demo/ice/plugins/IceCopyPastePlugin/IceCopyPastePlugin.js
@@ -0,0 +1,446 @@
+(function() {
+
+var exports = this, IceCopyPastePlugin;
+
+IceCopyPastePlugin = function(ice_instance) {
+ this._ice = ice_instance;
+ this._tmpNode = null;
+ this._tmpNodeTagName = 'icepaste';
+ this._pasteId = 'icepastediv';
+ var self = this;
+
+ // API
+
+ // 'formatted' - paste will be MS Word cleaned.
+ // 'formattedClean' - paste will be MS Word cleaned, insert and
+ // delete tags will be removed keeping insert content in place,
+ // and tags not found in `preserve` will be stripped.
+ this.pasteType = 'formattedClean';
+
+ // Subset of tags that will not be stripped when pasteType
+ // is set to 'formattedClean'. Parameter is of type string with
+ // comma delimited tag and attribute definitions. For example:
+ // 'p,a[href],i[style|title],span[*]'
+ // Would allow `p`, `a`, `i` and `span` tags. The attributes for
+ // each one of these tags would be cleaned as follows: `p` tags
+ // would have all attributes removed, `a` tags will have all but
+ // `href` attributes removed, `i` tags will have all but `style`
+ // and `title` attributes removed, and `span` tags will keep all attributes.
+ this.preserve = 'p';
+
+ // Callback triggered before any paste cleaning happens
+ this.beforePasteClean = function(body) { return body; };
+
+ // Callback triggered at the end of the paste cleaning
+ this.afterPasteClean = function(body) { return body; };
+
+ // Event Listener for copying
+ ice_instance.element.oncopy = function() { return self.handleCopy.apply(self); };
+};
+
+IceCopyPastePlugin.prototype = {
+
+ setSettings: function(settings) {
+ settings = settings || {};
+ ice.dom.extend(this, settings);
+
+ this.preserve += ',' + this._tmpNodeTagName;
+ this.setupPreserved();
+ },
+
+ keyDown: function(e) {
+ if (e.metaKey !== true && e.ctrlKey !== true)
+ return;
+ if (e.keyCode == 86)
+ this.handlePaste();
+ else if (e.keyCode == 88)
+ this.handleCut();
+ return true;
+ },
+
+ handleCopy: function(e) {},
+
+ // Inserts a temporary placeholder for the current range and removes
+ // the contents of the ice element body and calls a paste handler.
+ handlePaste: function(e) {
+
+ var range = this._ice.getCurrentRange();
+
+ if (!range.collapsed) {
+ if (this._ice.isTracking) {
+ this._ice.deleteContents();
+ range = range.cloneRange();
+ } else {
+ range.deleteContents();
+ range.collapse(true);
+ }
+ }
+
+ if (this._ice.isTracking)
+ this._ice._moveRangeToValidTrackingPos(range);
+
+ if (range.startContainer == this._ice.element) {
+ // Fix a potentially empty body with a bad selection
+ var firstBlock = ice.dom.find(this._ice.element, this._ice.blockEl)[0];
+ if(!firstBlock) {
+ firstBlock = ice.dom.create('<' + this._ice.blockEl + ' >
' + this._ice.blockEl + '>');
+ this._ice.element.appendChild(firstBlock);
+ }
+ range.setStart(firstBlock, 0);
+ range.collapse(true);
+ this._ice.env.selection.addRange(range);
+ }
+
+ this._tmpNode = this._ice.env.document.createElement(this._tmpNodeTagName);
+ range.insertNode(this._tmpNode);
+
+ switch (this.pasteType) {
+ case 'formatted':
+ this.setupPaste();
+ break;
+ case 'formattedClean':
+ this.setupPaste(true);
+ break;
+ }
+
+ return true;
+ },
+
+ // Create a temporary div and set focus to it so that the browser can paste into it.
+ // Set a timeout to push a paste handler on to the end of the execution stack.
+ setupPaste: function(stripTags) {
+ var div = this.createDiv(this._pasteId),
+ self = this,
+ range = this._ice.getCurrentRange();
+
+ range.selectNodeContents(div);
+ this._ice.selection.addRange(range);
+
+ div.onpaste = function(event) {
+ setTimeout(function(){
+ self.handlePasteValue(stripTags);
+ },0);
+ event.stopPropagation();
+ };
+
+ div.focus();
+ return true;
+ },
+
+ // By the time we get here, the pasted content will already be in the body. Extract the
+ // paste, format it, remove any Microsoft or extraneous tags outside of `this.preserve`
+ // and merge the pasted content into the original fragment body.
+ handlePasteValue: function(stripTags) {
+ // Get the pasted content.
+ var doc = this._ice.env.document,
+ pasteDiv = doc.getElementById(this._pasteId),
+ html = ice.dom.getHtml(pasteDiv),
+ childBlocks = ice.dom.children('' + html + '
', this._ice.blockEl);
+ if(childBlocks.length === 1 && ice.dom.getNodeTextContent('' + html + '
') === ice.dom.getNodeTextContent(childBlocks)) {
+ html = ice.dom.getHtml(html);
+ }
+
+ html = this.beforePasteClean.call(this, html);
+
+ if(stripTags) {
+
+ // Strip out change tracking tags.
+ html = this._ice.getCleanContent(html);
+ html = this.stripPaste(html);
+ }
+
+ html = this.afterPasteClean.call(this, html);
+ html = ice.dom.trim(html);
+ var range = this._ice.getCurrentRange();
+ range.setStartAfter(this._tmpNode);
+ range.collapse(true);
+
+ var innerBlock = null, lastEl = null, newEl = null;
+ var fragment = range.createContextualFragment(html);
+ var changeid = this._ice.startBatchChange();
+
+ // If fragment contains block level elements, most likely we will need to
+ // do some splitting so we do not have P tags in P tags, etc. Split the
+ // container from current selection and then insert paste contents after it.
+ if(ice.dom.hasBlockChildren(fragment)) {
+ // Split from current selection.
+ var block = ice.dom.isChildOfTagName(this._tmpNode, this._ice.blockEl);
+ range.setEndAfter(block.lastChild);
+ this._ice.selection.addRange(range);
+ var contents = range.extractContents();
+ var newblock = doc.createElement(this._ice.blockEl);
+ newblock.appendChild(contents);
+ ice.dom.insertAfter(block, newblock);
+
+ range.setStart(newblock, 0);
+ range.collapse(true);
+ this._ice.selection.addRange(range);
+ var prevBlock = range.startContainer;
+
+ // Paste all of the children in the fragment.
+ while(fragment.firstChild) {
+ if(fragment.firstChild.nodeType === 3 && !jQuery.trim(fragment.firstChild.nodeValue)) {
+ fragment.removeChild(fragment.firstChild);
+ continue;
+ }
+ // We may have blocks with text nodes at the beginning or end. For example, this paste:
+ // textnode blocktext
blocktext
moretext
+ // In which case we wrap the leading or trailing text nodes in blocks.
+ if(ice.dom.isBlockElement(fragment.firstChild)) {
+ if(fragment.firstChild.textContent !== "") {
+ innerBlock = null;
+ var insert = null;
+ if(this._ice.isTracking) {
+ insert = this._ice.createIceNode('insertType');
+ this._ice.addChange('insertType', [insert]);
+ newEl = doc.createElement(fragment.firstChild.tagName);
+ insert.innerHTML = fragment.firstChild.innerHTML;
+ newEl.appendChild(insert);
+ } else {
+ insert = newEl = doc.createElement(fragment.firstChild.tagName);
+ newEl.innerHTML = fragment.firstChild.innerHTML;
+ }
+ lastEl = insert;
+ ice.dom.insertBefore(prevBlock, newEl);
+ }
+ fragment.removeChild(fragment.firstChild);
+ } else {
+ if(!innerBlock) {
+ // Create a new block and append an insert
+ newEl = doc.createElement(this._ice.blockEl);
+ ice.dom.insertBefore(prevBlock, newEl);
+ if(this._ice.isTracking) {
+ innerBlock = this._ice.createIceNode('insertType');
+ this._ice.addChange('insertType', [innerBlock]);
+ newEl.appendChild(innerBlock);
+ } else {
+ innerBlock = newEl;
+ }
+ }
+ lastEl = innerBlock;
+ innerBlock.appendChild(fragment.removeChild(fragment.firstChild));
+ }
+ }
+ if (!newblock.textContent) {
+ newblock.parentNode.removeChild(newblock);
+ }
+
+ } else {
+ if(this._ice.isTracking) {
+ newEl = this._ice.createIceNode('insertType', fragment);
+ this._ice.addChange('insertType', [newEl]);
+ range.insertNode(newEl);
+ lastEl = newEl;
+ } else {
+ var child;
+ while((child = fragment.firstChild)) {
+ range.insertNode(child);
+ range.setStartAfter(child);
+ range.collapse(true);
+ lastEl = child;
+ }
+ }
+ }
+ this._ice.endBatchChange(changeid);
+ pasteDiv.parentNode.removeChild(pasteDiv);
+ this._cleanup(lastEl);
+ },
+
+
+ createDiv: function(id) {
+ var doc = this._ice.env.document, // Document object of window or tinyMCE iframe
+ oldEl = doc.getElementById(id);
+ if(oldEl) {
+ ice.dom.remove(oldEl);
+ }
+
+ var div = doc.createElement('div');
+ div.id = id;
+ div.setAttribute('contentEditable', true);
+ ice.dom.setStyle(div, 'width', '1px');
+ ice.dom.setStyle(div, 'height', '1px');
+ ice.dom.setStyle(div, 'overflow', 'hidden');
+ ice.dom.setStyle(div, 'position', 'fixed');
+ ice.dom.setStyle(div, 'top', '10px');
+ ice.dom.setStyle(div, 'left', '10px');
+
+ div.appendChild(doc.createElement('br'));
+ doc.body.appendChild(div);
+ return div;
+ },
+
+ // Intercepts cut operation and handles by creating an editable div, copying the current selection
+ // into it, deleting the current selection with track changes, and selecting the contents in the
+ // editable div.
+ handleCut: function() {
+ var self = this,
+ range = this._ice.getCurrentRange();
+ if (range.collapsed) return; // If nothing is selected, there's nothing to mark deleted
+
+ this.cutElement = this.createDiv('icecut');
+ // Chrome strips out spaces between text nodes and elements node during cut
+ this.cutElement.innerHTML = range.getHTMLContents().replace(/ /g, '> ');
+
+ if (this._ice.isTracking) this._ice.deleteContents();
+ else range.deleteContents();
+
+ var crange = this._ice.env.document.createRange();
+ crange.setStart(this.cutElement.firstChild, 0);
+ crange.setEndAfter(this.cutElement.lastChild);
+
+ setTimeout(function() {
+ self.cutElement.focus();
+
+ // After the browser cuts out of the `cutElement`, reset the range and remove the cut element.
+ setTimeout(function() {
+ ice.dom.remove(self.cutElement);
+ range.setStart(range.startContainer, range.startOffset);
+ range.collapse(false);
+ self._ice.env.selection.addRange(range);
+ }, 100);
+ }, 0);
+
+ self._ice.env.selection.addRange(crange);
+ },
+
+
+ // Strips ice change tracking tags, Microsoft Word styling/content, and any
+ // tags and attributes not found in `preserve` from the given `content`.
+ stripPaste: function(content) {
+ // Clean word stuff out and strip tags that are not in `this.preserve`.
+ content = this._cleanWordPaste(content);
+ content = this.cleanPreserved(content);
+ return content;
+ },
+
+ // Parses `preserve` to setup `_tags` with a comma delimited list of all of the
+ // defined tags, and the `_attributesMap` with a mapping between the allowed tags and
+ // an array of their allowed attributes. For example, given this value:
+ // `preserve` = 'p,a[href|class],span[*]'
+ // The following will result:
+ // `_tags` = 'p,a,span'
+ // `_attributesMap` = ['p' => [], 'a' => ['href', 'class'], 'span' => ['*']]
+ setupPreserved: function() {
+ var self = this;
+ this._tags = '';
+ this._attributesMap = [];
+
+ ice.dom.each(this.preserve.split(','), function(i, tagAttr) {
+ // Extract the tag and attributes list
+ tagAttr.match(/(\w+)(\[(.+)\])?/);
+ var tag = RegExp.$1;
+ var attr = RegExp.$3;
+
+ if(self._tags) self._tags += ',';
+ self._tags += tag.toLowerCase();
+ self._attributesMap[tag] = attr.split('|');
+ });
+ },
+
+ // Cleans the given `body` by removing any tags not found in `_tags` and replacing them with
+ // their inner contents, and removes attributes from any tags that aren't mapped in `_attributesMap`.
+ cleanPreserved: function(body) {
+ var self = this;
+ var bodyel = this._ice.env.document.createElement('div');
+ bodyel.innerHTML = body;
+
+ // Strip out any tags not found in `this._tags`, replacing the tags with their inner contents.
+ bodyel = ice.dom.stripEnclosingTags(bodyel, this._tags);
+
+ // Strip out any attributes from the allowed set of tags that don't match what is in the `_attributesMap`
+ ice.dom.each(ice.dom.find(bodyel, this._tags), function(i, el) {
+ if (ice.dom.hasClass(el, 'skip-clean')) {
+ return true;
+ }
+ var tag = el.tagName.toLowerCase();
+ var attrMatches = self._attributesMap[tag];
+
+ // Kleene star - keep all of the attributes for this tag.
+ if(attrMatches[0] && attrMatches[0] === '*')
+ return true;
+
+ // Remove any foreign attributes that do not match the map.
+ if(el.hasAttributes()) {
+ var attributes = el.attributes;
+ for(var i = attributes.length - 1; i >= 0; i--) {
+ if(!ice.dom.inArray(attributes[i].name, attrMatches)) {
+ el.removeAttribute(attributes[i].name);
+ }
+ }
+ }
+ });
+ return bodyel.innerHTML;
+ },
+
+ _cleanWordPaste: function(content) {
+ // Meta and link tags.
+ content = content.replace(/<(meta|link)[^>]+>/g, "");
+
+ // Comments.
+ content = content.replace(//g, '');
+
+ // Remove style tags.
+ content = content.replace(/