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