1 /*global window: true, GCN: true, jQuery: true*/
  2 (function (GCN) {
  3 
  4 	'use strict';
  5 
  6 	/**
  7 	 * Searches for the an Aloha editable object of the given id.
  8 	 *
  9 	 * @TODO: Once Aloha.getEditableById() is patched to not cause an
 10 	 *        JavaScript exception if the element for the given ID is not found
 11 	 *        then we can deprecate this function and use Aloha's instead.
 12 	 *
 13 	 * @static
 14 	 * @param {string} id Id of Aloha.Editable object to find.
 15 	 * @return {Aloha.Editable=} The editable object, if wound; otherwise null.
 16 	 */
 17 	function getAlohaEditableById(id) {
 18 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
 19 		if (!Aloha) {
 20 			return null;
 21 		}
 22 
 23 		// If the element is a textarea then route to the editable div.
 24 		var element = jQuery('#' + id);
 25 		if (element.length &&
 26 				element[0].nodeName.toLowerCase() === 'textarea') {
 27 			id += '-aloha';
 28 		}
 29 
 30 		var editables = Aloha.editables;
 31 		var j = editables.length;
 32 		while (j) {
 33 			if (editables[--j].getId() === id) {
 34 				return editables[j];
 35 			}
 36 		}
 37 
 38 		return null;
 39 	}
 40 
 41 	/**
 42 	 * Helper function to normalize the arguments that can be passed to the
 43 	 * `edit()' and `render()' methods.
 44 	 *
 45 	 * @private
 46 	 * @static
 47 	 * @param {arguments} args A list of arguments.
 48 	 * @return {object} Object containing an the properties `element',
 49 	 *                  `success' and `error', and `data'.
 50 	 */
 51 	function getRenderOptions(args) {
 52 		var argv = Array.prototype.slice.call(args);
 53 		var argc = args.length;
 54 		var arg;
 55 		var i;
 56 
 57 		var element;
 58 		var success;
 59 		var error;
 60 		var prerenderedData = false;
 61 
 62 		for (i = 0; i < argc; ++i) {
 63 			arg = argv[i];
 64 
 65 			switch (jQuery.type(arg)) {
 66 			case 'string':
 67 				element = jQuery(arg);
 68 				break;
 69 			case 'object':
 70 				if (element) {
 71 					prerenderedData = arg;
 72 				} else {
 73 					element = arg;
 74 				}
 75 				break;
 76 			case 'function':
 77 				if (success) {
 78 					error = arg;
 79 				} else {
 80 					success = arg;
 81 				}
 82 				break;
 83 			// Descarding all other types of arguments...
 84 			}
 85 		}
 86 
 87 		return {
 88 			element : element,
 89 			success : success,
 90 			error   : error,
 91 			data    : prerenderedData
 92 		};
 93 	}
 94 
 95 	/**
 96 	 * Exposes an API to operate on a Content.Node tag.
 97 	 *
 98 	 * @class
 99 	 * @name TagAPI
100 	 */
101 	var TagAPI = GCN.defineChainback({
102 
103 		__chainbacktype__: 'TagAPI',
104 
105 		/**
106 		 * A reference to the object in which this tag is contained.  This value
107 		 * is set during initialization.
108 		 *
109 		 * @type {GCN.ContentObject}
110 		 */
111 		_parent: null,
112 
113 		/**
114 		 * Name of this tag.
115 		 *
116 		 * @type {string}
117 		 */
118 		_name: null,
119 
120 		/**
121 		 * Gets this tag's information from the object that contains it.
122 		 *
123 		 * @param {function(TagAPI)} success Callback to be invoked when this
124 		 *                                   operation completes normally.
125 		 * @param {function(GCNError):boolean} error Custom error handler.
126 		 */
127 		'!_read': function (success, error) {
128 			var parent = this.parent();
129 			// Because tags always retrieve their data from a parent object,
130 			// this tag is only completely fetched if it's parent is also fetch.
131 			// The parent could have been cleared of all it's data using
132 			// _clearCache() while this tag was left in a _fetched state, so we
133 			// need to check.
134 			if (this._fetched && parent._fetched) {
135 				if (success) {
136 					this._invoke(success, [this]);
137 				}
138 				return;
139 			}
140 
141 			// Because when loading folders via folder(1).folders() will
142 			// fetch them without any tag data.  We therefore have to refetch
143 			// them wit their tag data.
144 			if (parent._fetched && !parent._data.tags) {
145 				parent._data.tags = {};
146 				parent.fetch(function (response) {
147 					if (GCN.getResponseCode(response) !== 'OK') {
148 						GCN.handleResponseError(response);
149 						return;
150 					}
151 					var newTags = {};
152 					jQuery.each(
153 						response[parent._type].tags,
154 						function (name, data) {
155 							if (!GCN.TagContainerAPI.hasTagData(parent, name)) {
156 								newTags[name] = data;
157 							}
158 						}
159 					);
160 					GCN.TagContainerAPI.extendTags(parent, newTags);
161 					parent._read(success, error);
162 				});
163 				return;
164 			}
165 
166 			var that = this;
167 
168 			// Take the data for this tag from it's container.
169 			parent._read(function () {
170 				that._data = parent._getTagData(that._name);
171 
172 				if (!that._data) {
173 					var err = GCN.createError('TAG_NOT_FOUND',
174 						'Could not find tag "' + that._name + '" in ' +
175 						parent._type + " " + parent._data.id, that);
176 					GCN.handleError(err, error);
177 					return;
178 				}
179 
180 				that._fetched = true;
181 
182 				if (success) {
183 					that._invoke(success, [that]);
184 				}
185 			}, error);
186 		},
187 
188 		/**
189 		 * Retrieve the object in which this tag is contained.  It does so by
190 		 * getting this chainback's "chainlink ancestor" object.
191 		 *
192 		 * @function
193 		 * @name parent
194 		 * @memberOf TagAPI
195 		 * @return {GCN.AbstractTagContainer}
196 		 */
197 		'!parent': function () {
198 			return this._ancestor();
199 		},
200 
201 		/**
202 		 * Initialize a tag object. Unlike other chainback objects, tags will
203 		 * always have a parent. If its parent have been loaded, we will
204 		 * immediately copy the this tag's data from the parent's `_data' object
205 		 * to the tag's `_data' object.
206 		 *
207 		 * @param {string|object}
208 		 *            settings
209 		 * @param {function(TagAPI)}
210 		 *            success Callback to be invoked when this operation
211 		 *            completes normally.
212 		 * @param {function(GCNError):boolean}
213 		 *            error Custom error handler.
214 		 */
215 		_init: function (settings, success, error) {
216 			if (jQuery.type(settings) === 'object') {
217 				this._name    = settings.name;
218 				this._data    = settings;
219 				this._data.id = settings.id;
220 				this._fetched = true;
221 			} else {
222 				// We don't want to reinitalize the data object when it
223 				// has not been fetched yet.
224 				if (!this._fetched) {
225 					this._data = {};
226 					this._data.id = this._name = settings;
227 				}
228 			}
229 
230 			if (success) {
231 				var that = this;
232 
233 				this._read(function (container) {
234 					that._read(success, error);
235 				}, error);
236 
237 			// Even if not success callback is given, read this tag's data from
238 			// is container, if that container has the data available.
239 			// If we are initializing a placeholder tag object (in the process
240 			// of creating brand new tag, for example), then its parent
241 			// container will not have any data for this tag yet.  We know that
242 			// we are working with a placeholder tag if no `_data.id' or `_name'
243 			// property is set.
244 			} else if (!this._fetched && this._name &&
245 			           this.parent()._fetched) {
246 				this._data = this.parent()._getTagData(this._name);
247 				this._fetched = !!this._data;
248 
249 			// We are propably initializing a placholder object, we will assign
250 			// it its own `_data' and `_fetched' properties so that it is not
251 			// accessing the prototype values.
252 			} else if (!this._fetched) {
253 				this._data = {};
254 				this._data.id = this._name = settings;
255 				this._fetched = false;
256 			}
257 		},
258 
259 		/**
260 		 * Gets or sets a property of this tags. Note that tags do not have a
261 		 * `_shadow' object, and we update the `_data' object directly.
262 		 *
263 		 * @function
264 		 * @name prop
265 		 * @memberOf TagAPI
266 		 * @param {string}
267 		 *            name Name of tag part.
268 		 * @param {*=}
269 		 *            set Optional value. If provided, the tag part will be
270 		 *            replaced with this value.
271 		 * @return {*} The value of the accessed tag part.
272 		 * @throws UNFETCHED_OBJECT_ACCESS
273 		 */
274 		'!prop': function (name, value) {
275 			var parent = this.parent();
276 
277 			if (!this._fetched) {
278 				GCN.error('UNFETCHED_OBJECT_ACCESS',
279 					'Calling method `prop()\' on an unfetched object: ' +
280 					parent._type + " " + parent._data.id, this);
281 
282 				return;
283 			}
284 
285 			if (jQuery.type(value) !== 'undefined') {
286 				this._data[name] = value;
287 				parent._update('tags.' + GCN.escapePropertyName(this.prop('name')),
288 					this._data);
289 			}
290 
291 			return this._data[name];
292 		},
293 
294 		/**
295 		 * <p>
296 		 * Gets or sets a part of a tag.
297 		 *
298 		 * <p>
299 		 * There exists different types of tag parts, and the possible value of
300 		 * each kind of tag part may differ.
301 		 *
302 		 * <p>
303 		 * Below is a list of possible kinds of tag parts, and references to
304 		 * what the possible range their values can take:
305 		 *
306 		 * <pre>
307 		 *      STRING : {@link TagParts.STRING}
308 		 *    RICHTEXT : {@link TagParts.RICHTEXT}
309 		 *     BOOLEAN : {@link TagParts.BOOLEAN}
310 		 *       IMAGE : {@link TagParts.IMAGE}
311 		 *        FILE : {@link TagParts.FILE}
312 		 *      FOLDER : {@link TagParts.FOLDER}
313 		 *        PAGE : {@link TagParts.PAGE}
314 		 *    OVERVIEW : {@link TagParts.OVERVIEW}
315 		 *     PAGETAG : {@link TagParts.PAGETAG}
316 		 * TEMPLATETAG : {@link TagParts.TEMPLATETAG}
317 		 *      SELECT : {@link TagParts.SELECT}
318 		 * MULTISELECT : {@link TagParts.MULTISELECT}
319 		 * </pre>
320 		 *
321 		 * @function
322 		 * @name part
323 		 * @memberOf TagAPI
324 		 *
325 		 * @param {string}
326 		 *            name Name of tag opart.
327 		 * @param {*=}
328 		 *            set Optional value.  If provided, the tag part will be
329 		 *            update with this value.  How this happends differs between
330 		 *            different type of tag parts.
331 		 * @return {*} The value of the accessed tag part.
332 		 * @throws UNFETCHED_OBJECT_ACCESS
333 		 * @throws PART_NOT_FOUND
334 		 */
335 		'!part': function (name, value) {
336 			var parent;
337 
338 			if (!this._fetched) {
339 				parent = this.parent();
340 
341 				GCN.error('UNFETCHED_OBJECT_ACCESS',
342 					'Calling method `prop()\' on an unfetched object: ' +
343 					parent._type + " " + parent._data.id, this);
344 
345 				return null;
346 			}
347 
348 			var part = this._data.properties[name];
349 
350 			if (!part) {
351 				parent = this.parent();
352 
353 				GCN.error('PART_NOT_FOUND', 'Tag "' + this._name +
354 					'" of ' + parent._type + ' ' + parent._data.id +
355 					' does not have a part "' + name + '"', this);
356 
357 				return null;
358 			}
359 
360 			if (jQuery.type(value) === 'undefined') {
361 				return GCN.TagParts.get(part);
362 			}
363 
364 			var partValue = GCN.TagParts.set(part, value);
365 
366 			// Each time we perform a write operation on a tag, we will update
367 			// the tag in the tag container's `_shadow' object as well.
368 			this.parent()._update('tags.' + GCN.escapePropertyName(this._name),
369 				this._data);
370 
371 			return partValue;
372 		},
373 
374 		/**
375 		 * Remove this tag from its containing object (it's parent).
376 		 *
377 		 * @function
378 		 * @memberOf TagAPI
379 		 * @name remove
380 		 * @param {function} callback A function that receive this tag's parent
381 		 *                            object as its only arguments.
382 		 */
383 		remove: function (success, error) {
384 			var parent = this.parent();
385 
386 			if (!parent.hasOwnProperty('_deletedTags')) {
387 				parent._deletedTags = [];
388 			}
389 
390 			GCN.pub('tag.before-deleted', {tag: this});
391 
392 			parent._deletedTags.push(this._name);
393 
394 			if (parent._data.tags &&
395 					parent._data.tags[this._name]) {
396 				delete parent._data.tags[this._name];
397 			}
398 
399 			if (parent._shadow.tags &&
400 					parent._shadow.tags[this._name]) {
401 				delete parent._shadow.tags[this._name];
402 			}
403 
404 			parent._removeAssociatedTagData(this._name);
405 
406 			if (success) {
407 				parent._persist(null, success, error);
408 			}
409 		},
410 
411 		/**
412 		 * Given a DOM element, will generate a template which represents this
413 		 * tag as it would be if rendered in the element.
414 		 *
415 		 * @param {jQuery.<HTMLElement>} $element DOM element with which to
416 		 *                                        generate the template.
417 		 * @return {string} Template string.
418 		 */
419 		'!_makeTemplate': function ($element) {
420 			if (0 === $element.length) {
421 				return '<node ' + this._name + '>';
422 			}
423 			var placeholder =
424 					'-{(' + this.parent().id() + ':' + this._name + ')}-';
425 			var template = jQuery.trim(
426 					$element.clone().html(placeholder)[0].outerHTML
427 				);
428 			return template.replace(placeholder, '<node ' + this._name + '>');
429 		},
430 
431 		/**
432 		 * Will render this tag in the given render `mode'.  If an element is
433 		 * provided, the content will be placed in that element.  If the `mode'
434 		 * is "edit", any rendered editables will be initialized for Aloha
435 		 * Editor.  Any editable that are rendered into an element will also be
436 		 * added to the tag's parent object's `_editables' array so that they
437 		 * can have their changed contents copied back into their corresponding
438 		 * tags during saving.
439 		 *
440 		 * @param {string} mode The rendering mode.  Valid values are "view",
441 		 *                      and "edit".
442 		 * @param {jQuery.<HTMLElement>} element DOM element into which the
443 		 *                                       the rendered content should be
444 		 *                                       placed.
445 		 * @param {function(string, TagAPI, object)} Optional success handler.
446 		 * @param {function(GCNError):boolean} Optional custom error handler.
447 		 */
448 		'!_render': function (mode, $element, success, error) {
449 			var tag = this._fork();
450 			tag._read(function () {
451 				var template = ($element && $element.length)
452 				             ? tag._makeTemplate($element)
453 				             : '<node ' + tag._name + '>';
454 
455 				var obj = tag.parent();
456 
457 				obj._renderTemplate(template, mode, function (data) {
458 
459 					// Because the parent content object needs to track any
460 					// blocks or editables that have been rendered in this tag.
461 					obj._processRenderedTags(data);
462 
463 					GCN._handleContentRendered(data.content, tag,
464 						function (html) {
465 							if ($element && $element.length) {
466 								GCN.renderOnto($element, html);
467 								// Because 'content-inserted' is deprecated by
468 								// 'tag.inserted'.
469 								GCN.pub('content-inserted', [$element, html]);
470 								GCN.pub('tag.inserted', [$element, html]);
471 							}
472 
473 							var frontendEditing = function (callback) {
474 								if ('edit' === mode) {
475 									// Because 'rendered-for-editing' is deprecated by
476 									// 'tag.rendered-for-editing'.
477 									GCN.pub('rendered-for-editing', {
478 										tag: tag,
479 										data: data,
480 										callback: callback
481 									});
482 									GCN.pub('tag.rendered-for-editing', {
483 										tag: tag,
484 										data: data,
485 										callback: callback
486 									});
487 								} else if (callback) {
488 									callback();
489 								}
490 							};
491 
492 							// Because the caller of edit() my wish to do things
493 							// in addition to, or instead of, our frontend
494 							// initialization.
495 							if (success) {
496 								tag._invoke(
497 									success,
498 									[html, tag, data, frontendEditing]
499 								);
500 							} else {
501 								frontendEditing();
502 							}
503 
504 							tag._merge();
505 						});
506 				}, function () {
507 					tag._merge();
508 				});
509 			}, error);
510 		},
511 
512 		/**
513 		 * <p>
514 		 * Render the tag based on its settings on the server. Can be called
515 		 * with the following arguments:<(p>
516 		 *
517 		 * <pre>
518 		 * // Render tag contents into div whose id is "content-div"
519 		 * render('#content-div') or render(jQuery('#content-div'))
520 		 * </pre>
521 		 *
522 		 * <pre>
523 		 * // Pass the html rendering of the tag in the given callback
524 		 * render(function(html, tag) {
525 		 *   // implementation!
526 		 * })
527 		 * </pre>
528 		 *
529 		 * Whenever a 2nd argument is provided, it will be taken as as custom
530 		 * error handler. Invoking render() without any arguments will yield no
531 		 * results.
532 		 *
533 		 * @function
534 		 * @name render
535 		 * @memberOf TagAPI
536 		 * @param {string|jQuery.HTMLElement}
537 		 *            selector jQuery selector or jQuery target element to be
538 		 *            used as render destination
539 		 * @param {function(string,
540 		 *            GCN.TagAPI)} success success function that will receive
541 		 *            the rendered html as well as the TagAPI object
542 		 */
543 		render: function () {
544 			var tag = this;
545 			var args = arguments;
546 			jQuery(function () {
547 				args = getRenderOptions(args);
548 				if (args.element || args.success) {
549 					tag._render(
550 						'view',
551 						args.element,
552 						args.success,
553 						args.error
554 					);
555 				}
556 			});
557 		},
558 
559 		/**
560 		 * <p>
561 		 * Renders this tag for editing.
562 		 * </p>
563 		 *
564 		 * <p>
565 		 * Differs from the render() method in that it calls this tag to be
566 		 * rendered in "edit" mode via the REST API so that it is rendered with
567 		 * any additional content that is appropriate for when this tag is used
568 		 * in edit mode.
569 		 * </p>
570 		 *
571 		 * <p>
572 		 * The GCN JS API library will also start keeping track of various
573 		 * aspects of this tag and its rendered content.
574 		 * </p>
575 		 *
576 		 * <p>
577 		 * When a jQuery selector is passed to this method, the contents of the
578 		 * rendered tag will overwrite the element identified by that selector.
579 		 * All rendered blocks and editables will be automatically placed into
580 		 * the DOM and initialize for editing.
581 		 * </p>
582 		 *
583 		 * <p>
584 		 * The behavior is different when this method is called with a function
585 		 * as its first argument.  In this case the rendered contents of the tag
586 		 * will not be autmatically placed into the DOM, but will be passed onto
587 		 * the callback function as argmuments.  It is then up to the caller to
588 		 * place the content into the DOM and initialize all rendered blocks and
589 		 * editables appropriately.
590 		 * </p>
591 		 *
592 		 * @function
593 		 * @name edit
594 		 * @memberOf TagAPI
595 		 * @param {(string|jQuery.HTMLElement)=} element
596 		 *            The element into which this tag is to be rendered.
597 		 * @param {function(string,TagAPI)=} success
598 		 *            A function that will be called once the tag is rendered.
599 		 * @param {function(GCNError):boolean=} error
600 		 *            A custom error handler.
601 		 */
602 		edit: function () {
603 			var tag = this;
604 			var args = getRenderOptions(arguments);
605 			if (args.data) {
606 
607 				// Because the parent content object needs to track any
608 				// blocks or editables that have been rendered in this tag.
609 				tag.parent()._processRenderedTags(args.data);
610 
611 				// Because 'rendered-for-editing' is deprecated in favor of
612 				// 'tag.rendered-for-editing'
613 				GCN.pub('rendered-for-editing', {
614 					tag: tag,
615 					data: args.data,
616 					callback: function () {
617 						if (args.success) {
618 							tag._invoke(
619 								args.success,
620 								[args.content, tag, args.data]
621 							);
622 						}
623 					}
624 				});
625 				GCN.pub('tag.rendered-for-editing', {
626 					tag: tag,
627 					data: args.data,
628 					callback: function () {
629 						if (args.success) {
630 							tag._invoke(
631 								args.success,
632 								[args.content, tag, args.data]
633 							);
634 						}
635 					}
636 				});
637 			} else {
638 				jQuery(function () {
639 					if (args.element || args.success) {
640 						tag._render(
641 							'edit',
642 							args.element,
643 							args.success,
644 							args.error
645 						);
646 					}
647 				});
648 			}
649 		},
650 
651 		/**
652 		 * Persists the changes to this tag on its container object.
653 		 *
654 		 * @todo: deprecated this method.
655 		 * @function
656 		 * @name save
657 		 * @memberOf TagAPI
658 		 * @param {function(TagAPI)} success Callback to be invoked when this
659 		 *                                   operation completes normally.
660 		 * @param {function(GCNError):boolean} error Custom error handler.
661 		 */
662 		save: function (success, error) {
663 			var that = this;
664 			this.parent().save(function () {
665 				if (success) {
666 					that._invoke(success, [that]);
667 				}
668 			}, error);
669 		}
670 
671 	});
672 
673 	// Unlike content objects, tags do not have unique ids and so we uniquely I
674 	// dentify tags by their name, and their parent's id.
675 	TagAPI._needsChainedHash = true;
676 
677 	GCN.tag = GCN.exposeAPI(TagAPI);
678 	GCN.TagAPI = TagAPI;
679 
680 }(GCN));
681