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 				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(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 			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 		 *
799 		 * Exmaple:
800 		 * <pre>
801 		 *	createTag('link', 'http://www.gentics.com', onSuccess, onError);
802 		 * </pre>
803 		 * or
804 		 * <pre>
805 		 *	createTag('link', onSuccess, onError);
806 		 * </pre>
807 		 *
808 		 * @public
809 		 * @function
810 		 * @name createTag
811 		 * @memberOf PageAPI
812 		 * @param {string|number} construct The name of the construct on which
813 		 *                                  the tag to be created should be
814 		 *                                  derived from.  Or the id of that
815 		 * @param {string=} magicValue Optional property that will override the
816 		 *                             default values of this tag type.
817 		 * @param {function(TagAPI)=} success Optional callback that will
818 		 *                                    receive the newly created tag as
819 		 *                                    its only argument.
820 		 * @param {function(GCNError):boolean=} error Optional custom error
821 		 *                                            handler.
822 		 * @return {TagAPI} The newly created tag.
823 		 * @throws INVALID_ARGUMENTS
824 		 */
825 		'!createTag': function () {
826 			return this._createTag.apply(this, arguments);
827 		},
828 
829 		/**
830 		 * Deletes the specified tag from this page.
831 		 *
832 		 * @public
833 		 * @function
834 		 * @name removeTag
835 		 * @memberOf PageAPI
836 		 * @param {string} id The id of the tag to be deleted.
837 		 * @param {function(PageAPI)=} success Optional callback that receive
838 		 *                                     this object as its only
839 		 *                                     argument.
840 		 * @param {function(GCNError):boolean=} error Optional custom error
841 		 *                                            handler.
842 		 */
843 		removeTag: function () {
844 			this._removeTag.apply(this, arguments);
845 		},
846 
847 		/**
848 		 * Deletes a set of tags from this page.
849 		 *
850 		 * @public
851 		 * @function
852 		 * @name removeTags
853 		 * @memberOf PageAPI
854 		 * @param {Array.<string>} ids The ids of the set of tags to be
855 		 *                             deleted.
856 		 * @param {function(PageAPI)=} success Optional callback that receive
857 		 *                                     this object as its only
858 		 *                                     argument.
859 		 * @param {function(GCNError):boolean=} error Optional custom error
860 		 *                                            handler.
861 		 */
862 		removeTags: function () {
863 			this._removeTags.apply(this, arguments);
864 		},
865 
866 		/**
867 		 * Marks the page as to be taken offline. This method will change the
868 		 * state of the page object.
869 		 *
870 		 * @public
871 		 * @function
872 		 * @name takeOffline
873 		 * @memberOf PageAPI
874 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
875 		 *                                    page object as the only argument.
876 		 * @param {function(GCNError):boolean=} error Optional custom error
877 		 *                                            handler.
878 		 */
879 		takeOffline: function (success, error) {
880 			var that = this;
881 
882 			this._read(function () {
883 				that._update('status', STATUS.OFFLINE, error);
884 				if (success) {
885 					that._save(null, success, error);
886 				}
887 			}, error);
888 		},
889 
890 		/**
891 		 * Trigger publish process for the page.
892 		 *
893 		 * @public
894 		 * @function
895 		 * @name publish
896 		 * @memberOf PageAPI
897 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
898 		 *                                    page object as the only argument.
899 		 * @param {function(GCNError):boolean=} error Optional custom error
900 		 *                                            handler.
901 		 */
902 		publish: function (success, error) {
903 			var that = this;
904 			this._fulfill(function () {
905 				that._authAjax({
906 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
907 					     '/publish/' + that.id(),
908 					type: 'POST',
909 					json: {}, // There needs to be at least empty content
910 					          // because of a bug in Jersey.
911 					success: function (response) {
912 						that._data.status = STATUS.PUBLISHED;
913 						if (success) {
914 							that._invoke(success, [that]);
915 						}
916 					},
917 					error: error
918 				});
919 			});
920 		},
921 
922 		/**
923 		 * Renders a preview of the current page.
924 		 *
925 		 * @public
926 		 * @function
927 		 * @name preview
928 		 * @memberOf PageAPI
929 		 * @param {function(string, PageAPI)} success Callback to receive the
930 		 *                                            rendered page preview as
931 		 *                                            the first argument, and
932 		 *                                            this page object as the
933 		 *                                            second.
934 		 * @param {function(GCNError):boolean=} error Optional custom error
935 		 *                                            handler.
936 		 */
937 		preview: function (success, error) {
938 			var that = this;
939 
940 			this._read(function () {
941 				that._authAjax({
942 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
943 					     '/preview/',
944 					json: {
945 						page: that._data // @FIXME Shouldn't this a be merge of
946 						                 //        the `_shadow' object and the
947 										 //        `_data'.
948 					},
949 					type: 'POST',
950 					error: error,
951 					success: function (response) {
952 						if (success) {
953 							GCN._handleContentRendered(response.preview, that,
954 								function (html) {
955 									that._invoke(success, [html, that]);
956 								});
957 						}
958 					}
959 				});
960 			}, error);
961 		},
962 
963 		/**
964 		 * Unlocks the page when finishing editing
965 		 *
966 		 * @public
967 		 * @function
968 		 * @name unlock
969 		 * @memberOf PageAPI
970 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
971 		 *                                    page object as the only argument.
972 		 * @param {function(GCNError):boolean=} error Optional custom error
973 		 *                                            handler.
974 		 */
975 		unlock: function (success, error) {
976 			var that = this;
977 			this._fulfill(function () {
978 				that._authAjax({
979 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
980 					     '/cancel/' + that.id(),
981 					type: 'POST',
982 					json: {}, // There needs to be at least empty content
983 					          // because of a bug in Jersey.
984 					error: error,
985 					success: function (response) {
986 						if (success) {
987 							that._invoke(success, [that]);
988 						}
989 					}
990 				});
991 			});
992 		},
993 
994 		/**
995 		 * @see GCN.ContentObjectAPI._processResponse
996 		 */
997 		'!_processResponse': function (data) {
998 			jQuery.extend(this._data, data[this._type]);
999 
1000 			// if data contains page variants turn them into page objects
1001 			if (this._data.pageVariants) {
1002 				var pagevars = [];
1003 				var i;
1004 				for (i = 0; i < this._data.pageVariants.length; i++) {
1005 					pagevars.push(this._continue(GCN.PageAPI,
1006 						this._data.pageVariants[i]));
1007 				}
1008 				this._data.pageVariants = pagevars;
1009 			}
1010 		},
1011 
1012 		/**
1013 		 * @override
1014 		 */
1015 		'!_removeAssociatedTagData': function (tagid) {
1016 			var block;
1017 			for (block in this._blocks) {
1018 				if (this._blocks.hasOwnProperty(block) &&
1019 						this._blocks[block].tagname === tagid) {
1020 					delete this._blocks[block];
1021 				}
1022 			}
1023 
1024 			var editable;
1025 			for (editable in this._editables) {
1026 				if (this._editables.hasOwnProperty(editable) &&
1027 						this._editables[editable].tagname === tagid) {
1028 					delete this._editables[editable];
1029 				}
1030 			}
1031 		}
1032 
1033 	});
1034 
1035 	GCN.page = GCN.exposeAPI(PageAPI);
1036 	GCN.PageAPI = PageAPI;
1037 
1038 }(GCN));
1039