mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-26 09:53:20 +01:00
2719 lines
75 KiB
JavaScript
2719 lines
75 KiB
JavaScript
/**
|
|
* EGroupware eTemplate2 - JS Nextmatch object
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage api
|
|
* @link http://www.egroupware.org
|
|
* @author Andreas Stöckel
|
|
* @copyright Stylite 2011
|
|
* @version $Id$
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
/*egw:uses
|
|
|
|
// Include the action system
|
|
egw_action.egw_action;
|
|
egw_action.egw_action_popup;
|
|
egw_action.egw_action_dragdrop;
|
|
egw_action.egw_menu_dhtmlx;
|
|
|
|
// Include some core classes
|
|
et2_core_widget;
|
|
et2_core_interfaces;
|
|
et2_core_DOMWidget;
|
|
|
|
// Include all widgets the nextmatch extension will create
|
|
et2_widget_template;
|
|
et2_widget_grid;
|
|
et2_widget_selectbox;
|
|
et2_widget_selectAccount;
|
|
et2_extension_customfields;
|
|
|
|
// Include all nextmatch subclasses
|
|
et2_extension_nextmatch_controller;
|
|
et2_extension_nextmatch_rowProvider;
|
|
et2_extension_nextmatch_dynheight;
|
|
|
|
// Include the grid classes
|
|
et2_dataview;
|
|
|
|
*/
|
|
|
|
/**
|
|
* Interface all special nextmatch header elements have to implement.
|
|
*/
|
|
var et2_INextmatchHeader = new Interface({
|
|
|
|
/**
|
|
* The 'setNextmatch' function is called by the parent nextmatch widget
|
|
* and tells the nextmatch header widgets which widget they should direct
|
|
* their 'sort', 'search' or 'filter' calls to.
|
|
*/
|
|
setNextmatch: function(_nextmatch) {}
|
|
});
|
|
|
|
var et2_INextmatchSortable = new Interface({
|
|
|
|
setSortmode: function(_mode) {}
|
|
|
|
});
|
|
|
|
/**
|
|
* Class which implements the "nextmatch" XET-Tag
|
|
*
|
|
* @augments et2_DOMWidget
|
|
*/
|
|
var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput],
|
|
{
|
|
attributes: {
|
|
"template": {
|
|
"name": "Template",
|
|
"type": "string",
|
|
"description": "The id of the template which contains the grid layout."
|
|
},
|
|
"hide_header": {
|
|
"name": "Hide header",
|
|
"type": "boolean",
|
|
"description": "Hide the header",
|
|
"default": false
|
|
},
|
|
"header_left": {
|
|
"name": "Left custom template",
|
|
"type": "string",
|
|
"description": "Customise the nextmatch - left side. Provided template becomes a child of nextmatch, and any input widgets with onChange can trigger the nextmatch to refresh by returning true.",
|
|
"default": ""
|
|
},
|
|
"header_right": {
|
|
"name": "Right custom template",
|
|
"type": "string",
|
|
"description": "Customise the nextmatch - right side. Provided template becomes a child of nextmatch, and any input widgets with onChange can trigger the nextmatch to refresh by returning true.",
|
|
"default": ""
|
|
},
|
|
"onselect": {
|
|
"name": "onselect",
|
|
"type": "js",
|
|
"default": et2_no_init,
|
|
"description": "JS code which gets executed when rows are selected. Can also be a app.appname.func(selected) style method"
|
|
},
|
|
"onfiledrop": {
|
|
"name": "onFileDrop",
|
|
"type": "js",
|
|
"default": et2_no_init,
|
|
"description": "JS code that gets executed when a _file_ is dropped on a row. Other drop interactions are handled by the action system. Return false to prevent the default link action."
|
|
},
|
|
"settings": {
|
|
"name": "Settings",
|
|
"type": "any",
|
|
"description": "The nextmatch settings",
|
|
"default": {}
|
|
}
|
|
},
|
|
|
|
legacyOptions: ["template","hide_header","header_left","header_right"],
|
|
createNamespace: true,
|
|
|
|
columns: [],
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @memberOf et2_nextmatch
|
|
*/
|
|
init: function() {
|
|
this._super.apply(this, arguments);
|
|
this.activeFilters = {col_filter:{}};
|
|
|
|
/*
|
|
Process selected custom fields here, so that the settings are correctly
|
|
set before the row template is parsed
|
|
*/
|
|
var prefs = this._getPreferences();
|
|
var cfs = {};
|
|
for(var i = 0; i < prefs.visible.length; i++)
|
|
{
|
|
if(prefs.visible[i].indexOf(et2_nextmatch_customfields.prototype.prefix) == 0)
|
|
{
|
|
cfs[prefs.visible[i].substr(1)] = !prefs.negated;
|
|
}
|
|
}
|
|
var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~');
|
|
if(typeof global_data == 'object' && global_data != null)
|
|
{
|
|
global_data.fields = cfs;
|
|
}
|
|
|
|
this.div = $j(document.createElement("div"))
|
|
.addClass("et2_nextmatch");
|
|
|
|
|
|
this.header = et2_createWidget("nextmatch_header_bar", {}, this);
|
|
this.innerDiv = $j(document.createElement("div"))
|
|
.appendTo(this.div);
|
|
|
|
// Create the dynheight component which dynamically scales the inner
|
|
// container.
|
|
this.dynheight = new et2_dynheight(this.getInstanceManager().DOMContainer,
|
|
this.innerDiv, 100);
|
|
|
|
// Create the outer grid container
|
|
this.dataview = new et2_dataview(this.innerDiv, this.egw());
|
|
|
|
// Blank placeholder
|
|
this.blank = $j(document.createElement("div"))
|
|
.appendTo(this.dataview.table);
|
|
|
|
// We cannot create the grid controller now, as this depends on the grid
|
|
// instance, which can first be created once we have the columns
|
|
this.controller = null;
|
|
this.rowProvider = null;
|
|
},
|
|
|
|
/**
|
|
* Destroys all
|
|
*/
|
|
destroy: function() {
|
|
// Stop autorefresh
|
|
if(this._autorefresh_timer)
|
|
{
|
|
window.clearInterval(this._autorefresh_timer);
|
|
}
|
|
|
|
// Free the grid components
|
|
this.dataview.free();
|
|
this.rowProvider.free();
|
|
this.controller.free();
|
|
this.dynheight.free();
|
|
|
|
this._super.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
* Loads the nextmatch settings
|
|
*/
|
|
transformAttributes: function(_attrs) {
|
|
this._super.apply(this, arguments);
|
|
|
|
if (this.id)
|
|
{
|
|
var entry = this.getArrayMgr("content").data;
|
|
_attrs["settings"] = {};
|
|
|
|
if (entry)
|
|
{
|
|
_attrs["settings"] = entry;
|
|
|
|
// Make sure there's an action var parameter
|
|
if(_attrs["settings"]["actions"] && !_attrs.settings["action_var"])
|
|
{
|
|
_attrs.settings.action_var = "action";
|
|
}
|
|
|
|
// Merge settings mess into attributes
|
|
for(var attr in this.attributes)
|
|
{
|
|
if(_attrs.settings[attr])
|
|
{
|
|
_attrs[attr] = _attrs.settings[attr];
|
|
delete _attrs.settings[attr];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
doLoadingFinished: function() {
|
|
this._super.apply(this, arguments);
|
|
|
|
// Register handler for dropped files, if possible
|
|
if(this.options.settings.row_id)
|
|
{
|
|
// Appname should be first part of the template name
|
|
var split = this.options.template.split('.');
|
|
var appname = split[0];
|
|
|
|
// Check link registry
|
|
if(this.egw().link_get_registry(appname))
|
|
{
|
|
var self = this;
|
|
// Register a handler
|
|
$j('table.egwGridView_grid',this.div)
|
|
.on('dragenter','tr',function(e) {
|
|
// Figure out _which_ row
|
|
var row = self.controller.getRowByNode(this);
|
|
|
|
if(!row || !row.uid)
|
|
{
|
|
return false;
|
|
}
|
|
e.stopPropagation(); e.preventDefault();
|
|
|
|
// Indicate acceptance
|
|
if(row.controller && row.controller._selectionMgr)
|
|
{
|
|
row.controller._selectionMgr.setFocused(row.uid,true);
|
|
}
|
|
return false;
|
|
})
|
|
.on('dragexit','tr', function(e) {
|
|
self.controller._selectionMgr.setFocused();
|
|
})
|
|
.on('dragover','tr',false).attr("dropzone","copy")
|
|
|
|
.on('drop', 'tr',function(e) {
|
|
self.handle_drop(e,this);
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Implements the et2_IResizeable interface - lets the dynheight manager
|
|
* update the width and height and then update the dataview container.
|
|
*/
|
|
resize: function() {
|
|
this.dynheight.update(function(_w, _h) {
|
|
this.dataview.resize(_w, _h);
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Sorts the nextmatch widget by the given ID.
|
|
*
|
|
* @param _id is the id of the data entry which should be sorted.
|
|
* @param _asc if true, the elements are sorted ascending, otherwise
|
|
* descending. If not set, the sort direction will be determined
|
|
* automatically.
|
|
*/
|
|
sortBy: function(_id, _asc, _update) {
|
|
if (typeof _update == "undefined")
|
|
{
|
|
_update = true;
|
|
}
|
|
|
|
// Create the "sort" entry in the active filters if it did not exist
|
|
// yet.
|
|
if (typeof this.activeFilters["sort"] == "undefined")
|
|
{
|
|
this.activeFilters["sort"] = {
|
|
"id": null,
|
|
"asc": true
|
|
};
|
|
}
|
|
|
|
// Determine the sort direction automatically if it is not set
|
|
if (typeof _asc == "undefined")
|
|
{
|
|
if (this.activeFilters["sort"].id == _id)
|
|
{
|
|
_asc = !this.activeFilters["sort"].asc;
|
|
}
|
|
}
|
|
|
|
// Update the entry in the activeFilters object
|
|
this.activeFilters["sort"] = {
|
|
"id": _id,
|
|
"asc": _asc
|
|
};
|
|
|
|
// Set the sortmode display
|
|
this.iterateOver(function(_widget) {
|
|
_widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none");
|
|
}, this, et2_INextmatchSortable);
|
|
|
|
if (_update)
|
|
{
|
|
this.applyFilters();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes the sort entry from the active filters object and thus returns to
|
|
* the natural sort order.
|
|
*/
|
|
resetSort: function() {
|
|
// Check whether the nextmatch widget is currently sorted
|
|
if (typeof this.activeFilters["sort"] != "undefined")
|
|
{
|
|
// Reset the sortmode
|
|
this.iterateOver(function(_widget) {
|
|
_widget.setSortmode("none");
|
|
}, this, et2_INextmatchSortable);
|
|
|
|
// Delete the "sort" filter entry
|
|
delete(this.activeFilters["sort"]);
|
|
this.applyFilters();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Apply current or modified filters on NM widget (updating rows accordingly)
|
|
*
|
|
* @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header
|
|
*/
|
|
applyFilters: function(_set) {
|
|
if(typeof this.activeFilters == "undefined")
|
|
{
|
|
this.activeFilters = {col_filter: {}};
|
|
}
|
|
if(typeof this.activeFilters.col_filter == "undefined")
|
|
{
|
|
this.activeFilters.col_filter = {};
|
|
}
|
|
|
|
if (typeof _set == 'object')
|
|
{
|
|
for(var s in _set)
|
|
{
|
|
if (s == 'col_filter')
|
|
{
|
|
for(var c in _set.col_filter)
|
|
{
|
|
this.activeFilters.col_filter[c] = _set.col_filter[c];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.activeFilters[s] = _set[s];
|
|
}
|
|
}
|
|
}
|
|
|
|
this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters);
|
|
|
|
// Update the filters in the grid controller
|
|
this.controller.setFilters(this.activeFilters);
|
|
|
|
// Update the header
|
|
this.header.setFilters(this.activeFilters);
|
|
|
|
// Update any column filters
|
|
this.iterateOver(function(column) {
|
|
// Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter
|
|
if(typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) return;
|
|
|
|
if(typeof column.set_value != "undefined" && column.id)
|
|
{
|
|
column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]);
|
|
}
|
|
if (column.id && typeof column.get_value == "function")
|
|
{
|
|
this[column.id] = column.get_value();
|
|
}
|
|
}, this.activeFilters.col_filter, et2_INextmatchHeader);
|
|
|
|
// Trigger an update
|
|
this.controller.update(true);
|
|
},
|
|
|
|
/**
|
|
* Refresh given rows for specified change
|
|
*
|
|
* Change type parameters allows for quicker refresh then complete server side reload:
|
|
* - update: request just modified data from given rows. Sorting is not considered,
|
|
* so if the sort field is changed, the row will not be moved.
|
|
* - edit: rows changed, but sorting may be affected. Requires full reload.
|
|
* - delete: just delete the given rows clientside (no server interaction neccessary)
|
|
* - add: requires full reload
|
|
*
|
|
* @param {string[]|string} _row_ids rows to refresh
|
|
* @param {?string} _type "update", "edit", "delete" or "add"
|
|
*
|
|
* @see jsapi.egw_refresh()
|
|
* @fires refresh from the widget itself
|
|
*/
|
|
refresh: function(_row_ids, _type) {
|
|
// Framework trying to refresh, but nextmatch not fully initialized
|
|
if(this.controller === null || !this.div.is(':visible'))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (typeof _type == 'undefined') _type = 'edit';
|
|
if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids];
|
|
if (typeof _row_ids == "undefined" || _row_ids === null)
|
|
{
|
|
this.applyFilters();
|
|
|
|
// Trigger an event so app code can act on it
|
|
$j(this).triggerHandler("refresh",[this]);
|
|
|
|
return;
|
|
}
|
|
|
|
// Use jsapi data module to update
|
|
var list = this.options.settings.get_rows.split('.', 2);
|
|
if (list.length < 2) list = this.options.settings.get_rows.split('_', 2); // support "app_something::method"
|
|
var app = list[0];
|
|
|
|
if(_type == "delete")
|
|
{
|
|
// Record current & next index
|
|
var uid = app + "::" + _row_ids[0];
|
|
var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid);
|
|
var next = (entry.ao?entry.ao.getNext(_row_ids.length):null);
|
|
if(next == null || !next.id)
|
|
{
|
|
// No next, select previous
|
|
next = (entry.ao?entry.ao.getPrevious(1):null);
|
|
}
|
|
|
|
// Stop automatic updating
|
|
this.dataview.grid.doInvalidate = false;
|
|
for(var i = 0; i < _row_ids.length; i++)
|
|
{
|
|
uid = app + "::" + _row_ids[i];
|
|
entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid);
|
|
|
|
// Unselect
|
|
this.controller._selectionMgr.setSelected(uid,false);
|
|
|
|
// Delete from internal references
|
|
this.controller.deleteRow(uid);
|
|
}
|
|
|
|
// Select & focus next row
|
|
if(next && next.id)
|
|
{
|
|
this.controller._selectionMgr.setSelected(next.id,true);
|
|
this.controller._selectionMgr.setFocused(next.id,true);
|
|
}
|
|
|
|
// Update the count
|
|
var total = this.dataview.grid._total - _row_ids.length;
|
|
this.dataview.grid.setTotalCount(total);
|
|
// Re-enable automatic updating
|
|
this.dataview.grid.doInvalidate = true;
|
|
this.dataview.grid.invalidate();
|
|
}
|
|
|
|
id_loop:
|
|
for(var i = 0; i < _row_ids.length; i++)
|
|
{
|
|
var uid = app + "::" + _row_ids[i];
|
|
switch(_type)
|
|
{
|
|
case "update":
|
|
if(!this.egw().dataRefreshUID(uid))
|
|
{
|
|
// Could not update just that row
|
|
this.applyFilters();
|
|
break id_loop;
|
|
}
|
|
break;
|
|
case "delete":
|
|
// Handled above, more code to execute after loop
|
|
break;
|
|
case "edit":
|
|
case "add":
|
|
default:
|
|
// Trigger refresh
|
|
this.applyFilters();
|
|
break id_loop;
|
|
}
|
|
}
|
|
// Trigger an event so app code can act on it
|
|
$j(this).triggerHandler("refresh",[this,_row_ids,_type]);
|
|
},
|
|
|
|
/**
|
|
* Gets the selection
|
|
*
|
|
* @return Object { ids: [UIDs], inverted: boolean}
|
|
*/
|
|
getSelection: function() {
|
|
var selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null;
|
|
if(typeof selected == "object" && selected != null)
|
|
{
|
|
return selected;
|
|
}
|
|
return {ids:[],all:false};
|
|
},
|
|
|
|
/**
|
|
* Event handler for when the selection changes
|
|
*
|
|
* If the onselect attribute was set to a string with javascript code, it will
|
|
* be executed "legacy style". You can get the selected values with getSelection().
|
|
* If the onselect attribute is in app.appname.function style, it will be called
|
|
* with the nextmatch and an array of selected row IDs.
|
|
*
|
|
* The array can be empty, if user cleared the selection.
|
|
*
|
|
* @param action ActionObject From action system. Ignored.
|
|
* @param senders ActionObjectImplemetation From action system. Ignored.
|
|
*/
|
|
onselect: function(action,senders) {
|
|
// Execute the JS code connected to the event handler
|
|
if (typeof this.options.onselect == 'function')
|
|
{
|
|
return this.options.onselect.call(this, this.getSelection().ids, this);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Generates the column caption for the given column widget
|
|
*
|
|
* @param {et2_widget} _widget
|
|
*/
|
|
_genColumnCaption: function(_widget) {
|
|
var result = null;
|
|
|
|
if(typeof _widget._genColumnCaption == "function") return _widget._genColumnCaption();
|
|
|
|
_widget.iterateOver(function(_widget) {
|
|
var label = (_widget.options.label ? _widget.options.label : _widget.options.empty_label);
|
|
if (!label) return; // skip empty, undefined or null labels
|
|
if (!result)
|
|
{
|
|
result = label;
|
|
}
|
|
else
|
|
{
|
|
result += ", " + label;
|
|
}
|
|
}, this, et2_INextmatchHeader);
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Generates the column name (internal) for the given column widget
|
|
* Used in preferences to refer to the columns by name instead of position
|
|
*
|
|
* See _getColumnCaption() for human fiendly captions
|
|
*
|
|
* @param {et2_widget} _widget
|
|
*/
|
|
_getColumnName: function(_widget) {
|
|
if(typeof _widget._getColumnName == 'function') return _widget._getColumnName();
|
|
|
|
var name = _widget.id;
|
|
var child_names = [];
|
|
var children = _widget.getChildren();
|
|
for(var i = 0; i < children.length; i++) {
|
|
if(children[i].id) child_names.push(children[i].id);
|
|
}
|
|
|
|
var colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_");
|
|
if(colName == "") {
|
|
this.egw().debug("info", "Unable to generate nm column name for ", _widget);
|
|
}
|
|
return colName;
|
|
},
|
|
|
|
|
|
/**
|
|
* Retrieve the user's preferences for this nextmatch merged with defaults
|
|
* Column display, column size, etc.
|
|
*/
|
|
_getPreferences: function() {
|
|
// Read preference or default for column visibility
|
|
var negated = false;
|
|
var columnPreference = "";
|
|
if(this.options.settings.default_cols)
|
|
{
|
|
negated = this.options.settings.default_cols[0] == "!";
|
|
columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols;
|
|
}
|
|
if(this.options.settings.selectcols)
|
|
{
|
|
columnPreference = this.options.settings.selectcols;
|
|
}
|
|
if(!this.options.settings.columnselection_pref)
|
|
{
|
|
// Set preference name so changes are saved
|
|
this.options.settings.columnselection_pref = this.options.template;
|
|
}
|
|
if(this.options.settings.columnselection_pref) {
|
|
var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, ".");
|
|
var app = list[0];
|
|
// 'nextmatch-' prefix is there in preference name, but not in setting, so add it in
|
|
var pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, list[0]);
|
|
if(pref)
|
|
{
|
|
negated = (pref[0] == "!");
|
|
columnPreference = negated ? pref.substring(1) : pref;
|
|
}
|
|
}
|
|
|
|
// If no column preference or default set, use all columns
|
|
if(typeof columnPreference =="string" && columnPreference.length == 0)
|
|
{
|
|
columnDisplay = {};
|
|
negated = true;
|
|
}
|
|
|
|
var columnDisplay = typeof columnPreference === "string"
|
|
? et2_csvSplit(columnPreference,null,",") : columnPreference;
|
|
|
|
// Adjusted column sizes
|
|
var size = {};
|
|
if(this.options.settings.columnselection_pref && app)
|
|
{
|
|
size = this.egw().preference("nextmatch-"+this.options.settings.columnselection_pref+"-size", app);
|
|
}
|
|
if(!size) size = {};
|
|
return {
|
|
visible: columnDisplay,
|
|
visible_negated: negated,
|
|
size: size
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Apply stored user preferences to discovered columns
|
|
*/
|
|
_applyUserPreferences: function(_row, _colData) {
|
|
var prefs = this._getPreferences();
|
|
var columnDisplay = prefs.visible;
|
|
var size = prefs.size;
|
|
var negated = prefs.visible_negated;
|
|
|
|
// Add in display preferences
|
|
if(columnDisplay && columnDisplay.length > 0)
|
|
{
|
|
RowLoop:
|
|
for(var i = 0; i < _row.length; i++)
|
|
{
|
|
if(_row[i].disabled)
|
|
{
|
|
_colData[i].disabled = true;
|
|
continue;
|
|
}
|
|
|
|
// Customfields needs special processing
|
|
if(_row[i].widget.instanceOf(et2_nextmatch_customfields))
|
|
{
|
|
// Find cf field
|
|
for(var j = 0; j < columnDisplay.length; j++)
|
|
{
|
|
if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) {
|
|
_row[i].widget.options.fields = {};
|
|
for(var k = i; k < columnDisplay.length; k++)
|
|
{
|
|
if(columnDisplay[k].indexOf(_row[i].widget.prefix) == 0)
|
|
{
|
|
_row[i].widget.options.fields[columnDisplay[k].substr(1)] = true;
|
|
}
|
|
}
|
|
// Resets field visibility too
|
|
_row[i].widget._getColumnName();
|
|
_colData[i].disabled = negated || jQuery.isEmptyObject(_row[i].widget.options.fields);
|
|
continue RowLoop;
|
|
}
|
|
}
|
|
}
|
|
|
|
var colName = this._getColumnName(_row[i].widget);
|
|
if(!colName) continue;
|
|
|
|
if(size[colName]) _colData[i].width = size[colName];
|
|
for(var j = 0; j < columnDisplay.length; j++)
|
|
{
|
|
if(columnDisplay[j] == colName)
|
|
{
|
|
_colData[i].disabled = negated;
|
|
|
|
continue RowLoop;
|
|
}
|
|
}
|
|
_colData[i].disabled = !negated;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Take current column display settings and store them in this.egw().preferences
|
|
* for next time
|
|
*/
|
|
_updateUserPreferences: function() {
|
|
var colMgr = this.dataview.getColumnMgr();
|
|
if(!this.options.settings.columnselection_pref) {
|
|
this.options.settings.columnselection_pref = this.options.template;
|
|
}
|
|
|
|
var visibility = colMgr.getColumnVisibilitySet();
|
|
var colDisplay = [];
|
|
var colSize = {};
|
|
var custom_fields = [];
|
|
|
|
// visibility is indexed by internal ID, widget is referenced by position, preference needs name
|
|
for(var i = 0; i < colMgr.columns.length; i++)
|
|
{
|
|
var widget = this.columns[i].widget;
|
|
var colName = this._getColumnName(widget);
|
|
if(colName) {
|
|
// Server side wants each cf listed as a seperate column
|
|
if(widget.instanceOf(et2_nextmatch_customfields))
|
|
{
|
|
// Just the ID for server side, not the whole nm name - some apps use it to skip custom fields
|
|
colName = widget.id;
|
|
for(var name in widget.options.fields) {
|
|
if(widget.options.fields[name]) custom_fields.push(widget.prefix+name);
|
|
}
|
|
}
|
|
if(visibility[colMgr.columns[i].id].visible) colDisplay.push(colName);
|
|
|
|
// When saving sizes, only save columns with explicit values, preserving relative vs fixed
|
|
// Others will be left to flex if width changes or more columns are added
|
|
if(colMgr.columns[i].relativeWidth)
|
|
{
|
|
colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%";
|
|
}
|
|
else if (colMgr.columns[i].fixedWidth)
|
|
{
|
|
colSize[colName] = colMgr.columns[i].fixedWidth;
|
|
}
|
|
} else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) {
|
|
this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id);
|
|
}
|
|
}
|
|
|
|
var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, ".");
|
|
var app = list[0];
|
|
|
|
// Server side wants each cf listed as a seperate column
|
|
jQuery.merge(colDisplay, custom_fields);
|
|
|
|
// Save visible columns
|
|
// 'nextmatch-' prefix is there in preference name, but not in setting, so add it in
|
|
egw.set_preference(app, "nextmatch-"+this.options.settings.columnselection_pref, colDisplay.join(","));
|
|
|
|
// Save adjusted column sizes
|
|
egw.set_preference(app, "nextmatch-"+this.options.settings.columnselection_pref+"-size", colSize);
|
|
|
|
// Update query value, so data source can use visible columns to exclude expensive sub-queries
|
|
var oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : [];
|
|
|
|
this.activeFilters.selectcols = colDisplay;
|
|
|
|
// We don't need to re-query if they've removed a column
|
|
var changed = [];
|
|
ColLoop:
|
|
for(var i = 0; i < colDisplay.length; i++)
|
|
{
|
|
for(var j = 0; j < oldCols.length; j++) {
|
|
if(colDisplay[i] == oldCols[j]) continue ColLoop;
|
|
}
|
|
changed.push(colDisplay[i]);
|
|
}
|
|
if(changed.length)
|
|
{
|
|
this.applyFilters();
|
|
}
|
|
},
|
|
|
|
_parseHeaderRow: function(_row, _colData) {
|
|
|
|
// Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget
|
|
|
|
for (var x = 0; x < _row.length; x++)
|
|
{
|
|
if(!_row[x].widget)
|
|
{
|
|
_row[x].widget = et2_createWidget("label");
|
|
}
|
|
}
|
|
|
|
// Get column display preference
|
|
this._applyUserPreferences(_row, _colData);
|
|
|
|
// Go over the header row and create the column entries
|
|
this.columns = new Array(_row.length);
|
|
var columnData = new Array(_row.length);
|
|
|
|
// No action columns in et2
|
|
var remove_action_index = null;
|
|
|
|
for (var x = 0; x < _row.length; x++)
|
|
{
|
|
this.columns[x] = {
|
|
"widget": _row[x].widget
|
|
};
|
|
|
|
|
|
columnData[x] = {
|
|
"id": "col_" + x,
|
|
"caption": this._genColumnCaption(_row[x].widget),
|
|
"visibility": (!_colData[x] || _colData[x].disabled) ?
|
|
ET2_COL_VISIBILITY_INVISIBLE : ET2_COL_VISIBILITY_VISIBLE,
|
|
"width": _colData[x] ? _colData[x].width : 0
|
|
};
|
|
|
|
// No action columns in et2
|
|
var colName = this._getColumnName(_row[x].widget);
|
|
if(colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all')
|
|
{
|
|
remove_action_index = x;
|
|
continue;
|
|
}
|
|
|
|
}
|
|
|
|
// Remove action column
|
|
if(remove_action_index != null)
|
|
{
|
|
this.columns.splice(remove_action_index,remove_action_index);
|
|
columnData.splice(remove_action_index,remove_action_index);
|
|
_colData.splice(remove_action_index,remove_action_index);
|
|
}
|
|
|
|
// Create the column manager and update the grid container
|
|
this.dataview.setColumns(columnData);
|
|
|
|
for (var x = 0; x < _row.length; x++)
|
|
{
|
|
// Append the widget to this container
|
|
this.addChild(_row[x].widget);
|
|
}
|
|
|
|
// Create the nextmatch row provider
|
|
this.rowProvider = new et2_nextmatch_rowProvider(
|
|
this.dataview.rowProvider, this._getSubgrid, this);
|
|
|
|
// Register handler to update preferences when column properties are changed
|
|
var self = this;
|
|
this.dataview.onUpdateColumns = function() {
|
|
// Use apply to make sure context is there
|
|
self._updateUserPreferences.apply(self);
|
|
|
|
// Allow column widgets a chance to resize
|
|
self.iterateOver(function(widget) {widget.resize();}, self, et2_IResizeable);
|
|
};
|
|
|
|
// Register handler for column selection popup, or disable
|
|
if(this.selectPopup)
|
|
{
|
|
this.selectPopup.remove();
|
|
this.selectPopup = null;
|
|
}
|
|
if(this.options.settings.no_columnselection)
|
|
{
|
|
this.dataview.selectColumnsClick = function() {return false;};
|
|
$j('span.selectcols',this.dataview.headTr).hide();
|
|
}
|
|
else
|
|
{
|
|
$j('span.selectcols',this.dataview.headTr).show();
|
|
this.dataview.selectColumnsClick = function(event) {
|
|
self._selectColumnsClick(event);
|
|
};
|
|
}
|
|
},
|
|
|
|
_parseDataRow: function(_row, _rowData, _colData) {
|
|
var columnWidgets = new Array(this.columns.length);
|
|
|
|
for (var x = 0; x < columnWidgets.length; x++)
|
|
{
|
|
if (typeof _row[x] != "undefined" && _row[x].widget)
|
|
{
|
|
columnWidgets[x] = _row[x].widget;
|
|
|
|
// Append the widget to this container
|
|
this.addChild(_row[x].widget);
|
|
}
|
|
else
|
|
{
|
|
columnWidgets[x] = _row[x].widget;
|
|
}
|
|
// Pass along column alignment
|
|
if(_row[x].align)
|
|
{
|
|
columnWidgets[x].align = _row[x].align;
|
|
}
|
|
}
|
|
|
|
this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this);
|
|
|
|
|
|
// Create the grid controller
|
|
this.controller = new et2_nextmatch_controller(
|
|
null,
|
|
this.egw(),
|
|
this.getInstanceManager().etemplate_exec_id,
|
|
this,
|
|
null,
|
|
this.dataview.grid,
|
|
this.rowProvider,
|
|
this.options.settings.action_links,
|
|
null,
|
|
this.options.actions
|
|
);
|
|
|
|
// Need to trigger empty row the first time
|
|
if(total == 0) this.controller._emptyRow();
|
|
|
|
// Set custom data cache prefix
|
|
if(this.options.settings.dataStorePrefix)
|
|
{
|
|
this.controller.setPrefix(this.options.settings.dataStorePrefix);
|
|
}
|
|
|
|
// Load the initial order
|
|
/*this.controller.loadInitialOrder(this._getInitialOrder(
|
|
this.options.settings.rows, this.options.settings.row_id
|
|
));*/
|
|
|
|
this.controller.setFilters(this.activeFilters);
|
|
|
|
// Set the initial row count
|
|
var total = typeof this.options.settings.total != "undefined" ?
|
|
this.options.settings.total : 0;
|
|
// This triggers an invalidate, which updates the grid
|
|
this.dataview.grid.setTotalCount(total);
|
|
|
|
// Insert any data sent from server, so invalidate finds data already
|
|
if(this.options.settings.rows)
|
|
{
|
|
var prefix = this.options.settings.dataStorePrefix;
|
|
if(!prefix)
|
|
{
|
|
prefix = this.options.settings.get_rows.split('.');
|
|
prefix = prefix[0];
|
|
}
|
|
this.controller.loadInitialData(
|
|
prefix,
|
|
this.options.settings.row_id,
|
|
this.options.settings.rows
|
|
);
|
|
// Remove, to prevent duplication
|
|
delete this.options.settings.rows;
|
|
}
|
|
},
|
|
|
|
_parseGrid: function(_grid) {
|
|
// Search the rows for a header-row - if one is found, parse it
|
|
for (var y = 0; y < _grid.rowData.length; y++)
|
|
{
|
|
// Parse the first row as a header, need header to parse the data rows
|
|
if (_grid.rowData[y]["class"] == "th" || y == 0)
|
|
{
|
|
this._parseHeaderRow(_grid.cells[y], _grid.colData);
|
|
}
|
|
else
|
|
{
|
|
this._parseDataRow(_grid.cells[y], _grid.rowData[y],
|
|
_grid.colData);
|
|
}
|
|
}
|
|
this.dataview.table.resize();
|
|
},
|
|
|
|
_getSubgrid: function (_row, _data, _controller) {
|
|
// Fetch the id of the element described by _data, this will be the
|
|
// parent_id of the elements in the subgrid
|
|
var rowId = _data.content[this.options.settings.row_id];
|
|
|
|
// Create a new grid with the row as parent and the dataview grid as
|
|
// parent grid
|
|
var grid = new et2_dataview_grid(_row, this.dataview.grid);
|
|
|
|
// Create a new controller for the grid
|
|
var controller = new et2_nextmatch_controller(
|
|
_controller,
|
|
this.egw(),
|
|
this.getInstanceManager().etemplate_exec_id,
|
|
this,
|
|
rowId,
|
|
grid,
|
|
this.rowProvider,
|
|
this.options.settings.action_links,
|
|
_controller.getObjectManager()
|
|
);
|
|
controller.update();
|
|
|
|
// Register inside the destruction callback of the grid
|
|
grid.setDestroyCallback(function () {
|
|
controller.free();
|
|
});
|
|
|
|
return grid;
|
|
},
|
|
|
|
_getInitialOrder: function (_rows, _rowId) {
|
|
|
|
var _order = [];
|
|
|
|
// Get the length of the non-numerical rows arra
|
|
var len = 0;
|
|
for (var key in _rows) {
|
|
if (!isNaN(key) && parseInt(key) > len)
|
|
len = parseInt(key);
|
|
}
|
|
|
|
// Iterate over the rows
|
|
for (var i = 0; i < len; i++)
|
|
{
|
|
// Get the uid from the data
|
|
var uid = this.egw().appName + '::' + _rows[i][_rowId];
|
|
|
|
// Store the data for that uid
|
|
this.egw().dataStoreUID(uid, _rows[i]);
|
|
|
|
// Push the uid onto the order array
|
|
_order.push(uid);
|
|
}
|
|
|
|
return _order;
|
|
},
|
|
|
|
_selectColumnsClick: function(e) {
|
|
var self = this;
|
|
var columnMgr = this.dataview.getColumnMgr();
|
|
|
|
// ID for faking letter selection in column selection
|
|
var LETTERS = '~search_letter~';
|
|
|
|
var columns = {};
|
|
var columns_selected = [];
|
|
|
|
for (var i = 0; i < columnMgr.columns.length; i++)
|
|
{
|
|
var col = columnMgr.columns[i];
|
|
var widget = this.columns[i].widget;
|
|
|
|
if(col.caption && col.visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT)
|
|
{
|
|
columns[col.id] = col.caption;
|
|
if(col.visibility == ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(col.id);
|
|
}
|
|
// Custom fields get listed separately
|
|
if(widget.instanceOf(et2_nextmatch_customfields))
|
|
{
|
|
for(var field_name in widget.customfields)
|
|
{
|
|
columns[widget.prefix+field_name] = " - "+widget.customfields[field_name].label;
|
|
if(widget.options.fields[field_name]) columns_selected.push(et2_customfields_list.prototype.prefix+field_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Letter search
|
|
if(this.options.settings.lettersearch)
|
|
{
|
|
columns[LETTERS] = egw.lang('Search letter');
|
|
if(this.header.lettersearch.is(':visible')) columns_selected.push(LETTERS);
|
|
}
|
|
|
|
// Build the popup
|
|
if(!this.selectPopup)
|
|
{
|
|
var select = et2_createWidget("select", {
|
|
multiple: true,
|
|
rows: 8,
|
|
empty_label:this.egw().lang("select columns"),
|
|
selected_first: false
|
|
}, this);
|
|
select.set_select_options(columns);
|
|
select.set_value(columns_selected);
|
|
|
|
var autoRefresh = et2_createWidget("select", {
|
|
"empty_label":"Refresh",
|
|
}, this);
|
|
autoRefresh.set_id("nm_autorefresh");
|
|
autoRefresh.set_select_options({
|
|
0: "off",
|
|
30: "30 seconds",
|
|
60: "1 Minute",
|
|
300: "5 Minutes"
|
|
});
|
|
autoRefresh.set_value(this._get_autorefresh());
|
|
autoRefresh.set_statustext(egw.lang("Automatically refresh list"));
|
|
|
|
var defaultCheck = et2_createWidget("select", {"empty_label":"Preference"}, this);
|
|
defaultCheck.set_id('nm_col_preference');
|
|
defaultCheck.set_select_options({
|
|
'default': {label: 'Default',title:'Set these columns as the default'},
|
|
'reset': {label: 'Reset', title:"Reset all user's column preferences"},
|
|
'force': {label: 'Force', title:'Force column preference so users cannot change it'}
|
|
});
|
|
defaultCheck.set_value(this.options.settings.columns_forced ? 'force': '');
|
|
|
|
var okButton = et2_createWidget("buttononly", {}, this);
|
|
okButton.set_label(this.egw().lang("ok"));
|
|
okButton.onclick = function() {
|
|
// Update visibility
|
|
var visibility = {};
|
|
for (var i = 0; i < columnMgr.columns.length; i++)
|
|
{
|
|
var col = columnMgr.columns[i];
|
|
if(col.caption && col.visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT )
|
|
{
|
|
visibility[col.id] = {visible: false};
|
|
}
|
|
}
|
|
var value = select.getValue();
|
|
|
|
// Update & remove letter filter
|
|
if(self.header.lettersearch)
|
|
{
|
|
var show_letters = true;
|
|
if(value.indexOf(LETTERS) >= 0)
|
|
{
|
|
value.splice(value.indexOf(LETTERS),1);
|
|
}
|
|
else
|
|
{
|
|
show_letters = false;
|
|
}
|
|
self._set_lettersearch(show_letters);
|
|
}
|
|
|
|
var column = 0;
|
|
for(var i = 0; i < value.length; i++)
|
|
{
|
|
// Handle skipped columns
|
|
while(value[i] != "col_"+column && column < columnMgr.columns.length)
|
|
{
|
|
column++;
|
|
}
|
|
if(visibility[value[i]])
|
|
{
|
|
visibility[value[i]].visible = true;
|
|
}
|
|
// Custom fields are listed seperately in column list, but are only 1 column
|
|
if(self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) {
|
|
var cf = self.columns[column].widget.options.customfields;
|
|
var visible = self.columns[column].widget.options.fields;
|
|
|
|
// Turn off all custom fields
|
|
for(var field_name in cf)
|
|
{
|
|
visible[field_name] = false;
|
|
}
|
|
// Turn on selected custom fields - start from 0 in case they're not in order
|
|
for(var j = 0; j < value.length; j++)
|
|
{
|
|
if(value[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue;
|
|
visible[value[j].substring(1)] = true;
|
|
i++;
|
|
}
|
|
self.columns[column].widget.set_visible(visible);
|
|
}
|
|
}
|
|
columnMgr.setColumnVisibilitySet(visibility);
|
|
|
|
// Hide popup
|
|
self.selectPopup.toggle();
|
|
|
|
self.dataview.updateColumns();
|
|
|
|
// Auto refresh
|
|
self._set_autorefresh(autoRefresh.get_value());
|
|
|
|
// Set default or clear forced?
|
|
if(defaultCheck.get_value())
|
|
{
|
|
self.getInstanceManager().submit();
|
|
}
|
|
};
|
|
|
|
var cancelButton = et2_createWidget("buttononly", {}, this);
|
|
cancelButton.set_label(this.egw().lang("cancel"));
|
|
cancelButton.onclick = function() {
|
|
self.selectPopup.toggle();
|
|
};
|
|
|
|
this.selectPopup = jQuery(document.createElement("div"))
|
|
.addClass("colselection ui-dialog ui-widget-content")
|
|
.append(select.getDOMNode())
|
|
.append(okButton.getDOMNode())
|
|
.append(cancelButton.getDOMNode())
|
|
.appendTo(this.innerDiv);
|
|
|
|
// Add autorefresh
|
|
this.selectPopup.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode()));
|
|
|
|
// Add default checkbox for admins
|
|
var apps = this.egw().user('apps');
|
|
if(apps['admin'])
|
|
{
|
|
this.selectPopup.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode()));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.selectPopup.toggle();
|
|
}
|
|
var t_position = jQuery(e.target).position();
|
|
var s_position = this.div.position();
|
|
this.selectPopup.css("top", t_position.top)
|
|
.css("left", s_position.left + this.div.width() - this.selectPopup.width());
|
|
},
|
|
|
|
/**
|
|
* Set the letter search preference, and update the UI
|
|
*
|
|
* @param {boolean} letters_on
|
|
*/
|
|
_set_lettersearch: function(letters_on) {
|
|
if(letters_on)
|
|
{
|
|
this.header.lettersearch.show();
|
|
}
|
|
else
|
|
{
|
|
this.header.lettersearch.hide();
|
|
}
|
|
var lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch";
|
|
this.egw().set_preference(this.egw().getAppName(),lettersearch_preference,letters_on);
|
|
},
|
|
|
|
/**
|
|
* Set the auto-refresh time period, and starts the timer if not started
|
|
*
|
|
* @param time int Refresh period, in seconds
|
|
*/
|
|
_set_autorefresh: function(time) {
|
|
// Store preference
|
|
var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh";
|
|
var app = this.options.template.split(".");
|
|
if(this._get_autorefresh() != time)
|
|
{
|
|
this.egw().set_preference(app[0],refresh_preference,time);
|
|
}
|
|
|
|
// Start / update timer
|
|
if (this._autorefresh_timer)
|
|
{
|
|
window.clearInterval(this._autorefresh_timer);
|
|
delete this._autorefresh_timer;
|
|
}
|
|
if(time > 0)
|
|
{
|
|
this._autorefresh_timer = setInterval(jQuery.proxy(this.refresh, this), time * 1000);
|
|
|
|
// Bind to tab show/hide events, so that we don't bother refreshing in the background
|
|
$j(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) {
|
|
// Stop
|
|
window.clearInterval(this._autorefresh_timer);
|
|
$j(e.target).off(e);
|
|
|
|
// If the autorefresh time is up, bind once to trigger a refresh
|
|
// (if needed) when tab is activated again
|
|
this._autorefresh_timer = setTimeout(jQuery.proxy(function() {
|
|
$j(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch',
|
|
// Important to use anonymous function instead of just 'this.refresh' because
|
|
// of the parameters passed
|
|
jQuery.proxy(function() {this.refresh();},this)
|
|
);
|
|
},this), time*1000);
|
|
},this));
|
|
$j(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) {
|
|
// Start normal autorefresh timer again
|
|
this._set_autorefresh(this._get_autorefresh());
|
|
$j(e.target).off(e);
|
|
},this));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get the auto-refresh timer
|
|
*
|
|
* @return int Refresh period, in secods
|
|
*/
|
|
_get_autorefresh: function() {
|
|
var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh";
|
|
var app = this.options.template.split(".");
|
|
return this.egw().preference(refresh_preference,app[0]);
|
|
},
|
|
|
|
/**
|
|
* When the template attribute is set, the nextmatch widget tries to load
|
|
* that template and to fetch the grid which is inside of it. It then calls
|
|
*/
|
|
set_template: function(_value) {
|
|
if(this.template)
|
|
{
|
|
// Stop early to prevent unneeded processing, and prevent infinite
|
|
// loops if the server changes the template in get_rows
|
|
if(this.template == _value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Free the grid components - they'll be re-created as the template is processed
|
|
this.dataview.free();
|
|
this.rowProvider.free();
|
|
this.controller.free();
|
|
|
|
// Clear this setting if it's the same as the template, or
|
|
// the columns will not be loaded
|
|
if(this.template == this.options.settings.columnselection_pref)
|
|
{
|
|
this.options.settings.columnselection_pref = _value;
|
|
}
|
|
this.dataview = new et2_dataview(this.innerDiv, this.egw());
|
|
}
|
|
|
|
// Create the template
|
|
var template = et2_createWidget("template", {"id": _value}, this);
|
|
|
|
if (!template)
|
|
{
|
|
this.egw().debug("error", "Error while loading definition template for " +
|
|
"nextmatch widget.",_value);
|
|
return;
|
|
}
|
|
|
|
// Deferred parse function - template might not be fully loaded
|
|
var parse = function(template)
|
|
{
|
|
// Keep the name of the template, as we'll free up the widget after parsing
|
|
this.template = _value;
|
|
|
|
// Fetch the grid element and parse it
|
|
var definitionGrid = template.getChildren()[0];
|
|
if (definitionGrid && definitionGrid instanceof et2_grid)
|
|
{
|
|
this._parseGrid(definitionGrid);
|
|
}
|
|
else
|
|
{
|
|
this.egw().debug("error", "Nextmatch widget expects a grid to be the " +
|
|
"first child of the defined template.");
|
|
return;
|
|
}
|
|
|
|
// Free the template again, but don't remove it
|
|
setTimeout(function() {
|
|
template.free();
|
|
},1);
|
|
|
|
// Call the "setNextmatch" function of all registered
|
|
// INextmatchHeader widgets.
|
|
this.iterateOver(function (_node) {
|
|
_node.setNextmatch(this);
|
|
}, this, et2_INextmatchHeader);
|
|
|
|
// Load the default sort order
|
|
if (this.options.settings.order && this.options.settings.sort)
|
|
{
|
|
this.sortBy(this.options.settings.order,
|
|
this.options.settings.sort == "ASC", false);
|
|
}
|
|
|
|
// Start auto-refresh
|
|
this._set_autorefresh(this._get_autorefresh());
|
|
};
|
|
|
|
// Template might not be loaded yet, defer parsing
|
|
var promise = []
|
|
template.loadingFinished(promise);
|
|
|
|
// Wait until template (& children) are done
|
|
jQuery.when.apply(null, promise).done(
|
|
jQuery.proxy(function() {
|
|
parse.call(this, template);
|
|
this.dynheight.initialized = false;
|
|
this.resize();
|
|
}, this)
|
|
);
|
|
},
|
|
|
|
// Some accessors to match conventions
|
|
set_hide_header: function(hide) {
|
|
(hide ? this.header.div.hide() : this.header.div.show());
|
|
},
|
|
|
|
set_header_left: function(template) {
|
|
this.header._build_left_right("left",template);
|
|
},
|
|
set_header_right: function(template) {
|
|
this.header._build_left_right("right",template);
|
|
},
|
|
set_no_filter: function(bool, filter_name) {
|
|
if(typeof filter_name == 'undefined')
|
|
{
|
|
filter_name = 'filter'
|
|
}
|
|
|
|
var filter = this.header[filter_name];
|
|
if(filter)
|
|
{
|
|
filter.set_disabled(bool);
|
|
}
|
|
else if (bool)
|
|
{
|
|
filter = this.header._build_select(filter_name, 'select',
|
|
this.settings[filter_name], this.settings[filter_name+'_no_lang']);
|
|
}
|
|
},
|
|
set_no_filter2: function(bool) {
|
|
this.set_no_filter(bool,'filter2');
|
|
},
|
|
|
|
/**
|
|
* Actions are handled by the controller, so ignore these during init.
|
|
*/
|
|
set_actions: function(actions) {
|
|
if(actions != this.options.actions && this.controller != null && this.controller._actionManager)
|
|
{
|
|
this.options.actions = actions;
|
|
this.options.settings.action_links = this._get_action_links(actions);
|
|
this.controller._actionManager.updateActions(actions,this.controller.egw.appName);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set a different / additional handler for dropped files.
|
|
*
|
|
* File dropping doesn't work with the action system, so we handle it in the
|
|
* nextmatch by linking automatically to the target row. This allows an additional handler.
|
|
* It should accept a row UID and a File[], and return a boolean Execute the default (link) action
|
|
*
|
|
* @param {String|Function} handler
|
|
*/
|
|
set_onfiledrop: function(handler) {
|
|
this.options.onfiledrop = handler;
|
|
},
|
|
|
|
/**
|
|
* Handle drops of files by linking to the row, if possible.
|
|
*
|
|
* HTML5 / native file drops conflict with jQueryUI draggable, which handles
|
|
* all our drop actions. So we side-step the issue by registering an additional
|
|
* drop handler on the rows parent. If the row/actions itself doesn't handle
|
|
* the drop, it should bubble and get handled here.
|
|
*/
|
|
handle_drop: function(event, target) {
|
|
// Check to see if we can handle the link
|
|
// First, find the UID
|
|
var row = this.controller.getRowByNode(target);
|
|
var uid = row.uid || null;
|
|
|
|
// Get the file information
|
|
var files = [];
|
|
if(event.originalEvent && event.originalEvent.dataTransfer &&
|
|
event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0)
|
|
{
|
|
files = event.originalEvent.dataTransfer.files;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Exectute the custom handler code
|
|
if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files))
|
|
{
|
|
return false;
|
|
}
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
if(!row || !row.uid) return false;
|
|
|
|
// Link the file to the row
|
|
// just use a link widget, it's all already done
|
|
var split = uid.split('::');
|
|
var link_value = {
|
|
to_app: split.shift(),
|
|
to_id: split.join('::')
|
|
}
|
|
// Create widget and mangle to our needs
|
|
var link = et2_createWidget("link-to", {value: link_value}, this);
|
|
link.loadingFinished();
|
|
link.file_upload.set_drop_target(false);
|
|
|
|
if(row.row.tr)
|
|
{
|
|
// Ignore most of the UI, just use the status indicators
|
|
var status = $j(document.createElement("div"))
|
|
.addClass('et2_link_to')
|
|
.width(row.row.tr.width())
|
|
.position({my: "left top", at: "left top", of: row.row.tr})
|
|
.append(link.status_span)
|
|
.append(link.file_upload.progress)
|
|
.appendTo(row.row.tr);
|
|
|
|
// Bind to link event so we can remove when done
|
|
link.div.on('link.et2_link_to', function(e, linked) {
|
|
if(!linked)
|
|
{
|
|
$j("li.success", link.file_upload.progress)
|
|
.removeClass('success').addClass('validation_error');
|
|
}
|
|
else
|
|
{
|
|
// Update row
|
|
link._parent.refresh(uid,'edit');
|
|
}
|
|
// Fade out nicely
|
|
status.delay(linked ? 1 : 2000)
|
|
.fadeOut(500, function() {
|
|
link.free();
|
|
status.remove();
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
// Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh
|
|
link.file_upload.set_value(files);
|
|
},
|
|
|
|
getDOMNode: function(_sender) {
|
|
if (_sender == this)
|
|
{
|
|
return this.div[0];
|
|
}
|
|
if (_sender == this.header)
|
|
{
|
|
return this.header.div[0];
|
|
}
|
|
for (var i = 0; i < this.columns.length; i++)
|
|
{
|
|
if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget)
|
|
{
|
|
return this.dataview.getHeaderContainerNode(i);
|
|
}
|
|
}
|
|
|
|
// Let header have a chance
|
|
if(_sender && _sender._parent && _sender._parent == this)
|
|
{
|
|
return this.header.getDOMNode(_sender);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
getPath: function() {
|
|
var path = this._super.apply(this,arguments);
|
|
if(this.id && path[path.length -1] == this.id) path.pop();
|
|
return path;
|
|
},
|
|
|
|
// Input widget
|
|
|
|
/**
|
|
* Get the current 'value' for the nextmatch
|
|
*/
|
|
getValue: function() {
|
|
var _ids = this.getSelection();
|
|
|
|
// Translate the internal uids back to server uids
|
|
var idsArr = _ids.ids;
|
|
for (var i = 0; i < idsArr.length; i++)
|
|
{
|
|
idsArr[i] = idsArr[i].split("::").pop();
|
|
}
|
|
var value = {
|
|
"selected": idsArr,
|
|
}
|
|
jQuery.extend(value, this.activeFilters, this.value);
|
|
return value;
|
|
},
|
|
resetDirty: function() {},
|
|
isDirty: function() { return typeof this.value !== 'undefined';},
|
|
isValid: function() { return true;},
|
|
set_value: function(_value)
|
|
{
|
|
this.value = _value;
|
|
}
|
|
});
|
|
et2_register_widget(et2_nextmatch, ["nextmatch"]);
|
|
|
|
/**
|
|
* Standard nextmatch header bar, containing filters, search, record count, letter filters, etc.
|
|
*
|
|
* Unable to use an existing template for this because parent (nm) doesn't, and template widget doesn't
|
|
* actually load templates from the server.
|
|
* @augments et2_DOMWidget
|
|
*/
|
|
var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader,
|
|
{
|
|
attributes: {
|
|
"filter_label": {
|
|
"name": "Filter label",
|
|
"type": "string",
|
|
"description": "Label for filter",
|
|
"default": "",
|
|
"translate": true
|
|
},
|
|
"filter_help": {
|
|
"name": "Filter help",
|
|
"type": "string",
|
|
"description": "Help message for filter",
|
|
"default": "",
|
|
"translate": true
|
|
},
|
|
"filter": {
|
|
"name": "Filter value",
|
|
"type": "any",
|
|
"description": "Current value for filter",
|
|
"default": ""
|
|
},
|
|
"no_filter": {
|
|
"name": "No filter",
|
|
"type": "boolean",
|
|
"description": "Remove filter",
|
|
"default": false
|
|
}
|
|
},
|
|
headers: [],
|
|
header_div: [],
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param nextmatch
|
|
* @param nm_div
|
|
* @memberOf et2_nextmatch_header_bar
|
|
*/
|
|
init: function(nextmatch, nm_div) {
|
|
this._super.apply(this, [nextmatch,nextmatch.options.settings]);
|
|
this.nextmatch = nextmatch;
|
|
this.div = jQuery(document.createElement("div"))
|
|
.addClass("nextmatch_header");
|
|
this._createHeader();
|
|
},
|
|
|
|
destroy: function() {
|
|
this.nextmatch = null;
|
|
|
|
this._super.apply(this, arguments);
|
|
this.div = null;
|
|
},
|
|
|
|
setNextmatch: function(nextmatch) {
|
|
var create_once = (this.nextmatch == null);
|
|
this.nextmatch = nextmatch;
|
|
if(create_once)
|
|
{
|
|
this._createHeader();
|
|
}
|
|
|
|
// Bind row count
|
|
this.nextmatch.dataview.grid.setInvalidateCallback(function () {
|
|
this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + "");
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Actions are handled by the controller, so ignore these
|
|
*/
|
|
set_actions: function(actions) {},
|
|
|
|
_createHeader: function() {
|
|
|
|
var self = this;
|
|
var nm_div = this.nextmatch.div;
|
|
var settings = this.nextmatch.options.settings;
|
|
|
|
this.div.prependTo(nm_div);
|
|
|
|
// Record count
|
|
this.count = jQuery(document.createElement("span"))
|
|
.addClass("header_count ui-corner-all");
|
|
|
|
// Need to figure out how to update this as grid scrolls
|
|
// this.count.append("? - ? ").append(egw.lang("of")).append(" ");
|
|
this.count_total = jQuery(document.createElement("span"))
|
|
.appendTo(this.count)
|
|
.text(settings.total + "");
|
|
this.count.prependTo(this.div);
|
|
|
|
// Set up so if row count changes, display is updated
|
|
// Register the handler which will update the "totalCount" display
|
|
|
|
// Left & Right headers
|
|
this.header_div = jQuery(document.createElement("div")).addClass("ui-helper-clearfix ui-helper-reset").prependTo(this.div);
|
|
this.headers = [{id:this.nextmatch.options.header_left}, {id:this.nextmatch.options.header_right}];
|
|
|
|
// Search
|
|
this.search_box = jQuery(document.createElement("div"))
|
|
.appendTo(this.div)
|
|
.addClass("search");
|
|
this.search = et2_createWidget("textbox", {"id":"search","blur":egw.lang("search")}, this);
|
|
this.search.input.attr("type", "search");
|
|
this.search.input.val(settings.search)
|
|
.on("keypress", function(event) {
|
|
if(event.which == 13)
|
|
{
|
|
self.nextmatch.applyFilters({search: self.search.getValue()});
|
|
}
|
|
});
|
|
|
|
this.filters = jQuery(document.createElement("div")).appendTo(this.div)
|
|
.addClass("filters");
|
|
|
|
// Add category
|
|
if(!settings.no_cat) {
|
|
settings.cat_id_label = egw.lang("Category");
|
|
this.category = this._build_select('cat_id', 'select-cat', settings.cat_id, true);
|
|
}
|
|
|
|
// Filter 1
|
|
if(!settings.no_filter) {
|
|
this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang);
|
|
}
|
|
|
|
// Filter 2
|
|
if(!settings.no_filter2) {
|
|
this.filter2 = this._build_select('filter2', 'select', settings.filter2, settings.filter2_no_lang);
|
|
}
|
|
|
|
// Favorites
|
|
this._setup_favorites(settings['favorites']);
|
|
|
|
// Export
|
|
if(typeof settings.csv_fields != "undefined" && settings.csv_fields != false)
|
|
{
|
|
var definition = settings.csv_fields;
|
|
if(settings.csv_fields === true)
|
|
{
|
|
definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().getAppName());
|
|
}
|
|
var button = et2_createWidget("buttononly", {id: "export", "label": "Export", image:"phpgwapi/filesave"}, this.nextmatch);
|
|
jQuery(button.getDOMNode()).appendTo(this.filters).css("float", "right")
|
|
.click(this.nextmatch, function(event) {
|
|
egw_openWindowCentered2( egw.link('/index.php', {
|
|
'menuaction': 'importexport.importexport_export_ui.export_dialog',
|
|
'appname': event.data.egw().getAppName(),
|
|
'definition': definition
|
|
}), '_blank', 850, 440, 'yes');
|
|
});
|
|
}
|
|
|
|
// Set activeFilters to current value
|
|
this.nextmatch.activeFilters.search = settings.search;
|
|
|
|
this.search_button = et2_createWidget("button", {id: "search_button","label":">"}, this);
|
|
this.search_button.onclick = function(event) {
|
|
self.nextmatch.activeFilters.search = self.search.getValue();
|
|
self.nextmatch.applyFilters();
|
|
};
|
|
|
|
|
|
// Letter search
|
|
var current_letter = this.nextmatch.options.settings.searchletter ?
|
|
this.nextmatch.options.settings.searchletter :
|
|
(this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false);
|
|
if(this.nextmatch.options.settings.lettersearch || current_letter)
|
|
{
|
|
this.lettersearch = jQuery(document.createElement("table"))
|
|
.css("width", "100%")
|
|
.appendTo(this.div);
|
|
var tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch);
|
|
var row = jQuery(document.createElement("tr")).appendTo(tbody);
|
|
|
|
// Capitals, A-Z
|
|
for(var i = 65; i <= 90; i++) {
|
|
var button = jQuery(document.createElement("td"))
|
|
.addClass("lettersearch")
|
|
.appendTo(row)
|
|
.attr("id", String.fromCharCode(i))
|
|
.text(String.fromCharCode(i));
|
|
if(String.fromCharCode(i) == current_letter) button.addClass("lettersearch_active");
|
|
}
|
|
button = jQuery(document.createElement("td"))
|
|
.addClass("lettersearch")
|
|
.appendTo(row)
|
|
.attr("id", "")
|
|
.text(egw.lang("all"));
|
|
if(!current_letter) button.addClass("lettersearch_active");
|
|
|
|
this.lettersearch.click(this.nextmatch, function(event) {
|
|
// this is the lettersearch table
|
|
jQuery("td",this).removeClass("lettersearch_active");
|
|
jQuery(event.target).addClass("lettersearch_active");
|
|
var letter = event.target.id;
|
|
event.data.activeFilters.searchletter = (letter == "" ? false : letter);
|
|
event.data.applyFilters();
|
|
});
|
|
// Set activeFilters to current value
|
|
this.nextmatch.activeFilters.searchletter = current_letter;
|
|
}
|
|
// Apply letter search preference
|
|
var lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch";
|
|
if(this.lettersearch && !egw.preference(lettersearch_preference,this.nextmatch.egw().getAppName()))
|
|
{
|
|
this.lettersearch.hide();
|
|
}
|
|
},
|
|
|
|
|
|
_build_left_right: function(left_or_right, template_name)
|
|
{
|
|
var existing = this.headers[left_or_right == "left" ? 0 : 1];
|
|
if(existing && existing._type)
|
|
{
|
|
if(existing.id == template_name) return;
|
|
existing.free();
|
|
this.headers[this.headers.indexOf(existing)] = '';
|
|
}
|
|
|
|
// Load the template
|
|
var header = et2_createWidget("template", {"id": template_name}, this);
|
|
jQuery(header.getDOMNode()).addClass(left_or_right == "left" ? "et2_hbox_left":"et2_hbox_right").addClass("nm_header");
|
|
this.headers[left_or_right == "left" ? 0 : 1] = header;
|
|
$j(header.getDOMNode()).on("load", jQuery.proxy(function() {
|
|
//header.loadingFinished();
|
|
this._bindHeaderInput(header);
|
|
},this));
|
|
header.loadingFinished();
|
|
},
|
|
|
|
/**
|
|
* Build the selectbox filters in the header bar
|
|
* Sets value, options, labels, and change handlers
|
|
*/
|
|
_build_select: function(name, type, value, lang) {
|
|
var widget_options = {
|
|
"id": name,
|
|
"label": this.nextmatch.options.settings[name+"_label"],
|
|
"no_lang": lang
|
|
};
|
|
|
|
// Set select options
|
|
// Check in content for options-<name>
|
|
var mgr = this.nextmatch.getArrayMgr("content");
|
|
var options = mgr.getEntry("options-" + name);
|
|
// Look in sel_options
|
|
if(!options) options = this.nextmatch.getArrayMgr("sel_options").getEntry(name);
|
|
// Check parent sel_options, because those are usually global and don't get passed down
|
|
if(!options) options = this.nextmatch.getArrayMgr("sel_options").parentMgr.getEntry(name);
|
|
// Sometimes legacy stuff puts it in here
|
|
if(!options) options = mgr.getEntry('rows[sel_options]['+name+']');
|
|
|
|
// Maybe in a row, and options got stuck in ${row} instead of top level
|
|
var row_stuck = ['${row}','{$row}'];
|
|
for(var i = 0; !options && i < row_stuck.length; i++)
|
|
{
|
|
var row_id = '';
|
|
if((!options || options.length == 0) && (
|
|
// perspectiveData.row in nm, data["${row}"] in an auto-repeat grid
|
|
this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]]))
|
|
{
|
|
var row_id = name.replace(/[0-9]+/,row_stuck[i]);
|
|
options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id);
|
|
if(!options)
|
|
{
|
|
row_id = row_stuck[i] + "["+name+"]";
|
|
options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id);
|
|
}
|
|
}
|
|
if(options)
|
|
{
|
|
this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].',row_id,name);
|
|
}
|
|
}
|
|
|
|
// Legacy: Add in 'All' option for cat_id, if not provided.
|
|
if(name == 'cat_id' && typeof options[''] == 'undefined' && typeof options[0] == 'undefined')
|
|
{
|
|
widget_options.empty_label = this.egw().lang('All');
|
|
this.egw().debug('warn', 'Nextmatch category filter had no "All" option. Added, but you should fix that.');
|
|
}
|
|
|
|
// Create widget
|
|
var select = et2_createWidget(type, widget_options, this);
|
|
|
|
if(options) select.set_select_options(options);
|
|
|
|
// Set value
|
|
select.set_value(value);
|
|
|
|
// Set activeFilters to current value
|
|
this.nextmatch.activeFilters[select.id] = select.get_value();
|
|
|
|
// Set onChange
|
|
var input = select.input;
|
|
|
|
// Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished()
|
|
select.attributes.select_options.ignore = true;
|
|
|
|
if (this.nextmatch.options.settings[name+"_onchange"])
|
|
{
|
|
// Make sure to get the new value for filtering
|
|
input.change(this.nextmatch, function(event) {
|
|
event.data.activeFilters[name] = select.getValue();
|
|
event.data.applyFilters();
|
|
});
|
|
|
|
// Get the onchange function string
|
|
var onchange = this.nextmatch.options.settings[name+"_onchange"];
|
|
|
|
// Real submits cause all sorts of problems
|
|
if(onchange.match(/this\.form\.submit/))
|
|
{
|
|
this.egw().debug("warn","%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.",name);
|
|
onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/,'return true;');
|
|
}
|
|
|
|
// Connect it to the onchange event of the input element - may submit
|
|
input.change(this.nextmatch, et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()));
|
|
}
|
|
else // default request changed rows with new filters, previous this.form.submit()
|
|
{
|
|
input.change(this.nextmatch, function(event) {
|
|
event.data.activeFilters[name] = select.getValue();
|
|
event.data.applyFilters();
|
|
});
|
|
}
|
|
return select;
|
|
},
|
|
|
|
/**
|
|
* Set up the favorites UI control
|
|
*
|
|
* @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of
|
|
* additional fields/settings to add in to the favorite.
|
|
*/
|
|
_setup_favorites: function(filters) {
|
|
if(typeof filters == "undefined")
|
|
{
|
|
// No favorites configured
|
|
return;
|
|
}
|
|
|
|
var list = et2_csvSplit(this.options.get_rows, 2, ".");
|
|
var widget_options = {
|
|
default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite",
|
|
app: list[0],
|
|
filters: filters,
|
|
sidebox_target:'favorite_sidebox_'+list[0]
|
|
};
|
|
this.favorites = et2_createWidget('favorites', widget_options, this);
|
|
|
|
// Add into header
|
|
$j(this.favorites.getDOMNode(this.favorites)).insertAfter(this.count).css("float","right");
|
|
},
|
|
|
|
/**
|
|
* Updates all the filter elements in the header
|
|
*
|
|
* Does not actually refresh the data, just sets values to match those given.
|
|
* Called by et2_nextmatch.applyFilters().
|
|
*
|
|
* @param filters Array Key => Value pairs of current filters
|
|
*/
|
|
setFilters: function(filters) {
|
|
|
|
// Use an array mgr to hande non-simple IDs
|
|
var mgr = new et2_arrayMgr(filters);
|
|
|
|
this.iterateOver(function(child) {
|
|
// Skip favorites, don't want them in the filter
|
|
if(typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) return;
|
|
|
|
var value = '';
|
|
if(typeof child.set_value != "undefined" && child.id)
|
|
{
|
|
value = mgr.getEntry(child.id);
|
|
if (value == null) value = '';
|
|
/**
|
|
* Sometimes a filter value is not in current options. This can
|
|
* happen in a saved favorite, for example, or if server changes
|
|
* some filter options, and the order doesn't work out. The normal behaviour
|
|
* is to warn & not set it, but for nextmatch we'll just add it
|
|
* in, and let the server either set it properly, or ignore.
|
|
*/
|
|
if(value && child.instanceOf(et2_selectbox) && typeof child.options.select_options[value] == 'undefined')
|
|
{
|
|
var old_options = child.options.select_options;
|
|
// Actual label is not available, obviously, or it would be there
|
|
old_options[value] = child.egw().lang("Loading");
|
|
child.set_select_options(old_options);
|
|
}
|
|
child.set_value(value);
|
|
}
|
|
if(typeof child.get_value == "function" && child.id)
|
|
{
|
|
// Put data in the proper place
|
|
var target = this;
|
|
var value = child.get_value();
|
|
|
|
// Split up indexes
|
|
var indexes = child.id.replace(/[/g,'[').split('[');
|
|
|
|
for(var i = 0; i < indexes.length; i++)
|
|
{
|
|
indexes[i] = indexes[i].replace(/]/g,'').replace(']','');
|
|
if (i < indexes.length-1)
|
|
{
|
|
if(typeof target[indexes[i]] == "undefined") target[indexes[i]] = {};
|
|
target = target[indexes[i]];
|
|
}
|
|
else
|
|
{
|
|
target[indexes[i]] = value;
|
|
}
|
|
}
|
|
}
|
|
}, filters);
|
|
|
|
// Letter search
|
|
if(this.nextmatch.options.settings.lettersearch)
|
|
{
|
|
jQuery("td",this.lettersearch).removeClass("lettersearch_active");
|
|
$j(filters.searchletter ? "td#"+filters.searchletter : "td.lettersearch[id='']").addClass("lettersearch_active");
|
|
|
|
// Set activeFilters to current value
|
|
filters.searchletter = $j("td.lettersearch_active").attr("id");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Help out nextmatch / widget stuff by checking to see if sender is part of header
|
|
*/
|
|
getDOMNode: function(_sender) {
|
|
var filters = [this.category, this.filter, this.filter2];
|
|
for(var i = 0; i < filters.length; i++)
|
|
{
|
|
if(_sender == filters[i])
|
|
{
|
|
// Give them the filter div
|
|
return this.filters[0];
|
|
}
|
|
}
|
|
if(_sender == this.search || _sender == this.search_button) return this.search_box[0];
|
|
|
|
if(_sender && _sender._type == "template")
|
|
{
|
|
for(var i = 0; i < this.headers.length; i++)
|
|
{
|
|
if(_sender.id == this.headers[i].id && _sender._parent == this) return this.header_div[0];
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
_bindHeaderInput: function(_widget) {
|
|
var header = this;
|
|
_widget.iterateOver(function(_widget){
|
|
// Previously set change function
|
|
var widget_change = _widget.change;
|
|
_widget.change = function(_node) {
|
|
// Call previously set change function
|
|
var result = widget_change.call(_widget,_node);
|
|
|
|
// Update filters
|
|
if(result && _widget.isDirty()) {
|
|
// Update dirty
|
|
_widget._oldValue = _widget.getValue();
|
|
|
|
var value = this.getInstanceManager().getValues(header);
|
|
// Filter now
|
|
header.nextmatch.applyFilters(value[header.nextmatch.id]);
|
|
}
|
|
// In case this gets bound twice, it's important to return
|
|
return true;
|
|
};
|
|
|
|
// Set activeFilters to current value
|
|
// Use an array mgr to hande non-simple IDs
|
|
var value = {};
|
|
value[_widget.id] = _widget._oldValue = _widget.getValue();
|
|
var mgr = new et2_arrayMgr(value);
|
|
jQuery.extend(this.nextmatch.activeFilters,mgr.data);
|
|
}, this, et2_inputWidget);
|
|
}
|
|
});
|
|
et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]);
|
|
|
|
/**
|
|
* Classes for the nextmatch sortheaders etc.
|
|
*
|
|
* @augments et2_baseWidget
|
|
*/
|
|
var et2_nextmatch_header = et2_baseWidget.extend(et2_INextmatchHeader,
|
|
{
|
|
attributes: {
|
|
"label": {
|
|
"name": "Caption",
|
|
"type": "string",
|
|
"description": "Caption for the nextmatch header",
|
|
"translate": true
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @memberOf et2_nextmatch_header
|
|
*/
|
|
init: function() {
|
|
this._super.apply(this, arguments);
|
|
|
|
this.labelNode = $j(document.createElement("span"));
|
|
this.nextmatch = null;
|
|
|
|
this.setDOMNode(this.labelNode[0]);
|
|
},
|
|
|
|
destroy: function() {
|
|
this._super.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
* Set nextmatch is the function which has to be implemented for the
|
|
* et2_INextmatchHeader interface.
|
|
*/
|
|
setNextmatch: function(_nextmatch) {
|
|
this.nextmatch = _nextmatch;
|
|
},
|
|
|
|
set_label: function(_value) {
|
|
this.label = _value;
|
|
|
|
this.labelNode.text(_value);
|
|
}
|
|
|
|
});
|
|
et2_register_widget(et2_nextmatch_header, ['nextmatch-header']);
|
|
|
|
/**
|
|
* Extend header to process customfields
|
|
*
|
|
* @augments et2_customfields_list
|
|
*/
|
|
var et2_nextmatch_customfields = et2_customfields_list.extend(et2_INextmatchHeader,
|
|
{
|
|
attributes: {
|
|
'customfields': {
|
|
'name': 'Custom fields',
|
|
'description': 'Auto filled'
|
|
},
|
|
'fields': {
|
|
'name': "Visible fields",
|
|
"description": "Auto filled"
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @memberOf et2_nextmatch_customfields
|
|
*/
|
|
init: function() {
|
|
this.nextmatch = null;
|
|
this._super.apply(this, arguments);
|
|
|
|
// Specifically take the whole column
|
|
this.table.css("width", "100%");
|
|
},
|
|
|
|
destroy: function() {
|
|
this.nextmatch = null;
|
|
this._super.apply(this, arguments);
|
|
},
|
|
|
|
transformAttributes: function(_attrs) {
|
|
this._super.apply(this, arguments);
|
|
|
|
// Add in settings that are objects
|
|
if(!_attrs.customfields)
|
|
{
|
|
// Check for custom stuff (unlikely)
|
|
var data = this.getArrayMgr("modifications").getEntry(this.id);
|
|
// Check for global settings
|
|
if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true);
|
|
for(var key in data)
|
|
{
|
|
if(typeof data[key] === 'object' && ! _attrs[key]) _attrs[key] = data[key];
|
|
}
|
|
}
|
|
},
|
|
|
|
setNextmatch: function(_nextmatch) {
|
|
this.nextmatch = _nextmatch;
|
|
this.loadFields();
|
|
},
|
|
|
|
/**
|
|
* Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio
|
|
*/
|
|
loadFields: function() {
|
|
if(this.nextmatch == null)
|
|
{
|
|
// not ready yet
|
|
return;
|
|
}
|
|
var columnMgr = this.nextmatch.dataview.getColumnMgr();
|
|
var nm_column = null;
|
|
for(var i = 0; i < this.nextmatch.columns.length; i++)
|
|
{
|
|
if(this.nextmatch.columns[i].widget == this)
|
|
{
|
|
nm_column = columnMgr.columns[i];
|
|
break;
|
|
}
|
|
}
|
|
if(!nm_column) return;
|
|
|
|
// Check for global setting changes (visibility)
|
|
var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~');
|
|
if(global_data != null && global_data.fields) this.options.fields = global_data.fields;
|
|
|
|
var apps = egw.link_app_list();
|
|
for(var field_name in this.options.customfields)
|
|
{
|
|
var field = this.options.customfields[field_name];
|
|
var cf_id = et2_customfields_list.prototype.prefix + field_name;
|
|
|
|
|
|
if(this.rows[field_name]) continue;
|
|
|
|
// Table row
|
|
var row = jQuery(document.createElement("tr"))
|
|
.appendTo(this.tbody);
|
|
var cf = jQuery(document.createElement("td"))
|
|
.appendTo(row);
|
|
this.rows[cf_id] = cf[0];
|
|
|
|
// Create widget by type
|
|
var widget = null;
|
|
|
|
if(field.type == 'select')
|
|
{
|
|
widget = et2_createWidget("nextmatch-filterheader", {
|
|
id: cf_id,
|
|
label: field.label,
|
|
select_options: field.values
|
|
}, this);
|
|
}
|
|
else if (apps[field.type])
|
|
{
|
|
widget = et2_createWidget("nextmatch-entryheader", {
|
|
id: cf_id,
|
|
only_app: field.type,
|
|
blur: field.label
|
|
}, this);
|
|
}
|
|
else
|
|
{
|
|
widget = et2_createWidget("nextmatch-sortheader", {
|
|
id: cf_id,
|
|
label: field.label
|
|
}, this);
|
|
}
|
|
|
|
// Check for column filter
|
|
if(!jQuery.isEmptyObject(this.options.fields) && (
|
|
this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined'))
|
|
{
|
|
cf.hide();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Override parent so we can update the nextmatch row too
|
|
*/
|
|
set_visible: function(_fields) {
|
|
this._super.apply(this, arguments);
|
|
|
|
// Find data row, and do it too
|
|
var self = this;
|
|
if(this.nextmatch)
|
|
{
|
|
this.nextmatch.iterateOver(
|
|
function(widget) {
|
|
if(widget == self) return;
|
|
widget.set_visible(_fields);
|
|
}, this, et2_customfields_list
|
|
);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Provide own column caption (column selection)
|
|
*
|
|
* If only one custom field, just use that, otherwise use "custom fields"
|
|
*/
|
|
_genColumnCaption: function() {
|
|
return egw.lang("Custom fields");
|
|
},
|
|
|
|
/**
|
|
* Provide own column naming, including only selected columns - only useful
|
|
* to nextmatch itself, not for sending server-side
|
|
*/
|
|
_getColumnName: function() {
|
|
var name = this.id;
|
|
var visible = [];
|
|
for(var field_name in this.options.customfields)
|
|
{
|
|
if(jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true)
|
|
{
|
|
visible.push(et2_customfields_list.prototype.prefix + field_name);
|
|
jQuery(this.rows[field_name]).show();
|
|
}
|
|
else if (typeof this.rows[field_name] != "undefined")
|
|
{
|
|
jQuery(this.rows[field_name]).hide();
|
|
}
|
|
}
|
|
|
|
if(visible.length) {
|
|
name +="_"+ visible.join("_");
|
|
}
|
|
else
|
|
{
|
|
// None hidden means all visible
|
|
jQuery(this.rows[field_name]).parent().parent().children().show();
|
|
}
|
|
|
|
// Update global custom fields column(s) - widgets will check on their own
|
|
|
|
// Check for custom stuff (unlikely)
|
|
var data = this.getArrayMgr("modifications").getEntry(this.id);
|
|
// Check for global settings
|
|
if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {};
|
|
if(!data.fields) data.fields = {};
|
|
for(var field in this.options.customfields)
|
|
{
|
|
data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]);
|
|
}
|
|
return name;
|
|
}
|
|
});
|
|
et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']);
|
|
|
|
/**
|
|
* @augments et2_nextmatch_header
|
|
*/
|
|
var et2_nextmatch_sortheader = et2_nextmatch_header.extend(et2_INextmatchSortable,
|
|
{
|
|
attributes: {
|
|
"sortmode": {
|
|
"name": "Sort order",
|
|
"type": "string",
|
|
"description": "Default sort order",
|
|
"translate": false
|
|
}
|
|
},
|
|
legacyOptions: ['sortmode'],
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @memberOf et2_nextmatch_sortheader
|
|
*/
|
|
init: function() {
|
|
this._super.apply(this, arguments);
|
|
|
|
this.sortmode = "none";
|
|
|
|
this.labelNode.addClass("nextmatch_sortheader none");
|
|
},
|
|
|
|
click: function() {
|
|
if (this.nextmatch && this._super.apply(this, arguments))
|
|
{
|
|
this.nextmatch.sortBy(this.id);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Wrapper to join up interface * framework
|
|
*/
|
|
set_sortmode: function(_mode)
|
|
{
|
|
this.setSortmode(_mode);
|
|
},
|
|
|
|
/**
|
|
* Function which implements the et2_INextmatchSortable function.
|
|
*/
|
|
setSortmode: function(_mode) {
|
|
// Remove the last sortmode class and add the new one
|
|
this.labelNode.removeClass(this.sortmode)
|
|
.addClass(_mode);
|
|
|
|
this.sortmode = _mode;
|
|
}
|
|
|
|
});
|
|
et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']);
|
|
|
|
/**
|
|
* @augments et2_selectbox
|
|
*/
|
|
var et2_nextmatch_filterheader = et2_selectbox.extend([et2_INextmatchHeader, et2_IResizeable],
|
|
{
|
|
/**
|
|
* Override to add change handler
|
|
*
|
|
* @memberOf et2_nextmatch_filterheader
|
|
*/
|
|
createInputWidget: function() {
|
|
// Make sure there's an option for all
|
|
if(!this.options.empty_label && !this.options.select_options[""])
|
|
{
|
|
this.options.empty_label = this.options.label ? this.options.label : egw.lang("All");
|
|
}
|
|
this._super.apply(this, arguments);
|
|
|
|
this.input.change(this, function(event) {
|
|
if(typeof event.data.nextmatch == 'undefined')
|
|
{
|
|
// Not fully set up yet
|
|
return;
|
|
}
|
|
if(typeof event.data.nextmatch.activeFilters.col_filter == 'undefined')
|
|
event.data.nextmatch.activeFilters.col_filter = {};
|
|
if(event.data.input.val())
|
|
{
|
|
event.data.nextmatch.activeFilters["col_filter"][event.data.id] = event.data.input.val();
|
|
}
|
|
else
|
|
{
|
|
delete (event.data.nextmatch.activeFilters["col_filter"][event.data.id]);
|
|
}
|
|
// Set value so it's there for response (otherwise it gets cleared if options are updated)
|
|
event.data.set_value(event.data.input.val());
|
|
|
|
event.data.nextmatch.applyFilters();
|
|
});
|
|
|
|
},
|
|
|
|
/**
|
|
* Set nextmatch is the function which has to be implemented for the
|
|
* et2_INextmatchHeader interface.
|
|
*/
|
|
setNextmatch: function(_nextmatch) {
|
|
this.nextmatch = _nextmatch;
|
|
|
|
// Set current filter value from nextmatch settings
|
|
if(this.nextmatch.options.settings.col_filter && typeof this.nextmatch.options.settings.col_filter[this.id] != "undefined")
|
|
{
|
|
this.set_value(this.nextmatch.options.settings.col_filter[this.id]);
|
|
|
|
// Make sure it's set in the nextmatch
|
|
_nextmatch.activeFilters.col_filter[this.id] = this.getValue();
|
|
}
|
|
},
|
|
|
|
// Make sure selectbox is not longer than the column
|
|
resize: function() {
|
|
this.input.css("max-width",jQuery(this.parentNode).innerWidth() + "px");
|
|
}
|
|
|
|
});
|
|
et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']);
|
|
|
|
/**
|
|
* @augments et2_selectAccount
|
|
*/
|
|
var et2_nextmatch_accountfilterheader = et2_selectAccount.extend([et2_INextmatchHeader, et2_IResizeable],
|
|
{
|
|
/**
|
|
* Override to add change handler
|
|
*
|
|
* @memberOf et2_nextmatch_accountfilterheader
|
|
*/
|
|
createInputWidget: function() {
|
|
// Make sure there's an option for all
|
|
if(!this.options.empty_label && !this.options.select_options[""])
|
|
{
|
|
this.options.empty_label = this.options.label ? this.options.label : egw.lang("All");
|
|
}
|
|
this._super.apply(this, arguments);
|
|
|
|
this.input.change(this, function(event) {
|
|
if(typeof event.data.nextmatch == 'undefined')
|
|
{
|
|
// Not fully set up yet
|
|
return;
|
|
}
|
|
if(typeof event.data.nextmatch.activeFilters.col_filter == 'undefined')
|
|
event.data.nextmatch.activeFilters.col_filter = {};
|
|
if(event.data.getValue())
|
|
{
|
|
event.data.nextmatch.activeFilters["col_filter"][event.data.id] = event.data.getValue();
|
|
}
|
|
else
|
|
{
|
|
delete (event.data.nextmatch.activeFilters["col_filter"][event.data.id]);
|
|
}
|
|
|
|
event.data.nextmatch.applyFilters();
|
|
});
|
|
|
|
},
|
|
|
|
set_select_options: function(_options) {
|
|
// Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished()
|
|
this.attributes.select_options.ignore = true;
|
|
this._super.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
* Set nextmatch is the function which has to be implemented for the
|
|
* et2_INextmatchHeader interface.
|
|
*/
|
|
setNextmatch: function(_nextmatch) {
|
|
this.nextmatch = _nextmatch;
|
|
|
|
// Set current filter value from nextmatch settings
|
|
if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id])
|
|
{
|
|
this.set_value(this.nextmatch.options.settings.col_filter[this.id]);
|
|
}
|
|
},
|
|
// Make sure selectbox is not longer than the column
|
|
resize: function() {
|
|
var max = jQuery(this.parentNode).innerWidth() - 4;
|
|
var surroundings = this.getSurroundings()._widgetSurroundings;
|
|
for(var i = 0; i < surroundings.length; i++)
|
|
{
|
|
max -= jQuery(surroundings[i]).outerWidth();
|
|
}
|
|
this.input.css("max-width",max + "px");
|
|
}
|
|
|
|
});
|
|
et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']);
|
|
|
|
/**
|
|
* @augments et2_link_entry
|
|
*/
|
|
var et2_nextmatch_entryheader = et2_link_entry.extend(et2_INextmatchHeader,
|
|
{
|
|
/**
|
|
* Override to add change handler
|
|
*
|
|
* @memberOf et2_link_entry
|
|
*/
|
|
select: function(event, selected) {
|
|
this._super.apply(this, arguments);
|
|
if(typeof this.nextmatch.activeFilters.col_filter == 'undefined')
|
|
this.nextmatch.activeFilters.col_filter = {};
|
|
if(selected && selected.item.value) {
|
|
if(event.data.options.only_app)
|
|
{
|
|
// Only one application, just give the ID
|
|
this.nextmatch.activeFilters["col_filter"][this.id] = selected.item.value;
|
|
}
|
|
else
|
|
{
|
|
// App is expecting app:id
|
|
this.nextmatch.activeFilters["col_filter"][this.id] =
|
|
event.data.app_select.val() + ":"+ selected.item.value;
|
|
|
|
}
|
|
} else {
|
|
delete (this.nextmatch.activeFilters["col_filter"][this.id]);
|
|
}
|
|
this.nextmatch.applyFilters.call(this.nextmatch);
|
|
},
|
|
|
|
/**
|
|
* Override to always return a string appname:id (or just id), parent returns an object
|
|
*/
|
|
getValue: function() {
|
|
var value = this._super.apply(this, arguments);
|
|
if(typeof value == "object" && value != null)
|
|
{
|
|
if(!value.app || !value.id) return null;
|
|
value = value.app +":"+value.id;
|
|
}
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
* Set nextmatch is the function which has to be implemented for the
|
|
* et2_INextmatchHeader interface.
|
|
*/
|
|
setNextmatch: function(_nextmatch) {
|
|
this.nextmatch = _nextmatch;
|
|
|
|
// Set current filter value from nextmatch settings
|
|
if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id])
|
|
{
|
|
this.set_value(this.nextmatch.options.settings.col_filter[this.id]);
|
|
|
|
if(this.getValue() != this.nextmatch.activeFilters.col_filter[this.id])
|
|
{
|
|
this.nextmatch.activeFilters.col_filter[this.id] = this.getValue();
|
|
}
|
|
|
|
// Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished()
|
|
this.attributes.value.ignore = true;
|
|
//this.attributes.select_options.ignore = true;
|
|
}
|
|
var self = this;
|
|
// Fire on lost focus, clear filter if user emptied box
|
|
this.search.focusout(this, function(event) {if(!self.search.val()) { self.select(event, {item:{value:null}});}});
|
|
}
|
|
});
|
|
et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']);
|
|
|
|
/**
|
|
* @augments et2_nextmatch_filterheader
|
|
*/
|
|
var et2_nextmatch_customfilter = et2_nextmatch_filterheader.extend(
|
|
{
|
|
attributes: {
|
|
"widget_type": {
|
|
"name": "Actual type",
|
|
"type": "string",
|
|
"description": "The actual type of widget you should use"
|
|
},
|
|
"widget_options": {
|
|
"name": "Actual options",
|
|
"type": "any",
|
|
"description": "The options for the actual widget",
|
|
"default": {}
|
|
},
|
|
},
|
|
legacyOptions: ["widget_type","widget_options"],
|
|
|
|
real_node: null,
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param _parent
|
|
* @param _attrs
|
|
* @memberOf et2_nextmatch_customfilter
|
|
*/
|
|
init: function(_parent, _attrs) {
|
|
this._super.apply(this, arguments);
|
|
|
|
switch(_attrs.widget_type)
|
|
{
|
|
case "link-entry":
|
|
_attrs.type = 'nextmatch-entryheader';
|
|
break;
|
|
default:
|
|
_attrs.type = _attrs.widget_type;
|
|
}
|
|
// Avoid warning about non-existant attribute
|
|
delete(_attrs.widget_type);
|
|
this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this._parent);
|
|
},
|
|
|
|
// Just pass the real DOM node through, in case anybody asks
|
|
getDOMNode: function(_sender) {
|
|
return this.real_node ? this.real_node.getDOMNode(_sender) : null;
|
|
},
|
|
|
|
// Also need to pass through real children
|
|
getChildren: function() {
|
|
return this.real_node.getChildren();
|
|
},
|
|
setNextmatch: function(_nextmatch)
|
|
{
|
|
if(this.real_node && this.real_node.setNextmatch)
|
|
{
|
|
return this.real_node.setNextmatch(_nextmatch);
|
|
}
|
|
}
|
|
});
|
|
et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']);
|