/** * Aloha.Browser * * The browser is an interface to interact with a Repository Managers. * * Reference: * www.aloha-editor.org/wiki/Repository * 3rd party tools: * www.jstree.com/documentation/core * www.trirand.com/blog (jqGrid) * layout.jquery-dev.net * * NOTE: regarding 'jquery-plugin' RequireJS loader: * due to Cross-Origin Resource Sharing (CORS) / Access-Control-Allow-Origin * problems with the CDN the jQuery plugins got a litte rewrite. * * ATTENTION: when updating the plugins you need to insert at the top of the file: * * define(['aloha/jquery'], function(jQuery) { * var $ = jQuery; * * and at the end of the file: * * }); * * see: README https://github.com/jrburke/require-jquery/ or * http://requirejs.org/docs/jquery.html * * others: depend.js from https://github.com/millermedeiros/requirejs-plugins * * todo: think about if using order plugin or priority config can help us here */ define( [ 'aloha/jquery', 'util/class', 'i18n!browser/nls/i18n', 'aloha/console', // this will load the correct language pack needed for the browser 'browser/locale', 'css!browser/css/browsercombined.css', 'browser/vendor/jquery.ui', 'browser/vendor/ui-layout', 'browser/vendor/jquery.jqGrid', 'browser/vendor/jquery.jstree' ], function ( jQuery, Class, i18n, Console ) { var uid = +(new Date), nsClasses = { 'tree' : 'aloha-browser-tree', 'tree-header' : 'aloha-browser-tree-header', 'grab-handle' : 'aloha-browser-grab-handle', 'shadow' : 'aloha-browser-shadow', 'rounded-top' : 'aloha-browser-rounded-top', 'list' : 'aloha-browser-list', 'list-altrow' : 'aloha-browser-list-altrow', 'list-resizable' : 'aloha-browser-list-resizable', 'list-pager' : 'aloha-browser-list-pager', 'list-btns' : 'aloha-browser-list-btns', 'search-btn' : 'aloha-browser-search-btn', 'search-field' : 'aloha-browser-search-field', 'search-icon' : 'aloha-browser-search-icon', 'close-btn' : 'aloha-browser-close-btn', 'btn' : 'aloha-browser-btn', 'btns' : 'aloha-browser-btns', 'grid' : 'aloha-browser-grid', 'clear' : 'aloha-browser-clear', 'inner' : 'aloha-browser-inner', 'modal-overlay' : 'aloha-browser-modal-overlay', 'modal-window' : 'aloha-browser-modal-window' }; // ------------------------------------------------------------------------ // Local (helper) functions // ------------------------------------------------------------------------ /** * Simple templating * * @param {String} str - The string containing placeholder keys in curly * brackets * @param {Object} obj - Associative array of replacing placeholder keys * with corresponding values */ function supplant (str, obj) { return str.replace(/\{([a-z0-9\-\_]+)\}/ig, function (str, p1, offset, s) { var replacement = obj[p1] || str; return (typeof replacement === 'function') ? replacement() : replacement; }); }; /** * Wrapper to call the supplant method on a given string, taking the * nsClasses object as the associative array containing the replacement * pairs * * @param {String} str * @return {String} */ function renderTemplate (str) { return (typeof str === 'string') ? supplant(str, nsClasses) : str; }; /** * @param {jQuery} el */ function disableSelection (el) { el.each(function() { jQuery(this) .attr('unselectable', 'on') .css({ '-moz-user-select':'none', '-webkit-user-select':'none', 'user-select':'none' }) .each(function() { this.onselectstart = function() { return false; }; }); }); }; var Browser = Class.extend({ _constructor: function() { this.init.apply(this, arguments); }, init: function(opts) { // Extend defaults var options = jQuery.extend({ // Set to true for development and debugging verbose : false, // The repository manager which this browser will interface with repositoryManager : null, repositoryFilter : [], objectTypeFilter : [], renditionFilter : ['cmis:none'], // ['*'] filter : ['url'], // DOMObject to which this instance of browser is bound to element : undefined, // root folder id rootFolderId : 'aloha', // root path to where Browser resources are located rootPath : '', treeWidth : 300, listWidth : 'auto', pageSize : 10, columns : { icon : {title: '', width: 30, sortable: false, resizable: false}, name : {title: i18n.t('Name'), width: 250, sorttype: 'text'}, url : {title: i18n.t('URL'), width: 250, sorttype: 'text'}, preview : {title: i18n.t('Preview'), width: 200, sorttype: 'text'} }, isFloating : false }, opts || {}); // If no element, the we will an overlay element onto which we will bind // our browser if (!options.element || !options.element.length) { options.isFloating = true; options.element = this.createOverlay(); } // Hash to store callbacks functions for each instance of browser this._callbacks = {}; // Cache of repository objects this._objs = {}; // this._searchQuery = null; this._orderBy = null; //--------------------------------------------------------------------- // paging properties //--------------------------------------------------------------------- this._pagingOffset = 0; // Total number of objects in a given folder. We don't use null because // isNaN(null) == false ! *sigh* this._pagingCount = undefined; this._pagingBtns = { first : null, end : null, next : null, prev : null }; // Register user defined implement methods and callbacks, and remove // them from the options object // TODO: Consider deprecating this altogether if (typeof options.implement === 'object') { jQuery.each(options.implement, function (k, v) { that[k] = v; }); delete options.implement; } // TODO: Consider deprecating this too if (typeof options.callbacks === 'object') { jQuery.each(options.callbacks, function () { that.callback(this[0], this[1]); }); delete options.callbacks; } // Insert the remaining options as properties of this object jQuery.extend(this, options); var that = this; var tree_width = this.treeWidth; var give = tree_width / 5; this.preloadImages(); this.element.attr('data-aloha-browser', ++uid).html(''); // set the total element width (if configured) if (this.totalWidth) { this.element.width(this.totalWidth); } this.grid = this.createGrid(this.element).resize(); this.tree = this.createTree(this.grid.find('.ui-layout-west')); this.list = this.createList(this.grid.find('.ui-layout-center')); this.grid.layout({ west__size : tree_width - 1, west__minSize : tree_width - give, west__maxSize : tree_width + give, center__size : 'auto', paneClass : 'ui-layout-pane', resizerClass : 'ui-layout-resizer', togglerClass : 'ui-layout-toggler', onresize : function (name, element) { if (name == 'center') { that.list.setGridWidth(element.width()); } } // , applyDefaultStyles: true // debugging }).sizePane('west', tree_width); // *** Fix for a ui-layout bug in chrome *** disableSelection(this.grid); this.close(); }, destroy: function () { }, instanceOf: 'Aloha.Browser', // ui components grid: null, tree: null, list: null, preloadImages: function () { var that = this; jQuery.each([ 'arrow-000-medium.png', 'arrow-180.png', 'arrow-315-medium.png', 'arrow-stop-180.png', 'arrow-stop.png', 'arrow.png', 'control-stop-square-small.png', 'folder-horizontal-open.png', 'folder-open.png', 'magnifier-left.png', 'page.png', 'picture.png', 'sort-alphabet-descending.png', 'sort-alphabet.png' ], function () { (new Image()).src = that.rootPath + 'img/' + this; }); }, /** * TODO: Is there a way we can guard against potential infinite loops? * * @param {Associative Array Object} fn - Name of any Browser method */ trigger: function (fn, returned) { var cb = this._callbacks; var func = cb[fn]; if (typeof func === 'object') { for (var i = 0, l = func.length; i < l; ++i) { if (typeof func[i] === 'function') { func[i].call(this, returned); } } } }, /** * Handles the binding of callbacks to any method in the browser. * Removes the necessity for functions to manually trigger callbacks * events within themselves by wrapping them within an function that * does it on their behalf when necessary. * A user can simply do the following: * browser.callback('anyFunction', function () { alert('called back'); }); * This will work regardless of whether browser.anyFunction manually * triggers any events. * * this.enableCallbacks actually does most of the heavy lifting. * * @param {String} fn - Name of any browser method * @param {Function} cb - Callback function to invoke */ callback: function (fn, cb) { if (typeof this[fn] != 'function') { Console.warn( 'Unable to add a callback to "' + fn + '" because it is not a method in Aloha.Browser.' ); return this; } if (typeof cb !== 'function') { Console.warn( 'Unable to add a callback to "' + fn + '" because ' + 'the callback object that was given is of type "' + (typeof cb) + '". ' + 'The callback object needs to be of type "function".' ); return this; } if (this._callbacks[fn] == undefined) { if (this.enableCallbacks(fn)) { this._callbacks[fn] = [cb]; } } else { this._callbacks[fn].push(cb); } return this; }, /** * Work-horse for this.callback method */ enableCallbacks: function (fn) { var browser_inst = this; var func = this[fn]; if (typeof func === 'function') { this[fn] = function () { var returned = func.apply(browser_inst, arguments); browser_inst.trigger.call(browser_inst, fn, returned); return returned; }; return true; } else { Console.warn( 'Cannot enable callbacks for function "' + fn + '" because no such method was found in Aloha.Browser.' ); return false; } }, getRepoChildren: function (params, callback) { var that = this; if (this.repositoryManager) { this.repositoryManager.getChildren(params, function (items) { that.processRepoResponse(items, callback); }); } }, queryRepository: function (params, callback) { var that = this; if (this.repositoryManager) { this.repositoryManager.query(params, function (response) { that.processRepoResponse( (response.results > 0) ? response.items : [], { numItems: response.numItems, hasMoreItems: response.hasMoreItems}, callback ); }); } }, processRepoResponse: function (items, metainfo, callback) { var that = this; var data = []; // if the second parameter is a function, it is the callback if (typeof metainfo === 'function') { callback = metainfo; metainfo = undefined; } jQuery.each(items, function () { data.push(that.harvestRepoObject(this)); }); if (typeof callback === 'function') { callback.call(this, data, metainfo); } }, /** * Convert a repository object into an object that can be used with our * tree component. Also add a reference to this object in our objs hash. * According to the Repository specification, each object will at least * have the following properties at least: id, name, url, and type. Any * and all other attributes are optional. */ harvestRepoObject: function (obj) { ++uid; var repo_obj = this._objs[uid] = jQuery.extend(obj, { uid : uid, loaded : false }); return this.processRepoObject(repo_obj); }, /** * Should return an object that is usable with your tree component */ processRepoObject: function (obj) { var icon = '', attr; switch (obj.baseType) { case 'folder': icon = 'folder'; break; case 'document': icon = 'document'; break; } // if the object has a type set, we set it as type to the node if (obj.type) { attr = {rel: obj.type}; } return { data: { title : obj.name, attr : {'data-rep-oobj': obj.uid}, icon : icon }, attr : attr, state: (obj.hasMoreItems || obj.baseType === 'folder') ? 'closed' : null, resource: obj }; }, fetchRepoRoot: function (callback) { if (this.repositoryManager) { this.getRepoChildren( { inFolderId : this.rootFolderId, repositoryFilter : this.repositoryFilter }, function (data) { if (typeof callback === 'function') { callback(data); } } ); } }, /** * User should implement this according to their needs * @param Object item - Repository resource for a row */ renderRowCols: function (item) { var row = {}; jQuery.each(this.columns, function (colName, v) { switch (colName) { case 'icon': row.icon = '
'; break; default: row[colName] = item[colName] || '--'; } }); return row; }, /** * User should implement this according to their needs * * @param {Object} item - Repository resource for a row */ onSelect: function (item) {}, /** * Fetch an object's children if we haven't already done so */ fetchChildren: function (obj, callback) { var that = this; if (obj.hasMoreItems == true || obj.baseType === 'folder') { if (obj.loaded == false) { this.getRepoChildren( { inFolderId : obj.id, repositoryId : obj.repositoryId }, function (data) { that._objs[obj.uid].loaded = true; if (typeof callback === 'function') { callback(data); } } ); } } }, getObjectFromCache: function (node) { var obj; if (typeof node === 'object') { var uid = node.find('a:first').attr('data-rep-oobj'); obj = this._objs[uid]; } return obj; }, rowClicked: function (event) { var row = jQuery(event.target).parent('tr'); var item = null; if (row.length > 0) { var uid = row.attr('id'); item = this._objs[uid]; this.onSelect(item); } return item; }, createTree: function (container) { var that = this; var tree = jQuery(renderTemplate('