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