Implemented keyboard navigation and data retrival for uids

This commit is contained in:
Andreas Stöckel 2012-03-29 14:11:22 +00:00
parent e20f2e9333
commit cfa9c190bb
8 changed files with 394 additions and 94 deletions

View File

@ -80,7 +80,8 @@ var et2_dataview_controller = Class.extend({
// Create the selection manager
this._selectionMgr = new et2_dataview_selectionManager(this._indexMap,
_actionObjectManager, this._selectionFetchRange, this);
_actionObjectManager, this._selectionFetchRange,
this._makeIndexVisible, this);
},
destroy: function () {
@ -658,8 +659,13 @@ var et2_dataview_controller = Class.extend({
// Update the total element count in the grid
this.self._grid.setTotalCount(_response.total);
this.self._selectionMgr.setTotalCount(_response.total);
},
/**
* Callback function used by the selection manager to translate the selected
* range to uids.
*/
_selectionFetchRange: function (_range, _callback, _context) {
this._dataProvider.dataFetch(
{ "start": _range.top, "num_rows": _range.bottom - _range.top + 1,
@ -668,6 +674,13 @@ var et2_dataview_controller = Class.extend({
_callback.call(_context, _response.order);
}
);
},
/**
* Tells the grid to make the given index visible.
*/
_makeIndexVisible: function (_idx) {
this._grid.makeIndexVisible(_idx);
}
});

View File

@ -12,20 +12,28 @@
/*egw:uses
et2_dataview_view_aoi;
egw_action.egw_keymanager;
*/
/**
* The selectioManager is internally used by the et2_dataview_controller class
* to manage the row selection.
* As the action system does not allow selection of entries which are currently
* not in the dom tree, we have to manage this in this class. The idea is to
* manage an external action object interface for each visible row and proxy all
* state changes between an dummy action object, that does no selection handling,
* and the external action object interface.
*/
var et2_dataview_selectionManager = Class.extend({
init: function (_indexMap, _actionObjectManager, _queryRangeCallback,
_context) {
_makeVisibleCallback, _context) {
// Copy the arguments
this._indexMap = _indexMap;
this._actionObjectManager = _actionObjectManager;
this._queryRangeCallback = _queryRangeCallback;
this._makeVisibleCallback = _makeVisibleCallback;
this._context = _context;
// Internal map which contains all curently selected uids and their
@ -34,79 +42,48 @@ var et2_dataview_selectionManager = Class.extend({
this._focusedEntry = null;
this._invertSelection = false;
this._inUpdate = false;
this._total = 0;
},
setIndexMap: function (_indexMap) {
this._indexMap = _indexMap;
},
setTotalCount: function (_total) {
this._total = _total;
},
registerRow: function (_uid, _idx, _tr, _links) {
// Get the corresponding entry from the registered rows array
var entry = this._getRegisteredRowsEntry(_uid);
// If the row has changed unregister the old one and do not delete
// entry from the entry map
if (entry.tr && entry.tr !== _tr) {
this.unregisterRow(_uid, entry.tr, true);
}
// Create the AOI for the tr
if (!entry.tr)
if (!entry.tr && _links)
{
// Create the AOI which is used internally in the selection manager
// this AOI is not connected to the AO, as the selection manager
// cares about selection etc.
entry.aoi = new et2_dataview_rowAOI(_tr);
entry.aoi.setStateChangeCallback(
function (_newState, _changedBit, _shiftState) {
if (_changedBit === EGW_AO_STATE_SELECTED)
{
// Call the select handler
this._handleSelect(
_uid,
entry,
egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK),
egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI)
);
}
}, this);
// Create AOI
if (_links)
{
var dummyAOI = new egwActionObjectInterface();
var self = this;
// Handling for Action Implementations updating the state
dummyAOI.doSetState = function (_state) {
if (!self._inUpdate)
{
// Update the "focused" flag
self.setFocused(_uid, egwBitIsSet(_state, EGW_AO_STATE_FOCUSED));
// Generally update the state
self._updateState(_uid, _state);
}
};
// Connect the "doTriggerEvent" of the dummy AOI to our internal
// aoi.
dummyAOI.doTriggerEvent = entry.aoi.doTiggerEvent;
// Implementation of the getDOMNode function, so that the event
// handlers can be properly bound
dummyAOI.getDOMNode = function () { return _tr; };
// Create an action object for the tr and connect it to a dummy AOI
entry.ao = this._actionObjectManager.addObject(_uid, dummyAOI);
entry.ao.updateActionLinks(_links);
}
this._attachActionObjectInterface(entry, _tr, _uid);
this._attachActionObject(entry, _tr, _uid, _links, _idx)
}
// Update the entry
entry.idx = _idx;
entry.ao._index = entry.idx = _idx;
entry.tr = _tr;
// Update the visible state of the _tr
this._updateEntryState(entry, entry.state);
},
unregisterRow: function (_uid, _tr) {
unregisterRow: function (_uid, _tr, _noDelete) {
// _noDelete defaults to false
_noDelete = _noDelete ? true : false;
if (typeof this._registeredRows[_uid] !== "undefined"
&& this._registeredRows[_uid].tr === _tr)
{
@ -122,7 +99,8 @@ var et2_dataview_selectionManager = Class.extend({
this._registeredRows[_uid].ao = null;
}
if (this._registeredRows[_uid].state === EGW_AO_STATE_NORMAL)
if (!_noDelete
&& this._registeredRows[_uid].state === EGW_AO_STATE_NORMAL)
{
delete this._registeredRows[_uid];
}
@ -180,10 +158,140 @@ var et2_dataview_selectionManager = Class.extend({
}
},
getSelected: function () {
// Collect all currently selected ids
var ids = [];
for (var key in this._registeredRows)
{
if (egwBitIsSet(this._registeredRows[key].state, EGW_AO_STATE_SELECTED))
{
ids.push(key);
}
}
// Return an array containing those ids
return {
"inverted": this._invertSelection,
"ids": ids
}
},
/** -- PRIVATE FUNCTIONS -- **/
_attachActionObjectInterface: function (_entry, _tr, _uid) {
// Create the AOI which is used internally in the selection manager
// this AOI is not connected to the AO, as the selection manager
// cares about selection etc.
_entry.aoi = new et2_dataview_rowAOI(_tr);
_entry.aoi.setStateChangeCallback(
function (_newState, _changedBit, _shiftState) {
if (_changedBit === EGW_AO_STATE_SELECTED)
{
// Call the select handler
this._handleSelect(
_uid,
_entry,
egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK),
egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI)
);
}
}, this);
},
_getDummyAOI: function (_entry, _tr, _uid, _idx) {
// Create AOI
var dummyAOI = new egwActionObjectInterface();
var self = this;
// Handling for Action Implementations updating the state
dummyAOI.doSetState = function (_state) {
if (!self._inUpdate)
{
// Update the "focused" flag
self.setFocused(_uid, egwBitIsSet(_state, EGW_AO_STATE_FOCUSED));
// Generally update the state
self._updateState(_uid, _state);
}
};
// Handle the "make visible" event, pass the request to the parent
// controller
dummyAOI.doMakeVisible = function () {
self._makeVisibleCallback.call(self._context, _idx);
}
// Connect the the two AOIs
dummyAOI.doTriggerEvent = _entry.aoi.doTiggerEvent;
// Implementation of the getDOMNode function, so that the event
// handlers can be properly bound
dummyAOI.getDOMNode = function () { return _tr; };
return dummyAOI;
},
_attachActionObject: function (_entry, _tr, _uid, _links, _idx) {
// Get the dummyAOI which connects the action object to the tr but
// does no selection handling
var dummyAOI = this._getDummyAOI(_entry, _tr, _uid, _idx);
// Create an action object for the tr and connect it to a dummy AOI
_entry.ao = this._actionObjectManager.addObject(_uid, dummyAOI);
_entry.ao.updateActionLinks(_links);
_entry.ao._index = _idx;
// Overwrite some functions like "traversePath", "getNext" and
// "getPrevious"
var self = this;
function getIndexAO (_idx) {
// Check whether the index is in the index map
if (typeof self._indexMap[_idx] !== "undefined"
&& self._indexMap[_idx].uid)
{
return self._getRegisteredRowsEntry(self._indexMap[_idx].uid).ao;
}
return null;
}
function getElementRelatively (_step) {
return getIndexAO(Math.max(0,
Math.min(self._total - 1, _entry.idx + _step)));
};
_entry.ao.getPrevious = function (_step) {
return getElementRelatively(-_step);
};
_entry.ao.getNext = function (_step) {
return getElementRelatively(_step);
};
_entry.ao.traversePath = function (_obj) {
// Get the start and the stop index
var s = Math.min(this._index, _obj._index);
var e = Math.max(this._index, _obj._index);
var result = [];
for (var i = s; i < e; i++)
{
var ao = getIndexAO(i);
if (ao)
{
result.push(ao);
}
}
return result;
};
},
_updateState: function (_uid, _state) {
var entry = this._getRegisteredRowsEntry(_uid);

View File

@ -496,6 +496,46 @@ var et2_dataview_grid = et2_dataview_container.extend(et2_dataview_IViewRange, {
}
},
/**
* Makes the given index visible: TODO: Propagate this to the parent grid.
*/
makeIndexVisible: function (_idx)
{
// Get the element range
var elemRange = this._getElementRange(_idx);
// Abort if the index was out of range
if (!elemRange)
{
return false;
}
// Calculate the current visible range
var visibleRange = et2_bounds(
this._viewRange.top + ET2_GRID_VIEW_EXT,
this._viewRange.bottom - ET2_GRID_VIEW_EXT);
// Check whether the element is currently completely visible -- if yes,
// do nothing
if (visibleRange.top < elemRange.top
&& visibleRange.bottom > elemRange.bottom)
{
return true;
}
if (elemRange.top < visibleRange.top)
{
this.scrollarea.scrollTop(elemRange.top);
}
else
{
var h = elemRange.bottom - elemRange.top;
this.scrollarea.scrollTop(elemRange.top - this._scrollHeight + h);
}
},
/* ---- PRIVATE FUNCTIONS ---- */
/* _inspectStructuralIntegrity: function() {
@ -515,6 +555,38 @@ var et2_dataview_grid = et2_dataview_container.extend(et2_dataview_IViewRange, {
}
},*/
/**
* Translates the given index to a range, returns false if the index is
* out of range.
*/
_getElementRange: function (_idx)
{
// Recalculate the element positions
this._recalculateElementPosition();
// Translate the given index to the map index
var mapIdx = this._calculateMapIndex(_idx);
// Do nothing if the given index is out of range
if (mapIdx === false)
{
return false;
}
// Get the map element
var elem = this._map[mapIdx];
// Get the element range
if (elem instanceof et2_dataview_spacer)
{
var avg = this.getAverageHeight();
return et2_range(elem.getTop() + avg * (elem.getIndex() - _idx),
avg);
}
return elem.getRange();
},
/**
* Recalculates the position of the currently managed containers. This
* routine only updates the pixel position of the elements -- the index of

View File

@ -550,8 +550,8 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, {
"nm",
this.dataview.grid,
this.rowProvider,
null,
this.options.settings.action_links,
null,
this.options.settings.actions
);

View File

@ -17,28 +17,33 @@
* @param _action action object with attributes caption, id, nm_action, ...
* @param _senders array of rows selected
*/
function nm_action(_action, _senders)
function nm_action(_action, _senders, _target, _ids)
{
// ignore checkboxes, unless they have an explicit defined nm_action
if (_action.checkbox && (!_action.data || typeof _action.data.nm_action == 'undefined')) return;
if (typeof _action.data == 'undefined' || !_action.data) _action.data = {};
if (typeof _action.data.nm_action == 'undefined') _action.data.nm_action = 'submit';
// ----------------------
// TODO: Parse the _ids.inverted flag!
// ----------------------
var idsArr = _ids.ids;
var ids = "";
for (var i = 0; i < _senders.length; i++)
for (var i = 0; i < idsArr.length; i++)
{
var app_id = _senders[i].id.split('::', 2);
var app_id = idsArr[i].split('::', 2);
var id = app_id[1];
ids += (id.indexOf(',') >= 0 ? '"'+id.replace(/"/g,'""')+'"' : id) +
((i < _senders.length - 1) ? "," : "");
((i < idsArr.length - 1) ? "," : "");
}
//console.log(_action); console.log(_senders);
var mgr = _action.getManager();
var select_all = mgr.getActionById("select_all");
var confirm_msg = (_senders.length > 1 || select_all && select_all.checked) &&
var confirm_msg = (idsArr.length > 1 || select_all && select_all.checked) &&
typeof _action.data.confirm_multiple != 'undefined' ?
_action.data.confirm_multiple : _action.data.confirm;
@ -49,7 +54,7 @@ function nm_action(_action, _senders)
if (!confirm(confirm_msg)) return;
}
// in case we only need to confirm multiple selected (only _action.data.confirm_multiple)
else if (typeof _action.data.confirm_multiple != 'undefined' && (_senders.length > 1 || select_all && select_all.checked))
else if (typeof _action.data.confirm_multiple != 'undefined' && (idsArr.length > 1 || select_all && select_all.checked))
{
if (!confirm(_action.data.confirm_multiple)) return;
}
@ -94,7 +99,7 @@ function nm_action(_action, _senders)
case 'egw_open':
var params = _action.data.egw_open.split('-'); // type-appname-idNum (idNum is part of id split by :), eg. "edit-infolog"
console.log(params);
var egw_open_id = _senders[0].id;
var egw_open_id = idsArr[0].id;
if (typeof params[2] != 'undefined') egw_open_id = egw_open_id.split(':')[params[2]];
egw_open(egw_open_id,params[1],params[0],params[3]);
break;
@ -103,7 +108,7 @@ function nm_action(_action, _senders)
// open div styled as popup contained in current form and named action.id+'_popup'
if (nm_popup_action == null)
{
nm_open_popup(_action, _senders);
nm_open_popup(_action, _ids);
break;
}
// fall through, if popup is open --> submit form
@ -202,7 +207,9 @@ function nm_compare_field(_action, _senders, _target)
return value == _action.data.fieldValue;
}
var nm_popup_action, nm_popup_senders = null;
// TODO: This code is rather suboptimal! No global variables as this code will
// run in a global context
var nm_popup_action, nm_popup_ids = null;
/**
* Open popup for a certain action requiring further input
@ -210,15 +217,15 @@ var nm_popup_action, nm_popup_senders = null;
* Popup needs to have eTemplate name of action id plus "_popup"
*
* @param _action
* @param _senders
* @param _ids
*/
function nm_open_popup(_action, _senders)
function nm_open_popup(_action, _ids)
{
var popup = document.getElementById(_action.getManager().etemplate_var_prefix + '[' + _action.id + '_popup]');
if (popup) {
nm_popup_action = _action;
nm_popup_senders = _senders;
nm_popup_ids = _ids;
popup.style.display = 'block';
}
}
@ -231,7 +238,7 @@ function nm_submit_popup(button)
button.form.submit_button.value = button.name; // set name of button (sub-action)
// call regular nm_action to transmit action and senders correct
nm_action(nm_popup_action, nm_popup_senders);
nm_action(nm_popup_action, null, null, nm_popup_ids);
}
/**

View File

@ -44,31 +44,28 @@ var et2_nextmatch_controller = et2_dataview_controller.extend(
* is given.
*/
init: function (_egw, _execId, _widgetId, _grid, _rowProvider,
_objectManager, _actionLinks, _actions) {
_actionLinks, _objectManager, _actions) {
// Create the action/object managers
if (_objectManager === null)
// Copy the egw reference
this.egw = _egw;
// Initialize the action and the object manager
if (!_objectManager)
{
this._actionManager = new egwActionManager();
this._actionManager.updateActions(_actions);
this._actionManager.setDefaultExecute("javaScript:nm_action");
this._objectManager = new egwActionObjectManager("",
this._actionManager, EGW_AO_FLAG_IS_CONTAINER);
this._initActions(_actions)
}
else
{
this._actionManager = null;
this._objectManager = _objectManager;
}
this._actionLinks = _actionLinks;
// Call the parent et2_dataview_controller constructor
this._super(_grid, this, this._rowCallback, this._linkCallback, this,
this._objectManager);
// Copy all parameters
this.egw = _egw;
// Copy the given parameters
this._actionLinks = _actionLinks
this._execId = _execId;
this._widgetId = _widgetId;
this._rowProvider = _rowProvider;
@ -78,22 +75,19 @@ var et2_nextmatch_controller = et2_dataview_controller.extend(
// Directly use the API-Implementation of dataRegisterUID and
// dataUnregisterUID
this.dataRegisterUID = _egw.dataRegisterUID;
this.dataUnregisterUID = _egw.dataUnregisterUID;
},
destroy: function () {
// If the actionManager variable is set, the object- and actionManager
// were created by this instance -- clear them
if (this._actionManager)
{
this._objectManager.clear();
// TODO: No such method. Maybe implement it?
//this._actionManager.clear();
this._objectManager.remove();
this._actionManager.remove();
}
//this._super();
this._super();
},
/**
@ -108,6 +102,36 @@ var et2_nextmatch_controller = et2_dataview_controller.extend(
/** -- PRIVATE FUNCTIONS -- **/
/**
* Initializes the action and the object manager.
*/
_initActions: function (_actions) {
// Generate a uid for the action and object manager
var uid = this.egw.uid();
// Initialize the action manager and add some actions to it
var gam = egw_getActionManager(this.egw.appName);
this._actionManager = gam.addAction("actionManager", uid);
this._actionManager.updateActions(_actions);
// Set the default execute handler
var self = this;
this._actionManager.setDefaultExecute(function (_action, _senders, _target) {
// Get the selected ids descriptor object
var ids = self._selectionMgr.getSelected();
// Call the nm_action function with the ids
nm_action(_action, _senders, _target, ids);
});
// Initialize the object manager
var gom = egw_getObjectManager(this.egw.appName);
this._objectManager = gom.addObject(
new egwActionObjectManager(uid, this._actionManager));
this._objectManager.flags = this._objectManager.flags
| EGW_AO_FLAG_DEFAULT_FOCUS | EGW_AO_FLAG_IS_CONTAINER;
},
/**
* Overwrites the inherited _destroyCallback function in order to be able
* to free the "rowWidget".
@ -166,8 +190,9 @@ var et2_nextmatch_controller = et2_dataview_controller.extend(
_context);
},
dataRegisterUID: function () {
// Overwritten in the constructor
dataRegisterUID: function (_uid, _callback, _context) {
this.egw.dataRegisterUID(_uid, _callback, _context, this._execId,
this._widgetId);
},
dataUnregisterUID: function () {

View File

@ -161,6 +161,24 @@ function egwAction(_parent, _id, _caption, _iconUrl, _onExecute, _allowOnMultipl
this.onExecute = new egwFnct(this, null, []);
}
/**
* Clears the element and removes it from the parent container
*/
egwAction.prototype.remove = function () {
// Remove all references to the child elements
this.children = [];
// Remove this element from the parent list
if (this.parent)
{
var idx = this.parent.children.indexOf(this);
if (idx >= 0)
{
this.parent.children.splice(idx, 1);
}
}
}
/**
* Searches for a specific action with the given id
*/

View File

@ -143,8 +143,18 @@ egw.extend("data", egw.MODULE_APP_LOCAL, function (_app, _wnd) {
* called.
*/
dataFetch: function (_execId, _queriedRange, _filters, _widgetId,
_callback, _context)
_callback, _context, _knownUids)
{
var lm = lastModification;
if (_queriedRange["no_data"])
{
lm = 0xFFFFFFFFFFFF;
}
else if (_queriedRange["only_data"])
{
lm = 0;
}
var request = egw.json(
"etemplate_widget_nextmatch::ajax_get_rows::etemplate",
[
@ -152,8 +162,8 @@ egw.extend("data", egw.MODULE_APP_LOCAL, function (_app, _wnd) {
_queriedRange,
_filters,
_widgetId,
egw.dataKnownUIDs(_app),
_queriedRange["no_data"] ? 0xFFFFFFFFFFFF : lastModification
_knownUids ? _knownUids : egw.dataKnownUIDs(_app),
lm
],
function(result) {
parseServerResponse(result, _callback, _context);
@ -186,6 +196,21 @@ egw.extend("data_storage", egw.MODULE_GLOBAL, function (_app, _wnd) {
*/
var registeredCallbacks = {};
/**
* Uids and timers used for querying data uids, hashed by the first few
* bytes of the _execId, stored as an object of the form
* {
* "timer": <QUEUE TIMER>,
* "uids": <ARRAY OF UIDS>
* }
*/
var queue = {};
/**
* Contains the queue timeout in milliseconds.
*/
var QUEUE_TIMEOUT = 10;
/**
* This constant specifies the maximum age of entries in the local storrage
* in milliseconds
@ -230,9 +255,14 @@ egw.extend("data_storage", egw.MODULE_GLOBAL, function (_app, _wnd) {
*
* @param _uid is the uid for which the callback should be registered.
* @param _callback is the callback which should get called.
* @param _context is an optional parameter which can
* @param _context is the optional context in which the callback will be
* executed
* @param _execId is the exec id which will be used in case the data is
* not available
* @param _widgetId is the widget id which will be used in case the uid
* has to be fetched.
*/
dataRegisterUID: function (_uid, _callback, _context) {
dataRegisterUID: function (_uid, _callback, _context, _execId, _widgetId) {
// Create the slot for the uid if it does not exist now
if (typeof registeredCallbacks[_uid] === "undefined")
{
@ -253,6 +283,33 @@ egw.extend("data_storage", egw.MODULE_GLOBAL, function (_app, _wnd) {
localStorage[_uid].timestamp = (new Date).getTime();
_callback.call(_context, localStorage[_uid].data);
}
else if (_execId && _widgetId)
{
// Get the first 50 bytes of the exex id
var hash = _execId.substring(0, 50);
// Create a new queue if it does not exist yet
if (typeof queue[hash] === "undefined")
{
var self = this;
queue[hash] = { "uids": [], "timer": null };
queue[hash].timer = window.setTimeout(function () {
// Fetch the data
self.dataFetch(_execId, {"start": 0, "num_rows": 0, "only_data": true},
[], _widgetId, null, null, queue[hash].uids);
// Delete the queue entry
delete queue[hash];
}, 10);
}
// Push the uid onto the queue
queue[hash].uids.push(_uid.split("::").pop());
}
else
{
this.debug("log", "Data for uid " + _uid + " not available.");
}
},
/**