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