diff --git a/etemplate/js/et2_core_common.js b/etemplate/js/et2_core_common.js index a04071750d..ff2a747d41 100644 --- a/etemplate/js/et2_core_common.js +++ b/etemplate/js/et2_core_common.js @@ -651,3 +651,11 @@ function et2_rangeIntersectDir(_ar1, _ar2) return 0; } +/** + * Returns whether two ranges are equal. + */ +function et2_rangeEqual(_ar1, _ar2) +{ + return _ar1.top === _ar2.top && _ar1.bottom === _ar2.bottom; +} + diff --git a/etemplate/js/et2_dataview_view_gridContainer.js b/etemplate/js/et2_dataview.js similarity index 93% rename from etemplate/js/et2_dataview_view_gridContainer.js rename to etemplate/js/et2_dataview.js index ec89040174..5474925b7f 100644 --- a/etemplate/js/et2_dataview_view_gridContainer.js +++ b/etemplate/js/et2_dataview.js @@ -1,12 +1,12 @@ /** - * eGroupWare eTemplate2 - Class which generates the outer container for the grid + * eGroupWare eTemplate2 - dataview code * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage dataview * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 + * @copyright Stylite 2011-2012 * @version $Id$ */ @@ -16,14 +16,20 @@ jquery.jquery; et2_core_common; + et2_dataview_model_columns; + et2_dataview_view_rowProvider; et2_dataview_view_grid; et2_dataview_view_resizeable; */ /** - * Base view class which is responsible for displaying a grid view element. + * The et2_dataview class is the main class for displaying a dataview. The + * dataview class manages the creation of the outer html nodes (like the table, + * header, etc.) and contains the root container: an instance of + * et2_dataview_view_grid, which can be accessed using the "grid" property of + * this object. */ -var et2_dataview_gridContainer = Class.extend({ +var et2_dataview = Class.extend({ /** * Constant which regulates the column padding. @@ -49,11 +55,10 @@ var et2_dataview_gridContainer = Class.extend({ * Constructor for the grid container * @param object _parentNode is the DOM-Node into which the grid view will be inserted */ - init: function(_parentNode, _dataProvider, _egw) { + init: function(_parentNode, _egw) { // Copy the arguments this.parentNode = $j(_parentNode); - this.dataProvider = _dataProvider; this.egw = _egw; // Initialize some variables @@ -344,7 +349,9 @@ var et2_dataview_gridContainer = Class.extend({ // Add the full row and spacer class this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow", - "width: " + (totalWidth - this.columnBorderWidth - 1) + "px; border-right-width: 0 !important;"); + "width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow", + "border-right-width: 0 !important;"); this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow", "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;"); }, @@ -430,11 +437,12 @@ var et2_dataview_gridContainer = Class.extend({ this.rowProvider = new et2_dataview_rowProvider(this.uniqueId, colIds); // Create the grid class and pass "19" as the starting average row height - this.grid = new et2_dataview_grid(null, this.uniqueId, colIds, - this.dataProvider, this.rowProvider, 19); + this.grid = new et2_dataview_grid(null, this.egw, this.rowProvider, 19); // Insert the grid into the DOM-Tree - this.containerTr.append(this.grid.getJNode()); + var tr = $j(this.grid._nodes[0]); + this.containerTr.replaceWith(tr); + this.containerTr = tr; }, /* --- Code for calculating the browser/css depending widths --- */ diff --git a/etemplate/js/et2_dataview_controller.js b/etemplate/js/et2_dataview_controller.js new file mode 100644 index 0000000000..4fbaa36460 --- /dev/null +++ b/etemplate/js/et2_dataview_controller.js @@ -0,0 +1,422 @@ +/** + * eGroupWare eTemplate2 + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011-2012 + * @version $Id$ + */ + +/*egw:uses + et2_core_common; + et2_core_inheritance; + + et2_dataview_interfaces; + et2_dataview_view_row; +*/ + +/** + * The fetch timeout specifies the time during which the controller tries to + * consolidate requests for rows. + */ +var ET2_DATAVIEW_FETCH_TIMEOUT = 50; + +/** + * The et2_dataview_controller class is the intermediate layer between a grid + * instance and the corresponding data source. It manages updating the grid, + * as well as inserting and deleting rows. + */ +var et2_dataview_controller = Class.extend({ + + /** + * Constructor of the et2_dataview_controller, connects to the grid + * callback. + */ + init: function (_grid, _dataProvider, _rowCallback, _context) + { + // Copy the given arguments + this._grid = _grid; + this._dataProvider = _dataProvider; + this._rowCallback = _rowCallback; + this._rowContext = _context; + + // Initialize the "index map" which contains all currently displayed + // containers hashed by the "index" + this._indexMap = {}; + + // "lastModified" contains the last timestap which was returned from the + // server. + this._lastModified = null; + + // Register the dataFetch callback + this._grid.setDataCallback(this._gridCallback, this); + }, + + /** + * The update function queries the server for changes in the currently + * managed index range -- those changes are then merged into the current + * view without a complete rebuild of every row. + */ + update: function () { + // Get the currently visible range from the grid + var range = this._grid.getIndexRange(); + + // Force range.top and range.bottom to contain an integer + if (range.top === false) + { + range.top = range.bottom = 0; + } + + // Require that range from the server + this._queueFetch(range.top, range.bottom - range.top + 1, + this._lastModified !== null); + }, + + /** + * Rebuilds the complete grid. + */ + reset: function () { + // Throw away all internal mappings and reset the timestamp + this._indexMap = {}; + this._lastModified = null; + + // Clear the grid + this._grid.clear(); + + // Update the data + this.update(); + }, + + + /* -- PRIVATE FUNCTIONS -- */ + + + _getIndexEntry: function (_idx) { + // Create an entry in the index map if it does not exist yet + if (typeof this._indexMap[_idx] === "undefined") + { + this._indexMap[_idx] = { + "row": null, + "uid": null + }; + } + + // Always update the index of the entries before returning them. This is + // neccessary, as when we remove the uid from an entry without row, its + // index does not get updated any further + this._indexMap[_idx]["idx"] = _idx; + + return this._indexMap[_idx]; + }, + + /** + * Inserts a new data row into the grid. index and uid are derived from the + * given management entry. If the data for the given uid does not exist yet, + * a "loading" placeholder will be shown instead. The function will do + * nothing if there already is a row associated to the entry. This function + * will not re-insert a row if the entry already had a row. + * + * @param _entry is the management entry for the index the row will be + * displayed at. + * @param _update specifies whether the row should be updated if _entry.row + * already exists. + * @return true, if all data for the row has been available, false + * otherwise. + */ + _insertDataRow: function (_entry, _update) { + // Abort if the entry already has a row but the _insert flag is not set + if (_entry.row && !_update) + { + return true; + } + + // Context used for the callback functions + var ctx = {"self": this, "entry": _entry}; + + // Create a new row instance, if it does not exist yet + var createdRow = false; + if (!_entry.row) + { + createdRow = true; + _entry.row = new et2_dataview_row(this._grid); + _entry.row.setDestroyCallback(this._destroyCallback, ctx); + } + + // Load the row data if we have a uid for the entry + this.hasData = false; // Gets updated by the _dataCallback + if (_entry.uid) + { + // Register the callback / immediately load the data + this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback, + ctx); + } + + // Display the loading "row prototype" if we don't have data for the row + if (!this.hasData) + { + // Get the average height, the "-5" derives from the td padding + var avg = Math.round(this._grid.getAverageHeight() - 5) + "px"; + var prototype = this._grid.getRowProvider().getPrototype("loading"); + $j("div", prototype).css("height", avg); + var node = _entry.row.getJNode(); + node.empty(); + node.append(prototype.children()); + } + + // Insert the row into the table -- the same row must never be inserted + // twice into the grid, so this function only executes the following + // code only if it is a newly created row. + if (createdRow) + { + this._grid.insertRow(_entry.idx, _entry.row); + } + + return this.hasData; + }, + + /** + * Function which gets called by the grid when data is requested. + * + * @param _idxStart is the index of the first row for which data is + * requested. + * @param _idxEnd is the index of the last requested row. + */ + _gridCallback: function (_idxStart, _idxEnd) { + + var needsData = false; + + // Iterate over all elements the dataview requested and create a row + // which indicates that we are currently loading data + for (var i = _idxStart; i <= _idxEnd; i++) + { + var entry = this._getIndexEntry(i); + + // Insert the row for the entry -- do not update rows which are + // already existing, as we do not have new data for those. + if (!this._insertDataRow(entry, false) && needsData === false) + { + needsData = i; + } + } + + // Queue fetching that data range + if (needsData !== false) + { + this._queueFetch(needsData, _idxEnd - needsData + 1, false); + } + }, + + /** + * + */ + _queueFetch: function (_start, _numRows, _refresh) { + // Sanitize the request + _start = Math.max(0, _start); + _numRows = Math.min(this._grid.getTotalCount(), _start + _numRows) + - _start; + + // Context used in the callback function + var ctx = { "self": this, "start": _start, "count": _numRows }; + + // Build the query + var query = { "start": _start, "num_rows": _numRows, "refresh": _refresh }; + + // Call the callback + this._dataProvider.dataFetch(query, this._lastModified, + this._fetchCallback, ctx); + }, + + /** + * + */ + _dataCallback: function (_data) { + // Set the "hasData" flag + this.self.hasData = true; + + // Call the row callback with the new data -- the row callback then + // generates the row DOM nodes that will be inserted into the grid + if (this.self._rowCallback) + { + // Remove everything from the current row + this.entry.row.clear(); + + // Fill the row DOM Node with data + this.self._rowCallback.call( + this.self._rowContext, + _data, + this.entry.row.getDOMNode(), + this.entry.idx, + this.entry + ); + + // Invalidate the current row entry + this.entry.row.invalidate(); + } + }, + + /** + * + */ + _destroyCallback: function (_row) { + // There is no further row connected to the entry + this.entry.row = null; + + // Unregister the data callback + this.self._dataProvider.dataUnregisterUID(this.entry.uid, + this.self._dataCallback, null); + }, + + /** + * Returns an array containing "_count" index mapping entries starting from + * the index given in "_start". + */ + _getIndexMapping: function (_start, _count) { + var result = []; + + for (var i = _start; i < _start + _count; i++) + { + result.push(this._getIndexEntry(i)); + } + + return result; + }, + + /** + * Updates the grid according to the new order. The function simply does the + * following: It iterates along the new order (given in _order) and the old + * order given in _idxMap. Iteration variables used are + * a) i -- points to the current entry in _order + * b) idx -- points to the current grid row that will be effected by + * this operation. + * c) mapIdx -- points to the current entry in _indexMap + * The following cases may occur: + * a) The current entry in the old order has no uid or no row -- in that + * case the row at the current position is simply updated, + * the old pointer will be incremented. + * b) The two uids differ -- insert a new row with the new uid, do not + * increment the old pointer. + * c) The two uids are the same -- increment the old pointer. + * In a last step all rows that are left in the old order are deleted. All + * newly created index entries are returned. This function does not update + * the internal mapping in _idxMap. + */ + _updateOrder: function (_start, _count, _idxMap, _order) { + // The result contains the newly created index map entries which have to + // be merged with the result + var result = []; + + // Iterate over the new order + var mapIdx = 0; + var idx = _start; + for (var i = 0; i < _order.length; i++, idx++) + { + var current = _idxMap[mapIdx]; + + if (!current.row || !current.uid) + { + // If there is no row yet at the current position or the uid + // of that entry is unknown, simply update the entry. + current.uid = _order[i]; + current.idx = idx; + this._insertDataRow(current, true); + mapIdx++; + } + else if (current.uid !== _order[i]) + { + // Insert a new row at the new position + var entry = { + "idx": idx, + "uid": _order[i], + "row": null + }; + this._insertDataRow(entry, true); + + // Remember the new entry + result.push(entry); + } + else + { + // Do nothing, the uids do not differ, just update the index of + // the element + current.idx = idx; + mapIdx++; + } + } + + // Delete as many rows as we have left + for (var i = mapIdx; i < _idxMap.length; i++) + { + this._grid.deleteRow(idx); + } + + return result; + }, + + _mergeResult: function (_newEntries, _invalidStartIdx, _diff) { + + if (_newEntries.length > 0 || _diff > 0) + { + // Create a new index map + var newMap = {}; + + // Insert all new entries into the new index map + for (var i = 0; i < _newEntries.length; i++) + { + newMap[_newEntries[i].idx] = _newEntries[i]; + } + + // Insert all old entries that have a row into the new index map + // while adjusting their indices + for (var key in this._indexMap) + { + // Get the corresponding index entry + var entry = this._indexMap[key]; + + // Only keep index entries which are currently displayed + if (entry.row) + { + // Calculate the new index -- if rows were deleted, we'll + // have to adjust the index + var newIdx = entry.idx >= _invalidStartIdx + ? entry.idx - _diff : entry.idx; + entry.idx = key; + newMap[newIdx] = entry; + } + } + + // Make the new index map the current index map + this._indexMap = newMap; + } + + }, + + _fetchCallback: function (_response) { + // Do nothing if _response.order evaluates to false + if (!_response.order) + { + return; + } + + // Get the current index map for the updated region + var idxMap = this.self._getIndexMapping(this.start, this.count); + + // Update the grid using the new order. The _updateOrder function does + // not update the internal mapping while inserting and deleting rows, as + // this would move us to another asymptotic runtime level. + var res = this.self._updateOrder(this.start, this.count, idxMap, + _response.order); + + // Merge the new indices, update all indices with rows that were not + // affected and invalidate all indices if there were changes + this.self._mergeResult(res, this.start + _response.order.length, + idxMap.length - _response.order.length); + + // Update the total element count in the grid + this.self._grid.setTotalCount(_response.total); + } + +}); + diff --git a/etemplate/js/et2_dataview_interfaces.js b/etemplate/js/et2_dataview_interfaces.js index 0d1ace3424..201e788b3d 100644 --- a/etemplate/js/et2_dataview_interfaces.js +++ b/etemplate/js/et2_dataview_interfaces.js @@ -1,5 +1,5 @@ /** - * eGroupWare eTemplate2 - Contains the dataview base object. + * eGroupWare eTemplate2 - Contains interfaces used inside the dataview * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate @@ -20,13 +20,7 @@ var et2_dataview_IInvalidatable = new Interface({ invalidate: function() {} -}); - -var et2_dataview_IDataRow = new Interface({ - - updateData: function(_data) {} - -}); +}); var et2_dataview_IViewRange = new Interface({ @@ -35,51 +29,70 @@ var et2_dataview_IViewRange = new Interface({ }); /** - * Interface which objects have to implement, that want to act as low level - * datasource. - */ -var et2_IRowFetcher = new Interface({ - - /** - * @param _fetchList is an array consisting of objects whith the entries - * "startIdx" and "count" - * @param _callback is the callback which is called when the data is ready - * (may be immediately or deferred). The callback has the following - * signature: - * function (_rows) - * where _rows is an associative array which contains the data for that row. - * @param _context is the context in which the callback should run. - */ - getRows: function(_fetchList, _callback, _context) {} - -}); - -/** - * Interface the data provider has to implement + * Interface a data provider has to implement. The data provider functions are + * called by the et2_dataview_controller class. The data provider basically acts + * like the egw api egw_data extension, but some etemplate specific stuff has + * been stripped away -- the implementation (for the nextmatch widget that is + * et2_extension_nextmatch_dataprovider) has to take care of that. */ var et2_IDataProvider = new Interface({ /** - * Returns the total count of grid elements + * This function is used by the et2_dataview_controller to fetch data for + * a certain range. The et2_dataview_controller provides data which allows + * to only update elements which really have changed. + * + * @param queriedRange is an object of the following form: + * { + * start: , + * num_rows: + * } + * @param knownRange is an array of the above form and informs the + * implementation which range is already known to the client. This parameter + * may be null in order to indicate that the client currently has no valid + * data. + * @param lastModification is the last timestamp that was returned from the + * data provider and for which the client has data. It may be null in order + * to indicate, that the client currently has no data or needs a complete + * refresh. + * @param callback is the function that should get called, once the data + * is available. The data passed to the callback function has the + * following form: + * { + * order: [uid, ...], + * total: , + * lastModification: + * } + * @param context is the context in which the callback function will get + * called. */ - getCount: function() {}, + dataFetch: function (_queriedRange, _lastModification, _callback, _context) {}, /** - * Registers the given dataRow for the given index. Calls _dataRow.updateData - * as soon as data is available for that row. + * Registers the intrest in a certain uid for a callback function. If + * the data for that uid changes or gets loaded, the given callback + * function is called. If the data for the given uid is available at the + * time of registering the callback, the callback is called immediately. + * + * @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 */ - registerDataRow: function(_dataRow, _idx) {}, + dataRegisterUID: function (_uid, _callback, _context) {}, /** - * Stops calling _dataRow.updateData for the dataRow registered for the given - * index. + * Unregisters the intrest of updates for a certain data uid. + * + * @param _uid is the data uid for which the callbacks should be + * unregistered. + * @param _callback specifies the specific callback that should be + * unregistered. If it evaluates to false, all callbacks (or those + * matching the optionally given context) are removed. + * @param _context specifies the callback context that should be + * unregistered. If it evaluates to false, all callbacks (or those + * matching the optionally given callback function) are removed. */ - unregisterDataRow: function(_idx) {}, - - /** - * Returns the action object manager (if one exists, otherwise null) - */ - getActionObjectManager: function() {} + dataUnregisterUID: function (_uid, _callback, _context) {} }); diff --git a/etemplate/js/et2_dataview_model_dataProvider.js b/etemplate/js/et2_dataview_model_dataProvider.js deleted file mode 100644 index 9f380592dd..0000000000 --- a/etemplate/js/et2_dataview_model_dataProvider.js +++ /dev/null @@ -1,288 +0,0 @@ -/** - * eGroupWare eTemplate2 - Class which contains a the data model - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link http://www.egroupware.org - * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ - */ - -"use strict" - -/*egw:uses - egw_action.egw_action; - - et2_core_inheritance; - et2_core_common; - et2_dataview_interfaces; -*/ - -var et2_dataview_dataProvider = Class.extend(et2_IDataProvider, { - - /** - * Creates this instance of the data provider. - */ - init: function(_source, _total, _actionMgr) { - this._source = _source; - this._total = _total; - - this._registeredRows = {}; - - this._data = {}; - this._dataCount = 0; - - this._queue = {}; - this._queueSize = 0; - - this._stepSize = 25; // Count of elements which is loaded at once - this._maxCount = 1000; // Maximum count before the elements are cleaned up - - // Create an action object manager - this.actionObjectManager = new egwActionObjectManager("", _actionMgr); - - var self = this; - this._cleanupInterval = window.setInterval(function() {self._cleanup()}, - 10 * 1000); - this._queueFlushTimeout = null; - }, - - destroy: function() { - // Destroy the cleanup timer callback - window.clearInterval(this._cleanupInterval); - - // Destroy the _queueFlushTimeout - this._stopFlushTimer(); - - // Destroy the actionObject manager - this.actionObjectManager.clear(); - }, - - /** - * Resets the stored data and aborts any queue request - */ - clear: function() { - this._data = {}; - this._dataCount = 0; - - this._queue = {}; - this._queueSize = 0; - - this._stopFlushTimer(); - - // Clear all elements in the action object manager - this.actionObjectManager.clear(); - }, - - /** - * Data is an object containing an "rows" and "readonlys" array. - */ - loadData: function(_data) { - this._receiveData(_data); - }, - - /** - * Returns the total count - */ - getCount: function() { - return this._total; - }, - - registerDataRow: function(_dataRow, _idx) { - // Make sure _idx is a int - _idx = parseInt(_idx); - - if (typeof this._registeredRows[_idx] != "undefined") - { - this.egw().debug("warn", "Overriding data row for index " + _idx); - } - - // Associate the given data row with that index - this._registeredRows[_idx] = _dataRow; - - // Check whether an entry exists in the data array - if yes, call the - // request immediately - if (typeof this._data[_idx] != "undefined") - { - if (this._data[_idx].data == false) - { - egw().debug("warn", "App provides blank first row, which causes problems"); - return; - } - this._callUpdateData(_idx); - } - else - { - this._queueIndex(_idx); - } - }, - - unregisterDataRow: function(_idx) { - // Make sure _idx is a int - _idx = parseInt(_idx); - - delete(this._registeredRows[_idx]); - }, - - getActionObjectManager: function() { - return this.actionObjectManager; - }, - - /* ---- PRIVATE FUNCTIONS ---- */ - - _queueIndex: function(_idx) { - // Mark the index as queued - if (typeof this._queue[_idx] == "undefined") - { - this._queue[_idx] = true; - this._queueSize++; - } - - if (this._queueSize > this._stepSize) - { - this._flushQueue(); - } - else - { - // (Re)start the queue flush timer - var self = this; - this._stopFlushTimer(); - this._queueFlushTimeout = window.setTimeout(function() { - self._queueFlushTimeout = null; - self._flushQueue(); - }, 50); - } - }, - - _flushQueue: function() { - // Stop the flush timer if it is still active - this._stopFlushTimer(); - - // Mark all elements in a radius of this._stepSize / 2 - var marked = {}; - var r = Math.floor(this._stepSize / 2); - for (var key in this._queue) - { - key = parseInt(key); - - var b = Math.max(0, key - r); - var t = Math.min(key + this._stepSize, this._total - 1); - var c = 0; - for (var i = b; i <= t && c < this._stepSize; i ++) - { - if (typeof this._data[i] == "undefined") - { - marked[i] = true; - c++; - } - } - } - - // Reset the queue - this._queue = {}; - this._queueSize = 0; - - // Create a list with start indices and counts - var fetchList = []; - var entry = null; - var last = 0; - - // Get the int keys and sort the array numeric - var arr = et2_arrayIntKeys(marked).sort(function(a,b){return a > b ? 1 : (a == b ? 0 : -1)}); - - for (var i = 0; i < arr.length; i++) - { - if (i == 0 || arr[i] - last > 1) - { - if (entry) - { - fetchList.push(entry); - } - entry = { - "startIdx": arr[i], - "count": 1 - }; - } - else - { - entry.count++; - } - - last = arr[i]; - } - - if (entry) - { - fetchList.push(entry); - } - - // Call the "getRows" callback - this._source.getRows(fetchList, this._receiveData, this); - }, - - _receiveData: function(_data) { - var time = (new Date).getTime(); - - for (var key in _data.rows) - { - if (!isNaN(key)) - { - // Make sure the key is a int - key = parseInt(key); - - // Copy the data for the given index - this._data[key] = { - "data": _data.rows[key], - "timestamp": time - }; - - // Update the row associated to the index - this._callUpdateData(key); - } - } - }, - - _stopFlushTimer: function() { - // Stop the queue flush timer - if (this._queueFlushTimeout !== null) - { - window.clearTimeout(this._queueFlushTimeout); - } - }, - - _callUpdateData: function(_idx) { - if (typeof this._registeredRows[_idx] != "undefined") - { - this._registeredRows[_idx].updateData({ - "content": this._data[_idx].data - }); - } - }, - - _cleanup: function() { - // Delete all data rows which have not been accessed for more than - // "delta" ms (5 minutes) - this method does not ensure that _dataCount - // gets below _maxCount! - var delta = 5 * 60 * 1000; - var now = (new Date).getTime(); - - if (this._dataCount > this._maxCount) - { - for (var key in this._data) - { - var entry = this._data[key]; - - if (now - entry.timestamp > delta) - { - delete(this._data[key]); - this._dataCount--; - } - } - } - } - -}); - - diff --git a/etemplate/js/et2_dataview_view_container.js b/etemplate/js/et2_dataview_view_container.js index 56f5077c63..925f29d2a0 100644 --- a/etemplate/js/et2_dataview_view_container.js +++ b/etemplate/js/et2_dataview_view_container.js @@ -1,12 +1,12 @@ /** - * eGroupWare eTemplate2 - Class which contains the "row" base class + * eGroupWare eTemplate2 - dataview code * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage dataview * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 + * @copyright Stylite 2012 * @version $Id$ */ @@ -17,60 +17,99 @@ et2_dataview_interfaces; */ +/** + * The et2_dataview_container class is the main object each dataview consits of. + * Each row, spacer as well as the grid itself are containers. A container is + * described by its parent element and a certain height. On the DOM-Level a + * container may consist of multiple "tr" nodes, which are treated as a unit. + * Some containers (like grid containers) are capable of managing a set of child + * containers. Each container can indicate, that it thinks that it's height + * might have changed. In that case it informs its parent element about that. + * The only requirement for the parent element is, that it implements the + * et2_dataview_IInvalidatable interface. + * A container does not know where it resides inside the grid, or whether it is + * currently visible or not -- this information is efficiently managed by the + * et2_dataview_grid container. + */ var et2_dataview_container = Class.extend(et2_dataview_IInvalidatable, { /** * Initializes the container object. * - * @param _dataProvider is the data provider for the element - * @param _rowProvider is the "rowProvider" of the element - * @param _invalidationElem is the element of which the "invalidate" method - * will be called if the height of the elements changes. It has to - * implement the et2_dataview_IInvalidatable interface. + * @param _parent is an object which implements the IInvalidatable + * interface. _parent may not be null. */ - init: function(_dataProvider, _rowProvider, _invalidationElem) { - this.dataProvider = _dataProvider; - this.rowProvider = _rowProvider; - this._invalidationElem = _invalidationElem; + init: function(_parent) { + // Copy the given invalidation element + this._parent = _parent; - this._nodes = []; - this._inTree = false; + this._nodes = []; // contains all DOM-Nodes this container exists of + this._inTree = false; // this._attachData = {"node": null, "prepend": false}; + this._destroyCallback = null; + this._destroyContext = null; + + this._height = false; + this._index = 0; + this._top = 0; }, + /** + * Destroys this container. Classes deriving from et2_dataview_container + * should override this method and take care of unregistering all event + * handlers etc. + */ destroy: function() { // Remove the nodes from the tree this.removeFromTree(); - }, - /** - * Setter function which can be used to update the invalidation element. - * - * @param _invalidationElem is the element of which the "invalidate" method - * will be called if the height of the elements changes. It has to - * implement the et2_dataview_IInvalidatable interface. - */ - setInvalidationElement: function(_invalidationElem) { - this._invalidationElem = _invalidationElem; - }, - - /** - * Inserts all container nodes into the DOM tree after the given element - */ - insertIntoTree: function(_afterNode, _prepend) { - if (!this._inTree && _afterNode != null) + // Call the callback function (if one is registered) + if (this._destroyCallback) { + this._destroyCallback.call(this._destroyContext, this); + } + }, + + /** + * Sets the "destroyCallback" -- the given function gets called whenever + * the container is destroyed. This instance is passed as an parameter to + * the callback. + */ + setDestroyCallback: function(_callback, _context) { + this._destroyCallback = _callback; + this._destroyContext = _context; + }, + + /** + * Inserts all container nodes into the DOM tree after or before the given + * element. + * + * @param _node is the node after/before which the container "tr"s should + * get inserted. _node should be a simple DOM node, not a jQuery object. + * @param _prepend specifies whether the container should be inserted before + * or after the given node. Inserting before is needed for inserting the + * first element in front of an spacer. + */ + insertIntoTree: function(_node, _prepend) { + + if (!this._inTree && _node != null && this._nodes.length > 0) + { + // Store the parent node and indicate that this element is now in + // the tree. + this._attachData = {"node": _node, "prepend": _prepend}; + this._inTree = true; + for (var i = 0; i < this._nodes.length; i++) { if (i == 0) { if (_prepend) { - _afterNode.prepend(this._nodes[i]); + _node.before(this._nodes[0]); } else { - _afterNode.after(this._nodes[i]); + _node.after(this._nodes[0]); } } else @@ -80,10 +119,8 @@ var et2_dataview_container = Class.extend(et2_dataview_IInvalidatable, { } } - // Save the "attachData" - this._inTree = true; - this._attachData = {"node": _afterNode, "prepend": _prepend}; - + // Invalidate this element in order to update the height of the + // parent this.invalidate(); } }, @@ -108,7 +145,9 @@ var et2_dataview_container = Class.extend(et2_dataview_IInvalidatable, { }, /** - * Appends a jQuery node to the container + * Appends a node to the container. + * + * @param _node is the DOM-Node which should be appended. */ appendNode: function(_node) { // Add the given node to the "nodes" array @@ -118,20 +157,20 @@ var et2_dataview_container = Class.extend(et2_dataview_IInvalidatable, { // tree. if (this._inTree) { - if (this._nodes.length == 1) + if (_nodes.length === 1) { - if (_prepend) + if (this._attachData.prepend) { - this._attachData.node.prepend(this._nodes[0]); + this._attachData.node.before(_node); } else { - this._attachData.node.after(this._nodes[0]); + this._attachData.node.after(_node); } } else { - this._nodes[_nodes.length - 2].after(_node); + _node.after(this._nodes[this._nodes.length - 2]); } this.invalidate(); @@ -139,7 +178,7 @@ var et2_dataview_container = Class.extend(et2_dataview_IInvalidatable, { }, /** - * Removes a jQuery node from the container + * Removes a certain node from the container */ removeNode: function(_node) { // Get the index of the node in the nodes array @@ -150,7 +189,7 @@ var et2_dataview_container = Class.extend(et2_dataview_IInvalidatable, { // Remove the node if the container is currently attached if (this._inTree) { - _node.remove(); + _node.parentNode.removeChild(_node); } // Remove the node from the nodes array @@ -173,41 +212,158 @@ var et2_dataview_container = Class.extend(et2_dataview_IInvalidatable, { }, /** - * Returns the accumulated height of all container nodes. Only visible nodes - * (without "display: none") are taken into account. + * Returns the first node of the container. */ - getHeight: function() { - var height = 0; - - if (this._inTree) - { - // Increment the height value for each visible container node - var self = this; - $j(this._nodes, ":visible").each(function() { - height += self._nodeHeight(this[0]); - }); - } - - return height; + getFirstNode: function() { + return this._nodes.length > 0 ? this._nodes[0] : null; }, /** - * Calls the "invalidate" function of the connected invalidation element. + * Returns the accumulated height of all container nodes. Only visible nodes + * (without "display: none" etc.) are taken into account. + */ + getHeight: function() { + if (this._height === false && this._inTree) + { + this._height = 0; + + // Increment the height value for each visible container node + for (var i = 0; i < this._nodes.length; i++) + { + if (this._isVisible(this._nodes[i][0])) + { + this._height += this._nodeHeight(this._nodes[i][0]); + } + } + } + + return this._height === false ? 0 : this._height; + }, + + /** + * Returns a datastructure containing information used for calculating the + * average row height of a grid. + * The datastructure has the + * { + * avgHeight: , + * avgCount: + * } + */ + getAvgHeightData: function() { + return { + "avgHeight": this.getHeight(), + "avgCount": 1 + } + }, + + /** + * Returns the previously set "pixel top" of the container. + */ + getTop: function() { + return this._top; + }, + + /** + * Returns the "pixel bottom" of the container. + */ + getBottom: function() { + return this._top + this.getHeight(); + }, + + /** + * Returns the range of the element. + */ + getRange: function() { + return et2_bounds(this.getTop(), this.getBottom()); + }, + + /** + * Returns the index of the element. + */ + getIndex: function() { + return this._index; + }, + + /** + * Returns how many elements this container represents. + */ + getCount: function() { + return 1; + }, + + /** + * Sets the top of the element. + */ + setTop: function(_value) { + this._top = _value; + }, + + /** + * Sets the index of the element. + */ + setIndex: function(_value) { + this._index = _value; + }, + + /* -- et2_dataview_IInvalidatable -- */ + + /** + * Broadcasts an invalidation through the container tree. Marks the own + * height as invalid. */ invalidate: function() { - this._invalidationElem.invalidate(); + // Abort if this element is already marked as invalid. + if (this._height !== false) + { + // Delete the own, probably computed height + this._height = false; + + // Broadcast the invalidation to the parent element + this._parent.invalidate(); + } + }, + + + /* -- PRIVATE FUNCTIONS -- */ + + + /** + * Used to check whether an element is visible or not (non recursive). + * + * @param _obj is the element which should be checked for visibility, it is + * only checked whether some stylesheet makes the element invisible, not if + * the given object is actually inside the DOM. + */ + _isVisible: function(_obj) { + + // Check whether the element is localy invisible + if (_obj.style && (_obj.style.display === "none" + || _obj.style.visiblity === "none")) + { + return false; + } + + // Get the computed style of the element + var style = window.getComputedStyle ? window.getComputedStyle(_obj, null) + : _obj.currentStyle; + if (style.display === "none" || style.visibility === "none") + { + return false; + } + + return true; } }); /** - * Returns the height of the container in pixels and zero if the element is not + * Returns the height of a node in pixels and zero if the element is not * visible. The height is clamped to positive values. - * The browser switch is placed at this position as the getHeight function is one - * of the mostly called functions in the whole grid code and should stay - * quite fast. + * The browser switch is placed at this position as the _nodeHeight function is + * one of the most frequently called functions in the whole grid code and should + * stay quite fast. */ -if ($j.browser.mozilla) +/*if ($j.browser.mozilla) { et2_dataview_container.prototype._nodeHeight = function(_node) { @@ -232,10 +388,10 @@ if ($j.browser.mozilla) } } else -{ +{*/ et2_dataview_container.prototype._nodeHeight = function(_node) { return _node.offsetHeight; } -} +//} diff --git a/etemplate/js/et2_dataview_view_grid.js b/etemplate/js/et2_dataview_view_grid.js index dd88eea6e4..cffbb664dc 100644 --- a/etemplate/js/et2_dataview_view_grid.js +++ b/etemplate/js/et2_dataview_view_grid.js @@ -17,169 +17,352 @@ et2_core_common; et2_dataview_interfaces; - et2_dataview_view_partitionTree; - et2_dataview_view_row; + et2_dataview_view_container; et2_dataview_view_spacer; - et2_dataview_view_rowProvider; */ /** - * Determines how many pixels the view range of the gridview is extended. + * Determines how many pixels the view range of the gridview is extended inside + * the scroll callback. */ -var ET2_GRID_VIEW_EXT = 25; +var ET2_GRID_VIEW_EXT = 50; /** * Determines the timeout after which the scroll-event is processed. */ -var ET2_GRID_SCROLL_TIMEOUT = 25; +var ET2_GRID_SCROLL_TIMEOUT = 50; -var partitionTree = null; +/** + * Determines the timeout after which the invalidate-request gets processed. + */ +var ET2_GRID_INVALIDATE_TIMEOUT = 25; -var et2_dataview_grid = Class.extend(et2_dataview_IViewRange, { +/** + * Determines how many elements are kept displayed outside of the current view + * range until they get removed. + */ +var ET2_GRID_HOLD_COUNT = 50; + +var et2_dataview_grid = et2_dataview_container.extend(et2_dataview_IViewRange, { /** * Creates the grid. * * @param _parent is the parent grid class - if null, this means that this - * is the outer grid which manages the scrollarea. If not null, all other - * parameters are ignored and copied from the given grid instance. - * @param _outerId is the id of the grid container it uses for the css - * classes. - * @param _columnIds is the id of the individual columns used for the css - * classes. + * is the outer grid which manages the scrollarea. If not null, all other + * parameters are ignored and copied from the given grid instance. * @param _avgHeight is the starting average height of the column rows. */ - init: function(_parent, _outerId, _columnIds, _dataProvider, _rowProvider, - _avgHeight) { + init: function (_parent, _egw, _rowProvider, _avgHeight) { + + // Call the inherited constructor + this._super(_parent); // If the parent is given, copy all other parameters from it if (_parent != null) { - this._outerId = _parent._outerId; - this._columnIds = _parent._columnIds; - this._dataProvider = _parent._dataProvider; - this._avgHeight = _parent._partitionTree.getAverageHeight(); + this.egw = _parent.egw; + this._orgAvgHeight = false; this._rowProvider = _parent._rowProvider; } else { // Otherwise copy the given parameters - this._outerId = _outerId; - this._columnIds = _columnIds; - this._dataProvider = _dataProvider; + this.egw = _egw; + this._orgAvgHeight = _avgHeight; this._rowProvider = _rowProvider; - this._avgHeight = _avgHeight; + // As this grid instance has no parent, we need a scroll container this._scrollHeight = 0; this._scrollTimeout = null; } - // The "treeChanged" variable is called whenever the viewrange has been - // set - changing the viewrange is the function which causes new elements - // to be generated and thus the partition tree to degenerate - this._treeChanged = false; + this._invalidateTimeout = null; - // Count of elements which are buffered at top and bottom of the viewrange - // before they get replaced with placeholders - this._holdCount = 50; + this._invalidateCallback = null; + this._invalidateContext = null; - // The current range holds the currently visible nodes - this._currentRange = et2_range(0, 0); + // _map contains a mapping between the grid indices and the elements + // associated to it. The first element in the array always refers to the + // element starting at index zero (being a spacer if the grid currently + // displays another range). + this._map = []; - // Build the grid outer nodes + // _viewRange contains the current pixel-range of the grid which is + // visible. + this._viewRange = et2_range(0, 0); + + // Holds the maximum count of elements + this._total = 0; + + // Holds data used for storing the current average height data + this._avgHeight = false; + this._avgCount = false; + + // Build the outer grid nodes this._createNodes(); - - // Create the partition tree object which is used to organize the tree - // items. - this._partitionTree = new et2_dataview_partitionTree(this._dataProvider, - this._rowProvider, this._avgHeight, this.innerTbody); - - // Setup the "rebuild" timer - it rebuilds the complete partition tree - // if any change has been done to it. Rebuilding the partition tree is - // necessary as the tree structure happens to degenerate. - var self = this; - this._rebuildTimer = window.setInterval(function() { - self._checkTreeRebuild(); - }, 10 * 1000); }, - destroy: function() { + destroy: function () { // Stop the scroll timeout if (this._scrollTimeout) { window.clearTimeout(this._scrollTimeout); } - // Stop the rebuild timer - window.clearInterval(this._rebuildTimer); + // Stop the invalidate timeout + if (this._invalidateTimeout) + { + window.clearTimeout(this._invalidateTimeout); + } - // Free the partition tree - this._partitionTree.free(); + this._super(); }, - clear: function() { - // Free the partition tree and recreate it - this._avgHeight = this._partitionTree.getAverageHeight(); - this._partitionTree.free(); - this._partitionTree = new et2_dataview_partitionTree(this._dataProvider, - this._rowProvider, this._avgHeight, this.innerTbody); + clear: function () { + // Store the old total count and rescue the current average height in + // form of the "original average height" + var oldTotalCount = this._total; + this._orgAvgHeight = this.getAverageHeight(); - // Set the viewrange again - this.setViewRange(this._currentRange); + // Set the total count to zero + this.setTotalCount(0); + + // Reset the total count value + this.setTotalCount(oldTotalCount); }, /** - * The setViewRange function updates the range in which columns are shown. + * The insertRow function can be called to insert the given container(s) at + * the given row index. If there currently is another container at that + * given position, the new container(s) will be inserted above the old + * container. Yet the "total count" of the grid will be preserved by + * removing the correct count of elements from the next possible spacer. If + * no spacer is found, the last containers will be removed. This causes + * inserting new containers at the end of a grid to be immediately removed + * again. + * + * @param _index is the row index at which the given container(s) should be + * inserted. + * @param _container is eiter a single et2_dataview_container instance + * which should be inserted at the given position. Or an array of + * et2_dataview_container instances. If you want to remove the container + * don't do that manually by calling its "destroy" function but use the + * deleteRow function. */ - setViewRange: function(_range) { - this._treeChanged = true; + insertRow: function (_index, _container) { + // Calculate the map element the given index refers to + var idx = this._calculateMapIndex(_index); - // Copy the given range - this._currentRange = et2_bounds(_range.top, _range.bottom); - - // Display all elements in the given range - var nodes = this._partitionTree.getRangeNodes(_range); - - for (var i = 0; i < nodes.length; i++) + if (idx !== false) { - if (nodes[i].implements(et2_dataview_IViewRange)) + // Wrap the container inside an array + if (_container instanceof et2_dataview_container) { - nodes[i].setViewRange(_range); + _container = [_container]; + } + + // Fetch the average height + var avg = this.getAverageHeight(); + + // Call the internal _doInsertContainer function + for (var i = 0; i < _container.length; i++) + { + this._doInsertContainer(_index, idx, _container[i], avg); + } + + // Schedule an "invalidate" event + this.invalidate(); + } + }, + + /** + * The deleteRow function can be used to remove the element at the given + * index. + * + * @param _index is the index from which should be deleted. If the given + * index is outside the so called "managedRange" nothing will happen, as the + * container has already been destroyed by the grid instance. + */ + deleteRow: function (_index) { + // Calculate the map element the given index refers to + var idx = this._calculateMapIndex(_index); + + if (idx !== false) + { + this._doDeleteContainer(idx, false); + + // Schedule an "invalidate" event + this.invalidate(); + } + }, + + /** + * The given callback gets called whenever the scroll position changed or + * the visible element range changed. The element indices are passed to the + * function as et2_range. + */ + setInvalidateCallback: function (_callback, _context) { + this._invalidateCallback = _callback; + this._invalidateContext = _context; + }, + + /** + * The setDataCallback function is used to set the callback that will be + * called when the grid requires new data. + * + * @param _callback is the callback function which gets called when the grid + * needs some new rows. + * @param _context is the context in which the callback function gets + * called. + */ + setDataCallback: function (_callback, _context) { + this._callback = _callback; + this._context = _context; + }, + + /** + * The updateTotalCount function can be used to update the total count of + * rows that are displayed inside the grid. Changing the count always causes + * the spacer at the bottom (if it exists) to be + * + * @param _count specifies how many entries the grid can show. + */ + setTotalCount: function (_count) { + // Abort if the total count has not changed + if (_count === this._total) + return; + + // Calculate how many elements have to be removed/added + var delta = Math.max(0, _count) - this._total; + + if (delta > 0) + { + this._appendEmptyRows(delta); + } + else + { + this._decreaseTotal(-delta); + } + + this._total = Math.max(0, _count); + + // Schedule an invalidate + this.invalidate(); + }, + + /** + * Returns the current "total" count. + */ + getTotalCount: function () { + return this._total; + }, + + /** + * The setViewRange function updates the range in which rows are shown. + */ + setViewRange: function (_range) { + // Set the new view range + this._viewRange = _range; + + // Immediately call the "invalidate" function + this._doInvalidate(); + }, + + /** + * Return the indices of the currently visible rows. + */ + getVisibleIndexRange: function (_viewRange) { + + function getElemIdx(_elem, _px) + { + if (_elem instanceof et2_dataview_spacer) + { + return _elem.getIndex() + + Math.floor((_px - _elem.getTop()) / this.getAverageHeight()); + } + + return _elem.getIndex(); + } + + var idxTop = 0; + var idxBottom = 0; + var vr; + + if (_viewRange) + { + vr = _viewRange; + } + else + { + // Calculate the "correct" view range by removing ET2_GRID_VIEW_EXT + vr = et2_bounds( + this._viewRange.top + ET2_GRID_VIEW_EXT, + this._viewRange.bottom - ET2_GRID_VIEW_EXT); + } + + // Get the elements at the top and the bottom of the view + var topElem = null; + var botElem = null; + for (var i = 0; i < this._map.length; i++) + { + if (!topElem && this._map[i].getBottom() > vr.top) + { + topElem = this._map[i]; + } + + if (this._map[i].getTop() > vr.bottom) + { + botElem = this._map[i]; + break; } } - // Calculate the range of the actually shown elements - var displayTop = _range.top; - var displayBottom = _range.bottom; - - if (nodes.length > 0) + if (!botElem) { - displayTop = nodes[0].getPosTop(); - displayBottom = nodes[nodes.length - 1].getPosBottom(); + botElem = this._map[this._map.length - 1]; } - // Hide everything except for _holdCount elements at the top and bottom - // of the viewrange - var ah = this._partitionTree.getAverageHeight(); - var reduceHeight = ah * this._holdCount; - - if (displayTop > reduceHeight) + if (topElem) { - this._partitionTree.reduceRange(et2_bounds(0, displayTop - reduceHeight)); + idxTop = getElemIdx.call(this, topElem, vr.top); + idxBottom = getElemIdx.call(this, botElem, vr.bottom); } - if (displayBottom + reduceHeight < this._partitionTree.getHeight()) + // Return the calculated index top and bottom + return et2_bounds(idxTop, idxBottom); + }, + + /** + * Returns index range of all currently managed rows. + */ + getIndexRange: function () { + var idxTop = false; + var idxBottom = false; + + for (var i = 0; i < this._map.length; i++) { - this._partitionTree.reduceRange(et2_bounds(displayBottom + reduceHeight, - this._partitionTree.getHeight())); + if (!(this._map[i] instanceof et2_dataview_spacer)) + { + var idx = this._map[i].getIndex(); + + if (idxTop === false) + { + idxTop = idx; + } + + idxBottom = idx; + } } + + return et2_bounds(idxTop, idxBottom); }, /** * Updates the scrollheight */ - setScrollHeight: function(_height) { - this._height = _height; + setScrollHeight: function (_height) { + this._scrollHeight = _height; // Update the height of the outer container if (this.scrollarea) @@ -188,39 +371,819 @@ var et2_dataview_grid = Class.extend(et2_dataview_IViewRange, { } // Update the viewing range - this.setViewRange(et2_range(this._currentRange.top, this._height)); + this.setViewRange(et2_range(this._viewRange.top, this._scrollHeight)); }, /** - * Returns the JQuery outer DOM-Node + * Returns the average row height data, overrides the corresponding function + * of the et2_dataview_container. */ - getJNode: function() { - return this.outerCell; + getAvgHeightData: function () { + + if (this._avgHeight === false) + { + var avgCount = 0; + var avgSum = 0; + + for (var i = 0; i < this._map.length; i++) + { + var data = this._map[i].getAvgHeightData(); + + if (data !== null) + { + avgSum += data.avgHeight * data.avgCount; + avgCount += data.avgCount; + } + } + + // Calculate the average height + if (avgCount > 0) + { + this._avgHeight = avgSum / avgCount; + this._avgCount = avgCount; + } + } + + // Return the calculated average height if it is available + if (this._avgHeight !== false) + { + return { + "avgCount": this._avgCount, + "avgHeight": this._avgHeight + } + } + + // Otherwise return the parent average height + if (this._parent) + { + return this._parent.getAvgHeightData(); + } + + // Otherwise return the original average height given in the constructor + if (this._orgAvgHeight !== false) + { + return { + "avgCount": 1, + "avgHeight": this._orgAvgHeight + } + } + else + { + return null; + } + }, + + /** + * Returns the average row height in pixels. + */ + getAverageHeight: function () { + var data = this.getAvgHeightData(); + return data ? data.avgHeight : 19; + }, + + /** + * Returns the row provider. + */ + getRowProvider: function () { + return this._rowProvider; + }, + + /** + * Called whenever the size of this or another element in the container tree + * changes. + */ + invalidate: function() { + // Clear the "_avgHeight" + this._avgHeight = false; + this._avgCount = false; + + // Clear any existing "invalidate" timeout + if (this._invalidateTimeout) + { + window.clearTimeout(this._invalidateTimeout); + } + + var self = this; + this._invalidateTimeout = window.setTimeout(function() { + self._invalidateTimeout = null; + self._doInvalidate(); + }, ET2_GRID_INVALIDATE_TIMEOUT); + + // Call the inherited invalidate function, broadcast the invalidation + // through the container tree. + if (this._parent !== null) + { + this._super(); + } }, /* ---- PRIVATE FUNCTIONS ---- */ - /** - * Checks whether the partition tree has to be rebuilt and if yes, does - * that. - */ - _checkTreeRebuild: function() { - if (this._treeChanged) +/* _inspectStructuralIntegrity: function() { + var idx = 0; + for (var i = 0; i < this._map.length; i++) { - var depth = this._partitionTree.getDepth(); - var count = this._partitionTree.getManagedCount(); - - // Check whether the depth of the tree is very unproportional - // regarding to the count of elements managed in it - if (count < Math.pow(ET2_PARTITION_TREE_WIDTH, depth - 1)) + if (this._map[i].getIndex() != idx) { - egw.debug("info", "Rebuilding dataview partition tree"); - this._partitionTree.rebuild(); - egw.debug("info", "Done."); + throw "Index missmatch!"; + } + idx += this._map[i].getCount(); + } + + if (idx !== this._total) + { + throw "Total count missmatch!"; + } + },*/ + + /** + * Recalculates the position of the currently managed containers. This + * routine only updates the pixel position of the elements -- the index of + * the elements is guaranteed to be maintained correctly by all high level + * functions of the grid, as the index position is needed to be correct for + * the "deleteRow" and "insertRow" functions, and we cannot effort to call + * this calculation method after every change in the grid mapping. + */ + _recalculateElementPosition: function() { + for (var i = 0; i < this._map.length; i++) + { + if (i == 0) + { + this._map[i].setTop(0); + } + else + { + this._map[i].setTop(this._map[i - 1].getBottom()); + } + } + }, + + /** + * The "_calculateVisibleMappingIndices" function calculates the indices of + * the _map array, which refer to containers that are currently (partially) + * visible. This function is used internally by "_doInvalidate". + */ + _calculateVisibleMappingIndices: function() { + // First update the "top" and "bottom", and "index" values of all + // managed elements, and at the same time calculate the mapping indices + // of the elements which are inside the current view range. + var mapVis = {"top": false, "bottom": false}; + + for (var i = 0; i < this._map.length; i++) + { + // Update the top of the "map visible index" -- set it to the first + // element index, where the bottom line is beneath the top line + // of the view range. + if (mapVis.top === false + && this._map[i].getBottom() > this._viewRange.top) + { + mapVis.top = i; } - // Reset the "treeChanged" function. - this._treeChanged = false; + // Update the bottom of the "map visible index" -- set it to the + // first element index, where the top line is beneath the bottom + // line of the view range. + if (mapVis.bottom === false + && this._map[i].getTop() > this._viewRange.bottom) + { + mapVis.bottom = i; + break; + } + } + + return mapVis; + }, + + /** + * Deletes all elements which are "out of the view range". This function is + * internally used by "_doInvalidate". How many elements that are out of the + * view range get preserved fully depends on the ET2_GRID_HOLD_COUNT + * variable. + * + * @param _mapVis contains the _map indices of the just visible containers. + */ + _cleanupOutOfRangeElements: function(_mapVis) { + + // Iterates over the map from and to the given indices and pushes all + // elements onto the given array, which are more than ET2_GRID_HOLD_COUNT + // elements remote from the start. + function searchElements(_arr, _start, _stop, _dir) + { + var dist = 0; + for (var i = _start; _dir > 0 ? i <= _stop : i >= _stop; i += _dir) + { + if (dist > ET2_GRID_HOLD_COUNT) + { + _arr.push(i); + } + else + { + dist += this._map[i].getCount(); + } + } + } + + // Collect all elements that will be deleted at the top and at the + // bottom of the grid + var deleteTop = []; + var deleteBottom = []; + + if (_mapVis.top !== false) + { + searchElements.call(this, deleteTop, _mapVis.top, 0, -1); + } + + if (_mapVis.bottom !== false) + { + searchElements.call(this, deleteBottom, _mapVis.bottom, + this._map.length - 1, 1); + } + + // The offset variable specifies how many elements have been deleted + // from the map -- this variable is needed as deleting elements from the + // map shifts the map indices. We iterate in oposite direction over the + // elements, as this allows the _doDeleteContainer/ container function + // to extend the (possibly) existing spacer at the top of the grid + var offs = 0; + for (var i = deleteTop.length - 1; i >= 0; i--) + { + // Delete the container and calculate the new offset + var mapLength = this._map.length; + this._doDeleteContainer(deleteTop[i] - offs, true); + offs += mapLength - this._map.length; + } + + for (var i = deleteBottom.length - 1; i >= 0; i--) + { + this._doDeleteContainer(deleteBottom[i] - offs, true); + } + + return deleteBottom.length + deleteTop.length > 0; + }, + + /** + * The _updateContainers function is used internally by "_doInvalidate" in + * order to call the "setViewRange" function of all containers the implement + * that interfaces (needed for nested grids), and to request new elements + * for all currently visible spacers. + */ + _updateContainers: function() { + for (var i = 0; i < this._map.length; i++) + { + var container = this._map[i]; + + // Check which type the container object has + var isSpacer = container instanceof et2_dataview_spacer; + var hasIViewRange = !isSpacer + && container.implements(et2_dataview_IViewRange); + + // If the container has one of those special types, calculate the + // view range and use that to update the view range of the element + // or to request new elements for the spacer + if (isSpacer || hasIViewRange) + { + // Calculate the relative view range and check whether + // the element is really visible + var elemRange = container.getRange(); + + // Abort if the element is not inside the visible range + if (!et2_rangeIntersect(this._viewRange, elemRange)) + { + continue; + } + + if (hasIViewRange) + { + // Update the view range of the container + container.setViewRange(et2_bounds( + this._viewRange.top - elemRange.top, + this._viewRange.bottom - elemRange.top)); + } + else // This container is a spacer + { + // Obtain the average element height + var avg = container._rowHeight; + + // Get the visible container range (vcr) + var vcr_top = Math.max(this._viewRange.top, elemRange.top); + var vcr_bot = Math.min(this._viewRange.bottom, elemRange.bottom); + + // Calculate the indices of the elements which will be + // requested + var cidx = container.getIndex(); + var ccnt = container.getCount(); + + // Calculate the start index -- prevent vtop from getting + // negative (and so idxStart being smaller than cidx) and + // ensure that idxStart is not larger than the maximum + // container index. + var vtop = Math.max(0, vcr_top); + var idxStart = Math.floor( + Math.min(cidx + ccnt - 1, + cidx + (vtop - elemRange.top) / avg)); + + // Calculate the end index -- prevent vtop from getting + // negative (and so idxEnd being smaller than cidx) and + // ensure that idxEnd is not larger than the maximum + // container index. + var vbot = Math.max(0, vcr_bot); + var idxEnd = Math.ceil( + Math.min(cidx + ccnt - 1, + cidx + (vbot - elemRange.top) / avg)); + + // Call the data callback + if (this._callback) + { + var self = this; + window.setTimeout(function() { + self._callback.call(self._context, idxStart, idxEnd); + }, 0); + } + } + } + } + }, + + /** + * Invalidate iterates over the "mapping" array. It calculates which + * containers have to be removed and where new containers should be added. + */ + _doInvalidate: function() { + // Update the pixel positions + this._recalculateElementPosition(); + + // Call the callback + if (this._invalidateCallback) + { + var range = this.getVisibleIndexRange( + et2_range(this.scrollarea.scrollTop(), this._scrollHeight)); + this._invalidateCallback.call(this._invalidateContext, range); + } + + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + var mapVis = this._calculateVisibleMappingIndices(); + + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + if (this._cleanupOutOfRangeElements(mapVis)) + { + this._recalculateElementPosition(); + } + + // Update the view range of all visible elements that implement the + // corresponding interface and request elements for all visible spacers + this._updateContainers(); + }, + + /** + * Translates the given grid index into the element index of the map. If the + * given index is completely out of the range, "false" is returned. + */ + _calculateMapIndex: function(_index) { + + var top = 0; + var bot = this._map.length - 1; + + while (top <= bot) + { + var idx = Math.floor((top + bot) / 2); + var elem = this._map[idx]; + + var realIdx = elem.getIndex(); + var realCnt = elem.getCount(); + + if (_index >= realIdx && _index < realIdx + realCnt) + { + return idx; + } + else if (_index < realIdx) + { + bot = idx - 1; + } + else + { + top = idx + 1; + } + } + + return false; + }, + + _insertContainerAtSpacer: function(_index, _mapIndex, _mapElem, _container, + _avg) { + // Set the index of the new container + _container.setIndex(_index); + + // Calculate at which position the spacer has to be splitted + var splitIdx = _index - _mapElem.getIndex(); + + // Get the count of elements that remain at the top of the splitter + var cntTop = splitIdx; + + // Get the count of elements that remain at the bottom of the splitter + // -- it has to be one element less than before + var cntBottom = _mapElem.getCount() - splitIdx - 1; + + // Split the containers if cntTop and cntBottom are larger than zero + if (cntTop > 0 && cntBottom > 0) + { + // Set the new count of the currently existing container, preserving + // its height as it was + _mapElem.setCount(cntTop); + + // Add the new element after the old container + _container.insertIntoTree(_mapElem.getLastNode()); + + // Create a new spacer and add it after the newly inserted container + var newSpacer = new et2_dataview_spacer(this, + this._rowProvider); + newSpacer.setCount(cntBottom, _avg); + newSpacer.setIndex(_index + 1); + newSpacer.insertIntoTree(_container.getLastNode()); + + // Insert the container and the new spacer into the map + this._map.splice(_mapIndex + 1, 0, _container, newSpacer); + } + else if (cntTop === 0 && cntBottom > 0) + { + // Simply adjust the size of the old spacer and insert the new + // container in front of it + _container.insertIntoTree(_mapElem.getFirstNode(), true); + _mapElem.setIndex(_index + 1); + _mapElem.setCount(cntBottom, _avg); + + this._map.splice(_mapIndex, 0, _container); + } + else if (cntTop > 0 && cntBottom === 0) + { + // Simply add the new container to the end of the old container and + // adjust the count of the old spacer to the remaining count. + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.setCount(cntTop); + + this._map.splice(_mapIndex + 1, 0, _container); + } + else // if (cntTop === 0 && cntBottom === 0) + { + // Append the new container to the current container and then + // destroy the old container + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.free(); + + this._map.splice(_mapIndex, 1, _container); + } + }, + + _insertContainerAtElement: function(_index, _mapIndex, _mapElem, _container, + _avg) { + // In a first step, simply insert the element at the specified position, + // in front of the element _mapElem. + _container.setIndex(_index); + _container.insertIntoTree(_mapElem.getFirstNode(), true); + this._map.splice(_mapIndex, 0, _container); + + // Search for the next spacer and increment the indices of all other + // elements until there + var _newIndex = _index + 1; + for (var i = _mapIndex + 1; i < this._map.length; i++) + { + // Update the index of the element + this._map[i].setIndex(_newIndex++); + + // We've found a spacer -- decrement its element count and abort + if (this._map[i] instanceof et2_dataview_spacer) + { + this._decrementSpacerCount(i, _avg); + return; + } + } + + // We've found no spacer so far, remove the last element from the map in + // order to obtain the "totalCount" (especially the last element is no + // spacer, so the following code cannot remove a spacer) + this._map.pop().free(); + }, + + /** + * Inserts the given container at the given index. + */ + _doInsertContainer: function(_index, _mapIndex, _container, _avg) { + // Check whether the given element at the map index is a spacer. If + // yes, we have to split the spacer at that position. + var mapElem = this._map[_mapIndex]; + + if (mapElem instanceof et2_dataview_spacer) + { + this._insertContainerAtSpacer(_index, _mapIndex, mapElem, + _container, _avg); + } + else + { + this._insertContainerAtElement(_index, _mapIndex, mapElem, + _container, _avg); + } + +// this._inspectStructuralIntegrity(); + }, + + /** + * Replaces the container at the given index with a spacer. The function + * tries to extend any spacer lying above or below the given _mapIndex. + * This code does not destroy the given container, but maintains its map + * index. + * + * @param _mapIndex is the index of _mapElem in the _map array. + * @param _mapElem is the container which should be replaced. + */ + _replaceContainerWithSpacer: function(_mapIndex, _mapElem) { + // Check whether a spacer can be extended above or below the given + // _mapIndex + var spacerAbove = null; + var spacerBelow = null; + + if (_mapIndex > 0 + && this._map[_mapIndex - 1] instanceof et2_dataview_spacer) + { + spacerAbove = this._map[_mapIndex - 1]; + } + + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) + { + spacerBelow = this._map[_mapIndex + 1]; + } + + if (!spacerAbove && !spacerBelow) + { + // No spacer can be extended -- simply create a new one + var spacer = new et2_dataview_spacer(this, this._rowProvider); + spacer.setIndex(_mapElem.getIndex()); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(1, _mapElem.getHeight()); + + // Insert the new spacer at the correct place into the DOM tree and + // the mapping + spacer.insertIntoTree(_mapElem.getLastNode()); + this._map.splice(_mapIndex + 1, 0, spacer); + } + else if (spacerAbove && spacerBelow) + { + // We're going to consolidate the upper and the lower spacer. To do + // that we'll calculate a new count of elements and a new average + // height, so that the upper container can get the height of all + // three elements together + var totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight() + + _mapElem.getHeight(); + var totalCount = spacerAbove.getCount() + spacerBelow.getCount() + + 1; + var newAvg = totalHeight / totalCount; + + // Update the upper spacer + spacerAbove.addAvgHeight(_mapElem.getHeight()); + spacerAbove.setCount(totalCount, newAvg); + + // Delete the lower spacer and remove it from the mapping + spacerBelow.free(); + this._map.splice(_mapIndex + 1, 1); + } + else + { + // One of the two spacers is available + var spacer = spacerAbove || spacerBelow; + + // Calculate the new count and the new average height of that spacer + var totalCount = spacer.getCount() + 1; + var totalHeight = spacer.getHeight() + _mapElem.getHeight(); + var newAvg = totalHeight / totalCount; + + // Set the new container height + spacer.setIndex(Math.min(spacer.getIndex(), _mapElem.getIndex())); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(totalCount, newAvg); + } + }, + + /** + * Checks whether there is another spacer below the given map index and if + * yes, consolidates the two. + */ + _consolidateSpacers: function(_mapIndex) { + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex] instanceof et2_dataview_spacer + && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) + { + var spacerAbove = this._map[_mapIndex]; + var spacerBelow = this._map[_mapIndex + 1]; + + // Calculate the new height/count of both containers + var totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight(); + var totalCount = spacerAbove.getCount() + spacerBelow.getCount(); + var newAvg = totalCount / totalHeight; + + // Extend the new spacer + spacerAbove.setCount(totalCount, newAvg); + + // Delete the old spacer + spacerBelow.free(); + this._map.splice(_mapIndex + 1, 1); + } + }, + + /** + * Decrements the count of the spacer at the given _mapIndex by one. If the + * given spacer has no more elements, it will be removed from the mapping. + * Note that this function does not update the indices of the following + * elements, this function is only used internally by the + * _insertContainerAtElement function and the _doDeleteContainer function + * where appropriate adjustments to the map data structure are done. + * + * @param _mapIndex is the index of the spacer in the "map" data structure. + * @param _avg is the new average height of the container, may be + * "undefined" in which case the height of the spacer rows is kept as it + * was. + */ + _decrementSpacerCount: function(_mapIndex, _avg) { + var cnt = this._map[_mapIndex].getCount() - 1; + if (cnt > 0) + { + this._map[_mapIndex].setCount(cnt, _avg); + } + else + { + this._map[_mapIndex].free(); + this._map.splice(_mapIndex, 1); + } + }, + + /** + * Deletes the container at the given index. + */ + _doDeleteContainer: function(_mapIndex, _replaceWithSpacer) { + // _replaceWithSpacer defaults to false + _replaceWithSpacer = _replaceWithSpacer ? _replaceWithSpacer : false; + + // Fetch the element at the given map index + var mapElem = this._map[_mapIndex]; + + // Indicates whether an element has really been removed -- if yes, the + // bottom spacer will be extended + var removedElement = false; + + // Check whether the map element is a spacer -- if yes, we have to do + // some special treatment + if (mapElem instanceof et2_dataview_spacer) + { + // Do nothing if the "_replaceWithSpacer" flag is true as the + // element already is a spacer + if (!_replaceWithSpacer) + { + this._decrementSpacerCount(_mapIndex); + removedElement = true; + } + } + else + { + if (_replaceWithSpacer) + { + this._replaceContainerWithSpacer(_mapIndex, mapElem); + } + else + { + removedElement = true; + } + + // Remove the complete (current) container, decrement the _mapIndex + this._map[_mapIndex].free(); + this._map.splice(_mapIndex, 1); + _mapIndex--; + + // The delete operation may have created two joining spacers -- this + // is highly suboptimal, so we'll consolidate those two spacers + this._consolidateSpacers(_mapIndex); + } + + // Update the indices of all elements after the current one, if we've + // really removed an element + if (removedElement) + { + for (var i = _mapIndex + 1; i < this._map.length; i++) + { + this._map[i].setIndex(this._map[i].getIndex() - 1); + } + + // Extend the last spacer as we have to maintain the spacer count + this._appendEmptyRows(1); + } + +// this._inspectStructuralIntegrity(); + }, + + /** + * The appendEmptyRows function is used internally to append empty rows to + * the end of the table. This functionality is needed in order to maintain + * the "total count" in the _doDeleteContainer function and to increase the + * "total count" in the "setCount" function. + * + * @param _count specifies the count of empty rows that will be added to the + * end of the table. + */ + _appendEmptyRows: function(_count) { + // Special case -- the last element in the "_map" is no spacer -- this + // means, that the "managedRange" is currently at the bottom of the list + // -- so we have to insert a new spacer + var spacer = null; + var lastIndex = this._map.length - 1; + if (this._map.length === 0 || + !(this._map[lastIndex] instanceof et2_dataview_spacer)) + { + // Create a new spacer + spacer = new et2_dataview_spacer(this, this._rowProvider); + + // Insert the spacer -- we have a special case if there currently is + // no element inside the mapping + if (this._map.length === 0) + { + // Add a dummy element to the grid + var dummy = $j(document.createElement("tr")); + this.innerTbody.append(dummy); + + // Append the spacer to the grid + spacer.setIndex(0); + spacer.insertIntoTree(dummy, false); + + // Remove the dummy element + dummy.remove(); + } + else + { + // Insert the new spacer after the last element + spacer.setIndex(this._map[lastIndex].getIndex() + 1); + spacer.insertIntoTree(this._map[lastIndex].getLastNode()); + } + + // Add the new spacer to the mapping + this._map.push(spacer); + } + else + { + // Get the spacer at the bottom of the mapping + spacer = this._map[lastIndex]; + } + + // Update the spacer count + spacer.setCount(_count + spacer.getCount(), this.getAverageHeight()); + }, + + /** + * The _decreaseTotal function is used to decrease the total row count in + * the grid. It tries to remove the given count of rows from the spacer + * located at the bottom of the grid, if this is not possible, it starts + * removing complete rows. + * + * @param _delta specifies how many rows should be removed. + */ + _decreaseTotal: function(_delta) { + // Iterate over the current mapping, starting at the bottom and delete + // rows. _delta is decreased for each removed row. Abort when delta is + // zero or the map is empty + while (_delta > 0 && this._map.length > 0) + { + var cont = this._map[this._map.length - 1]; + + // Remove as many containers as possible from spacers + if (cont instanceof et2_dataview_spacer) + { + var diff = cont.getCount() - _delta; + + if (diff > 0) + { + // We're done as the spacer still has entries left + _delta = 0; + cont.setCount(diff, this.getAverageHeight()); + break; + } + else + { + // Decrease _delta by the count of rows the spacer had + _delta -= diff + _delta; + } + } + else + { + // We're going to remove a single row: remove it + _delta -= 1; + } + + // Destroy the container if there are no rows left + cont.free(); + this._map.pop(); + } + + // Check whether _delta is really zero + if (_delta > 0) + { + this.egw.debug('error', "Error while decreasing the total count - requested to remove more rows than available."); } }, @@ -228,9 +1191,14 @@ var et2_dataview_grid = Class.extend(et2_dataview_IViewRange, { * Creates the grid DOM-Nodes */ _createNodes: function() { + + this.tr = $j(document.createElement("tr")); + this.outerCell = $j(document.createElement("td")) .addClass("frame") - .attr("colspan", this._columnIds.length + (this._parent ? 0 : 1)); + .attr("colspan", this._rowProvider.getColumnCount() + + (this._parent ? 0 : 1)) + .appendTo(this.tr); // Create the scrollarea div if this is the outer grid this.scrollarea = null; @@ -246,16 +1214,27 @@ var et2_dataview_grid = Class.extend(et2_dataview_IViewRange, { window.clearTimeout(e.data._scrollTimeout); } + // Clear any existing "invalidate" timeout (as the + // "setViewRange" function triggered by the scroll event + // forces an "invalidate"). + if (e.data._invalidateTimeout) + { + window.clearTimeout(e.data._invalidateTimeout); + e.data._invalidateTimeout = null; + } + // Set a new timeout which calls the setViewArea // function e.data._scrollTimeout = window.setTimeout(function() { - if (typeof ET2_GRID_PAUSE != "undefined") - return; - - e.data.setViewRange(et2_range( + var newRange = et2_range( e.data.scrollarea.scrollTop() - ET2_GRID_VIEW_EXT, - e.data._height + ET2_GRID_VIEW_EXT * 2 - )); + e.data._scrollHeight + ET2_GRID_VIEW_EXT * 2 + ); + + if (!et2_rangeEqual(newRange, e.data._viewRange)) + { + e.data.setViewRange(newRange); + } }, ET2_GRID_SCROLL_TIMEOUT); }) .height(this._scrollHeight) @@ -269,6 +1248,9 @@ var et2_dataview_grid = Class.extend(et2_dataview_IViewRange, { this.innerTbody = $j(document.createElement("tbody")) .appendTo(table); + + // Set the tr as container element + this.appendNode(this.tr[0]); } }); diff --git a/etemplate/js/et2_dataview_view_partitionContainerNodes.js b/etemplate/js/et2_dataview_view_partitionContainerNodes.js deleted file mode 100644 index d29d282a00..0000000000 --- a/etemplate/js/et2_dataview_view_partitionContainerNodes.js +++ /dev/null @@ -1,256 +0,0 @@ -/** - * eGroupWare eTemplate2 - Class which contains an management tree for the grid rows - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link http://www.egroupware.org - * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ - */ - -"use strict" - -/*egw:uses - et2_core_common; // for et2_range functions - et2_core_inheritance; - et2_dataview_interfaces; - et2_dataview_view_partitionNode; -*/ - -/** - * The partition container node base class implements most functionality for - * nodes which have a container. - */ -var et2_dataview_partitionContainerNode = et2_dataview_partitionNode.extend({ - - /** - * Copies the given container object and inserts the container into the tree - * - if it is not already in there. - */ - init: function(_root, _container, _args) { - this._super(_root); - - // Copy the container parameter and set its "invalidationElement" to - // this node - this.container = _container; - this.container.setInvalidationElement(this); - }, - - /** - * Inserts the container into the tree if it is not already inserted. - */ - initializeContainer: function() { - // Obtain the node index and insert the container nodes at the node - // returned by getNodeAt. If idx is zero, the returned node will be - // the outer container, so the container nodes have to be prepended - // to it. - var idx = this.getStartIndex(); - this.container.insertIntoTree(this.getRoot().getNodeAt(idx - 1), - idx == 0); - - this.invalidate(); - }, - - /** - * Destroys the container if it is still set - e.g. the spacer node - * sets the container to null before "free" ist called in some cases, in - * order to pass the container object to another spacer node. - */ - destroy: function() { - if (this.container) - { - this.container.free(); - } - - this._super(); - }, - - /** - * Returns the height of the container - */ - calculateHeight: function() { - return this.container.getHeight(); - }, - - /** - * Calls the "insertNodeAfter" function of the container to insert the node. - */ - insertNodeAfter: function(_node) { - this.container.insertNodeAfter(_node); - }, - - /** - * Returns the "lastNode" of the container - */ - getNodeAt: function(_idx) { - if (_idx >= this.getStartIndex() && _idx < this.getStopIndex()) - { - return this.container.getLastNode(); - } - - return null; - } - -}); - -/** - * Node which represents a spacer. Complete parts of the tree can be - * transformed into spacer nodes. - */ -var et2_dataview_partitionSpacerNode = et2_dataview_partitionContainerNode.extend({ - - init: function(_root, _count, _avgHeight, _container) { - - // Create the container if it has not been passed as a third parameter - var container; - if (typeof _container != "undefined") - { - container = _container; - } - else - { - container = new et2_dataview_spacer(_root.getDataProvider(), - _root.getRowProvider(), this); - } - - // Call the inherited constructor - this._super(_root, container); - - // Copy the count and average height parameters - this updates the height - // of the outer container - this.setParameters(_count, _avgHeight); - }, - - getCount: function() { - return this._count; - }, - - getAvgHeight: function() { - return this._avgHeight; - }, - - setParameters: function(_count, _avgHeight) { - if (_count != this._count || _avgHeight != this._avgHeight) - { - - // Copy the given parameters - this._count = _count; - this._avgHeight = _avgHeight; - - // Invalidate this element - this.invalidate(); - - // Call the container function to set the total height - this.container.setHeight(this._count * this._avgHeight); - } - }, - - /** - * Creates the nodes which fall in the given range and returns them - */ - getRangeNodes: function(_range, _create) { - - // If no new nodes should be created, simply return this node - if (!_create) - { - return this._super(_range); - } - - var insertNodes = []; - - // Copy parent and pidx as we'll have to access those objects after this - // one gets freed - var parent = this._parent; - var pidx = this._pidx; - - // Get the top and bottom of this node - var t = this.getPosTop(); - var b = this.getPosBottom(); - - // Get the start and stop index of the elements which have to be - // created. - var ah = this._avgHeight; - var startIdx = Math.max(0, Math.floor((_range.top - t) / ah)); - var stopIdx = Math.min(this._count, Math.ceil((_range.bottom - t) / ah)); - - if (startIdx > 0 && startIdx < this._count) - { - // Create a spacer which contains the elements until startIdx - insertNodes.push(new et2_dataview_partitionSpacerNode(this.getRoot(), - startIdx, ah, this.container)); - this.container = null; - } - - // Calculate the current average height - ah = this.getRoot().getAverageHeight(); - - // Create the elements from start to stop index - for (var i = startIdx; i < stopIdx; i++) - { - var rowNode = new et2_dataview_partitionRowNode(this.getRoot(), ah); - insertNodes.push(rowNode); - } - - if (stopIdx < this._count - 1 && stopIdx > 0) - { - // Create a spacer which contains the elements starting from - // stop index - var l = this._count - stopIdx; - insertNodes.push(new et2_dataview_partitionSpacerNode(this.getRoot(), - l, ah)); - } - - // Check whether insertNodes really has entrys - this is not the case - // if the given range is just outside the range of this element - if (insertNodes.length > 0) - { - // Free this element - this.free(); - - // Insert the newly created nodes at the original place of this node - parent.insertNodes(pidx, insertNodes); - - // Insert the newly created elements into the DOM-Tree - for (var i = 0; i < insertNodes.length; i++) - { - insertNodes[i].initializeContainer(); - } - - return false; - } - - return []; - }, - - getAvgHeightData: function(_data) { - // Do nothing here, as the spacers should not be inside the average - // height statistic. - } - -}); - -var et2_dataview_partitionRowNode = et2_dataview_partitionContainerNode.extend({ - - init: function(_root, _avgHeight) { - - var container = new et2_dataview_row(_root.getDataProvider(), - _root.getRowProvider(), this, _avgHeight); - - this._super(_root, container); - }, - - initializeContainer: function() { - this._super(); - - this.container.setIdx(this.getStartIndex()); - }, - - getIdxNode: function(_node, _create) { - return this.node; - } - -}); - - diff --git a/etemplate/js/et2_dataview_view_partitionNode.js b/etemplate/js/et2_dataview_view_partitionNode.js deleted file mode 100644 index a1cfd572e3..0000000000 --- a/etemplate/js/et2_dataview_view_partitionNode.js +++ /dev/null @@ -1,323 +0,0 @@ -/** - * eGroupWare eTemplate2 - Class which contains an management tree for the grid rows - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link http://www.egroupware.org - * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ - */ - -"use strict" - -/*egw:uses - et2_core_common; // for et2_range functions - et2_core_inheritance; - et2_dataview_interfaces; -*/ - -/** - * The partition node tree manages all rows in a dataview. As a dataview may have - * many thousands of lines, the rows are organized in an a tree. The leafs of the - * tree represent the single rows, upper layers represent groups of nodes. - * Each node has a "height" value and is capable of calculate the exact position - * of a row and its top and bottom value. - * Additionaly, a leaf can represent an unlimited number of rows. In this way - * the partition tree is built dynamically and is also capable of "forgetting" - * information about the rows by simply reducing the tree nodes at a certain - * position. - */ - -var et2_dataview_IPartitionHeight = new Interface({ - - calculateHeight: function() {} - -}); - -/** - * Abstract base class for partition nodes - contains the code for calculating - * the top, bottom, height and (start) index of the node - */ -var et2_dataview_partitionNode = Class.extend([et2_dataview_IPartitionHeight, - et2_dataview_IInvalidatable], { - - init: function(_root) { - - this._root = _root; - this._parent = null; - this._pidx = 0; - - // Initialize the temporary storage elements - this.doInvalidate(); - this._invalid = true; - - }, - - destroy: function() { - - // Remove this element from the parent children list - if (this._parent) - { - this._parent.removePIdxNode(this._pidx); - } - - }, - - setParent: function(_parent) { - if (this._parent != _parent) - { - this._parent = _parent; - this.invalidate(); - } - }, - - setPIdx: function(_pidx) { - if (this._pidx != _pidx) - { - this._pidx = _pidx; - this.invalidate(); - } - }, - - /** - * Invalidates cached values - override the "doInvalidate" function. - * - * @param _sender is the node wich originally triggerd the invalidation, can - * be ommited when calling this function. - */ - invalidate: function(_sender) { - - // If the _sender parameter is not given, assume that this element is - // the one which triggered the invalidation - var origin = typeof _sender == "undefined"; - - if ((origin || _sender != this) && !this._invalid) - { - this.doInvalidate(); - this._invalid = true; - - // Invalidate the parent node - if (this._parent) - { - // Invalidate the neighbor node - if (this._pidx < this._parent._children.length - 1) - { - this._parent._children[this._pidx + 1].invalidate(); - } - - this._parent.invalidate(origin ? this : _sender); - } - } - }, - - /** - * Performs the actual invalidation. - */ - doInvalidate: function() { - this._height = false; - this._posTop = false; - this._posBottom = false; - this._startIdx = false; - this._stopIdx = false; - }, - - /** - * Returns the root node of the partition tree - */ - getRoot: function() { - return this._root; - }, - - /** - * Returns the height of this node - */ - getHeight: function() { - // Calculate the height value if it is currently invalid - if (this._height === false) - { - this._height = this.calculateHeight(); - - // Do a sanity check for the value - if the height wasn't a number - // it could easily destroy the posTop and posBottom values of the - // complete tree! - if (isNaN(this._height)) - { - this.egw().debug("error", "calculateHeight returned a NaN value!"); - this._height = 0; - } - - this._invalid = false; - } - - return this._height; - }, - - /** - * Returns the top position of the node in px - */ - getPosTop: function() { - if (this._posTop === false) - { - this._posTop = this._accumulateValue(this.getPosTop, this.getPosBottom); - - this._invalid = false; - } - - return this._posTop; - }, - - /** - * Returns the bottom position of the node in px - */ - getPosBottom: function() { - if (this._posBottom === false) - { - this._posBottom = this.getPosTop() + this.getHeight(); - - this._invalid = false; - } - - return this._posBottom; - }, - - /** - * Returns an range object - */ - getRange: function() { - return { - "top": this.getPosTop(), - "bottom": this.getPosBottom() - }; - }, - - - /** - * Returns true if the node intersects with the given range - */ - inRange: function(_ar) { - return et2_rangeIntersect(this.getRange(), _ar); - }, - - /** - * Returns the overall start index of the node - */ - getStartIndex: function() { - if (this._startIdx === false) - { - this._startIdx = this._accumulateValue(this.getStartIndex, - this.getStopIndex); - - this._invalid = false; - } - - return this._startIdx; - }, - - /** - * Returns the overall stop index of the node - */ - getStopIndex: function() { - if (this._stopIdx === false) - { - this._stopIdx = this.getStartIndex() + this.getCount(); - - this._invalid = false; - } - - return this._stopIdx; - }, - - /** - * Returns the index range object - */ - getIdxRange: function() { - return { - "top": this.getStartIndex(), - "bottom": this.getStopIndex() - }; - }, - - /** - * Checks whether this element is inside the given index range - */ - inIdxRange: function(_idxRange) { - return et2_rangeIntersect(this.getIdxRange, _idxRange); - }, - - /** - * Returns the count of leafs which are below this node - */ - getCount: function() { - return 1; - }, - - /** - * Returns the nodes which reside in the given range - */ - getRangeNodes: function(_range, _create) { - if (this.inRange(_range)) - { - return [this]; - } - - return []; - }, - - /** - * Returns the nodes which are inside the given index range - */ - getIdxRangeNodes: function(_idxRange, _create) { - if (this.inIdxRange(_idxRange)) - { - return [this]; - } - - return []; - }, - - /** - * Returns the (maximum) depth of the tree - */ - getDepth: function() { - return 1; - }, - - getAvgHeightData: function(_data) { - _data.count++; - _data.height += this.getHeight(); - }, - - getNodeIdx: function(_idx, _create) { - return null; - }, - - getRowProvider: function() { - return this.getRoot().getRowProvider(); - }, - - getDataProvider: function() { - return this.getRoot().getDataProvider(); - }, - - /* ---- PRIVATE FUNCTIONS ---- */ - - _accumulateValue: function(_f1, _f2) { - if (this._parent) - { - if (this._pidx == 0) - { - return _f1.call(this._parent); - } - else - { - return _f2.call(this._parent._children[this._pidx - 1]); - } - } - - return 0; - } - -}); - diff --git a/etemplate/js/et2_dataview_view_partitionOrganizationNode.js b/etemplate/js/et2_dataview_view_partitionOrganizationNode.js deleted file mode 100644 index 92dede388f..0000000000 --- a/etemplate/js/et2_dataview_view_partitionOrganizationNode.js +++ /dev/null @@ -1,643 +0,0 @@ -/** - * eGroupWare eTemplate2 - Class which implements the organization node - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link http://www.egroupware.org - * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ - */ - -"use strict" - -/*egw:uses - et2_core_common; // for et2_range functions - et2_core_inheritance; - et2_dataview_interfaces; - et2_dataview_view_partitionNode; -*/ - -/** - * The ET2_PARTITION_TREE_WIDTH defines the count of children a node will be - * created with. - */ -var ET2_PARTITION_TREE_WIDTH = 10; - -/** - * An partition tree organization node can contain child nodes and organizes - * those. - */ -var et2_dataview_partitionOrganizationNode = et2_dataview_partitionNode.extend( - /*et2_dataview_IIndexOperations, */{ - - init: function(_root, _parent, _pidx) { - - if (typeof _parent == "undefined") - { - _parent = null; - } - - if (typeof _pidx == "undefined") - { - _pidx = 0; - } - - // Call the parent constructor - this._super(_root); - - this._children = []; - - // Set the given parent and parent-index - this.setParent(_parent); - this.setPIdx(_pidx); - - }, - - destroy: function() { - // Free all child nodes - for (var i = this._children.length - 1; i >= 0; i--) - { - this._children[i].free(); - } - - this._super(); - }, - - /** - * Delete the buffered element count - */ - doInvalidate: function() { - this._super(); - - this._count = false; - this._depth = false; - this._avgHeightData = false; - }, - - /** - * Calculates the count of elements by accumulating the counts of the child - * elements. - */ - getCount: function() { - if (this._count === false) - { - // Calculate the count of nodes - this._count = 0; - for (var i = 0; i < this._children.length; i++) - { - this._count += this._children[i].getCount(); - } - } - - return this._count; - }, - - /** - * Calculates the height of this node by accumulating the height of the - * child nodes. - */ - calculateHeight: function() { - var result = 0; - for (var i = 0; i < this._children.length; i++) - { - result += this._children[i].getHeight(); - } - - return result; - }, - - /** - * Removes the given node from the tree - */ - removeNode: function(_node) { - // Search the element on this level - for (var i = 0; i < this._children.length; i++) - { - if (this._children[i] == _node) - { - this.removePIdxNode(i); - return true; - } - } - - // Search the element on a lower level - for (var i = 0; i < this._children.length; i++) - { - if (this._children[i] instanceof et2_dataview_partitionOrganizationNode && - this._children[i].removeNode(_node)) - { - return true; - } - } - - return false; - }, - - /** - * Removes the child with the given index in the _children list - */ - removePIdxNode: function(_pidx) { - // Invalidate this element - this.invalidate(); - - // Delete the element at the given pidx and remove the parent reference - this._children.splice(_pidx, 1)[0].setParent(null); - - // Recalculate the pidx of the children behind the one removed - for (var i = _pidx; i < this._children.length; i++) - { - this._children[i]._pidx--; - } - - return true; - }, - - /** - * Removes the child with the given overall index - */ - removeIdxNode: function(_idx) { - return this._iterateToIndex(_idx, function(ei, bi, child) { - if (child.implements(et2_dataview_IIndexOperations)) - { - return child.removeIdxNode(_idx); - } - - return this.removePIdxNode(i); - }, false); - }, - - /** - * Returns the node with the given overall index and null if it is not found - */ - getIdxNode: function(_idx) { - return this._iterateToIndex(_idx, function(ei, bi, child) { - if (child.implements(et2_dataview_IIndexOperations)) - { - return child.getIdxNode() - } - - if (idx == bi) - { - return child; - } - }, null); - }, - - /** - * getNodeAt returns the DOM node at the given index - */ - getNodeAt: function(_idx) { - return this._iterateToIndex(_idx, function(ei, bi, child) { - return child.getNodeAt(_idx); - }, null); - }, - - /** - * Returns all nodes in the given range - */ - getRangeNodes: function(_range, _create) { - - if (typeof _create == "undefined") - { - _create = true; - } - - var result = []; - - // Create a copy of the children of this element, as the child list may - // change due to new children being inserted. - var children = this._copyChildren(); - - // We did not have a intersect in the range now - var hadIntersect = false; - for (var i = 0; i < children.length; i++) - { - if (children[i].inRange(_range)) - { - hadIntersect = true; - - var res = children[i].getRangeNodes(_range, _create); - - if (res === false) - { - return this.getRangeNodes(_range, _create); - } - - // Append the search results of the given element - result = result.concat(res); - } - else - { - // Abort as we are out of the range where intersects can happen - if (hadIntersect) - { - break; - } - } - } - - return result; - }, - - /** - * Returns the nodes which are inside the given range - */ - getIdxRangeNodes: function(_idxRange, _create) { - - if (typeof _create == "undefined") - { - _create = true; - } - - var result = []; - - // Create a copy of the children of this element, as the child list may - // change due to new children being inserted. - var children = this._copyChildren(); - - // We did not have a intersect in the range now - var hadIntersect = false; - for (var i = 0; i < children.length; i++) - { - if (children[i].inIdxRange(_idxRange)) - { - hadIntersect = true; - - // Append the search results of the given element - var res = children[i].getIdxRangeNodes(_idxRange, - _create); - - if (res === false) - { - return this.getIdxRangeNodes(_idxRange, _create); - } - - result = result.concat(res); - } - else - { - // Abort as we are out of the range where intersects can happen - if (hadIntersect) - { - break; - } - } - } - - return result; - }, - - /** - * Reduces the given range to a spacer - */ - reduceRange: function(_range) { - this._reduce(this.getRangeNodes(_range, false)) - }, - - /** - * Reduces the given index range to a spacer - */ - reduceIdxRange: function(_range) { - this._reduce(this.getIdxRangeNodes(_range, false)); - }, - - getDepth: function() { - if (this._depth === false) - { - this._depth = 0; - - // Get the maximum depth and increase it by one - for (var i = 0; i < this._children.length; i++) - { - this._depth = Math.max(this._depth, this._children[i].getDepth()); - } - this._depth++; - } - - return this._depth; - }, - - _insertLeft: function(_idx, _nodes) { - // Check whether the node left to the given index can still take some - // nodes - if yes, insert the maximum amount of nodes into this node - if (_idx > 0 && this._children[_idx - 1] instanceof et2_dataview_partitionOrganizationNode - && this._children[_idx - 1]._children.length < ET2_PARTITION_TREE_WIDTH) - { - // Calculate how many children can be inserted into the left node - var child = this._children[_idx - 1]; - var c = Math.min(ET2_PARTITION_TREE_WIDTH - child._children.length, _nodes.length); - - // Insert the remaining children into the left node - if (c > 0) - { - var nodes = _nodes.splice(0, c); - child.insertNodes(child._children.length, nodes); - } - } - }, - - _insertRight: function(_idx, _nodes) { - // Check whether the node right to the given index can still take some - // nodes - if yes, insert the nodes there - if (_idx < this._children.length && - this._children[_idx] instanceof et2_dataview_partitionOrganizationNode && - this._children[_idx]._children.length < ET2_PARTITION_TREE_WIDTH) - { - var child = this._children[_idx]; - var c = Math.min(ET2_PARTITION_TREE_WIDTH - child._children.length, _nodes.length); - - // Insert the remaining children into the left node - if (c > 0) - { - var nodes = _nodes.splice(_nodes.length - c, c); - child.insertNodes(0, nodes); - } - } - }, - - /** - * Groups the nodes which should be inserted by packages of ten and insert - * those as children. If there are more than ET2_PARTITION_TREE_WIDTH - * children as a result of this action, this node gets destroyed and the - * children are given to the parent node. - */ - insertNodes: function(_idx, _nodes) { - // Break if no nodes are to be inserted - if (_nodes.length == 0) - { - return; - } - - // Invalidate this node - this.invalidate(); - - // Try to insert the given objects into an organization node at the left - // or right side of the given index - this._insertLeft(_idx, _nodes); - this._insertRight(_idx, _nodes); - - // Update the pidx of the nodes after _idx - for (var i = _idx; i < this._children.length; i++) - { - this._children[i].setPIdx(i + _nodes.length); - } - - // Set the parent and the pidx of the new nodes - for (var i = 0; i < _nodes.length; i++) - { - _nodes[i].setParent(this); - _nodes[i].setPIdx(_idx + i); - } - - // Simply insert the nodes at the given position - this._children.splice.apply(this._children, [_idx, 0].concat(_nodes)); - - // Check whether the width of this element is greater than ET2_PARTITION_TREE_WIDTH - // If yes, split the children into groups of ET2_PARTITION_TREE_WIDTH and - // insert those into this node - /*if (this._children.length > ET2_PARTITION_TREE_WIDTH) - { - var insertNodes = []; - - while (_nodes.length > 0) - { - var orgaNode = new et2_dataview_partitionOrganizationNode(this, - insertNodes.length); - - // Get groups of ET2_PARTITION_TREE_WIDTH from the nodes while - // reading the first level of nodes from organization nodes - var newNodes = []; - var isPartial = false; - while (newNodes.length < ET2_PARTITION_TREE_WIDTH && _nodes.length > 0) - { - var node = _nodes[0]; - - if (!(node instanceof et2_dataview_partitionOrganizationNode)) - { - newNodes.push(_nodes.shift()); - isPartial = true; - } - else - { - if (node._children.length == 0) - { - // Remove the node from the list and free it - _nodes.shift().free(); - } - else - { - if (!isPartial && node._children.length == ET2_PARTITION_TREE_WIDTH) - { - newNodes.push(_nodes.shift()); - } - else - { - newNodes = newNodes.concat(node._children.splice(0, - ET2_PARTITION_TREE_WIDTH - newNodes.length)); - isPartial = true; - } - } - } - } - - orgaNode.insertNodes(0, newNodes); - - insertNodes.push(orgaNode); - } - - this._children = []; - this.insertNodes(0, insertNodes); - }*/ - }, - - rebuild: function() { - // Get all leafs - var children = []; - this._getFlatList(children); - - // Free all organization nodes - this._clear(); - - this.insertNodes(0, children); - }, - - /** - * Accumulates the "average height" data - */ - getAvgHeightData: function(_data) { - if (this._avgHeightData == false) - { - this._avgHeightData = {"count": 0, "height": 0}; - - for (var i = 0; i < this._children.length; i++) - { - this._children[i].getAvgHeightData(this._avgHeightData); - } - - this._invalid = false; - } - - // Increment the data object entries by the buffered avg height data - _data.count += this._avgHeightData.count; - _data.height += this._avgHeightData.height; - }, - - debug: function() { - var children = []; - var offs = false; - this._getFlatList(children); - - for (var i = 0; i < children.length; i++) - { - var idx = children[i].getStartIndex(); - var node = children[i].getNodeAt(idx); - - if (node) - { - if (offs === false) - { - offs = node.offset().top; - } - - var actualTop = node.offset().top - offs; - var calculatedTop = children[i].getPosTop(); - - if (Math.abs(actualTop - calculatedTop) > 1) - { - egw.debug("warn", i, "Position missmatch at idx ", idx, - actualTop, calculatedTop, node); - } - - var actualHeight = node.outerHeight(true); - var calculateHeight = children[i].getHeight(); - - if (Math.abs(actualHeight - calculateHeight) > 1) - { - egw.debug("warn", i, "Height missmatch at idx ", idx, - actualHeight, calculateHeight, node); - } - } - } - }, - - /* ---- PRIVATE FUNCTIONS ---- */ - - _copyChildren: function() { - // Copy the child array as querying the child nodes may change the tree - var children = new Array(this._children.length); - for (var i = 0; i < this._children.length; i++) - { - children[i] = this._children[i]; - } - - return children; - }, - - _iterateToIndex: function(_idx, _func, _res) { - for (var i = 0; i < this._children.length; i++) - { - var child = this._children[i]; - - var bi = child.getStartIndex(); - var ei = child.getStopIndex(); - - if (bi > _idx) - { - return res; - } - - if (bi <= _idx && ei > _idx) - { - return _func.call(this, bi, ei, child); - } - } - - return res; - }, - - /** - * Reduces the given nodes to a single spacer - */ - _reduce: function(_nodes) { - if (_nodes.length == 0) - { - return; - } - - // Check whether the first or last node is a spacer, if not create - // a new one - var ph; - if (_nodes[0] instanceof et2_dataview_partitionSpacerNode) - { - ph = _nodes[0] - } - else if (_nodes[_nodes.length - 1] instanceof et2_dataview_partitionSpacerNode) - { - ph = _nodes[_nodes.length - 1]; - } - else - { - // Create a new spacer node and insert it at the place of the - // first node of the range - ph = new et2_dataview_partitionSpacerNode(this.getRoot(), 0, 0); - _nodes[0]._parent.insertNodes(_nodes[0]._pidx, [ph]); - - // Initialize the new placeholder - ph.initializeContainer(); - } - - // Get the height of the resulting spacer - var height = _nodes[_nodes.length - 1].getPosBottom() - _nodes[0].getPosTop(); - - // Get the count of actual elements in the nodes - var count = 0; - for (var i = 0; i < _nodes.length; i++) - { - count += _nodes[i].getCount(); - } - - // Update the spacer parameters - ph.setParameters(count, height / count); - - // Free all elements (except for the spacer) - for (var i = _nodes.length - 1; i >= 0; i--) - { - if (_nodes[i] != ph) - { - _nodes[i].free(); - } - } - }, - - /** - * Used when rebuilding the tree - */ - _getFlatList: function(_res) { - for (var i = 0; i < this._children.length; i++) - { - if (this._children[i] instanceof et2_dataview_partitionOrganizationNode) - { - this._children[i]._getFlatList(_res); - } - else - { - _res.push(this._children[i]); - } - } - }, - - _clear: function() { - for (var i = this._children.length - 1; i >= 0; i--) - { - if (this._children[i] instanceof et2_dataview_partitionOrganizationNode) - { - this._children[i].free(); - } - } - - this._children = []; - } -}); - diff --git a/etemplate/js/et2_dataview_view_partitionTree.js b/etemplate/js/et2_dataview_view_partitionTree.js deleted file mode 100644 index 4cafab8b01..0000000000 --- a/etemplate/js/et2_dataview_view_partitionTree.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * eGroupWare eTemplate2 - Class which contains an management tree for the grid rows - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link http://www.egroupware.org - * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ - */ - -"use strict" - -/*egw:uses - et2_core_common; // for et2_range functions - et2_core_inheritance; - - et2_dataview_interfaces; - - et2_dataview_view_partitionOrganizationNode; - et2_dataview_view_partitionContainerNodes; -*/ - -/** - * Main class for the usage of the partition tree - */ -var et2_dataview_partitionTree = et2_dataview_partitionOrganizationNode.extend({ - - init: function(_dataProvider, _rowProvider, _avgHeight, _node) { - this._super(this); - - this._avgHeight = _avgHeight; - this._node = _node; - this._count = _dataProvider.getCount(); - - this._dataProvider = _dataProvider; - this._rowProvider = _rowProvider; - - egw.debug("Creating partition tree with ", this._count, - " elements of avgHeight ", this._avgHeight); - - // Append a spacer node to the children - var ph = new et2_dataview_partitionSpacerNode(this, this._count, - this._avgHeight); - ph.setParent(this); - ph.initializeContainer(); - - this._children = [ph]; - }, - - getAverageHeight: function() { - var data = {"count": 0, "height": 0}; - - this.getAvgHeightData(data); - - if (data.count == 0) - { - return this._avgHeight; - } - - return data.height / data.count; - }, - - /** - * Returns the actual count of managed objects inside of the tree - getCount - * in contrast returns the count of "virtual" objects including the - * spacers. - */ - getManagedCount: function() { - var data = {"count": 0, "height": 0}; - this.getAvgHeightData(data); - - return data.count; - }, - - /** - * Returns the node after which new nodes have to be inserted for the given - * index. - */ - getNodeAt: function(_idx) { - - // Insert the given node to the top of the parent container - if (_idx < 0) - { - return this._node; - } - - // Otherwise search for the tree node with that index - return this._super(_idx); - }, - - getRowProvider: function() { - return this._rowProvider; - }, - - getDataProvider: function() { - return this._dataProvider; - } - -}); - - diff --git a/etemplate/js/et2_dataview_view_row.js b/etemplate/js/et2_dataview_view_row.js index c993395653..ed0f6838aa 100644 --- a/etemplate/js/et2_dataview_view_row.js +++ b/etemplate/js/et2_dataview_view_row.js @@ -1,12 +1,12 @@ /** - * eGroupWare eTemplate2 - Class which contains a factory method for rows + * eGroupWare eTemplate2 - dataview * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage dataview * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 + * @copyright Stylite 2011-2012 * @version $Id$ */ @@ -15,109 +15,36 @@ /*egw:uses egw_action.egw_action; - et2_dataview_interfaces; et2_dataview_view_container; - et2_dataview_view_rowAOI; */ -var et2_dataview_row = et2_dataview_container.extend(et2_dataview_IDataRow, { +var et2_dataview_row = et2_dataview_container.extend({ - init: function(_dataProvider, _rowProvider, _invalidationElem, _avgHeight) { + /** + * Creates the row container. Use the "setRow" function to load the actual + * row content. + * + * @param _parent is the row parent container. + */ + init: function(_parent) { + // Call the inherited constructor + this._super(_parent); - this._super(_dataProvider, _rowProvider, _invalidationElem); - - this._avgHeight = _avgHeight; - this._idx = null; - - this.rowWidget = null; - this.actionObject = null; - this.hasAvgHeight = false; - - // Get the default row object and scale the row to the average height - this.tr = this.rowProvider.getPrototype("dataRow"); - - // Append the row + // Create the outer "tr" tag and append it to the container + this.tr = $j(document.createElement("tr")); this.appendNode(this.tr); }, - destroy: function() { - - // Unregister the row from the data provider - if (this._idx !== null) - { - this.dataProvider.unregisterDataRow(this._idx); - } - - // Free the row widget first, if it has been set - if (this.rowWidget) - { - this.rowWidget.free(); - } - - // Free the action object and remove it from the action object manager - if (this.actionObject) - { - // Delete the action object from the surrounding manager - this.actionObject.remove(); - } - - this._super(); + clear: function() { + this.tr.empty(); }, - setIdx: function(_idx) { - this._idx = _idx; - - // Register the row in the data provider - this.dataProvider.registerDataRow(this, _idx); - - // Set the default height of the rowWidget has not been immediately - // created - if (!this.rowWidget) - { - $j("td:first", this.tr).height(this._avgHeight); - this.hasAvgHeight = true; - } + getDOMNode: function() { + return this.tr[0]; }, - updateData: function(_data) { - // Free all old data in the action object - if (this.actionObject) - { - this.actionObject.clear(); - } - - // Create an action object interface for the row - var aom = this.dataProvider.getActionObjectManager(); - if (aom) - { - // this is rather hackisch, but I have no idea how to get action_links & row_id here otherwise - this.actionObject = aom.addObject(_data.content[aom.manager.row_id], - new et2_dataview_rowAOI(this.tr[0])); - - // TODO: The action links should be inside the data and not inside - // the row classes... - this.actionObject.updateActionLinks(aom.manager.action_links); - } - - // Reset the height - if (this.hasAvgHeight) - { - $j("td:first", this.tr).height("auto"); - this.hasAvgHeight = false; - } - - // Free the row widget if it already existed - if (this.rowWidget != null) - { - this.rowWidget.free(); - } - - // Create the row widget - it automatically generates the widgets and - // attaches the given data to them - this.rowWidget = this.rowProvider.getDataRow(_data, this.tr, this._idx); - - // Invalidate this element - this.invalidate(); + getJNode: function() { + return this.tr; } }); diff --git a/etemplate/js/et2_dataview_view_rowAOI.js b/etemplate/js/et2_dataview_view_rowAOI.js deleted file mode 100644 index 7850888104..0000000000 --- a/etemplate/js/et2_dataview_view_rowAOI.js +++ /dev/null @@ -1,348 +0,0 @@ -/** - * EGroupware eTemplate nextmatch row action object interface - * - * @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 (as AT stylite.de) - * @author Ralf Becker - * @version $Id$ - */ - -"use strict"; - -/*egw:uses - egw_action.egw_action_common; - egw_action.egw_action; -*/ - -/** - * Contains the action object interface implementation for the nextmatch widget - * row. - */ -/** - * An action object interface for each nextmatch widget row - "inherits" from - * egwActionObjectInterface - */ -function et2_dataview_rowAOI(_node) -{ - var aoi = new egwActionObjectInterface(); - - aoi.node = _node; - - aoi.checkBox = null; //($j(":checkbox", aoi.node))[0]; - - // Rows without a checkbox OR an id set are unselectable - aoi.doGetDOMNode = function() { - return aoi.node; - } - - // Prevent the browser from selecting the content of the element, when - // a special key is pressed. - $j(_node).mousedown(egwPreventSelect); - - // Now append some action code to the node - var selectHandler = function(e) { - - // Reset the focus so that keyboard navigation will work properly - // after the element has been clicked - egwUnfocus(); - - // Reset the prevent selection code (in order to allow wanted - // selection of text) - _node.onselectstart = null; - - if (e.target != aoi.checkBox) - { - var selected = egwBitIsSet(aoi.getState(), EGW_AO_STATE_SELECTED); - var state = EGW_AO_SHIFT_STATE_NONE; // Multiple row selction does not work right now - - aoi.updateState(EGW_AO_STATE_SELECTED, - !egwBitIsSet(state, EGW_AO_SHIFT_STATE_MULTI) || !selected, - state); - } - }; - - if (egwIsMobile()) { - _node.ontouchend = selectHandler; - } else { - $j(_node).click(selectHandler); - } - - $j(aoi.checkBox).change(function() { - aoi.updateState(EGW_AO_STATE_SELECTED, this.checked, EGW_AO_SHIFT_STATE_MULTI); - }); - - aoi.doSetState = function(_state) { - var selected = egwBitIsSet(_state, EGW_AO_STATE_SELECTED); - - if (this.checkBox) - { - this.checkBox.checked = selected; - } - - $j(this.node).toggleClass('focused', - egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); - $j(this.node).toggleClass('selected', - selected); - } - - return aoi; -} - -/** - * Default action for nextmatch rows, runs action specified _action.data.nm_action: see nextmatch_widget::egw_actions() - * - * @param _action action object with attributes caption, id, nm_action, ... - * @param _senders array of rows selected - */ -function nm_action(_action, _senders) -{ - // 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'; - - var ids = ""; - for (var i = 0; i < _senders.length; i++) - { - ids += (_senders[i].id.indexOf(',') >= 0 ? '"'+_senders[i].id.replace(/"/g,'""')+'"' : _senders[i].id) + - ((i < _senders.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) && - typeof _action.data.confirm_multiple != 'undefined' ? - _action.data.confirm_multiple : _action.data.confirm; - - // let user confirm the action first (if not select_all set and nm_action == 'submit' --> confirmed later) - if (!(select_all && select_all.checked && _action.data.nm_action == 'submit') && - typeof _action.data.confirm != 'undefined') - { - 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)) - { - if (!confirm(_action.data.confirm_multiple)) return; - } - - var url = '#'; - if (typeof _action.data.url != 'undefined') - { - url = _action.data.url.replace(/(\$|%24)id/,encodeURIComponent(ids)); - } - - var target = null; - if (typeof _action.data.target != 'undefined') - { - target = _action.data.target; - } - - switch(_action.data.nm_action) - { - case 'alert': - alert(_action.caption + " (\'" + _action.id + "\') executed on rows: " + ids); - break; - - case 'location': - if(target) - { - window.open(url, target); - } - else - { - window.location.href = url; - } - break; - - case 'popup': - egw_openWindowCentered2(url,target,_action.data.width,_action.data.height); - break; - - 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; - 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; - - case 'open_popup': - // open div styled as popup contained in current form and named action.id+'_popup' - if (nm_popup_action == null) - { - nm_open_popup(_action, _senders); - break; - } - // fall through, if popup is open --> submit form - case 'submit': - // let user confirm select-all - if (select_all && select_all.checked) - { - if (!confirm((confirm_msg ? confirm_msg : _action.caption.replace(/^( | | )+/,''))+"\n\n"+select_all.hint)) return; - } - var checkboxes = mgr.getActionsByAttr("checkbox", true); - var checkboxes_elem = document.getElementById(mgr.etemplate_var_prefix+'[nm][checkboxes]'); - if (checkboxes && checkboxes_elem) - for (var i in checkboxes) - checkboxes_elem.value += checkboxes[i].id + ":" + (checkboxes[i].checked ? "1" : "0") + ";"; - - document.getElementById(mgr.etemplate_var_prefix+'[nm][nm_action]').value = _action.id; - document.getElementById(mgr.etemplate_var_prefix+'[nm][selected]').value = ids; - if (typeof _action.data.button != 'undefined') - { - submitit(mgr.etemplate_form.context, mgr.etemplate_var_prefix+'[nm][rows]['+_action.data.button+']['+ids+']'); - } - else - { - mgr.etemplate_form.submit(); - } - // Clear action in case there's another one - document.getElementById(mgr.etemplate_var_prefix+'[nm][nm_action]').value = null; - - break; - } -} - -/** - * Callback to check if none of _senders rows has disableClass set - * - * @param _action egwAction object, we use _action.data.disableClass to check - * @param _senders array of egwActionObject objects - * @param _target egwActionObject object, get's called for every object in _senders - * @returns boolean true if none has disableClass, false otherwise - */ -function nm_not_disableClass(_action, _senders, _target) -{ - return !$j(_target.iface.getDOMNode()).hasClass(_action.data.disableClass); -} - -/** - * Callback to check if all of _senders rows have enableClass set - * - * @param _action egwAction object, we use _action.data.enableClass to check - * @param _senders array of egwActionObject objects - * @param _target egwActionObject object, get's called for every object in _senders - * @returns boolean true if none has disableClass, false otherwise - */ -function nm_enableClass(_action, _senders, _target) -{ - return $j(_target.iface.getDOMNode()).hasClass(_action.data.enableClass); -} - -/** - * Enable an _action, if it matches a given regular expresstion in _action.data.enableId - * - * @param _action egwAction object, we use _action.data.enableId to check - * @param _senders array of egwActionObject objects - * @param _target egwActionObject object, get's called for every object in _senders - * @returns boolean true if _target.id matches _action.data.enableId - */ -function nm_enableId(_action, _senders, _target) -{ - if (typeof _action.data.enableId == 'string') - _action.data.enableId = new RegExp(_action.data.enableId); - - return _target.id.match(_action.data.enableId); -} - -/** - * Callback to check if a certain field (_action.data.fieldId) is (not) equal to given value (_action.data.fieldValue) - * - * If field is not found, we return false too! - * - * @param _action egwAction object, we use _action.data.fieldId to check agains _action.data.fieldValue - * @param _senders array of egwActionObject objects - * @param _target egwActionObject object, get's called for every object in _senders - * @returns boolean true if field found and has specified value, false otherwise - */ -function nm_compare_field(_action, _senders, _target) -{ - var field = document.getElementById(_action.data.fieldId); - - if (!field) return false; - - var value = $j(field).val(); - - if (_action.data.fieldValue.substr(0,1) == '!') - return value != _action.data.fieldValue.substr(1); - - return value == _action.data.fieldValue; -} - -var nm_popup_action, nm_popup_senders = null; - -/** - * Open popup for a certain action requiring further input - * - * Popup needs to have eTemplate name of action id plus "_popup" - * - * @param _action - * @param _senders - */ -function nm_open_popup(_action, _senders) -{ - var popup = document.getElementById(_action.getManager().etemplate_var_prefix + '[' + _action.id + '_popup]'); - - if (popup) { - nm_popup_action = _action; - nm_popup_senders = _senders; - popup.style.display = 'block'; - } -} - -/** - * Submit a popup action - */ -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); -} - -/** - * Hide popup - */ -function nm_hide_popup(element, div_id) -{ - var prefix = element.id.substring(0,element.id.indexOf('[')); - var popup = document.getElementById(prefix+'['+div_id+']'); - - // Hide popup - if(popup) { - popup.style.display = 'none'; - } - nm_popup_action = null; - nm_popup_senders = null; - - return false; -} - -/** - * Activate/click first link in row - */ -function nm_activate_link(_action, _senders) -{ - // $j(_senders[0].iface.getDOMNode()).find('a:first').trigger('click'); not sure why this is NOT working - - var a_href = $j(_senders[0].iface.getDOMNode()).find('a:first'); - - if (typeof a_href != undefined) - { - var target = a_href.attr('target'); - var href = a_href.attr('href'); - if (target) - window.open(href,target); - else - window.location = href; - } -} - diff --git a/etemplate/js/et2_dataview_view_rowProvider.js b/etemplate/js/et2_dataview_view_rowProvider.js index ebcddb7f46..1b9ce2bdb0 100644 --- a/etemplate/js/et2_dataview_view_rowProvider.js +++ b/etemplate/js/et2_dataview_view_rowProvider.js @@ -38,6 +38,11 @@ var et2_dataview_rowProvider = Class.extend({ this._createFullRowPrototype(); this._createDefaultPrototype(); this._createEmptyPrototype(); + this._createLoadingPrototype(); + }, + + getColumnCount: function() { + return this._columnIds.length; }, /** @@ -62,368 +67,15 @@ var et2_dataview_rowProvider = Class.extend({ return this._prototypes[_name].clone(); }, - /** - * Returns an array containing objects which have variable attributes - */ - _getVariableAttributeSet: function(_widget) { - var variableAttributes = []; - - _widget.iterateOver(function(_widget) { - // Create the attribtues - var hasAttr = false; - var widgetData = { - "widget": _widget, - "data": [] - }; - - // Get all attribute values - for (var key in _widget.attributes) - { - if (!_widget.attributes[key].ignore && - typeof _widget.options[key] != "undefined") - { - var val = _widget.options[key]; - - // TODO: Improve detection - if (typeof val == "string" && val.indexOf("$") >= 0) - { - hasAttr = true; - widgetData.data.push({ - "attribute": key, - "expression": val - }); - } - } - } - - // Add the entry if there is any data in it - if (hasAttr) - { - variableAttributes.push(widgetData); - } - - }, this); - - return variableAttributes; - }, - - _seperateWidgets: function(_varAttrs) { - // The detachable array contains all widgets which implement the - // et2_IDetachedDOM interface for all needed attributes - var detachable = []; - - // The remaining array creates all widgets which have to be completely - // cloned when the widget tree is created - var remaining = []; - - // Iterate over the widgets - for (var i = 0; i < _varAttrs.length; i++) - { - var widget = _varAttrs[i].widget; - - // Check whether the widget parents are not allready in the "remaining" - // slot - if this is the case do not include the widget at all. - var insertWidget = true; - var checkWidget = function (_widget) { - if (_widget.parent != null) - { - for (var i = 0; i < remaining.length; i++) - { - if (remaining[i].widget == _widget.parent) - { - insertWidget = false; - return; - } - } - - checkWidget(_widget.parent); - } - }; - checkWidget(widget); - - // Handle the next widget if this one should not be included. - if (!insertWidget) - { - continue; - } - - // Check whether the widget implements the et2_IDetachedDOM interface - var isDetachable = false; - if (widget.implements(et2_IDetachedDOM)) - { - // Get all attributes the widgets supports to be set in the - // "detached" mode - var supportedAttrs = []; - widget.getDetachedAttributes(supportedAttrs); - supportedAttrs.push("id"); - isDetachable = true; - - for (var j = 0; j < _varAttrs[i].data.length/* && isDetachable*/; j++) - { - var data = _varAttrs[i].data[j]; - - var supportsAttr = supportedAttrs.indexOf(data.attribute) != -1; - - if (!supportsAttr) - { - egw.debug("warn", "et2_IDetachedDOM widget " + - widget._type + " does not support " + data.attribute); - } - - isDetachable &= supportsAttr; - } - } - - // Insert the widget into the correct slot - if (isDetachable) - { - detachable.push(_varAttrs[i]); - } - else - { - remaining.push(_varAttrs[i]); - } - } - - return { - "detachable": detachable, - "remaining": remaining - }; - }, - - /** - * Removes to DOM code for all widgets in the "remaining" slot - */ - _stripTemplateRow: function(_rowTemplate) { - _rowTemplate.placeholders = []; - - for (var i = 0; i < _rowTemplate.seperated.remaining.length; i++) - { - var entry = _rowTemplate.seperated.remaining[i]; - - // Issue a warning - widgets which do not implement et2_IDOMNode - // are very slow - egw.debug("warn", "Non-clonable widget '"+ entry.widget._type + "' in dataview row - this " + - "might be slow", entry); - - // Set the placeholder for the entry to null - entry.placeholder = null; - - // Get the outer DOM-Node of the widget - if (entry.widget.implements(et2_IDOMNode)) - { - var node = entry.widget.getDOMNode(entry.widget); - - if (node && node.parentNode) - { - // Get the parent node and replace the node with a placeholder - entry.placeholder = document.createElement("span"); - node.parentNode.replaceChild(entry.placeholder, node); - _rowTemplate.placeholders.push({ - "widget": entry.widget, - "func": this._compileDOMAccessFunc(_rowTemplate.row, - entry.placeholder) - }); - } - } - } - }, - - _nodeIndex: function(_node) { - if(_node.parentNode == null) - { - return 0; - } - for (var i = 0; i < _node.parentNode.childNodes.length; i++) - { - if (_node.parentNode.childNodes[i] == _node) - { - return i; - } - } - - return -1; - }, - - /** - * Returns a function which does a relative access on the given DOM-Node - */ - _compileDOMAccessFunc: function(_root, _target) { - function recordPath(_root, _target, _path) - { - if (typeof _path == "undefined") - { - _path = []; - } - - if (_root != _target && _target) - { - // Get the index of _target in its parent node - var idx = this._nodeIndex(_target); - if (idx >= 0) - { - // Add the access selector - _path.unshift("childNodes[" + idx + "]"); - - // Record the remaining path - return recordPath.call(this, _root, _target.parentNode, _path); - } - - throw("Internal error while compiling DOM access function."); - } - else - { - _path.unshift("_node"); - return "return " + _path.join(".") + ";"; - } - } - - return new Function("_node", recordPath.call(this, _root, _target)); - }, - - /** - * Builds relative paths to the DOM-Nodes and compiles fast-access functions - */ - _buildNodeAccessFuncs: function(_rowTemplate) { - for (var i = 0; i < _rowTemplate.seperated.detachable.length; i++) - { - var entry = _rowTemplate.seperated.detachable[i]; - - // Get all needed nodes from the widget - var nodes = entry.widget.getDetachedNodes(); - var nodeFuncs = entry.nodeFuncs = new Array(nodes.length); - - // Record the path to each DOM-Node - for (var j = 0; j < nodes.length; j++) - { - nodeFuncs[j] = this._compileDOMAccessFunc(_rowTemplate.row, - nodes[j]); - } - } - }, - - /** - * Creates the data row prototype - */ - setDataRowTemplate: function(_widgets, _rootWidget) { - // Copy the root widget - this._rootWidget = _rootWidget; - - // Create the base row - var row = this.getPrototype("default"); - - // Copy the row template - var rowTemplate = { - "row": row[0], - "widgets": _widgets, - "root": _rootWidget, - "seperated": null, - "mgrs": _rootWidget.getArrayMgrs() - }; - - // Create the row widget and insert the given widgets into the row - var rowWidget = new et2_dataview_rowWidget(rowTemplate.mgrs, row[0]); - rowWidget._parent = _rootWidget; - rowWidget.createWidgets(_widgets); - - // Get the set containing all variable attributes - var variableAttributes = this._getVariableAttributeSet(rowWidget); - - // Filter out all widgets which do not implement the et2_IDetachedDOM - // interface or do not support all attributes listed in the et2_IDetachedDOM - // interface. A warning is issued for all those widgets as they heavily - // degrade the performance of the dataview - var seperated = rowTemplate.seperated = - this._seperateWidgets(variableAttributes); - - // Remove all DOM-Nodes of all widgets inside the "remaining" slot from - // the row-template, then build the access functions for the detachable - // widgets - this._stripTemplateRow(rowTemplate); - this._buildNodeAccessFuncs(rowTemplate); - - this._prototypes["dataRow"] = row; - this._template = rowTemplate; - }, - - getDataRow: function(_data, _row, _idx) { - - // Create array managers with the given data merged in - var mgrs = et2_arrayMgrs_expand(rowWidget, this._template.mgrs, - _data, _idx); - - // Insert the widgets into the row which do not provide the functions - // to set the _data directly - var rowWidget = null; - if (this._template.seperated.remaining.length > 0) - { - // Transform the variable attributes - for (var i = 0; i < this._template.seperated.remaining.length; i++) - { - var entry = this._template.seperated.remaining[i]; - - for (var j = 0; j < entry.data.length; j++) - { - var set = entry.data[j]; - entry.widget.options[set.attribute] = mgrs["content"].expandName(set.expression); - } - } - - // Create the row widget - var rowWidget = new et2_dataview_rowTemplateWidget(this._rootWidget, - _row[0]); - - // Let the row widget create the widgets - rowWidget.createWidgets(mgrs, this._template.placeholders); - } - - // Update the content of all other widgets - for (var i = 0; i < this._template.seperated.detachable.length; i++) - { - var entry = this._template.seperated.detachable[i]; - - // Parse the attribute expressions - var data = {}; - for (var j = 0; j < entry.data.length; j++) - { - var set = entry.data[j]; - data[set.attribute] = mgrs["content"].expandName(set.expression); - } - - // Retrieve all DOM-Nodes - var nodes = new Array(entry.nodeFuncs.length); - for (var j = 0; j < nodes.length; j++) - { - // Use the previously compiled node function to get the node - // from the entry - nodes[j] = entry.nodeFuncs[j](_row[0]); - } - if(typeof nodes[0] == "undefined") - egw.debug("warn", "Missing node", entry.widget.id,nodes, entry ); - - // Set the array managers first - entry.widget._mgrs = mgrs; - if (typeof data.id != "undefined") - { - entry.widget.id = data.id; - } - - // Adjust data for that row - entry.widget.transformAttributes.call(entry.widget,data); - - // Call the setDetachedAttributes function - entry.widget.setDetachedAttributes(nodes, data); - } - - return rowWidget; - }, /* ---- PRIVATE FUNCTIONS ---- */ + _createFullRowPrototype: function() { var tr = $j(document.createElement("tr")); var td = $j(document.createElement("td")) - .attr("span", this._columnIds.length) + .addClass(this._outerId + "_td_fullRow") + .attr("colspan", this._columnIds.length) .appendTo(tr); var div = $j(document.createElement("div")) .addClass(this._outerId + "_div_fullRow") @@ -452,106 +104,14 @@ var et2_dataview_rowProvider = Class.extend({ _createEmptyPrototype: function() { this._prototypes["empty"] = $j(document.createElement("tr")); + }, + + _createLoadingPrototype: function() { + var fullRow = this.getPrototype("fullRow"); + $j("div", fullRow).addClass("loading"); + + this._prototypes["loading"] = fullRow; } }); -var et2_dataview_rowWidget = et2_widget.extend(et2_IDOMNode, { - - init: function(_mgrs, _row) { - // Call the parent constructor with some dummy attributes - this._super(null, {"id": "", "type": "rowWidget"}); - - // Initialize some variables - this._widgets = []; - - // Copy the given DOM node and the content arrays - this._mgrs = _mgrs; - this._row = _row; - }, - - /** - * Copies the given array manager and clones the given widgets and inserts - * them into the row which has been passed in the constructor. - */ - createWidgets: function(_widgets) { - // Clone the given the widgets with this element as parent - this._widgets = new Array(_widgets.length); - for (var i = 0; i < _widgets.length; i++) - { - this._widgets[i] = _widgets[i].clone(this); - this._widgets[i].loadingFinished(); - } - }, - - /** - * Returns the column node for the given sender - */ - getDOMNode: function(_sender) { - for (var i = 0; i < this._widgets.length; i++) - { - if (this._widgets[i] == _sender) - { - return this._row.childNodes[i].childNodes[0]; // Return the i-th td tag - } - } - - return null; - } - -}); - -var et2_dataview_rowTemplateWidget = et2_widget.extend(et2_IDOMNode, { - - init: function(_root, _row) { - // Call the parent constructor with some dummy attributes - this._super(null, {"id": "", "type": "rowTemplateWidget"}); - - this._root = _root; - this._mgrs = {}; - this._row = _row; - - // Set parent to root widget, so sub-widget calls still work - this._parent = _root; - - // Clone the widgets inside the placeholders array - this._widgets = []; - }, - - createWidgets: function(_mgrs, _widgets) { - // Set the array managers - don't use setArrayMgrs here as this creates - // an unnecessary copy of the object - this._mgrs = _mgrs; - - this._widgets = new Array(_widgets.length); - for (var i = 0; i < _widgets.length; i++) - { - this._row.childNodes[0].childNodes[0]; - - this._widgets[i] = { - "widget": _widgets[i].widget.clone(this), - "node": _widgets[i].func(this._row) - }; - this._widgets[i].widget.loadingFinished(); - } - }, - - /** - * Returns the column node for the given sender - */ - getDOMNode: function(_sender) { - - for (var i = 0; i < this._widgets.length; i++) - { - if (this._widgets[i].widget == _sender) - { - return this._widgets[i].node; - } - } - - return null; - } - -}); - - diff --git a/etemplate/js/et2_dataview_view_spacer.js b/etemplate/js/et2_dataview_view_spacer.js index 96677d33a8..759e039b2f 100644 --- a/etemplate/js/et2_dataview_view_spacer.js +++ b/etemplate/js/et2_dataview_view_spacer.js @@ -19,25 +19,69 @@ var et2_dataview_spacer = et2_dataview_container.extend({ - init: function(_dataProvider, _rowProvider, _invalidationElem) { - + init: function (_parent, _rowProvider) { // Call the inherited container constructor - this._super(_dataProvider, _rowProvider, _invalidationElem); + this._super(_parent); + + // Initialize the row count and the row height + this._count = 0; + this._rowHeight = 19; + this._avgSum = 0; + this._avgCount = 0; // Get the spacer row and append it to the container - this.spacerNode = this.rowProvider.getPrototype("spacer", + this.spacerNode = _rowProvider.getPrototype("spacer", this._createSpacerPrototype, this); this._phDiv = $j("td", this.spacerNode); this.appendNode(this.spacerNode); }, - setHeight: function(_height) { - this._phDiv.height(_height); + setCount: function (_count, _rowHeight) { + // Set the new count and _rowHeight if given + this._count = _count; + if (typeof _rowHeight !== "undefined") + { + this._rowHeight = _rowHeight; + } + + // Update the element height + this._phDiv.height(this._count * this._rowHeight); + + // Call the invalidate function + this.invalidate(); + }, + + getCount: function () { + return this._count; + }, + + getHeight: function () { + // Set the calculated height, so that "invalidate" will work correctly + this._height = this._count * this._rowHeight; + + return this._height; + }, + + getAvgHeightData: function () { + if (this._avgCount > 0) + { + return { + "avgHeight": this._avgSum / this._avgCount, + "avgCount": this._avgCount + }; + } + + return null; + }, + + addAvgHeight: function (_height) { + this._avgSum += _height; + this._avgCount++; }, /* ---- PRIVATE FUNCTIONS ---- */ - _createSpacerPrototype: function(_outerId, _columnIds) { + _createSpacerPrototype: function (_outerId, _columnIds) { var tr = $j(document.createElement("tr")); var td = $j(document.createElement("td")) diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js index 337a8e100e..9d139197b3 100644 --- a/etemplate/js/et2_extension_nextmatch.js +++ b/etemplate/js/et2_extension_nextmatch.js @@ -33,13 +33,13 @@ et2_widget_selectbox; et2_extension_customfields; - // Include the dynheight manager + // Include all nextmatch subclasses + et2_extension_nextmatch_controller; + et2_extension_nextmatch_rowProvider; et2_extension_nextmatch_dynheight; // Include the grid classes - et2_dataview_view_gridContainer; - et2_dataview_model_dataProvider; - et2_dataview_model_columns; + et2_dataview; */ @@ -115,25 +115,19 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { this.dynheight = new et2_dynheight(this.egw().window, this.innerDiv, 150); - // Create the action manager - this.actionManager = new egwActionManager(); - - // Create the data provider which cares about streaming the row data - // efficiently to the rows. - var total = typeof this.options.settings.total != "undefined" ? - this.options.settings.total : 0; - this.dataProvider = new et2_dataview_dataProvider(this, total, - this.actionManager); - // Load the first data into the dataProvider - if (this.options.settings.rows) +/* if (this.options.settings.rows) { this.dataProvider.loadData({"rows": this.options.settings.rows}); - } + }*/ // Create the outer grid container - this.dataviewContainer = new et2_dataview_gridContainer(this.innerDiv, - this.dataProvider, this.egw()); + this.dataview = new et2_dataview(this.innerDiv, this.egw()); + + // 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; this.activeFilters = {}; }, @@ -173,31 +167,10 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { */ resize: function() { this.dynheight.update(function(_w, _h) { - this.dataviewContainer.resize(_w, _h); + this.dataview.resize(_w, _h); }, this); }, - /** - * Get Rows callback - */ - getRows: function(_fetchList, _callback, _context) { - // Create an ajax-request - var request = new egw_json_request( - "etemplate_widget_nextmatch::ajax_get_rows::etemplate", [ - this.getInstanceManager().etemplate_exec_id, - _fetchList, - this.activeFilters - ], this); - - var nextmatch = this; - // Send the request - request.sendRequest(true, function(_data) { - _callback.call(_context, _data); - // Let anything else know - nextmatch.div.trigger({type:'nm_data', nm_data: _data, 'nextmatch': nextmatch}); - }, null); - }, - /** * Sorts the nextmatch widget by the given ID. * @@ -270,10 +243,8 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { applyFilters: function() { this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); - // Clear the dataprovider and the dataview container - this will cause - // the grid to reload. - this.dataProvider.clear(); - this.dataviewContainer.clear(); + // Update the filters in the grid controller + this.controller.setFilters(this.activeFilters); }, /** @@ -431,7 +402,7 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { * for next time */ _updateUserPreferences: function() { - var colMgr = this.dataviewContainer.getColumnMgr() + var colMgr = this.dataview.getColumnMgr() if(!this.options.settings.columnselection_pref) { this.options.settings.columnselection_pref = this.options.template; } @@ -510,7 +481,6 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { "widget": _row[x].widget }; - columnData[x] = { "id": "col_" + x, "caption": this._genColumnCaption(_row[x].widget), @@ -524,14 +494,22 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { } // Create the column manager and update the grid container - this.dataviewContainer.setColumns(columnData); + this.dataview.setColumns(columnData); + + // Create the nextmatch row provider + this.rowProvider = new et2_nextmatch_rowProvider( + this.dataview.rowProvider); - var self = this; // Register handler to update preferences when column properties are changed - this.dataviewContainer.onUpdateColumns = function() { self._updateUserPreferences();}; - + var self = this; + this.dataview.onUpdateColumns = function() { + self._updateUserPreferences(); + }; + // Register handler for column selection popup - this.dataviewContainer.selectColumnsClick = function(event) { self._selectColumnsClick(event);}; + this.dataview.selectColumnsClick = function(event) { + self._selectColumnsClick(event); + }; }, _parseDataRow: function(_row, _colData) { @@ -552,7 +530,22 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { } } - this.dataviewContainer.rowProvider.setDataRowTemplate(columnWidgets, this); + this.rowProvider.setDataRowTemplate(columnWidgets, this); + + // Set the initial row count + var total = typeof this.options.settings.total != "undefined" ? + this.options.settings.total : 0; + this.dataview.grid.setTotalCount(total); + + // Create the grid controller + this.controller = new et2_nextmatch_controller( + this.egw(), + this.getInstanceManager().etemplate_exec_id, + "nm", + this.dataview.grid, + this.rowProvider); + + this.controller.setFilters(this.activeFilters); }, _parseGrid: function(_grid) { @@ -573,7 +566,7 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { _selectColumnsClick: function(e) { var self = this; - var columnMgr = this.dataviewContainer.columnMgr; + var columnMgr = this.dataview.getColumnMgr(); var columns = {}; var columns_selected = []; for (var i = 0; i < columnMgr.columns.length; i++) @@ -662,7 +655,7 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { columnMgr.setColumnVisibilitySet(visibility); self.selectPopup.toggle(); - self.dataviewContainer.updateColumns(); + self.dataview.updateColumns(); // Set default? if(defaultCheck.get_value() == "true") @@ -753,7 +746,7 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { * Activates the actions */ set_settings: function(_settings) { - if (_settings.actions) +/* if (_settings.actions) { // Read the actions from the settings array this.actionManager.updateActions(_settings.actions); @@ -761,7 +754,7 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { // this is rather hackisch, but I have no idea how to get the action_link & row_id to the actionObject of the row otherwise this.actionManager.action_links = _settings.action_links; this.actionManager.row_id = _settings.row_id; - } + }*/ }, getDOMNode: function(_sender) { @@ -774,7 +767,7 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, { { if (_sender == this.columns[i].widget) { - return this.dataviewContainer.getHeaderContainerNode(i); + return this.dataview.getHeaderContainerNode(i); } } @@ -1171,7 +1164,7 @@ var et2_nextmatch_customfields = et2_customfields_list.extend(et2_INextmatchHead // not ready yet return; } - var columnMgr = this.nextmatch.dataviewContainer.columnMgr; + var columnMgr = this.nextmatch.dataview.getColumnMgr(); var nm_column = null; for(var i = 0; i < this.nextmatch.columns.length; i++) { diff --git a/etemplate/js/et2_extension_nextmatch_controller.js b/etemplate/js/et2_extension_nextmatch_controller.js new file mode 100644 index 0000000000..7646e82017 --- /dev/null +++ b/etemplate/js/et2_extension_nextmatch_controller.js @@ -0,0 +1,135 @@ +/** + * eGroupWare eTemplate2 - Class which contains a the data model for nextmatch widgets + * + * @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 2012 + * @version $Id$ + */ + +"use strict"; + +/*egw:uses + et2_core_common; + et2_core_inheritance; + + et2_dataview_view_row; + + et2_dataview_controller; + + et2_dataview_interfaces; + + egw_data; +*/ + +var et2_nextmatch_controller = et2_dataview_controller.extend( + et2_IDataProvider, { + + /** + * Initializes the nextmatch controller. + * + * @param _egw is the api instance + * @param _execId is the execId of the etemplate + * @param _widgetId is the id of the nextmatch-widget we are fetching data + * for. + */ + init: function (_egw, _execId, _widgetId, _grid, _rowProvider) { + + // Call the parent et2_dataview_controller constructor + this._super(_grid, this, this._rowCallback, this); + + // Copy all parameters + this.egw = _egw; + this._execId = _execId; + this._widgetId = _widgetId; + this._rowProvider = _rowProvider; + + // We start with no filters + this._filters = {}; + + // Directly use the API-Implementation of dataRegisterUID and + // dataUnregisterUID + this.dataRegisterUID = _egw.dataRegisterUID; + this.dataUnregisterUID = _egw.dataUnregisterUID; + }, + + /** + * Updates the filter instance. + */ + setFilters: function (_filters) { + // Update the filters, reset the "lastModification" + this._filters = _filters; + this._lastModified = null; + + // Trigger an update + this.update(); + }, + + + /** -- PRIVATE FUNCTIONS -- **/ + + /** + * Overwrites the inherited _destroyCallback function in order to be able + * to free the "rowWidget". + */ + _destroyCallback: function (_row) { + // Destroy any widget associated to the row + if (this.entry.widget) + { + this.entry.widget.free(); + this.entry.widget = null; + } + + // Call the inherited function + this._super.call(this, arguments); + }, + + /** + * Creates the actual data row. + * + * @param _data is an array containing the row data + * @param _tr is the tr into which the data will be inserted + * @param _idx is the index of the row + * @param _entry is the internal row datastructure of the controller, in + * this special case used to store the rowWidget reference, so that it can + * be properly freed. + */ + _rowCallback: function (_data, _tr, _idx, _entry) { + // Let the row provider fill in the data row -- store the returned + // rowWidget inside the _entry + _entry.widget = this._rowProvider.getDataRow( + { "content": _data }, _tr, _idx); + }, + + + /** -- Implementation of et2_IDataProvider -- **/ + + + dataFetch: function (_queriedRange, _lastModification, _callback, + _context) { + // Pass the fetch call to the API, multiplex the data about the + // nextmatch instance into the call. + this.egw.dataFetch( + this._execId, + _queriedRange, + this._filters, + this._widgetId, + _lastModification, + _callback, + _context); + }, + + dataRegisterUID: function () { + // Overwritten in the constructor + }, + + dataUnregisterUID: function () { + // Overwritten in the constructor + } + +}); + + diff --git a/etemplate/js/et2_extension_nextmatch_dynheight.js b/etemplate/js/et2_extension_nextmatch_dynheight.js index 39a712ff39..73056e62bf 100644 --- a/etemplate/js/et2_extension_nextmatch_dynheight.js +++ b/etemplate/js/et2_extension_nextmatch_dynheight.js @@ -57,9 +57,6 @@ var et2_dynheight = Class.extend({ // Initialize the height calculation this._initialize(); - console.log(this.outerNode); - console.log(this.innerNode); - // Get the outer container height var oh = this.outerNode.height(); diff --git a/etemplate/js/et2_extension_nextmatch_rowProvider.js b/etemplate/js/et2_extension_nextmatch_rowProvider.js new file mode 100644 index 0000000000..37f66817d5 --- /dev/null +++ b/etemplate/js/et2_extension_nextmatch_rowProvider.js @@ -0,0 +1,504 @@ +/** + * eGroupWare eTemplate2 - Class which contains a factory method for rows + * + * @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 2012 + * @version $Id$ + */ + +/*egw:uses + jquery.jquery; + et2_core_inheritance; + et2_core_interfaces; + et2_core_arrayMgr; + et2_core_widget; +*/ + +/** + * The row provider contains prototypes (full clonable dom-trees) + * for all registered row types. + */ +var et2_nextmatch_rowProvider = Class.extend({ + + /** + * Creates the nextmatch row provider. + */ + init: function (_rowProvider) { + // Copy the arguments + this._rowProvider = _rowProvider; + }, + + /** + * Creates the data row prototype + */ + setDataRowTemplate: function(_widgets, _rootWidget) { + // Copy the root widget + this._rootWidget = _rootWidget; + + // Create the base row + var row = this._rowProvider.getPrototype("default"); + + // Copy the row template + var rowTemplate = { + "row": row[0], + "widgets": _widgets, + "root": _rootWidget, + "seperated": null, + "mgrs": _rootWidget.getArrayMgrs() + }; + + // Create the row widget and insert the given widgets into the row + var rowWidget = new et2_nextmatch_rowWidget(rowTemplate.mgrs, row[0]); + rowWidget._parent = _rootWidget; + rowWidget.createWidgets(_widgets); + + // Get the set containing all variable attributes + var variableAttributes = this._getVariableAttributeSet(rowWidget); + + // Filter out all widgets which do not implement the et2_IDetachedDOM + // interface or do not support all attributes listed in the et2_IDetachedDOM + // interface. A warning is issued for all those widgets as they heavily + // degrade the performance of the dataview + var seperated = rowTemplate.seperated = + this._seperateWidgets(variableAttributes); + + // Remove all DOM-Nodes of all widgets inside the "remaining" slot from + // the row-template, then build the access functions for the detachable + // widgets + this._stripTemplateRow(rowTemplate); + this._buildNodeAccessFuncs(rowTemplate); + + // Create the DOM row template + var tmpl = document.createDocumentFragment(); + row.children().each(function() { tmpl.appendChild(this); }); + + this._dataRow = tmpl; + this._template = rowTemplate; + }, + + getDataRow: function(_data, _tr, _idx) { + + // Clone the row template + var row = this._dataRow.cloneNode(true); + + // Create array managers with the given data merged in + var mgrs = et2_arrayMgrs_expand(rowWidget, this._template.mgrs, + _data, _idx); + + // Insert the widgets into the row which do not provide the functions + // to set the _data directly + var rowWidget = null; + if (this._template.seperated.remaining.length > 0) + { + // Transform the variable attributes + for (var i = 0; i < this._template.seperated.remaining.length; i++) + { + var entry = this._template.seperated.remaining[i]; + + for (var j = 0; j < entry.data.length; j++) + { + var set = entry.data[j]; + entry.widget.options[set.attribute] = mgrs["content"].expandName(set.expression); + } + } + + // Create the row widget + var rowWidget = new et2_nextmatch_rowTemplateWidget(this._rootWidget, + row); + + // Let the row widget create the widgets + rowWidget.createWidgets(mgrs, this._template.placeholders); + } + + // Update the content of all other widgets + for (var i = 0; i < this._template.seperated.detachable.length; i++) + { + var entry = this._template.seperated.detachable[i]; + + // Parse the attribute expressions + var data = {}; + for (var j = 0; j < entry.data.length; j++) + { + var set = entry.data[j]; + data[set.attribute] = mgrs["content"].expandName(set.expression); + } + + // Retrieve all DOM-Nodes + var nodes = new Array(entry.nodeFuncs.length); + for (var j = 0; j < nodes.length; j++) + { + // Use the previously compiled node function to get the node + // from the entry + nodes[j] = entry.nodeFuncs[j](row); + } + if(typeof nodes[0] == "undefined") + egw.debug("warn", "Missing node", entry.widget.id,nodes, entry ); + + // Set the array managers first + entry.widget._mgrs = mgrs; + if (typeof data.id != "undefined") + { + entry.widget.id = data.id; + } + + // Adjust data for that row + entry.widget.transformAttributes.call(entry.widget,data); + + // Call the setDetachedAttributes function + entry.widget.setDetachedAttributes(nodes, data); + } + + // Insert the row into the tr + _tr.appendChild(row); + + return rowWidget; + }, + + + /** -- PRIVATE FUNCTIONS -- **/ + + /** + * Returns an array containing objects which have variable attributes + */ + _getVariableAttributeSet: function(_widget) { + var variableAttributes = []; + + _widget.iterateOver(function(_widget) { + // Create the attribtues + var hasAttr = false; + var widgetData = { + "widget": _widget, + "data": [] + }; + + // Get all attribute values + for (var key in _widget.attributes) + { + if (!_widget.attributes[key].ignore && + typeof _widget.options[key] != "undefined") + { + var val = _widget.options[key]; + + // TODO: Improve detection + if (typeof val == "string" && val.indexOf("$") >= 0) + { + hasAttr = true; + widgetData.data.push({ + "attribute": key, + "expression": val + }); + } + } + } + + // Add the entry if there is any data in it + if (hasAttr) + { + variableAttributes.push(widgetData); + } + + }, this); + + return variableAttributes; + }, + + _seperateWidgets: function(_varAttrs) { + // The detachable array contains all widgets which implement the + // et2_IDetachedDOM interface for all needed attributes + var detachable = []; + + // The remaining array creates all widgets which have to be completely + // cloned when the widget tree is created + var remaining = []; + + // Iterate over the widgets + for (var i = 0; i < _varAttrs.length; i++) + { + var widget = _varAttrs[i].widget; + + // Check whether the widget parents are not allready in the "remaining" + // slot - if this is the case do not include the widget at all. + var insertWidget = true; + var checkWidget = function (_widget) { + if (_widget.parent != null) + { + for (var i = 0; i < remaining.length; i++) + { + if (remaining[i].widget == _widget.parent) + { + insertWidget = false; + return; + } + } + + checkWidget(_widget.parent); + } + }; + checkWidget(widget); + + // Handle the next widget if this one should not be included. + if (!insertWidget) + { + continue; + } + + // Check whether the widget implements the et2_IDetachedDOM interface + var isDetachable = false; + if (widget.implements(et2_IDetachedDOM)) + { + // Get all attributes the widgets supports to be set in the + // "detached" mode + var supportedAttrs = []; + widget.getDetachedAttributes(supportedAttrs); + supportedAttrs.push("id"); + isDetachable = true; + + for (var j = 0; j < _varAttrs[i].data.length/* && isDetachable*/; j++) + { + var data = _varAttrs[i].data[j]; + + var supportsAttr = supportedAttrs.indexOf(data.attribute) != -1; + + if (!supportsAttr) + { + egw.debug("warn", "et2_IDetachedDOM widget " + + widget._type + " does not support " + data.attribute); + } + + isDetachable &= supportsAttr; + } + } + + // Insert the widget into the correct slot + if (isDetachable) + { + detachable.push(_varAttrs[i]); + } + else + { + remaining.push(_varAttrs[i]); + } + } + + return { + "detachable": detachable, + "remaining": remaining + }; + }, + + /** + * Removes to DOM code for all widgets in the "remaining" slot + */ + _stripTemplateRow: function(_rowTemplate) { + _rowTemplate.placeholders = []; + + for (var i = 0; i < _rowTemplate.seperated.remaining.length; i++) + { + var entry = _rowTemplate.seperated.remaining[i]; + + // Issue a warning - widgets which do not implement et2_IDOMNode + // are very slow + egw.debug("warn", "Non-clonable widget '"+ entry.widget._type + "' in dataview row - this " + + "might be slow", entry); + + // Set the placeholder for the entry to null + entry.placeholder = null; + + // Get the outer DOM-Node of the widget + if (entry.widget.implements(et2_IDOMNode)) + { + var node = entry.widget.getDOMNode(entry.widget); + + if (node && node.parentNode) + { + // Get the parent node and replace the node with a placeholder + entry.placeholder = document.createElement("span"); + node.parentNode.replaceChild(entry.placeholder, node); + _rowTemplate.placeholders.push({ + "widget": entry.widget, + "func": this._compileDOMAccessFunc(_rowTemplate.row, + entry.placeholder) + }); + } + } + } + }, + + _nodeIndex: function(_node) { + if(_node.parentNode == null) + { + return 0; + } + for (var i = 0; i < _node.parentNode.childNodes.length; i++) + { + if (_node.parentNode.childNodes[i] == _node) + { + return i; + } + } + + return -1; + }, + + /** + * Returns a function which does a relative access on the given DOM-Node + */ + _compileDOMAccessFunc: function(_root, _target) { + function recordPath(_root, _target, _path) + { + if (typeof _path == "undefined") + { + _path = []; + } + + if (_root != _target && _target) + { + // Get the index of _target in its parent node + var idx = this._nodeIndex(_target); + if (idx >= 0) + { + // Add the access selector + _path.unshift("childNodes[" + idx + "]"); + + // Record the remaining path + return recordPath.call(this, _root, _target.parentNode, _path); + } + + throw("Internal error while compiling DOM access function."); + } + else + { + _path.unshift("_node"); + return "return " + _path.join(".") + ";"; + } + } + + return new Function("_node", recordPath.call(this, _root, _target)); + }, + + /** + * Builds relative paths to the DOM-Nodes and compiles fast-access functions + */ + _buildNodeAccessFuncs: function(_rowTemplate) { + for (var i = 0; i < _rowTemplate.seperated.detachable.length; i++) + { + var entry = _rowTemplate.seperated.detachable[i]; + + // Get all needed nodes from the widget + var nodes = entry.widget.getDetachedNodes(); + var nodeFuncs = entry.nodeFuncs = new Array(nodes.length); + + // Record the path to each DOM-Node + for (var j = 0; j < nodes.length; j++) + { + nodeFuncs[j] = this._compileDOMAccessFunc(_rowTemplate.row, + nodes[j]); + } + } + } + +}); + +var et2_nextmatch_rowWidget = et2_widget.extend(et2_IDOMNode, { + + init: function(_mgrs, _row) { + // Call the parent constructor with some dummy attributes + this._super(null, {"id": "", "type": "rowWidget"}); + + // Initialize some variables + this._widgets = []; + + // Copy the given DOM node and the content arrays + this._mgrs = _mgrs; + this._row = _row; + }, + + /** + * Copies the given array manager and clones the given widgets and inserts + * them into the row which has been passed in the constructor. + */ + createWidgets: function(_widgets) { + // Clone the given the widgets with this element as parent + this._widgets = new Array(_widgets.length); + for (var i = 0; i < _widgets.length; i++) + { + this._widgets[i] = _widgets[i].clone(this); + this._widgets[i].loadingFinished(); + } + }, + + /** + * Returns the column node for the given sender + */ + getDOMNode: function(_sender) { + for (var i = 0; i < this._widgets.length; i++) + { + if (this._widgets[i] == _sender) + { + return this._row.childNodes[i].childNodes[0]; // Return the i-th td tag + } + } + + return null; + } + +}); + +var et2_nextmatch_rowTemplateWidget = et2_widget.extend(et2_IDOMNode, { + + init: function(_root, _row) { + // Call the parent constructor with some dummy attributes + this._super(null, {"id": "", "type": "rowTemplateWidget"}); + + this._root = _root; + this._mgrs = {}; + this._row = _row; + + // Set parent to root widget, so sub-widget calls still work + this._parent = _root; + + // Clone the widgets inside the placeholders array + this._widgets = []; + }, + + createWidgets: function(_mgrs, _widgets) { + // Set the array managers - don't use setArrayMgrs here as this creates + // an unnecessary copy of the object + this._mgrs = _mgrs; + + this._widgets = new Array(_widgets.length); + for (var i = 0; i < _widgets.length; i++) + { + this._row.childNodes[0].childNodes[0]; + + this._widgets[i] = { + "widget": _widgets[i].widget.clone(this), + "node": _widgets[i].func(this._row) + }; + this._widgets[i].widget.loadingFinished(); + } + }, + + /** + * Returns the column node for the given sender + */ + getDOMNode: function(_sender) { + + for (var i = 0; i < this._widgets.length; i++) + { + if (this._widgets[i].widget == _sender) + { + return this._widgets[i].node; + } + } + + return null; + } + +}); + + diff --git a/etemplate/js/test/grid.css b/etemplate/js/test/grid.css index 65339e9422..897121cfdd 100644 --- a/etemplate/js/test/grid.css +++ b/etemplate/js/test/grid.css @@ -39,6 +39,7 @@ .egwGridView_spacer { background-image: url(gfx/non_loaded_bg.png); background-position: top left; +/* background-attachment: fixed;*/ } .egwGridView_outer { diff --git a/etemplate/js/test/jquery.js b/etemplate/js/test/jquery.js index 7cf5af7810..2e79bf4b39 100644 --- a/etemplate/js/test/jquery.js +++ b/etemplate/js/test/jquery.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v1.5 + * jQuery JavaScript Library v1.7.1 * http://jquery.com/ * * Copyright 2011, John Resig @@ -11,12 +11,14 @@ * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * - * Date: Mon Jan 31 08:31:29 2011 -0500 + * Date: Mon Nov 21 21:11:03 2011 -0500 */ (function( window, undefined ) { // Use the correct document accordingly with window argument (sandbox) -var document = window.document; +var document = window.document, + navigator = window.navigator, + location = window.location; var jQuery = (function() { // Define a local copy of jQuery @@ -35,8 +37,8 @@ var jQuery = function( selector, context ) { rootjQuery, // A simple way to check for HTML strings or ID strings - // (both of which we optimize for) - quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/, + // Prioritize #id over to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, // Check if a string has a non-whitespace character in it rnotwhite = /\S/, @@ -45,9 +47,6 @@ var jQuery = function( selector, context ) { trimLeft = /^\s+/, trimRight = /\s+$/, - // Check for digits - rdigit = /\d/, - // Match a standalone tag rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, @@ -63,21 +62,24 @@ var jQuery = function( selector, context ) { rmsie = /(msie) ([\w.]+)/, rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + // Keep a UserAgent string for use with jQuery.browser userAgent = navigator.userAgent, // For matching the engine and version of the browser browserMatch, - // Has the ready events already been bound? - readyBound = false, - // The deferred used on DOM ready readyList, - // Promise methods - promiseMethods = "then done fail isResolved isRejected promise".split( " " ), - // The ready event handler DOMContentLoaded, @@ -113,7 +115,7 @@ jQuery.fn = jQuery.prototype = { if ( selector === "body" && !context && document.body ) { this.context = document; this[0] = document.body; - this.selector = "body"; + this.selector = selector; this.length = 1; return this; } @@ -121,7 +123,13 @@ jQuery.fn = jQuery.prototype = { // Handle HTML strings if ( typeof selector === "string" ) { // Are we dealing with HTML string or an ID? - match = quickExpr.exec( selector ); + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } // Verify a match, and that no context was specified for #id if ( match && (match[1] || !context) ) { @@ -129,7 +137,7 @@ jQuery.fn = jQuery.prototype = { // HANDLE: $(html) -> $(array) if ( match[1] ) { context = context instanceof jQuery ? context[0] : context; - doc = (context ? context.ownerDocument || context : document); + doc = ( context ? context.ownerDocument || context : document ); // If a single string is passed in and it's a single tag // just do a createElement and skip the rest @@ -146,7 +154,7 @@ jQuery.fn = jQuery.prototype = { } else { ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); - selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes; + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; } return jQuery.merge( this, selector ); @@ -176,7 +184,7 @@ jQuery.fn = jQuery.prototype = { // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { - return (context || rootjQuery).find( selector ); + return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) @@ -190,7 +198,7 @@ jQuery.fn = jQuery.prototype = { return rootjQuery.ready( selector ); } - if (selector.selector !== undefined) { + if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } @@ -202,7 +210,7 @@ jQuery.fn = jQuery.prototype = { selector: "", // The current version of jQuery being used - jquery: "1.5", + jquery: "1.7.1", // The default length of a jQuery object is 0 length: 0, @@ -247,7 +255,7 @@ jQuery.fn = jQuery.prototype = { ret.context = this.context; if ( name === "find" ) { - ret.selector = this.selector + (this.selector ? " " : "") + selector; + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; } else if ( name ) { ret.selector = this.selector + "." + name + "(" + selector + ")"; } @@ -268,15 +276,16 @@ jQuery.fn = jQuery.prototype = { jQuery.bindReady(); // Add the callback - readyList.done( fn ); + readyList.add( fn ); return this; }, eq: function( i ) { + i = +i; return i === -1 ? this.slice( i ) : - this.slice( i, +i + 1 ); + this.slice( i, i + 1 ); }, first: function() { @@ -313,7 +322,7 @@ jQuery.fn = jQuery.prototype = { jQuery.fn.init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, + var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, @@ -378,9 +387,11 @@ jQuery.extend = jQuery.fn.extend = function() { jQuery.extend({ noConflict: function( deep ) { - window.$ = _$; + if ( window.$ === jQuery ) { + window.$ = _$; + } - if ( deep ) { + if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } @@ -394,15 +405,19 @@ jQuery.extend({ // the ready event fires. See #6781 readyWait: 1, + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + // Handle when the DOM is ready ready: function( wait ) { - // A third-party is pushing the ready event forwards - if ( wait === true ) { - jQuery.readyWait--; - } - - // Make sure that the DOM is not already loaded - if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). if ( !document.body ) { return setTimeout( jQuery.ready, 1 ); @@ -417,21 +432,21 @@ jQuery.extend({ } // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); + readyList.fireWith( document, [ jQuery ] ); // Trigger any bound ready events if ( jQuery.fn.trigger ) { - jQuery( document ).trigger( "ready" ).unbind( "ready" ); + jQuery( document ).trigger( "ready" ).off( "ready" ); } } }, bindReady: function() { - if ( readyBound ) { + if ( readyList ) { return; } - readyBound = true; + readyList = jQuery.Callbacks( "once memory" ); // Catch cases where $(document).ready() is called after the // browser event has already occurred. @@ -452,7 +467,7 @@ jQuery.extend({ } else if ( document.attachEvent ) { // ensure firing before onload, // maybe late but safe also for iframes - document.attachEvent("onreadystatechange", DOMContentLoaded); + document.attachEvent( "onreadystatechange", DOMContentLoaded ); // A fallback to window.onload, that will always work window.attachEvent( "onload", jQuery.ready ); @@ -487,8 +502,8 @@ jQuery.extend({ return obj && typeof obj === "object" && "setInterval" in obj; }, - isNaN: function( obj ) { - return obj == null || !rdigit.test( obj ) || isNaN( obj ); + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); }, type: function( obj ) { @@ -505,10 +520,15 @@ jQuery.extend({ return false; } - // Not own constructor property must be Object - if ( obj.constructor && - !hasOwn.call(obj, "constructor") && - !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 return false; } @@ -529,7 +549,7 @@ jQuery.extend({ }, error: function( msg ) { - throw msg; + throw new Error( msg ); }, parseJSON: function( data ) { @@ -540,69 +560,66 @@ jQuery.extend({ // Make sure leading/trailing whitespace is removed (IE can't handle it) data = jQuery.trim( data ); + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + // Make sure the incoming data is actual JSON // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test(data.replace(rvalidescape, "@") - .replace(rvalidtokens, "]") - .replace(rvalidbraces, "")) ) { + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { - // Try to use the native JSON parser first - return window.JSON && window.JSON.parse ? - window.JSON.parse( data ) : - (new Function("return " + data))(); + return ( new Function( "return " + data ) )(); - } else { - jQuery.error( "Invalid JSON: " + data ); } + jQuery.error( "Invalid JSON: " + data ); }, // Cross-browser xml parsing - // (xml & tmp used internally) - parseXML: function( data , xml , tmp ) { - - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); + parseXML: function( data ) { + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; } - - tmp = xml.documentElement; - - if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) { + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { jQuery.error( "Invalid XML: " + data ); } - return xml; }, noop: function() {}, - // Evalulates a script in a global context + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context globalEval: function( data ) { - if ( data && rnotwhite.test(data) ) { - // Inspired by code by Andrea Giammarchi - // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html - var head = document.getElementsByTagName("head")[0] || document.documentElement, - script = document.createElement("script"); - - script.type = "text/javascript"; - - if ( jQuery.support.scriptEval() ) { - script.appendChild( document.createTextNode( data ) ); - } else { - script.text = data; - } - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709). - head.insertBefore( script, head.firstChild ); - head.removeChild( script ); + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); } }, + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); }, @@ -611,7 +628,7 @@ jQuery.extend({ each: function( object, callback, args ) { var name, i = 0, length = object.length, - isObj = length === undefined || jQuery.isFunction(object); + isObj = length === undefined || jQuery.isFunction( object ); if ( args ) { if ( isObj ) { @@ -637,8 +654,11 @@ jQuery.extend({ } } } else { - for ( var value = object[0]; - i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } } } @@ -666,10 +686,8 @@ jQuery.extend({ if ( array != null ) { // The window, strings (and functions) also have 'length' - // The extra typeof function check is to prevent crashes - // in Safari 2 (See: #3039) // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 - var type = jQuery.type(array); + var type = jQuery.type( array ); if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { push.call( ret, array ); @@ -681,14 +699,22 @@ jQuery.extend({ return ret; }, - inArray: function( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); - } + inArray: function( elem, array, i ) { + var len; - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } } } @@ -733,15 +759,30 @@ jQuery.extend({ // arg is for internal usage only map: function( elems, callback, arg ) { - var ret = [], value; + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; // Go through the array, translating each of the items to their - // new value (or values). - for ( var i = 0, length = elems.length; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); - if ( value != null ) { - ret[ ret.length ] = value; + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } } } @@ -752,36 +793,35 @@ jQuery.extend({ // A global GUID counter for objects guid: 1, - proxy: function( fn, proxy, thisObject ) { - if ( arguments.length === 2 ) { - if ( typeof proxy === "string" ) { - thisObject = fn; - fn = thisObject[ proxy ]; - proxy = undefined; - - } else if ( proxy && !jQuery.isFunction( proxy ) ) { - thisObject = proxy; - proxy = undefined; - } + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; } - if ( !proxy && fn ) { + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), proxy = function() { - return fn.apply( thisObject || this, arguments ); + return fn.apply( context, args.concat( slice.call( arguments ) ) ); }; - } // Set the guid of unique handler to the same of original handler, so it can be removed - if ( fn ) { - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - } + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - // So proxy can be declared as an argument return proxy; }, // Mutifunctional method to get and set values to a collection - // The value/s can be optionally by executed if its a function + // The value/s can optionally be executed if it's a function access: function( elems, key, value, exec, fn, pass ) { var length = elems.length; @@ -810,156 +850,7 @@ jQuery.extend({ }, now: function() { - return (new Date()).getTime(); - }, - - // Create a simple deferred (one callbacks list) - _Deferred: function() { - var // callbacks list - callbacks = [], - // stored [ context , args ] - fired, - // to avoid firing when already doing so - firing, - // flag to know if the deferred has been cancelled - cancelled, - // the deferred itself - deferred = { - - // done( f1, f2, ...) - done: function() { - if ( !cancelled ) { - var args = arguments, - i, - length, - elem, - type, - _fired; - if ( fired ) { - _fired = fired; - fired = 0; - } - for ( i = 0, length = args.length; i < length; i++ ) { - elem = args[ i ]; - type = jQuery.type( elem ); - if ( type === "array" ) { - deferred.done.apply( deferred, elem ); - } else if ( type === "function" ) { - callbacks.push( elem ); - } - } - if ( _fired ) { - deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); - } - } - return this; - }, - - // resolve with given context and args - resolveWith: function( context, args ) { - if ( !cancelled && !fired && !firing ) { - firing = 1; - try { - while( callbacks[ 0 ] ) { - callbacks.shift().apply( context, args ); - } - } - finally { - fired = [ context, args ]; - firing = 0; - } - } - return this; - }, - - // resolve with this as context and given arguments - resolve: function() { - deferred.resolveWith( jQuery.isFunction( this.promise ) ? this.promise() : this, arguments ); - return this; - }, - - // Has this deferred been resolved? - isResolved: function() { - return !!( firing || fired ); - }, - - // Cancel - cancel: function() { - cancelled = 1; - callbacks = []; - return this; - } - }; - - return deferred; - }, - - // Full fledged deferred (two callbacks list) - Deferred: function( func ) { - var deferred = jQuery._Deferred(), - failDeferred = jQuery._Deferred(), - promise; - // Add errorDeferred methods, then and promise - jQuery.extend( deferred, { - then: function( doneCallbacks, failCallbacks ) { - deferred.done( doneCallbacks ).fail( failCallbacks ); - return this; - }, - fail: failDeferred.done, - rejectWith: failDeferred.resolveWith, - reject: failDeferred.resolve, - isRejected: failDeferred.isResolved, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj , i /* internal */ ) { - if ( obj == null ) { - if ( promise ) { - return promise; - } - promise = obj = {}; - } - i = promiseMethods.length; - while( i-- ) { - obj[ promiseMethods[ i ] ] = deferred[ promiseMethods[ i ] ]; - } - return obj; - } - } ); - // Make sure only one callback list will be used - deferred.then( failDeferred.cancel, deferred.cancel ); - // Unexpose cancel - delete deferred.cancel; - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - return deferred; - }, - - // Deferred helper - when: function( object ) { - var args = arguments, - length = args.length, - deferred = length <= 1 && object && jQuery.isFunction( object.promise ) ? - object : - jQuery.Deferred(), - promise = deferred.promise(), - resolveArray; - - if ( length > 1 ) { - resolveArray = new Array( length ); - jQuery.each( args, function( index, element ) { - jQuery.when( element ).then( function( value ) { - resolveArray[ index ] = arguments.length > 1 ? slice.call( arguments, 0 ) : value; - if( ! --length ) { - deferred.resolveWith( promise, resolveArray ); - } - }, deferred.reject ); - } ); - } else if ( deferred !== object ) { - deferred.resolve( object ); - } - return promise; + return ( new Date() ).getTime(); }, // Use of jQuery.browser is frowned upon. @@ -977,32 +868,29 @@ jQuery.extend({ }, sub: function() { - function jQuerySubclass( selector, context ) { - return new jQuerySubclass.fn.init( selector, context ); + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); } - jQuery.extend( true, jQuerySubclass, this ); - jQuerySubclass.superclass = this; - jQuerySubclass.fn = jQuerySubclass.prototype = this(); - jQuerySubclass.fn.constructor = jQuerySubclass; - jQuerySubclass.subclass = this.subclass; - jQuerySubclass.fn.init = function init( selector, context ) { - if ( context && context instanceof jQuery && !(context instanceof jQuerySubclass) ) { - context = jQuerySubclass(context); + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); } - return jQuery.fn.init.call( this, selector, context, rootjQuerySubclass ); + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); }; - jQuerySubclass.fn.init.prototype = jQuerySubclass.fn; - var rootjQuerySubclass = jQuerySubclass(document); - return jQuerySubclass; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; }, browser: {} }); -// Create readyList deferred -readyList = jQuery._Deferred(); - // Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); @@ -1019,12 +907,6 @@ if ( jQuery.browser.webkit ) { jQuery.browser.safari = true; } -if ( indexOf ) { - jQuery.inArray = function( elem, array ) { - return indexOf.call( array, elem ); - }; -} - // IE doesn't match non-breaking spaces with \s if ( rnotwhite.test( "\xA0" ) ) { trimLeft = /^[\s\xA0]+/; @@ -1070,34 +952,420 @@ function doScrollCheck() { jQuery.ready(); } -// Expose jQuery to the global object -return (window.jQuery = window.$j = jQuery); +return jQuery; })(); -(function() { +// String to Object flags format cache +var flagsCache = {}; - jQuery.support = {}; +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} - var div = document.createElement("div"); +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { - div.style.display = "none"; - div.innerHTML = "
a"; + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; - var all = div.getElementsByTagName("*"), - a = div.getElementsByTagName("a")[0], - select = document.createElement("select"), - opt = select.appendChild( document.createElement("option") ); + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!memory; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + marginDiv, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = "
a"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; // Can't get basic test support if ( !all || !all.length || !a ) { - return; + return {}; } - jQuery.support = { + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: div.firstChild.nodeType === 3, + leadingWhitespace: ( div.firstChild.nodeType === 3 ), // Make sure that tbody elements aren't automatically inserted // IE will insert them into empty tables @@ -1108,17 +1376,17 @@ return (window.jQuery = window.$j = jQuery); htmlSerialize: !!div.getElementsByTagName("link").length, // Get the style information from getAttribute - // (IE uses .cssText insted) - style: /red/.test( a.getAttribute("style") ), + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), // Make sure that URLs aren't manipulated // (IE normalizes it by default) - hrefNormalized: a.getAttribute("href") === "/a", + hrefNormalized: ( a.getAttribute("href") === "/a" ), // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55$/.test( a.style.opacity ), + opacity: /^0.55/.test( a.style.opacity ), // Verify style float existence // (IE uses styleFloat instead of cssFloat) @@ -1127,121 +1395,151 @@ return (window.jQuery = window.$j = jQuery); // Make sure that if no value is specified for a checkbox // that it defaults to "on". // (WebKit defaults to "" instead) - checkOn: div.getElementsByTagName("input")[0].value === "on", + checkOn: ( input.value === "on" ), // Make sure that a selected-by-default option has a working selected property. // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: opt.selected, + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, deleteExpando: true, - optDisabled: false, - checkClone: false, - _scriptEval: null, noCloneEvent: true, - boxModel: null, inlineBlockNeedsLayout: false, shrinkWrapBlocks: false, - reliableHiddenOffsets: true + reliableMarginRight: true }; + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as diabled) + // (WebKit marks them as disabled) select.disabled = true; - jQuery.support.optDisabled = !opt.disabled; - - jQuery.support.scriptEval = function() { - if ( jQuery.support._scriptEval === null ) { - var root = document.documentElement, - script = document.createElement("script"), - id = "script" + jQuery.now(); - - script.type = "text/javascript"; - try { - script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); - } catch(e) {} - - root.insertBefore( script, root.firstChild ); - - // Make sure that the execution of code works by injecting a script - // tag with appendChild/createTextNode - // (IE doesn't support this, fails, and uses .text instead) - if ( window[ id ] ) { - jQuery.support._scriptEval = true; - delete window[ id ]; - } else { - jQuery.support._scriptEval = false; - } - - root.removeChild( script ); - // release memory in IE - root = script = id = null; - } - - return jQuery.support._scriptEval; - }; + support.optDisabled = !opt.disabled; // Test to see if it's possible to delete an expando from an element // Fails in Internet Explorer try { delete div.test; - - } catch(e) { - jQuery.support.deleteExpando = false; + } catch( e ) { + support.deleteExpando = false; } - if ( div.attachEvent && div.fireEvent ) { - div.attachEvent("onclick", function click() { + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { // Cloning a node shouldn't copy over any // bound event handlers (IE does this) - jQuery.support.noCloneEvent = false; - div.detachEvent("onclick", click); + support.noCloneEvent = false; }); - div.cloneNode(true).fireEvent("onclick"); + div.cloneNode( true ).fireEvent( "onclick" ); } - div = document.createElement("div"); - div.innerHTML = ""; + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; - var fragment = document.createDocumentFragment(); - fragment.appendChild( div.firstChild ); + input.setAttribute("checked", "checked"); + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); // WebKit doesn't clone checked state correctly in fragments - jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - // Figure out if the W3C box model works as expected - // document.body must exist before we can do this + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + div.innerHTML = ""; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = marginDiv = div = input = null; + + // Run tests that need a body at doc ready jQuery(function() { - var div = document.createElement("div"), + var container, outer, inner, table, td, offsetSupport, + conMarginTop, ptlm, vb, style, html, body = document.getElementsByTagName("body")[0]; - // Frameset documents with no body should not run this code if ( !body ) { + // Return for frameset docs that don't have a body return; } - div.style.width = div.style.paddingLeft = "1px"; - body.appendChild( div ); - jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; + conMarginTop = 1; + ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;"; + vb = "visibility:hidden;border:0;"; + style = "style='" + ptlm + "border:5px solid #000;padding:0;'"; + html = "
" + + "" + + "
"; - if ( "zoom" in div.style ) { - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - // (IE < 8 does this) - div.style.display = "inline"; - div.style.zoom = 1; - jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2; + container = document.createElement("div"); + container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); - // Check if elements with layout shrink-wrap their children - // (IE 6 does this) - div.style.display = ""; - div.innerHTML = "
"; - jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2; - } - - div.innerHTML = "
t
"; - var tds = div.getElementsByTagName("td"); + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); // Check if table cells still have offsetWidth/Height when they are set // to display:none and there are still other visible table cells in a @@ -1250,54 +1548,77 @@ return (window.jQuery = window.$j = jQuery); // display:none (it is still safe to use offsets if a parent element is // hidden; don safety goggles and see bug #4512 for more information). // (only IE 8 fails this test) - jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0; + div.innerHTML = "
t
"; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); - tds[0].style.display = ""; - tds[1].style.display = "none"; + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; // Check if empty table cells still have offsetWidth/Height - // (IE < 8 fail this test) - jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0; - div.innerHTML = ""; + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - body.removeChild( div ).style.display = "none"; - div = tds = null; + // Figure out if the W3C box model works as expected + div.innerHTML = ""; + div.style.width = div.style.paddingLeft = "1px"; + jQuery.boxModel = support.boxModel = div.offsetWidth === 2; + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); + } + + div.style.cssText = ptlm + vb; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + body.removeChild( container ); + div = container = null; + + jQuery.extend( support, offsetSupport ); }); - // Technique from Juriy Zaytsev - // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ - var eventSupported = function( eventName ) { - var el = document.createElement("div"); - eventName = "on" + eventName; - - // We only care about the case where non-standard event systems - // are used, namely in IE. Short-circuiting here helps us to - // avoid an eval call (in setAttribute) which can cause CSP - // to go haywire. See: https://developer.mozilla.org/en/Security/CSP - if ( !el.attachEvent ) { - return true; - } - - var isSupported = (eventName in el); - if ( !isSupported ) { - el.setAttribute(eventName, "return;"); - isSupported = typeof el[eventName] === "function"; - } - el = null; - - return isSupported; - }; - - jQuery.support.submitBubbles = eventSupported("submit"); - jQuery.support.changeBubbles = eventSupported("change"); - - // release memory in IE - div = all = a = null; + return support; })(); -var rbrace = /^(?:\{.*\}|\[.*\])$/; + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; jQuery.extend({ cache: {}, @@ -1320,8 +1641,7 @@ jQuery.extend({ hasData: function( elem ) { elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - - return !!elem && !jQuery.isEmptyObject(elem); + return !!elem && !isEmptyDataObject( elem ); }, data: function( elem, name, data, pvt /* Internal Use Only */ ) { @@ -1329,7 +1649,9 @@ jQuery.extend({ return; } - var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache, + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", // We have to handle DOM nodes and JS objects differently because IE6-7 // can't GC object references properly across the DOM-JS boundary @@ -1341,11 +1663,12 @@ jQuery.extend({ // Only defining an ID for JS objects if its cache already exists allows // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando; + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; // Avoid doing any more work than we need to when trying to get data on an // object that has no data at all - if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) { + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { return; } @@ -1353,51 +1676,73 @@ jQuery.extend({ // Only DOM nodes need a new unique ID for each element since their data // ends up in the global cache if ( isNode ) { - elem[ jQuery.expando ] = id = ++jQuery.uuid; + elem[ internalKey ] = id = ++jQuery.uuid; } else { - id = jQuery.expando; + id = internalKey; } } if ( !cache[ id ] ) { cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } } // An object can be passed to jQuery.data instead of a key/value pair; this gets // shallow copied over onto the existing cache - if ( typeof name === "object" ) { + if ( typeof name === "object" || typeof name === "function" ) { if ( pvt ) { - cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name); + cache[ id ] = jQuery.extend( cache[ id ], name ); } else { - cache[ id ] = jQuery.extend(cache[ id ], name); + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); } } - thisCache = cache[ id ]; + privateCache = thisCache = cache[ id ]; - // Internal jQuery data is stored in a separate object inside the object's data + // jQuery data() is stored in a separate object inside the object's internal data // cache in order to avoid key collisions between internal data and user-defined - // data - if ( pvt ) { - if ( !thisCache[ internalKey ] ) { - thisCache[ internalKey ] = {}; + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; } - thisCache = thisCache[ internalKey ]; + thisCache = thisCache.data; } if ( data !== undefined ) { - thisCache[ name ] = data; + thisCache[ jQuery.camelCase( name ) ] = data; } - // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should - // not attempt to inspect the internal events object using jQuery.data, as this - // internal data object is undocumented and subject to change. - if ( name === "events" && !thisCache[name] ) { - return thisCache[ internalKey ] && thisCache[ internalKey ].events; + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; } - return getByName ? thisCache[ name ] : thisCache; + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; }, removeData: function( elem, name, pvt /* Internal Use Only */ ) { @@ -1405,13 +1750,18 @@ jQuery.extend({ return; } - var internalKey = jQuery.expando, isNode = elem.nodeType, + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, // See jQuery.data for more information cache = isNode ? jQuery.cache : elem, // See jQuery.data for more information - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + id = isNode ? elem[ internalKey ] : internalKey; // If there is already no cache entry for this object, there is no // purpose in continuing @@ -1420,60 +1770,74 @@ jQuery.extend({ } if ( name ) { - var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ]; + + thisCache = pvt ? cache[ id ] : cache[ id ].data; if ( thisCache ) { - delete thisCache[ name ]; + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } // If there is no data left in the cache, we want to continue // and let the cache object itself get destroyed - if ( !jQuery.isEmptyObject(thisCache) ) { + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { return; } } } // See jQuery.data for more information - if ( pvt ) { - delete cache[ id ][ internalKey ]; + if ( !pvt ) { + delete cache[ id ].data; // Don't destroy the parent cache unless the internal data object // had been the only thing left in it - if ( !jQuery.isEmptyObject(cache[ id ]) ) { + if ( !isEmptyDataObject(cache[ id ]) ) { return; } } - var internalCache = cache[ id ][ internalKey ]; - // Browsers that fail expando deletion also refuse to delete expandos on // the window, but it will allow it on all other JS objects; other browsers // don't care - if ( jQuery.support.deleteExpando || cache != window ) { + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { delete cache[ id ]; } else { cache[ id ] = null; } - // We destroyed the entire user cache at once because it's faster than - // iterating through each key, but we need to continue to persist internal - // data if it existed - if ( internalCache ) { - cache[ id ] = {}; - cache[ id ][ internalKey ] = internalCache; - - // Otherwise, we need to eliminate the expando on the node to avoid + // We destroyed the cache and need to eliminate the expando on the node to avoid // false lookups in the cache for entries that no longer exist - } else if ( isNode ) { + if ( isNode ) { // IE does not allow us to delete expando properties from nodes, // nor does it have a removeAttribute function on Document nodes; // we must handle all of these cases if ( jQuery.support.deleteExpando ) { - delete elem[ jQuery.expando ]; + delete elem[ internalKey ]; } else if ( elem.removeAttribute ) { - elem.removeAttribute( jQuery.expando ); + elem.removeAttribute( internalKey ); } else { - elem[ jQuery.expando ] = null; + elem[ internalKey ] = null; } } }, @@ -1499,22 +1863,25 @@ jQuery.extend({ jQuery.fn.extend({ data: function( key, value ) { - var data = null; + var parts, attr, name, + data = null; if ( typeof key === "undefined" ) { if ( this.length ) { data = jQuery.data( this[0] ); - if ( this[0].nodeType === 1 ) { - var attr = this[0].attributes, name; + if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) { + attr = this[0].attributes; for ( var i = 0, l = attr.length; i < l; i++ ) { name = attr[i].name; if ( name.indexOf( "data-" ) === 0 ) { - name = name.substr( 5 ); + name = jQuery.camelCase( name.substring(5) ); + dataAttr( this[0], name, data[ name ] ); } } + jQuery._data( this[0], "parsedAttrs", true ); } } @@ -1526,7 +1893,7 @@ jQuery.fn.extend({ }); } - var parts = key.split("."); + parts = key.split("."); parts[1] = parts[1] ? "." + parts[1] : ""; if ( value === undefined ) { @@ -1544,12 +1911,12 @@ jQuery.fn.extend({ } else { return this.each(function() { - var $this = jQuery( this ), + var self = jQuery( this ), args = [ parts[0], value ]; - $this.triggerHandler( "setData" + parts[1] + "!", args ); + self.triggerHandler( "setData" + parts[1] + "!", args ); jQuery.data( this, key, value ); - $this.triggerHandler( "changeData" + parts[1] + "!", args ); + self.triggerHandler( "changeData" + parts[1] + "!", args ); }); } }, @@ -1565,14 +1932,17 @@ function dataAttr( elem, key, data ) { // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute if ( data === undefined && elem.nodeType === 1 ) { - data = elem.getAttribute( "data-" + key ); + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); if ( typeof data === "string" ) { try { data = data === "true" ? true : data === "false" ? false : data === "null" ? null : - !jQuery.isNaN( data ) ? parseFloat( data ) : + jQuery.isNumeric( data ) ? parseFloat( data ) : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {} @@ -1588,38 +1958,97 @@ function dataAttr( elem, key, data ) { return data; } +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + jQuery.extend({ - queue: function( elem, type, data ) { - if ( !elem ) { - return; + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); } + }, - type = (type || "fx") + "queue"; - var q = jQuery._data( elem, type ); + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, - // Speed up dequeue by getting out quickly if this is just a lookup - if ( !data ) { + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } return q || []; } - - if ( !q || jQuery.isArray(data) ) { - q = jQuery._data( elem, type, jQuery.makeArray(data) ); - - } else { - q.push( data ); - } - - return q; }, dequeue: function( elem, type ) { type = type || "fx"; var queue = jQuery.queue( elem, type ), - fn = queue.shift(); + fn = queue.shift(), + hooks = {}; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { @@ -1630,16 +2059,18 @@ jQuery.extend({ // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { - queue.unshift("inprogress"); + queue.unshift( "inprogress" ); } - fn.call(elem, function() { - jQuery.dequeue(elem, type); - }); + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); } if ( !queue.length ) { - jQuery.removeData( elem, type + "queue", true ); + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); } } }); @@ -1654,7 +2085,7 @@ jQuery.fn.extend({ if ( data === undefined ) { return jQuery.queue( this[0], type ); } - return this.each(function( i ) { + return this.each(function() { var queue = jQuery.queue( this, type, data ); if ( type === "fx" && queue[0] !== "inprogress" ) { @@ -1667,23 +2098,54 @@ jQuery.fn.extend({ jQuery.dequeue( this, type ); }); }, - // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; type = type || "fx"; - return this.queue( type, function() { - var elem = this; - setTimeout(function() { - jQuery.dequeue( elem, type ); - }, time ); + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; }); }, - clearQueue: function( type ) { return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise(); } }); @@ -1691,66 +2153,67 @@ jQuery.fn.extend({ var rclass = /[\n\t\r]/g, - rspaces = /\s+/, + rspace = /\s+/, rreturn = /\r/g, - rspecialurl = /^(?:href|src|style)$/, rtype = /^(?:button|input)$/i, rfocusable = /^(?:button|input|object|select|textarea)$/i, rclickable = /^a(?:rea)?$/i, - rradiocheck = /^(?:radio|checkbox)$/i; - -jQuery.props = { - "for": "htmlFor", - "class": "className", - readonly: "readOnly", - maxlength: "maxLength", - cellspacing: "cellSpacing", - rowspan: "rowSpan", - colspan: "colSpan", - tabindex: "tabIndex", - usemap: "useMap", - frameborder: "frameBorder" -}; + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; jQuery.fn.extend({ attr: function( name, value ) { return jQuery.access( this, name, value, true, jQuery.attr ); }, - removeAttr: function( name, fn ) { - return this.each(function(){ - jQuery.attr( this, name, "" ); - if ( this.nodeType === 1 ) { - this.removeAttribute( name ); - } + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.prop ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} }); }, addClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.addClass( value.call(this, i, self.attr("class")) ); + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); }); } if ( value && typeof value === "string" ) { - var classNames = (value || "").split( rspaces ); + classNames = value.split( rspace ); - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; if ( elem.nodeType === 1 ) { - if ( !elem.className ) { + if ( !elem.className && classNames.length === 1 ) { elem.className = value; } else { - var className = " " + elem.className + " ", - setClass = elem.className; + setClass = " " + elem.className + " "; - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { - setClass += " " + classNames[c]; + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; } } elem.className = jQuery.trim( setClass ); @@ -1763,24 +2226,25 @@ jQuery.fn.extend({ }, removeClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.removeClass( value.call(this, i, self.attr("class")) ); + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); }); } if ( (value && typeof value === "string") || value === undefined ) { - var classNames = (value || "").split( rspaces ); + classNames = ( value || "" ).split( rspace ); - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; if ( elem.nodeType === 1 && elem.className ) { if ( value ) { - var className = (" " + elem.className + " ").replace(rclass, " "); - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - className = className.replace(" " + classNames[c] + " ", " "); + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); } elem.className = jQuery.trim( className ); @@ -1799,9 +2263,8 @@ jQuery.fn.extend({ isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { - return this.each(function(i) { - var self = jQuery(this); - self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); }); } @@ -1812,7 +2275,7 @@ jQuery.fn.extend({ i = 0, self = jQuery( this ), state = stateVal, - classNames = value.split( rspaces ); + classNames = value.split( rspace ); while ( (className = classNames[ i++ ]) ) { // check each className given, space seperated list @@ -1833,9 +2296,11 @@ jQuery.fn.extend({ }, hasClass: function( selector ) { - var className = " " + selector + " "; - for ( var i = 0, l = this.length; i < l; i++ ) { - if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { return true; } } @@ -1844,77 +2309,42 @@ jQuery.fn.extend({ }, val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + if ( !arguments.length ) { - var elem = this[0]; - if ( elem ) { - if ( jQuery.nodeName( elem, "option" ) ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; + hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; } - // We need to handle select boxes special - if ( jQuery.nodeName( elem, "select" ) ) { - var index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } - - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; - - // Don't return options that are disabled or in a disabled optgroup - if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && - (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { - - // Get the specific value for the option - value = jQuery(option).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - } - - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { - return elem.getAttribute("value") === null ? "on" : elem.value; - } - - // Everything else, we just grab the value - return (elem.value || "").replace(rreturn, ""); + ret = elem.value; + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; } - return undefined; + return; } - var isFunction = jQuery.isFunction(value); + isFunction = jQuery.isFunction( value ); - return this.each(function(i) { - var self = jQuery(this), val = value; + return this.each(function( i ) { + var self = jQuery(this), val; if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { - val = value.call(this, i, self.val()); + val = value.call( this, i, self.val() ); + } else { + val = value; } // Treat null/undefined as ""; convert numbers to string @@ -1922,27 +2352,16 @@ jQuery.fn.extend({ val = ""; } else if ( typeof val === "number" ) { val += ""; - } else if ( jQuery.isArray(val) ) { - val = jQuery.map(val, function (value) { + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { return value == null ? "" : value + ""; }); } - if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { - this.checked = jQuery.inArray( self.val(), val ) >= 0; + hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ]; - } else if ( jQuery.nodeName( this, "select" ) ) { - var values = jQuery.makeArray(val); - - jQuery( "option", this ).each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - this.selectedIndex = -1; - } - - } else { + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { this.value = val; } }); @@ -1950,6 +2369,74 @@ jQuery.fn.extend({ }); jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + attrFn: { val: true, css: true, @@ -1962,248 +2449,473 @@ jQuery.extend({ }, attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + // don't get/set attributes on text, comment and attribute nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || elem.nodeType === 2 ) { - return undefined; + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; } if ( pass && name in jQuery.attrFn ) { - return jQuery(elem)[name](value); + return jQuery( elem )[ name ]( value ); } - var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), - // Whether we are setting (or getting) - set = value !== undefined; + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } - // Try to normalize/fix the name - name = notxml && jQuery.props[ name ] || name; + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - // Only do all the following if this is a node (faster for style) - if ( elem.nodeType === 1 ) { - // These attributes require special treatment - var special = rspecialurl.test( name ); + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } - // Safari mis-reports the default selected property of an option - // Accessing the parent's selectedIndex property fixes it - if ( name === "selected" && !jQuery.support.optSelected ) { - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; + if ( value !== undefined ) { - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; - // If applicable, access the attribute via the DOM 0 way - // 'in' checks fail in Blackberry 4.7 #6931 - if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { - if ( set ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; - if ( value === null ) { - if ( elem.nodeType === 1 ) { - elem.removeAttribute( name ); - } - - } else { - elem[ name ] = value; - } - } - - // browsers index elements by id/name on forms, give priority to attributes. - if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { - return elem.getAttributeNode( name ).nodeValue; - } - - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - if ( name === "tabIndex" ) { - var attributeNode = elem.getAttributeNode( "tabIndex" ); - - return attributeNode && attributeNode.specified ? - attributeNode.value : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - - return elem[ name ]; - } - - if ( !jQuery.support.style && notxml && name === "style" ) { - if ( set ) { - elem.style.cssText = "" + value; - } - - return elem.style.cssText; - } - - if ( set ) { - // convert the value to a string (all browsers do this but IE) see #1070 + } else { elem.setAttribute( name, "" + value ); + return value; } - // Ensure that missing attributes return undefined - // Blackberry 4.7 returns "" from getAttribute #6938 - if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { - return undefined; - } + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; - var attr = !jQuery.support.hrefNormalized && notxml && special ? - // Some attributes require a special call on IE - elem.getAttribute( name, 2 ) : - elem.getAttribute( name ); + } else { + + ret = elem.getAttribute( name ); // Non-existent attributes return null, we normalize to undefined - return attr === null ? undefined : attr; + return ret === null ? + undefined : + ret; } - // Handle everything which isn't a DOM element node - if ( set ) { - elem[ name ] = value; + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + + // See #9699 for explanation of this approach (setting first, then removal) + jQuery.attr( elem, name, "" ); + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( rboolean.test( name ) && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } } - return elem[ name ]; } }); +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + -var rnamespaces = /\.(.*)$/, - rformElems = /^(?:textarea|input|select)$/i, - rperiod = /\./g, - rspace = / /g, - rescape = /[^\w\s.|`]/g, - fcleanup = function( nm ) { - return nm.replace(rescape, "\\$&"); +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /\bhover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; }, - eventKey = "events"; + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; /* - * A number of helper functions used for managing events. - * Many of the ideas behind this code originated from - * Dean Edwards' addEvent library. + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { - // Bind an event to an element - // Original by Dean Edwards - add: function( elem, types, handler, data ) { - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { return; } - // For whatever reason, IE has trouble passing the window object - // around, causing it to be cloned in the process - if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) { - elem = window; - } - - if ( handler === false ) { - handler = returnFalse; - } else if ( !handler ) { - // Fixes bug #7229. Fix recommended by jdalton - return; - } - - var handleObjIn, handleObj; - + // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; } - // Make sure that the function being executed has a unique ID + // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } - // Init the element's event structure - var elemData = jQuery._data( elem ); - - // If no elemData is found then we must be trying to bind to one of the - // banned noData elements - if ( !elemData ) { - return; - } - - var events = elemData[ eventKey ], - eventHandle = elemData.handle; - - if ( typeof events === "function" ) { - // On plain objects events is a fn that holds the the data - // which prevents this data from being JSON serialized - // the function does not need to be called, it just contains the data - eventHandle = events.handle; - events = events.events; - - } else if ( !events ) { - if ( !elem.nodeType ) { - // On plain objects, create a fn that acts as the holder - // of the values to avoid JSON serialization of event data - elemData[ eventKey ] = elemData = function(){}; - } - + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { elemData.events = events = {}; } - + eventHandle = elemData.handle; if ( !eventHandle ) { - elemData.handle = eventHandle = function() { - // Handle the second event of a trigger and when - // an event is called after a page has unloaded - return typeof jQuery !== "undefined" && !jQuery.event.triggered ? - jQuery.event.handle.apply( eventHandle.elem, arguments ) : + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; } - // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native events in IE. - eventHandle.elem = elem; - // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); - types = types.split(" "); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { - var type, i = 0, namespaces; + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); - while ( (type = types[ i++ ]) ) { - handleObj = handleObjIn ? - jQuery.extend({}, handleObjIn) : - { handler: handler, data: data }; + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; - // Namespaced event handlers - if ( type.indexOf(".") > -1 ) { - namespaces = type.split("."); - type = namespaces.shift(); - handleObj.namespace = namespaces.slice(0).sort().join("."); + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; - } else { - namespaces = []; - handleObj.namespace = ""; - } + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; - handleObj.type = type; - if ( !handleObj.guid ) { - handleObj.guid = handler.guid; - } + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); - // Get the current list of functions bound to this event - var handlers = events[ type ], - special = jQuery.event.special[ type ] || {}; - - // Init the event handler queue + // Init the event handler queue if we're the first + handlers = events[ type ]; if ( !handlers ) { handlers = events[ type ] = []; + handlers.delegateCount = 0; - // Check for a special event handler - // Only use addEventListener/attachEvent if the special - // events handler returns false + // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { @@ -2223,10 +2935,14 @@ jQuery.event = { } } - // Add the function to the element's handler list - handlers.push( handleObj ); + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } - // Keep track of which events have been used, for global triggering + // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } @@ -2237,313 +2953,230 @@ jQuery.event = { global: {}, // Detach an event or set of events from an element - remove: function( elem, types, handler, pos ) { - // don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { return; } - if ( handler === false ) { - handler = returnFalse; - } + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; - var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ), - events = elemData && elemData[ eventKey ]; - - if ( !elemData || !events ) { - return; - } - - if ( typeof events === "function" ) { - elemData = events; - events = events.events; - } - - // types is actually an event object here - if ( types && types.type ) { - handler = types.handler; - types = types.type; - } - - // Unbind all events for the element - if ( !types || typeof types === "string" && types.charAt(0) === "." ) { - types = types || ""; - - for ( type in events ) { - jQuery.event.remove( elem, type + types ); - } - - return; - } - - // Handle multiple events separated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - types = types.split(" "); - - while ( (type = types[ i++ ]) ) { - origType = type; - handleObj = null; - all = type.indexOf(".") < 0; - namespaces = []; - - if ( !all ) { - // Namespaced event handlers - namespaces = type.split("."); - type = namespaces.shift(); - - namespace = new RegExp("(^|\\.)" + - jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - eventType = events[ type ]; - - if ( !eventType ) { - continue; - } - - if ( !handler ) { - for ( j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( all || namespace.test( handleObj.namespace ) ) { - jQuery.event.remove( elem, origType, handleObj.handler, j ); - eventType.splice( j--, 1 ); - } + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } - continue; } special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; - for ( j = pos || 0; j < eventType.length; j++ ) { + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { handleObj = eventType[ j ]; - if ( handler.guid === handleObj.guid ) { - // remove the given handler for the given type - if ( all || namespace.test( handleObj.namespace ) ) { - if ( pos == null ) { - eventType.splice( j--, 1 ); - } + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } + if ( handleObj.selector ) { + eventType.delegateCount--; } - - if ( pos != null ) { - break; + if ( special.remove ) { + special.remove.call( elem, handleObj ); } } } - // remove generic event handler if no more handlers exist - if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } - ret = null; delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { - var handle = elemData.handle; + handle = elemData.handle; if ( handle ) { handle.elem = null; } - delete elemData.events; - delete elemData.handle; - - if ( typeof elemData === "function" ) { - jQuery.removeData( elem, eventKey, true ); - - } else if ( jQuery.isEmptyObject( elemData ) ) { - jQuery.removeData( elem, undefined, true ); - } + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); } }, - // bubbling is internal - trigger: function( event, data, elem /*, bubbling */ ) { + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + // Event object or event type var type = event.type || event, - bubbling = arguments[3]; + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; - if ( !bubbling ) { - event = typeof event === "object" ? - // jQuery.Event object - event[ jQuery.expando ] ? event : - // Object literal - jQuery.extend( jQuery.Event(type), event ) : - // Just the event type (string) - jQuery.Event(type); + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } - if ( type.indexOf("!") >= 0 ) { - event.type = type = type.slice(0, -1); - event.exclusive = true; - } + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } - // Handle a global trigger - if ( !elem ) { - // Don't bubble custom events when global (to avoid too much overhead) - event.stopPropagation(); + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } - // Only trigger if we've ever bound an event for it - if ( jQuery.event.global[ type ] ) { - // XXX This code smells terrible. event.js should not be directly - // inspecting the data cache - jQuery.each( jQuery.cache, function() { - // internalKey variable is just used to make it easier to find - // and potentially change this stuff later; currently it just - // points to jQuery.expando - var internalKey = jQuery.expando, - internalCache = this[ internalKey ]; - if ( internalCache && internalCache.events && internalCache.events[type] ) { - jQuery.event.trigger( event, data, internalCache.handle.elem ); - } - }); + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); } } + return; + } - // Handle triggering a single element - - // don't do events on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { - return undefined; - } - - // Clean up in case it is reused - event.result = undefined; + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { event.target = elem; - - // Clone the incoming data, if any - data = jQuery.makeArray( data ); - data.unshift( event ); } - event.currentTarget = elem; + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); - // Trigger the event, it is assumed that "handle" is a function - var handle = elem.nodeType ? - jQuery._data( elem, "handle" ) : - (jQuery._data( elem, eventKey ) || {}).handle; - - if ( handle ) { - handle.apply( elem, data ); + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; } - var parent = elem.parentNode || elem.ownerDocument; + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - // Trigger an inline bound script - try { - if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { - if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { - event.result = false; - event.preventDefault(); - } + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; } - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (inlineError) {} - - if ( !event.isPropagationStopped() && parent ) { - jQuery.event.trigger( event, data, parent, true ); - - } else if ( !event.isDefaultPrevented() ) { - var old, - target = event.target, - targetType = type.replace( rnamespaces, "" ), - isClick = jQuery.nodeName( target, "a" ) && targetType === "click", - special = jQuery.event.special[ targetType ] || {}; - - if ( (!special._default || special._default.call( elem, event ) === false) && - !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { - - try { - if ( target[ targetType ] ) { - // Make sure that we don't accidentally re-trigger the onFOO events - old = target[ "on" + targetType ]; - - if ( old ) { - target[ "on" + targetType ] = null; - } - - jQuery.event.triggered = true; - target[ targetType ](); - } - - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (triggerError) {} - - if ( old ) { - target[ "on" + targetType ] = old; - } - - jQuery.event.triggered = false; + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); } } - }, - handle: function( event ) { - var all, handlers, namespaces, namespace_re, events, - namespace_sort = [], - args = jQuery.makeArray( arguments ); + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { - event = args[0] = jQuery.event.fix( event || window.event ); - event.currentTarget = this; + cur = eventPath[i][0]; + event.type = eventPath[i][1]; - // Namespaced event handlers - all = event.type.indexOf(".") < 0 && !event.exclusive; - - if ( !all ) { - namespaces = event.type.split("."); - event.type = namespaces.shift(); - namespace_sort = namespaces.slice(0).sort(); - namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)"); + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } } + event.type = type; - event.namespace = event.namespace || namespace_sort.join("."); + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { - events = jQuery._data(this, eventKey); + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { - if ( typeof events === "function" ) { - events = events.events; - } + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { - handlers = (events || {})[ event.type ]; + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; - if ( events && handlers ) { - // Clone the handlers to prevent manipulation - handlers = handlers.slice(0); - - for ( var j = 0, l = handlers.length; j < l; j++ ) { - var handleObj = handlers[ j ]; - - // Filter the functions by class - if ( all || namespace_re.test( handleObj.namespace ) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handleObj.handler; - event.data = handleObj.data; - event.handleObj = handleObj; - - var ret = handleObj.handler.apply( this, args ); - - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); - } + if ( old ) { + elem[ ontype ] = null; } - if ( event.isImmediatePropagationStopped() ) { - break; + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; } } } @@ -2552,90 +3185,193 @@ jQuery.event = { return event.result; }, - props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Determine handlers that should run if there are delegated events + // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } - // store a copy of the original event object - // and "clone" to set read-only properties - var originalEvent = event; + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + event = jQuery.Event( originalEvent ); - for ( var i = this.props.length, prop; i; ) { - prop = this.props[ --i ]; + for ( i = copy.length; i; ) { + prop = copy[ --i ]; event[ prop ] = originalEvent[ prop ]; } - // Fix target property, if necessary + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) if ( !event.target ) { - // Fixes #1925 where srcElement might not be defined either - event.target = event.srcElement || document; + event.target = originalEvent.srcElement || document; } - // check if target is a textnode (safari) + // Target should not be a text node (#504, Safari) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } - // Add relatedTarget, if necessary - if ( !event.relatedTarget && event.fromElement ) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; - } - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && event.clientX != null ) { - var doc = document.documentElement, - body = document.body; - - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Add which for key events - if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { - event.which = event.charCode != null ? event.charCode : event.keyCode; - } - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if ( !event.metaKey && event.ctrlKey ) { + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { event.metaKey = event.ctrlKey; } - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && event.button !== undefined ) { - event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); - } - - return event; + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; }, - // Deprecated, use jQuery.guid instead - guid: 1E8, - - // Deprecated, use jQuery.proxy instead - proxy: jQuery.proxy, - special: { ready: { // Make sure the ready event is setup - setup: jQuery.bindReady, - teardown: jQuery.noop + setup: jQuery.bindReady }, - live: { - add: function( handleObj ) { - jQuery.event.add( this, - liveConvert( handleObj.origType, handleObj.selector ), - jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); - }, + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, - remove: function( handleObj ) { - jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); - } + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" }, beforeunload: { @@ -2652,9 +3388,35 @@ jQuery.event = { } } } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } } }; +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { if ( elem.removeEventListener ) { @@ -2667,10 +3429,10 @@ jQuery.removeEvent = document.removeEventListener ? } }; -jQuery.Event = function( src ) { +jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword - if ( !this.preventDefault ) { - return new jQuery.Event( src ); + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); } // Event object @@ -2680,17 +3442,21 @@ jQuery.Event = function( src ) { // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse; + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; // Event type } else { this.type = src; } - // timeStamp is buggy for some events on Firefox(#3843) - // So we won't rely on the native value - this.timeStamp = jQuery.now(); + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; @@ -2746,289 +3512,268 @@ jQuery.Event.prototype = { isImmediatePropagationStopped: returnFalse }; -// Checks if an event happened on an element within another element -// Used in jQuery.event.special.mouseenter and mouseleave handlers -var withinElement = function( event ) { - // Check if mouse(over|out) are still within the same parent element - var parent = event.relatedTarget; - - // Firefox sometimes assigns relatedTarget a XUL element - // which we cannot access the parentNode property of - try { - // Traverse up the tree - while ( parent && parent !== this ) { - parent = parent.parentNode; - } - - if ( parent !== this ) { - // set the correct event type - event.type = event.data; - - // handle event if we actually just moused on to a non sub-element - jQuery.event.handle.apply( this, arguments ); - } - - // assuming we've left the element since we most likely mousedover a xul element - } catch(e) { } -}, - -// In case of event delegation, we only need to rename the event.type, -// liveHandler will take care of the rest. -delegate = function( event ) { - event.type = event.data; - jQuery.event.handle.apply( this, arguments ); -}; - -// Create mouseenter and mouseleave events +// Create mouseenter/leave events using mouseover/out and event-time checks jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { - setup: function( data ) { - jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); - }, - teardown: function( data ) { - jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; } }; }); -// submit delegation +// IE submit delegation if ( !jQuery.support.submitBubbles ) { jQuery.event.special.submit = { - setup: function( data, namespaces ) { - if ( this.nodeName && this.nodeName.toLowerCase() !== "form" ) { - jQuery.event.add(this, "click.specialSubmit", function( e ) { - var elem = e.target, - type = elem.type; - - if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { - e.liveFired = undefined; - return trigger( "submit", this, arguments ); - } - }); - - jQuery.event.add(this, "keypress.specialSubmit", function( e ) { - var elem = e.target, - type = elem.type; - - if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { - e.liveFired = undefined; - return trigger( "submit", this, arguments ); - } - }); - - } else { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { return false; } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener }, - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialSubmit" ); + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); } }; - } -// change delegation, happens here so we have bind. +// IE change delegation and checkbox/radio fix if ( !jQuery.support.changeBubbles ) { - var changeFilters, - - getVal = function( elem ) { - var type = elem.type, val = elem.value; - - if ( type === "radio" || type === "checkbox" ) { - val = elem.checked; - - } else if ( type === "select-multiple" ) { - val = elem.selectedIndex > -1 ? - jQuery.map( elem.options, function( elem ) { - return elem.selected; - }).join("-") : - ""; - - } else if ( elem.nodeName.toLowerCase() === "select" ) { - val = elem.selectedIndex; - } - - return val; - }, - - testChange = function testChange( e ) { - var elem = e.target, data, val; - - if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { - return; - } - - data = jQuery._data( elem, "_change_data" ); - val = getVal(elem); - - // the current data will be also retrieved by beforeactivate - if ( e.type !== "focusout" || elem.type !== "radio" ) { - jQuery._data( elem, "_change_data", val ); - } - - if ( data === undefined || val === data ) { - return; - } - - if ( data != null || val ) { - e.type = "change"; - e.liveFired = undefined; - return jQuery.event.trigger( e, arguments[1], elem ); - } - }; - jQuery.event.special.change = { - filters: { - focusout: testChange, - beforedeactivate: testChange, + setup: function() { - click: function( e ) { - var elem = e.target, type = elem.type; - - if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { - return testChange.call( this, e ); + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); } - }, - - // Change has to be called before submit - // Keydown will be called before keypress, which is used in submit-event delegation - keydown: function( e ) { - var elem = e.target, type = elem.type; - - if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || - (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || - type === "select-multiple" ) { - return testChange.call( this, e ); - } - }, - - // Beforeactivate happens also before the previous element is blurred - // with this event you can't trigger a change event, but you can store - // information - beforeactivate: function( e ) { - var elem = e.target; - jQuery._data( elem, "_change_data", getVal(elem) ); - } - }, - - setup: function( data, namespaces ) { - if ( this.type === "file" ) { return false; } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; - for ( var type in changeFilters ) { - jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); - } - - return rformElems.test( this.nodeName ); + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); }, - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialChange" ); + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); return rformElems.test( this.nodeName ); } }; - - changeFilters = jQuery.event.special.change.filters; - - // Handle when the input is .focus()'d - changeFilters.focus = changeFilters.beforeactivate; -} - -function trigger( type, elem, args ) { - args[0].type = type; - return jQuery.event.handle.apply( elem, args ); } // Create "bubbling" focus and blur events -if ( document.addEventListener ) { +if ( !jQuery.support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + jQuery.event.special[ fix ] = { setup: function() { - this.addEventListener( orig, handler, true ); - }, - teardown: function() { - this.removeEventListener( orig, handler, true ); + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } } }; - - function handler( e ) { - e = jQuery.event.fix( e ); - e.type = fix; - return jQuery.event.handle.call( this, e ); - } }); } -jQuery.each(["bind", "one"], function( i, name ) { - jQuery.fn[ name ] = function( type, data, fn ) { - // Handle object literals - if ( typeof type === "object" ) { - for ( var key in type ) { - this[ name ](key, data, type[key], fn); +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); } return this; } - if ( jQuery.isFunction( data ) || data === false ) { - fn = data; - data = undefined; - } - - var handler = name === "one" ? jQuery.proxy( fn, function( event ) { - jQuery( this ).unbind( event, handler ); - return fn.apply( this, arguments ); - }) : fn; - - if ( type === "unload" && name !== "one" ) { - this.one( type, data, fn ); - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.add( this[i], type, handler, data ); + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; } } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on.call( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); return this; - }; -}); - -jQuery.fn.extend({ - unbind: function( type, fn ) { - // Handle object literals - if ( typeof type === "object" && !type.preventDefault ) { - for ( var key in type ) { - this.unbind(key, type[key]); - } - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.remove( this[i], type, fn ); - } - } - + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); return this; }, delegate: function( selector, types, data, fn ) { - return this.live( types, data, fn, selector ); + return this.on( types, selector, data, fn ); }, - undelegate: function( selector, types, fn ) { - if ( arguments.length === 0 ) { - return this.unbind( "live" ); - - } else { - return this.die( types, null, fn, selector ); - } + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); }, trigger: function( type, data ) { @@ -3036,38 +3781,36 @@ jQuery.fn.extend({ jQuery.event.trigger( type, data, this ); }); }, - triggerHandler: function( type, data ) { if ( this[0] ) { - var event = jQuery.Event( type ); - event.preventDefault(); - event.stopPropagation(); - jQuery.event.trigger( event, data, this[0] ); - return event.result; + return jQuery.event.trigger( type, data, this[0], true ); } }, toggle: function( fn ) { // Save reference to arguments for access in closure var args = arguments, - i = 1; + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; while ( i < args.length ) { - jQuery.proxy( fn, args[ i++ ] ); + args[ i++ ].guid = guid; } - return this.click( jQuery.proxy( fn, function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - })); + return this.click( toggler ); }, hover: function( fnOver, fnOut ) { @@ -3075,169 +3818,9 @@ jQuery.fn.extend({ } }); -var liveMap = { - focus: "focusin", - blur: "focusout", - mouseenter: "mouseover", - mouseleave: "mouseout" -}; - -jQuery.each(["live", "die"], function( i, name ) { - jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { - var type, i = 0, match, namespaces, preType, - selector = origSelector || this.selector, - context = origSelector ? this : jQuery( this.context ); - - if ( typeof types === "object" && !types.preventDefault ) { - for ( var key in types ) { - context[ name ]( key, data, types[key], selector ); - } - - return this; - } - - if ( jQuery.isFunction( data ) ) { - fn = data; - data = undefined; - } - - types = (types || "").split(" "); - - while ( (type = types[ i++ ]) != null ) { - match = rnamespaces.exec( type ); - namespaces = ""; - - if ( match ) { - namespaces = match[0]; - type = type.replace( rnamespaces, "" ); - } - - if ( type === "hover" ) { - types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); - continue; - } - - preType = type; - - if ( type === "focus" || type === "blur" ) { - types.push( liveMap[ type ] + namespaces ); - type = type + namespaces; - - } else { - type = (liveMap[ type ] || type) + namespaces; - } - - if ( name === "live" ) { - // bind live handler - for ( var j = 0, l = context.length; j < l; j++ ) { - jQuery.event.add( context[j], "live." + liveConvert( type, selector ), - { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); - } - - } else { - // unbind live handler - context.unbind( "live." + liveConvert( type, selector ), fn ); - } - } - - return this; - }; -}); - -function liveHandler( event ) { - var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, - elems = [], - selectors = [], - events = jQuery._data( this, eventKey ); - - if ( typeof events === "function" ) { - events = events.events; - } - - // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911) - if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) { - return; - } - - if ( event.namespace ) { - namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - event.liveFired = this; - - var live = events.live.slice(0); - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { - selectors.push( handleObj.selector ); - - } else { - live.splice( j--, 1 ); - } - } - - match = jQuery( event.target ).closest( selectors, event.currentTarget ); - - for ( i = 0, l = match.length; i < l; i++ ) { - close = match[i]; - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) { - elem = close.elem; - related = null; - - // Those two events require additional checking - if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { - event.type = handleObj.preType; - related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; - } - - if ( !related || related !== elem ) { - elems.push({ elem: elem, handleObj: handleObj, level: close.level }); - } - } - } - } - - for ( i = 0, l = elems.length; i < l; i++ ) { - match = elems[i]; - - if ( maxLevel && match.level > maxLevel ) { - break; - } - - event.currentTarget = match.elem; - event.data = match.handleObj.data; - event.handleObj = match.handleObj; - - ret = match.handleObj.origHandler.apply( match.elem, arguments ); - - if ( ret === false || event.isPropagationStopped() ) { - maxLevel = match.level; - - if ( ret === false ) { - stop = false; - } - if ( event.isImmediatePropagationStopped() ) { - break; - } - } - } - - return stop; -} - -function liveConvert( type, selector ) { - return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&"); -} - jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { // Handle event binding jQuery.fn[ name ] = function( data, fn ) { @@ -3247,16 +3830,25 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl } return arguments.length > 0 ? - this.bind( name, data, fn ) : + this.on( name, null, data, fn ) : this.trigger( name ); }; if ( jQuery.attrFn ) { jQuery.attrFn[ name ] = true; } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } }); + /*! * Sizzle CSS Selector Engine * Copyright 2011, The Dojo Foundation @@ -3266,10 +3858,14 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl (function(){ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), done = 0, toString = Object.prototype.toString, hasDuplicate = false, - baseHasDuplicate = true; + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; // Here we check if the JavaScript engine is using some sort of // optimization where it does not always call our comparision @@ -3320,7 +3916,7 @@ var Sizzle = function( selector, context, results, seed ) { if ( parts.length > 1 && origPOS.exec( selector ) ) { if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); + set = posProcess( parts[0] + parts[1], context, seed ); } else { set = Expr.relative[ parts[0] ] ? @@ -3334,7 +3930,7 @@ var Sizzle = function( selector, context, results, seed ) { selector += parts.shift(); } - set = posProcess( selector, set ); + set = posProcess( selector, set, seed ); } } @@ -3453,22 +4049,21 @@ Sizzle.matchesSelector = function( node, expr ) { }; Sizzle.find = function( expr, context, isXML ) { - var set; + var set, i, len, match, type, left; if ( !expr ) { return []; } - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var match, - type = Expr.order[i]; + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; + left = match[1]; match.splice( 1, 1 ); if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace(/\\/g, ""); + match[1] = (match[1] || "").replace( rBackslash, "" ); set = Expr.find[ type ]( match, context, isXML ); if ( set != null ) { @@ -3490,17 +4085,18 @@ Sizzle.find = function( expr, context, isXML ) { Sizzle.filter = function( expr, set, inplace, not ) { var match, anyFound, + type, found, item, filter, left, + i, pass, old = expr, result = [], curLoop = set, isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); while ( expr && set.length ) { - for ( var type in Expr.filter ) { + for ( type in Expr.filter ) { if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var found, item, - filter = Expr.filter[ type ], - left = match[1]; + filter = Expr.filter[ type ]; + left = match[1]; anyFound = false; @@ -3526,10 +4122,10 @@ Sizzle.filter = function( expr, set, inplace, not ) { } if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { if ( item ) { found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; + pass = not ^ found; if ( inplace && found != null ) { if ( pass ) { @@ -3580,7 +4176,46 @@ Sizzle.filter = function( expr, set, inplace, not ) { }; Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; }; var Expr = Sizzle.selectors = { @@ -3607,13 +4242,16 @@ var Expr = Sizzle.selectors = { attrHandle: { href: function( elem ) { return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); } }, relative: { "+": function(checkSet, part){ var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test( part ), + isTag = isPartStr && !rNonWord.test( part ), isPartStrNotTag = isPartStr && !isTag; if ( isTag ) { @@ -3641,7 +4279,7 @@ var Expr = Sizzle.selectors = { i = 0, l = checkSet.length; - if ( isPartStr && !/\W/.test( part ) ) { + if ( isPartStr && !rNonWord.test( part ) ) { part = part.toLowerCase(); for ( ; i < l; i++ ) { @@ -3675,7 +4313,7 @@ var Expr = Sizzle.selectors = { doneName = done++, checkFn = dirCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { + if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; @@ -3689,7 +4327,7 @@ var Expr = Sizzle.selectors = { doneName = done++, checkFn = dirCheck; - if ( typeof part === "string" && !/\W/.test( part ) ) { + if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; @@ -3732,7 +4370,7 @@ var Expr = Sizzle.selectors = { }, preFilter: { CLASS: function( match, curLoop, inplace, result, not, isXML ) { - match = " " + match[1].replace(/\\/g, "") + " "; + match = " " + match[1].replace( rBackslash, "" ) + " "; if ( isXML ) { return match; @@ -3755,11 +4393,11 @@ var Expr = Sizzle.selectors = { }, ID: function( match ) { - return match[1].replace(/\\/g, ""); + return match[1].replace( rBackslash, "" ); }, TAG: function( match, curLoop ) { - return match[1].toLowerCase(); + return match[1].replace( rBackslash, "" ).toLowerCase(); }, CHILD: function( match ) { @@ -3790,14 +4428,14 @@ var Expr = Sizzle.selectors = { }, ATTR: function( match, curLoop, inplace, result, not, isXML ) { - var name = match[1] = match[1].replace(/\\/g, ""); + var name = match[1] = match[1].replace( rBackslash, "" ); if ( !isXML && Expr.attrMap[name] ) { match[1] = Expr.attrMap[name]; } // Handle if an un-quoted value was used - match[4] = ( match[4] || match[5] || "" ).replace(/\\/g, ""); + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); if ( match[2] === "~=" ) { match[4] = " " + match[4] + " "; @@ -3852,7 +4490,9 @@ var Expr = Sizzle.selectors = { selected: function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly - elem.parentNode.selectedIndex; + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } return elem.selected === true; }, @@ -3874,41 +4514,53 @@ var Expr = Sizzle.selectors = { }, text: function( elem ) { - return "text" === elem.type; + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); }, + radio: function( elem ) { - return "radio" === elem.type; + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; }, checkbox: function( elem ) { - return "checkbox" === elem.type; + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; }, file: function( elem ) { - return "file" === elem.type; + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; }, + password: function( elem ) { - return "password" === elem.type; + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; }, submit: function( elem ) { - return "submit" === elem.type; + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; }, image: function( elem ) { - return "image" === elem.type; + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; }, reset: function( elem ) { - return "reset" === elem.type; + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; }, button: function( elem ) { - return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; }, input: function( elem ) { return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; } }, setFilters: { @@ -3953,7 +4605,7 @@ var Expr = Sizzle.selectors = { return filter( elem, i, match, array ); } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; } else if ( name === "not" ) { var not = match[3]; @@ -3972,7 +4624,10 @@ var Expr = Sizzle.selectors = { }, CHILD: function( elem, match ) { - var type = match[1], + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], node = elem; switch ( type ) { @@ -4000,18 +4655,18 @@ var Expr = Sizzle.selectors = { return true; case "nth": - var first = match[2], - last = match[3]; + first = match[2]; + last = match[3]; if ( first === 1 && last === 0 ) { return true; } - var doneName = match[0], - parent = elem.parentNode; + doneName = match[0]; + parent = elem.parentNode; - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { @@ -4019,10 +4674,10 @@ var Expr = Sizzle.selectors = { } } - parent.sizcache = doneName; + parent[ expando ] = doneName; } - var diff = elem.nodeIndex - last; + diff = elem.nodeIndex - last; if ( first === 0 ) { return diff === 0; @@ -4038,7 +4693,7 @@ var Expr = Sizzle.selectors = { }, TAG: function( elem, match ) { - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; }, CLASS: function( elem, match ) { @@ -4048,7 +4703,9 @@ var Expr = Sizzle.selectors = { ATTR: function( elem, match ) { var name = match[1], - result = Expr.attrHandle[ name ] ? + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : elem[ name ] != null ? elem[ name ] : @@ -4059,6 +4716,8 @@ var Expr = Sizzle.selectors = { return result == null ? type === "!=" : + !type && Sizzle.attr ? + result != null : type === "=" ? value === check : type === "*=" ? @@ -4161,6 +4820,16 @@ if ( document.documentElement.compareDocumentPosition ) { } else { sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + var al, bl, ap = [], bp = [], @@ -4168,13 +4837,8 @@ if ( document.documentElement.compareDocumentPosition ) { bup = b.parentNode, cur = aup; - // The nodes are identical, we can exit early - if ( a === b ) { - hasDuplicate = true; - return 0; - // If the nodes are siblings (or identical) we can do a quick check - } else if ( aup === bup ) { + if ( aup === bup ) { return siblingCheck( a, b ); // If no parents were found then the nodes are disconnected @@ -4234,26 +4898,6 @@ if ( document.documentElement.compareDocumentPosition ) { }; } -// Utility function for retreiving the text value of an array of DOM nodes -Sizzle.getText = function( elems ) { - var ret = "", elem; - - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += Sizzle.getText( elem.childNodes ); - } - } - - return ret; -}; - // Check to see if the browser returns elements by name when // querying by getElementById (and provide a workaround) (function(){ @@ -4407,7 +5051,8 @@ if ( document.querySelectorAll ) { // and working up from there (Thanks to Andrew Dupont for the technique) // IE 8 doesn't work on object elements } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - var old = context.getAttribute( "id" ), + var oldContext = context, + old = context.getAttribute( "id" ), nid = old || id, hasParent = context.parentNode, relativeHierarchySelector = /^\s*[+~]/.test( query ); @@ -4429,7 +5074,7 @@ if ( document.querySelectorAll ) { } catch(pseudoError) { } finally { if ( !old ) { - context.removeAttribute( "id" ); + oldContext.removeAttribute( "id" ); } } } @@ -4449,19 +5094,23 @@ if ( document.querySelectorAll ) { (function(){ var html = document.documentElement, - matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector, - pseudoWorks = false; - - try { - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( document.documentElement, "[test!='']:sizzle" ); - - } catch( pseudoError ) { - pseudoWorks = true; - } + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + Sizzle.matchesSelector = function( node, expr ) { // Make sure that attribute selectors are quoted expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); @@ -4469,7 +5118,15 @@ if ( document.querySelectorAll ) { if ( !Sizzle.isXML( node ) ) { try { if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { - return matches.call( node, expr ); + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } } } catch(e) {} } @@ -4518,13 +5175,13 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { elem = elem[dir]; while ( elem ) { - if ( elem.sizcache === doneName ) { + if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; + elem[ expando ] = doneName; elem.sizset = i; } @@ -4551,14 +5208,14 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { elem = elem[dir]; while ( elem ) { - if ( elem.sizcache === doneName ) { + if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 ) { if ( !isXML ) { - elem.sizcache = doneName; + elem[ expando ] = doneName; elem.sizset = i; } @@ -4606,7 +5263,7 @@ Sizzle.isXML = function( elem ) { return documentElement ? documentElement.nodeName !== "HTML" : false; }; -var posProcess = function( selector, context ) { +var posProcess = function( selector, context, seed ) { var match, tmpSet = [], later = "", @@ -4622,13 +5279,16 @@ var posProcess = function( selector, context ) { selector = Expr.relative[selector] ? selector + "*" : selector; for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); + Sizzle( selector, root[i], tmpSet, seed ); } return Sizzle.filter( later, tmpSet ); }; // EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.filters; @@ -4658,17 +5318,30 @@ var runtil = /Until$/, jQuery.fn.extend({ find: function( selector ) { - var ret = this.pushStack( "", "find", selector ), - length = 0; + var self = this, + i, l; - for ( var i = 0, l = this.length; i < l; i++ ) { + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { length = ret.length; jQuery.find( selector, this[i], ret ); if ( i > 0 ) { // Make sure that the results are unique - for ( var n = length; n < ret.length; n++ ) { - for ( var r = 0; r < length; r++ ) { + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { if ( ret[r] === ret[n] ) { ret.splice(n--, 1); break; @@ -4701,47 +5374,42 @@ jQuery.fn.extend({ }, is: function( selector ) { - return !!selector && jQuery.filter( selector, this ).length > 0; + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); }, closest: function( selectors, context ) { var ret = [], i, l, cur = this[0]; - + + // Array (deprecated as of jQuery 1.7) if ( jQuery.isArray( selectors ) ) { - var match, selector, - matches = {}, - level = 1; + var level = 1; - if ( cur && selectors.length ) { - for ( i = 0, l = selectors.length; i < l; i++ ) { - selector = selectors[i]; + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { - if ( !matches[selector] ) { - matches[selector] = jQuery.expr.match.POS.test( selector ) ? - jQuery( selector, context || this.context ) : - selector; + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); } } - while ( cur && cur.ownerDocument && cur !== context ) { - for ( selector in matches ) { - match = matches[selector]; - - if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { - ret.push({ selector: selector, elem: cur, level: level }); - } - } - - cur = cur.parentNode; - level++; - } + cur = cur.parentNode; + level++; } return ret; } - var pos = POS.test( selectors ) ? - jQuery( selectors, context || this.context ) : null; + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; for ( i = 0, l = this.length; i < l; i++ ) { cur = this[i]; @@ -4753,14 +5421,14 @@ jQuery.fn.extend({ } else { cur = cur.parentNode; - if ( !cur || !cur.ownerDocument || cur === context ) { + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { break; } } } } - ret = ret.length > 1 ? jQuery.unique(ret) : ret; + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; return this.pushStack( ret, "closest", selectors ); }, @@ -4768,12 +5436,17 @@ jQuery.fn.extend({ // Determine the position of an element within // the matched set of elements index: function( elem ) { - if ( !elem || typeof elem === "string" ) { - return jQuery.inArray( this[0], - // If it receives a string, the selector is used - // If it receives nothing, the siblings are used - elem ? jQuery( elem ) : this.parent().children() ); + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + // Locate the position of the desired element return jQuery.inArray( // If it receives a jQuery object, the first element is used @@ -4783,7 +5456,7 @@ jQuery.fn.extend({ add: function( selector, context ) { var set = typeof selector === "string" ? jQuery( selector, context ) : - jQuery.makeArray( selector ), + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), all = jQuery.merge( this.get(), set ); return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? @@ -4844,12 +5517,7 @@ jQuery.each({ } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ), - // The variable 'args' was introduced in - // https://github.com/jquery/jquery/commit/52a0238 - // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed. - // http://code.google.com/p/v8/issues/detail?id=1050 - args = slice.call(arguments); + var ret = jQuery.map( this, fn, until ); if ( !runtil.test( name ) ) { selector = until; @@ -4865,7 +5533,7 @@ jQuery.each({ ret = ret.reverse(); } - return this.pushStack( ret, name, args.join(",") ); + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); }; }); @@ -4921,6 +5589,11 @@ jQuery.extend({ // Implement the identical functionality for filter and not function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + if ( jQuery.isFunction( qualifier ) ) { return jQuery.grep(elements, function( elem, i ) { var retVal = !!qualifier.call( elem, i, elem ); @@ -4929,7 +5602,7 @@ function winnow( elements, qualifier, keep ) { } else if ( qualifier.nodeType ) { return jQuery.grep(elements, function( elem, i ) { - return (elem === qualifier) === keep; + return ( elem === qualifier ) === keep; }); } else if ( typeof qualifier === "string" ) { @@ -4945,22 +5618,42 @@ function winnow( elements, qualifier, keep ) { } return jQuery.grep(elements, function( elem, i ) { - return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; }); } -var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, rleadingWhitespace = /^\s+/, rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, rtagName = /<([\w:]+)/, rtbody = /", "" ], legend: [ 1, "
", "
" ], @@ -4970,7 +5663,8 @@ var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, col: [ 2, "", "
" ], area: [ 1, "", "" ], _default: [ 0, "", "" ] - }; + }, + safeFragment = createSafeFragment( document ); wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; @@ -5021,7 +5715,7 @@ jQuery.fn.extend({ } return elem; - }).append(this); + }).append( this ); } return this; @@ -5048,8 +5742,10 @@ jQuery.fn.extend({ }, wrap: function( html ) { - return this.each(function() { - jQuery( this ).wrapAll( html ); + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); }); }, @@ -5083,7 +5779,7 @@ jQuery.fn.extend({ this.parentNode.insertBefore( elem, this ); }); } else if ( arguments.length ) { - var set = jQuery(arguments[0]); + var set = jQuery.clean( arguments ); set.push.apply( set, this.toArray() ); return this.pushStack( set, "before", arguments ); } @@ -5096,7 +5792,7 @@ jQuery.fn.extend({ }); } else if ( arguments.length ) { var set = this.pushStack( this, "after", arguments ); - set.push.apply( set, jQuery(arguments[0]).toArray() ); + set.push.apply( set, jQuery.clean(arguments) ); return set; } }, @@ -5111,7 +5807,7 @@ jQuery.fn.extend({ } if ( elem.parentNode ) { - elem.parentNode.removeChild( elem ); + elem.parentNode.removeChild( elem ); } } } @@ -5136,7 +5832,7 @@ jQuery.fn.extend({ }, clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? true : dataAndEvents; + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; return this.map( function () { @@ -5151,7 +5847,7 @@ jQuery.fn.extend({ null; // See if we can take a shortcut and just use innerHTML - } else if ( typeof value === "string" && !rnocache.test( value ) && + } else if ( typeof value === "string" && !rnoInnerhtml.test( value ) && (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) && !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) { @@ -5213,7 +5909,9 @@ jQuery.fn.extend({ } }); } else { - return this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ); + return this.length ? + this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) : + this; } }, @@ -5275,7 +5973,7 @@ jQuery.fn.extend({ // in certain situations (Bug #8070). // Fragments from the fragment cache must always be cloned and never used // in place. - results.cacheable || (l > 1 && i < lastIndex) ? + results.cacheable || ( l > 1 && i < lastIndex ) ? jQuery.clone( fragment, true, true ) : fragment ); @@ -5304,44 +6002,49 @@ function cloneCopyEvent( src, dest ) { return; } - var internalKey = jQuery.expando, - oldData = jQuery.data( src ), - curData = jQuery.data( dest, oldData ); + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; - // Switch to use the internal data object, if it exists, for the next - // stage of data copying - if ( (oldData = oldData[ internalKey ]) ) { - var events = oldData.events; - curData = curData[ internalKey ] = jQuery.extend({}, oldData); + if ( events ) { + delete curData.handle; + curData.events = {}; - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( var type in events ) { - for ( var i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ], events[ type ][ i ].data ); - } + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data ); } } } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } } -function cloneFixAttributes(src, dest) { +function cloneFixAttributes( src, dest ) { + var nodeName; + // We do not need to do anything for non-Elements if ( dest.nodeType !== 1 ) { return; } - var nodeName = dest.nodeName.toLowerCase(); - // clearAttributes removes the attributes, which we don't want, // but also removes the attachEvent events, which we *do* want - dest.clearAttributes(); + if ( dest.clearAttributes ) { + dest.clearAttributes(); + } // mergeAttributes, in contrast, only merges back on the // original attributes, not the events - dest.mergeAttributes(src); + if ( dest.mergeAttributes ) { + dest.mergeAttributes( src ); + } + + nodeName = dest.nodeName.toLowerCase(); // IE6-8 fail to clone children inside object elements that use // the proprietary classid attribute value (rather than the type @@ -5380,22 +6083,38 @@ function cloneFixAttributes(src, dest) { } jQuery.buildFragment = function( args, nodes, scripts ) { - var fragment, cacheable, cacheresults, - doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document); + var fragment, cacheable, cacheresults, doc, + first = args[ 0 ]; + + // nodes may contain either an explicit document object, + // a jQuery collection or context object. + // If nodes[0] contains a valid object to assign to doc + if ( nodes && nodes[0] ) { + doc = nodes[0].ownerDocument || nodes[0]; + } + + // Ensure that an attr object doesn't incorrectly stand in as a document object + // Chrome and Firefox seem to allow this to occur and will throw exception + // Fixes #8950 + if ( !doc.createDocumentFragment ) { + doc = document; + } // Only cache "small" (1/2 KB) HTML strings that are associated with the main document // Cloning options loses the selected state, so don't cache them // IE 6 doesn't like it when you put or elements in a fragment // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache - if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document && - args[0].charAt(0) === "<" && !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) { + // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 + if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && + first.charAt(0) === "<" && !rnocache.test( first ) && + (jQuery.support.checkClone || !rchecked.test( first )) && + (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { cacheable = true; - cacheresults = jQuery.fragments[ args[0] ]; - if ( cacheresults ) { - if ( cacheresults !== 1 ) { - fragment = cacheresults; - } + + cacheresults = jQuery.fragments[ first ]; + if ( cacheresults && cacheresults !== 1 ) { + fragment = cacheresults; } } @@ -5405,7 +6124,7 @@ jQuery.buildFragment = function( args, nodes, scripts ) { } if ( cacheable ) { - jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1; + jQuery.fragments[ first ] = cacheresults ? fragment : 1; } return { fragment: fragment, cacheable: cacheable }; @@ -5431,7 +6150,7 @@ jQuery.each({ } else { for ( var i = 0, l = insert.length; i < l; i++ ) { - var elems = (i > 0 ? this.clone(true) : this).get(); + var elems = ( i > 0 ? this.clone(true) : this ).get(); jQuery( insert[i] )[ original ]( elems ); ret = ret.concat( elems ); } @@ -5441,56 +6160,102 @@ jQuery.each({ }; }); +function getAll( elem ) { + if ( typeof elem.getElementsByTagName !== "undefined" ) { + return elem.getElementsByTagName( "*" ); + + } else if ( typeof elem.querySelectorAll !== "undefined" ) { + return elem.querySelectorAll( "*" ); + + } else { + return []; + } +} + +// Used in clean, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( elem.type === "checkbox" || elem.type === "radio" ) { + elem.defaultChecked = elem.checked; + } +} +// Finds all inputs and passes them to fixDefaultChecked +function findInputs( elem ) { + var nodeName = ( elem.nodeName || "" ).toLowerCase(); + if ( nodeName === "input" ) { + fixDefaultChecked( elem ); + // Skip scripts, get other children + } else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) { + jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); + } +} + +// Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js +function shimCloneNode( elem ) { + var div = document.createElement( "div" ); + safeFragment.appendChild( div ); + + div.innerHTML = elem.outerHTML; + return div.firstChild; +} + jQuery.extend({ clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var clone = elem.cloneNode(true), - srcElements, - destElements, - i; + var srcElements, + destElements, + i, + // IE<=8 does not properly clone detached, unknown element nodes + clone = jQuery.support.html5Clone || !rnoshimcache.test( "<" + elem.nodeName ) ? + elem.cloneNode( true ) : + shimCloneNode( elem ); - if ( !jQuery.support.noCloneEvent && (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { // IE copies events bound via attachEvent when using cloneNode. // Calling detachEvent on the clone will also remove the events // from the original. In order to get around this, we use some // proprietary methods to clear the events. Thanks to MooTools // guys for this hotness. - // Using Sizzle here is crazy slow, so we use getElementsByTagName - // instead - srcElements = elem.getElementsByTagName("*"); - destElements = clone.getElementsByTagName("*"); + cloneFixAttributes( elem, clone ); + + // Using Sizzle here is crazy slow, so we use getElementsByTagName instead + srcElements = getAll( elem ); + destElements = getAll( clone ); // Weird iteration because IE will replace the length property // with an element if you are cloning the body and one of the // elements on the page has a name or id of "length" for ( i = 0; srcElements[i]; ++i ) { - cloneFixAttributes( srcElements[i], destElements[i] ); + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + cloneFixAttributes( srcElements[i], destElements[i] ); + } } - - cloneFixAttributes( elem, clone ); } // Copy the events from the original to the clone if ( dataAndEvents ) { - cloneCopyEvent( elem, clone ); - if ( deepDataAndEvents && "getElementsByTagName" in elem ) { + if ( deepDataAndEvents ) { + srcElements = getAll( elem ); + destElements = getAll( clone ); - srcElements = elem.getElementsByTagName("*"); - destElements = clone.getElementsByTagName("*"); - - if ( srcElements.length ) { - for ( i = 0; srcElements[i]; ++i ) { - cloneCopyEvent( srcElements[i], destElements[i] ); - } + for ( i = 0; srcElements[i]; ++i ) { + cloneCopyEvent( srcElements[i], destElements[i] ); } } } + + srcElements = destElements = null; + // Return the cloned set return clone; - }, + }, + clean: function( elems, context, fragment, scripts ) { + var checkScriptType; + context = context || document; // !context.createElement fails in IE with an error but returns typeof 'object' @@ -5498,7 +6263,7 @@ jQuery.extend({ context = context.ownerDocument || context[0] && context[0].ownerDocument || document; } - var ret = []; + var ret = [], j; for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { if ( typeof elem === "number" ) { @@ -5510,54 +6275,76 @@ jQuery.extend({ } // Convert html string into DOM nodes - if ( typeof elem === "string" && !rhtml.test( elem ) ) { - elem = context.createTextNode( elem ); + if ( typeof elem === "string" ) { + if ( !rhtml.test( elem ) ) { + elem = context.createTextNode( elem ); + } else { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(rxhtmlTag, "<$1>"); - } else if ( typeof elem === "string" ) { - // Fix "XHTML"-style tags in all browsers - elem = elem.replace(rxhtmlTag, "<$1>"); + // Trim whitespace, otherwise indexOf won't work as expected + var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(), + wrap = wrapMap[ tag ] || wrapMap._default, + depth = wrap[0], + div = context.createElement("div"); - // Trim whitespace, otherwise indexOf won't work as expected - var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(), - wrap = wrapMap[ tag ] || wrapMap._default, - depth = wrap[0], - div = context.createElement("div"); + // Append wrapper element to unknown element safe doc fragment + if ( context === document ) { + // Use the fragment we've already created for this document + safeFragment.appendChild( div ); + } else { + // Use a fragment created with the owner document + createSafeFragment( context ).appendChild( div ); + } - // Go to html and back, then peel off extra wrappers - div.innerHTML = wrap[1] + elem + wrap[2]; + // Go to html and back, then peel off extra wrappers + div.innerHTML = wrap[1] + elem + wrap[2]; - // Move to the right depth - while ( depth-- ) { - div = div.lastChild; - } + // Move to the right depth + while ( depth-- ) { + div = div.lastChild; + } - // Remove IE's autoinserted from table fragments - if ( !jQuery.support.tbody ) { + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { - // String was a , *may* have spurious - var hasBody = rtbody.test(elem), - tbody = tag === "table" && !hasBody ? - div.firstChild && div.firstChild.childNodes : + // String was a
, *may* have spurious + var hasBody = rtbody.test(elem), + tbody = tag === "table" && !hasBody ? + div.firstChild && div.firstChild.childNodes : - // String was a bare or - wrap[1] === "
" && !hasBody ? - div.childNodes : - []; + // String was a bare or + wrap[1] === "
" && !hasBody ? + div.childNodes : + []; - for ( var j = tbody.length - 1; j >= 0 ; --j ) { - if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { - tbody[ j ].parentNode.removeChild( tbody[ j ] ); + for ( j = tbody.length - 1; j >= 0 ; --j ) { + if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { + tbody[ j ].parentNode.removeChild( tbody[ j ] ); + } } } - } + // IE completely kills leading whitespace when innerHTML is used + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + } - // IE completely kills leading whitespace when innerHTML is used - if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + elem = div.childNodes; } + } - elem = div.childNodes; + // Resets defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + var len; + if ( !jQuery.support.appendChecked ) { + if ( elem[0] && typeof (len = elem.length) === "number" ) { + for ( j = 0; j < len; j++ ) { + findInputs( elem[j] ); + } + } else { + findInputs( elem ); + } } if ( elem.nodeType ) { @@ -5568,13 +6355,18 @@ jQuery.extend({ } if ( fragment ) { + checkScriptType = function( elem ) { + return !elem.type || rscriptType.test( elem.type ); + }; for ( i = 0; ret[i]; i++ ) { if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); } else { if ( ret[i].nodeType === 1 ) { - ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) ); + var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType ); + + ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); } fragment.appendChild( ret[i] ); } @@ -5585,7 +6377,9 @@ jQuery.extend({ }, cleanData: function( elems ) { - var data, id, cache = jQuery.cache, internalKey = jQuery.expando, special = jQuery.event.special, + var data, id, + cache = jQuery.cache, + special = jQuery.event.special, deleteExpando = jQuery.support.deleteExpando; for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { @@ -5596,7 +6390,7 @@ jQuery.extend({ id = elem[ jQuery.expando ]; if ( id ) { - data = cache[ id ] && cache[ id ][ internalKey ]; + data = cache[ id ]; if ( data && data.events ) { for ( var type in data.events ) { @@ -5636,7 +6430,7 @@ function evalScript( i, elem ) { dataType: "script" }); } else { - jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "/*$0*/" ) ); } if ( elem.parentNode ) { @@ -5649,10 +6443,11 @@ function evalScript( i, elem ) { var ralpha = /alpha\([^)]*\)/i, ropacity = /opacity=([^)]*)/, - rdashAlpha = /-([a-z])/ig, - rupper = /([A-Z])/g, + // fixed for IE9, see #8346 + rupper = /([A-Z]|^ms)/g, rnumpx = /^-?\d+(?:px)?$/i, rnum = /^-?\d/, + rrelNum = /^([\-+])=([\-+.\de]+)/, cssShow = { position: "absolute", visibility: "hidden", display: "block" }, cssWidth = [ "Left", "Right" ], @@ -5660,11 +6455,7 @@ var ralpha = /alpha\([^)]*\)/i, curCSS, getComputedStyle, - currentStyle, - - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }; + currentStyle; jQuery.fn.css = function( name, value ) { // Setting 'undefined' is a no-op @@ -5699,11 +6490,14 @@ jQuery.extend({ // Exclude the following css properties to add px cssNumber: { - "zIndex": true, + "fillOpacity": true, "fontWeight": true, + "lineHeight": true, "opacity": true, - "zoom": true, - "lineHeight": true + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true }, // Add in properties whose names you wish to fix before @@ -5721,20 +6515,29 @@ jQuery.extend({ } // Make sure that we're working with the right name - var ret, origName = jQuery.camelCase( name ), + var ret, type, origName = jQuery.camelCase( name ), style = elem.style, hooks = jQuery.cssHooks[ origName ]; name = jQuery.cssProps[ origName ] || origName; // Check if we're setting a value if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( +( ret[1] + 1) * +ret[2] ) + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + // Make sure that NaN and null values aren't set. See: #7116 - if ( typeof value === "number" && isNaN( value ) || value == null ) { + if ( value == null || type === "number" && isNaN( value ) ) { return; } // If a number was passed in, add 'px' to the (except for certain CSS properties) - if ( typeof value === "number" && !jQuery.cssNumber[ origName ] ) { + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { value += "px"; } @@ -5759,11 +6562,17 @@ jQuery.extend({ }, css: function( elem, name, extra ) { - // Make sure that we're working with the right name - var ret, origName = jQuery.camelCase( name ), - hooks = jQuery.cssHooks[ origName ]; + var ret, hooks; - name = jQuery.cssProps[ origName ] || origName; + // Make sure that we're working with the right name + name = jQuery.camelCase( name ); + hooks = jQuery.cssHooks[ name ]; + name = jQuery.cssProps[ name ] || name; + + // cssFloat needs a special treatment + if ( name === "cssFloat" ) { + name = "float"; + } // If a hook was provided get the computed value from there if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) { @@ -5771,7 +6580,7 @@ jQuery.extend({ // Otherwise, if a way to get the computed value exists, use that } else if ( curCSS ) { - return curCSS( elem, name, origName ); + return curCSS( elem, name ); } }, @@ -5791,10 +6600,6 @@ jQuery.extend({ for ( name in options ) { elem.style[ name ] = old[ name ]; } - }, - - camelCase: function( string ) { - return string.replace( rdashAlpha, fcamelCase ); } }); @@ -5808,44 +6613,21 @@ jQuery.each(["height", "width"], function( i, name ) { if ( computed ) { if ( elem.offsetWidth !== 0 ) { - val = getWH( elem, name, extra ); - + return getWH( elem, name, extra ); } else { jQuery.swap( elem, cssShow, function() { val = getWH( elem, name, extra ); }); } - if ( val <= 0 ) { - val = curCSS( elem, name, name ); - - if ( val === "0px" && currentStyle ) { - val = currentStyle( elem, name, name ); - } - - if ( val != null ) { - // Should return "auto" instead of 0, use 0 for - // temporary backwards-compat - return val === "" || val === "auto" ? "0px" : val; - } - } - - if ( val < 0 || val == null ) { - val = elem.style[ name ]; - - // Should return "auto" instead of 0, use 0 for - // temporary backwards-compat - return val === "" || val === "auto" ? "0px" : val; - } - - return typeof val === "string" ? val : val + "px"; + return val; } }, set: function( elem, value ) { if ( rnumpx.test( value ) ) { // ignore negative width and height values #1599 - value = parseFloat(value); + value = parseFloat( value ); if ( value >= 0 ) { return value + "px"; @@ -5862,42 +6644,73 @@ if ( !jQuery.support.opacity ) { jQuery.cssHooks.opacity = { get: function( elem, computed ) { // IE uses filters for opacity - return ropacity.test((computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "") ? - (parseFloat(RegExp.$1) / 100) + "" : + return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? + ( parseFloat( RegExp.$1 ) / 100 ) + "" : computed ? "1" : ""; }, set: function( elem, value ) { - var style = elem.style; + var style = elem.style, + currentStyle = elem.currentStyle, + opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", + filter = currentStyle && currentStyle.filter || style.filter || ""; // IE has trouble with opacity if it does not have layout // Force it by setting the zoom level style.zoom = 1; - // Set the alpha filter to set the opacity - var opacity = jQuery.isNaN(value) ? - "" : - "alpha(opacity=" + value * 100 + ")", - filter = style.filter || ""; + // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 + if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) { - style.filter = ralpha.test(filter) ? - filter.replace(ralpha, opacity) : - style.filter + ' ' + opacity; + // Setting style.filter to null, "" & " " still leave "filter:" in the cssText + // if "filter:" is present at all, clearType is disabled, we want to avoid this + // style.removeAttribute is IE Only, but so apparently is this code path... + style.removeAttribute( "filter" ); + + // if there there is no filter style applied in a css rule, we are done + if ( currentStyle && !currentStyle.filter ) { + return; + } + } + + // otherwise, set new filter values + style.filter = ralpha.test( filter ) ? + filter.replace( ralpha, opacity ) : + filter + " " + opacity; } }; } +jQuery(function() { + // This hook cannot be added until DOM ready because the support test + // for it is not run until after DOM ready + if ( !jQuery.support.reliableMarginRight ) { + jQuery.cssHooks.marginRight = { + get: function( elem, computed ) { + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // Work around by temporarily setting element display to inline-block + var ret; + jQuery.swap( elem, { "display": "inline-block" }, function() { + if ( computed ) { + ret = curCSS( elem, "margin-right", "marginRight" ); + } else { + ret = elem.style.marginRight; + } + }); + return ret; + } + }; + } +}); + if ( document.defaultView && document.defaultView.getComputedStyle ) { - getComputedStyle = function( elem, newName, name ) { + getComputedStyle = function( elem, name ) { var ret, defaultView, computedStyle; name = name.replace( rupper, "-$1" ).toLowerCase(); - if ( !(defaultView = elem.ownerDocument.defaultView) ) { - return undefined; - } - - if ( (computedStyle = defaultView.getComputedStyle( elem, null )) ) { + if ( (defaultView = elem.ownerDocument.defaultView) && + (computedStyle = defaultView.getComputedStyle( elem, null )) ) { ret = computedStyle.getPropertyValue( name ); if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) { ret = jQuery.style( elem, name ); @@ -5910,25 +6723,32 @@ if ( document.defaultView && document.defaultView.getComputedStyle ) { if ( document.documentElement.currentStyle ) { currentStyle = function( elem, name ) { - var left, + var left, rsLeft, uncomputed, ret = elem.currentStyle && elem.currentStyle[ name ], - rsLeft = elem.runtimeStyle && elem.runtimeStyle[ name ], style = elem.style; + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret === null && style && (uncomputed = style[ name ]) ) { + ret = uncomputed; + } + // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 // If we're not dealing with a regular pixel number // but a number that has a weird ending, we need to convert it to pixels if ( !rnumpx.test( ret ) && rnum.test( ret ) ) { + // Remember the original values left = style.left; + rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; // Put in the new values to get a computed value out if ( rsLeft ) { elem.runtimeStyle.left = elem.currentStyle.left; } - style.left = name === "fontSize" ? "1em" : (ret || 0); + style.left = name === "fontSize" ? "1em" : ( ret || 0 ); ret = style.pixelLeft + "px"; // Revert the changed values @@ -5945,27 +6765,52 @@ if ( document.documentElement.currentStyle ) { curCSS = getComputedStyle || currentStyle; function getWH( elem, name, extra ) { - var which = name === "width" ? cssWidth : cssHeight, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight; - if ( extra === "border" ) { - return val; + // Start with offset property + var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + which = name === "width" ? cssWidth : cssHeight, + i = 0, + len = which.length; + + if ( val > 0 ) { + if ( extra !== "border" ) { + for ( ; i < len; i++ ) { + if ( !extra ) { + val -= parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; + } + if ( extra === "margin" ) { + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; + } else { + val -= parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; + } + } + } + + return val + "px"; } - jQuery.each( which, function() { - if ( !extra ) { - val -= parseFloat(jQuery.css( elem, "padding" + this )) || 0; + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, name ); + if ( val < 0 || val == null ) { + val = elem.style[ name ] || 0; + } + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + + // Add padding, border, margin + if ( extra ) { + for ( ; i < len; i++ ) { + val += parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; + if ( extra !== "padding" ) { + val += parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; + } + if ( extra === "margin" ) { + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; + } } + } - if ( extra === "margin" ) { - val += parseFloat(jQuery.css( elem, "margin" + this )) || 0; - - } else { - val -= parseFloat(jQuery.css( elem, "border" + this + "Width" )) || 0; - } - }); - - return val; + return val + "px"; } if ( jQuery.expr && jQuery.expr.filters ) { @@ -5973,7 +6818,7 @@ if ( jQuery.expr && jQuery.expr.filters ) { var width = elem.offsetWidth, height = elem.offsetHeight; - return (width === 0 && height === 0) || (!jQuery.support.reliableHiddenOffsets && (elem.style.display || jQuery.css( elem, "display" )) === "none"); + return ( width === 0 && height === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); }; jQuery.expr.filters.visible = function( elem ) { @@ -5988,8 +6833,10 @@ var r20 = /%20/g, rbracket = /\[\]$/, rCRLF = /\r?\n/g, rhash = /#.*$/, - rheaders = /^(.*?):\s*(.*?)\r?$/mg, // IE leaves an \r character at EOL - rinput = /^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, + rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL + rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, rnoContent = /^(?:GET|HEAD)$/, rprotocol = /^\/\//, rquery = /\?/, @@ -5997,7 +6844,7 @@ var r20 = /%20/g, rselectTextarea = /^(?:select|textarea)/i, rspacesAjax = /\s+/, rts = /([?&])_=[^&]*/, - rurl = /^(\w+:)\/\/([^\/?#:]+)(?::(\d+))?/, + rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/, // Keep a copy of the old load method _load = jQuery.fn.load, @@ -6018,7 +6865,31 @@ var r20 = /%20/g, * 2) the catchall symbol "*" can be used * 3) selection will start with transport dataType and THEN go to "*" if needed */ - transports = {}; + transports = {}, + + // Document location + ajaxLocation, + + // Document location segments + ajaxLocParts, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = ["*/"] + ["*"]; + +// #8138, IE may throw an exception when accessing +// a field from window.location if document.domain has been set +try { + ajaxLocation = location.href; +} catch( e ) { + // Use the href attribute of an A element + // since IE will modify it given document.location + ajaxLocation = document.createElement( "a" ); + ajaxLocation.href = ""; + ajaxLocation = ajaxLocation.href; +} + +// Segment location into parts +ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; // Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport function addToPrefiltersOrTransports( structure ) { @@ -6040,7 +6911,7 @@ function addToPrefiltersOrTransports( structure ) { placeBefore; // For each dataType in the dataTypeExpression - for(; i < length; i++ ) { + for ( ; i < length; i++ ) { dataType = dataTypes[ i ]; // We control if we're asked to add before // any existing element @@ -6056,8 +6927,8 @@ function addToPrefiltersOrTransports( structure ) { }; } -//Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jXHR, +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, dataType /* internal */, inspected /* internal */ ) { dataType = dataType || options.dataTypes[ 0 ]; @@ -6071,17 +6942,17 @@ function inspectPrefiltersOrTransports( structure, options, originalOptions, jXH executeOnly = ( structure === prefilters ), selection; - for(; i < length && ( executeOnly || !selection ); i++ ) { - selection = list[ i ]( options, originalOptions, jXHR ); + for ( ; i < length && ( executeOnly || !selection ); i++ ) { + selection = list[ i ]( options, originalOptions, jqXHR ); // If we got redirected to another dataType - // we try there if not done already + // we try there if executing only and not done already if ( typeof selection === "string" ) { - if ( inspected[ selection ] ) { + if ( !executeOnly || inspected[ selection ] ) { selection = undefined; } else { options.dataTypes.unshift( selection ); selection = inspectPrefiltersOrTransports( - structure, options, originalOptions, jXHR, selection, inspected ); + structure, options, originalOptions, jqXHR, selection, inspected ); } } } @@ -6089,13 +6960,29 @@ function inspectPrefiltersOrTransports( structure, options, originalOptions, jXH // we try the catchall dataType if not done already if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) { selection = inspectPrefiltersOrTransports( - structure, options, originalOptions, jXHR, "*", inspected ); + structure, options, originalOptions, jqXHR, "*", inspected ); } // unnecessary when only executing (prefilters) // but it'll be ignored by the caller in that case return selection; } +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } +} + jQuery.fn.extend({ load: function( url, params, callback ) { if ( typeof url !== "string" && _load ) { @@ -6121,7 +7008,7 @@ jQuery.fn.extend({ if ( jQuery.isFunction( params ) ) { // We assume that it's the callback callback = params; - params = null; + params = undefined; // Otherwise, build a param string } else if ( typeof params === "object" ) { @@ -6139,14 +7026,14 @@ jQuery.fn.extend({ dataType: "html", data: params, // Complete callback (responseText is used internally) - complete: function( jXHR, status, responseText ) { - // Store the response as specified by the jXHR object - responseText = jXHR.responseText; + complete: function( jqXHR, status, responseText ) { + // Store the response as specified by the jqXHR object + responseText = jqXHR.responseText; // If successful, inject the HTML into all the matched elements - if ( jXHR.isResolved() ) { + if ( jqXHR.isResolved() ) { // #4825: Get the actual response in case // a dataFilter is present in ajaxSettings - jXHR.done(function( r ) { + jqXHR.done(function( r ) { responseText = r; }); // See if a selector was specified @@ -6165,7 +7052,7 @@ jQuery.fn.extend({ } if ( callback ) { - self.each( callback, [ responseText, status, jXHR ] ); + self.each( callback, [ responseText, status, jqXHR ] ); } } }); @@ -6203,9 +7090,9 @@ jQuery.fn.extend({ // Attach a bunch of functions for handling common AJAX events jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ jQuery.fn[ o ] = function( f ){ - return this.bind( o, f ); + return this.on( o, f ); }; -} ); +}); jQuery.each( [ "get", "post" ], function( i, method ) { jQuery[ method ] = function( url, data, callback, type ) { @@ -6213,7 +7100,7 @@ jQuery.each( [ "get", "post" ], function( i, method ) { if ( jQuery.isFunction( data ) ) { type = type || callback; callback = data; - data = null; + data = undefined; } return jQuery.ajax({ @@ -6224,27 +7111,37 @@ jQuery.each( [ "get", "post" ], function( i, method ) { dataType: type }); }; -} ); +}); jQuery.extend({ getScript: function( url, callback ) { - return jQuery.get( url, null, callback, "script" ); + return jQuery.get( url, undefined, callback, "script" ); }, getJSON: function( url, data, callback ) { return jQuery.get( url, data, callback, "json" ); }, - ajaxSetup: function( settings ) { - jQuery.extend( true, jQuery.ajaxSettings, settings ); - if ( settings.context ) { - jQuery.ajaxSettings.context = settings.context; + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + if ( settings ) { + // Building a settings object + ajaxExtend( target, jQuery.ajaxSettings ); + } else { + // Extending ajaxSettings + settings = target; + target = jQuery.ajaxSettings; } + ajaxExtend( target, settings ); + return target; }, ajaxSettings: { - url: location.href, + url: ajaxLocation, + isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), global: true, type: "GET", contentType: "application/x-www-form-urlencoded", @@ -6259,7 +7156,6 @@ jQuery.extend({ cache: null, traditional: false, headers: {}, - crossDomain: null, */ accepts: { @@ -6267,7 +7163,7 @@ jQuery.extend({ html: "text/html", text: "text/plain", json: "application/json, text/javascript", - "*": "*/*" + "*": allTypes }, contents: { @@ -6297,6 +7193,15 @@ jQuery.extend({ // Parse text as xml "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + context: true, + url: true } }, @@ -6306,9 +7211,8 @@ jQuery.extend({ // Main method ajax: function( url, options ) { - // If options is not an object, - // we simulate pre-1.5 signature - if ( typeof options !== "object" ) { + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { options = url; url = undefined; } @@ -6317,21 +7221,25 @@ jQuery.extend({ options = options || {}; var // Create the final options object - s = jQuery.extend( true, {}, jQuery.ajaxSettings, options ), - // Callbacks contexts - // We force the original context if it exists - // or take it from jQuery.ajaxSettings otherwise - // (plain objects used as context get extended) - callbackContext = - ( s.context = ( "context" in options ? options : jQuery.ajaxSettings ).context ) || s, - globalEventContext = callbackContext === s ? jQuery.event : jQuery( callbackContext ), + s = jQuery.ajaxSetup( {}, options ), + // Callbacks context + callbackContext = s.context || s, + // Context for global events + // It's the callbackContext if one was provided in the options + // and if it's a DOM node or a jQuery collection + globalEventContext = callbackContext !== s && + ( callbackContext.nodeType || callbackContext instanceof jQuery ) ? + jQuery( callbackContext ) : jQuery.event, // Deferreds deferred = jQuery.Deferred(), - completeDeferred = jQuery._Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), // Status-dependent callbacks statusCode = s.statusCode || {}, + // ifModified key + ifModifiedKey, // Headers (they are sent all at once) requestHeaders = {}, + requestHeadersNames = {}, // Response headers responseHeadersString, responseHeaders, @@ -6340,22 +7248,24 @@ jQuery.extend({ // timeout handle timeoutTimer, // Cross-domain detection vars - loc = document.location, - protocol = loc.protocol || "http:", parts, - // The jXHR state + // The jqXHR state state = 0, + // To know if global events are to be dispatched + fireGlobals, // Loop variable i, // Fake xhr - jXHR = { + jqXHR = { readyState: 0, // Caches the header setRequestHeader: function( name, value ) { - if ( state === 0 ) { - requestHeaders[ name.toLowerCase() ] = value; + if ( !state ) { + var lname = name.toLowerCase(); + name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; + requestHeaders[ name ] = value; } return this; }, @@ -6377,7 +7287,15 @@ jQuery.extend({ } match = responseHeaders[ key.toLowerCase() ]; } - return match || null; + return match === undefined ? null : match; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( !state ) { + s.mimeType = type; + } + return this; }, // Cancel the request @@ -6394,7 +7312,7 @@ jQuery.extend({ // Callback for when everything is done // It is defined here because jslint complains if it is declared // at the end of the function (which would be more logical and readable) - function done( status, statusText, responses, headers) { + function done( status, nativeStatusText, responses, headers ) { // Called once if ( state === 2 ) { @@ -6410,19 +7328,20 @@ jQuery.extend({ } // Dereference transport for early garbage collection - // (no matter how long the jXHR object will be used) + // (no matter how long the jqXHR object will be used) transport = undefined; // Cache response headers responseHeadersString = headers || ""; // Set readyState - jXHR.readyState = status ? 4 : 0; + jqXHR.readyState = status > 0 ? 4 : 0; var isSuccess, success, error, - response = responses ? ajaxHandleResponses( s, jXHR, responses ) : undefined, + statusText = nativeStatusText, + response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined, lastModified, etag; @@ -6432,11 +7351,11 @@ jQuery.extend({ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { - if ( ( lastModified = jXHR.getResponseHeader( "Last-Modified" ) ) ) { - jQuery.lastModified[ s.url ] = lastModified; + if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) { + jQuery.lastModified[ ifModifiedKey ] = lastModified; } - if ( ( etag = jXHR.getResponseHeader( "Etag" ) ) ) { - jQuery.etag[ s.url ] = etag; + if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) { + jQuery.etag[ ifModifiedKey ] = etag; } } @@ -6463,7 +7382,7 @@ jQuery.extend({ // We extract error from statusText // then normalize statusText and status for non-aborts error = statusText; - if( status ) { + if ( !statusText || status ) { statusText = "error"; if ( status < 0 ) { status = 0; @@ -6472,30 +7391,30 @@ jQuery.extend({ } // Set data for the fake xhr object - jXHR.status = status; - jXHR.statusText = statusText; + jqXHR.status = status; + jqXHR.statusText = "" + ( nativeStatusText || statusText ); // Success/Error if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jXHR ] ); + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); } else { - deferred.rejectWith( callbackContext, [ jXHR, statusText, error ] ); + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); } // Status-dependent callbacks - jXHR.statusCode( statusCode ); + jqXHR.statusCode( statusCode ); statusCode = undefined; - if ( s.global ) { + if ( fireGlobals ) { globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ), - [ jXHR, s, isSuccess ? success : error ] ); + [ jqXHR, s, isSuccess ? success : error ] ); } // Complete - completeDeferred.resolveWith( callbackContext, [ jXHR, statusText ] ); + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - if ( s.global ) { - globalEventContext.trigger( "ajaxComplete", [ jXHR, s] ); + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); // Handle the global AJAX counter if ( !( --jQuery.active ) ) { jQuery.event.trigger( "ajaxStop" ); @@ -6504,22 +7423,22 @@ jQuery.extend({ } // Attach deferreds - deferred.promise( jXHR ); - jXHR.success = jXHR.done; - jXHR.error = jXHR.fail; - jXHR.complete = completeDeferred.done; + deferred.promise( jqXHR ); + jqXHR.success = jqXHR.done; + jqXHR.error = jqXHR.fail; + jqXHR.complete = completeDeferred.add; // Status-dependent callbacks - jXHR.statusCode = function( map ) { + jqXHR.statusCode = function( map ) { if ( map ) { var tmp; if ( state < 2 ) { - for( tmp in map ) { + for ( tmp in map ) { statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; } } else { - tmp = map[ jXHR.status ]; - jXHR.then( tmp, tmp ); + tmp = map[ jqXHR.status ]; + jqXHR.then( tmp, tmp ); } } return this; @@ -6528,18 +7447,18 @@ jQuery.extend({ // Remove hash character (#7531: and string promotion) // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) // We also use the url parameter if available - s.url = ( "" + ( url || s.url ) ).replace( rhash, "" ).replace( rprotocol, protocol + "//" ); + s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); // Extract dataTypes list s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax ); // Determine if a cross-domain request is in order - if ( !s.crossDomain ) { + if ( s.crossDomain == null ) { parts = rurl.exec( s.url.toLowerCase() ); s.crossDomain = !!( parts && - ( parts[ 1 ] != protocol || parts[ 2 ] != loc.hostname || + ( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] || ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != - ( loc.port || ( protocol === "http:" ? 80 : 443 ) ) ) + ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) ); } @@ -6549,7 +7468,15 @@ jQuery.extend({ } // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jXHR ); + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefiler, stop there + if ( state === 2 ) { + return false; + } + + // We can fire global events as of now if asked to + fireGlobals = s.global; // Uppercase the type s.type = s.type.toUpperCase(); @@ -6558,7 +7485,7 @@ jQuery.extend({ s.hasContent = !rnoContent.test( s.type ); // Watch for a new set of requests - if ( s.global && jQuery.active++ === 0 ) { + if ( fireGlobals && jQuery.active++ === 0 ) { jQuery.event.trigger( "ajaxStart" ); } @@ -6568,8 +7495,13 @@ jQuery.extend({ // If data is available, append data to url if ( s.data ) { s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; + // #9682: remove data so that it's not used in an eventual retry + delete s.data; } + // Get ifModifiedKey before adding the anti-cache parameter + ifModifiedKey = s.url; + // Add anti-cache in url if needed if ( s.cache === false ) { @@ -6578,83 +7510,86 @@ jQuery.extend({ ret = s.url.replace( rts, "$1_=" + ts ); // if nothing was replaced, add timestamp to the end - s.url = ret + ( (ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); + s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); } } // Set the correct header, if data is being sent if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - requestHeaders[ "content-type" ] = s.contentType; + jqXHR.setRequestHeader( "Content-Type", s.contentType ); } // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { - if ( jQuery.lastModified[ s.url ] ) { - requestHeaders[ "if-modified-since" ] = jQuery.lastModified[ s.url ]; + ifModifiedKey = ifModifiedKey || s.url; + if ( jQuery.lastModified[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] ); } - if ( jQuery.etag[ s.url ] ) { - requestHeaders[ "if-none-match" ] = jQuery.etag[ s.url ]; + if ( jQuery.etag[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] ); } } // Set the Accepts header for the server, depending on the dataType - requestHeaders.accept = s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? - s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) : - s.accepts[ "*" ]; + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? + s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); // Check for headers option for ( i in s.headers ) { - requestHeaders[ i.toLowerCase() ] = s.headers[ i ]; + jqXHR.setRequestHeader( i, s.headers[ i ] ); } // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jXHR, s ) === false || state === 2 ) ) { + if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { // Abort if not done already - done( 0, "abort" ); - // Return false - jXHR = false; + jqXHR.abort(); + return false; + } + + // Install callbacks on deferreds + for ( i in { success: 1, error: 1, complete: 1 } ) { + jqXHR[ i ]( s[ i ] ); + } + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); } else { - - // Install callbacks on deferreds - for ( i in { success: 1, error: 1, complete: 1 } ) { - jXHR[ i ]( s[ i ] ); + jqXHR.readyState = 1; + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = setTimeout( function(){ + jqXHR.abort( "timeout" ); + }, s.timeout ); } - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - // Set state as sending - state = jXHR.readyState = 1; - // Send global event - if ( s.global ) { - globalEventContext.trigger( "ajaxSend", [ jXHR, s ] ); - } - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = setTimeout( function(){ - jXHR.abort( "timeout" ); - }, s.timeout ); - } - - try { - transport.send( requestHeaders, done ); - } catch (e) { - // Propagate exception as error if not done - if ( status < 2 ) { - done( -1, e ); - // Simply rethrow otherwise - } else { - jQuery.error( e ); - } + try { + state = 1; + transport.send( requestHeaders, done ); + } catch (e) { + // Propagate exception as error if not done + if ( state < 2 ) { + done( -1, e ); + // Simply rethrow otherwise + } else { + throw e; } } } - return jXHR; + + return jqXHR; }, // Serialize an array of form elements or a set of @@ -6673,11 +7608,11 @@ jQuery.extend({ } // If an array was passed in, assume that it is an array of form elements. - if ( jQuery.isArray( a ) || a.jquery ) { + if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { // Serialize the form elements jQuery.each( a, function() { add( this.name, this.value ); - } ); + }); } else { // If traditional, encode the "old" way (the way 1.3.2 or older @@ -6693,7 +7628,7 @@ jQuery.extend({ }); function buildParams( prefix, obj, traditional, add ) { - if ( jQuery.isArray( obj ) && obj.length ) { + if ( jQuery.isArray( obj ) ) { // Serialize array item. jQuery.each( obj, function( i, v ) { if ( traditional || rbracket.test( prefix ) ) { @@ -6713,16 +7648,9 @@ function buildParams( prefix, obj, traditional, add ) { }); } else if ( !traditional && obj != null && typeof obj === "object" ) { - // If we see an array here, it is empty and should be treated as an empty - // object - if ( jQuery.isArray( obj ) || jQuery.isEmptyObject( obj ) ) { - add( prefix, "" ); - // Serialize object item. - } else { - jQuery.each( obj, function( k, v ) { - buildParams( prefix + "[" + k + "]", v, traditional, add ); - }); + for ( var name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); } } else { @@ -6749,7 +7677,7 @@ jQuery.extend({ * - finds the right dataType (mediates between content-type and expected dataType) * - returns the corresponding response */ -function ajaxHandleResponses( s, jXHR, responses ) { +function ajaxHandleResponses( s, jqXHR, responses ) { var contents = s.contents, dataTypes = s.dataTypes, @@ -6760,9 +7688,9 @@ function ajaxHandleResponses( s, jXHR, responses ) { firstDataType; // Fill responseXXX fields - for( type in responseFields ) { + for ( type in responseFields ) { if ( type in responses ) { - jXHR[ responseFields[type] ] = responses[ type ]; + jqXHR[ responseFields[type] ] = responses[ type ]; } } @@ -6770,7 +7698,7 @@ function ajaxHandleResponses( s, jXHR, responses ) { while( dataTypes[ 0 ] === "*" ) { dataTypes.shift(); if ( ct === undefined ) { - ct = jXHR.getResponseHeader( "content-type" ); + ct = s.mimeType || jqXHR.getResponseHeader( "content-type" ); } } @@ -6822,8 +7750,9 @@ function ajaxConvert( s, response ) { } var dataTypes = s.dataTypes, - converters = s.converters, + converters = {}, i, + key, length = dataTypes.length, tmp, // Current and previous dataTypes @@ -6838,14 +7767,24 @@ function ajaxConvert( s, response ) { conv2; // For each dataType in the chain - for( i = 1; i < length; i++ ) { + for ( i = 1; i < length; i++ ) { + + // Create converters map + // with lowercased keys + if ( i === 1 ) { + for ( key in s.converters ) { + if ( typeof key === "string" ) { + converters[ key.toLowerCase() ] = s.converters[ key ]; + } + } + } // Get the dataTypes prev = current; current = dataTypes[ i ]; // If current is auto dataType, update it to prev - if( current === "*" ) { + if ( current === "*" ) { current = prev; // If no auto and dataTypes are actually different } else if ( prev !== "*" && prev !== current ) { @@ -6857,7 +7796,7 @@ function ajaxConvert( s, response ) { // If there is no direct converter, search transitively if ( !conv ) { conv2 = undefined; - for( conv1 in converters ) { + for ( conv1 in converters ) { tmp = conv1.split( " " ); if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) { conv2 = converters[ tmp[1] + " " + current ]; @@ -6891,7 +7830,7 @@ function ajaxConvert( s, response ) { var jsc = jQuery.now(), - jsre = /(\=)\?(&|$)|()\?\?()/i; + jsre = /(\=)\?(&|$)|\?\?/i; // Default jsonp settings jQuery.ajaxSetup({ @@ -6902,15 +7841,14 @@ jQuery.ajaxSetup({ }); // Detect, normalize options and install callbacks for jsonp requests -jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, dataIsString /* internal */ ) { +jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { - dataIsString = ( typeof s.data === "string" ); + var inspectData = s.contentType === "application/x-www-form-urlencoded" && + ( typeof s.data === "string" ); if ( s.dataTypes[ 0 ] === "jsonp" || - originalSettings.jsonpCallback || - originalSettings.jsonp != null || s.jsonp !== false && ( jsre.test( s.url ) || - dataIsString && jsre.test( s.data ) ) ) { + inspectData && jsre.test( s.data ) ) ) { var responseContainer, jsonpCallback = s.jsonpCallback = @@ -6923,7 +7861,7 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, dataIsString if ( s.jsonp !== false ) { url = url.replace( jsre, replace ); if ( s.url === url ) { - if ( dataIsString ) { + if ( inspectData ) { data = data.replace( jsre, replace ); } if ( s.data === data ) { @@ -6936,32 +7874,24 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, dataIsString s.url = url; s.data = data; + // Install callback window[ jsonpCallback ] = function( response ) { responseContainer = [ response ]; }; - s.complete = [ function() { - + // Clean-up function + jqXHR.always(function() { // Set callback back to previous value window[ jsonpCallback ] = previous; - // Call if it was a function and we have a response - if ( previous) { - if ( responseContainer && jQuery.isFunction( previous ) ) { - window[ jsonpCallback ] ( responseContainer[ 0 ] ); - } - } else { - // else, more memory leak avoidance - try{ - delete window[ jsonpCallback ]; - } catch( e ) {} + if ( responseContainer && jQuery.isFunction( previous ) ) { + window[ jsonpCallback ]( responseContainer[ 0 ] ); } - - }, s.complete ]; + }); // Use data converter to retrieve json after script execution s.converters["script json"] = function() { - if ( ! responseContainer ) { + if ( !responseContainer ) { jQuery.error( jsonpCallback + " was not called" ); } return responseContainer[ 0 ]; @@ -6973,7 +7903,7 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, dataIsString // Delegate to script return "script"; } -} ); +}); @@ -6981,10 +7911,10 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, dataIsString // Install script dataType jQuery.ajaxSetup({ accepts: { - script: "text/javascript, application/javascript" + script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" }, contents: { - script: /javascript/ + script: /javascript|ecmascript/ }, converters: { "text script": function( text ) { @@ -7003,7 +7933,7 @@ jQuery.ajaxPrefilter( "script", function( s ) { s.type = "GET"; s.global = false; } -} ); +}); // Bind script tag hack transport jQuery.ajaxTransport( "script", function(s) { @@ -7012,7 +7942,7 @@ jQuery.ajaxTransport( "script", function(s) { if ( s.crossDomain ) { var script, - head = document.getElementsByTagName( "head" )[ 0 ] || document.documentElement; + head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; return { @@ -7031,7 +7961,7 @@ jQuery.ajaxTransport( "script", function(s) { // Attach handlers for all browsers script.onload = script.onreadystatechange = function( _, isAbort ) { - if ( !script.readyState || /loaded|complete/.test( script.readyState ) ) { + if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { // Handle memory leak in IE script.onload = script.onreadystatechange = null; @@ -7062,22 +7992,33 @@ jQuery.ajaxTransport( "script", function(s) { } }; } -} ); +}); -var // Next active xhr id - xhrId = jQuery.now(), +var // #5280: Internet Explorer will keep connections alive if we don't abort on unload + xhrOnUnloadAbort = window.ActiveXObject ? function() { + // Abort all pending requests + for ( var key in xhrCallbacks ) { + xhrCallbacks[ key ]( 0, 1 ); + } + } : false, + xhrId = 0, + xhrCallbacks; - // active xhrs - xhrs = {}, +// Functions to create xhrs +function createStandardXHR() { + try { + return new window.XMLHttpRequest(); + } catch( e ) {} +} - // #5280: see below - xhrUnloadAbortInstalled, - - // XHR used to determine supports properties - testXHR; +function createActiveXHR() { + try { + return new window.ActiveXObject( "Microsoft.XMLHTTP" ); + } catch( e ) {} +} // Create the request object // (This is still attached to ajaxSettings for backward compatibility) @@ -7089,34 +8030,18 @@ jQuery.ajaxSettings.xhr = window.ActiveXObject ? * we need a fallback. */ function() { - if ( window.location.protocol !== "file:" ) { - try { - return new window.XMLHttpRequest(); - } catch( xhrError ) {} - } - - try { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } catch( activeError ) {} + return !this.isLocal && createStandardXHR() || createActiveXHR(); } : // For all other browsers, use the standard XMLHttpRequest object - function() { - return new window.XMLHttpRequest(); - }; + createStandardXHR; -// Test if we can create an xhr object -try { - testXHR = jQuery.ajaxSettings.xhr(); -} catch( xhrCreationException ) {} - -//Does this browser support XHR requests? -jQuery.support.ajax = !!testXHR; - -// Does this browser support crossDomain XHR requests -jQuery.support.cors = testXHR && ( "withCredentials" in testXHR ); - -// No need for the temporary xhr anymore -testXHR = undefined; +// Determine support properties +(function( xhr ) { + jQuery.extend( jQuery.support, { + ajax: !!xhr, + cors: !!xhr && ( "withCredentials" in xhr ) + }); +})( jQuery.ajaxSettings.xhr() ); // Create transport if the browser can provide an xhr if ( jQuery.support.ajax ) { @@ -7130,26 +8055,10 @@ if ( jQuery.support.ajax ) { return { send: function( headers, complete ) { - // #5280: we need to abort on unload or IE will keep connections alive - if ( !xhrUnloadAbortInstalled ) { - - xhrUnloadAbortInstalled = 1; - - jQuery(window).bind( "unload", function() { - - // Abort all pending requests - jQuery.each( xhrs, function( _, xhr ) { - if ( xhr.onreadystatechange ) { - xhr.onreadystatechange( 1 ); - } - } ); - - } ); - } - // Get a new xhr var xhr = s.xhr(), - handle; + handle, + i; // Open the socket // Passing null username, generates a login popup on Opera (#2865) @@ -7159,19 +8068,32 @@ if ( jQuery.support.ajax ) { xhr.open( s.type, s.url, s.async ); } - // Requested-With header - // Not set for crossDomain requests with no content - // (see why at http://trac.dojotoolkit.org/ticket/9486) - // Won't change header if already provided - if ( !( s.crossDomain && !s.hasContent ) && !headers["x-requested-with"] ) { - headers[ "x-requested-with" ] = "XMLHttpRequest"; + // Apply custom fields if provided + if ( s.xhrFields ) { + for ( i in s.xhrFields ) { + xhr[ i ] = s.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( s.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( s.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !s.crossDomain && !headers["X-Requested-With"] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; } // Need an extra try/catch for cross domain requests in Firefox 3 try { - jQuery.each( headers, function( key, value ) { - xhr.setRequestHeader( key, value ); - } ); + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } } catch( _ ) {} // Do send the request @@ -7182,74 +8104,80 @@ if ( jQuery.support.ajax ) { // Listener callback = function( _, isAbort ) { - // Was never called and is aborted or complete - if ( callback && ( isAbort || xhr.readyState === 4 ) ) { + var status, + statusText, + responseHeaders, + responses, + xml; - // Only called once - callback = 0; + // Firefox throws exceptions when accessing properties + // of an xhr when a network error occured + // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) + try { - // Do not keep as active anymore - if ( handle ) { - xhr.onreadystatechange = jQuery.noop; - delete xhrs[ handle ]; - } + // Was never called and is aborted or complete + if ( callback && ( isAbort || xhr.readyState === 4 ) ) { - // If it's an abort - if ( isAbort ) { - // Abort it manually if needed - if ( xhr.readyState !== 4 ) { - xhr.abort(); + // Only called once + callback = undefined; + + // Do not keep as active anymore + if ( handle ) { + xhr.onreadystatechange = jQuery.noop; + if ( xhrOnUnloadAbort ) { + delete xhrCallbacks[ handle ]; + } } - } else { - // Get info - var status = xhr.status, - statusText, - responseHeaders = xhr.getAllResponseHeaders(), - responses = {}, + + // If it's an abort + if ( isAbort ) { + // Abort it manually if needed + if ( xhr.readyState !== 4 ) { + xhr.abort(); + } + } else { + status = xhr.status; + responseHeaders = xhr.getAllResponseHeaders(); + responses = {}; xml = xhr.responseXML; - // Construct response list - if ( xml && xml.documentElement /* #4958 */ ) { - responses.xml = xml; + // Construct response list + if ( xml && xml.documentElement /* #4958 */ ) { + responses.xml = xml; + } + responses.text = xhr.responseText; + + // Firefox throws an exception when accessing + // statusText for faulty cross-domain requests + try { + statusText = xhr.statusText; + } catch( e ) { + // We normalize with Webkit giving an empty statusText + statusText = ""; + } + + // Filter status for non standard behaviors + + // If the request is local and we have data: assume a success + // (success with no data won't get notified, that's the best we + // can do given current implementations) + if ( !status && s.isLocal && !s.crossDomain ) { + status = responses.text ? 200 : 404; + // IE - #1450: sometimes returns 1223 when it should be 204 + } else if ( status === 1223 ) { + status = 204; + } } - responses.text = xhr.responseText; - - // Firefox throws an exception when accessing - // statusText for faulty cross-domain requests - try { - statusText = xhr.statusText; - } catch( e ) { - // We normalize with Webkit giving an empty statusText - statusText = ""; - } - - // Filter status for non standard behaviours - status = - // Opera returns 0 when it should be 304 - // Webkit returns 0 for failing cross-domain no matter the real status - status === 0 ? - ( - // Webkit, Firefox: filter out faulty cross-domain requests - !s.crossDomain || statusText ? - ( - // Opera: filter out real aborts #6060 - responseHeaders ? - 304 : - 0 - ) : - // We assume 302 but could be anything cross-domain related - 302 - ) : - ( - // IE sometimes returns 1223 when it should be 204 (see #1450) - status == 1223 ? - 204 : - status - ); - - // Call complete - complete( status, statusText, responses, responseHeaders ); } + } catch( firefoxAccessException ) { + if ( !isAbort ) { + complete( -1, firefoxAccessException ); + } + } + + // Call complete if needed + if ( responses ) { + complete( status, statusText, responses, responseHeaders ); } }; @@ -7259,9 +8187,17 @@ if ( jQuery.support.ajax ) { if ( !s.async || xhr.readyState === 4 ) { callback(); } else { - // Add to list of active xhrs - handle = xhrId++; - xhrs[ handle ] = xhr; + handle = ++xhrId; + if ( xhrOnUnloadAbort ) { + // Create the active xhrs callbacks list if needed + // and attach the unload handler + if ( !xhrCallbacks ) { + xhrCallbacks = {}; + jQuery( window ).unload( xhrOnUnloadAbort ); + } + // Add to list of active xhrs callbacks + xhrCallbacks[ handle ] = callback; + } xhr.onreadystatechange = callback; } }, @@ -7280,6 +8216,7 @@ if ( jQuery.support.ajax ) { var elemdisplay = {}, + iframe, iframeDoc, rfxtypes = /^(?:toggle|show|hide)$/, rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i, timerId, @@ -7290,42 +8227,49 @@ var elemdisplay = {}, [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ], // opacity animations [ "opacity" ] - ]; + ], + fxNow; jQuery.fn.extend({ show: function( speed, easing, callback ) { var elem, display; if ( speed || speed === 0 ) { - return this.animate( genFx("show", 3), speed, easing, callback); + return this.animate( genFx("show", 3), speed, easing, callback ); } else { for ( var i = 0, j = this.length; i < j; i++ ) { - elem = this[i]; - display = elem.style.display; + elem = this[ i ]; - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if ( !jQuery._data(elem, "olddisplay") && display === "none" ) { - display = elem.style.display = ""; - } + if ( elem.style ) { + display = elem.style.display; - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if ( display === "" && jQuery.css( elem, "display" ) === "none" ) { - jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName)); + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !jQuery._data(elem, "olddisplay") && display === "none" ) { + display = elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( display === "" && jQuery.css(elem, "display") === "none" ) { + jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); + } } } // Set the display of most of the elements in a second loop // to avoid the constant reflow for ( i = 0; i < j; i++ ) { - elem = this[i]; - display = elem.style.display; + elem = this[ i ]; - if ( display === "" || display === "none" ) { - elem.style.display = jQuery._data(elem, "olddisplay") || ""; + if ( elem.style ) { + display = elem.style.display; + + if ( display === "" || display === "none" ) { + elem.style.display = jQuery._data( elem, "olddisplay" ) || ""; + } } } @@ -7338,18 +8282,27 @@ jQuery.fn.extend({ return this.animate( genFx("hide", 3), speed, easing, callback); } else { - for ( var i = 0, j = this.length; i < j; i++ ) { - var display = jQuery.css( this[i], "display" ); + var elem, display, + i = 0, + j = this.length; - if ( display !== "none" && !jQuery._data( this[i], "olddisplay" ) ) { - jQuery._data( this[i], "olddisplay", display ); + for ( ; i < j; i++ ) { + elem = this[i]; + if ( elem.style ) { + display = jQuery.css( elem, "display" ); + + if ( display !== "none" && !jQuery._data( elem, "olddisplay" ) ) { + jQuery._data( elem, "olddisplay", display ); + } } } // Set the display of the elements in a second loop // to avoid the constant reflow for ( i = 0; i < j; i++ ) { - this[i].style.display = "none"; + if ( this[i].style ) { + this[i].style.display = "none"; + } } return this; @@ -7384,35 +8337,57 @@ jQuery.fn.extend({ }, animate: function( prop, speed, easing, callback ) { - var optall = jQuery.speed(speed, easing, callback); + var optall = jQuery.speed( speed, easing, callback ); if ( jQuery.isEmptyObject( prop ) ) { - return this.each( optall.complete ); + return this.each( optall.complete, [ false ] ); } - return this[ optall.queue === false ? "each" : "queue" ](function() { + // Do not change referenced properties as per-property easing will be lost + prop = jQuery.extend( {}, prop ); + + function doAnimation() { // XXX 'this' does not always have a nodeName when running the // test suite - var opt = jQuery.extend({}, optall), p, + if ( optall.queue === false ) { + jQuery._mark( this ); + } + + var opt = jQuery.extend( {}, optall ), isElement = this.nodeType === 1, hidden = isElement && jQuery(this).is(":hidden"), - self = this; + name, val, p, e, + parts, start, end, unit, + method; + + // will store per property easing and be used to determine when an animation is complete + opt.animatedProperties = {}; for ( p in prop ) { - var name = jQuery.camelCase( p ); + // property name normalization + name = jQuery.camelCase( p ); if ( p !== name ) { prop[ name ] = prop[ p ]; delete prop[ p ]; - p = name; } - if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) { - return opt.complete.call(this); + val = prop[ name ]; + + // easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default) + if ( jQuery.isArray( val ) ) { + opt.animatedProperties[ name ] = val[ 1 ]; + val = prop[ name ] = val[ 0 ]; + } else { + opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing'; } - if ( isElement && ( p === "height" || p === "width" ) ) { + if ( val === "hide" && hidden || val === "show" && !hidden ) { + return opt.complete.call( this ); + } + + if ( isElement && ( name === "height" || name === "width" ) ) { // Make sure that nothing sneaks out // Record all 3 overflow attributes because IE does not // change the overflow attribute when overflowX and @@ -7420,66 +8395,60 @@ jQuery.fn.extend({ opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ]; // Set display property to inline-block for height/width - // animations on inline elements that are having width/height - // animated + // animations on inline elements that are having width/height animated if ( jQuery.css( this, "display" ) === "inline" && jQuery.css( this, "float" ) === "none" ) { - if ( !jQuery.support.inlineBlockNeedsLayout ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) { this.style.display = "inline-block"; } else { - var display = defaultDisplay(this.nodeName); - - // inline-level elements accept inline-block; - // block-level elements need to be inline with layout - if ( display === "inline" ) { - this.style.display = "inline-block"; - - } else { - this.style.display = "inline"; - this.style.zoom = 1; - } + this.style.zoom = 1; } } } - - if ( jQuery.isArray( prop[p] ) ) { - // Create (if needed) and add to specialEasing - (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1]; - prop[p] = prop[p][0]; - } } if ( opt.overflow != null ) { this.style.overflow = "hidden"; } - opt.curAnim = jQuery.extend({}, prop); + for ( p in prop ) { + e = new jQuery.fx( this, opt, p ); + val = prop[ p ]; - jQuery.each( prop, function( name, val ) { - var e = new jQuery.fx( self, opt, name ); + if ( rfxtypes.test( val ) ) { - if ( rfxtypes.test(val) ) { - e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop ); + // Tracks whether to show or hide based on private + // data attached to the element + method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 ); + if ( method ) { + jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" ); + e[ method ](); + } else { + e[ val ](); + } } else { - var parts = rfxnum.exec(val), - start = e.cur() || 0; + parts = rfxnum.exec( val ); + start = e.cur(); if ( parts ) { - var end = parseFloat( parts[2] ), - unit = parts[3] || "px"; + end = parseFloat( parts[2] ); + unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" ); // We need to compute starting value if ( unit !== "px" ) { - jQuery.style( self, name, (end || 1) + unit); - start = ((end || 1) / e.cur()) * start; - jQuery.style( self, name, start + unit); + jQuery.style( this, p, (end || 1) + unit); + start = ( (end || 1) / e.cur() ) * start; + jQuery.style( this, p, start + unit); } // If a +=/-= token was provided, we're doing a relative animation if ( parts[1] ) { - end = ((parts[1] === "-=" ? -1 : 1) * end) + start; + end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start; } e.custom( start, end, unit ); @@ -7488,48 +8457,94 @@ jQuery.fn.extend({ e.custom( start, val, "" ); } } - }); + } // For JS strict compliance return true; - }); + } + + return optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); }, - stop: function( clearQueue, gotoEnd ) { - var timers = jQuery.timers; - - if ( clearQueue ) { - this.queue([]); + stop: function( type, clearQueue, gotoEnd ) { + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); } - this.each(function() { - // go in reverse order so anything added to the queue during the loop is ignored - for ( var i = timers.length - 1; i >= 0; i-- ) { - if ( timers[i].elem === this ) { - if (gotoEnd) { - // force the next step to be the last - timers[i](true); - } + return this.each(function() { + var index, + hadTimers = false, + timers = jQuery.timers, + data = jQuery._data( this ); - timers.splice(i, 1); + // clear marker counters if we know they won't be + if ( !gotoEnd ) { + jQuery._unmark( true, this ); + } + + function stopQueue( elem, data, index ) { + var hooks = data[ index ]; + jQuery.removeData( elem, index, true ); + hooks.stop( gotoEnd ); + } + + if ( type == null ) { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && index.indexOf(".run") === index.length - 4 ) { + stopQueue( this, data, index ); + } + } + } else if ( data[ index = type + ".run" ] && data[ index ].stop ){ + stopQueue( this, data, index ); + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + if ( gotoEnd ) { + + // force the next step to be the last + timers[ index ]( true ); + } else { + timers[ index ].saveState(); + } + hadTimers = true; + timers.splice( index, 1 ); } } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( !( gotoEnd && hadTimers ) ) { + jQuery.dequeue( this, type ); + } }); - - // start the next in the queue if the last step wasn't forced - if ( !gotoEnd ) { - this.dequeue(); - } - - return this; } }); +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout( clearFxNow, 0 ); + return ( fxNow = jQuery.now() ); +} + +function clearFxNow() { + fxNow = undefined; +} + +// Generate parameters to create a standard animation function genFx( type, num ) { var obj = {}; - jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() { + jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() { obj[ this ] = type; }); @@ -7538,9 +8553,9 @@ function genFx( type, num ) { // Generate shortcuts for custom animations jQuery.each({ - slideDown: genFx("show", 1), - slideUp: genFx("hide", 1), - slideToggle: genFx("toggle", 1), + slideDown: genFx( "show", 1 ), + slideUp: genFx( "hide", 1 ), + slideToggle: genFx( "toggle", 1 ), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } @@ -7552,25 +8567,34 @@ jQuery.each({ jQuery.extend({ speed: function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend({}, speed) : { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { complete: fn || !fn && easing || jQuery.isFunction( speed ) && speed, duration: speed, - easing: fn && easing || easing && !jQuery.isFunction(easing) && easing + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing }; opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : - opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : jQuery.fx.speeds._default; + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } // Queueing opt.old = opt.complete; - opt.complete = function() { - if ( opt.queue !== false ) { - jQuery(this).dequeue(); - } + + opt.complete = function( noUnmark ) { if ( jQuery.isFunction( opt.old ) ) { opt.old.call( this ); } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } else if ( noUnmark !== false ) { + jQuery._unmark( this ); + } }; return opt; @@ -7581,7 +8605,7 @@ jQuery.extend({ return firstNum + diff * p; }, swing: function( p, n, firstNum, diff ) { - return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum; + return ( ( -Math.cos( p*Math.PI ) / 2 ) + 0.5 ) * diff + firstNum; } }, @@ -7592,9 +8616,7 @@ jQuery.extend({ this.elem = elem; this.prop = prop; - if ( !options.orig ) { - options.orig = {}; - } + options.orig = options.orig || {}; } }); @@ -7606,17 +8628,21 @@ jQuery.fx.prototype = { this.options.step.call( this.elem, this.now, this ); } - (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this ); + ( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this ); }, // Get the current size cur: function() { - if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) { + if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) { return this.elem[ this.prop ]; } - var r = parseFloat( jQuery.css( this.elem, this.prop ) ); - return r || 0; + var parsed, + r = jQuery.css( this.elem, this.prop ); + // Empty strings, null, undefined and "auto" are converted to 0, + // complex values such as "rotate(1rad)" are returned as is, + // simple values such as "10px" are parsed to Float. + return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed; }, // Start an animation from one number to another @@ -7624,34 +8650,45 @@ jQuery.fx.prototype = { var self = this, fx = jQuery.fx; - this.startTime = jQuery.now(); - this.start = from; + this.startTime = fxNow || createFxNow(); this.end = to; - this.unit = unit || this.unit || "px"; - this.now = this.start; + this.now = this.start = from; this.pos = this.state = 0; + this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" ); function t( gotoEnd ) { - return self.step(gotoEnd); + return self.step( gotoEnd ); } + t.queue = this.options.queue; t.elem = this.elem; + t.saveState = function() { + if ( self.options.hide && jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) { + jQuery._data( self.elem, "fxshow" + self.prop, self.start ); + } + }; if ( t() && jQuery.timers.push(t) && !timerId ) { - timerId = setInterval(fx.tick, fx.interval); + timerId = setInterval( fx.tick, fx.interval ); } }, // Simple 'show' function show: function() { + var dataShow = jQuery._data( this.elem, "fxshow" + this.prop ); + // Remember where we started, so that we can go back to it later - this.options.orig[this.prop] = jQuery.style( this.elem, this.prop ); + this.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop ); this.options.show = true; // Begin the animation - // Make sure that we start at a small width/height to avoid any - // flash of content - this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur()); + // Make sure that we start at a small width/height to avoid any flash of content + if ( dataShow !== undefined ) { + // This show is picking up where a previous hide or show left off + this.custom( this.cur(), dataShow ); + } else { + this.custom( this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur() ); + } // Start by showing the element jQuery( this.elem ).show(); @@ -7660,69 +8697,84 @@ jQuery.fx.prototype = { // Simple 'hide' function hide: function() { // Remember where we started, so that we can go back to it later - this.options.orig[this.prop] = jQuery.style( this.elem, this.prop ); + this.options.orig[ this.prop ] = jQuery._data( this.elem, "fxshow" + this.prop ) || jQuery.style( this.elem, this.prop ); this.options.hide = true; // Begin the animation - this.custom(this.cur(), 0); + this.custom( this.cur(), 0 ); }, // Each step of an animation step: function( gotoEnd ) { - var t = jQuery.now(), done = true; + var p, n, complete, + t = fxNow || createFxNow(), + done = true, + elem = this.elem, + options = this.options; - if ( gotoEnd || t >= this.options.duration + this.startTime ) { + if ( gotoEnd || t >= options.duration + this.startTime ) { this.now = this.end; this.pos = this.state = 1; this.update(); - this.options.curAnim[ this.prop ] = true; + options.animatedProperties[ this.prop ] = true; - for ( var i in this.options.curAnim ) { - if ( this.options.curAnim[i] !== true ) { + for ( p in options.animatedProperties ) { + if ( options.animatedProperties[ p ] !== true ) { done = false; } } if ( done ) { // Reset the overflow - if ( this.options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { - var elem = this.elem, - options = this.options; + if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { - jQuery.each( [ "", "X", "Y" ], function (index, value) { - elem.style[ "overflow" + value ] = options.overflow[index]; - } ); + jQuery.each( [ "", "X", "Y" ], function( index, value ) { + elem.style[ "overflow" + value ] = options.overflow[ index ]; + }); } // Hide the element if the "hide" operation was done - if ( this.options.hide ) { - jQuery(this.elem).hide(); + if ( options.hide ) { + jQuery( elem ).hide(); } // Reset the properties, if the item has been hidden or shown - if ( this.options.hide || this.options.show ) { - for ( var p in this.options.curAnim ) { - jQuery.style( this.elem, p, this.options.orig[p] ); + if ( options.hide || options.show ) { + for ( p in options.animatedProperties ) { + jQuery.style( elem, p, options.orig[ p ] ); + jQuery.removeData( elem, "fxshow" + p, true ); + // Toggle data is no longer needed + jQuery.removeData( elem, "toggle" + p, true ); } } // Execute the complete function - this.options.complete.call( this.elem ); + // in the event that the complete function throws an exception + // we must ensure it won't be called twice. #5684 + + complete = options.complete; + if ( complete ) { + + options.complete = false; + complete.call( elem ); + } } return false; } else { - var n = t - this.startTime; - this.state = n / this.options.duration; - - // Perform the easing function, defaults to swing - var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop]; - var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear"); - this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration); - this.now = this.start + ((this.end - this.start) * this.pos); + // classical easing cannot be used with an Infinity duration + if ( options.duration == Infinity ) { + this.now = t; + } else { + n = t - this.startTime; + this.state = n / options.duration; + // Perform the easing function, defaults to swing + this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration ); + this.now = this.start + ( (this.end - this.start) * this.pos ); + } // Perform the next step of the animation this.update(); } @@ -7733,11 +8785,15 @@ jQuery.fx.prototype = { jQuery.extend( jQuery.fx, { tick: function() { - var timers = jQuery.timers; + var timer, + timers = jQuery.timers, + i = 0; - for ( var i = 0; i < timers.length; i++ ) { - if ( !timers[i]() ) { - timers.splice(i--, 1); + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); } } @@ -7767,7 +8823,7 @@ jQuery.extend( jQuery.fx, { _default: function( fx ) { if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) { - fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit; + fx.elem.style[ fx.prop ] = fx.now + fx.unit; } else { fx.elem[ fx.prop ] = fx.now; } @@ -7775,6 +8831,14 @@ jQuery.extend( jQuery.fx, { } }); +// Adds width/height step functions +// Do not set anything below 0 +jQuery.each([ "width", "height" ], function( i, prop ) { + jQuery.fx.step[ prop ] = function( fx ) { + jQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit ); + }; +}); + if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.animated = function( elem ) { return jQuery.grep(jQuery.timers, function( fn ) { @@ -7783,17 +8847,45 @@ if ( jQuery.expr && jQuery.expr.filters ) { }; } +// Try to restore the default display value of an element function defaultDisplay( nodeName ) { - if ( !elemdisplay[ nodeName ] ) { - var elem = jQuery("<" + nodeName + ">").appendTo("body"), - display = elem.css("display"); + if ( !elemdisplay[ nodeName ] ) { + + var body = document.body, + elem = jQuery( "<" + nodeName + ">" ).appendTo( body ), + display = elem.css( "display" ); elem.remove(); + // If the simple way fails, + // get element's real default display by attaching it to a temp iframe if ( display === "none" || display === "" ) { - display = "block"; + // No iframe to use yet, so create it + if ( !iframe ) { + iframe = document.createElement( "iframe" ); + iframe.frameBorder = iframe.width = iframe.height = 0; + } + + body.appendChild( iframe ); + + // Create a cacheable copy of the iframe document on first call. + // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML + // document to it; WebKit & Firefox won't allow reusing the iframe document. + if ( !iframeDoc || !iframe.createElement ) { + iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; + iframeDoc.write( ( document.compatMode === "CSS1Compat" ? "" : "" ) + "" ); + iframeDoc.close(); + } + + elem = iframeDoc.createElement( nodeName ); + + iframeDoc.body.appendChild( elem ); + + display = jQuery.css( elem, "display" ); + body.removeChild( iframe ); } + // Store the correct default display elemdisplay[ nodeName ] = display; } @@ -7840,8 +8932,8 @@ if ( "getBoundingClientRect" in document.documentElement ) { win = getWindow(doc), clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, - scrollTop = (win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop ), - scrollLeft = (win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft), + scrollTop = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop, + scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft, top = box.top + scrollTop - clientTop, left = box.left + scrollLeft - clientLeft; @@ -7866,8 +8958,6 @@ if ( "getBoundingClientRect" in document.documentElement ) { return jQuery.offset.bodyOffset( elem ); } - jQuery.offset.initialize(); - var computedStyle, offsetParent = elem.offsetParent, prevOffsetParent = elem, @@ -7880,7 +8970,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { left = elem.offsetLeft; while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { - if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { break; } @@ -7892,7 +8982,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { top += elem.offsetTop; left += elem.offsetLeft; - if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { + if ( jQuery.support.doesNotAddBorder && !(jQuery.support.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } @@ -7901,7 +8991,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { offsetParent = elem.offsetParent; } - if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { + if ( jQuery.support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } @@ -7914,7 +9004,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { left += body.offsetLeft; } - if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { top += Math.max( docElem.scrollTop, body.scrollTop ); left += Math.max( docElem.scrollLeft, body.scrollLeft ); } @@ -7924,47 +9014,12 @@ if ( "getBoundingClientRect" in document.documentElement ) { } jQuery.offset = { - initialize: function() { - var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.css(body, "marginTop") ) || 0, - html = "
"; - - jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } ); - - container.innerHTML = html; - body.insertBefore( container, body.firstChild ); - innerDiv = container.firstChild; - checkDiv = innerDiv.firstChild; - td = innerDiv.nextSibling.firstChild.firstChild; - - this.doesNotAddBorder = (checkDiv.offsetTop !== 5); - this.doesAddBorderForTableAndCells = (td.offsetTop === 5); - - checkDiv.style.position = "fixed"; - checkDiv.style.top = "20px"; - - // safari subtracts parent border width here which is 5px - this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15); - checkDiv.style.position = checkDiv.style.top = ""; - - innerDiv.style.overflow = "hidden"; - innerDiv.style.position = "relative"; - - this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5); - - this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop); - - body.removeChild( container ); - body = container = innerDiv = checkDiv = table = td = null; - jQuery.offset.initialize = jQuery.noop; - }, bodyOffset: function( body ) { var top = body.offsetTop, left = body.offsetLeft; - jQuery.offset.initialize(); - - if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) { + if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { top += parseFloat( jQuery.css(body, "marginTop") ) || 0; left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; } @@ -7984,26 +9039,28 @@ jQuery.offset = { curOffset = curElem.offset(), curCSSTop = jQuery.css( elem, "top" ), curCSSLeft = jQuery.css( elem, "left" ), - calculatePosition = (position === "absolute" && jQuery.inArray('auto', [curCSSTop, curCSSLeft]) > -1), + calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, props = {}, curPosition = {}, curTop, curLeft; - // need to be able to calculate position if either top or left is auto and position is absolute + // need to be able to calculate position if either top or left is auto and position is either absolute or fixed if ( calculatePosition ) { curPosition = curElem.position(); + curTop = curPosition.top; + curLeft = curPosition.left; + } else { + curTop = parseFloat( curCSSTop ) || 0; + curLeft = parseFloat( curCSSLeft ) || 0; } - curTop = calculatePosition ? curPosition.top : parseInt( curCSSTop, 10 ) || 0; - curLeft = calculatePosition ? curPosition.left : parseInt( curCSSLeft, 10 ) || 0; - if ( jQuery.isFunction( options ) ) { options = options.call( elem, i, curOffset ); } - if (options.top != null) { - props.top = (options.top - curOffset.top) + curTop; + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; } - if (options.left != null) { - props.left = (options.left - curOffset.left) + curLeft; + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; } if ( "using" in options ) { @@ -8016,6 +9073,7 @@ jQuery.offset = { jQuery.fn.extend({ + position: function() { if ( !this[0] ) { return null; @@ -8063,29 +9121,16 @@ jQuery.fn.extend({ jQuery.each( ["Left", "Top"], function( i, name ) { var method = "scroll" + name; - jQuery.fn[ method ] = function(val) { - var elem = this[0], win; + jQuery.fn[ method ] = function( val ) { + var elem, win; - if ( !elem ) { - return null; - } + if ( val === undefined ) { + elem = this[ 0 ]; - if ( val !== undefined ) { - // Set the scroll offset - return this.each(function() { - win = getWindow( this ); + if ( !elem ) { + return null; + } - if ( win ) { - win.scrollTo( - !i ? val : jQuery(win).scrollLeft(), - i ? val : jQuery(win).scrollTop() - ); - - } else { - this[ method ] = val; - } - }); - } else { win = getWindow( elem ); // Return the scroll offset @@ -8094,6 +9139,21 @@ jQuery.each( ["Left", "Top"], function( i, name ) { win.document.body[ method ] : elem[ method ]; } + + // Set the scroll offset + return this.each(function() { + win = getWindow( this ); + + if ( win ) { + win.scrollTo( + !i ? val : jQuery( win ).scrollLeft(), + i ? val : jQuery( win ).scrollTop() + ); + + } else { + this[ method ] = val; + } + }); }; }); @@ -8108,22 +9168,28 @@ function getWindow( elem ) { -// Create innerHeight, innerWidth, outerHeight and outerWidth methods +// Create width, height, innerHeight, innerWidth, outerHeight and outerWidth methods jQuery.each([ "Height", "Width" ], function( i, name ) { var type = name.toLowerCase(); // innerHeight and innerWidth - jQuery.fn["inner" + name] = function() { - return this[0] ? - parseFloat( jQuery.css( this[0], type, "padding" ) ) : + jQuery.fn[ "inner" + name ] = function() { + var elem = this[0]; + return elem ? + elem.style ? + parseFloat( jQuery.css( elem, type, "padding" ) ) : + this[ type ]() : null; }; // outerHeight and outerWidth - jQuery.fn["outer" + name] = function( margin ) { - return this[0] ? - parseFloat( jQuery.css( this[0], type, margin ? "margin" : "border" ) ) : + jQuery.fn[ "outer" + name ] = function( margin ) { + var elem = this[0]; + return elem ? + elem.style ? + parseFloat( jQuery.css( elem, type, margin ? "margin" : "border" ) ) : + this[ type ]() : null; }; @@ -8144,9 +9210,10 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { if ( jQuery.isWindow( elem ) ) { // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode // 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat - var docElemProp = elem.document.documentElement[ "client" + name ]; + var docElemProp = elem.document.documentElement[ "client" + name ], + body = elem.document.body; return elem.document.compatMode === "CSS1Compat" && docElemProp || - elem.document.body[ "client" + name ] || docElemProp; + body && body[ "client" + name ] || docElemProp; // Get document width or height } else if ( elem.nodeType === 9 ) { @@ -8162,7 +9229,7 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { var orig = jQuery.css( elem, type ), ret = parseFloat( orig ); - return jQuery.isNaN( ret ) ? orig : ret; + return jQuery.isNumeric( ret ) ? ret : orig; // Set the width or height on the element (default to pixels if value is unitless) } else { @@ -8173,4 +9240,27 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { }); -})(window); + + +// Expose jQuery to the global object +window.jQuery = window.$j = jQuery; + +// Expose jQuery as an AMD module, but only for AMD loaders that +// understand the issues with loading multiple versions of jQuery +// in a page that all might call define(). The loader will indicate +// they have special allowances for multiple jQuery versions by +// specifying define.amd.jQuery = true. Register as a named module, +// since jQuery can be concatenated with other files that may use define, +// but not use a proper concatenation script that understands anonymous +// AMD modules. A named AMD is safest and most robust way to register. +// Lowercase jquery is used because AMD module names are derived from +// file names, and jQuery is normally delivered in a lowercase file name. +// Do this after creating the global so that if an AMD module wants to call +// noConflict to hide this version of jQuery, it will work. +if ( typeof define === "function" && define.amd && define.amd.jQuery ) { + define( "jquery", [], function () { return jQuery; } ); +} + + + +})( window ); diff --git a/etemplate/js/test/test_dataview.html b/etemplate/js/test/test_dataview.html new file mode 100644 index 0000000000..353899aa68 --- /dev/null +++ b/etemplate/js/test/test_dataview.html @@ -0,0 +1,174 @@ + + + ET2 - Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

EGroupware ETemplate2 Dataview Test

+ +
+ + + 0-0 +
+ +
+
+ + + + +