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