diff --git a/www/common/diffDOM.js b/www/common/diffDOM.js
new file mode 100644
index 000000000..18ac7be12
--- /dev/null
+++ b/www/common/diffDOM.js
@@ -0,0 +1,1349 @@
+(function() {
+ "use strict";
+
+ var diffcount;
+
+ var Diff = function (options) {
+ var diff = this;
+ Object.keys(options).forEach(function(option) {
+ diff[option] = options[option];
+ });
+ };
+
+ Diff.prototype = {
+ toString: function() {
+ return JSON.stringify(this);
+ }
+
+ // TODO: compress diff output by replacing these keys with numbers or alike:
+ /* 'addAttribute' = 0,
+ 'modifyAttribute' = 1,
+ 'removeAttribute' = 2,
+ 'modifyTextElement' = 3,
+ 'relocateGroup' = 4,
+ 'removeElement' = 5,
+ 'addElement' = 6,
+ 'removeTextElement' = 7,
+ 'addTextElement' = 8,
+ 'replaceElement' = 9,
+ 'modifyValue' = 10,
+ 'modifyChecked' = 11,
+ 'modifySelected' = 12,
+ 'modifyComment' = 13,
+ 'action' = 14,
+ 'route' = 15,
+ 'oldValue' = 16,
+ 'newValue' = 17,
+ 'element' = 18,
+ 'group' = 19,
+ 'from' = 20,
+ 'to' = 21,
+ 'name' = 22,
+ 'value' = 23,
+ 'data' = 24,
+ 'attributes' = 25,
+ 'nodeName' = 26,
+ 'childNodes' = 27,
+ 'checked' = 28,
+ 'selected' = 29;*/
+ };
+
+ var SubsetMapping = function SubsetMapping(a, b) {
+ this.old = a;
+ this.new = b;
+ };
+
+ SubsetMapping.prototype = {
+ contains: function contains(subset) {
+ if (subset.length < this.length) {
+ return subset.new >= this.new && subset.new < this.new + this.length;
+ }
+ return false;
+ },
+ toString: function toString() {
+ return this.length + " element subset, first mapping: old " + this.old + " → new " + this.new;
+ }
+ };
+
+ var elementDescriptors = function(el) {
+ var output = [];
+ if (el.nodeName !== '#text' && el.nodeName !== '#comment') {
+ output.push(el.nodeName);
+ if (el.attributes) {
+ if (el.attributes.class) {
+ output.push(el.nodeName + '.' + el.attributes.class.replace(/ /g, '.'));
+ }
+ if (el.attributes.id) {
+ output.push(el.nodeName + '#' + el.attributes.id);
+ }
+ }
+
+ }
+ return output;
+ };
+
+ var findUniqueDescriptors = function(li) {
+ var uniqueDescriptors = {},
+ duplicateDescriptors = {};
+
+ li.forEach(function(node) {
+ elementDescriptors(node).forEach(function(descriptor) {
+ var inUnique = descriptor in uniqueDescriptors,
+ inDupes = descriptor in duplicateDescriptors;
+ if (!inUnique && !inDupes) {
+ uniqueDescriptors[descriptor] = true;
+ } else if (inUnique) {
+ delete uniqueDescriptors[descriptor];
+ duplicateDescriptors[descriptor] = true;
+ }
+ });
+
+ });
+
+ return uniqueDescriptors;
+ };
+
+ var uniqueInBoth = function(l1, l2) {
+ var l1Unique = findUniqueDescriptors(l1),
+ l2Unique = findUniqueDescriptors(l2),
+ inBoth = {};
+
+ Object.keys(l1Unique).forEach(function(key) {
+ if (l2Unique[key]) {
+ inBoth[key] = true;
+ }
+ });
+
+ return inBoth;
+ };
+
+ var removeDone = function(tree) {
+ delete tree.outerDone;
+ delete tree.innerDone;
+ delete tree.valueDone;
+ if (tree.childNodes) {
+ return tree.childNodes.every(removeDone);
+ } else {
+ return true;
+ }
+ };
+
+ var isEqual = function(e1, e2) {
+
+ var e1Attributes, e2Attributes;
+
+ if (!['nodeName', 'value', 'checked', 'selected', 'data'].every(function(element) {
+ if (e1[element] !== e2[element]) {
+ return false;
+ }
+ return true;
+ })) {
+ return false;
+ }
+
+ if (Boolean(e1.attributes) !== Boolean(e2.attributes)) {
+ return false;
+ }
+
+ if (Boolean(e1.childNodes) !== Boolean(e2.childNodes)) {
+ return false;
+ }
+
+ if (e1.attributes) {
+ e1Attributes = Object.keys(e1.attributes);
+ e2Attributes = Object.keys(e2.attributes);
+
+ if (e1Attributes.length !== e2Attributes.length) {
+ return false;
+ }
+ if (!e1Attributes.every(function(attribute) {
+ if (e1.attributes[attribute] !== e2.attributes[attribute]) {
+ return false;
+ }
+ })) {
+ return false;
+ }
+ }
+
+ if (e1.childNodes) {
+ if (e1.childNodes.length !== e2.childNodes.length) {
+ return false;
+ }
+ if (!e1.childNodes.every(function(childNode, index) {
+ return isEqual(childNode, e2.childNodes[index]);
+ })) {
+
+ return false;
+ }
+
+ }
+
+ return true;
+
+ };
+
+
+ var roughlyEqual = function(e1, e2, uniqueDescriptors, sameSiblings, preventRecursion) {
+ var childUniqueDescriptors, nodeList1, nodeList2;
+
+ if (!e1 || !e2) {
+ return false;
+ }
+
+ if (e1.nodeName !== e2.nodeName) {
+ return false;
+ }
+
+ if (e1.nodeName === '#text') {
+ // Note that we initially don't care what the text content of a node is,
+ // the mere fact that it's the same tag and "has text" means it's roughly
+ // equal, and then we can find out the true text difference later.
+ return preventRecursion ? true : e1.data === e2.data;
+ }
+
+
+ if (e1.nodeName in uniqueDescriptors) {
+ return true;
+ }
+
+ if (e1.attributes && e2.attributes) {
+
+ if (e1.attributes.id && e1.attributes.id === e2.attributes.id) {
+ var idDescriptor = e1.nodeName + '#' + e1.attributes.id;
+ if (idDescriptor in uniqueDescriptors) {
+ return true;
+ }
+ }
+ if (e1.attributes.class && e1.attributes.class === e2.attributes.class) {
+ var classDescriptor = e1.nodeName + '.' + e1.attributes.class.replace(/ /g, '.');
+ if (classDescriptor in uniqueDescriptors) {
+ return true;
+ }
+ }
+ }
+
+ if (sameSiblings) {
+ return true;
+ }
+
+ nodeList1 = e1.childNodes ? e1.childNodes.slice().reverse() : [];
+ nodeList2 = e2.childNodes ? e2.childNodes.slice().reverse() : [];
+
+ if (nodeList1.length !== nodeList2.length) {
+ return false;
+ }
+
+ if (preventRecursion) {
+ return nodeList1.every(function(element, index) {
+ return element.nodeName === nodeList2[index].nodeName;
+ });
+ } else {
+ // note: we only allow one level of recursion at any depth. If 'preventRecursion'
+ // was not set, we must explicitly force it to true for child iterations.
+ childUniqueDescriptors = uniqueInBoth(nodeList1, nodeList2);
+ return nodeList1.every(function(element, index) {
+ return roughlyEqual(element, nodeList2[index], childUniqueDescriptors, true, true);
+ });
+ }
+ };
+
+
+ var cloneObj = function(obj) {
+ // TODO: Do we really need to clone here? Is it not enough to just return the original object?
+ return JSON.parse(JSON.stringify(obj));
+ //return obj;
+ };
+
+ /**
+ * based on https://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Longest_common_substring#JavaScript
+ */
+ var findCommonSubsets = function(c1, c2, marked1, marked2) {
+ var lcsSize = 0,
+ index = [],
+ matches = Array.apply(null, new Array(c1.length + 1)).map(function() {
+ return [];
+ }), // set up the matching table
+ uniqueDescriptors = uniqueInBoth(c1, c2),
+ // If all of the elements are the same tag, id and class, then we can
+ // consider them roughly the same even if they have a different number of
+ // children. This will reduce removing and re-adding similar elements.
+ subsetsSame = c1.length === c2.length,
+ origin, ret;
+
+ if (subsetsSame) {
+
+ c1.some(function(element, i) {
+ var c1Desc = elementDescriptors(element),
+ c2Desc = elementDescriptors(c2[i]);
+ if (c1Desc.length !== c2Desc.length) {
+ subsetsSame = false;
+ return true;
+ }
+ c1Desc.some(function(description, i) {
+ if (description !== c2Desc[i]) {
+ subsetsSame = false;
+ return true;
+ }
+ });
+ if (!subsetsSame) {
+ return true;
+ }
+
+ });
+ }
+
+ // fill the matches with distance values
+ c1.forEach(function(c1Element, c1Index) {
+ c2.forEach(function(c2Element, c2Index) {
+ if (!marked1[c1Index] && !marked2[c2Index] && roughlyEqual(c1Element, c2Element, uniqueDescriptors, subsetsSame)) {
+ matches[c1Index + 1][c2Index + 1] = (matches[c1Index][c2Index] ? matches[c1Index][c2Index] + 1 : 1);
+ if (matches[c1Index + 1][c2Index + 1] >= lcsSize) {
+ lcsSize = matches[c1Index + 1][c2Index + 1];
+ index = [c1Index + 1, c2Index + 1];
+ }
+ } else {
+ matches[c1Index + 1][c2Index + 1] = 0;
+ }
+ });
+ });
+ if (lcsSize === 0) {
+ return false;
+ }
+ origin = [index[0] - lcsSize, index[1] - lcsSize];
+ ret = new SubsetMapping(origin[0], origin[1]);
+ ret.length = lcsSize;
+
+ return ret;
+ };
+
+ /**
+ * This should really be a predefined function in Array...
+ */
+ var makeArray = function(n, v) {
+ return Array.apply(null, new Array(n)).map(function() {
+ return v;
+ });
+ };
+
+ /**
+ * Generate arrays that indicate which node belongs to which subset,
+ * or whether it's actually an orphan node, existing in only one
+ * of the two trees, rather than somewhere in both.
+ *
+ * So if t1 =