Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Gallery Build Tag: gallery-2011.09.07-20-35

  • Loading branch information...
commit 59bab35338efbf39661992b5943d8541b4b6cb46 1 parent 179488a
YUI Builder authored
Showing with 5,674 additions and 83 deletions.
  1. +13 −0 build/gallery-bulkedit/assets/gallery-bulkedit-core.css
  2. 0  build/gallery-bulkedit/assets/skins/sam/gallery-bulkedit-skin.css
  3. +1 −0  build/gallery-bulkedit/assets/skins/sam/gallery-bulkedit.css
  4. +2,654 −0 build/gallery-bulkedit/gallery-bulkedit-debug.js
  5. +6 −0 build/gallery-bulkedit/gallery-bulkedit-min.js
  6. +2,651 −0 build/gallery-bulkedit/gallery-bulkedit.js
  7. +24 −6 build/gallery-form/gallery-form-debug.js
  8. +4 −4 build/gallery-form/gallery-form-min.js
  9. +21 −3 build/gallery-form/gallery-form.js
  10. +95 −6 build/gallery-makenode/gallery-makenode-debug.js
  11. +1 −1  build/gallery-makenode/gallery-makenode-min.js
  12. +95 −6 build/gallery-makenode/gallery-makenode.js
  13. +19 −14 build/gallery-model-sync-rest/gallery-model-sync-rest-debug.js
  14. +1 −1  build/gallery-model-sync-rest/gallery-model-sync-rest-min.js
  15. +19 −14 build/gallery-model-sync-rest/gallery-model-sync-rest.js
  16. +29 −8 build/gallery-model-sync-yql/gallery-model-sync-yql-debug.js
  17. +1 −1  build/gallery-model-sync-yql/gallery-model-sync-yql-min.js
  18. +29 −8 build/gallery-model-sync-yql/gallery-model-sync-yql.js
  19. +2 −2 build/gallery-querybuilder/gallery-querybuilder-debug.js
  20. +1 −1  build/gallery-querybuilder/gallery-querybuilder-min.js
  21. +2 −2 build/gallery-querybuilder/gallery-querybuilder.js
  22. +2 −2 build/gallery-treeble/gallery-treeble-debug.js
  23. +2 −2 build/gallery-treeble/gallery-treeble-min.js
  24. +2 −2 build/gallery-treeble/gallery-treeble.js
View
13 build/gallery-bulkedit/assets/gallery-bulkedit-core.css
@@ -0,0 +1,13 @@
+.yui3-htmltablebulkedit .yui3-bulkedit-record-message-container
+{
+ display: none;
+}
+
+.yui3-htmltablebulkedit .bulkedit-hasrecorderror .yui3-bulkedit-record-message-container,
+.yui3-htmltablebulkedit .bulkedit-hasrecordwarn .yui3-bulkedit-record-message-container,
+.yui3-htmltablebulkedit .bulkedit-hasrecordsuccess .yui3-bulkedit-record-message-container,
+.yui3-htmltablebulkedit .bulkedit-hasrecordinfo .yui3-bulkedit-record-message-container
+{
+ display: table-row;
+ *display: block;
+}
View
0  build/gallery-bulkedit/assets/skins/sam/gallery-bulkedit-skin.css
No changes.
View
1  build/gallery-bulkedit/assets/skins/sam/gallery-bulkedit.css
@@ -0,0 +1 @@
+.yui3-htmltablebulkedit .yui3-bulkedit-record-message-container{display:none}.yui3-htmltablebulkedit .bulkedit-hasrecorderror .yui3-bulkedit-record-message-container,.yui3-htmltablebulkedit .bulkedit-hasrecordwarn .yui3-bulkedit-record-message-container,.yui3-htmltablebulkedit .bulkedit-hasrecordsuccess .yui3-bulkedit-record-message-container,.yui3-htmltablebulkedit .bulkedit-hasrecordinfo .yui3-bulkedit-record-message-container{display:table-row;*display:block}
View
2,654 build/gallery-bulkedit/gallery-bulkedit-debug.js
@@ -0,0 +1,2654 @@
+YUI.add('gallery-bulkedit', function(Y) {
+
+"use strict";
+
+/**
+ * @module gallery-bulkedit
+ */
+
+/**********************************************************************
+ * <p>BulkEditDataSource manages a YUI DataSource + diffs (insertions,
+ * removals, changes to values).</p>
+ *
+ * <p>The YUI DataSource must be immutable, e.g., if it is an XHR
+ * datasource, the data must not change.</p>
+ *
+ * <p>By using a DataSource, we can support both client-side pagination
+ * (all data pre-loaded, best-effort save allowed) and server-side
+ * pagination (load data when needed, only all-or-nothing save allowed).
+ * Server-side pagination is useful when editing a large amount of existing
+ * records or after uploading a large number of new records. (Store the new
+ * records in a scratch space, so everything does not have to be sent back
+ * to the client after parsing.) In the case of bulk upload, server-side
+ * validation will catch errors in unviewed records.</p>
+ *
+ * <p>The responseSchema passed to the YUI DataSource must include a
+ * comparator for each field that should not be treated like a string.
+ * This comparator can either be 'integer', 'decimal', or a function which
+ * takes two arguments.</p>
+ *
+ * @class BulkEditDataSource
+ * @extends DataSource.Local
+ * @constructor
+ * @param config {Object}
+ */
+function BulkEditDataSource()
+{
+ BulkEditDataSource.superclass.constructor.apply(this, arguments);
+}
+
+BulkEditDataSource.NAME = "bulkEditDataSource";
+
+BulkEditDataSource.ATTRS =
+{
+ /**
+ * REQUIRED. The original data. This must be immutable, i.e., the
+ * values must not change.
+ *
+ * @config ds
+ * @type {DataSource}
+ * @writeonce
+ */
+ ds:
+ {
+ writeOnce: true
+ },
+
+ /**
+ * REQUIRED. The function to convert the initial request into a
+ * request usable by the underlying DataSource. This function takes
+ * one argument: state (startIndex,resultCount,...).
+ *
+ * @config generateRequest
+ * @type {Function}
+ * @writeonce
+ */
+ generateRequest:
+ {
+ validator: Y.Lang.isFunction,
+ writeOnce: true
+ },
+
+ /**
+ * REQUIRED. The name of the key in each record that stores an
+ * identifier which is unique across the entire data set.
+ *
+ * @config uniqueIdKey
+ * @type {String}
+ * @writeonce
+ */
+ uniqueIdKey:
+ {
+ validator: Y.Lang.isString,
+ writeOnce: true
+ },
+
+ /**
+ * The function to call to generate a unique id for a new record. The
+ * default generates "bulk-edit-new-id-#".
+ *
+ * @config generateUniqueId
+ * @type {Function}
+ * @writeonce
+ */
+ generateUniqueId:
+ {
+ value: function()
+ {
+ idCounter++;
+ return uniqueIdPrefix + idCounter;
+ },
+ validator: Y.Lang.isFunction,
+ writeOnce: true
+ },
+
+ /**
+ * OGNL expression telling how to extract the startIndex from the
+ * received data, e.g., <code>.meta.startIndex</code>. If it is not
+ * provided, startIndex is always assumed to be zero.
+ *
+ * @config startIndexExpr
+ * @type {String}
+ * @writeonce
+ */
+ startIndexExpr:
+ {
+ validator: Y.Lang.isString,
+ writeOnce: true
+ },
+
+ /**
+ * OGNL expression telling where in the response to store the total
+ * number of records, e.g., <code>.meta.totalRecords</code>. This is
+ * only appropriate for DataSources that always return the entire data
+ * set.
+ *
+ * @config totalRecordsReturnExpr
+ * @type {String}
+ * @writeonce
+ */
+ totalRecordsReturnExpr:
+ {
+ validator: Y.Lang.isString,
+ writeOnce: true
+ },
+
+ /**
+ * REQUIRED. The function to call to extract the total number of
+ * records from the response.
+ *
+ * @config extractTotalRecords
+ * @type {Function}
+ * @writeonce
+ */
+ extractTotalRecords:
+ {
+ validator: Y.Lang.isFunction,
+ writeOnce: true
+ }
+};
+
+var uniqueIdPrefix = 'bulk-edit-new-id-',
+ idCounter = 0,
+
+ inserted_prefix = 'be-ds-i:',
+ inserted_re = /^be-ds-i:/,
+ removed_prefix = 'be-ds-r:',
+ removed_re = /^be-ds-r:/;
+
+BulkEditDataSource.comparator =
+{
+ 'string': function(a,b)
+ {
+ return (Y.Lang.trim(a.toString()) === Y.Lang.trim(b.toString()));
+ },
+
+ 'integer': function(a,b)
+ {
+ return (parseInt(a,10) === parseInt(b,10));
+ },
+
+ 'decimal': function(a,b)
+ {
+ return (parseFloat(a,10) === parseFloat(b,10));
+ },
+
+ 'boolean': function(a,b)
+ {
+ return (((a && b) || (!a && !b)) ? true : false);
+ }
+};
+
+function fromDisplayIndex(
+ /* int */ index)
+{
+ var count = -1;
+ for (var i=0; i<this._index.length; i++)
+ {
+ var j = this._index[i];
+ if (!removed_re.test(j))
+ {
+ count++;
+ if (count === index)
+ {
+ return i;
+ }
+ }
+ }
+
+ return false;
+}
+
+function adjustRequest()
+{
+ var r = this._callback.request;
+ this._callback.adjust =
+ {
+ origStart: r.startIndex,
+ origCount: r.resultCount
+ };
+
+ if (!this._index)
+ {
+ return;
+ }
+
+ // find index of first record to request
+
+ var start = Math.min(r.startIndex, this._index.length);
+ var end = 0;
+ for (var i=0; i<start; i++)
+ {
+ var j = this._index[i];
+ if (!inserted_re.test(j))
+ {
+ end++;
+ }
+
+ if (removed_re.test(j))
+ {
+ start++;
+ }
+ }
+
+ r.startIndex = end;
+
+ this._callback.adjust.indexStart = i;
+
+ // adjust number of records to request
+
+ var count = 0;
+ while (i < this._index.length && count < this._callback.adjust.origCount)
+ {
+ var j = this._index[i];
+ if (inserted_re.test(j))
+ {
+ r.resultCount--;
+ }
+
+ if (removed_re.test(j))
+ {
+ r.resultCount++;
+ }
+ else
+ {
+ count++;
+ }
+
+ i++;
+ }
+
+ this._callback.adjust.indexEnd = i;
+}
+
+function internalSuccess(e)
+{
+ if (!e.response || e.error ||
+ !Y.Lang.isArray(e.response.results))
+ {
+ internalFailure.apply(this, arguments);
+ return;
+ }
+
+ // synch response arrives before setting _tId
+
+ if (!Y.Lang.isUndefined(this._callback._tId) &&
+ e.tId !== this._callback._tId)
+ {
+ return; // cancelled request
+ }
+
+ this._callback.response = e.response;
+ checkFinished.call(this);
+}
+
+function internalFailure(e)
+{
+ if (e.tId === this._callback._tId)
+ {
+ this._callback.error = e.error;
+ this._callback.response = e.response;
+ this.fire('response', this._callback);
+ }
+}
+
+function checkFinished()
+{
+ if (this._generatingRequest || !this._callback.response)
+ {
+ return;
+ }
+
+ if (!this._fields)
+ {
+ this._fields = {};
+ Y.Array.each(this.get('ds').schema.get('schema').resultFields, function(value)
+ {
+ if (Y.Lang.isObject(value))
+ {
+ this._fields[ value.key ] = value;
+ }
+ },
+ this);
+ }
+
+ var response = {};
+ Y.mix(response, this._callback.response);
+ response.results = [];
+ response = Y.clone(response, true);
+
+ var dataStartIndex = 0;
+ if (this.get('startIndexExpr'))
+ {
+ eval('dataStartIndex=this._callback.response'+this.get('startIndexExpr'));
+ }
+
+ var startIndex = this._callback.request.startIndex - dataStartIndex;
+ response.results = this._callback.response.results.slice(startIndex, startIndex + this._callback.request.resultCount);
+
+ // insertions/removals
+
+ if (!this._index)
+ {
+ if (this.get('totalRecordsReturnExpr'))
+ {
+ eval('response'+this.get('totalRecordsReturnExpr')+'='+this._callback.response.results.length);
+ }
+ this._count = this.get('extractTotalRecords')(response);
+
+ this._index = [];
+ for (var i=0; i<this._count; i++)
+ {
+ this._index.push(i);
+ }
+ }
+ else
+ {
+ var adjust = this._callback.adjust;
+ for (var i=adjust.indexStart, k=0; i<adjust.indexEnd; i++, k++)
+ {
+ var j = this._index[i];
+ if (inserted_re.test(j))
+ {
+ var id = j.substr(inserted_prefix.length);
+ response.results.splice(k,0, Y.clone(this._new[id], true));
+ }
+ else if (removed_re.test(j))
+ {
+ response.results.splice(k,1);
+ k--;
+ }
+ }
+ }
+
+ // save results so we can refer to them later
+
+ this._records = [];
+ this._recordMap = {};
+ var uniqueIdKey = this.get('uniqueIdKey');
+
+ Y.Array.each(response.results, function(value)
+ {
+ var rec = Y.clone(value, true);
+ this._records.push(rec);
+ this._recordMap[ rec[ uniqueIdKey ] ] = rec;
+ },
+ this);
+
+ // merge in diffs
+
+ Y.Array.each(response.results, function(rec)
+ {
+ var diff = this._diff[ rec[ uniqueIdKey ] ];
+ if (diff)
+ {
+ Y.mix(rec, diff, true);
+ }
+ },
+ this);
+
+ this._callback.response = response;
+ this.fire('response', this._callback);
+}
+
+Y.extend(BulkEditDataSource, Y.DataSource.Local,
+{
+ initializer: function(config)
+ {
+ if (!(config.ds instanceof Y.DataSource.Local))
+ {
+ Y.error('BulkEditDataSource requires DataSource');
+ }
+
+ if (!config.generateRequest)
+ {
+ Y.error('BulkEditDataSource requires generateRequest function');
+ }
+
+ if (!config.uniqueIdKey)
+ {
+ Y.error('BulkEditDataSource requires uniqueIdKey configuration');
+ }
+
+ if (!config.extractTotalRecords)
+ {
+ Y.error('BulkEditDataSource requires extractTotalRecords function');
+ }
+
+ this._index = null;
+ this._count = 0;
+ this._new = {};
+ this._diff = {};
+ },
+
+ /**
+ * @return {boolean} true if the raw data is stored locally
+ * @protected
+ */
+ _dataIsLocal: function()
+ {
+ return (Y.Lang.isArray(this.get('ds').get('source')));
+ },
+
+ /**
+ * Flush the underlying datasource's cache.
+ *
+ * @protected
+ */
+ _flushCache: function()
+ {
+ var ds = this.get('ds');
+ if (ds.cache && Y.Lang.isFunction(ds.cache.flush))
+ {
+ ds.cache.flush();
+ }
+ },
+
+ /**
+ * Use this instead of any meta information in response.
+ *
+ * @return {Number} the total number of records
+ */
+ getRecordCount: function()
+ {
+ return this._count;
+ },
+
+ /**
+ * @return {Number} the records returned by the latest request
+ */
+ getCurrentRecords: function()
+ {
+ return this._records;
+ },
+
+ /**
+ * @return {Object} the records returned by the latest request, keyed by record id
+ */
+ getCurrentRecordMap: function()
+ {
+ return this._recordMap;
+ },
+
+ /**
+ * @param record_index {Number}
+ * @param key {String} field key
+ * @return {mixed} the value of the specified field in the specified record
+ */
+ getValue: function(
+ /* int */ record_index,
+ /* string */ key)
+ {
+ if (!this._dataIsLocal())
+ {
+ Y.error('BulkEditDataSource.getValue() can only be called when using a local datasource');
+ }
+
+ var j = fromDisplayIndex.call(this, record_index);
+ if (j === false)
+ {
+ return false;
+ }
+
+ j = this._index[j];
+ if (inserted_re.test(j))
+ {
+ var record_id = j.substr(inserted_prefix.length);
+ var record = this._new[ record_id ];
+ }
+ else
+ {
+ var record = this.get('ds').get('source')[j];
+ var record_id = record[ this.get('uniqueIdKey') ];
+ }
+
+ if (this._diff[ record_id ] &&
+ !Y.Lang.isUndefined(this._diff[ record_id ][ key ]))
+ {
+ return this._diff[ record_id ][ key ];
+ }
+ else
+ {
+ return record[key];
+ }
+ },
+
+ /**
+ * When using a remote datasource, this will include changes made to
+ * deleted records.
+ *
+ * @return {Object} map of all changed values, keyed by record id
+ */
+ getChanges: function()
+ {
+ return this._diff;
+ },
+
+ /**
+ * @return {Array} list of removed record indices, based on initial ordering
+ */
+ getRemovedRecordIndexes: function()
+ {
+ var list = [];
+ Y.Array.each(this._index, function(j)
+ {
+ if (removed_re.test(j))
+ {
+ list.push(parseInt(j.substr(removed_prefix.length), 10));
+ }
+ });
+
+ return list;
+ },
+
+ /**
+ * You must reload() the widget after calling this function!
+ *
+ * @param index {Number} insertion index
+ * @param record {Object|String} record to insert or id of record to clone
+ * @return {String} id of newly inserted record
+ * @protected
+ */
+ insertRecord: function(
+ /* int */ index,
+ /* object */ record)
+ {
+ this._count++;
+
+ var record_id = String(this.get('generateUniqueId')());
+
+ this._new[ record_id ] = {};
+ this._new[ record_id ][ this.get('uniqueIdKey') ] = record_id;
+
+ var j = fromDisplayIndex.call(this, index);
+ if (j === false)
+ {
+ j = this._index.length;
+ }
+ this._index.splice(j, 0, inserted_prefix+record_id);
+
+ if (record && !Y.Lang.isObject(record)) // clone existing record
+ {
+ var s = record.toString();
+ record = Y.clone(this._recordMap[s] || this._new[s], true);
+ var diff = this._diff[s];
+ if (record && diff)
+ {
+ Y.mix(record, diff, true);
+ }
+ }
+
+ if (record) // insert initial values into _diff
+ {
+ var uniqueIdKey = this.get('uniqueIdKey');
+ Y.Object.each(record, function(value, key)
+ {
+ if (key != uniqueIdKey)
+ {
+ this.updateValue(record_id, key, value);
+ }
+ },
+ this);
+ }
+
+ return record_id;
+ },
+
+ /**
+ * You must reload() the widget after calling this function!
+ *
+ * @param index {Number} index of record to remove
+ * @return {boolean} true if record was removed
+ * @protected
+ */
+ removeRecord: function(
+ /* int */ index)
+ {
+ var j = fromDisplayIndex.call(this, index);
+ if (j === false)
+ {
+ return false;
+ }
+
+ this._count--;
+
+ if (inserted_re.test(this._index[j]))
+ {
+ var record_id = this._index[j].substr(inserted_prefix.length);
+ delete this._new[ record_id ];
+ this._index.splice(j,1);
+ }
+ else
+ {
+ if (this._dataIsLocal())
+ {
+ var record_id = this.get('ds').get('source')[ this._index[j] ][ this.get('uniqueIdKey') ].toString();
+ }
+
+ this._index[j] = removed_prefix + this._index[j];
+ }
+
+ if (record_id)
+ {
+ delete this._diff[ record_id ];
+ }
+
+ return true;
+ },
+
+ /**
+ * Update a value in a record.
+ *
+ * @param record_id {String}
+ * @param key {String} field key
+ * @param value {String} new item value
+ * @protected
+ */
+ updateValue: function(
+ /* string */ record_id,
+ /* string */ key,
+ /* string */ value)
+ {
+ if (key == this.get('uniqueIdKey'))
+ {
+ Y.error('BulkEditDataSource.updateValue() does not allow changing the id for a record. Use BulkEditDataSource.updateRecordId() instead.');
+ }
+
+ record_id = record_id.toString();
+
+ var record = this._recordMap[ record_id ];
+ if (record && this._getComparator(key)(record[key] || '', value || ''))
+ {
+ if (this._diff[ record_id ])
+ {
+ delete this._diff[ record_id ][ key ];
+ }
+ }
+ else // might be new record
+ {
+ if (!this._diff[ record_id ])
+ {
+ this._diff[ record_id ] = {};
+ }
+ this._diff[ record_id ][ key ] = value;
+ }
+ },
+
+ /**
+ * @param key {String} field key
+ * @return {Function} comparator function for the given field
+ * @protected
+ */
+ _getComparator: function(
+ /* string */ key)
+ {
+ var f = (this._fields[key] && this._fields[key].comparator) || 'string';
+ if (Y.Lang.isFunction(f))
+ {
+ return f;
+ }
+ else if (BulkEditDataSource.comparator[f])
+ {
+ return BulkEditDataSource.comparator[f];
+ }
+ else
+ {
+ return BulkEditDataSource.comparator.string;
+ }
+ },
+
+ /**
+ * Merge changes into the underlying data, to flush diffs for a record.
+ * Only usable with DataSource.Local. When using best-effort save on
+ * the server, call this for each record that was successfully saved.
+ *
+ * @param record_id {String}
+ */
+ mergeChanges: function(
+ /* string */ record_id)
+ {
+ if (!this._dataIsLocal())
+ {
+ Y.error('BulkEditDataSource.mergeChanges() can only be called when using a local datasource');
+ }
+
+ record_id = record_id.toString();
+
+ function merge(rec)
+ {
+ if (rec[ this.get('uniqueIdKey') ].toString() === record_id)
+ {
+ var diff = this._diff[ record_id ];
+ if (diff)
+ {
+ Y.mix(rec, diff, true);
+ delete this._diff[ record_id ];
+ }
+ return true;
+ }
+ }
+
+ var found = false;
+ this._flushCache();
+
+ Y.Array.some(this.get('ds').get('source'), function(value)
+ {
+ if (merge.call(this, value))
+ {
+ found = true;
+ return true;
+ }
+ },
+ this);
+
+ if (!found)
+ {
+ Y.Object.some(this._new, function(value)
+ {
+ if (merge.call(this, value))
+ {
+ found = true;
+ return true;
+ }
+ },
+ this);
+ }
+ },
+
+ /**
+ * <p>Completely remove a record, from both the display and the
+ * underlying data. Only usable with DataSource.Local. When using
+ * best-effort save on the server, call this for each record that was
+ * successfully deleted.</p>
+ *
+ * <p>You must reload() the widget after calling this function!</p>
+ *
+ * @param record_id {String}
+ */
+ killRecord: function(
+ /* string */ record_id)
+ {
+ if (!this._dataIsLocal())
+ {
+ Y.error('BulkEditDataSource.killRecord() can only be called when using a local datasource');
+ }
+
+ record_id = record_id.toString();
+
+ function kill(rec)
+ {
+ if (rec[ this.get('uniqueIdKey') ].toString() === record_id)
+ {
+ var info = {};
+ this.recordIdToIndex(record_id, info);
+
+ var j = this._index[ info.internal_index ];
+ this._index.splice(info.internal_index, 1);
+ if (!inserted_re.test(j))
+ {
+ for (var i=info.internal_index; i<this._index.length; i++)
+ {
+ var k = this._index[i];
+ if (removed_re.test(k))
+ {
+ this._index[i] = removed_prefix +
+ (parseInt(k.substr(removed_prefix.length), 10)-1);
+ }
+ else if (!inserted_re.test(k))
+ {
+ this._index[i]--;
+ }
+ }
+ }
+
+ this._count--;
+ delete this._diff[ record_id ];
+ return true;
+ }
+ }
+
+ var found = false;
+ this._flushCache();
+
+ var data = this.get('ds').get('source');
+ Y.Array.some(data, function(value, i)
+ {
+ if (kill.call(this, value))
+ {
+ data.splice(i,1);
+ found = true;
+ return true;
+ }
+ },
+ this);
+
+ if (!found)
+ {
+ Y.Object.some(this._new, function(value, id)
+ {
+ if (kill.call(this, value))
+ {
+ delete this._new[id];
+ found = true;
+ return true;
+ }
+ },
+ this);
+ }
+ },
+
+ /**
+ * <p>Change the id of a record. Only usable with DataSource.Local.
+ * When using best-effort save on the server, call this for each newly
+ * created record that was successfully saved.</p>
+ *
+ * <p>You must reload() the widget after calling this function!</p>
+ *
+ * @param orig_record_id {String}
+ * @param new_record_id {String}
+ */
+ updateRecordId: function(
+ /* string */ orig_record_id,
+ /* string */ new_record_id)
+ {
+ if (!this._dataIsLocal())
+ {
+ Y.error('BulkEditDataSource.updateRecordId() can only be called when using a local datasource');
+ }
+
+ orig_record_id = orig_record_id.toString();
+ new_record_id = new_record_id.toString();
+
+ function update(rec)
+ {
+ if (rec[ this.get('uniqueIdKey') ].toString() === orig_record_id)
+ {
+ var info = {};
+ this.recordIdToIndex(orig_record_id, info);
+ var j = info.internal_index;
+ if (inserted_re.test(this._index[j]))
+ {
+ this._index[j] = inserted_prefix + new_record_id;
+ }
+
+ rec[ this.get('uniqueIdKey') ] = new_record_id;
+ if (this._diff[ orig_record_id ])
+ {
+ this._diff[ new_record_id ] = this._diff[ orig_record_id ];
+ delete this._diff[ orig_record_id ];
+ }
+ return true;
+ }
+ }
+
+ var found = false;
+ this._flushCache();
+
+ Y.Array.some(this.get('ds').get('source'), function(value)
+ {
+ if (update.call(this, value))
+ {
+ found = true;
+ return true;
+ }
+ },
+ this);
+
+ if (!found)
+ {
+ Y.Object.some(this._new, function(value, id)
+ {
+ if (update.call(this, value))
+ {
+ this._new[ new_record_id ] = value;
+ delete this._new[id];
+ found = true;
+ return true;
+ }
+ },
+ this);
+ }
+ },
+
+ /**
+ * Find the index of the given record id. Only usable with
+ * DataSource.Local.
+ *
+ * @param record_id {String}
+ * @return {Number} index or record or -1 if not found
+ */
+ recordIdToIndex: function(
+ /* string */ record_id,
+ /* object */ return_info)
+ {
+ if (!this._dataIsLocal())
+ {
+ Y.error('BulkEditDataSource.recordIdToIndex() can only be called when using a local datasource');
+ }
+
+ record_id = record_id.toString();
+
+ var records = this.get('ds').get('source');
+ var count = 0;
+ for (var i=0; i<this._index.length; i++)
+ {
+ var j = this._index[i];
+ var ins = inserted_re.test(j);
+ var del = removed_re.test(j);
+ if ((ins &&
+ j.substr(inserted_prefix.length) === record_id) ||
+ (!ins && !del &&
+ records[j][ this.get('uniqueIdKey') ].toString() === record_id))
+ {
+ if (return_info)
+ {
+ return_info.internal_index = i;
+ }
+ return count;
+ }
+
+ if (!del)
+ {
+ count++;
+ }
+ }
+
+ return -1;
+ },
+
+ /**
+ * Merges edits into data and returns result.
+ *
+ * @protected
+ */
+ _defRequestFn: function(e)
+ {
+ this._callback = e;
+ adjustRequest.call(this);
+
+ this._generatingRequest = true;
+
+ this._callback._tId = this.get('ds').sendRequest(
+ {
+ request: this.get('generateRequest')(this._callback.request),
+ callback:
+ {
+ success: Y.bind(internalSuccess, this),
+ failure: Y.bind(internalFailure, this)
+ }
+ });
+
+ this._generatingRequest = false;
+ checkFinished.call(this);
+ }
+});
+
+Y.BulkEditDataSource = BulkEditDataSource;
+/**********************************************************************
+ * A widget for editing many records at once.
+ *
+ * @module gallery-bulkedit
+ */
+
+/**
+ * <p>BulkEditor provides the basic structure for editing all the records
+ * in a BulkEditDataSource. The fields for editing a record are rendered
+ * into a "row". This could be a div, a tbody, or something else.</p>
+ *
+ * <p>All event handlers must be placed on the container, not individual
+ * DOM elements.</p>
+ *
+ * <p>Errors must be returned from the server in the order in which records
+ * are displayed. Because of this, when data is sent to the server:</p>
+ * <ul>
+ * <li>If the server knows the ordering, you can send the diffs. (Diffs are an unordered map, keyed on the record id.)</li>
+ * <li>If the server doesn't know the ordering, you must send all the data.</li>
+ * </ul>
+ *
+ * @class BulkEditor
+ * @extends Widget
+ * @constructor
+ * @param config {Object}
+ */
+function BulkEditor()
+{
+ BulkEditor.superclass.constructor.apply(this, arguments);
+}
+
+BulkEditor.NAME = "bulkedit";
+
+BulkEditor.ATTRS =
+{
+ /**
+ * @config ds
+ * @type {BulkEditDataSource}
+ * @writeonce
+ */
+ ds:
+ {
+ validator: function(value)
+ {
+ return (value instanceof BulkEditDataSource);
+ },
+ writeOnce: true
+ },
+
+ /**
+ * Configuration for each field: type (input|select|textarea), label,
+ * validation (css, regex, msg, fn; see
+ * gallery-formmgr-css-validation). Derived classes can require
+ * additional keys.
+ *
+ * @config fields
+ * @type {Object}
+ * @writeonce
+ */
+ fields:
+ {
+ validator: Y.Lang.isObject,
+ writeOnce: true
+ },
+
+ /**
+ * Paginator for switching between pages of records. BulkEditor
+ * expects it to be configured to display ValidationPageLinks, so the
+ * user can see which pages have errors that need to be fixed.
+ *
+ * @config paginator
+ * @type {Paginator}
+ * @writeonce
+ */
+ paginator:
+ {
+ validator: function(value)
+ {
+ return (value instanceof Y.Paginator);
+ },
+ writeOnce: true
+ },
+
+ /**
+ * Extra key/value pairs to pass in the DataSource request.
+ *
+ * @config requestExtra
+ * @type {Object}
+ * @writeonce
+ */
+ requestExtra:
+ {
+ value: {},
+ validator: Y.Lang.isObject,
+ writeOnce: true
+ },
+
+ /**
+ * CSS class used to temporarily highlight a record.
+ *
+ * @config pingClass
+ * @type {String}
+ * @default "yui3-bulkedit-ping"
+ */
+ pingClass:
+ {
+ value: Y.ClassNameManager.getClassName(BulkEditor.NAME, 'ping'),
+ validator: Y.Lang.isString
+ },
+
+ /**
+ * Duration in seconds that pingClass is applied to a record.
+ *
+ * @config pingTimeout
+ * @type {Number}
+ * @default 2
+ */
+ pingTimeout:
+ {
+ value: 2,
+ validator: Y.Lang.isNumber
+ }
+};
+
+/**
+ * @event notifyErrors
+ * @description Fires when widget-level validation messages need to be displayed.
+ * @param msgs {Array} the messages to display
+ */
+/**
+ * @event clearErrorNotification
+ * @description Fires when widget-level validation messages should be cleared.
+ */
+
+var default_page_size = 1e9,
+
+ id_prefix = 'bulk-editor',
+ id_separator = '__',
+ id_regex = new RegExp('^' + id_prefix + id_separator + '(.+?)(?:' + id_separator + '(.+?))?$'),
+
+ field_container_class = Y.ClassNameManager.getClassName(BulkEditor.NAME, 'field-container'),
+ field_container_class_prefix = field_container_class + '-',
+ field_class_prefix = Y.ClassNameManager.getClassName(BulkEditor.NAME, 'field') + '-',
+
+ class_re_prefix = '(?:^|\\s)(?:',
+ class_re_suffix = ')(?:\\s|$)',
+
+ status_prefix = 'bulkedit-has',
+ status_pattern = status_prefix + '([a-z]+)',
+ status_re = new RegExp(class_re_prefix + status_pattern + class_re_suffix),
+
+ record_status_prefix = 'bulkedit-hasrecord',
+ record_status_pattern = record_status_prefix + '([a-z]+)',
+ record_status_re = new RegExp(class_re_prefix + record_status_pattern + class_re_suffix),
+
+ message_container_class = Y.ClassNameManager.getClassName(BulkEditor.NAME, 'message-text'),
+
+ perl_flags_regex = /^\(\?([a-z]+)\)/;
+
+BulkEditor.record_container_class = Y.ClassNameManager.getClassName(BulkEditor.NAME, 'bd');
+BulkEditor.record_msg_container_class = Y.ClassNameManager.getClassName(BulkEditor.NAME, 'record-message-container');
+
+function switchPage(state)
+{
+ this.saveChanges();
+
+ var pg = this.get('paginator');
+ pg.setTotalRecords(state.totalRecords, true);
+ pg.setStartIndex(state.recordOffset, true);
+ pg.setRowsPerPage(state.rowsPerPage, true);
+ pg.setPage(state.page, true);
+ this._updatePageStatus();
+ this.reload();
+}
+
+Y.extend(BulkEditor, Y.Widget,
+{
+ initializer: function(config)
+ {
+ if (config.paginator)
+ {
+ config.paginator.on('changeRequest', switchPage, this);
+ }
+ },
+
+ renderUI: function()
+ {
+ this.clearServerErrors();
+ this.reload();
+ },
+
+ /**
+ * Reloads the current page of records. This will erase any changes
+ * unsaved changes!
+ */
+ reload: function()
+ {
+ if (!this.busy)
+ {
+ this.plug(Y.Plugin.BusyOverlay);
+ }
+ this.busy.show();
+
+ var pg = this.get('paginator');
+ var request =
+ {
+ startIndex: pg ? pg.getStartIndex() : 0,
+ resultCount: pg ? pg.getRowsPerPage() : default_page_size
+ };
+ Y.mix(request, this.get('requestExtra'));
+
+ var ds = this.get('ds');
+ ds.sendRequest(
+ {
+ request: request,
+ callback:
+ {
+ success: Y.bind(function(e)
+ {
+ this.busy.hide();
+ if (pg && pg.getStartIndex() >= ds.getRecordCount())
+ {
+ pg.setPage(pg.getPreviousPage());
+ return;
+ }
+
+ this._render(e.response);
+ this._updatePaginator(e.response);
+ this.scroll_to_index = -1;
+ },
+ this),
+
+ failure: Y.bind(function()
+ {
+ Y.log('error loading data in BulkEditor', 'error');
+
+ this.busy.hide();
+ this.scroll_to_index = -1;
+ },
+ this)
+ }
+ });
+ },
+
+ /**
+ * Save the modified values from the current page of records.
+ */
+ saveChanges: function()
+ {
+ var ds = this.get('ds');
+ var records = ds.getCurrentRecords();
+ var id_key = ds.get('uniqueIdKey');
+ Y.Object.each(this.get('fields'), function(value, key)
+ {
+ Y.Array.each(records, function(r)
+ {
+ var node = this.getFieldElement(r, key);
+ ds.updateValue(r[ id_key ], key, node.get('value'));
+ },
+ this);
+ },
+ this);
+ },
+
+ /**
+ * Retrieve *all* the data. Do not call this if you use server-side
+ * pagination.
+ *
+ * @param callback {Object} callback object which will be invoked by DataSource
+ */
+ getAllValues: function(callback)
+ {
+ var request =
+ {
+ startIndex: 0,
+ resultCount: this.get('ds').getRecordCount()
+ };
+ Y.mix(request, this.get('requestExtra'));
+
+ this.get('ds').sendRequest(
+ {
+ request: request,
+ callback: callback
+ });
+ },
+
+ /**
+ * @return {Object} map of all changed values, keyed by record id
+ */
+ getChanges: function()
+ {
+ return this.get('ds').getChanges();
+ },
+
+ /**
+ * <p>Insert a new record.</p>
+ *
+ * <p>You must reload() the widget after calling this function!</p>
+ *
+ * @param index {Number} insertion index
+ * @param record {Object|String} record to insert or id of record to clone
+ * @return {String} the new record's id
+ */
+ insertRecord: function(
+ /* int */ index,
+ /* object */ record)
+ {
+ var record_id = this.get('ds').insertRecord(index, record);
+ if (index <= this.server_errors.records.length)
+ {
+ this.server_errors.records.splice(index,0, { id: record_id });
+ // leave entry in record_map undefined
+ this._updatePageStatus();
+ }
+ return record_id;
+ },
+
+ /**
+ * <p>Remove a record. The removal will be recorded in the diffs.
+ * There is no way to un-remove a record, so if you need that
+ * functionality, you may want to use highlighting to indicate removed
+ * records instead.</p>
+ *
+ * <p>You must reload() the widget after calling this function!</p>
+ *
+ * @param index {Number}
+ * @return {Boolean} true if the record was successfully removed
+ */
+ removeRecord: function(
+ /* int */ index)
+ {
+ if (this.get('ds').removeRecord(index))
+ {
+ if (index < this.server_errors.records.length)
+ {
+ var rec = this.server_errors.records[index];
+ this.server_errors.records.splice(index,1);
+ delete this.server_errors.record_map[ rec[ this.get('ds').get('uniqueIdKey') ] ];
+ this._updatePageStatus();
+ }
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ },
+
+ /**
+ * @param key {String} field key
+ * @return {Object} field configuration
+ */
+ getFieldConfig: function(
+ /* string */ key)
+ {
+ return this.get('fields')[key] || {};
+ },
+
+ /**
+ * @param record {String|Object} record id or record object
+ * @return {String} id of DOM element containing the record's input elements
+ */
+ getRecordContainerId: function(
+ /* string/object */ record)
+ {
+ if (Y.Lang.isString(record))
+ {
+ return id_prefix + id_separator + record;
+ }
+ else
+ {
+ return id_prefix + id_separator + record[ this.get('ds').get('uniqueIdKey') ];
+ }
+ },
+
+ /**
+ * @param record {String|Object} record id or record object
+ * @param key {String} field key
+ * @return {String} id of DOM element containing the field's input element
+ */
+ getFieldId: function(
+ /* string/object */ record,
+ /* string */ key)
+ {
+ return this.getRecordContainerId(record) + id_separator + key;
+ },
+
+ /**
+ * @param key {String|Node} field key or field input element
+ * @return {Object} object containing record and field_key
+ */
+ getRecordAndFieldKey: function(
+ /* string/element */ field)
+ {
+ var m = id_regex.exec(Y.Lang.isString(field) ? field : field.get('id'));
+ if (m && m.length > 0)
+ {
+ return { record: this.get('ds').getCurrentRecordMap()[ m[1] ], field_key: m[2] };
+ }
+ },
+
+ /**
+ * @param obj {Object|Node} record object, record container, or any node inside record container
+ * @return {String} record id
+ */
+ getRecordId: function(
+ /* object/element */ obj)
+ {
+ if (Y.Lang.isObject(obj) && !(obj instanceof Y.Node))
+ {
+ return obj[ this.get('ds').get('uniqueIdKey') ];
+ }
+
+ var node = obj.getAncestorByClassName(BulkEditor.record_container_class, true);
+ if (node)
+ {
+ var m = id_regex.exec(node.get('id'));
+ if (m && m.length > 0)
+ {
+ return m[1];
+ }
+ }
+ },
+
+ /**
+ * @param record {String|Object|Node} record id, record object, record container, or any node inside record container
+ * @return {Node} node containing rendered record
+ */
+ getRecordContainer: function(
+ /* string/object/element */ record)
+ {
+ if (Y.Lang.isString(record))
+ {
+ var id = id_prefix + id_separator + record;
+ }
+ else if (record instanceof Y.Node)
+ {
+ return record.getAncestorByClassName(BulkEditor.record_container_class, true);
+ }
+ else // record object
+ {
+ var id = this.getRecordContainerId(record);
+ }
+
+ return Y.one('#'+id);
+ },
+
+ /**
+ * @param record {String|Object|Node} record id, record object, record container, or any node inside record container
+ * @param key {String} field key
+ * @return {Node} node containing rendered field
+ */
+ getFieldContainer: function(
+ /* string/object/element */ record,
+ /* string */ key)
+ {
+ var field = this.getFieldElement(record, key);
+ return field.getAncestorByClassName(field_container_class, true);
+ },
+
+ /**
+ * @param record {String|Object|Node} record id, record object, record container, or any node inside record container
+ * @param key {String} field key
+ * @return {Node} field's input element
+ */
+ getFieldElement: function(
+ /* string/object/element */ record,
+ /* string */ key)
+ {
+ if (record instanceof Y.Node)
+ {
+ record = this.getRecordId(record);
+ }
+ return Y.one('#'+this.getFieldId(record, key));
+ },
+
+ /**
+ * Paginate and/or scroll to make the specified record visible. Record
+ * is pinged to help the user find it.
+ *
+ * @param index {Number} record index
+ */
+ showRecordIndex: function(
+ /* int */ index)
+ {
+ if (index < 0 || this.get('ds').getRecordCount() <= index)
+ {
+ return;
+ }
+
+ var pg = this.get('paginator');
+ var start = pg ? pg.getStartIndex() : 0;
+ var count = pg ? pg.getRowsPerPage() : default_page_size;
+ if (start <= index && index < start+count)
+ {
+ var node = this.getRecordContainer(this.get('ds').getCurrentRecords()[ index - start ]);
+ node.scrollIntoView();
+ this.pingRecord(node);
+ }
+ else if (pg)
+ {
+ this.scroll_to_index = index;
+ pg.setPage(1 + Math.floor(index / count));
+ }
+ },
+
+ /**
+ * Paginate and/or scroll to make the specified record visible. Record
+ * is pinged to help the user find it.
+ *
+ * @param id {Number} record id
+ */
+ showRecordId: function(
+ /* string */ id)
+ {
+ var index = this.get('ds').recordIdToIndex(id);
+ if (index >= 0)
+ {
+ this.showRecordIndex(index);
+ }
+ },
+
+ /**
+ * Apply a class to the DOM element containing the record for a short
+ * while. Your CSS can use this class to highlight the record in some
+ * way.
+ *
+ * @param record {String|Object|Node} record id, record object, record container, or any node inside record container
+ */
+ pingRecord: function(
+ /* string/object/element */ record)
+ {
+ var ping = this.get('pingClass');
+ if (ping)
+ {
+ var node = this.getRecordContainer(record);
+ node.addClass(ping);
+ Y.later(this.get('pingTimeout')*1000, null, function()
+ {
+ node.removeClass(ping);
+ });
+ }
+ },
+
+ /**
+ * Render the current page of records.
+ *
+ * @param response {Object} response from data source
+ * @protected
+ */
+ _render: function(response)
+ {
+ Y.log('_render', 'debug');
+
+ var container = this.get('contentBox');
+ Y.Event.purgeElement(container);
+ this._renderContainer(container);
+ container.set('scrollTop', 0);
+ container.set('scrollLeft', 0);
+
+ Y.Array.each(response.results, function(record)
+ {
+ var node = this._renderRecordContainer(container, record);
+ this._renderRecord(node, record);
+ },
+ this);
+
+ if (this.auto_validate)
+ {
+ this.validate();
+ }
+
+ if (this.scroll_to_index >= 0)
+ {
+ this.showRecordIndex(this.scroll_to_index);
+ this.scroll_to_index = -1;
+ }
+ },
+
+ /**
+ * Derived class should override to create a structure for the records.
+ *
+ * @param container {Node}
+ * @protected
+ */
+ _renderContainer: function(
+ /* element */ container)
+ {
+ container.set('innerHTML', '');
+ },
+
+ /**
+ * Derived class must override to create a container for the record.
+ *
+ * @param container {Node}
+ * @param record {Object} record data
+ * @protected
+ */
+ _renderRecordContainer: function(
+ /* element */ container,
+ /* object */ record)
+ {
+ return null;
+ },
+
+ /**
+ * Derived class can override if it needs to do more than just call
+ * _renderField() for each field.
+ *
+ * @param container {Node} record container
+ * @param record {Object} record data
+ * @protected
+ */
+ _renderRecord: function(
+ /* element */ container,
+ /* object */ record)
+ {
+ Y.Object.each(this.get('fields'), function(field, key)
+ {
+ this._renderField(
+ {
+ container: container,
+ key: key,
+ value: record[key],
+ field: field,
+ record: record
+ });
+ },
+ this);
+ },
+
+ /**
+ * If _renderRecord is not overridden, derived class must override this
+ * function to render the field.
+ *
+ * @param o {Object}
+ * container {Node} record container,
+ * key {String} field key,
+ * value {Mixed} field value,
+ * field {Object} field configuration,
+ * record {Object} record data
+ * @protected
+ */
+ _renderField: function(
+ /* object */ o)
+ {
+ },
+
+ /**
+ * Update the paginator to match the data source meta information.
+ *
+ * @param response {Object} response from DataSource
+ * @protected
+ */
+ _updatePaginator: function(response)
+ {
+ var pg = this.get('paginator');
+ if (pg)
+ {
+ pg.setTotalRecords(this.get('ds').getRecordCount(), true);
+ }
+ },
+
+ /**
+ * Clear errors received from the server. This clears all displayed
+ * messages.
+ */
+ clearServerErrors: function()
+ {
+ if (this.server_errors && this.server_errors.page &&
+ this.server_errors.page.length)
+ {
+ this.fire('clearErrorNotification');
+ }
+
+ this.server_errors =
+ {
+ page: [],
+ records: [],
+ record_map: {}
+ };
+
+ var pg = this.get('paginator');
+ if (pg)
+ {
+ pg.set('pageStatus', []);
+ }
+ this.first_error_page = -1;
+
+ this._clearValidationMessages();
+ },
+
+ /**
+ * Set page level, record level, and field level errors received from
+ * the server. A message can be either a string (assumed to be an
+ * error) or an object providing msg and type, where type can be
+ * 'error', 'warn', 'info', or 'success'.
+ *
+ * @param page_errors {Array} list of page-level error messages
+ * @param record_field_errors {Array} list of objects *in record display order*,
+ * each of which defines id (String), recordError (message),
+ * and fieldErrors (map of field keys to error messages)
+ */
+ setServerErrors: function(
+ /* array */ page_errors,
+ /* array */ record_field_errors)
+ {
+ if (this.server_errors.page.length &&
+ (!page_errors || !page_errors.length))
+ {
+ this.fire('clearErrorNotification');
+ }
+
+ this.server_errors =
+ {
+ page: page_errors || [],
+ records: record_field_errors || [],
+ record_map: {}
+ };
+
+ Y.Array.each(this.server_errors.records, function(r)
+ {
+ this.server_errors.record_map[ r.id ] = r;
+ },
+ this);
+
+ this._updatePageStatus();
+
+ var pg = this.get('paginator');
+ if (!pg || pg.getCurrentPage() === this.first_error_page)
+ {
+ this.validate();
+ }
+ else
+ {
+ this.auto_validate = true;
+ pg.setPage(this.first_error_page);
+ }
+ },
+
+ /**
+ * Update paginator to show which pages have errors.
+ *
+ * @protected
+ */
+ _updatePageStatus: function()
+ {
+ var pg = this.get('paginator');
+ if (!pg)
+ {
+ return;
+ }
+
+ var page_size = pg ? pg.getRowsPerPage() : default_page_size;
+ var status = this.page_status.slice(0);
+
+ this.first_error_page = -1;
+
+ var r = this.server_errors.records;
+ for (var i=0; i<r.length; i++)
+ {
+ if (r[i].recordError || r[i].fieldErrors)
+ {
+ var j = Math.floor(i / page_size);
+ status[j] = 'error';
+ if (this.first_error_page == -1)
+ {
+ this.first_error_page = i;
+ }
+ }
+ }
+
+ pg.set('pageStatus', status);
+ },
+
+ /**
+ * Validate the visible values (if using server-side pagination) or all
+ * the values (if using client-side pagination or no pagination).
+ *
+ * @return {Boolean} true if all checked values are acceptable
+ */
+ validate: function()
+ {
+ this.saveChanges();
+
+ this._clearValidationMessages();
+ this.auto_validate = true;
+
+ var status = this._validateVisibleFields();
+ var pg = this.get('paginator');
+ if (!status && pg)
+ {
+ this.page_status[ pg.getCurrentPage()-1 ] = 'error';
+ }
+
+ status = this._validateAllPages() && status; // status last to guarantee call
+
+ if (!status || this.server_errors.page.length ||
+ this.server_errors.records.length)
+ {
+ var err = this.server_errors.page.slice(0);
+ if (err.length === 0)
+ {
+ err.push(Y.FormManager.Strings.validation_error);
+ }
+ this.fire('notifyErrors', { msgs: err });
+
+ this.get('contentBox').getElementsByClassName(BulkEditor.record_container_class).some(function(node)
+ {
+ if (node.hasClass(status_pattern))
+ {
+ node.scrollIntoView();
+ return true;
+ }
+ });
+ }
+
+ this._updatePageStatus();
+ return status;
+ },
+
+ /**
+ * Validate the visible values.
+ *
+ * @param container {Node} if null, uses contentBox
+ * @return {Boolean} true if all checked values are acceptable
+ * @protected
+ */
+ _validateVisibleFields: function(
+ /* object */ container)
+ {
+ var status = true;
+
+ if (!container)
+ {
+ container = this.get('contentBox');
+ }
+
+ // fields
+
+ var e1 = container.getElementsByTagName('input');
+ var e2 = container.getElementsByTagName('textarea');
+ var e3 = container.getElementsByTagName('select');
+
+ Y.FormManager.cleanValues(e1);
+ Y.FormManager.cleanValues(e2);
+
+ status = this._validateElements(e1) && status; // status last to guarantee call
+ status = this._validateElements(e2) && status;
+ status = this._validateElements(e3) && status;
+
+ // records -- after fields, since field class regex would wipe out record class
+
+ container.getElementsByClassName(BulkEditor.record_container_class).each(function(node)
+ {
+ var id = this.getRecordId(node);
+ var err = this.server_errors.record_map[id];
+ if (err && err.recordError)
+ {
+ err = err.recordError;
+ if (Y.Lang.isString(err))
+ {
+ var msg = err;
+ var type = 'error';
+ }
+ else
+ {
+ var msg = err.msg;
+ var type = err.type;
+ }
+ this.displayRecordMessage(id, msg, type, false);
+ status = status && !(type == 'error' || type == 'warn');
+ }
+ },
+ this);
+
+ return status;
+ },
+
+ /**
+ * Validate the given elements.
+ *
+ * @param nodes {NodeList}
+ * @return {Boolean} true if all checked values are acceptable
+ * @protected
+ */
+ _validateElements: function(
+ /* array */ nodes)
+ {
+ var status = true;
+ nodes.each(function(node)
+ {
+ var field_info = this.getRecordAndFieldKey(node);
+ if (!field_info)
+ {
+ return;
+ }
+
+ var field = this.getFieldConfig(field_info.field_key);
+ var msg_list = field.validation && field.validation.msg;
+
+ var info = Y.FormManager.validateFromCSSData(node, msg_list);
+ if (info.error)
+ {
+ this.displayFieldMessage(node, info.error, 'error', false);
+ status = false;
+ return;
+ }
+
+ if (info.keepGoing)
+ {
+ if (field.validation && Y.Lang.isString(field.validation.regex))
+ {
+ var flags = '';
+ var m = perl_flags_regex.exec(field.validation.regex);
+ if (m && m.length == 2)
+ {
+ flags = m[1];
+ field.validation.regex = field.validation.regex.replace(perl_flags_regex, '');
+ }
+ field.validation.regex = new RegExp(field.validation.regex, flags);
+ }
+
+ if (field.validation &&
+ field.validation.regex instanceof RegExp &&
+ !field.validation.regex.test(node.get('value')))
+ {
+ this.displayFieldMessage(node, msg_list && msg_list.regex, 'error', false);
+ status = false;
+ return;
+ }
+ }
+
+ if (field.validation &&
+ Y.Lang.isFunction(field.validation.fn) &&
+ !field.validation.fn.call(this, node))
+ {
+ status = false;
+ return;
+ }
+
+ var err = this.server_errors.record_map[ this.getRecordId(field_info.record) ];
+ if (err && err.fieldErrors)
+ {
+ var f = err.fieldErrors[ field_info.field_key ];
+ if (f)
+ {
+ if (Y.Lang.isString(f))
+ {
+ var msg = f;
+ var type = 'error';
+ }
+ else
+ {
+ var msg = f.msg;
+ var type = f.type;
+ }
+ this.displayFieldMessage(node, msg, type, false);
+ status = status && !(type == 'error' || type == 'warn');
+ return;
+ }
+ }
+ },
+ this);
+
+ return status;
+ },
+
+ /**
+ * If the data is stored locally and we paginate, validate all of it
+ * and mark the pages that have invalid values.
+ *
+ * @return {Boolean} true if all checked values are acceptable
+ * @protected
+ */
+ _validateAllPages: function()
+ {
+ var ds = this.get('ds');
+ var pg = this.get('paginator');
+ if (!pg || !ds._dataIsLocal())
+ {
+ return true;
+ }
+
+ if (!this.validation_node)
+ {
+ this.validation_node = Y.Node.create('<input></input>');
+ }
+
+ if (!this.validation_keys)
+ {
+ this.validation_keys = [];
+ Y.Object.each(this.get('fields'), function(value, key)
+ {
+ if (value.validation)
+ {
+ this.validation_keys.push(key);
+ }
+ },
+ this);
+ }
+
+ var count = ds.getRecordCount();
+ var page_size = pg.getRowsPerPage();
+ for (var i=0; i<count; i++)
+ {
+ var status = true;
+ Y.Array.each(this.validation_keys, function(key)
+ {
+ var field = this.get('fields')[key];
+ var value = ds.getValue(i, key);
+
+ this.validation_node.set('value', Y.Lang.isUndefined(value) ? '' : value);
+ this.validation_node.set('className', field.validation.css || '');
+
+ var info = Y.FormManager.validateFromCSSData(this.validation_node);
+ if (info.error)
+ {
+ status = false;
+ return;
+ }
+
+ if (info.keepGoing)
+ {
+ if (field.validation.regex instanceof RegExp &&
+ !field.validation.regex.test(value))
+ {
+ status = false;
+ return;
+ }
+ }
+ },
+ this);
+
+ if (!status)
+ {
+ var j = Math.floor(i / page_size);
+ i = (j+1)*page_size - 1; // skip to next page
+
+ this.page_status[j] = 'error';
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Clear all displayed messages.
+ */
+ _clearValidationMessages: function()
+ {
+ this.has_validation_messages = false;
+ this.auto_validate = false;
+ this.page_status = [];
+
+ this.fire('clearErrorNotification');
+
+ var container = this.get('contentBox');
+
+ container.getElementsByClassName(status_pattern).removeClass(status_pattern);
+ container.getElementsByClassName(record_status_pattern).removeClass(record_status_pattern);
+ container.getElementsByClassName(message_container_class).set('innerHTML', '');
+ },
+
+ /**
+ * Display a message for the specified field.
+ *
+ * @param e {Node} field input element
+ * @param msg {String} message to display
+ * @param type {String} message type: error, warn, info, success
+ * @param scroll {Boolean} whether or not to scroll to the field
+ */
+ displayFieldMessage: function(
+ /* element */ e,
+ /* string */ msg,
+ /* string */ type,
+ /* boolean */ scroll)
+ {
+ if (Y.Lang.isUndefined(scroll))
+ {
+ scroll = !this.has_validation_messages;
+ }
+
+ var bd1 = this.getRecordContainer(e);
+ var changed = this._updateRecordStatus(bd1, type, status_pattern, status_re, status_prefix);
+
+ var bd2 = e.getAncestorByClassName(field_container_class);
+ if (Y.FormManager.statusTakesPrecedence(this._getElementStatus(bd2, status_re), type))
+ {
+ if (msg)
+ {
+ var m = bd2.getElementsByClassName(message_container_class);
+ if (m && m.size() > 0)
+ {
+ m.item(0).set('innerHTML', msg);
+ }
+ }
+
+ bd2.replaceClass(status_pattern, status_prefix + type);
+ this.has_validation_messages = true;
+ }
+
+ if (changed && scroll)
+ {
+ bd1.scrollIntoView();
+ }
+ },
+
+ /**
+ * Display a message for the specified record.
+ *
+ * @param id {String} record id
+ * @param msg {String} message to display
+ * @param type {String} message type: error, warn, info, success
+ * @param scroll {Boolean} whether or not to scroll to the field
+ */
+ displayRecordMessage: function(
+ /* string */ id,
+ /* string */ msg,
+ /* string */ type,
+ /* boolean */ scroll)
+ {
+ if (Y.Lang.isUndefined(scroll))
+ {
+ scroll = !this.has_validation_messages;
+ }
+
+ var bd1 = this.getRecordContainer(id);
+ var changed = this._updateRecordStatus(bd1, type, status_pattern, status_re, status_prefix);
+ if (this._updateRecordStatus(bd1, type, record_status_pattern, record_status_re, record_status_prefix) &&
+ msg) // msg last to guarantee call
+ {
+ var bd2 = bd1.getElementsByClassName(BulkEditor.record_msg_container_class).item(0);
+ if (bd2)
+ {
+ var m = bd2.getElementsByClassName(message_container_class);
+ if (m && m.size() > 0)
+ {
+ m.item(0).set('innerHTML', msg);
+ }
+ }
+ }
+
+ if (changed && scroll)
+ {
+ bd1.scrollIntoView();
+ }
+ },
+
+ /**
+ * @param n {Node}
+ * @param r {RegExp}
+ * @return {Mixed} status or false
+ * @protected
+ */
+ _getElementStatus: function(
+ /* Node */ n,
+ /* regex */ r)
+ {
+ var m = r.exec(n.get('className'));
+ return (m && m.length > 1 ? m[1] : false);
+ },
+
+ /**
+ * Update the status of the node, if the new status has higher precedence.
+ *
+ * @param bd {Node}
+ * @param type {String} new status
+ * @param p {String} pattern for extracting status
+ * @param r {RegExpr} regex for extracting status
+ * @param prefix {String} status prefix
+ * @return {Boolean} true if status was modified
+ */
+ _updateRecordStatus: function(
+ /* element */ bd,
+ /* string */ type,
+ /* string */ p,
+ /* regex */ r,
+ /* string */ prefix)
+ {
+ if (Y.FormManager.statusTakesPrecedence(this._getElementStatus(bd, r), type))
+ {
+ bd.replaceClass(p, prefix + type);
+ this.has_validation_messages = true;
+ return true;
+ }
+
+ return false;
+ }
+});
+
+//
+// Markup
+//
+
+function cleanHTML(s)
+{
+ if (!s)
+ {
+ return '';
+ }
+
+ return s.toString()
+ .replace(/<\/?script>/ig, '')
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+}
+
+BulkEditor.error_msg_markup = Y.Lang.sub('<div class="{c}"></div>',
+{
+ c: message_container_class
+});
+
+/**
+ * @method labelMarkup
+ * @static
+ * @param o {Object}
+ * key {String} field key,
+ * value {Mixed} field value,
+ * field {Object} field configuration,
+ * record {Object} record data
+ * @return {String} markup for the label of the specified field
+ */
+BulkEditor.labelMarkup = function(o)
+{
+ var label = '<label for="{id}">{label}</label>';
+
+ return Y.Lang.sub(label,
+ {
+ id: this.getFieldId(o.record, o.key),
+ label: o.field.label
+ });
+};
+
+/**
+ * Map of field type (input,select,textarea) to function that generates the
+ * required markup. You can add additional entries. Each function takes a
+ * single argument: an object defining
+ * key {String} field key,
+ * value {Mixed} field value,
+ * field {Object} field configuration,
+ * record {Object} record data
+ *
+ * @property Y.BulkEditor.markup
+ * @type {Object}
+ * @static
+ */
+BulkEditor.markup =
+{
+ input: function(o)
+ {
+ var input =
+ '<div class="{cont}{key}">' +
+ '{label}{msg1}' +
+ '<input type="text" id="{id}" value="{value}" class="{field}{key} {yiv}" />' +
+ '{msg2}' +
+ '</div>';
+
+ var label = o.field && o.field.label ? BulkEditor.labelMarkup.call(this, o) : '';
+
+ return Y.Lang.sub(input,
+ {
+ cont: field_container_class + ' ' + field_container_class_prefix,
+ field: field_class_prefix,
+ key: o.key,
+ id: this.getFieldId(o.record, o.key),
+ label: label,
+ value: cleanHTML(o.value),
+ yiv: (o.field && o.field.validation && o.field.validation.css) || '',
+ msg1: label ? BulkEditor.error_msg_markup : '',
+ msg2: label ? '' : BulkEditor.error_msg_markup
+ });
+ },
+
+ select: function(o)
+ {
+ var select =
+ '<div class="{cont}{key}">' +
+ '{label}{msg1}' +
+ '<select id="{id}" class="{field}{key}">{options}</select>' +
+ '{msg2}' +
+ '</div>';
+
+ var option = '<option value="{value}" {selected}>{text}</option>';
+
+ var options = '';
+ Y.Array.each(o.field.values, function(v)
+ {
+ options += Y.Lang.sub(option,
+ {
+ value: v.value,
+ text: cleanHTML(v.text),
+ selected: o.value && o.value.toString() === v.value ? 'selected' : ''
+ });
+ });
+
+ var label = o.field && o.field.label ? BulkEditor.labelMarkup.call(this, o) : '';
+
+ return Y.Lang.sub(select,
+ {
+ cont: field_container_class + ' ' + field_container_class_prefix,
+ field: field_class_prefix,
+ key: o.key,
+ id: this.getFieldId(o.record, o.key),
+ label: label,
+ options: options,
+ yiv: (o.field && o.field.validation && o.field.validation.css) || '',
+ msg1: label ? BulkEditor.error_msg_markup : '',
+ msg2: label ? '' : BulkEditor.error_msg_markup
+ });
+ },
+
+ textarea: function(o)
+ {
+ var textarea =
+ '<div class="{cont}{key}">' +
+ '{label}{msg1}' +
+ '<textarea id="{id}" class="satg-textarea-field {prefix}{key} {yiv}">{value}</textarea>' +
+ '{msg2}' +
+ '</div>';
+
+ var label = o.field && o.field.label ? BulkEditor.labelMarkup.call(this, o) : '';
+
+ return Y.Lang.sub(textarea,
+ {
+ cont: field_container_class + ' ' + field_container_class_prefix,
+ prefix: field_class_prefix,
+ key: o.key,
+ id: this.getFieldId(o.record, o.key),
+ label: label,
+ value: cleanHTML(o.value),
+ yiv: (o.field && o.field.validation && o.field.validation.css) || '',
+ msg1: label ? BulkEditor.error_msg_markup : '',
+ msg2: label ? '' : BulkEditor.error_msg_markup
+ });
+ }
+};
+
+/**
+ * @method fieldMarkup
+ * @static
+ * @param key {String} field key
+ * @param record {Object}
+ * @return {String} markup for the specified field
+ */
+BulkEditor.fieldMarkup = function(key, record)
+{
+ var field = this.getFieldConfig(key);
+ return BulkEditor.markup[ field.type || 'input' ].call(this,
+ {
+ key: key,
+ value: record[key],
+ field: field,
+ record: record
+ });
+};
+
+Y.BulkEditor = BulkEditor;
+/**
+ * @module gallery-bulkedit
+ */
+
+/**
+ * <p>HTMLTableBulkEditor builds an HTML table with one tbody for each
+ * record.</p>
+ *
+ * @class HTMLTableBulkEditor
+ * @extends BulkEditor
+ * @constructor
+ * @param config {Object}
+ */
+function HTMLTableBulkEditor()
+{
+ HTMLTableBulkEditor.superclass.constructor.apply(this, arguments);
+}
+
+HTMLTableBulkEditor.NAME = "htmltablebulkedit";
+
+HTMLTableBulkEditor.ATTRS =
+{
+ /**
+ * Configuration for each column: key, label, formatter.
+ *
+ * @config columns
+ * @type {Array}
+ * @writeonce
+ */
+ columns:
+ {
+ validator: Y.Lang.isObject,
+ writeOnce: true
+ },
+
+ /**
+ * <p>Array of event delegations that will be attached to the container
+ * via Y.delegate(). Each item is an object defining type, nodes, fn.
+ * The function will be called in the context of the BulkEditor
+ * instance.</p>
+ *
+ * <p>Attaching events to the container before the table is created does
+ * not work in all browsers.</p>
+ *
+ * @config events
+ * @type {Array}
+ * @writeonce
+ */
+ events:
+ {
+ validator: Y.Lang.isObject,
+ writeOnce: true
+ }
+};
+
+var cell_class = Y.ClassNameManager.getClassName(HTMLTableBulkEditor.NAME, 'cell'),
+ cell_class_prefix = cell_class + '-',
+ odd_class = Y.ClassNameManager.getClassName(HTMLTableBulkEditor.NAME, 'odd'),
+ even_class = Y.ClassNameManager.getClassName(HTMLTableBulkEditor.NAME, 'even'),
+ msg_class = Y.ClassNameManager.getClassName(HTMLTableBulkEditor.NAME, 'record-message'),
+ liner_class = Y.ClassNameManager.getClassName(HTMLTableBulkEditor.NAME, 'liner');
+
+/**
+ * Renders an input element in the cell.
+ *
+ * @method inputFormatter
+ * @static
+ * @param o {Object} cell, key, value, field, column, record
+ */
+HTMLTableBulkEditor.inputFormatter = function(o)
+{
+ o.cell.set('innerHTML', BulkEditor.markup.input.call(this, o));
+};
+
+/**
+ * Renders a textarea element in the cell.
+ *
+ * @method textareaFormatter
+ * @static
+ * @param o {Object} cell, key, value, field, column, record
+ */
+HTMLTableBulkEditor.textareaFormatter = function(o)
+{
+ o.cell.set('innerHTML', BulkEditor.markup.textarea.call(this, o));
+};
+
+/**
+ * Renders a select element in the cell.
+ *
+ * @method selectFormatter
+ * @static
+ * @param o {Object} cell, key, value, field, column, record
+ */
+HTMLTableBulkEditor.selectFormatter = function(o)
+{
+ o.cell.set('innerHTML', BulkEditor.markup.select.call(this, o));
+};
+
+/**
+ * Map of field type to cell formatter.
+ *
+ * @property Y.HTMLTableBulkEditor.defaults
+ * @type {Object}
+ * @static
+ */
+HTMLTableBulkEditor.defaults =
+{
+ input:
+ {
+ formatter: HTMLTableBulkEditor.inputFormatter
+ },
+
+ select:
+ {
+ formatter: HTMLTableBulkEditor.selectFormatter
+ },
+
+ textarea:
+ {
+ formatter: HTMLTableBulkEditor.textareaFormatter
+ }
+};
+
+function moveFocus(e)
+{
+ e.halt();
+
+ var info = this.getRecordAndFieldKey(e.target);
+ if (!info)
+ {
+ return;