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 	 * @private
 14 	 * @const
 15 	 * @type {object.<string, boolean>} Default page settings.
 16 	 */
 17 	var DEFAULT_SETTINGS = {
 18 		// Load folder information
 19 		folder: true,
 20 
 21 		// Lock page when loading it
 22 		update: true,
 23 
 24 		// Have language variants be included in the page response.
 25 		langvars: true,
 26 
 27 		// Have page variants be included in the page response.
 28 		pagevars: true
 29 	};
 30 
 31 	/**
 32 	 * Searches for the an Aloha editable object of the given id.
 33 	 *
 34 	 * @TODO: Once Aloha.getEditableById() is patched to not cause an
 35 	 *        JavaScript exception if the element for the given ID is not found
 36 	 *        then we can deprecate this function and use Aloha's instead.
 37 	 *
 38 	 * @static
 39 	 * @param {string} id Id of Aloha.Editable object to find.
 40 	 * @return {Aloha.Editable=} The editable object, if wound; otherwise null.
 41 	 */
 42 	function getAlohaEditableById(id) {
 43 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
 44 		if (!Aloha) {
 45 			return null;
 46 		}
 47 
 48 		// If the element is a textarea then route to the editable div.
 49 		var element = jQuery('#' + id);
 50 		if (element.length &&
 51 				element[0].nodeName.toLowerCase() === 'textarea') {
 52 			id += '-aloha';
 53 		}
 54 
 55 		var editables = Aloha.editables;
 56 		var j = editables.length;
 57 		while (j) {
 58 			if (editables[--j].getId() === id) {
 59 				return editables[j];
 60 			}
 61 		}
 62 
 63 		return null;
 64 	}
 65 
 66 	/**
 67 	 * Checks whether the given tag is a magic link block.
 68 	 *
 69 	 * @private
 70 	 * @static
 71 	 * @param {GCN.Tag} tag Must be a tag that has already been fetched.
 72 	 * @param {object} constructs Set of constructs.
 73 	 * @return {boolean} True if the given tag has the magic link constructId.
 74 	 */
 75 	function isMagicLinkTag(tag, constructs) {
 76 		return !!(constructs[GCN.settings.MAGIC_LINK] &&
 77 		          (constructs[GCN.settings.MAGIC_LINK].constructId ===
 78 			       tag.prop('constructId')));
 79 	}
 80 
 81 	/**
 82 	 * Checks whether or not the given block has a corresponding element in the
 83 	 * document DOM.
 84 	 *
 85 	 * @private
 86 	 * @static
 87 	 * @param {object}
 88 	 * @return {boolean} True if an inline element for this block exists.
 89 	 */
 90 	function hasInlineElement(block) {
 91 		return 0 < jQuery('#' + block.element).length;
 92 	}
 93 
 94 	/**
 95 	 * For a given list of editables and a list of blocks, determine in which
 96 	 * editable each block was rendered.  The result is map with a list of
 97 	 * blocks mapped against the id of the rendered editable element.
 98 	 *
 99 	 * @param {string} content The rendered content in which the list of
100 	 *                         editables, and blocks are contained.
101 	 * @param {Array.<object>} editables A list of editables contained in the
102 	 *                                   rendered content.
103 	 * @param {Array.<object>} blocks A list of blocks containd in content
104 	 *                                string string.
105 	 * @return {object<string, <Array>} A object whose keys are editable ids,
106 	 *                                  and whose values are Array of block
107 	 *                                  contained in the corresponding
108 	 *                                  editable.
109 	 */
110 	function getMapOfBlocksToEditables(content, editables, blocks) {
111 		var i;
112 		var $content = jQuery('<div>' + content + '</div>');
113 		var map = {};
114 		var editableElementId;
115 
116 		var selection = [];
117 		for (i = 0; i < editables.length; i++) {
118 			selection.push('#' + editables[i].element);
119 		}
120 
121 		var markerClass = 'aloha-editable-tmp-marker__';
122 		var $editables = $content.find(selection.join(','));
123 		$editables.addClass(markerClass);
124 
125 		var $block;
126 		var $parent;
127 		for (i = 0; i < blocks.length; i++) {
128 			$block = $content.find('#' + blocks[i].element);
129 			if ($block.length) {
130 				$parent = $block.parents('.' + markerClass);
131 				if ($parent.length) {
132 					editableElementId = $parent.attr('id');
133 					if (editableElementId) {
134 						if (!map[editableElementId]) {
135 							map[editableElementId] = [];
136 						}
137 						map[editableElementId].push(blocks[i]);
138 					}
139 				}
140 			}
141 		}
142 
143 		return map;
144 	}
145 
146 	/**
147 	 * @private
148 	 * @const
149 	 * @type {number}
150 	 */
151 	//var TYPE_ID = 10007;
152 
153 	/**
154 	 * @private
155 	 * @const
156 	 * @type {Enum}
157 	 */
158 	var STATUS = {
159 
160 		// page was not found in the database
161 		NOTFOUND: -1,
162 
163 		// page is locally modified and not yet (re-)published
164 		MODIFIED: 0,
165 
166 		// page is marked to be published (dirty)
167 		TOPUBLISH: 1,
168 
169 		// page is published and online
170 		PUBLISHED: 2,
171 
172 		// Page is offline
173 		OFFLINE: 3,
174 
175 		// Page is in the queue (publishing of the page needs to be affirmed)
176 		QUEUE: 4,
177 
178 		// page is in timemanagement and outside of the defined timespan
179 		// (currently offline)
180 		TIMEMANAGEMENT: 5,
181 
182 		// page is to be published at a given time (not yet)
183 		TOPUBLISH_AT: 6
184 	};
185 
186 	/**
187 	 * Given a link, will read the data-gentics-aloha-object-id attribute and
188 	 * from it, will determine the backend object that was selected by the
189 	 * repository browser.
190 	 *
191 	 * @param {jQuery.<HTMLElement>} link A link in an editable.
192 	 * @return {Object}
193 	 *        A map containing the gtxalohapagelink part keyword and
194 	 *        value. The keyword may be either be 'url' or 'fileurl'
195 	 *        depending on the type of object linked to. The value may
196 	 *        be a string url ('http://...') for external links or an
197 	 *        integer for internal links.
198 	 */
199 	function getRepositoryLinkAsPart(link) {
200 		var data = link.attr('data-gentics-aloha-object-id');
201 		var value;
202 		var keyword;
203 		if (data) {
204 			var parts = data.split('.');
205 			if (parts.length === 2) {
206 				if (parts[0] === '10008' || parts[0] === '10011') {
207 					keyword = 'fileurl';
208 				} else { // assumes 10007 for page links
209 					keyword = 'url';
210 				}
211 				value = parts[1];
212 			} else {
213 				keyword = 'url';
214 				value = data;
215 			}
216 			if (null !== value && typeof value !== 'undefined') {
217 				value = parseInt(value, 10);
218 			}
219 		} else {
220 			keyword = 'url';
221 			value = link.attr('data-gentics-gcn-url');
222 		}
223 		return {
224 			keyword: keyword,
225 			value: value
226 		};
227 	}
228 
229 	/**
230 	* @class
231 	* @name PageAPI
232 	* @extends ContentObjectAPI
233 	* @extends TagContainerAPI
234 	*
235 	* Page object information can be extended using the default REST-API.
236 	* options:
237 	*
238 	* - update: true
239 	* Whether the page should be locked in the backend when loading it.
240 	* default: true
241 	*
242 	* - template: true
243 	* Whether the template information should be embedded in the page object.
244 	* default: true
245 	*
246 	* - folder: true,
247 	* Whether the folder information should be embedded in the page object.
248 	* default: true
249 	* WARNING: do not turn this option off - it will leave the API in a broken
250 	*          state.
251 	*
252 	* - langvars: false,
253 	* When the language variants shall be embedded in the page response.
254 	* default: false
255 
256 	* - workflow: false,
257 	* When the workflow information shall be embedded in the page response.
258 	* default: false
259 
260 	* - pagevars: false,
261 	* When the page variants shall be embedded in the page response. Page
262 	* variants will contain folder information.
263 	* default: false
264 	*
265 	* - translationstatus: false
266 	* Will return information on the page's translation status.
267 	* default: false
268 	*/
269 	var PageAPI = GCN.defineChainback({
270 		/** @lends PageAPI */
271 
272 		__chainbacktype__: 'PageAPI',
273 		_extends: [ GCN.ContentObjectAPI, GCN.TagContainerAPI ],
274 		_type: 'page',
275 
276 		/**
277 		 * @private
278 		 * @type {Array.<object>} A hash set of block tags belonging to this
279 		 *                        content object.  This set is added to when
280 		 *                        this page's tags are rendered.
281 		 */
282 		_blocks: {},
283 
284 		/**
285 		 * @private
286 		 * @type {Array.<object>} A hash set of editable tags belonging to this
287 		 *                        content object.  This set is added to when
288 		 *                        this page's tags are rendered.
289 		 */
290 		_editables: {},
291 
292 		/**
293 		 * @type {Array.string} Writable properties for the page object.
294 		 */
295 		WRITEABLE_PROPS: ['cdate',
296 		                  'description',
297 		                  'fileName',
298 		                  'folderId', // @TODO Check if moving a page is
299 		                              //       implemented correctly.
300 		                  'name',
301 		                  'priority',
302 		                  'templateId'],
303 
304 		/**
305 		 * Gets all blocks in this page.  Will return an array of all block
306 		 * objects found in the page AFTER they have been rendered using an
307 		 * `edit()' call for a contenttag.
308 		 * NOTE: If you have just loaded the page and not used the `edit()'
309 		 *       method for any tag the array will be empty.  Only those blocks
310 		 *       that have been initialized using `edit()' will be available.
311 		 *
312 		 * @return {Array.<object>} Array of block objects.
313 		 */
314 		'!blocks': function () {
315 			return this._blocks;
316 		},
317 
318 		/**
319 		 * Looks for a block with the given id in the `_blocks' array.
320 		 *
321 		 * @private
322 		 * @param {string} id The block's id.
323 		 * @return {?object} The block data object.
324 		 */
325 		'!_getBlockById': function (id) {
326 			return this._blocks[id];
327 		},
328 
329 		/**
330 		 * Maps the received editables into this content object's `_editable'
331 		 * hash.
332 		 *
333 		 * @private
334 		 * @param {Array.<object>} editables An set of object representing
335 		 *                                   editable tags that have been
336 		 *                                   rendered.
337 		 */
338 		'!_storeRenderedEditables': function (editables) {
339 			if (!this.hasOwnProperty('_editables')) {
340 				this._editables = {};
341 			}
342 
343 			var j = editables && editables.length;
344 
345 			while (j) {
346 				this._editables[editables[--j].element] = editables[j];
347 			}
348 		},
349 
350 		/**
351 		 * Maps received blocks of this content object into the `_blocks' hash.
352 		 *
353 		 * @private
354 		 * @param {Array.<object>} blocks An set of object representing
355 		 *                                block tags that have been rendered.
356 		 */
357 		'!_storeRenderedBlocks': function (blocks) {
358 			if (!this.hasOwnProperty('_blocks')) {
359 				this._blocks = {};
360 			}
361 
362 			var j = blocks && blocks.length;
363 
364 			while (j) {
365 				this._blocks[blocks[--j].element] = blocks[j];
366 			}
367 		},
368 
369 		/**
370 		 * @override
371 		 */
372 		'!_processRenderedTags': function (data) {
373 			var tags = this._getEditablesAndBlocks(data);
374 			var map = getMapOfBlocksToEditables(data.content, tags.editables,
375 				tags.blocks);
376 
377 			this._storeRenderedEditables(tags.editables);
378 			this._storeRenderedBlocks(tags.blocks);
379 
380 			var editableId;
381 			for (editableId in map) {
382 				if (map.hasOwnProperty(editableId) &&
383 						this._editables[editableId]) {
384 					if (!this._editables[editableId]._gcnContainedBlocks) {
385 						this._editables[editableId]._gcnContainedBlocks =
386 							map[editableId];
387 					} else {
388 						this._editables[editableId]._gcnContainedBlocks =
389 							this._editables[editableId]._gcnContainedBlocks
390 							    .concat(map[editableId]);
391 					}
392 				}
393 			}
394 
395 			return tags;
396 		},
397 
398 		/**
399 		 * Processes rendered tags, and update the `_blocks' and `_editables'
400 		 * array accordingly.  This function is called during pre-saving to
401 		 * update this page's editable tags.
402 		 *
403 		 * @private
404 		 */
405 		'!_prepareTagsForSaving': function (success, error) {
406 			if (!this.hasOwnProperty('_deletedBlocks')) {
407 				this._deletedBlocks = [];
408 			}
409 
410 			var editableId;
411 			var containedBlocks = [];
412 			for (editableId in this._editables) {
413 				if (this._editables.hasOwnProperty(editableId)) {
414 					if (this._editables[editableId]._gcnContainedBlocks) {
415 						containedBlocks = containedBlocks.concat(
416 							this._editables[editableId]._gcnContainedBlocks
417 						);
418 					}
419 				}
420 			}
421 
422 			var blocks = [];
423 			var j = containedBlocks.length;
424 			while (j--) {
425 				if (this._blocks[containedBlocks[j]]) {
426 					blocks.push(containedBlocks[j]);
427 				}
428 			}
429 
430 			var that = this;
431 			this._addNewLinkBlocks(function () {
432 				GCN.Admin.constructs(function (constructs) {
433 					that._removeOldLinkBlocks(blocks, constructs, function () {
434 						that._removeUnusedLinkBlocks(blocks, constructs, function () {
435 							that._updateEditableBlocks();
436 							if (success) {
437 								success();
438 							}
439 						}, error);
440 					}, error);
441 				}, error);
442 			}, error);
443 		},
444 
445 		/**
446 		 * Removes any link blocks that existed in rendered tags, but have
447 		 * since been removed by the user while editing.
448 		 *
449 		 * @private
450 		 * @param {Array.<object>} blocks An array of blocks belonging to this
451 		 *                                page.
452 		 * @param {object} constrcts A set of constructs.
453 		 * @param {function} success
454 		 * @param {function(GCNError):boolean=} error Optional custom error
455 		 *                                            handler.
456 		 */
457 		'!_removeUnusedLinkBlocks': function (blocks, constructs, success,
458 		                                      error) {
459 			if (0 === blocks.length) {
460 				if (success) {
461 					success();
462 				}
463 				return;
464 			}
465 
466 			var j = blocks.length;
467 			var numToProcess = j;
468 			var that = this;
469 
470 			var onProcess = function () {
471 				if (0 === --numToProcess) {
472 					if (success) {
473 						success();
474 					}
475 				}
476 			};
477 
478 			var onError = function (GCNError) {
479 				if (error) {
480 					error(GCNError);
481 				}
482 				return;
483 			};
484 
485 			var createBlockTagProcessor = function (block) {
486 				return function (tag) {
487 					if (isMagicLinkTag(tag, constructs) &&
488 							!hasInlineElement(block)) {
489 						that._deletedBlocks.push(block.tagname);
490 					}
491 					onProcess();
492 				};
493 			};
494 
495 			while (j--) {
496 				this.tag(blocks[j].tagname,
497 					createBlockTagProcessor(blocks[j]), onError);
498 			}
499 		},
500 
501 		/**
502 		 * Adds any newly created link blocks into this page object.  This is
503 		 * done by looking for all link blocks that do not have corresponding
504 		 * tag in this object's `_blocks' list.  For each anchor tag we find,
505 		 * create a tag for it and, add it in the list of tags.
506 		 *
507 		 * @private
508 		 * @param {function} success Function to invoke if this function
509 		 *                           successeds.
510 		 * @param {function(GCNError):boolean=} error Optional custom error
511 		 *                                            handler.
512 		 */
513 		'!_addNewLinkBlocks': function (success, error) {
514 			// Limit the search for links to be done only withing rendered
515 			// editables.
516 			var id;
517 			var $editables = jQuery();
518 			for (id in this._editables) {
519 				if (this._editables.hasOwnProperty(id)) {
520 					$editables = $editables.add('#' + id);
521 				}
522 			}
523 
524 			var selector = [
525 				'a[data-gentics-aloha-repository="com.gentics.aloha.GCN.Page"]',
526 				'a[data-GENTICS-aloha-repository="com.gentics.aloha.GCN.Page"]',
527 				'a[data-gentics-gcn-url]'
528 			].join(',');
529 
530 			var links = $editables.find(selector);
531 			if (0 === links.length) {
532 				if (success) {
533 					success();
534 				}
535 				return;
536 			}
537 
538 			var link;
539 			var j = links.length;
540 			var numToProcess = j;
541 			var that = this;
542 
543 			var onProcessed = function () {
544 				if (0 === --numToProcess) {
545 					success();
546 				}
547 			};
548 
549 			var onError = function (GCNError) {
550 				if (error) {
551 					error(GCNError);
552 				}
553 				return;
554 			};
555 
556 			var createOnEditHandler = function (link) {
557 				return function (html, tag) {
558 					link.attr('id', jQuery(html).attr('id'));
559 					var part = getRepositoryLinkAsPart(link);
560 					tag.part(part.keyword, part.value);
561 					onProcessed();
562 				};
563 			};
564 
565 			var tag;
566 
567 			while (j--) {
568 				link = links.eq(j);
569 				if (link.attr('data-gcnignore') === true) {
570 					onProcessed();
571 				} else if (this._getBlockById(link.attr('id'))) {
572 					tag = this.tag(this._getBlockById(link.attr('id')).tagname);
573 					tag.part('text', link.html());
574 					var part = getRepositoryLinkAsPart(link);
575 					tag.part(part.keyword, part.value);
576 					onProcessed();
577 				} else {
578 					this.createTag({keyword: GCN.settings.MAGIC_LINK, magicValue: link.html()})
579 					    .edit(createOnEditHandler(link), onError);
580 				}
581 			}
582 		},
583 
584 		/**
585 		 * Any links that change from internal GCN links to external links will
586 		 * have their corresponding blocks added to the '_deletedBlocks' list
587 		 * since they these links no longer need to be tracked.  Any tags in
588 		 * this list will be removed during saving.
589 		 *
590 		 * @private
591 		 * @param {Array.<object>} blocks An array of blocks belonging to this
592 		 *                                page.
593 		 * @param {object} constrcts A set of constructs.
594 		 * @param {function} success
595 		 * @param {function(GCNError):boolean=} error Optional custom error
596 		 *                                            handler.
597 		 */
598 		'!_removeOldLinkBlocks': function (blocks, constructs, success, error) {
599 			if (0 === blocks.length) {
600 				if (success) {
601 					success();
602 				}
603 				return;
604 			}
605 
606 			var $links = jQuery('a');
607 			var j = blocks.length;
608 			var numToProcess = j;
609 			var that = this;
610 
611 			var onProcess = function () {
612 				if (0 === --numToProcess) {
613 					if (success) {
614 						success();
615 					}
616 				}
617 			};
618 
619 			var onError = function (GCNError) {
620 				if (error) {
621 					error(GCNError);
622 				}
623 				return;
624 			};
625 
626 			var createBlockTagProcessor = function (block) {
627 				return function (tag) {
628 					if (!isMagicLinkTag(tag, constructs)) {
629 						onProcess();
630 						return;
631 					}
632 
633 					var a = $links.filter('#' + block.element);
634 
635 					if (a.length) {
636 						var isExternal = (GCN_REPOSITORY_ID !==
637 							a.attr('data-gentics-aloha-repository')) &&
638 							!a.attr('data-gentics-gcn-url');
639 
640 						// An external tag was found.  Stop tracking it and
641 						// remove it from the list of blocks.
642 						if (isExternal) {
643 							a.removeAttr('id');
644 							that._deletedBlocks.push(block.tagname);
645 							delete that._blocks[block.element];
646 						}
647 
648 					// No anchor tag was found for this block.  Add it to the
649 					// "delete" list.
650 					} else {
651 						that._deletedBlocks.push(block.tagname);
652 						delete that._blocks[block.element];
653 					}
654 
655 					onProcess();
656 				};
657 			};
658 
659 			while (j) {
660 				this.tag(blocks[--j].tagname,
661 					createBlockTagProcessor(blocks[j]), onError);
662 			}
663 		},
664 
665 		/**
666 		 * Writes the contents of editables back into their corresponding tags.
667 		 * If a corresponding tag cannot be found for an editable, a new one
668 		 * will be created for it.
669 		 *
670 		 * A reference for each editable tag is then added to the `_shadow'
671 		 * object in order that the tag will be sent with the save request.
672 		 *
673 		 * @private
674 		 */
675 		'!_updateEditableBlocks': function () {
676 			var element;
677 			var elementId;
678 			var editable;
679 			var editables = this._editables;
680 			var tags = this._data.tags;
681 			var tagname;
682 			var html;
683 			var alohaEditable;
684 			var cleanElement;
685 			var customSerializer;
686 
687 			// ASSERT(tag)
688 
689 			for (elementId in editables) {
690 				if (editables.hasOwnProperty(elementId)) {
691 					editable = editables[elementId];
692 					element = jQuery('#' + elementId);
693 
694 					// If this editable has no element that was placed in the
695 					// DOM, then do not attempt to update it.
696 					if (0 === element.length) {
697 						continue;
698 					}
699 
700 					tagname = editable.tagname;
701 
702 					if (!tags[tagname]) {
703 						tags[tagname] = {
704 							name       : tagname,
705 							active     : true,
706 							properties : {}
707 						};
708 					} else {
709 						// make sure, that all tags (which relate to editables)
710 						// are activated
711 						tags[tagname].active = true;
712 					}
713 
714 					// If the editable element has been `aloha()'fied, then we
715 					// need to use `getContents()' from is corresponding
716 					// Aloha.Editable object in order to get clean HTML.
717 
718 					alohaEditable = getAlohaEditableById(elementId);
719 
720 					if (alohaEditable) {
721 						// Avoid the unnecessary overhead of custom editable
722 						// serialization by calling html ourselves.
723 						cleanElement = jQuery('<div>')
724 							.append(alohaEditable.getContents(true));
725 						alohaEditable.setUnmodified();
726 						// Apply the custom editable serialization as the last step.
727 						customSerializer = window.Aloha.Editable.getContentSerializer();
728 						html = this.encode(cleanElement, customSerializer);
729 					} else {
730 						html = this.encode(element);
731 					}
732 
733 					tags[tagname].properties[editable.partname] = {
734 						stringValue: html,
735 						type: 'RICHTEXT'
736 					};
737 
738 					this._update('tags.' + GCN.escapePropertyName(tagname),
739 						tags[tagname]);
740 				}
741 			}
742 		},
743 
744 		/**
745 		 * @see ContentObjectAPI.!_loadParams
746 		 */
747 		'!_loadParams': function () {
748 			return jQuery.extend(DEFAULT_SETTINGS, this._settings);
749 		},
750 
751 		/**
752 		 * Get this page's template.
753 		 *
754 		 * @public
755 		 * @function
756 		 * @name template
757 		 * @memberOf PageAPI
758 		 * @param {funtion(TemplateAPI)=} success Optional callback to receive
759 		 *                                        a {@link TemplateAPI} object
760 		 *                                        as the only argument.
761 		 * @param {function(GCNError):boolean=} error Optional custom error
762 		 *                                            handler.
763 		 * @return {TemplateAPI} This page's parent template.
764 		 */
765 		'!template': function (success, error) {
766 			var id = this._fetched ? this.prop('templateId') : null;
767 			return this._continue(GCN.TemplateAPI, id, success, error);
768 		},
769 
770 		/**
771 		 * @override
772 		 * @see ContentObjectAPI._save
773 		 */
774 		'!_save': function (settings, success, error) {
775 			var that = this;
776 			this._fulfill(function () {
777 				that._read(function () {
778 					var fork = that._fork();
779 					fork._prepareTagsForSaving(function () {
780 						fork._persist(settings, function () {
781 							if (success) {
782 								that._invoke(success, [that]);
783 							}
784 							fork._merge();
785 						}, error);
786 					}, error);
787 				}, error);
788 			}, error);
789 		},
790 
791 		//---------------------------------------------------------------------
792 		// Surface the tag container methods that are applicable for GCN page
793 		// objects.
794 		//---------------------------------------------------------------------
795 
796 		/**
797 		 * Creates a tag of a given tagtype in this page.
798 		 * The first parameter should either be the construct keyword or ID,
799 		 * or an object containing exactly one of the following property sets:<br/>
800 		 * <ol>
801 		 *   <li><i>keyword</i> to create a tag based on the construct with given keyword</li>
802 		 *   <li><i>constructId</i> to create a tag based on the construct with given ID</li>
803 		 *   <li><i>sourcePageId</i> and <i>sourceTagname</i> to create a tag as copy of the given tag from the page</li>
804 		 * </ol>
805 		 * 
806 		 * Exmaple:
807 		 * <pre>
808 		 *  createTag('link', onSuccess, onError);
809 		 * </pre>
810 		 * or
811 		 * <pre>
812 		 *  createTag({keyword: 'link', magicValue: 'http://www.gentics.com'}, onSuccess, onError);
813 		 * </pre>
814 		 * or
815 		 * <pre>
816 		 *  createTag({sourcePageId: 4711, sourceTagname: 'link'}, onSuccess, onError);
817 		 * </pre>
818 		 *
819 		 * @public
820 		 * @function
821 		 * @name createTag
822 		 * @memberOf PageAPI
823 		 * @param {string|number|object} construct either the keyword of the construct, or the ID of the construct
824 		 *                               or an object with the following properties
825 		 *                               <ul>
826 		 *                                <li><i>keyword</i> keyword of the construct</li>
827 		 *                                <li><i>constructId</i> ID of the construct</li>
828 		 *                                <li><i>magicValue</i> magic value to be filled into the tag</li>
829 		 *                                <li><i>sourcePageId</i> source page id</li>
830 		 *                                <li><i>sourceTagname</i> source tag name</li>
831 		 *                               </ul>
832 		 * @param {function(TagAPI)=} success Optional callback that will
833 		 *                                    receive the newly created tag as
834 		 *                                    its only argument.
835 		 * @param {function(GCNError):boolean=} error Optional custom error
836 		 *                                            handler.
837 		 * @return {TagAPI} The newly created tag.
838 		 * @throws INVALID_ARGUMENTS
839 		 */
840 		'!createTag': function () {
841 			return this._createTag.apply(this, arguments);
842 		},
843 
844 		/**
845 		 * Deletes the specified tag from this page.
846 		 *
847 		 * @public
848 		 * @function
849 		 * @name removeTag
850 		 * @memberOf PageAPI
851 		 * @param {string} id The id of the tag to be deleted.
852 		 * @param {function(PageAPI)=} success Optional callback that receive
853 		 *                                     this object as its only
854 		 *                                     argument.
855 		 * @param {function(GCNError):boolean=} error Optional custom error
856 		 *                                            handler.
857 		 */
858 		removeTag: function () {
859 			this._removeTag.apply(this, arguments);
860 		},
861 
862 		/**
863 		 * Deletes a set of tags from this page.
864 		 *
865 		 * @public
866 		 * @function
867 		 * @name removeTags
868 		 * @memberOf PageAPI
869 		 * @param {Array.<string>} ids The ids of the set of tags to be
870 		 *                             deleted.
871 		 * @param {function(PageAPI)=} success Optional callback that receive
872 		 *                                     this object as its only
873 		 *                                     argument.
874 		 * @param {function(GCNError):boolean=} error Optional custom error
875 		 *                                            handler.
876 		 */
877 		removeTags: function () {
878 			this._removeTags.apply(this, arguments);
879 		},
880 
881 		/**
882 		 * Marks the page as to be taken offline. This method will change the
883 		 * state of the page object.
884 		 *
885 		 * @public
886 		 * @function
887 		 * @name takeOffline
888 		 * @memberOf PageAPI
889 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
890 		 *                                    page object as the only argument.
891 		 * @param {function(GCNError):boolean=} error Optional custom error
892 		 *                                            handler.
893 		 */
894 		takeOffline: function (success, error) {
895 			var that = this;
896 
897 			this._read(function () {
898 				that._update('status', STATUS.OFFLINE, error);
899 				if (success) {
900 					that._save(null, success, error);
901 				}
902 			}, error);
903 		},
904 
905 		/**
906 		 * Trigger publish process for the page.
907 		 *
908 		 * @public
909 		 * @function
910 		 * @name publish
911 		 * @memberOf PageAPI
912 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
913 		 *                                    page object as the only argument.
914 		 * @param {function(GCNError):boolean=} error Optional custom error
915 		 *                                            handler.
916 		 */
917 		publish: function (success, error) {
918 			var that = this;
919 			this._fulfill(function () {
920 				that._authAjax({
921 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
922 					     '/publish/' + that.id(),
923 					type: 'POST',
924 					json: {}, // There needs to be at least empty content
925 					          // because of a bug in Jersey.
926 					success: function (response) {
927 						that._data.status = STATUS.PUBLISHED;
928 						if (success) {
929 							that._invoke(success, [that]);
930 						}
931 					},
932 					error: error
933 				});
934 			});
935 		},
936 
937 		/**
938 		 * Renders a preview of the current page.
939 		 *
940 		 * @public
941 		 * @function
942 		 * @name preview
943 		 * @memberOf PageAPI
944 		 * @param {function(string, PageAPI)} success Callback to receive the
945 		 *                                            rendered page preview as
946 		 *                                            the first argument, and
947 		 *                                            this page object as the
948 		 *                                            second.
949 		 * @param {function(GCNError):boolean=} error Optional custom error
950 		 *                                            handler.
951 		 */
952 		preview: function (success, error) {
953 			var that = this;
954 
955 			this._read(function () {
956 				that._authAjax({
957 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
958 					     '/preview/',
959 					json: {
960 						page: that._data // @FIXME Shouldn't this a be merge of
961 						                 //        the `_shadow' object and the
962 										 //        `_data'.
963 					},
964 					type: 'POST',
965 					error: error,
966 					success: function (response) {
967 						if (success) {
968 							GCN._handleContentRendered(response.preview, that,
969 								function (html) {
970 									that._invoke(success, [html, that]);
971 								});
972 						}
973 					}
974 				});
975 			}, error);
976 		},
977 
978 		/**
979 		 * Unlocks the page when finishing editing
980 		 *
981 		 * @public
982 		 * @function
983 		 * @name unlock
984 		 * @memberOf PageAPI
985 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
986 		 *                                    page object as the only argument.
987 		 * @param {function(GCNError):boolean=} error Optional custom error
988 		 *                                            handler.
989 		 */
990 		unlock: function (success, error) {
991 			var that = this;
992 			this._fulfill(function () {
993 				that._authAjax({
994 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
995 					     '/cancel/' + that.id(),
996 					type: 'POST',
997 					json: {}, // There needs to be at least empty content
998 					          // because of a bug in Jersey.
999 					error: error,
1000 					success: function (response) {
1001 						if (success) {
1002 							that._invoke(success, [that]);
1003 						}
1004 					}
1005 				});
1006 			});
1007 		},
1008 
1009 		/**
1010 		 * @see GCN.ContentObjectAPI._processResponse
1011 		 */
1012 		'!_processResponse': function (data) {
1013 			jQuery.extend(this._data, data[this._type]);
1014 
1015 			// if data contains page variants turn them into page objects
1016 			if (this._data.pageVariants) {
1017 				var pagevars = [];
1018 				var i;
1019 				for (i = 0; i < this._data.pageVariants.length; i++) {
1020 					pagevars.push(this._continue(GCN.PageAPI,
1021 						this._data.pageVariants[i]));
1022 				}
1023 				this._data.pageVariants = pagevars;
1024 			}
1025 		},
1026 
1027 		/**
1028 		 * @override
1029 		 */
1030 		'!_removeAssociatedTagData': function (tagid) {
1031 			var block;
1032 			for (block in this._blocks) {
1033 				if (this._blocks.hasOwnProperty(block) &&
1034 						this._blocks[block].tagname === tagid) {
1035 					delete this._blocks[block];
1036 				}
1037 			}
1038 
1039 			var editable;
1040 			for (editable in this._editables) {
1041 				if (this._editables.hasOwnProperty(editable) &&
1042 						this._editables[editable].tagname === tagid) {
1043 					delete this._editables[editable];
1044 				}
1045 			}
1046 		}
1047 
1048 	});
1049 
1050 	GCN.page = GCN.exposeAPI(PageAPI);
1051 	GCN.PageAPI = PageAPI;
1052 
1053 }(GCN));
1054