Historylog widget for et2

This commit is contained in:
Nathan Gray 2012-05-24 15:45:29 +00:00
parent 88df7e232c
commit 5647df9636
8 changed files with 1225 additions and 0 deletions

View File

@ -0,0 +1,55 @@
<?php
/**
* eGroupWare eTemplate2 - History log server-side
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link http://www.egroupware.org
* @author Nathan Gray
* @copyright 2012 Nathan Gray
* @version $Id$
*/
/**
* eTemplate history log widget displays a list of changes to the current record.
* The widget is encapsulated, and only needs the record's ID, and a map of
* fields:widgets for display
*/
class etemplate_widget_historylog extends etemplate_widget
{
/**
* Fill type options in self::$request->sel_options to be used on the client
*
* @param string $cname
*/
public function beforeSendToClient($cname)
{
$form_name = self::form_name($cname, $this->id);
foreach(self::$request->content[$form_name]['status-widgets'] as $key => $type)
{
if(!is_array($type))
{
list($basetype) = explode('-',$type);
$widget = @self::factory($basetype, '<?xml version="1.0"?><'.$basetype.' type="'.$type.'"/>', $key);
$widget->id = $key;
$widget->attrs['type'] = $type;
$widget->type = $type;
if(method_exists($widget, 'beforeSendToClient'))
{
$widget->beforeSendToClient($cname);
}
}
else
{
if (!is_array(self::$request->sel_options[$key])) self::$request->sel_options[$key] = array();
self::$request->sel_options[$key] += $type;
}
}
}
}
?>

View File

@ -0,0 +1,165 @@
/**
* eGroupWare eTemplate2 - JS Diff object
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link http://www.egroupware.org
* @author Nathan Gray
* @copyright Nathan Gray 2012
* @version $Id$
*/
"use strict";
/*egw:uses
jquery.jquery;
jquery.jquery-ui;
lib/jsdifflib/difflib;
lib/jsdifflib/diffview;
et2_core_valueWidget;
*/
/**
* Class that displays the diff between two [text] values
*/
var et2_diff = et2_valueWidget.extend([et2_IDetachedDOM], {
attributes: {
"value": {
"type": "any"
}
},
init: function() {
this._super.apply(this, arguments);
this.mini = true;
this.egw().includeCSS('etemplate/js/lib/jsdifflib/diffview.css');
this.div = document.createElement("div");
jQuery(this.div).addClass('diff');
},
set_value: function(value) {
jQuery(this.div).empty();
if(value['old'] && value['new']) {
// Build diff
var old_text = difflib.stringAsLines(value['old']);
var new_text = difflib.stringAsLines(value['new']);
var sm = new difflib.SequenceMatcher(old_text, new_text);
var opcodes = sm.get_opcodes();
var view = diffview.buildView({
baseTextLines: old_text,
newTextLines: new_text,
opcodes: opcodes,
baseTextName: '',//this.egw().lang('Old value'),
newTextName: '',//this.egw().lang('New value'),
viewType: 1
});
jQuery(this.div).append(view);
if(this.mini) {
view = jQuery(view);
this.minify(view);
var self = this;
jQuery('<span class="ui-icon ui-icon-circle-plus">&nbsp;</span>')
.appendTo(self.div)
.css("cursor", "pointer")
.click({diff: view, div: self.div}, function(e) {
var diff = e.data.diff;
var div = e.data.div;
self.un_minify(diff);
var dialog_div = jQuery('<div>')
.append(diff);
dialog_div.dialog({
title: self.options.label,
width: 400,
height: 300,
modal: true,
buttons: [{text: self.egw().lang('ok'), click: function() {jQuery(this).dialog("close");}}],
close: function(event, ui) {
// Need to destroy the dialog, etemplate widget needs divs back where they were
diff.dialog("destroy");
self.minify(this);
// Put it back where it came from, or et2 will error when clear() is called
diff.prependTo(div);
}
});
});
}
}
else if(typeof value != 'object')
{
jQuery(this.div).append(value);
}
},
set_label: function(_label) {
this.options.label = _label;
},
/**
* Make the diff into a mini-diff
*/
minify: function(view) {
view = jQuery(view)
.addClass('mini')
// Dialog changes these, if resized
.width('100%').css('height', 'inherit')
.show();
jQuery('th', view).hide();
jQuery('td.equal',view).hide()
.prevAll().hide();
},
un_minify: function(view) {
jQuery(view).removeClass('mini').show();
jQuery('th',view).show();
jQuery('td.equal',view).show();
},
/**
* Code for implementing et2_IDetachedDOM
* Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree
*/
/**
* Build a list of attributes which can be set when working in the
* "detached" mode in the _attrs array which is provided
* by the calling code.
*/
getDetachedAttributes: function(_attrs) {
_attrs.push("value", "label");
},
/**
* Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be
* passed to the "setDetachedAttributes" function in the same order.
*/
getDetachedNodes: function() {
return [this.div];
},
/**
* Sets the given associative attribute->value array and applies the
* attributes to the given DOM-Node.
*
* @param _nodes is an array of nodes which has to be in the same order as
* the nodes returned by "getDetachedNodes"
* @param _values is an associative array which contains a subset of attributes
* returned by the "getDetachedAttributes" function and sets them to the
* given values.
*/
setDetachedAttributes: function(_nodes, _values) {
this.div = _nodes[0];
if(typeof _values['label'] != 'undefined')
{
this.set_label(_values['label']);
}
if(typeof _values['value'] != 'undefined')
{
this.set_value(_values['value']);
}
}
});
et2_register_widget(et2_diff, ["diff"]);

View File

@ -0,0 +1,305 @@
/**
* eGroupWare eTemplate2 - JS History log
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link http://www.egroupware.org
* @author Nathan Gray
* @copyright 2012 Nathan Gray
* @version $Id$
*/
"use strict";
/*egw:uses
jquery.jquery;
jquery.jquery-ui;
et2_core_valueWidget;
// Include the grid classes
et2_dataview;
*/
/**
* eTemplate history log widget displays a list of changes to the current record.
* The widget is encapsulated, and only needs the record's ID, and a map of
* fields:widgets for display
*/
var et2_historylog = et2_valueWidget.extend([et2_IDataProvider],{
columns: [
{'id': 'timestamp', caption: 'Date', 'width': '120px', widget_type: 'date-time'},
{'id': 'owner', caption: 'User', 'width': '150px', widget_type: 'select-account'},
{'id': 'status', caption: 'Changed', 'width': '120px', widget_type: 'select'},
{'id': 'new_value', caption: 'New Value'},
{'id': 'old_value', caption: 'Old Value'}
],
init: function() {
this._super.apply(this, arguments);
this.div = $j(document.createElement("div"))
.addClass("et2_historylog");
this.innerDiv = $j(document.createElement("div"))
.appendTo(this.div);
this._filters = {
record_id: this.options.value.id,
appname: this.options.value.app,
get_rows: 'historylog::get_rows'
};
},
doLoadingFinished: function() {
this._super.apply(this, arguments);
// Find the tab widget, if there is one
var tabs = this;
var count = 0;
do {
tabs = tabs._parent;
} while (tabs != this.getRoot() && tabs._type != 'tabbox');
if(tabs != this.getRoot())
{
// Find the tab index
for(var i = 0; i < tabs.tabData.length; i++)
{
// Find the tab
if(tabs.tabData[i].contentDiv.has(this.div).length)
{
// Bind the action to when the tab is selected
var handler = function(e) {
e.data.div.unbind("click.history");
e.data.history.finishInit();
e.data.history.dynheight.update(function(_w, _h) {
e.data.history.dataview.resize(_w, _h);
});
};
tabs.tabData[i].flagDiv.bind("click.history",{"history": this, div: tabs.tabData[i].flagDiv}, handler);
break;
}
}
}
else
{
this.finishInit();
}
},
finishInit: function() {
// Create the dynheight component which dynamically scales the inner
// container.
this.dynheight = new et2_dynheight(this.egw().window,
this.innerDiv, 250
);
// Create the outer grid container
this.dataview = new et2_dataview(this.innerDiv, this.egw());
this.dataview.setColumns(jQuery.extend(true, [],this.columns));
// Create widgets for columns that stay the same, and set up varying widgets
this.createWidgets();
// Create the gridview controller
var linkCallback = function() {};
this.controller = new et2_dataview_controller(null, this.dataview.grid,
this, this.rowCallback, linkCallback, this,
null
);
// Trigger the initial update
this.controller.update();
// Write something inside the column headers
for (var i = 0; i < this.columns.length; i++)
{
$j(this.dataview.getHeaderContainerNode(i)).text(this.columns[i].caption);
}
// Register a resize callback
var self = this;
$j(window).resize(function() {
self.dynheight.update(function(_w, _h) {
self.dataview.resize(_w, _h);
});
});
},
/**
* Destroys all
*/
destroy: function() {
// Free the widgets
for(var i = 0; i < this.columns.length; i++)
{
if(this.columns[i].widget) this.columns[i].widget.destroy();
}
for(var key in this.fields)
{
this.fields[key].widget.destroy();
}
this.diff.widget.destroy();
// Free the grid components
this.dataview.free();
this.rowProvider.free();
this.controller.free();
this.dynheight.free();
this._super.apply(this, arguments);
},
createWidgets: function() {
// Constant widgets - first 3 columns
for(var i = 0; i < this.columns.length; i++)
{
if(this.columns[i].widget_type)
{
var attrs = {'readonly': true, 'id': this.columns[i].id};
this.columns[i].widget = et2_createWidget(this.columns[i].widget_type, attrs, this);
this.columns[i].widget.transformAttributes(attrs);
this.columns[i].nodes = $j(this.columns[i].widget.getDetachedNodes());
}
}
// Per-field widgets - new value & old value
this.fields = {};
for(var key in this.options.value['status-widgets'])
{
var field = this.options.value['status-widgets'][key];
var attrs = {'readonly': true, 'id': key};
if(typeof field == 'object') attrs['select-options'] = field;
var widget = et2_createWidget(typeof field == 'string' ? field : 'select', attrs, this);
widget.transformAttributes(attrs);
this.fields[key] = {
attrs: attrs,
widget: widget,
nodes: jQuery(widget.getDetachedNodes())
};
}
// Widget for text diffs
var diff = et2_createWidget('diff', {}, this);
this.diff = {
widget: diff,
nodes: jQuery(diff.getDetachedNodes())
};
},
getDOMNode: function(_sender) {
if (_sender == this)
{
return this.div[0];
}
for (var i = 0; i < this.columns.length; i++)
{
if (_sender == this.columns[i].widget)
{
return this.dataview.getHeaderContainerNode(i);
}
}
return null;
},
dataFetch: function (_queriedRange, _callback, _context) {
// Pass the fetch call to the API
this.egw().dataFetch(
this.getInstanceManager().etemplate_exec_id,
_queriedRange,
this._filters,
this.id,
_callback,
_context
);
},
// Needed by interface
dataRegisterUID: function (_uid, _callback, _context) {
this.egw().dataRegisterUID(_uid, _callback, _context, this.getInstanceManager().etemplate_exec_id,
this.id);
},
dataUnregisterUID: function (_uid, _callback, _context) {
// Needed by interface
},
/**
* The row callback gets called by the gridview controller whenever
* the actual DOM-Nodes for a node with the given data have to be
* created.
*/
rowCallback: function(_data, _row, _idx, _entry) {
var tr = _row.getDOMNode();
jQuery(tr).attr("valign","top");
var row = this.dataview.rowProvider.getPrototype("default");
var self = this;
$j("div", row).each(function (i) {
var nodes = [];
var widget = self.columns[i].widget;
if(typeof widget == 'undefined' && typeof self.fields[_data.status] != 'undefined')
{
nodes = self.fields[_data.status].nodes.clone();
widget = self.fields[_data.status].widget;
}
else if (widget)
{
nodes = self.columns[i].nodes.clone();
}
else if (self._needsDiffWidget(_data['status'], _data[self.columns[i].id]))
{
var jthis = jQuery(this);
if(i == 3)
{
// DIff widget
widget = self.diff.widget;
nodes = self.diff.nodes.clone();
_data[self.columns[i].id] = {
'old': _data[self.columns[i+1].id],
'new': _data[self.columns[i].id]
};
// Skip column 4
jthis.parents("td").attr("colspan", 2)
.css("border-right", "none");
jthis.css("width", "100%");
if(widget) widget.setDetachedAttributes(nodes, {
value:_data[self.columns[i].id],
label: jthis.parents("td").prev().text()
});
}
else if (i == 4)
{
// Skip column 4
jthis.parents("td").remove();
}
}
else
{
nodes = '<span>'+_data[self.columns[i].id] + '</span>';
}
if(widget) widget.setDetachedAttributes(nodes, {value:_data[self.columns[i].id]});
$j(this).append(nodes);
});
$j(tr).append(row.children());
return tr;
},
/**
* How to tell if the row needs a diff widget or not
*/
_needsDiffWidget: function(columnName, value) {
return columnName == 'note' || columnName == 'description' || value && value.length > 100
},
});
et2_register_widget(et2_historylog, ['historylog']);

View File

@ -27,10 +27,12 @@
et2_widget_checkbox;
et2_widget_radiobox;
et2_widget_date;
et2_widget_diff;
et2_widget_styles;
et2_widget_html;
et2_widget_tabs;
et2_widget_tree;
et2_widget_historylog;
et2_widget_hrule;
et2_widget_image;
et2_widget_file;

View File

@ -0,0 +1,407 @@
/***
This is part of jsdifflib v1.0. <http://snowtide.com/jsdifflib>
Copyright (c) 2007, Snowtide Informatics Systems, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the Snowtide Informatics Systems nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
***/
/* Author: Chas Emerick <cemerick@snowtide.com> */
__whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true};
difflib = {
defaultJunkFunction: function (c) {
return __whitespace.hasOwnProperty(c);
},
stripLinebreaks: function (str) { return str.replace(/^[\n\r]*|[\n\r]*$/g, ""); },
stringAsLines: function (str) {
var lfpos = str.indexOf("\n");
var crpos = str.indexOf("\r");
var linebreak = ((lfpos > -1 && crpos > -1) || crpos < 0) ? "\n" : "\r";
var lines = str.split(linebreak);
for (var i = 0; i < lines.length; i++) {
lines[i] = difflib.stripLinebreaks(lines[i]);
}
return lines;
},
// iteration-based reduce implementation
__reduce: function (func, list, initial) {
if (initial != null) {
var value = initial;
var idx = 0;
} else if (list) {
var value = list[0];
var idx = 1;
} else {
return null;
}
for (; idx < list.length; idx++) {
value = func(value, list[idx]);
}
return value;
},
// comparison function for sorting lists of numeric tuples
__ntuplecomp: function (a, b) {
var mlen = Math.max(a.length, b.length);
for (var i = 0; i < mlen; i++) {
if (a[i] < b[i]) return -1;
if (a[i] > b[i]) return 1;
}
return a.length == b.length ? 0 : (a.length < b.length ? -1 : 1);
},
__calculate_ratio: function (matches, length) {
return length ? 2.0 * matches / length : 1.0;
},
// returns a function that returns true if a key passed to the returned function
// is in the dict (js object) provided to this function; replaces being able to
// carry around dict.has_key in python...
__isindict: function (dict) {
return function (key) { return dict.hasOwnProperty(key); };
},
// replacement for python's dict.get function -- need easy default values
__dictget: function (dict, key, defaultValue) {
return dict.hasOwnProperty(key) ? dict[key] : defaultValue;
},
SequenceMatcher: function (a, b, isjunk) {
this.set_seqs = function (a, b) {
this.set_seq1(a);
this.set_seq2(b);
}
this.set_seq1 = function (a) {
if (a == this.a) return;
this.a = a;
this.matching_blocks = this.opcodes = null;
}
this.set_seq2 = function (b) {
if (b == this.b) return;
this.b = b;
this.matching_blocks = this.opcodes = this.fullbcount = null;
this.__chain_b();
}
this.__chain_b = function () {
var b = this.b;
var n = b.length;
var b2j = this.b2j = {};
var populardict = {};
for (var i = 0; i < b.length; i++) {
var elt = b[i];
if (b2j.hasOwnProperty(elt)) {
var indices = b2j[elt];
if (n >= 200 && indices.length * 100 > n) {
populardict[elt] = 1;
delete b2j[elt];
} else {
indices.push(i);
}
} else {
b2j[elt] = [i];
}
}
for (var elt in populardict) {
if (populardict.hasOwnProperty(elt)) {
delete b2j[elt];
}
}
var isjunk = this.isjunk;
var junkdict = {};
if (isjunk) {
for (var elt in populardict) {
if (populardict.hasOwnProperty(elt) && isjunk(elt)) {
junkdict[elt] = 1;
delete populardict[elt];
}
}
for (var elt in b2j) {
if (b2j.hasOwnProperty(elt) && isjunk(elt)) {
junkdict[elt] = 1;
delete b2j[elt];
}
}
}
this.isbjunk = difflib.__isindict(junkdict);
this.isbpopular = difflib.__isindict(populardict);
}
this.find_longest_match = function (alo, ahi, blo, bhi) {
var a = this.a;
var b = this.b;
var b2j = this.b2j;
var isbjunk = this.isbjunk;
var besti = alo;
var bestj = blo;
var bestsize = 0;
var j = null;
var j2len = {};
var nothing = [];
for (var i = alo; i < ahi; i++) {
var newj2len = {};
var jdict = difflib.__dictget(b2j, a[i], nothing);
for (var jkey in jdict) {
if (jdict.hasOwnProperty(jkey)) {
j = jdict[jkey];
if (j < blo) continue;
if (j >= bhi) break;
newj2len[j] = k = difflib.__dictget(j2len, j - 1, 0) + 1;
if (k > bestsize) {
besti = i - k + 1;
bestj = j - k + 1;
bestsize = k;
}
}
}
j2len = newj2len;
}
while (besti > alo && bestj > blo && !isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) {
besti--;
bestj--;
bestsize++;
}
while (besti + bestsize < ahi && bestj + bestsize < bhi &&
!isbjunk(b[bestj + bestsize]) &&
a[besti + bestsize] == b[bestj + bestsize]) {
bestsize++;
}
while (besti > alo && bestj > blo && isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) {
besti--;
bestj--;
bestsize++;
}
while (besti + bestsize < ahi && bestj + bestsize < bhi && isbjunk(b[bestj + bestsize]) &&
a[besti + bestsize] == b[bestj + bestsize]) {
bestsize++;
}
return [besti, bestj, bestsize];
}
this.get_matching_blocks = function () {
if (this.matching_blocks != null) return this.matching_blocks;
var la = this.a.length;
var lb = this.b.length;
var queue = [[0, la, 0, lb]];
var matching_blocks = [];
var alo, ahi, blo, bhi, qi, i, j, k, x;
while (queue.length) {
qi = queue.pop();
alo = qi[0];
ahi = qi[1];
blo = qi[2];
bhi = qi[3];
x = this.find_longest_match(alo, ahi, blo, bhi);
i = x[0];
j = x[1];
k = x[2];
if (k) {
matching_blocks.push(x);
if (alo < i && blo < j)
queue.push([alo, i, blo, j]);
if (i+k < ahi && j+k < bhi)
queue.push([i + k, ahi, j + k, bhi]);
}
}
matching_blocks.sort(difflib.__ntuplecomp);
var i1 = j1 = k1 = block = 0;
var non_adjacent = [];
for (var idx in matching_blocks) {
if (matching_blocks.hasOwnProperty(idx)) {
block = matching_blocks[idx];
i2 = block[0];
j2 = block[1];
k2 = block[2];
if (i1 + k1 == i2 && j1 + k1 == j2) {
k1 += k2;
} else {
if (k1) non_adjacent.push([i1, j1, k1]);
i1 = i2;
j1 = j2;
k1 = k2;
}
}
}
if (k1) non_adjacent.push([i1, j1, k1]);
non_adjacent.push([la, lb, 0]);
this.matching_blocks = non_adjacent;
return this.matching_blocks;
}
this.get_opcodes = function () {
if (this.opcodes != null) return this.opcodes;
var i = 0;
var j = 0;
var answer = [];
this.opcodes = answer;
var block, ai, bj, size, tag;
var blocks = this.get_matching_blocks();
for (var idx in blocks) {
if (blocks.hasOwnProperty(idx)) {
block = blocks[idx];
ai = block[0];
bj = block[1];
size = block[2];
tag = '';
if (i < ai && j < bj) {
tag = 'replace';
} else if (i < ai) {
tag = 'delete';
} else if (j < bj) {
tag = 'insert';
}
if (tag) answer.push([tag, i, ai, j, bj]);
i = ai + size;
j = bj + size;
if (size) answer.push(['equal', ai, i, bj, j]);
}
}
return answer;
}
// this is a generator function in the python lib, which of course is not supported in javascript
// the reimplementation builds up the grouped opcodes into a list in their entirety and returns that.
this.get_grouped_opcodes = function (n) {
if (!n) n = 3;
var codes = this.get_opcodes();
if (!codes) codes = [["equal", 0, 1, 0, 1]];
var code, tag, i1, i2, j1, j2;
if (codes[0][0] == 'equal') {
code = codes[0];
tag = code[0];
i1 = code[1];
i2 = code[2];
j1 = code[3];
j2 = code[4];
codes[0] = [tag, Math.max(i1, i2 - n), i2, Math.max(j1, j2 - n), j2];
}
if (codes[codes.length - 1][0] == 'equal') {
code = codes[codes.length - 1];
tag = code[0];
i1 = code[1];
i2 = code[2];
j1 = code[3];
j2 = code[4];
codes[codes.length - 1] = [tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)];
}
var nn = n + n;
var groups = [];
for (var idx in codes) {
if (codes.hasOwnProperty(idx)) {
code = codes[idx];
tag = code[0];
i1 = code[1];
i2 = code[2];
j1 = code[3];
j2 = code[4];
if (tag == 'equal' && i2 - i1 > nn) {
groups.push([tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]);
i1 = Math.max(i1, i2-n);
j1 = Math.max(j1, j2-n);
}
groups.push([tag, i1, i2, j1, j2]);
}
}
if (groups && groups[groups.length - 1][0] == 'equal') groups.pop();
return groups;
}
this.ratio = function () {
matches = difflib.__reduce(
function (sum, triple) { return sum + triple[triple.length - 1]; },
this.get_matching_blocks(), 0);
return difflib.__calculate_ratio(matches, this.a.length + this.b.length);
}
this.quick_ratio = function () {
var fullbcount, elt;
if (this.fullbcount == null) {
this.fullbcount = fullbcount = {};
for (var i = 0; i < this.b.length; i++) {
elt = this.b[i];
fullbcount[elt] = difflib.__dictget(fullbcount, elt, 0) + 1;
}
}
fullbcount = this.fullbcount;
var avail = {};
var availhas = difflib.__isindict(avail);
var matches = numb = 0;
for (var i = 0; i < this.a.length; i++) {
elt = this.a[i];
if (availhas(elt)) {
numb = avail[elt];
} else {
numb = difflib.__dictget(fullbcount, elt, 0);
}
avail[elt] = numb - 1;
if (numb > 0) matches++;
}
return difflib.__calculate_ratio(matches, this.a.length + this.b.length);
}
this.real_quick_ratio = function () {
var la = this.a.length;
var lb = this.b.length;
return _calculate_ratio(Math.min(la, lb), la + lb);
}
this.isjunk = isjunk ? isjunk : difflib.defaultJunkFunction;
this.a = this.b = null;
this.set_seqs(a, b);
}
}

View File

@ -0,0 +1,83 @@
/*
This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib>
Copyright 2007 - 2011 Chas Emerick <cemerick@snowtide.com>. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of Chas Emerick.
*/
table.diff {
border-collapse:collapse;
border:1px solid darkgray;
white-space:pre-wrap
}
table.diff tbody {
font-family:Courier, monospace
}
table.diff tbody th {
font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif;
background:#EED;
font-size:11px;
font-weight:normal;
border:1px solid #BBC;
color:#886;
padding:.3em .5em .1em 2em;
text-align:right;
vertical-align:top
}
table.diff thead {
border-bottom:1px solid #BBC;
background:#EFEFEF;
font-family:Verdana
}
table.diff thead th.texttitle {
text-align:left
}
table.diff tbody td {
padding:0px .4em;
padding-top:.4em;
vertical-align:top;
}
table.diff .empty {
background-color:#DDD;
}
table.diff .replace {
background-color:#FD8
}
table.diff .delete {
background-color:#E99;
}
table.diff .skip {
background-color:#EFEFEF;
border:1px solid #AAA;
border-right:1px solid #BBC;
}
table.diff .insert {
background-color:#9E9
}
table.diff th.author {
text-align:right;
border-top:1px solid #BBC;
background:#EFEFEF
}

View File

@ -0,0 +1,197 @@
/*
This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib>
Copyright 2007 - 2011 Chas Emerick <cemerick@snowtide.com>. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of Chas Emerick.
*/
diffview = {
/**
* Builds and returns a visual diff view. The single parameter, `params', should contain
* the following values:
*
* - baseTextLines: the array of strings that was used as the base text input to SequenceMatcher
* - newTextLines: the array of strings that was used as the new text input to SequenceMatcher
* - opcodes: the array of arrays returned by SequenceMatcher.get_opcodes()
* - baseTextName: the title to be displayed above the base text listing in the diff view; defaults
* to "Base Text"
* - newTextName: the title to be displayed above the new text listing in the diff view; defaults
* to "New Text"
* - contextSize: the number of lines of context to show around differences; by default, all lines
* are shown
* - viewType: if 0, a side-by-side diff view is generated (default); if 1, an inline diff view is
* generated
*/
buildView: function (params) {
var baseTextLines = params.baseTextLines;
var newTextLines = params.newTextLines;
var opcodes = params.opcodes;
var baseTextName = params.baseTextName ? params.baseTextName : "Base Text";
var newTextName = params.newTextName ? params.newTextName : "New Text";
var contextSize = params.contextSize;
var inline = (params.viewType == 0 || params.viewType == 1) ? params.viewType : 0;
if (baseTextLines == null)
throw "Cannot build diff view; baseTextLines is not defined.";
if (newTextLines == null)
throw "Cannot build diff view; newTextLines is not defined.";
if (!opcodes)
throw "Canno build diff view; opcodes is not defined.";
function celt (name, clazz) {
var e = document.createElement(name);
e.className = clazz;
return e;
}
function telt (name, text) {
var e = document.createElement(name);
e.appendChild(document.createTextNode(text));
return e;
}
function ctelt (name, clazz, text) {
var e = document.createElement(name);
e.className = clazz;
e.appendChild(document.createTextNode(text));
return e;
}
var tdata = document.createElement("thead");
var node = document.createElement("tr");
tdata.appendChild(node);
if (inline) {
node.appendChild(document.createElement("th"));
node.appendChild(document.createElement("th"));
node.appendChild(ctelt("th", "texttitle", baseTextName + " vs. " + newTextName));
} else {
node.appendChild(document.createElement("th"));
node.appendChild(ctelt("th", "texttitle", baseTextName));
node.appendChild(document.createElement("th"));
node.appendChild(ctelt("th", "texttitle", newTextName));
}
tdata = [tdata];
var rows = [];
var node2;
/**
* Adds two cells to the given row; if the given row corresponds to a real
* line number (based on the line index tidx and the endpoint of the
* range in question tend), then the cells will contain the line number
* and the line of text from textLines at position tidx (with the class of
* the second cell set to the name of the change represented), and tidx + 1 will
* be returned. Otherwise, tidx is returned, and two empty cells are added
* to the given row.
*/
function addCells (row, tidx, tend, textLines, change) {
if (tidx < tend) {
row.appendChild(telt("th", (tidx + 1).toString()));
row.appendChild(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
return tidx + 1;
} else {
row.appendChild(document.createElement("th"));
row.appendChild(celt("td", "empty"));
return tidx;
}
}
function addCellsInline (row, tidx, tidx2, textLines, change) {
row.appendChild(telt("th", tidx == null ? "" : (tidx + 1).toString()));
row.appendChild(telt("th", tidx2 == null ? "" : (tidx2 + 1).toString()));
row.appendChild(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
}
for (var idx = 0; idx < opcodes.length; idx++) {
code = opcodes[idx];
change = code[0];
var b = code[1];
var be = code[2];
var n = code[3];
var ne = code[4];
var rowcnt = Math.max(be - b, ne - n);
var toprows = [];
var botrows = [];
for (var i = 0; i < rowcnt; i++) {
// jump ahead if we've alredy provided leading context or if this is the first range
if (contextSize && opcodes.length > 1 && ((idx > 0 && i == contextSize) || (idx == 0 && i == 0)) && change=="equal") {
var jump = rowcnt - ((idx == 0 ? 1 : 2) * contextSize);
if (jump > 1) {
toprows.push(node = document.createElement("tr"));
b += jump;
n += jump;
i += jump - 1;
node.appendChild(telt("th", "..."));
if (!inline) node.appendChild(ctelt("td", "skip", ""));
node.appendChild(telt("th", "..."));
node.appendChild(ctelt("td", "skip", ""));
// skip last lines if they're all equal
if (idx + 1 == opcodes.length) {
break;
} else {
continue;
}
}
}
toprows.push(node = document.createElement("tr"));
if (inline) {
if (change == "insert") {
addCellsInline(node, null, n++, newTextLines, change);
} else if (change == "replace") {
botrows.push(node2 = document.createElement("tr"));
if (b < be) addCellsInline(node, b++, null, baseTextLines, "delete");
if (n < ne) addCellsInline(node2, null, n++, newTextLines, "insert");
} else if (change == "delete") {
addCellsInline(node, b++, null, baseTextLines, change);
} else {
// equal
addCellsInline(node, b++, n++, baseTextLines, change);
}
} else {
b = addCells(node, b, be, baseTextLines, change);
n = addCells(node, n, ne, newTextLines, change);
}
}
for (var i = 0; i < toprows.length; i++) rows.push(toprows[i]);
for (var i = 0; i < botrows.length; i++) rows.push(botrows[i]);
}
rows.push(node = ctelt("th", "author", "diff view generated by "));
node.setAttribute("colspan", inline ? 3 : 4);
node.appendChild(node2 = telt("a", "jsdifflib"));
node2.setAttribute("href", "http://github.com/cemerick/jsdifflib");
tdata.push(node = document.createElement("tbody"));
for (var idx in rows) node.appendChild(rows[idx]);
node = celt("table", "diff" + (inline ? " inlinediff" : ""));
for (var idx in tdata) node.appendChild(tdata[idx]);
return node;
}
}

View File

@ -262,6 +262,17 @@ span.et2_date span {
font-size: 9pt;
}
/**
* Diff widget
*/
.diff thead,.author {
display: none;
}
.diff .ui-icon {
margin-top: -16px;
float: right;
}
/** Display a loading icon **/
.loading {
background-position: center;