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