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