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 			if (this._fetched) {
129 				if (success) {
130 					this._invoke(success, [this]);
131 				}
132 				return;
133 			}
134 
135 			var that = this;
136 			var parent = this.parent();
137 
138 			// assert(parent)
139 
140 			// Take the data for this tag from it's container.
141 			parent._read(function () {
142 				that._data = parent._getTagData(that._name);
143 
144 				if (!that._data) {
145 					var err = GCN.createError('TAG_NOT_FOUND',
146 						'Could not find tag "' + that._name + '" in ' +
147 						parent._type + " " + parent._data.id, that);
148 					GCN.handleError(err, error);
149 					return;
150 				}
151 
152 				that._fetched = true;
153 
154 				if (success) {
155 					that._invoke(success, [that]);
156 				}
157 			}, error);
158 		},
159 
160 		/**
161 		 * Retrieve the object in which this tag is contained.  It does so by
162 		 * getting this chainback's "chainlink ancestor" object.
163 		 *
164 		 * @function
165 		 * @name parent
166 		 * @memberOf TagAPI
167 		 * @return {GCN.AbstractTagContainer}
168 		 */
169 		'!parent': function () {
170 			return this._ancestor();
171 		},
172 
173 		/**
174 		 * Initialize a tag object. Unlike other chainback objects, tags will
175 		 * always have a parent. If its parent have been loaded, we will
176 		 * immediately copy the this tag's data from the parent's `_data' object
177 		 * to the tag's `_data' object.
178 		 *
179 		 * @param {string|object}
180 		 *            settings
181 		 * @param {function(TagAPI)}
182 		 *            success Callback to be invoked when this operation
183 		 *            completes normally.
184 		 * @param {function(GCNError):boolean}
185 		 *            error Custom error handler.
186 		 */
187 		_init: function (settings, success, error) {
188 			if (jQuery.type(settings) === 'object') {
189 				this._name    = settings.name;
190 				this._data    = settings;
191 				this._data.id = settings.id;
192 				this._fetched = true;
193 			} else {
194 				// We don't want to reinitalize the data object when it
195 				// has not been fetched yet.
196 				if (!this._fetched) {
197 					this._data = {};
198 					this._data.id = this._name = settings;
199 				}
200 			}
201 
202 			if (success) {
203 				var that = this;
204 
205 				this._read(function (container) {
206 					that._read(success, error);
207 				}, error);
208 
209 			// Even if not success callback is given, read this tag's data from
210 			// is container, it that container has the data available.
211 			// If we are initializing a placeholder tag object (in the process
212 			// of creating brand new tag, for example), then its parent
213 			// container will not have any data for this tag yet.  We know that
214 			// we are working with a placeholder tag if no `_data.id' or `_name'
215 			// property is set.
216 			} else if (!this._fetched && this._name &&
217 			           this.parent()._fetched) {
218 				this._data = this.parent()._getTagData(this._name);
219 				this._fetched = !!this._data;
220 
221 			// We are propably initializing a placholder object, we will assign
222 			// it its own `_data' and `_fetched' properties so that it is not
223 			// accessing the prototype values.
224 			} else if (!this._fetched) {
225 				this._data = {};
226 				this._data.id = this._name = settings;
227 				this._fetched = false;
228 			}
229 		},
230 
231 		/**
232 		 * Gets or sets a property of this tags. Note that tags do not have a
233 		 * `_shadow' object, and we update the `_data' object directly.
234 		 *
235 		 * @function
236 		 * @name prop
237 		 * @memberOf TagAPI
238 		 * @param {string}
239 		 *            name Name of tag part.
240 		 * @param {*=}
241 		 *            set Optional value. If provided, the tag part will be
242 		 *            replaced with this value.
243 		 * @return {*} The value of the accessed tag part.
244 		 * @throws UNFETCHED_OBJECT_ACCESS
245 		 */
246 		'!prop': function (name, value) {
247 			var parent = this.parent();
248 
249 			if (!this._fetched) {
250 				GCN.error('UNFETCHED_OBJECT_ACCESS',
251 					'Calling method `prop()\' on an unfetched object: ' +
252 					parent._type + " " + parent._data.id, this);
253 
254 				return;
255 			}
256 
257 			if (jQuery.type(value) !== 'undefined') {
258 				this._data[name] = value;
259 				parent._update('tags.' + GCN.escapePropertyName(name),
260 					this._data);
261 			}
262 
263 			return this._data[name];
264 		},
265 
266 		/**
267 		 * <p>
268 		 * Gets or sets a part of a tag.
269 		 *
270 		 * <p>
271 		 * There exists different types of tag parts, and the possible value of
272 		 * each kind of tag part may differ.
273 		 *
274 		 * <p>
275 		 * Below is a list of possible kinds of tag parts, and references to
276 		 * what the possible range their values can take:
277 		 *
278 		 * <pre>
279 		 *      STRING : {@link TagParts.STRING}
280 		 *    RICHTEXT : {@link TagParts.RICHTEXT}
281 		 *     BOOLEAN : {@link TagParts.BOOLEAN}
282 		 *       IMAGE : {@link TagParts.IMAGE}
283 		 *        FILE : {@link TagParts.FILE}
284 		 *      FOLDER : {@link TagParts.FOLDER}
285 		 *        PAGE : {@link TagParts.PAGE}
286 		 *    OVERVIEW : {@link TagParts.OVERVIEW}
287 		 *     PAGETAG : {@link TagParts.PAGETAG}
288 		 * TEMPLATETAG : {@link TagParts.TEMPLATETAG}
289 		 *      SELECT : {@link TagParts.SELECT}
290 		 * MULTISELECT : {@link TagParts.MULTISELECT}
291 		 * </pre>
292 		 *
293 		 * @function
294 		 * @name part
295 		 * @memberOf TagAPI
296 		 *
297 		 * @param {string}
298 		 *            name Name of tag opart.
299 		 * @param {*=}
300 		 *            set Optional value.  If provided, the tag part will be
301 		 *            update with this value.  How this happends differs between
302 		 *            different type of tag parts.
303 		 * @return {*} The value of the accessed tag part.
304 		 * @throws UNFETCHED_OBJECT_ACCESS
305 		 * @throws PART_NOT_FOUND
306 		 */
307 		'!part': function (name, value) {
308 			var parent;
309 
310 			if (!this._fetched) {
311 				parent = this.parent();
312 
313 				GCN.error('UNFETCHED_OBJECT_ACCESS',
314 					'Calling method `prop()\' on an unfetched object: ' +
315 					parent._type + " " + parent._data.id, this);
316 
317 				return null;
318 			}
319 
320 			var part = this._data.properties[name];
321 
322 			if (!part) {
323 				parent = this.parent();
324 
325 				GCN.error('PART_NOT_FOUND', 'Tag "' + this._name +
326 					'" of ' + parent._type + ' ' + parent._data.id +
327 					' does not have a part "' + name + '"', this);
328 
329 				return null;
330 			}
331 
332 			if (jQuery.type(value) === 'undefined') {
333 				return GCN.TagParts.get(part);
334 			}
335 
336 			var partValue = GCN.TagParts.set(part, value);
337 
338 			// Each time we perform a write operation on a tag, we will update
339 			// the tag in the tag container's `_shadow' object as well.
340 			this.parent()._update('tags.' + GCN.escapePropertyName(this._name),
341 				this._data);
342 
343 			return partValue;
344 		},
345 
346 		/**
347 		 * Remove this tag from its containing object (it's parent).
348 		 *
349 		 * @function
350 		 * @memberOf TagAPI
351 		 * @name remove
352 		 * @param {function} callback A function that receive this tag's parent
353 		 *                            object as its only arguments.
354 		 */
355 		remove: function (success, error) {
356 			var parent = this.parent();
357 
358 			if (!parent.hasOwnProperty('_deletedTags')) {
359 				parent._deletedTags = [];
360 			}
361 
362 			GCN.pub('tag.before-deleted', {tag: this});
363 
364 			parent._deletedTags.push(this._name);
365 
366 			if (parent._data.tags &&
367 					parent._data.tags[this._name]) {
368 				delete parent._data.tags[this._name];
369 			}
370 
371 			if (parent._shadow.tags &&
372 					parent._shadow.tags[this._name]) {
373 				delete parent._shadow.tags[this._name];
374 			}
375 
376 			parent._removeAssociatedTagData(this._name);
377 
378 			if (success) {
379 				parent._persist(null, success, error);
380 			}
381 		},
382 
383 		/**
384 		 * Given a DOM element, will generate a template which represents this
385 		 * tag as it would be if rendered in the element.
386 		 *
387 		 * @param {jQuery.<HTMLElement>} $element DOM element with which to
388 		 *                                        generate the template.
389 		 * @return {string} Template string.
390 		 */
391 		'!_makeTemplate': function ($element) {
392 			if (0 === $element.length) {
393 				return '<node ' + this._name + '>';
394 			}
395 			var placeholder =
396 					'-{(' + this.parent().id() + ':' + this._name + ')}-';
397 			var template = jQuery.trim(
398 					$element.clone().html(placeholder)[0].outerHTML
399 				);
400 			return template.replace(placeholder, '<node ' + this._name + '>');
401 		},
402 
403 		/**
404 		 * Will render this tag in the given render `mode'.  If an element is
405 		 * provided, the content will be placed in that element.  If the `mode'
406 		 * is "edit", any rendered editables will be initialized for Aloha
407 		 * Editor.  Any editable that are rendered into an element will also be
408 		 * added to the tag's parent object's `_editables' array so that they
409 		 * can have their changed contents copied back into their corresponding
410 		 * tags during saving.
411 		 *
412 		 * @param {string} mode The rendering mode.  Valid values are "view",
413 		 *                      and "edit".
414 		 * @param {jQuery.<HTMLElement>} element DOM element into which the
415 		 *                                       the rendered content should be
416 		 *                                       placed.
417 		 * @param {function(string, TagAPI, object)} Optional success handler.
418 		 * @param {function(GCNError):boolean} Optional custom error handler.
419 		 */
420 		'!_render': function (mode, $element, success, error) {
421 			var tag = this;
422 
423 			tag._read(function () {
424 				// Because no further operations are allowed on this tag until
425 				// we the rendering process finished is completed on its parent
426 				// content object.
427 				tag._procure();
428 
429 				var template = ($element && $element.length)
430 				             ? tag._makeTemplate($element)
431 				             : '<node ' + tag._name + '>';
432 
433 				var obj = tag.parent();
434 
435 				obj._renderTemplate(template, mode, function (data) {
436 
437 					// Because the parent content object needs to track any
438 					// blocks or editables that have been rendered in this tag.
439 					obj._processRenderedTags(data);
440 					
441 					GCN._handleContentRendered(data.content, tag,
442 						function (html) {
443 							if ($element && $element.length) {
444 								GCN.renderOnto($element, html);
445 								GCN.pub('content-inserted', [$element, html]);
446 							}
447 
448 							var frontendEditing = function (callback) {
449 								if ('edit' === mode) {
450 									GCN.pub('rendered-for-editing', {
451 										tag: tag,
452 										data: data,
453 										callback: callback
454 									});
455 								} else if (callback) {
456 									callback();
457 								}
458 							};
459 
460 							// Because the caller of edit() my wish to do things
461 							// in addition to, or instead of, our frontend
462 							// initialization.
463 							if (success) {
464 								tag._invoke(
465 									success,
466 									[html, tag, data, frontendEditing]
467 								);
468 							} else {
469 								frontendEditing();
470 							}
471 
472 							// Because now, both the tag, and its content object
473 							// are stable can on the tag object that were queued
474 							// during the rendering process can now be
475 							// initiatated.
476 							tag._vacate();
477 						});
478 				}, function () {
479 					tag._vacate();
480 				});
481 			}, error);
482 		},
483 
484 		/**
485 		 * <p>
486 		 * Render the tag based on its settings on the server. Can be called
487 		 * with the following arguments:<(p>
488 		 *
489 		 * <pre>
490 		 * // Render tag contents into div whose id is "content-div"
491 		 * render('#content-div') or render(jQuery('#content-div'))
492 		 * </pre>
493 		 *
494 		 * <pre>
495 		 * // Pass the html rendering of the tag in the given callback
496 		 * render(function(html, tag) {
497 		 *   // implementation!
498 		 * })
499 		 * </pre>
500 		 *
501 		 * Whenever a 2nd argument is provided, it will be taken as as custom
502 		 * error handler. Invoking render() without any arguments will yield no
503 		 * results.
504 		 *
505 		 * @function
506 		 * @name render
507 		 * @memberOf TagAPI
508 		 * @param {string|jQuery.HTMLElement}
509 		 *            selector jQuery selector or jQuery target element to be
510 		 *            used as render destination
511 		 * @param {function(string,
512 		 *            GCN.TagAPI)} success success function that will receive
513 		 *            the rendered html as well as the TagAPI object
514 		 */
515 		render: function () {
516 			var tag = this;
517 			var args = arguments;
518 			jQuery(function () {
519 				args = getRenderOptions(args);
520 				if (args.element || args.success) {
521 					tag._render(
522 						'view',
523 						args.element,
524 						args.success,
525 						args.error
526 					);
527 				}
528 			});
529 		},
530 
531 		/**
532 		 * <p>
533 		 * Renders this tag for editing.
534 		 * </p>
535 		 *
536 		 * <p>
537 		 * Differs from the render() method in that it calls this tag to be
538 		 * rendered in "edit" mode via the REST API so that it is rendered with
539 		 * any additional content that is appropriate for when this tag is used
540 		 * in edit mode.
541 		 * </p>
542 		 *
543 		 * <p>
544 		 * The GCN JS API library will also start keeping track of various
545 		 * aspects of this tag and its rendered content.
546 		 * </p>
547 		 *
548 		 * @function
549 		 * @name edit
550 		 * @memberOf TagAPI
551 		 * @param {(string|jQuery.HTMLElement)=} element
552 		 *            The element into which this tag is to be rendered.
553 		 * @param {function(string,TagAPI)=} success
554 		 *            A function that will be called once the tag is rendered.
555 		 * @param {function(GCNError):boolean=} error
556 		 *            A custom error handler.
557 		 */
558 		edit: function () {
559 			var tag = this;
560 			var args = getRenderOptions(arguments);
561 			if (args.data) {
562 				
563 				// Because the parent content object needs to track any
564 				// blocks or editables that have been rendered in this tag.
565 				tag.parent()._processRenderedTags(args.data);
566 				
567 				GCN.pub('rendered-for-editing', {
568 					tag: tag,
569 					data: args.data,
570 					callback: function () {
571 						if (args.success) {
572 							tag._invoke(
573 								args.success,
574 								[args.content, tag, args.data]
575 							);
576 						}
577 					}
578 				});
579 			} else {
580 				jQuery(function () {
581 					if (args.element || args.success) {
582 						tag._render(
583 							'edit',
584 							args.element,
585 							args.success,
586 							args.error
587 						);
588 					}
589 				});
590 			}
591 		},
592 
593 		/**
594 		 * Persists the changes to this tag on its container object.
595 		 *
596 		 * @function
597 		 * @name save
598 		 * @memberOf TagAPI
599 		 * @param {function(TagAPI)} success Callback to be invoked when this
600 		 *                                   operation completes normally.
601 		 * @param {function(GCNError):boolean} error Custom error handler.
602 		 */
603 		save: function (success, error) {
604 			var that = this;
605 			this.parent().save(function () {
606 				if (success) {
607 					that._invoke(success, [that]);
608 				}
609 			}, error);
610 		}
611 
612 	});
613 
614 	// Unlike content objects, tags do not have unique ids and so we uniquely I
615 	// dentify tags by their name, and their parent's id.
616 	TagAPI._needsChainedHash = true;
617 
618 	GCN.tag = GCN.exposeAPI(TagAPI);
619 	GCN.TagAPI = TagAPI;
620 
621 }(GCN));
622