1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	/**
  6 	 * @private
  7 	 * @const
  8 	 * @type {string}
  9 	 */
 10 	var GCN_REPOSITORY_ID = 'com.gentics.aloha.GCN.Page';
 11 
 12 	/**
 13 	 * jQuery selector string for GCN internal links.
 14 	 * @const
 15 	 * @type {string}
 16 	 */
 17 	var GCN_LINK_SELECTOR =
 18 			'a[data-gentics-aloha-repository="' + GCN_REPOSITORY_ID + '"]';
 19 
 20 	/**
 21 	 * @private
 22 	 * @const
 23 	 * @type {object.<string, boolean>} Default page settings.
 24 	 */
 25 	var DEFAULT_SETTINGS = {
 26 		// Load folder information
 27 		folder: true,
 28 
 29 		// Lock page when loading it
 30 		update: true,
 31 
 32 		// Have language variants be included in the page response.
 33 		langvars: true,
 34 
 35 		// Have page variants be included in the page response.
 36 		pagevars: true
 37 	};
 38 
 39 	/**
 40 	 * Checks whether the given tag is a magic link block.
 41 	 *
 42 	 * @param {TagAPI} tag A tag that must already have been fetched.
 43 	 * @param {Object} constructs Set of constructs.
 44 	 * @return {boolean} True if the given tag's constructId is equal to the
 45 	 *                   `MAGIC_LINK' value.
 46 	 */
 47 	function isMagicLinkTag(tag, constructs) {
 48 		return !!(constructs[GCN.settings.MAGIC_LINK]
 49 					&& (constructs[GCN.settings.MAGIC_LINK].constructId
 50 						=== tag.prop('constructId')));
 51 	}
 52 
 53 	/**
 54 	 * Checks whether or not the given anchor element is an internal GCN link.
 55 	 *
 56 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
 57 	 *                                     DOM element.
 58 	 * @return {boolean} True if the anchor element holds an external url.
 59 	 */
 60 	function isExternalLink($link) {
 61 		return GCN_REPOSITORY_ID !==
 62 			$link.attr('data-gentics-aloha-repository');
 63 	}
 64 
 65 	/**
 66 	 * Given a list of editable objects, returns a jQuery set containing these
 67 	 * editables' corresponding DOM elements that are attached to the document.
 68 	 *
 69 	 * @param {object<string, object>} editables A list of editable objects
 70 	 *                                           mapped to the id of their
 71 	 *                                           corresponding DOM element.
 72 	 * @return {jQuery.<HTMLElement>} A jQuery set of editable DOM elements.
 73 	 */
 74 	function getEditables(page) {
 75 		var id;
 76 		var $editables = jQuery();
 77 		var editables = page._editables;
 78 		for (id in editables) {
 79 			if (editables.hasOwnProperty(id)) {
 80 				$editables = $editables.add('#' + id);
 81 			}
 82 		}
 83 		return $editables;
 84 	}
 85 
 86 	/**
 87 	 * Returns all editables associated with the given page that have been
 88 	 * rendered in the document for editing.
 89 	 *
 90 	 * @param {PageAPI} page
 91 	 * @return {object} A set of editable objects.
 92 	 */
 93 	function getEditedEditables(page) {
 94 		return page._editables;
 95 	}
 96 
 97 	/**
 98 	 * Derives a list of the blocks that were rendered inside at least one of
 99 	 * the given page's edit()ed editables.
100 	 *
101 	 * @param {object<string, object>} editables A list of editable objects
102 	 *                                           mapped to the id of their
103 	 *                                           corresponding DOM element.
104 	 * @return {Array.<object>} The set of blocks contained in any of the
105 	 *                          page's rendered editables.
106 	 */
107 	function getRenderedBlocks(page) {
108 		var editables = getEditedEditables(page);
109 		var editableId;
110 		var renderedBlocks = [];
111 		for (editableId in editables) {
112 			if (editables.hasOwnProperty(editableId)) {
113 				if (editables[editableId]._gcnContainedBlocks) {
114 					renderedBlocks = renderedBlocks.concat(
115 						editables[editableId]._gcnContainedBlocks
116 					);
117 				}
118 			}
119 		}
120 		return renderedBlocks;
121 	}
122 
123 	/**
124 	 * Gets the DOM element associated with the given block.
125 	 *
126 	 * @param {object} block
127 	 * @return {?jQuery.<HTMLElement>} A jQuery unit set of the block's
128 	 *                                 corresponding DOM element, or null if no
129 	 *                                 element for the given block exists in
130 	 *                                 the document.
131 	 */
132 	function getElement(block) {
133 		var $element = jQuery('#' + block.element);
134 		return $element.length ? $element : null;
135 	}
136 
137 	/**
138 	 * Retrieves a jQuery set of all link elements that are contained in
139 	 * editables associated with the given page.
140 	 *
141 	 * @param {PageAPI} page
142 	 * @return {jQuery.<HTMLElement>} A jQuery set of link elements.
143 	 */
144 	function getEditableLinks(page) {
145 		return getEditables(page).find('a');
146 	}
147 
148 	/**
149 	 * Synonym for getting contstruct system-wide.
150 	 *
151 	 * @param {function(object)} success A callback function that receives a
152 	 *                                   list of constructs when they are
153 	 *                                   successfully loaded.
154 	 * @param {error(GCNError):boolean=} error Optional custom error handler.
155 	 */
156 	function getAllConstructs(success, error) {
157 		GCN.Admin.constructs(success, error);
158 	}
159 
160 	/**
161 	 * Determines all blocks that no longer need their tags to be kept in the
162 	 * given page's tag list.
163 	 *
164 	 * @param {PageAPI} page
165 	 * @param {function(Array.<object>)} success A callback function that
166 	 *                                           receives a list of obsolete
167 	 *                                           blocks.
168 	 * @param {function(GCNError):boolean=} error Optional custom error
169 	 *                                            handler.
170 	 */
171 	function getObsoleteBlocks(page, success, error) {
172 		var blocks = getRenderedBlocks(page);
173 		if (0 === blocks.length) {
174 			success([]);
175 			return;
176 		}
177 		var $links = getEditableLinks(page);
178 		var numToProcess = blocks.length;
179 		var obsolete = [];
180 		var onSuccess = function () {
181 			if ((0 === --numToProcess) && success) {
182 				success(obsolete);
183 			}
184 		};
185 		var onError = function (GCNError) {
186 			if (error) {
187 				return error(GCNError);
188 			}
189 		};
190 		getAllConstructs(function (constructs) {
191 			var processTag = function (block) {
192 				page.tag(block.tagname, function (tag) {
193 					if (isMagicLinkTag(tag, constructs)) {
194 						var $element = getElement(block);
195 						if (!$element) {
196 							obsolete.push(block);
197 						} else if (isExternalLink($element)) {
198 							$element.removeAttr('id');
199 							obsolete.push(block);
200 						}
201 					}
202 					onSuccess();
203 				}, onError);
204 			};
205 			var i;
206 			for (i = 0; i < blocks.length; i++) {
207 				processTag(blocks[i]);
208 			}
209 		});
210 	}
211 
212 	/**
213 	 * Checks whether or not the given block has a corresponding element in the
214 	 * document DOM.
215 	 *
216 	 * @private
217 	 * @static
218 	 * @param {object}
219 	 * @return {boolean} True if an inline element for this block exists.
220 	 */
221 	function hasInlineElement(block) {
222 		return !!getElement(block);
223 	}
224 
225 	/**
226 	 * Matches the name "aloha-block" in a className string.
227 	 *
228 	 * @const
229 	 * @type {RegExp}
230 	 */
231 	var ALOHA_BLOCK_CLASS = /\baloha-block\b/g;
232 
233 	/**
234 	 * Strips unwanted names from the given className string.
235 	 *
236 	 * All class names beginning with "aloha-block*" will be removed.
237 	 *
238 	 * @param {string} classes Space seperated list of classes.
239 	 * @return {string} Sanitized classes string.
240 	 */
241 	function cleanBlockClasses(classes) {
242 		return classes ? classes.replace(ALOHA_BLOCK_CLASS, '') : '';
243 	}
244 
245 	/**
246 	 * Determines the backend object that was set to the given link.
247 	 *
248 	 * @param {jQuery.<HTMLElement>} $link A link in an editable.
249 	 * @return {Object} An object containing the gtxalohapagelink part keyword
250 	 *                  and value.  The keyword may be either be "url" or
251 	 *                  "fileurl" depending on the type of object linked to.
252 	 *                  The value may be a string url ("http://...") for
253 	 *                  external links or an integer for internal links.
254 	 */
255 	function getTagPartsFromLink($link) {
256 		var linkData = $link.attr('data-gentics-aloha-object-id');
257 		var tagparts = {
258 			text: $link.text(),
259 			title: $link.attr('title'),
260 			target: $link.attr('target'),
261 			'class': cleanBlockClasses($link.attr('class'))
262 		};
263 
264 		if (!linkData) {
265 			tagparts.url = $link.attr('href');
266 			return tagparts;
267 		}
268 
269 		var idParts = linkData.split('.');
270 
271 		if (2 !== idParts.length) {
272 			tagparts.url = linkData;
273 		} else if ('10007' === idParts[0]) {
274 			tagparts.url = parseInt(idParts[1], 10);
275 			tagparts.fileurl = 0;
276 		} else if ('10008' === idParts[0] || '10011' === idParts[0]) {
277 			tagparts.url = 0;
278 			tagparts.fileUrl = parseInt(idParts[1], 10);
279 		} else {
280 			tagparts.extUrl = $link.attr('href');
281 		}
282 
283 		return tagparts;
284 	}
285 
286 	/**
287 	 * Checks whether a page object has a corresponding tag for a given link
288 	 * DOM element.
289 	 *
290 	 * @param {PageAPI} page The page object in which to look for the link tag.
291 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
292 	 *                                     DOM element.
293 	 * @return {boolean} True if there is a tag on the page that corresponds with
294 	 *                   the givn link.
295 	 */
296 	function hasTagForLink(page, $link) {
297 		var id = $link.attr('id');
298 		return !!(id && page._getBlockById(id));
299 	}
300 
301 	/**
302 	 * Checks whether or not the given part has a part type of the given
303 	 * name
304 	 * 
305 	 * @param {TagAPI} tag
306 	 * @param {string} part Part name
307 	 * @return {boolean} True of part exists in the given tag.
308 	 */
309 	function hasTagPart(tag, part) {
310 		return !!tag._data.properties[part];
311 	}
312 
313 	/**
314 	 * Updates the parts of a tag in the page object that corresponds to the
315 	 * given link DOM element.
316 	 *
317 	 * @param {PageAPI} page
318 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
319 	 *                                     DOM element.
320 	 */
321 	function updateLinkTag(page, $link) {
322 		var block = page._getBlockById($link.attr('id'));
323 		// ASSERT(block)
324 		var tag = page.tag(block.tagname);
325 		var parts = getTagPartsFromLink($link);
326 		var part;
327 		for (part in parts) {
328 			if (parts.hasOwnProperty(part) && hasTagPart(tag, part)) {
329 				tag.part(part, parts[part]);
330 			}
331 		}
332 	}
333 
334 	/**
335 	 * Creates a new tag for the given link in the page.
336 	 *
337 	 * @param {PageAPI} page
338 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
339 	 *                                     element.
340 	 * @param {function} success Callback function that whill be called when
341 	 *                           the new tag is created.
342 	 * @param {function(GCNError):boolean=} error Optional custom error
343 	 *                                            handler.
344 	 */
345 	function createLinkTag(page, $link, success, error) {
346 		page.createTag({
347 			keyword: GCN.settings.MAGIC_LINK,
348 			magicValue: $link.html()
349 		}).edit(function (html, tag) {
350 			// Copy over the rendered id value for the link so that we can bind
351 			// the link in the DOM with the newly created block.
352 			$link.attr('id', jQuery(html).attr('id'));
353 			updateLinkTag(page, $link);
354 			success();
355 		}, error);
356 	}
357 
358 	/**
359 	 * Identifies internal GCN links in the given page's rendered editables,
360 	 * and updates their corresponding content tags, or create new tags for the
361 	 * if they are new links.
362 	 *
363 	 *  @param {PageAPI} page
364 	 *  @param {function} success
365 	 *  @param {function} error
366 	 */
367 	function processGCNLinks(page, success, error) {
368 		var $links = getEditableLinks(page);
369 		var $gcnlinks = $links.filter(GCN_LINK_SELECTOR);
370 		if (0 === $gcnlinks.length) {
371 			if (success) {
372 				success();
373 			}
374 			return;
375 		}
376 		var numToProcess = $gcnlinks.length;
377 		var onSuccess = function () {
378 			if ((0 === --numToProcess) && success) {
379 				success();
380 			}
381 		};
382 		var onError = function (GCNError) {
383 			if (error) {
384 				return error(GCNError);
385 			}
386 		};
387 		var i;
388 		for (i = 0; i < $gcnlinks.length; i++) {
389 			if (hasTagForLink(page, $gcnlinks.eq(i))) {
390 				updateLinkTag(page, $gcnlinks.eq(i));
391 				onSuccess();
392 			} else {
393 				createLinkTag(page, $gcnlinks.eq(i), onSuccess, onError);
394 			}
395 		}
396 	}
397 
398 	/**
399 	 * Adds the given blocks into the page's list of blocks that are to be
400 	 * deleted when the page is saved.
401 	 *
402 	 * @param {PageAPI} page
403 	 * @param {Array.<object>} blocks Blocks that are to be marked for deletion.
404 	 */
405 	function deleteBlocks(page, blocks) {
406 		blocks = jQuery.isArray(blocks) ? blocks : [blocks];
407 		var i;
408 		for (i = 0; i < blocks.length; i++) {
409 			if (-1 ===
410 					jQuery.inArray(blocks[i].tagname, page._deletedBlocks)) {
411 				page._deletedBlocks.push(blocks[i].tagname);
412 			}
413 			delete page._blocks[blocks[i].element];
414 		}
415 	}
416 
417 	/**
418 	 * Removes all tags on the given page which belong to links that are no
419 	 * longer present in any of the page's rendered editables.
420 	 *
421 	 * @param {PageAPI} page
422 	 * @param {function} success Callback function that will be invoked when
423 	 *                           all obsolete tags have been successfully
424 	 *                           marked for deletion.
425 	 * @param {function(GCNError):boolean=} error Optional custom error
426 	 *                                            handler.
427 	 */
428 	function deleteObsoleteLinkTags(page, success, error) {
429 		getObsoleteBlocks(page, function (obsolete) {
430 			deleteBlocks(page, obsolete);
431 			if (success) {
432 				success();
433 			}
434 		}, error);
435 	}
436 
437 	/**
438 	 * Searches for the an Aloha editable object of the given id.
439 	 *
440 	 * @TODO: Once Aloha.getEditableById() is patched to not cause an
441 	 *        JavaScript exception if the element for the given ID is not found
442 	 *        then we can deprecate this function and use Aloha's instead.
443 	 *
444 	 * @static
445 	 * @param {string} id Id of Aloha.Editable object to find.
446 	 * @return {Aloha.Editable=} The editable object, if wound; otherwise null.
447 	 */
448 	function getAlohaEditableById(id) {
449 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
450 		if (!Aloha) {
451 			return null;
452 		}
453 
454 		// If the element is a textarea then route to the editable div.
455 		var element = jQuery('#' + id);
456 		if (element.length &&
457 				element[0].nodeName.toLowerCase() === 'textarea') {
458 			id += '-aloha';
459 		}
460 
461 		var editables = Aloha.editables;
462 		var j = editables.length;
463 		while (j) {
464 			if (editables[--j].getId() === id) {
465 				return editables[j];
466 			}
467 		}
468 
469 		return null;
470 	}
471 
472 	/**
473 	 * For a given list of editables and a list of blocks, determines in which
474 	 * editable each block is contained.  The result is a map of block sets.
475 	 * Each of these sets of blocks are mapped against the id of the editable
476 	 * in which they are rendered.
477 	 *
478 	 * @param {string} content The rendered content in which both the list of
479 	 *                         editables, and blocks are contained.
480 	 * @param {Array.<object>} editables A list of editables contained in the
481 	 *                                   content.
482 	 * @param {Array.<object>} blocks A list of blocks containd in the content.
483 	 * @return {object<string, Array>} A object whose keys are editable ids,
484 	 *                                  and whose values are arrays of blocks
485 	 *                                  contained in the corresponding
486 	 *                                  editable.
487 	 */
488 	function categorizeBlocksAgainstEditables(content, editables, blocks) {
489 		var i;
490 		var $content = jQuery('<div>' + content + '</div>');
491 		var sets = {};
492 		var editableId;
493 
494 		var editablesSelectors = [];
495 		for (i = 0; i < editables.length; i++) {
496 			editablesSelectors.push('#' + editables[i].element);
497 		}
498 
499 		var markerClass = 'aloha-editable-tmp-marker__';
500 		var $editables = $content.find(editablesSelectors.join(','));
501 		$editables.addClass(markerClass);
502 
503 		var $block;
504 		var $parent;
505 		for (i = 0; i < blocks.length; i++) {
506 			$block = $content.find('#' + blocks[i].element);
507 			if ($block.length) {
508 				$parent = $block.closest('.' + markerClass);
509 				if ($parent.length) {
510 					editableId = $parent.attr('id');
511 					if (editableId) {
512 						if (!sets[editableId]) {
513 							sets[editableId] = [];
514 						}
515 						sets[editableId].push(blocks[i]);
516 					}
517 				}
518 			}
519 		}
520 
521 		return sets;
522 	}
523 
524 	/**
525 	 * Causes the given editables to be tracked, so that when the content
526 	 * object is saved, these editables will be processed.
527 	 *
528 	 * @private
529 	 * @param {PageAPI} page
530 	 * @param {Array.<object>} editables A set of object representing
531 	 *                                   editable tags that have been
532 	 *                                   rendered.
533 	 */
534 	function trackEditables(page, editables) {
535 		if (!page.hasOwnProperty('_editables')) {
536 			page._editables = {};
537 		}
538 		var i;
539 		for (i = 0; i < editables.length; i++) {
540 			page._editables[editables[i].element] = editables[i];
541 		}
542 	}
543 
544 	/**
545 	 * Causes the given blocks to be tracked so that when the content object is
546 	 * saved, these editables will be processed.
547 	 *
548 	 * @private
549 	 * @param {PageAPI} page
550 	 * @param {Array.<object>} blocks An set of object representing block
551 	 *                                tags that have been rendered.
552 	 */
553 	function trackBlocks(page, blocks) {
554 		if (!page.hasOwnProperty('_blocks')) {
555 			page._blocks = {};
556 		}
557 		var i;
558 		for (i = 0; i < blocks.length; i++) {
559 			page._blocks[blocks[i].element] = blocks[i];
560 		}
561 	}
562 
563 	/**
564 	 * Associates a list of blocks with an editable so that it can later be
565 	 * determined which blocks are contained inside which editable.
566 	 *
567 	 * @param object
568 	 */
569 	function associateBlocksWithEditable(editable, blocks) {
570 		editable._gcnContainedBlocks = editable._gcnContainedBlocks
571 		                             ? editable._gcnContainedBlocks.concat(blocks)
572 		                             : blocks;
573 	}
574 
575 	/**
576 	 * @private
577 	 * @const
578 	 * @type {number}
579 	 */
580 	//var TYPE_ID = 10007;
581 
582 	/**
583 	 * @private
584 	 * @const
585 	 * @type {Enum}
586 	 */
587 	var STATUS = {
588 
589 		// page was not found in the database
590 		NOTFOUND: -1,
591 
592 		// page is locally modified and not yet (re-)published
593 		MODIFIED: 0,
594 
595 		// page is marked to be published (dirty)
596 		TOPUBLISH: 1,
597 
598 		// page is published and online
599 		PUBLISHED: 2,
600 
601 		// Page is offline
602 		OFFLINE: 3,
603 
604 		// Page is in the queue (publishing of the page needs to be affirmed)
605 		QUEUE: 4,
606 
607 		// page is in timemanagement and outside of the defined timespan
608 		// (currently offline)
609 		TIMEMANAGEMENT: 5,
610 
611 		// page is to be published at a given time (not yet)
612 		TOPUBLISH_AT: 6
613 	};
614 
615 	/**
616 	* @class
617 	* @name PageAPI
618 	* @extends ContentObjectAPI
619 	* @extends TagContainerAPI
620 	*
621 	* Page object information can be extended using the default REST-API.
622 	* options:
623 	*
624 	* - update: true
625 	* Whether the page should be locked in the backend when loading it.
626 	* default: true
627 	*
628 	* - template: true
629 	* Whether the template information should be embedded in the page object.
630 	* default: true
631 	*
632 	* - folder: true,
633 	* Whether the folder information should be embedded in the page object.
634 	* default: true
635 	* WARNING: do not turn this option off - it will leave the API in a broken
636 	*          state.
637 	*
638 	* - langvars: false,
639 	* When the language variants shall be embedded in the page response.
640 	* default: false
641 
642 	* - workflow: false,
643 	* When the workflow information shall be embedded in the page response.
644 	* default: false
645 
646 	* - pagevars: false,
647 	* When the page variants shall be embedded in the page response. Page
648 	* variants will contain folder information.
649 	* default: false
650 	*
651 	* - translationstatus: false
652 	* Will return information on the page's translation status.
653 	* default: false
654 	*/
655 	var PageAPI = GCN.defineChainback({
656 		/** @lends PageAPI */
657 
658 		__chainbacktype__: 'PageAPI',
659 		_extends: [ GCN.ContentObjectAPI, GCN.TagContainerAPI ],
660 		_type: 'page',
661 
662 		/**
663 		 * A hash set of block tags belonging to this page.  This set grows as
664 		 * this page's tags are rendered.
665 		 *
666 		 * @private
667 		 * @type {Array.<object>}
668 		 */
669 		_blocks: {},
670 
671 		/**
672 		 * A hash set of editable tags belonging to this page.  This set grows
673 		 * as this page's tags are rendered.
674 		 *
675 		 * @private
676 		 * @type {Array.<object>} 
677 		 */
678 		_editables: {},
679 
680 		/**
681 		 * Writable properties for the page object.
682 		 *
683 		 * @type {Array.string} Writable properties for the page object.
684 		 * @const
685 		 */
686 		WRITEABLE_PROPS: ['cdate',
687 		                  'description',
688 		                  'fileName',
689 		                  'folderId', // @TODO Check if moving a page is
690 		                              //       implemented correctly.
691 		                  'name',
692 		                  'priority',
693 		                  'templateId'],
694 
695 		/**
696 		 * Gets all blocks that are associated with this page.
697 <<<<<<< HEAD
698 		 *
699 		 * It is important to note that the set of blocks in the returned array
700 		 * will only include those that are the returned by the server when
701 		 * calling  edit() on a tag that belongs to this page. 
702 		 *
703 =======
704 		 *
705 		 * It is important to note that the set of blocks in the returned array
706 		 * will only include those that are the returned by the server when
707 		 * calling  edit() on a tag that belongs to this page. 
708 		 *
709 >>>>>>> c55e5259a789aa7d67b059a511259bb9651d9db8
710 		 * @return {Array.<object>} The set of blocks that have been
711 		 *                          initialized by calling edit() on one of
712 		 *                          this page's tags.
713 		 */
714 		'!blocks': function () {
715 			return this._blocks;
716 		},
717 
718 		/**
719 		 * Retrieves a block with the given id among the blocks that are
720 		 * tracked by this page content object.
721 		 *
722 		 * @private
723 		 * @param {string} id The block's id.
724 		 * @return {?object} The block data object.
725 		 */
726 		'!_getBlockById': function (id) {
727 			return this._blocks[id];
728 		},
729 
730 		/**
731 		 * Extracts the editables and blocks that have been rendered from the
732 		 * REST API render call's response data, and stores them in the page.
733 		 *
734 		 * @override
735 		 */
736 		'!_processRenderedTags': function (data) {
737 			var tags = this._getEditablesAndBlocks(data);
738 			var containedBlocks = categorizeBlocksAgainstEditables(
739 				data.content,
740 				tags.editables,
741 				tags.blocks
742 			);
743 
744 			trackEditables(this, tags.editables);
745 			trackBlocks(this, tags.blocks);
746 
747 			var editableId;
748 			for (editableId in containedBlocks) {
749 				if (containedBlocks.hasOwnProperty(editableId)
750 						&& this._editables[editableId]) {
751 					associateBlocksWithEditable(this._editables[editableId],
752 						containedBlocks[editableId]);
753 				}
754 			}
755 
756 			return tags;
757 		},
758 
759 		/**
760 		 * Processes this page's tags in preparation for saving.
761 		 *
762 		 * The preparation process:
763 		 *
764 		 * 1. For all editables associated with this page, determine which of
765 		 *    their blocks have been rendered into the DOM for editing so that
766 		 *    changes to the DOM can be reflected in the corresponding data
767 		 *    structures before pushing the tags to the server.
768 		 *
769 		 * 2. 
770 		 *
771 		 * Processes rendered tags, and updates the `_blocks' and `_editables'
772 		 * arrays accordingly.  This function is called during pre-saving to
773 		 * update this page's editable tags.
774 		 *
775 		 * @private
776 		 */
777 		'!_prepareTagsForSaving': function (success, error) {
778 			if (!this.hasOwnProperty('_deletedBlocks')) {
779 				this._deletedBlocks = [];
780 			}
781 			var page = this;
782 			processGCNLinks(page, function () {
783 				deleteObsoleteLinkTags(page, function () {
784 					page._updateEditableBlocks();
785 					if (success) {
786 						success();
787 					}
788 				}, error);
789 			}, error);
790 		},
791 
792 		/**
793 		 * Writes the contents of editables back into their corresponding tags.
794 		 * If a corresponding tag cannot be found for an editable, a new one
795 		 * will be created for it.
796 		 *
797 		 * A reference for each editable tag is then added to the `_shadow'
798 		 * object in order that the tag will be sent with the save request.
799 		 *
800 		 * @private
801 		 */
802 		'!_updateEditableBlocks': function () {
803 			var $element;
804 			var elementId;
805 			var editable;
806 			var editables = this._editables;
807 			var tags = this._data.tags;
808 			var tagname;
809 			var html;
810 			var alohaEditable;
811 			var $cleanElement;
812 			var customSerializer;
813 
814 			for (elementId in editables) {
815 				if (editables.hasOwnProperty(elementId)) {
816 					editable = editables[elementId];
817 					$element = jQuery('#' + elementId);
818 
819 					// If this editable has no element that was placed in the
820 					// DOM, then do not attempt to update it.
821 					if (0 === $element.length) {
822 						continue;
823 					}
824 
825 					tagname = editable.tagname;
826 
827 					if (!tags[tagname]) {
828 						tags[tagname] = {
829 							name       : tagname,
830 							active     : true,
831 							properties : {}
832 						};
833 					} else {
834 						// Make sure, that all tags (which relate to editables)
835 						// are activated.
836 						tags[tagname].active = true;
837 					}
838 
839 					// If the editable element has been aloha()fied, then we
840 					// need to use getContents() from is corresponding
841 					// Aloha.Editable object in order to get clean HTML.
842 
843 					alohaEditable = getAlohaEditableById(elementId);
844 
845 					if (alohaEditable) {
846 						// Avoid the unnecessary overhead of custom editable
847 						// serialization by calling html ourselves.
848 						$cleanElement = jQuery('<div>').append(
849 							alohaEditable.getContents(true)
850 						);
851 						alohaEditable.setUnmodified();
852 						// Apply the custom editable serialization as the last step.
853 						customSerializer = window.Aloha.Editable.getContentSerializer();
854 						html = this.encode($cleanElement, customSerializer);
855 					} else {
856 						html = this.encode($element);
857 					}
858 
859 					tags[tagname].properties[editable.partname] = {
860 						stringValue: html,
861 						type: 'RICHTEXT'
862 					};
863 
864 					this._update('tags.' + GCN.escapePropertyName(tagname),
865 						tags[tagname]);
866 				}
867 			}
868 		},
869 
870 		/**
871 		 * @see ContentObjectAPI.!_loadParams
872 		 */
873 		'!_loadParams': function () {
874 			return jQuery.extend(DEFAULT_SETTINGS, this._settings);
875 		},
876 
877 		/**
878 		 * Get this page's template.
879 		 *
880 		 * @public
881 		 * @function
882 		 * @name template
883 		 * @memberOf PageAPI
884 		 * @param {funtion(TemplateAPI)=} success Optional callback to receive
885 		 *                                        a {@link TemplateAPI} object
886 		 *                                        as the only argument.
887 		 * @param {function(GCNError):boolean=} error Optional custom error
888 		 *                                            handler.
889 		 * @return {TemplateAPI} This page's parent template.
890 		 */
891 		'!template': function (success, error) {
892 			var id = this._fetched ? this.prop('templateId') : null;
893 			return this._continue(GCN.TemplateAPI, id, success, error);
894 		},
895 
896 		/**
897 		 * @override
898 		 * @see ContentObjectAPI._save
899 		 */
900 		'!_save': function (settings, success, error) {
901 			var that = this;
902 			this._fulfill(function () {
903 				that._read(function () {
904 					var fork = that._fork();
905 					fork._prepareTagsForSaving(function () {
906 						fork._persist(settings, function () {
907 							if (success) {
908 								that._invoke(success, [that]);
909 							}
910 							fork._merge();
911 						}, error);
912 					}, error);
913 				}, error);
914 			}, error);
915 		},
916 
917 		//---------------------------------------------------------------------
918 		// Surface the tag container methods that are applicable for GCN page
919 		// objects.
920 		//---------------------------------------------------------------------
921 
922 		/**
923 		 * Creates a tag of a given tagtype in this page.
924 		 * The first parameter should either be the construct keyword or ID,
925 		 * or an object containing exactly one of the following property sets:<br/>
926 		 * <ol>
927 		 *   <li><i>keyword</i> to create a tag based on the construct with given keyword</li>
928 		 *   <li><i>constructId</i> to create a tag based on the construct with given ID</li>
929 		 *   <li><i>sourcePageId</i> and <i>sourceTagname</i> to create a tag as copy of the given tag from the page</li>
930 		 * </ol>
931 		 * 
932 		 * Exmaple:
933 		 * <pre>
934 		 *  createTag('link', onSuccess, onError);
935 		 * </pre>
936 		 * or
937 		 * <pre>
938 		 *  createTag({keyword: 'link', magicValue: 'http://www.gentics.com'}, onSuccess, onError);
939 		 * </pre>
940 		 * or
941 		 * <pre>
942 		 *  createTag({sourcePageId: 4711, sourceTagname: 'link'}, onSuccess, onError);
943 		 * </pre>
944 		 *
945 		 * @public
946 		 * @function
947 		 * @name createTag
948 		 * @memberOf PageAPI
949 		 * @param {string|number|object} construct either the keyword of the construct, or the ID of the construct
950 		 *                               or an object with the following properties
951 		 *                               <ul>
952 		 *                                <li><i>keyword</i> keyword of the construct</li>
953 		 *                                <li><i>constructId</i> ID of the construct</li>
954 		 *                                <li><i>magicValue</i> magic value to be filled into the tag</li>
955 		 *                                <li><i>sourcePageId</i> source page id</li>
956 		 *                                <li><i>sourceTagname</i> source tag name</li>
957 		 *                               </ul>
958 		 * @param {function(TagAPI)=} success Optional callback that will
959 		 *                                    receive the newly created tag as
960 		 *                                    its only argument.
961 		 * @param {function(GCNError):boolean=} error Optional custom error
962 		 *                                            handler.
963 		 * @return {TagAPI} The newly created tag.
964 		 * @throws INVALID_ARGUMENTS
965 		 */
966 		'!createTag': function () {
967 			return this._createTag.apply(this, arguments);
968 		},
969 
970 		/**
971 		 * Deletes the specified tag from this page.
972 		 *
973 		 * @public
974 		 * @function
975 		 * @name removeTag
976 		 * @memberOf PageAPI
977 		 * @param {string} id The id of the tag to be deleted.
978 		 * @param {function(PageAPI)=} success Optional callback that receive
979 		 *                                     this object as its only
980 		 *                                     argument.
981 		 * @param {function(GCNError):boolean=} error Optional custom error
982 		 *                                            handler.
983 		 */
984 		removeTag: function () {
985 			this._removeTag.apply(this, arguments);
986 		},
987 
988 		/**
989 		 * Deletes a set of tags from this page.
990 		 *
991 		 * @public
992 		 * @function
993 		 * @name removeTags
994 		 * @memberOf PageAPI
995 		 * @param {Array.<string>} ids The ids of the set of tags to be
996 		 *                             deleted.
997 		 * @param {function(PageAPI)=} success Optional callback that receive
998 		 *                                     this object as its only
999 		 *                                     argument.
1000 		 * @param {function(GCNError):boolean=} error Optional custom error
1001 		 *                                            handler.
1002 		 */
1003 		removeTags: function () {
1004 			this._removeTags.apply(this, arguments);
1005 		},
1006 
1007 		/**
1008 		 * Marks the page as to be taken offline. This method will change the
1009 		 * state of the page object.
1010 		 *
1011 		 * @public
1012 		 * @function
1013 		 * @name takeOffline
1014 		 * @memberOf PageAPI
1015 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
1016 		 *                                    page object as the only argument.
1017 		 * @param {function(GCNError):boolean=} error Optional custom error
1018 		 *                                            handler.
1019 		 */
1020 		takeOffline: function (success, error) {
1021 			var that = this;
1022 
1023 			this._read(function () {
1024 				that._update('status', STATUS.OFFLINE, error);
1025 				if (success) {
1026 					that._save(null, success, error);
1027 				}
1028 			}, error);
1029 		},
1030 
1031 		/**
1032 		 * Trigger publish process for the page.
1033 		 *
1034 		 * @public
1035 		 * @function
1036 		 * @name publish
1037 		 * @memberOf PageAPI
1038 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
1039 		 *                                    page object as the only argument.
1040 		 * @param {function(GCNError):boolean=} error Optional custom error
1041 		 *                                            handler.
1042 		 */
1043 		publish: function (success, error) {
1044 			var that = this;
1045 			this._fulfill(function () {
1046 				that._authAjax({
1047 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
1048 					     '/publish/' + that.id(),
1049 					type: 'POST',
1050 					json: {}, // There needs to be at least empty content
1051 					          // because of a bug in Jersey.
1052 					success: function (response) {
1053 						that._data.status = STATUS.PUBLISHED;
1054 						if (success) {
1055 							that._invoke(success, [that]);
1056 						}
1057 					},
1058 					error: error
1059 				});
1060 			});
1061 		},
1062 
1063 		/**
1064 		 * Renders a preview of the current page.
1065 		 *
1066 		 * @public
1067 		 * @function
1068 		 * @name preview
1069 		 * @memberOf PageAPI
1070 		 * @param {function(string, PageAPI)} success Callback to receive the
1071 		 *                                            rendered page preview as
1072 		 *                                            the first argument, and
1073 		 *                                            this page object as the
1074 		 *                                            second.
1075 		 * @param {function(GCNError):boolean=} error Optional custom error
1076 		 *                                            handler.
1077 		 */
1078 		preview: function (success, error) {
1079 			var that = this;
1080 
1081 			this._read(function () {
1082 				that._authAjax({
1083 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
1084 					     '/preview/',
1085 					json: {
1086 						page: that._data // @FIXME Shouldn't this a be merge of
1087 						                 //        the `_shadow' object and the
1088 										 //        `_data'.
1089 					},
1090 					type: 'POST',
1091 					error: error,
1092 					success: function (response) {
1093 						if (success) {
1094 							GCN._handleContentRendered(response.preview, that,
1095 								function (html) {
1096 									that._invoke(success, [html, that]);
1097 								});
1098 						}
1099 					}
1100 				});
1101 			}, error);
1102 		},
1103 
1104 		/**
1105 		 * Unlocks the page when finishing editing
1106 		 *
1107 		 * @public
1108 		 * @function
1109 		 * @name unlock
1110 		 * @memberOf PageAPI
1111 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
1112 		 *                                    page object as the only argument.
1113 		 * @param {function(GCNError):boolean=} error Optional custom error
1114 		 *                                            handler.
1115 		 */
1116 		unlock: function (success, error) {
1117 			var that = this;
1118 			this._fulfill(function () {
1119 				that._authAjax({
1120 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
1121 					     '/cancel/' + that.id(),
1122 					type: 'POST',
1123 					json: {}, // There needs to be at least empty content
1124 					          // because of a bug in Jersey.
1125 					error: error,
1126 					success: function (response) {
1127 						if (success) {
1128 							that._invoke(success, [that]);
1129 						}
1130 					}
1131 				});
1132 			});
1133 		},
1134 
1135 		/**
1136 		 * @see GCN.ContentObjectAPI._processResponse
1137 		 */
1138 		'!_processResponse': function (data) {
1139 			jQuery.extend(this._data, data[this._type]);
1140 
1141 			// if data contains page variants turn them into page objects
1142 			if (this._data.pageVariants) {
1143 				var pagevars = [];
1144 				var i;
1145 				for (i = 0; i < this._data.pageVariants.length; i++) {
1146 					pagevars.push(this._continue(GCN.PageAPI,
1147 						this._data.pageVariants[i]));
1148 				}
1149 				this._data.pageVariants = pagevars;
1150 			}
1151 		},
1152 
1153 		/**
1154 		 * @override
1155 		 */
1156 		'!_removeAssociatedTagData': function (tagid) {
1157 			var block;
1158 			for (block in this._blocks) {
1159 				if (this._blocks.hasOwnProperty(block) &&
1160 						this._blocks[block].tagname === tagid) {
1161 					delete this._blocks[block];
1162 				}
1163 			}
1164 
1165 			var editable;
1166 			for (editable in this._editables) {
1167 				if (this._editables.hasOwnProperty(editable) &&
1168 						this._editables[editable].tagname === tagid) {
1169 					delete this._editables[editable];
1170 				}
1171 			}
1172 		}
1173 
1174 	});
1175 
1176 	GCN.page = GCN.exposeAPI(PageAPI);
1177 	GCN.PageAPI = PageAPI;
1178 
1179 }(GCN));
1180