jsondiffpatch
Version:
Diff & Patch for Javascript objects
282 lines (251 loc) • 8.8 kB
JavaScript
var base = require('./base');
var BaseFormatter = base.BaseFormatter;
var HtmlFormatter = function HtmlFormatter() {};
HtmlFormatter.prototype = new BaseFormatter();
function htmlEscape(text) {
var html = text;
var replacements = [
[/&/g, '&'],
[/</g, '<'],
[/>/g, '>'],
[/'/g, '''],
[/"/g, '"']
];
for (var i = 0; i < replacements.length; i++) {
html = html.replace(replacements[i][0], replacements[i][1]);
}
return html;
}
HtmlFormatter.prototype.typeFormattterErrorFormatter = function(context, err) {
context.out('<pre class="jsondiffpatch-error">' + err + '</pre>');
};
HtmlFormatter.prototype.formatValue = function(context, value) {
context.out('<pre>' + htmlEscape(JSON.stringify(value, null, 2)) + '</pre>');
};
HtmlFormatter.prototype.formatTextDiffString = function(context, value) {
var lines = this.parseTextDiff(value);
context.out('<ul class="jsondiffpatch-textdiff">');
for (var i = 0, l = lines.length; i < l; i++) {
var line = lines[i];
context.out('<li>' +
'<div class="jsondiffpatch-textdiff-location">' +
'<span class="jsondiffpatch-textdiff-line-number">' +
line.location.line +
'</span>' +
'<span class="jsondiffpatch-textdiff-char">' +
line.location.chr +
'</span>' +
'</div>' +
'<div class="jsondiffpatch-textdiff-line">');
var pieces = line.pieces;
for (var pieceIndex = 0, piecesLength = pieces.length; pieceIndex < piecesLength; pieceIndex++) {
/* global unescape */
var piece = pieces[pieceIndex];
context.out('<span class="jsondiffpatch-textdiff-' + piece.type + '">' +
htmlEscape(unescape(piece.text)) + '</span>');
}
context.out('</div></li>');
}
context.out('</ul>');
};
var adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(node) {
node = node || document;
var getElementText = function(el) {
return el.textContent || el.innerText;
};
var eachByQuery = function(el, query, fn) {
var elems = el.querySelectorAll(query);
for (var i = 0, l = elems.length; i < l; i++) {
fn(elems[i]);
}
};
var eachChildren = function(el, fn) {
for (var i = 0, l = el.children.length; i < l; i++) {
fn(el.children[i], i);
}
};
eachByQuery(node, '.jsondiffpatch-arrow', function(arrow) {
var arrowParent = arrow.parentNode;
var svg = arrow.children[0],
path = svg.children[1];
svg.style.display = 'none';
var destination = getElementText(arrowParent.querySelector('.jsondiffpatch-moved-destination'));
var container = arrowParent.parentNode;
var destinationElem;
eachChildren(container, function(child) {
if (child.getAttribute('data-key') === destination) {
destinationElem = child;
}
});
if (!destinationElem) {
return;
}
try {
var distance = destinationElem.offsetTop - arrowParent.offsetTop;
svg.setAttribute('height', Math.abs(distance) + 6);
arrow.style.top = (-8 + (distance > 0 ? 0 : distance)) + 'px';
var curve = distance > 0 ?
'M30,0 Q-10,' + Math.round(distance / 2) + ' 26,' + (distance - 4) :
'M30,' + (-distance) + ' Q-10,' + Math.round(-distance / 2) + ' 26,4';
path.setAttribute('d', curve);
svg.style.display = '';
} catch (err) {
return;
}
});
};
HtmlFormatter.prototype.rootBegin = function(context, type, nodeType) {
var nodeClass = 'jsondiffpatch-' + type +
(nodeType ? ' jsondiffpatch-child-node-type-' + nodeType : '');
context.out('<div class="jsondiffpatch-delta ' + nodeClass + '">');
};
HtmlFormatter.prototype.rootEnd = function(context) {
context.out('</div>' + (context.hasArrows ?
('<script type="text/javascript">setTimeout(' +
adjustArrows.toString() +
',10);</script>') : ''));
};
HtmlFormatter.prototype.nodeBegin = function(context, key, leftKey, type, nodeType) {
var nodeClass = 'jsondiffpatch-' + type +
(nodeType ? ' jsondiffpatch-child-node-type-' + nodeType : '');
context.out('<li class="' + nodeClass + '" data-key="' + leftKey + '">' +
'<div class="jsondiffpatch-property-name">' + leftKey + '</div>');
};
HtmlFormatter.prototype.nodeEnd = function(context) {
context.out('</li>');
};
/* jshint camelcase: false */
HtmlFormatter.prototype.format_unchanged = function(context, delta, left) {
if (typeof left === 'undefined') {
return;
}
context.out('<div class="jsondiffpatch-value">');
this.formatValue(context, left);
context.out('</div>');
};
HtmlFormatter.prototype.format_movedestination = function(context, delta, left) {
if (typeof left === 'undefined') {
return;
}
context.out('<div class="jsondiffpatch-value">');
this.formatValue(context, left);
context.out('</div>');
};
HtmlFormatter.prototype.format_node = function(context, delta, left) {
// recurse
var nodeType = (delta._t === 'a') ? 'array' : 'object';
context.out('<ul class="jsondiffpatch-node jsondiffpatch-node-type-' + nodeType + '">');
this.formatDeltaChildren(context, delta, left);
context.out('</ul>');
};
HtmlFormatter.prototype.format_added = function(context, delta) {
context.out('<div class="jsondiffpatch-value">');
this.formatValue(context, delta[0]);
context.out('</div>');
};
HtmlFormatter.prototype.format_modified = function(context, delta) {
context.out('<div class="jsondiffpatch-value jsondiffpatch-left-value">');
this.formatValue(context, delta[0]);
context.out('</div>' +
'<div class="jsondiffpatch-value jsondiffpatch-right-value">');
this.formatValue(context, delta[1]);
context.out('</div>');
};
HtmlFormatter.prototype.format_deleted = function(context, delta) {
context.out('<div class="jsondiffpatch-value">');
this.formatValue(context, delta[0]);
context.out('</div>');
};
HtmlFormatter.prototype.format_moved = function(context, delta) {
context.out('<div class="jsondiffpatch-value">');
this.formatValue(context, delta[0]);
context.out('</div><div class="jsondiffpatch-moved-destination">' + delta[1] + '</div>');
// draw an SVG arrow from here to move destination
context.out(
/*jshint multistr: true */
'<div class="jsondiffpatch-arrow" style="position: relative; left: -34px;">\
<svg width="30" height="60" style="position: absolute; display: none;">\
<defs>\
<marker id="markerArrow" markerWidth="8" markerHeight="8" refx="2" refy="4"\
orient="auto" markerUnits="userSpaceOnUse">\
<path d="M1,1 L1,7 L7,4 L1,1" style="fill: #339;" />\
</marker>\
</defs>\
<path d="M30,0 Q-10,25 26,50" style="stroke: #88f; stroke-width: 2px; fill: none;\
stroke-opacity: 0.5; marker-end: url(#markerArrow);"></path>\
</svg>\
</div>');
context.hasArrows = true;
};
HtmlFormatter.prototype.format_textdiff = function(context, delta) {
context.out('<div class="jsondiffpatch-value">');
this.formatTextDiffString(context, delta[0]);
context.out('</div>');
};
/* jshint camelcase: true */
var showUnchanged = function(show, node, delay) {
var el = node || document.body;
var prefix = 'jsondiffpatch-unchanged-';
var classes = {
showing: prefix + 'showing',
hiding: prefix + 'hiding',
visible: prefix + 'visible',
hidden: prefix + 'hidden',
};
var list = el.classList;
if (!list) {
return;
}
if (!delay) {
list.remove(classes.showing);
list.remove(classes.hiding);
list.remove(classes.visible);
list.remove(classes.hidden);
if (show === false) {
list.add(classes.hidden);
}
return;
}
if (show === false) {
list.remove(classes.showing);
list.add(classes.visible);
setTimeout(function() {
list.add(classes.hiding);
}, 10);
} else {
list.remove(classes.hiding);
list.add(classes.showing);
list.remove(classes.hidden);
}
var intervalId = setInterval(function() {
adjustArrows(el);
}, 100);
setTimeout(function() {
list.remove(classes.showing);
list.remove(classes.hiding);
if (show === false) {
list.add(classes.hidden);
list.remove(classes.visible);
} else {
list.add(classes.visible);
list.remove(classes.hidden);
}
setTimeout(function() {
list.remove(classes.visible);
clearInterval(intervalId);
}, delay + 400);
}, delay);
};
var hideUnchanged = function(node, delay) {
return showUnchanged(false, node, delay);
};
exports.HtmlFormatter = HtmlFormatter;
exports.showUnchanged = showUnchanged;
exports.hideUnchanged = hideUnchanged;
var defaultInstance;
exports.format = function(delta, left) {
if (!defaultInstance) {
defaultInstance = new HtmlFormatter();
}
return defaultInstance.format(delta, left);
};