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