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