From 5647df9636d5d4255e12bc0b9484c62fa1321eea Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Thu, 24 May 2012 15:45:29 +0000 Subject: [PATCH] Historylog widget for et2 --- .../class.etemplate_widget_historylog.inc.php | 55 +++ etemplate/js/et2_widget_diff.js | 165 +++++++ etemplate/js/et2_widget_historylog.js | 305 +++++++++++++ etemplate/js/etemplate2.js | 2 + etemplate/js/lib/jsdifflib/difflib.js | 407 ++++++++++++++++++ etemplate/js/lib/jsdifflib/diffview.css | 83 ++++ etemplate/js/lib/jsdifflib/diffview.js | 197 +++++++++ etemplate/templates/default/etemplate2.css | 11 + 8 files changed, 1225 insertions(+) create mode 100644 etemplate/inc/class.etemplate_widget_historylog.inc.php create mode 100644 etemplate/js/et2_widget_diff.js create mode 100644 etemplate/js/et2_widget_historylog.js create mode 100755 etemplate/js/lib/jsdifflib/difflib.js create mode 100644 etemplate/js/lib/jsdifflib/diffview.css create mode 100644 etemplate/js/lib/jsdifflib/diffview.js diff --git a/etemplate/inc/class.etemplate_widget_historylog.inc.php b/etemplate/inc/class.etemplate_widget_historylog.inc.php new file mode 100644 index 0000000000..f7e03a3766 --- /dev/null +++ b/etemplate/inc/class.etemplate_widget_historylog.inc.php @@ -0,0 +1,55 @@ +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, '<'.$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; + } + + } + } +} +?> diff --git a/etemplate/js/et2_widget_diff.js b/etemplate/js/et2_widget_diff.js new file mode 100644 index 0000000000..bd7cd62092 --- /dev/null +++ b/etemplate/js/et2_widget_diff.js @@ -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(' ') + .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('
') + .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"]); diff --git a/etemplate/js/et2_widget_historylog.js b/etemplate/js/et2_widget_historylog.js new file mode 100644 index 0000000000..a7cc595e37 --- /dev/null +++ b/etemplate/js/et2_widget_historylog.js @@ -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 = ''+_data[self.columns[i].id] + ''; + } + 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']); diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 024f7ffe08..1a7b51b67e 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -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; diff --git a/etemplate/js/lib/jsdifflib/difflib.js b/etemplate/js/lib/jsdifflib/difflib.js new file mode 100755 index 0000000000..db2c0e3ac4 --- /dev/null +++ b/etemplate/js/lib/jsdifflib/difflib.js @@ -0,0 +1,407 @@ +/*** +This is part of jsdifflib v1.0. + +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 */ +__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); + } +} diff --git a/etemplate/js/lib/jsdifflib/diffview.css b/etemplate/js/lib/jsdifflib/diffview.css new file mode 100644 index 0000000000..811a593b77 --- /dev/null +++ b/etemplate/js/lib/jsdifflib/diffview.css @@ -0,0 +1,83 @@ +/* +This is part of jsdifflib v1.0. + +Copyright 2007 - 2011 Chas Emerick . 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 +} \ No newline at end of file diff --git a/etemplate/js/lib/jsdifflib/diffview.js b/etemplate/js/lib/jsdifflib/diffview.js new file mode 100644 index 0000000000..a228a34d2b --- /dev/null +++ b/etemplate/js/lib/jsdifflib/diffview.js @@ -0,0 +1,197 @@ +/* +This is part of jsdifflib v1.0. + +Copyright 2007 - 2011 Chas Emerick . 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; + } +} \ No newline at end of file diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index d824597568..1b10ea7d0f 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -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;