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