1 /* repositorymanager.js is part of Aloha Editor project http://aloha-editor.org
  2  *
  3  * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor.
  4  * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria.
  5  * Contributors http://aloha-editor.org/contribution.php
  6  *
  7  * Aloha Editor is free software; you can redistribute it and/or
  8  * modify it under the terms of the GNU General Public License
  9  * as published by the Free Software Foundation; either version 2
 10  * of the License, or any later version.
 11  *
 12  * Aloha Editor is distributed in the hope that it will be useful,
 13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU General Public License
 18  * along with this program; if not, write to the Free Software
 19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 20  *
 21  * As an additional permission to the GNU GPL version 2, you may distribute
 22  * non-source (e.g., minimized or compacted) forms of the Aloha-Editor
 23  * source code without the copy of the GNU GPL normally required,
 24  * provided you include this license notice and a URL through which
 25  * recipients can access the Corresponding Source.
 26  */
 27 define([
 28 	'jquery',
 29 	'util/class',
 30 	'aloha/core',
 31 	'aloha/console',
 32 	'aloha/repositoryobjects' // Provides Aloha.RepositoryFolder
 33 ], function (
 34 	$,
 35 	Class,
 36 	Aloha,
 37 	Console,
 38 	__unused__
 39 ) {
 40 	'use strict';
 41 
 42 	/**
 43 	 * Given an input set, returns a new set which is a range of the input set
 44 	 * that maps to the given predicate.
 45 	 *
 46 	 * Prefers native Array.prototype.filter() where available (after JavaScript
 47 	 * 1.6).
 48 	 *
 49 	 * @param {function:boolean} predicate
 50 	 * @return {Array} Sub set of domain
 51 	 */
 52 	var filter = (function (predicate) {
 53 		if (predicate) {
 54 			return function (domain, predicate) {
 55 				return domain.filter(predicate);
 56  57 			};
 58 		}
 59 
 60 		return function (domain, predicate) {
 61 			var codomain = [],
 62 				i,
 63 				len = domain.length;
 64 			for (i = 0; i < len; i++) {
 65 				if (predicate(domain[i])) {
 66 					codomain.push(domain[i]);
 67 				}
 68 			}
 69 			return codomain;
 70 		};
 71  72 	}(Array.prototype.hasOwnProperty('filter')));
 73 
 74 	/**
 75 	 * Bundles results, and meta information in preparation for the JSON Reader.
 76 	 *
 77 	 * Used with query().
 78 	 *
 79 	 * @param {Array.<Document|Folder>} items Results, collected from all
 80 	 *                                        repositories.
 81 	 * @param {object<string, number>} meta Optional object containing metainfo.
 82 	 * @return {object} Result object.
 83 	 */
 84 	function bundle(items, meta) {
 85 		var result = {
 86 			items: items,
 87 			results: items.length
 88 		};
 89 		if (meta) {
 90 			result.numItems = meta.numItems;
 91 			result.hasMoreItems = meta.hasMoreItems;
 92 			result.timeout = meta.timeout;
 93 		}
 94 		return result;
 95 	}
 96 
 97 	/**
 98 	 * Passes all the results we have collected to the client through the
 99 	 * callback it specified.
100 	 *
101 	 * TODO: Implement sorting based on repository specification sort
102 	 *       items by weight.
103 	 * items.sort(function (a, b) {
104 	 *	return (b.weight || 0) - (a.weight || 0);
105 	 * });
106 	 *
107 	 * @param {function} callback Callback specified by client when invoking
108 	 *                            the query method.
109 	 * @param {Array.<Document|Folder>|object<string, number>} results
110 	 */
111 	function report(callback, results) {
112 		callback(results);
113 	}
114 
115 	/**
116 	 * Predicates; used to filter lists of repositories based on whether they
117 	 * implement a method or not.
118 119 	 *
120 	 * @type {object<string, function(Repository):boolean}
121 	 */
122 	var repositoryFilters = {
123 		query: function (repository) {
124 			return typeof repository.query === 'function';
125 		},
126 		getChildren: function (repository) {
127 			return typeof repository.getChildren === 'function';
128 		},
129 		getSelectedFolder: function (repository) {
130 			return typeof repository.getSelectedFolder === 'function';
131 		}
132 	};
133 
134 	/**
135 	 * Repository Manager.
136 	 *
137 	 * @namespace Aloha
138 	 * @class RepositoryManager
139 	 * @singleton
140 	 */
141 	var RepositoryManager = Class.extend({
142 
143 		repositories: [],
144 
145 		settings: (Aloha.settings && Aloha.settings.repositories) || {},
146 
147 		initialized: false,
148 
149 		/**
150 		 * Initializes all registered repositories.
151 		 *
152 		 *                            ???
153 		 *                             |
154 		 *                             v
155 		 *
156 		 * Warning: testing has shown that repositories are maybe not loaded yet
157 		 * (found that case in IE7), so don't rely on that in this init
158 		 * function.
159 		 *
160 		 *                             ^
161 		 *                             |
162 		 *                            !!!
163 		 */
164 		init: function () {
165 			var manager = this;
166 			if (typeof manager.settings.timeout === 'undefined') {
167 				manager.settings.timeout = 5000;
168 			}
169 			var i;
170 			for (i = 0; i < manager.repositories.length; i++) {
171 				manager.initRepository(manager.repositories[i]);
172 			}
173 			manager.initialized = true;
174 		},
175 
176 		/**
177 178 		 * Registers a Repository.
179 		 *
180 		 * If the repositorie is registered after the Repository Manager is
181 		 * initialized it will be automatically initialized.
182 		 *
183 		 * @param {Repository} repository Repository to register.
184 		 */
185 186 		register: function (repository) {
187 			var manager = this;
188 			if (!manager.getRepository(repository.repositoryId)) {
189 				manager.repositories.push(repository);
190 				if (manager.initialized) {
191 					manager.initRepository(repository);
192 				}
193 			} else {
194 				Console.warn(manager, 'A repository with name "'
195 						+ repository.repositoryId
196 						+ '" already registerd. Ignoring this.');
197 			}
198 		},
199 
200 		/**
201 		 * Initializes a repository.
202 		 *
203 		 * @param {Repository} repository Repository to initialize.
204 		 */
205 		initRepository: function (repository) {
206 			var manager = this;
207 			if (!repository.settings) {
208 				repository.settings = {};
209 			}
210 			if (manager.settings[repository.repositoryId]) {
211 				$.extend(repository.settings,
212 				         manager.settings[repository.repositoryId]);
213 			}
214 			repository.init();
215 		},
216 
217 		/**
218 		 * Returns the repository identified by repositoryId.
219 		 *
220 		 * @param {String} id Id of repository to retrieve.
221 		 * @return {Repository|null} Repository or null if none with the given
222 		 *                           id is found.
223 		 */
224 		getRepository: function (id) {
225 			var manager = this;
226 			var i;
227 			for (i = 0; i < manager.repositories.length; i++) {
228 				if (manager.repositories[i].repositoryId === id) {
229 					return manager.repositories[i];
230 				}
231 			}
232 			return null;
233 234 		},
235 
236 		/**
237 		 * Searches all repositories for repositoryObjects matching query and
238 		 * repositoryObjectType.
239 		 *
240 		 * <pre><code>
241 		 *  // Example:
242 		 *  var params = {
243 		 *      queryString: 'hello',
244 		 *      objectTypeFilter: ['website'],
245 		 *      filter: null,
246 		 *      inFolderId: null,
247 		 *      orderBy: null,
248 		 *      maxItems: null,
249 		 *      skipCount: null,
250 		 *      renditionFilter: null,
251 		 *      repositoryId: null
252 		 *  };
253 		 *  Aloha.RepositoryManager.query(params, function (items) {
254 		 *      Console.log(items);
255 		 *  });
256 		 * </code></pre>
257 		 *
258 		 * @param {object<string, mixed>} params
259 		 *
260 		 *       queryString: String             The query string for full text
261 		 *                                       search.
262 		 *  objectTypeFilter: Array   (optional) Object types to be retrieved.
263 		 *            filter: Array   (optional) Attributes that will be
264 		 *                                       included.
265 		 *        inFolderId: boolean (optional) Whether or not a candidate
266 		 *                                       object is a child-object of the
267 		 *                                       folder object identified by the
268 		 *                                       given inFolderId (objectId).
269 		 *          inTreeId: boolean (optional) This indicates whether or
270 		 *                                       not a candidate object is a
271 		 *                                       descendant-object of the folder
272 		 *                                       object identified by the given
273 		 *                                       inTreeId (objectId).
274 		 *           orderBy: Array   (optional) example: [{
275 		 *                                           lastModificationDate: 'DESC',
276 		 *                                           name: 'ASC'
277 		 *                                       }]
278 		 *          maxItems: number  (optional) Number of items to include in
279 		 *                                       result set.
280 		 *         skipCount: number  (optional) This is tricky in a merged
281 		 *                                       multi repository scenario.
282 		 *   renditionFilter: Array   (optional) Instead of termlist, an
283 		 *                                       array of kind or mimetype is
284 		 *                                       expected.  If null or an empty
285 		 *                                       set, then all renditions are
286 		 *                                       returned. See
287 		 *                                       http://docs.oasis-open.org/cmis/CMIS/v1.0/cd04/cmis-spec-v1.0.html#_Ref237323310
288 		 *                                       for renditionFilter.
289 		 *
290 		 * @param {function(Document|Folder)} callback Function to be invoked
291 		 *                                             after the repository
292 		 *                                             manager has finished
293 		 *                                             querying all
294 		 *                                             repositories.
295 		 */
296 		query: function (params, callback) {
297 			var manager = this;
298 
299 			var i;
300 
301 			// The merged results, collected from repository responses.
302 			var results = [];
303 
304 			// The merged metainfo, collected from repository responses.
305 			var allmetainfo = {
306 				numItems: 0,
307 				hasMoreItems: false
308 			};
309 
310 			// A counting semaphore (working in reverse, ie: 0 means free).
311 			var numOpenQueries;
312 
313 			// Unless the calling client specifies otherwise, the manager will
314 			// wait a maximum of 5 seconds for all repositories to be queried
315 			// and respond. 5 seconds is deemed to be the reasonable time to
316 			// wait when querying the repository manager in the context of
317 			// something like autocomplete.
318 			var timeout = (params.timeout && parseInt(params.timeout, 10))
319 			           || manager.settings.timeout;
320 
321 			// When this timer times-out, whatever has been collected in
322 			// `results' will be returned to the calling client and all further
323 			// processing aborted.
324 			var timer = window.setTimeout(function () {
325 				// Store in metainfo that a timeout occurred.
326 				allmetainfo = allmetainfo || {};
327 				allmetainfo.timeout = true;
328 
329 				if (numOpenQueries > 0) {
330 					Console.warn(manager, numOpenQueries
331 							+ ' repositories did not return before the '
332 							+ 'configured timeout of ' + timeout + 'ms.');
333 					numOpenQueries = 0;
334 				}
335 				clearTimeout(timer);
336 				report(callback, bundle(results, allmetainfo));
337 			}, timeout);
338 
339 			/**
340 			 * Invoked by each repository when it wants to present its results
341 			 * to the manager.
342 			 *
343 			 * Collects the results from each repository, and decrements the
344 			 * numOpenQueries semaphore to indicate that there is one less
345 			 * repository for which the manager is waiting for a reponse.
346 			 *
347 			 * If a repository invokes this callback after all openCallbacks
348 			 * have been closed (ie: numOpenQueries == 0), then the repository
349 			 * was too late ("missed the ship"), and will be ignored.
350 			 *
351 			 * If numOpenQueries decrements to 0 during this call, it means that
352 			 * the the manager is ready to report the results back to the client
353 			 * through the report() method.
354 			 *
355 356 			 * @param {Array.<Document|Folder>} items Results returned by the
357 			 *                                        repository.
358 			 * @param {object<string, number>} metainfo Optional Metainfo
359 			 *                                          returned by some
360 			 *                                          repositories.
361 			 */
362 			var process = function (items, metainfo) {
363 				var repository = this;
364 
365 				if (0 === numOpenQueries) {
366 					return;
367 				}
368 
369 				if (items && items.length) {
370 
371 					// Because some negligent repository implementations do not
372 					// set repositoryId properly.
373 					if (!items[0].repositoryId) {
374 						var id = repository.repositoryId;
375 						var i;
376 						for (i = 0; i < items.length; i++) {
377 							items[i].repositoryId = id;
378 						}
379 					}
380 
381 					$.merge(results, items);
382 				}
383 
384 				if (metainfo && allmetainfo) {
385 					allmetainfo.numItems =
386 						($.isNumeric(metainfo.numItems) &&
387 						 $.isNumeric(allmetainfo.numItems))
388 							? allmetainfo.numItems + metainfo.numItems
389 							: undefined;
390 
391 					allmetainfo.hasMoreItems =
392 						(typeof metainfo.hasMoreItems === 'boolean' &&
393 						 typeof allmetainfo.hasMoreItems === 'boolean')
394 							? allmetainfo.hasMoreItems || metainfo.hasMoreItems
395 							: undefined;
396 
397 					if (metainfo.timeout) {
398 						allmetainfo.timeout = true;
399 					}
400 				} else {
401 
402 					// Because if even one repository does not return metainfo,
403 					// so we have no aggregated metainfo at all.
404 					allmetainfo = undefined;
405 				}
406 
407 				Console.debug(manager, 'The repository '
408 						+ repository.repositoryId + 'returned with '
409 						+ items.length + ' results.');
410 
411 				// TODO: how to return the metainfo here?
412 				if (0 === --numOpenQueries) {
413 					clearTimeout(timer);
414 					report(callback, bundle(results, allmetainfo));
415 				}
416 			};
417 
418 			var repositories = params.repositoryId
419 			                 ? [manager.getRepository(params.repositoryId)]
420 			                 : manager.repositories;
421 
422 			var queue = filter(repositories, repositoryFilters.query);
423 
424 			// If none of the repositories implemented the query method, then
425 			// don't wait for the timeout, simply report to the client.
426 			if (0 === queue.length) {
427 				clearTimeout(timer);
428 				report(callback, bundle(results, allmetainfo));
429 				return;
430 			}
431 
432 			var makeProcess = function (repository) {
433 				return function () {
434 					process.apply(repository, arguments);
435 				};
436 			};
437 
438 			numOpenQueries = queue.length;
439 
440 			for (i = 0; i < queue.length; i++) {
441 				queue[i].query(params, makeProcess(queue[i]));
442 			}
443 		},
444 
445 		/**
446 		 * Retrieves children items.
447 		 *
448 		 * @param {object<string,mixed>} params Object with properties.
449 		 *
450 		 *  objectTypeFilter: Array   (optional) Object types to be retrieved.
451 		 *            filter: Array   (optional) Attributes to be retrieved.
452 		 *        inFolderId: boolean (optional) This indicates whether or not
453 		 *                                       a candidate object is a
454 		 *                                       child-object of the folder
455 		 *                                       object identified by the given
456 		 *                                       inFolderId (objectId).
457 		 *           orderBy: Array   (optional) example: [{
458 		 *                                           lastModificationDate: 'DESC',
459 		 *                                           name: 'ASC'
460 		 *                                       }]
461 		 *          maxItems: number  (optional) number Items to return as a result.
462 		 *         skipCount: number  (optional) This is tricky in a merged
463 		 *                                       multi repository scenario.
464 		 *   renditionFilter: Array   (optional) Instead of termlist an Array
465 		 *                                       of kind or mimetype is
466 		 *                                       expected. If null or
467 		 *                                       Array.length == 0 all
468 		 *                                       renditions are returned. See
469 		 *                                       http://docs.oasis-open.org/cmis/CMIS/v1.0/cd04/cmis-spec-v1.0.html#_Ref237323310
470 		 *                                       for renditionFilter.
471 		 *
472 		 * @param {function(Document|Folder)} callback Function to be invoked
473 		 *                                             after the repository
474 		 *                                             manager has finished
475 		 *                                             querying all
476 		 *                                             repositories.
477 		 */
478 		getChildren: function (params, callback) {
479 			var manager = this;
480 
481 			var i;
482 
483 			// The marged results, collected from repository responses.
484 			var results = [];
485 
486 			// A counting semaphore (working in reverse, ie: 0 means free).
487 			var numOpenQueries = 0;
488 
489 			var timeout = (params.timeout && parseInt(params.timeout, 10))
490 			           || manager.settings.timeout;
491 
492 			var timer = window.setTimeout(function () {
493 				if (numOpenQueries > 0) {
494 					Console.warn(manager, numOpenQueries
495 							+ ' repositories did not respond before the '
496 							+ 'configured timeout of ' + timeout + 'ms.');
497 					numOpenQueries = 0;
498 				}
499 				clearTimeout(timer);
500 				report(callback, results);
501 			}, timeout);
502 
503 			var process = function (items) {
504 				if (0 === numOpenQueries) {
505 					return;
506 				}
507 				if (items) {
508 					$.merge(results, items);
509 				}
510 				if (0 === --numOpenQueries) {
511 					clearTimeout(timer);
512 					report(callback, results);
513 				}
514 			};
515 
516 			var repositories = params.repositoryId
517 			                 ? [manager.getRepository(params.repositoryId)]
518 			                 : manager.repositories;
519 
520 			if (params.repositoryFilter && params.repositoryFilter.length) {
521 				repositories = filter(repositories, function (repository) {
522 					return -1 < $.inArray(repository.repositoryId,
523 						params.repositoryFilter);
524 				});
525 			}
526 
527 			// If the inFolderId is the default id of 'aloha', then return all
528 			// registered repositories as the result set.
529 			if ('aloha' === params.inFolderId) {
530 				var hasRepoFilter = params.repositoryFilter
531 				                 && 0 < params.repositoryFilter.length;
532 
533 				for (i = 0; i < repositories.length; i++) {
534 					results.push(new Aloha.RepositoryFolder({
535 						id: repositories[i].repositoryId,
536 						name: repositories[i].repositoryName,
537 						repositoryId: repositories[i].repositoryId,
538 						type: 'repository',
539 						hasMoreItems: true
540 					}));
541 				}
542 
543 				clearTimeout(timer);
544 				report(callback, results);
545 				return;
546 			}
547 
548 			var queue = filter(repositories, repositoryFilters.getChildren);
549 
550 			if (0 === queue.length) {
551 				clearTimeout(timer);
552 				report(callback, results);
553 				return;
554 			}
555 
556 			numOpenQueries = queue.length;
557 
558 			for (i = 0; i < queue.length; i++) {
559 				queue[i].getChildren(params, process);
560 			}
561 		},
562 
563 		/**
564 		 * @fixme: Not tested, but the code for this function does not seem to
565 		 *        compute repository.makeClean will be undefined
566 		 *
567 		 * @todo: Rewrite this function header comment so that is clearer
568 		 *
569 		 * Pass an object, which represents an marked repository to corresponding
570 		 * repository, so that it can make the content clean (prepare for saving)
571 		 *
572 		 * @param {jQuery} obj - representing an editable
573 		 * @return void
574 		 */
575 		makeClean: function (obj) {
576 			// iterate through all registered repositories
577 			var that = this,
578 				repository = {},
579 				i = 0,
580 				j = that.repositories.length;
581 
582 			// find all repository tags
583 			obj.find('[data-gentics-aloha-repository=' + this.prefix + ']').each(function () {
584 				while (i < j) {
585 					repository.makeClean(obj);
586 					i += 1;
587 				}
588 				Console.debug(that, 'Passing contents of HTML Element with id { ' + this.attr('id') + ' } for cleaning to repository { ' + repository.repositoryId + ' }');
589 				repository.makeClean(this);
590 			});
591 		},
592 
593 		/**
594 		 * Marks an object as repository of this type and with this item.id.
595 		 * Objects can be any DOM objects as A, SPAN, ABBR, etc. or
596 		 * special objects such as aloha-aloha_block elements.
597 		 *
598 		 * Marks the target obj with two private attributes:
599 		 * (see http://dev.w3.org/html5/spec/elements.html#embedding-custom-non-visible-data)
600 		 *	- data-gentics-aloha-repository: stores the repositoryId
601 		 *	- data-gentics-aloha-object-id: stores the object.id
602 		 *
603 		 * @param {HTMLElement} obj DOM object to mark.
604 		 * @param {Aloha.Repository.Object} item Item which is applied to obj,
605 		 *                                       if set to null, the
606 		 *                                       "data-GENTICS-..." attributes
607 		 *                                       are removed.
608 		 */
609 		markObject: function (obj, item) {
610 			if (!obj) {
611 				return;
612 			}
613 
614 			var manager = this, $obj = $(obj);
615 
616 			if (item) {
617 				var repository = manager.getRepository(item.repositoryId);
618 				if (repository) {
619 					// only mark the object if something changed
620 					if ($obj.attr('data-gentics-aloha-repository') !== item.repositoryId ||
621 							$obj.attr('data-gentics-aloha-object-id') !== item.id) {
622 						$obj.attr({
623 							'data-gentics-aloha-repository': item.repositoryId,
624 							'data-gentics-aloha-object-id': item.id
625 						});
626 						repository.markObject(obj, item);
627 					}
628 				} else {
629 					Console.error(manager, 'Trying to apply a repository "'
630 							+ item.name
631 							+ '" to an object, but item has no repositoryId.');
632 				}
633 			} else {
634 				$obj.removeAttr('data-gentics-aloha-repository')
635 				    .removeAttr('data-gentics-aloha-object-id');
636 			}
637 		},
638 
639 		/**
640 		 * Get the object for which the given DOM object is marked from the
641 		 * repository.
642 		 *
643 		 * Will initialize the item cache (per repository) if not already done.
644 		 *
645 		 * @param {HTMLElement} element DOM object which probably is marked.
646 		 * @param {function} callback
647 		 */
648 		getObject: function (element, callback) {
649 			var manager = this;
650 			var $element = $(element);
651 			var itemId = $element.attr('data-gentics-aloha-object-id');
652 			var repositoryId = $element.attr('data-gentics-aloha-repository');
653 			var repository = manager.getRepository(repositoryId);
654 
655 			if (repository && itemId) {
656 				if (!manager.itemCache) {
657 					manager.itemCache = [];
658 				}
659 
660 				var cache = manager.itemCache[repositoryId];
661 				if (!cache) {
662 					cache = manager.itemCache[repositoryId] = [];
663 				}
664 
665 				if (cache[itemId]) {
666 					callback([cache[itemId]]);
667 				} else {
668 					repository.getObjectById(itemId, function (items) {
669 						cache[itemId] = items[0];
670 						callback(items);
671 					});
672 				}
673 			}
674 		},
675 
676 		/**
677 		 * Mark a folder as opened.
678 		 *
679 		 * Called by a repository client (eg: repository browser) when a folder
680 		 * is opened.
681 		 *
682 		 * @param {object|Folder} folder Object with property repositoryId.
683 		 */
684 		folderOpened: function (folder) {
685 			var repository = this.getRepository(folder.repositoryId);
686 			if (typeof repository.folderOpened === 'function') {
687 				repository.folderOpened(folder);
688 			}
689 		},
690 
691 		/**
692 		 * Mark a folder as closed.
693 		 *
694 		 * Called by a repository client (eg: repository browser) when a folder
695 		 * is closed.
696 		 *
697 		 * @param {object|Folder} folder Object with property repositoryId.
698 		 */
699 		folderClosed: function (folder) {
700 			var repository = this.getRepository(folder.repositoryId);
701 			if (typeof repository.folderClosed === 'function') {
702 				repository.folderClosed(folder);
703 			}
704 		},
705 
706 		/**
707 		 * Mark a folder as selected.
708 		 *
709 		 * Called by a repository client (eg: repository browser) when a folder
710 		 * is selected.
711 		 *
712 		 * @param {object|Folder} folder Object with property repositoryId.
713 		 */
714 		folderSelected: function (folder) {
715 			var repository = this.getRepository(folder.repositoryId);
716 			if (typeof repository.folderSelected === 'function') {
717 				repository.folderSelected(folder);
718 			}
719 		},
720 
721 		/**
722 		 * Retrieve the selected folder.
723 		 *
724 		 * @return {Folder} Selected folder or null if it cannot be found.
725 		 */
726 		getSelectedFolder: function () {
727 			var repositories = filter(this.repositories,
728 					repositoryFilters.getSelectedFolder);
729 			var i;
730 			var selected;
731 			for (i = 0; i < repositories.length; i++) {
732 				selected = repositories[i].getSelectedFolder();
733 				if (selected) {
734 					return selected;
735 				}
736 			}
737 			return null;
738 		},
739 
740 		/**
741 		 * Human readable representation of repository manager.
742 		 *
743 		 * @return {string}
744 		 */
745 		toString: function () {
746 			return 'repositorymanager';
747 		}
748 
749 	});
750 
751 	Aloha.RepositoryManager = new RepositoryManager();
752 753 
	return Aloha.RepositoryManager;
754 });
755