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 extened 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 				that.node().constructs(function (constructs) {
433 					that._removeOldLinkBlocks(blocks, constructs, function () {
434 						that._removeUnusedLinkBlocks(blocks, constructs, function () {
435 							that._updateEditableBlocks();
436 							success();
437 						}, error);
438 					}, error);
439 				}, error);
440 			}, error);
441 		},
442 
443 		/**
444 		 * Removes any link blocks that existed in rendered tags, but have
445 		 * since been removed by the user while editing.
446 		 *
447 		 * @private
448 		 * @param {Array.<object>} blocks An array of blocks belonging to this
449 		 *                                page.
450 		 * @param {object} constrcts A set of constructs.
451 		 * @param {function} success
452 		 * @param {function(GCNError):boolean=} error Optional custom error
453 		 *                                            handler.
454 		 */
455 		'!_removeUnusedLinkBlocks': function (blocks, constructs, success,
456 		                                      error) {
457 			if (0 === blocks.length) {
458 				if (success) {
459 					success();
460 				}
461 				return;
462 			}
463 
464 			var j = blocks.length;
465 			var numToProcess = j;
466 
467 			var onProcess = function () {
468 				if (0 === --numToProcess) {
469 					if (success) {
470 						success();
471 					}
472 				}
473 			};
474 
475 			var onError = function (GCNError) {
476 				if (error) {
477 					error(GCNError);
478 				}
479 				return;
480 			};
481 
482 			var that = this;
483 			var createBlockTagProcessor = function (block) {
484 				return function (tag) {
485 					if (isMagicLinkTag(tag, constructs) &&
486 							!hasInlineElement(block)) {
487 						that._deletedBlocks.push(block.tagname);
488 					}
489 
490 					onProcess();
491 				};
492 			};
493 
494 			while (j) {
495 				this.tag(blocks[--j].tagname,
496 					createBlockTagProcessor(blocks[j]), onError);
497 			}
498 		},
499 
500 		/**
501 		 * Adds any newly created link blocks into this page object.  This is
502 		 * done by looking for all link blocks that do not have corresponding
503 		 * tag in this object's `_blocks' list.  For each anchor tag we find,
504 		 * create a tag for it and, add it in the list of tags.
505 		 *
506 		 * @private
507 		 * @param {function} success Function to invoke if this function
508 		 *                           successeds.
509 		 * @param {function(GCNError):boolean=} error Optional custom error
510 		 *                                            handler.
511 		 */
512 		'!_addNewLinkBlocks': function (success, error) {
513 			// Limit the search for links to be done only withing rendered
514 			// editables.
515 			var id;
516 			var $editables = jQuery();
517 			for (id in this._editables) {
518 				if (this._editables.hasOwnProperty(id)) {
519 					$editables = $editables.add('#' + id);
520 				}
521 			}
522 
523 			var selector = [
524 				'a[data-gentics-aloha-repository="com.gentics.aloha.GCN.Page"]',
525 				'a[data-GENTICS-aloha-repository="com.gentics.aloha.GCN.Page"]',
526 				'a[data-gentics-gcn-url]'
527 			].join(',');
528 
529 			var links = $editables.find(selector);
530 
531 			if (0 === links.length) {
532 				if (success) {
533 					success();
534 				}
535 
536 				return;
537 			}
538 
539 			var link;
540 			var j = links.length;
541 			var numToProcess = j;
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(GCN.settings.MAGIC_LINK, 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 
610 			var onProcess = function () {
611 				if (0 === --numToProcess) {
612 					if (success) {
613 						success();
614 					}
615 				}
616 			};
617 
618 			var onError = function (GCNError) {
619 				if (error) {
620 					error(GCNError);
621 				}
622 				return;
623 			};
624 
625 			var that = this;
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 
685 			for (elementId in editables) {
686 				if (editables.hasOwnProperty(elementId)) {
687 					editable = editables[elementId];
688 					element = jQuery('#' + elementId);
689 
690 					// If this editable has no element that was placed in the
691 					// DOM, then do not attempt to update it.
692 					if (0 === element.length) {
693 						continue;
694 					}
695 
696 					tagname = editable.tagname;
697 
698 					if (!tags[tagname]) {
699 						tags[tagname] = {
700 							name       : tagname,
701 							active     : true,
702 							properties : {}
703 						};
704 					} else {
705 						// make sure, that all tags (which relate to editables)
706 						// are activated
707 						tags[tagname].active = true;
708 					}
709 
710 					// If the editable element has been `aloha()'fied, then we
711 					// need to use `getContents()' from is corresponding
712 					// Aloha.Editable object in order to get clean HTML.
713 
714 					alohaEditable = getAlohaEditableById(elementId);
715 
716 					if (alohaEditable) {
717 						html = alohaEditable.getContents();
718 						alohaEditable.setUnmodified();
719 					} else {
720 						html = element.html();
721 					}
722 
723 					tags[tagname].properties[editable.partname] = {
724 						stringValue: this.encode(html),
725 						type: 'RICHTEXT'
726 					};
727 
728 					this._update('tags.' + tagname, tags[tagname]);
729 				}
730 			}
731 		},
732 
733 		/**
734 		 * @see ContentObjectAPI.!_loadParams
735 		 */
736 		'!_loadParams': function () {
737 			return jQuery.extend(DEFAULT_SETTINGS, this._settings);
738 		},
739 
740 		/**
741 		 * Get this page's template.
742 		 *
743 		 * @public
744 		 * @function
745 		 * @name template
746 		 * @memberOf PageAPI
747 		 * @param {funtion(TemplateAPI)=} success Optional callback to receive
748 		 *                                        a {@link TemplateAPI} object
749 		 *                                        as the only argument.
750 		 * @param {function(GCNError):boolean=} error Optional custom error
751 		 *                                            handler.
752 		 * @return {TemplateAPI} This page's parent template.
753 		 */
754 		'!template': function (success, error) {
755 			var id = this._fetched ? this.prop('templateId') : null;
756 			return this._continue(GCN.TemplateAPI, id, success, error);
757 		},
758 
759 		/**
760 		 * @override
761 		 * @see ContentObjectAPI._save
762 		 */
763 		'!_save': function (settings, success, error) {
764 			var that = this;
765 			this._continueWith(function () {
766 				var fork = that._fork();
767 				fork._prepareTagsForSaving(function () {
768 					fork._persist(settings, function () {
769 						if (success) {
770 							success(that);
771 						}
772 						fork._merge();
773 					}, error);
774 				}, error);
775 			}, error);
776 		},
777 
778 		//---------------------------------------------------------------------
779 		// Surface the tag container methods that are applicable for GCN page
780 		// objects.
781 		//---------------------------------------------------------------------
782 
783 		/**
784 		 * Creates a tag of a given tagtype in this page.
785 		 *
786 		 * Exmaple:
787 		 * <pre>
788 		 *	createTag('link', 'http://www.gentics.com', onSuccess, onError);
789 		 * </pre>
790 		 * or
791 		 * <pre>
792 		 *	createTag('link', onSuccess, onError);
793 		 * </pre>
794 		 *
795 		 * @public
796 		 * @function
797 		 * @name createTag
798 		 * @memberOf PageAPI
799 		 * @param {string|number} construct The name of the construct on which
800 		 *                                  the tag to be created should be
801 		 *                                  derived from.  Or the id of that
802 		 * @param {string=} magicValue Optional property that will override the
803 		 *                             default values of this tag type.
804 		 * @param {function(TagAPI)=} success Optional callback that will
805 		 *                                    receive the newly created tag as
806 		 *                                    its only argument.
807 		 * @param {function(GCNError):boolean=} error Optional custom error
808 		 *                                            handler.
809 		 * @return {TagAPI} The newly created tag.
810 		 * @throws INVALID_ARGUMENTS
811 		 */
812 		'!createTag': function () {
813 			return this._createTag.apply(this, arguments);
814 		},
815 
816 		/**
817 		 * Deletes the specified tag from this page.
818 		 *
819 		 * @public
820 		 * @function
821 		 * @name removeTag
822 		 * @memberOf PageAPI
823 		 * @param {string} id The id of the tag to be deleted.
824 		 * @param {function(PageAPI)=} success Optional callback that receive
825 		 *                                     this object as its only
826 		 *                                     argument.
827 		 * @param {function(GCNError):boolean=} error Optional custom error
828 		 *                                            handler.
829 		 */
830 		removeTag: function () {
831 			this._removeTag.apply(this, arguments);
832 		},
833 
834 		/**
835 		 * Deletes a set of tags from this page.
836 		 *
837 		 * @public
838 		 * @function
839 		 * @name removeTags
840 		 * @memberOf PageAPI
841 		 * @param {Array.<string>} ids The ids of the set of tags to be
842 		 *                             deleted.
843 		 * @param {function(PageAPI)=} success Optional callback that receive
844 		 *                                     this object as its only
845 		 *                                     argument.
846 		 * @param {function(GCNError):boolean=} error Optional custom error
847 		 *                                            handler.
848 		 */
849 		removeTags: function () {
850 			this._removeTags.apply(this, arguments);
851 		},
852 
853 		/**
854 		 * Marks the page as to be taken offline. This method will change the
855 		 * state of the page object.
856 		 *
857 		 * @public
858 		 * @function
859 		 * @name takeOffline
860 		 * @memberOf PageAPI
861 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
862 		 *                                    page object as the only argument.
863 		 * @param {function(GCNError):boolean=} error Optional custom error
864 		 *                                            handler.
865 		 */
866 		takeOffline: function (success, error) {
867 			var that = this;
868 
869 			this._read(function () {
870 				that._update('status', STATUS.OFFLINE, error);
871 				if (success) {
872 					that._save(null, success, error);
873 				}
874 			}, error);
875 		},
876 
877 		/**
878 		 * Trigger publish process for the page.
879 		 *
880 		 * @public
881 		 * @function
882 		 * @name publish
883 		 * @memberOf PageAPI
884 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
885 		 *                                    page object as the only argument.
886 		 * @param {function(GCNError):boolean=} error Optional custom error
887 		 *                                            handler.
888 		 */
889 		publish: function (success, error) {
890 			var that = this;
891 			this._continueWith(function () {
892 				that._authAjax({
893 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
894 					     '/publish/' + that.id(),
895 					type: 'POST',
896 					json: {}, // There needs to be at least empty content
897 					          // because of a bug in Jersey.
898 					success: function (response) {
899 						that._data.status = STATUS.PUBLISHED;
900 						if (success) {
901 							success(that);
902 						}
903 					},
904 					error: error
905 				});
906 			});
907 		},
908 
909 		/**
910 		 * Renders a preview of the current page.
911 		 *
912 		 * @public
913 		 * @function
914 		 * @name preview
915 		 * @memberOf PageAPI
916 		 * @param {function(string, PageAPI)} success Callback to receive the
917 		 *                                            rendered page preview as
918 		 *                                            the first argument, and
919 		 *                                            this page object as the
920 		 *                                            second.
921 		 * @param {function(GCNError):boolean=} error Optional custom error
922 		 *                                            handler.
923 		 */
924 		preview: function (success, error) {
925 			var that = this;
926 
927 			this._read(function () {
928 				that._authAjax({
929 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
930 					     '/preview/',
931 					json: {
932 						page: that._data // @FIXME Shouldn't this a be merge of
933 						                 //        the `_shadow' object and the
934 										 //        `_data'.
935 					},
936 					type: 'POST',
937 					error: error,
938 					success: function (response) {
939 						if (success) {
940 							GCN._handleContentRendered(response.preview,
941 								function (html) {
942 									success(html, that);
943 								});
944 						}
945 					}
946 				});
947 			}, error);
948 		},
949 
950 		/**
951 		 * Unlocks the page when finishing editing
952 		 *
953 		 * @public
954 		 * @function
955 		 * @name unlock
956 		 * @memberOf PageAPI
957 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
958 		 *                                    page object as the only argument.
959 		 * @param {function(GCNError):boolean=} error Optional custom error
960 		 *                                            handler.
961 		 */
962 		unlock: function (success, error) {
963 			var that = this;
964 			this._continueWith(function () {
965 				that._authAjax({
966 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
967 					     '/cancel/' + that.id(),
968 					type: 'POST',
969 					json: {}, // There needs to be at least empty content
970 					          // because of a bug in Jersey.
971 					error: error,
972 					success: function (response) {
973 						if (success) {
974 							success(that);
975 						}
976 					}
977 				});
978 			});
979 		},
980 
981 		/**
982 		 * @see GCN.ContentObjectAPI._processResponse
983 		 */
984 		'!_processResponse': function (data) {
985 			jQuery.extend(this._data, data[this._type]);
986 
987 			// if data contains page variants turn them into page objects
988 			if (this._data.pageVariants) {
989 				var pagevars = [];
990 				var i;
991 				for (i = 0; i < this._data.pageVariants.length; i++) {
992 					pagevars.push(this._continue(GCN.PageAPI,
993 							this._data.pageVariants[i]));
994 				}
995 				this._data.pageVariants = pagevars;
996 			}
997 		},
998 
999 		/**
1000 		 * @override
1001 		 */
1002 		'!_removeAssociatedTagData': function (tagid) {
1003 			var block;
1004 			for (block in this._blocks) {
1005 				if (this._blocks.hasOwnProperty(block) &&
1006 						this._blocks[block].tagname === tagid) {
1007 					delete this._blocks[block];
1008 				}
1009 			}
1010 
1011 			var editable;
1012 			for (editable in this._editables) {
1013 				if (this._editables.hasOwnProperty(editable) &&
1014 						this._editables[editable].tagname === tagid) {
1015 					delete this._editables[editable];
1016 				}
1017 			}
1018 		}
1019 
1020 	});
1021 
1022 	GCN.page = GCN.exposeAPI(PageAPI);
1023 	GCN.PageAPI = PageAPI;
1024 
1025 }(GCN));
1026