1 /*global jQuery:true, GCN: true */
  2 (function (GCN) {
  3 
  4 	'use strict';
  5 
  6 	/**
  7 	 * @class
  8 	 * @name TagContainerAPI
  9 	 */
 10 	GCN.TagContainerAPI = GCN.defineChainback({
 11 		/** @lends TagContainerAPI */
 12 
 13 		/**
 14 		 * @private
 15 		 * @type {object<number, string>} Hash, mapping tag ids to their
 16 		 *                                corresponding names.
 17 		 */
 18 		_tagIdToNameMap: null,
 19 
 20 		/**
 21 		 * @private
 22 		 * @type {object<number, string>} Hash, mapping tag ids to their
 23 		 *                                corresponding names for newly created
 24 		 *                                tags.
 25 		 */
 26 		_createdTagIdToNameMap: {},
 27 
 28 		/**
 29 		 * @private
 30 		 * @type {Array.<object>} A set of blocks that are are to be removed
 31 		 *                        from this content object when saving it.
 32 		 *                        This array is populated during the save
 33 		 *                        process.  It get filled just before
 34 		 *                        persisting the data to the server, and gets
 35 		 *                        emptied as soon as the save operation
 36 		 *                        succeeds.
 37 		 */
 38 		_deletedBlocks: [],
 39 
 40 		/**
 41 		 * @private
 42 		 * @type {Array.<object>} A set of tags that are are to be removed from
 43 		 *                        from this content object when it is saved.
 44 		 */
 45 		_deletedTags: [],
 46 
 47 		/**
 48 		 * Searching for a tag of a given id from the object structure that is
 49 		 * returned by the REST API would require O(N) time.  This function,
 50 		 * builds a hash that maps the tag id with its corresponding name, so
 51 		 * that it can be mapped in O(1) time instead.
 52 		 *
 53 		 * @private
 54 		 * @return {object<number,string>} A hash map where the key is the tag
 55 		 *                                 id, and the value is the tag name.
 56 		 */
 57 		'!_mapTagIdsToNames': function () {
 58 			var name;
 59 			var map = {};
 60 			var tags = this._data.tags;
 61 			for (name in tags) {
 62 				if (tags.hasOwnProperty(name)) {
 63 					map[tags[name].id] = name;
 64 				}
 65 			}
 66 			return map;
 67 		},
 68 
 69 		/**
 70 		 * Retrieves data for a tag from the internal data object.
 71 		 *
 72 		 * @private
 73 		 * @param {string} name The name of the tag.
 74 		 * @return {!object} The tag data, or null if a there if no tag
 75 		 *                   matching the given name.
 76 		 */
 77 		'!_getTagData': function (name) {
 78 			return (this._data.tags && this._data.tags[name]) ||
 79 			       (this._shadow.tags && this._shadow.tags[name]);
 80 		},
 81 
 82 		/**
 83 		 * Get the tag whose id is `id'.
 84 		 * Builds the `_tagIdToNameMap' hash map if it doesn't already exist.
 85 		 *
 86 		 * @todo: Should we deprecate this?
 87 		 * @private
 88 		 * @param {number} id Id of tag to retrieve.
 89 		 * @return {object} The tag's data.
 90 		 */
 91 		'!_getTagDataById': function (id) {
 92 			if (!this._tagIdToNameMap) {
 93 				this._tagIdToNameMap = this._mapTagIdsToNames();
 94 			}
 95 			return this._getTagData(this._tagIdToNameMap[id] ||
 96 				 this._createdTagIdToNameMap[id]);
 97 		},
 98 
 99 		/**
100 		 * Extracts the editables and blocks that have been rendered.
101 		 *
102 		 * @param {object} data The response object received from the
103 		 *                      renderTemplate() call.
104 		 * @return {object} An object containing two properties: an array of
105 		 *                  blocks, and an array of editables.
106 		 */
107 		'!_processRenderedTags': function (data) {
108 			var tags = this._getEditablesAndBlocks(data);
109 			this._storeRenderedEditables(tags.editables);
110 			this._storeRenderedBlocks(tags.blocks);
111 			return tags;
112 		},
113 
114 		/**
115 		 * Get this content object's node.
116 		 *
117 		 * @public
118 		 * @function
119 		 * @name node
120 		 * @memberOf ContentObjectAPI
121 		 * @param {funtion(NodeAPI)=} success Optional callback to receive a
122 		 *                                    {@link NodeAPI} object as the
123 		 *                                    only argument.
124 		 * @param {function(GCNError):boolean=} error Optional custom error
125 		 *                                            handler.
126 		 * @return {NodeAPI} This object's node.
127 		 */
128 		'!node': function (success, error) {
129 			return this.folder().node();
130 		},
131 
132 		/**
133 		 * Get this content object's parent folder.
134 		 *
135 		 * @public
136 		 * @function
137 		 * @name folder
138 		 * @memberOf ContentObjectAPI
139 		 * @param {funtion(FolderAPI)=} success Optional callback to receive a
140 		 *                                      {@link FolderAPI} object as the
141 		 *                                      only argument.
142 		 * @param {function(GCNError):boolean=} error Optional custom error
143 		 *                                            handler.
144 		 * @return {FolderAPI} This object's parent folder.
145 		 */
146 		'!folder': function (success, error) {
147 			var id = this._fetched ? this.prop('folderId') : null;
148 			return this._continue(GCN.FolderAPI, id, success, error);
149 		},
150 
151 		/**
152 		 * Gets a tag of the specified id, contained in this content object.
153 		 *
154 		 * @name tag
155 		 * @function
156 		 * @memberOf TagContainerAPI
157 		 * @param {function} success
158 		 * @param {function} error
159 		 * @return TagAPI
160 		 */
161 		'!tag': function (id, success, error) {
162 			return this._continue(GCN.TagAPI, id, success, error);
163 		},
164 
165 		/**
166 		 * Retrieves a collection of tags from this content object.
167 		 *
168 		 * @name tags
169 		 * @function
170 		 * @memberOf TagContainerAPI
171 		 * @param {object|string|number} settings (Optional)
172 		 * @param {function} success callback
173 		 * @param {function} error (Optional)
174 		 * @return TagContainerAPI
175 		 */
176 		'!tags': function () {
177 			var args = Array.prototype.slice.call(arguments);
178 
179 			if (args.length === 0) {
180 				return;
181 			}
182 
183 			var i;
184 			var j = args.length;
185 			var filter = {};
186 			var filters;
187 			var hasFilter = false;
188 			var success;
189 			var error;
190 
191 			// Determine `success', `error', `filter'
192 			for (i = 0; i < j; ++i) {
193 				switch (jQuery.type(args[i])) {
194 				case 'function':
195 					if (success) {
196 						error = args[i];
197 					} else {
198 						success = args[i];
199 					}
200 					break;
201 				case 'number':
202 				case 'string':
203 					filters = [args[i]];
204 					break;
205 				case 'array':
206 					filters = args[i];
207 					break;
208 				default:
209 					return;
210 				}
211 			}
212 
213 			if (jQuery.type(filters) === 'array') {
214 				var k = filters.length;
215 				while (k) {
216 					filter[filters[--k]] = true;
217 				}
218 				hasFilter = true;
219 			}
220 
221 			var that = this;
222 
223 			if (success) {
224 				this._read(function () {
225 					var tags = that._data.tags;
226 					var tag;
227 					var list = [];
228 
229 					for (tag in tags) {
230 						if (tags.hasOwnProperty(tag)) {
231 							if (!hasFilter || filter[tag]) {
232 								list.push(that._continue(GCN.TagAPI, tags[tag],
233 									null, error));
234 							}
235 						}
236 					}
237 
238 					that._invoke(success, [list]);
239 				}, error);
240 			}
241 		},
242 
243 		/**
244 		 * Internal method to create a tag of a given tagtype in this content
245 		 * object.
246 		 *
247 		 * @private
248 		 * @param {string|number|object} construct either the keyword of the construct, or the ID of the construct
249 		 *                               or an object with the following properties
250 		 *                               <ul>
251 		 *                                <li><i>keyword</i> keyword of the construct</li>
252 		 *                                <li><i>constructId</i> ID of the construct</li>
253 		 *                                <li><i>magicValue</i> magic value to be filled into the tag</li>
254 		 *                                <li><i>sourcePageId</i> source page id</li>
255 		 *                                <li><i>sourceTagname</i> source tag name</li>
256 		 *                               </ul>
257 		 * @param {function(TagAPI)=} success Optional callback that will
258 		 *                                    receive the newly created tag as
259 		 *                                    its only argument.
260 		 * @param {function(GCNError):boolean=} error Optional custom error
261 		 *                                            handler.
262 		 * @return {TagAPI} The newly created tag.
263 		 */
264 		'!_createTag': function () {
265 			var args = Array.prototype.slice.call(arguments);
266 
267 			if (args.length === 0) {
268 				GCN.error('INVALID_ARGUMENTS', '`createTag()\' requires at ' +
269 					'least one argument.  See documentation.');
270 				return;
271 			}
272 
273 			var success;
274 			var error;
275 			var magicValue;
276 			var construct;
277 			var sourcePageID;
278 			var sourceTagname;
279 			var i;
280 			var j = args.length;
281 
282 			// first parameter may be string, integer or object
283 			if (jQuery.type(args[0]) === 'string' || jQuery.type(args[0]) === 'number') {
284 				construct = args[0];
285 			} else if (jQuery.type(args[0]) === 'object') {
286 				if ((args[0].keyword && args[0].constructId) ||
287 					(args[0].keyword && (args[0].sourcePageId || args[0].sourceTagname)) ||
288 					(args[0].constructId && (args[0].sourcePageId || args[0].sourceTagname))) {
289 					GCN.error('INVALID_ARGUMENTS', '`createTag()\' supports only the combinations ' +
290 						'`keyword\' or `constructId\' or `sourcePageId/sourceTagname\'.');
291 				}
292 				if (args[0].keyword) {
293 					construct = args[0].keyword;
294 				} else if (args[0].constructId) {
295 					construct = args[0].constructId;
296 				} else if (args[0].sourcePageId && args[0].sourceTagname) {
297 					sourcePageID = args[0].sourcePageId;
298 					sourceTagname = args[0].sourceTagname;
299 				} else {
300 					GCN.error('INVALID_ARGUMENTS', '`createTag()\' supports only the combinations ' +
301 						'`keyword\' or `constructId\' or `sourcePageId/sourceTagname\'.');
302 				}
303 
304 				if (args[0].magicValue) {
305 					magicValue = args[0].magicValue;
306 				}
307 			}
308 			// Determine `success' and `error'
309 			for (i = 1; i < j; ++i) {
310 				if (jQuery.type(args[i]) === 'function') {
311 					if (success) {
312 						error = args[i];
313 					} else {
314 						success = args[i];
315 					}
316 					break;
317 				}
318 			}
319 
320 			var that = this;
321 
322 			// We use a uniqueId to avoid a fetus being created.
323 			// This is to avoid the following szenario:
324 			//
325 			// var tag1 = container.createTag(...);
326 			// var tag2 = container.createTag(...);
327 			// tag1 === tag2 // is true which is wrong
328 			//
329 			// However, for all other cases, where we get an existing object, we want this behaviour:
330 			// var folder1 = page(1).folder(...);
331 			// var folder2 = page(1).folder(...);
332 			// folder1 === folder2 // is true which is correct
333 			//
334 			// So, createTag() is different from other chainback methods
335 			// in that each invokation must create a new instance, while
336 			// other chainback methods must return the same.
337 			//
338 			// The id will be reset as soon as the tag object is
339 			// realized. This happens below as soon as we get a success
340 			// response with the correct tag id.
341 			var newId = GCN.uniqueId('TagApi-unique-');
342 
343 			// First create a new TagAPI instance that will have this container
344 			// as its ancestor.  Also acquire a lock on the newly created tag
345 			// object so that any further operations on it will be queued until
346 			// we release the lock.
347 			var tag = this._continue(GCN.TagAPI, newId)._procure();
348 
349 			if (construct) {
350 			this.node().constructs(function (constructs) {
351 				var constructId;
352 
353 				if ('number' === jQuery.type(construct)) {
354 					constructId = construct;
355 				} else if (constructs[construct]) {
356 					constructId = constructs[construct].constructId;
357 				} else {
358 					var err = GCN.createError('CONSTRUCT_NOT_FOUND',
359 						'Cannot find constuct `' + construct + '\'',
360 						constructs);
361 					GCN.handleError(err, error);
362 					return;
363 				}
364 
365 					var restUrl = GCN.settings.BACKEND_PATH + '/rest/' +
366 						that._type + '/newtag/' + that._data.id;
367 
368 				that._authAjax({
369 					type  : 'POST',
370 					url   : restUrl,
371 						json  : {magicValue: magicValue, constructId: constructId},
372 					error : function (xhr, status, msg) {
373 						GCN.handleHttpError(xhr, msg, error);
374 						tag._vacate();
375 					},
376 					success: function (response) {
377 							that._handleCreateTagResponse(tag, response, success, error);
378 						}
379 					});
380 				}, error);
381 			} else {
382 				var restUrl = GCN.settings.BACKEND_PATH + '/rest/' +
383 					that._type + '/newtag/' + that._data.id;
384 
385 				that._authAjax({
386 					type  : 'POST',
387 					url   : restUrl,
388 					json  : {copyPageId: sourcePageID, copyTagname: sourceTagname},
389 					error : function (xhr, status, msg) {
390 						GCN.handleHttpError(xhr, msg, error);
391 						tag._vacate();
392 					},
393 					success: function (response) {
394 						that._handleCreateTagResponse(tag, response, success, error);
395 					}
396 				});
397 			}
398 
399 			return tag;
400 		},
401 
402 		/**
403 		 * Internal helper method to handle the create tag response
404 		 * 
405 		 * @private
406 		 * @param {TagAPI} tag
407 		 * @param {object} response response object from the REST call
408 		 * @param {function(TagContainerAPI)=} success optional success handler
409 		 * @param {function(GCNError):boolean=} error optional error handler
410 		 */
411 		'!_handleCreateTagResponse': function (tag, response, success, error) {
412 			var that = this;
413 
414 						if (GCN.getResponseCode(response) === 'OK') {
415 							var data = response.tag;
416 
417 							tag._name    = data.name;
418 							tag._data    = data;
419 							tag._fetched = true;
420 
421 							// The tag's id is still newId from
422 							// above. We have to realize the tag so that
423 							// it gets the correct id. The new id
424 							// changes its hash, so it must also be
425 							// removed and reinserted from the caches.
426 							tag._removeFromTempCache();
427 							tag._setHash(data.id)._addToCache();
428 
429 							// Add this tag into the tag's container `_shadow'
430 							// object, and `_tagIdToNameMap hash'.
431 
432 							var shouldCreateObjectIfUndefined = true;
433 
434 							// Add this tag into the `_shadow' object.
435 							that._update('tags.' + GCN.escapePropertyName(data.name),
436 								data, error, shouldCreateObjectIfUndefined);
437 
438 							// TODO: We need to store the tag inside the
439 							// `_data' object for now.  A change should be made
440 							// so that when containers are saved, the data in
441 							// the _shadow object is properly transfered into
442 							// the _data object.
443 
444 							that._data.tags[data.name] = data;
445 
446 							if (!that.hasOwnProperty('_createdTagIdToNameMap')) {
447 								that._createdTagIdToNameMap = {};
448 							}
449 
450 							that._createdTagIdToNameMap[data.id] = data.name;
451 
452 							if (success) {
453 								that._invoke(success, [tag]);
454 							}
455 						} else {
456 							tag._die(GCN.getResponseCode(response));
457 							GCN.handleResponseError(response, error);
458 						}
459 
460 						// Hold onto the mutex until this tag object has been
461 						// fully realized and placed inside its container.
462 						tag._vacate();
463 		},
464 
465 		/**
466 		 * Internal method to delete the specified tag from this content
467 		 * object.
468 		 *
469 		 * @private
470 		 * @param {string} id The id of the tag to be deleted.
471 		 * @param {function(TagContainerAPI)=} success Optional callback that
472 		 *                                             receive this object as
473 		 *                                             its only argument.
474 		 * @param {function(GCNError):boolean=} error Optional custom error
475 		 *                                            handler.
476 		 */
477 		'!_removeTag': function (id, success, error) {
478 			this.tag(id).remove(success, error);
479 		},
480 
481 		/**
482 		 * Internal method to delete a set of tags from this content object.
483 		 *
484 		 * @private
485 		 * @param {Array.<string>} ids The ids of the set of tags to be
486 		 *                             deleted.
487 		 * @param {function(TagContainerAPI)=} success Optional callback that
488 		 *                                             receive this object as
489 		 *                                             its only argument.
490 		 * @param {function(GCNError):boolean=} error Optional custom error
491 		 *                                            handler.
492 		 */
493 		'!_removeTags': function (ids, success, error) {
494 			var that = this;
495 			this.tags(ids, function (tags) {
496 				var j = tags.length;
497 				while (j--) {
498 					tags[j].remove(null, error);
499 				}
500 				if (success) {
501 					that.save(success, error);
502 				}
503 			}, error);
504 		},
505 
506 		/**
507 		 * Given a data object received from a "/rest/page/render" call, map
508 		 * the blocks and editables into a list of each.
509 		 *
510 		 * Note that if a tag is both an editable and a block, it will be
511 		 * listed in both the blocks list and in the editables list.
512 		 *
513 		 * @param {object} data
514 		 * @return {object<string, Array.<object>>} A map containing a set of
515 		 *                                          editables and a set of
516 		 *                                          blocks.
517 		 */
518 		'!_getEditablesAndBlocks': function (data) {
519 			if (!data || !data.tags) {
520 				return {
521 					blocks: [],
522 					editables: []
523 				};
524 			}
525 
526 			var tag;
527 			var tags = data.tags;
528 			var numTags = tags.length;
529 			var numEditables;
530 			var blocks = [];
531 			var editables = [];
532 			var isBlock;
533 			var i;
534 			var j;
535 
536 			for (i = 0; i < numTags; i++) {
537 				tag = tags[i];
538 
539 				if (tag.editables) {
540 					numEditables = tag.editables.length;
541 					for (j = 0; j < numEditables; j++) {
542 						tag.editables[j].tagname = tag.tagname;
543 					}
544 					editables = editables.concat(tag.editables);
545 				}
546 
547 				/*
548 				 * Depending on tag.onlyeditables to determine whether or not a
549 				 * given tag is a block or editable is unreliable since it is
550 				 * possible to have a block which only contains editables:
551 				 *
552 				 * {
553 				 *  "tagname":"content",
554 				 *  "editables":[{
555 				 *	  "element":"GENTICS_EDITABLE_1234",
556 				 *	  "readonly":false,
557 				 *	  "partname":"editablepart"
558 				 *  }],
559 				 *  "element":"GENTICS_BLOCK_1234",
560 				 *  "onlyeditables":true
561 				 *  "tagname":"tagwitheditable"
562 				 * }
563 				 *
564 				 * in the above example, tag.onlyeditable is true but tag is a
565 				 * block, since the tag's element and the editable's element
566 				 * are not the same.
567 				 */
568 
569 				if (!tag.editables || tag.editables.length > 1) {
570 					isBlock = true;
571 				} else {
572 					isBlock = (1 === tag.editables.length) &&
573 						(tag.editables[0].element !== tag.element);
574 				}
575 
576 				if (isBlock) {
577 					blocks.push(tag);
578 				}
579 			}
580 
581 			return {
582 				blocks: blocks,
583 				editables: editables
584 			};
585 		}
586 
587 	});
588 
589 }(GCN));
590