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