UNPKG

pix-angular-filebrowser

Version:

File browser for Web2

679 lines (586 loc) 23.9 kB
function FileBrowserController( $scope, $translate, $q, FileBrowserService, ItemsCollectionModel, NotificationsService, uiGridConstants, PIXFolder, hotkeys) { // our grid api var gridApi; var self = this; // and array to hold the currently selected items self.selection = []; // a way to keep track of our selection direction for keyboard hotkeys var lastSelectionDirection = 0; // set the loading state to true self.isLoading = true; // set up our grid options self.gridOptions = { // sorting options enableSorting: true, useExternalSorting: true, // selection options enableRowSelection: true, // enableSelectAll: true, enableSelectionBatchEvent: true, enableRowHeaderSelection: false, multiSelect: true, modifierKeysToMultiSelect: true, // core setup columnDefs: [], rowTemplate: 'pix.filebrowser.row.html', data: FileBrowserService.items, excessRows: 50, onRegisterApi: onRegisterApi, enableHorizontalScrollbar: uiGridConstants.scrollbars.NEVER }; // setup our columns, first translate the header strings $q.all({ name: $translate('FileBrowser.Column.Name'), description: $translate('FileBrowser.Column.Description'), createdOn: $translate('FileBrowser.Column.CreatedOn'), createdBy: $translate('FileBrowser.Column.CreatedBy'), modifiedOn: $translate('FileBrowser.Column.ModifiedOn'), modifiedBy: $translate('FileBrowser.Column.ModifiedBy') }) .then(function(names){ self.gridOptions.columnDefs = [ { name: names.name, field: 'viewData.fields.name', enableColumnMenu: false, cellTemplate: 'pix.filebrowser.name.html' }, { name: names.description, field: 'viewData.fields.description', enableColumnMenu: false, cellClass: 'text-muted' }, { name: names.createdOn, field: 'viewData.fields.created_on', enableColumnMenu: false, cellTemplate: 'pix.filebrowser.date.html', cellClass: 'text-muted' }, { name: names.createdBy, field: 'viewData.createdBy.viewData.label', enableColumnMenu: false, cellClass: 'text-muted' }, { displayName: names.modifiedOn, field: 'viewData.fields.modified_on', enableColumnMenu: false, cellTemplate: 'pix.filebrowser.date.html', cellClass: 'text-muted' }, { displayName: names.modifiedBy, field: 'viewData.modifiedBy.viewData.label', enableColumnMenu: false, cellClass: 'text-muted' } ] }); /** * A callback which is called when the grid API is ready * * @param gridApi {object} an instance of the ui-grid api */ function onRegisterApi(api) { // stash the grid API for later gridApi = api; // register our selection and sort changed handler gridApi.selection.on.rowSelectionChanged($scope, onSelectionChanged); gridApi.selection.on.rowSelectionChangedBatch($scope, onSelectionChanged); gridApi.core.on.sortChanged( $scope, onSortChanged); } /** * The selection changed handler */ function onSelectionChanged() { // update our selection array when the selection has changed self.selection = gridApi.selection.getSelectedRows(); } /** * The sort changed handler * * @param grid {object} an instance of the grid * @param sortColumns {array} an array of columns to sort by */ function onSortChanged(grid, sortColumns) { // build out our accessors array (an array of functions that return the cell value for a given column/row var accessors = []; var directions = []; _.each(sortColumns, function(col) { accessors.push(function(item) { return _.result(item, col.field); }) directions.push(col.sort.direction === 'asc'); }); FileBrowserService.sort(accessors, directions); } /** * Moves the current selection or appends to it (this is used for keyboard shortcuts) * * @param direction 1 means forward, -1 means backwards * @param append if true selected items will be appended otherwise they will be replaced */ function moveSelection(direction, append) { if (self.selection.length === 0) { gridApi.selection.selectRowByVisibleIndex(0); } else { // if we are in append mode and we just changed direction, we should unset what we just set if (append && self.selection.length > 1 && lastSelectionDirection !== 0 && lastSelectionDirection != direction) { // grab the end item and unselect it var end = self.selection[direction > 0 ? 0 : self.selection.length -1]; gridApi.selection.unSelectRow(end); return; } else { // otherwise just keep track of our direction lastSelectionDirection = direction; } // grab the first or last item depending on the direction we are moving var item = self.selection[direction < 0 ? 0 : self.selection.length -1]; // if we are not appending unselect everything if (!append) { _.each(self.selection, gridApi.selection.unSelectRow); } // get the next or prev item index var index = _.findIndex(FileBrowserService.items, function(i) { return i.id === item.id; }); index += direction; // keep index in range index = Math.min(Math.max(index, 0), FileBrowserService.items.length-1); var nextItem = FileBrowserService.items[index]; // select the row gridApi.selection.selectRow(nextItem); } } // set up our hotkeys hotkeys.bindTo($scope) .add({ combo: 'up', description: 'Move up', callback: function() { moveSelection(-1, false); } }) .add({ combo: 'shift+up', description: "Move down", callback: function() { moveSelection(-1, true); } }) .add({ combo: 'down', description: 'Move down', callback: function() { moveSelection(1, false); } }) .add({ combo: 'shift+down', description: 'Move down', callback: function() { moveSelection(1, true); } }) .add({ combo: 'enter', description: "Open in viewer", callback: function() { openInViewer(); } }) .add({ combo: 'left', description: 'collapse', callback: function() { _.each(self.selection, function(item) { if (item.isContainer) { self.collapse(item); } }) } }) .add({ combo: 'right', description: 'collapse', callback: function() { _.each(self.selection, function(item) { if (item.isContainer) { self.expand(item); } }) } }); // add our expand function to the scope self.expand = function(item, $event) { // stop the mouse event (an expand doesn't mean we are selecting that item) if ($event) { $event.stopImmediatePropagation(); } // set the loading state item.$$loading = true; // call expand on our service FileBrowserService.expand(item) .then(function() { // on success set expanded to true item.$$expanded = true; }, function() { // on failure flash an error NotificationsService.flashWarnings($translate('FileBrowser.Error.Expand', item)); item.$$expanded = false; }) .finally(function() { // either way set our loading state to false item.$$loading = false; }) }; // add our collapse function to the scope self.collapse = function(item, $event) { // stop the event propagation (collapse does not mean selection) if ($event) { $event.stopImmediatePropagation(); } FileBrowserService.collapse(item); item.$$expanded = false; }; /** * Open an item (or the current selection in the viewer) * * @param item {PIXItem} the item open. If no item is passed, we'll use the current selection */ function openInViewer(item) { // if we have an item wrap it in an array, otherwise just grab the selection var collection = item ? [item] : self.selection; // create a new ItemsCollectionModel using our selected items var collectionModel = new ItemsCollectionModel(); collectionModel.initializeFromExistingData(collection); // dispatch an event with our collection to open the viewer $scope.emitBusEvent('PIX.VIEWER.OPEN', { items: collectionModel, current: collectionModel.collection[0], skipUnpackCurrent: true }); } // add open in viewer to the scope self.openInViewer = openInViewer; /** * Shares the currently selected item(s) */ function share() { // create a collection from our selection var collectionModel = new ItemsCollectionModel(); collectionModel.initializeFromExistingData(self.selection); // set the attachments to our collection and show the message dialog self.sendAttachments = collectionModel; self.sendVisible = true; } self.share = share; // register a listener for the share close event and upon i hide the message-send directive $scope.onBusEvent('PIX.SHARE.CLOSE', function() { self.sendVisible = false; }); self.isLoading = true; FileBrowserService.init().then(function() { /** * Check if the item is a container and already expanded. If it is, update the UI. * @param item */ function checkDeepExpansion(item) { if (item.contentsCollectionModel && item.$$expanded) { FileBrowserService.expand(item); item.contentsCollectionModel.collection.forEach(function(innerItem) { checkDeepExpansion(innerItem); }); } } self.isLoading = false; FileBrowserService.items.forEach(function(item) { checkDeepExpansion(item); }); }); Object.defineProperty(self, 'canShare', { get: function () { if (!self.selection || self.selection.length === 0) { return false; } return _.every(self.selection, function (item) { return item.viewData.permissions.canSend && !(item instanceof PIXFolder); }); } }); } FileBrowserController.$inject = ["$scope", "$translate", "$q", "FileBrowserService", "ItemsCollectionModel", "NotificationsService", "uiGridConstants", "PIXFolder", "hotkeys"]; angular .module('PIX.FileBrowser.Controller', [ 'angularMoment', 'cfp.hotkeys' ]) .controller('FileBrowserController', FileBrowserController); (function () { 'use strict'; angular .module('PIX.FileBrowser', [ // filebrowser dependencies 'PIX.FileBrowser.Controller', 'PIX.FileBrowser.Templates', 'PIX.FileBrowser.Service', // PIX dependencies 'PIX.ItemTypes.PIXFolder', 'PIX.Items.CollectionModel', 'PIX.Accessibility.Loading', // third party libraries 'ui.grid', 'ui.grid.selection', 'angularMoment' ]); })(); (function () { 'use strict'; function FileBrowserService( $http, $q, $translate, $log, APP_SETTINGS, LOADING_OBJECT, PIXFolder, ItemsCollectionModel, NotificationsService, SessionService) { // PRIVATE variables var items = []; var isInitialized = false; var root; /** * Sets the items list in place so our scope reference stays intact * * @param val {array} Array of items to set the items array to */ function setItems(val) { items.length = 0; addItems(val); } function addItems(val, index) { index = index || 0; var args = [index,0].concat(val); Array.prototype.splice.apply(items, args); } function replaceItems(val, index) { index = index || 0; var args = [index,val.length].concat(val); Array.prototype.splice.apply(items, args); } /** * Expands a given object and places it in the proper place of the data array. * * @param item {PIXContainer} the item to expand */ function expand(item) { // set the parent state to loading. // call expand on the item to load the initial set of data return item.expand() .then(function () { // set up the attributes that are needed for the children (tree depth and parent) var childAttrs = { '$$level': (item.$$level + 1 ) || 1, '$$parentId': item.id } // grab the index of the parent var parentIndex = items.indexOf(item); // stores the current index our list is at var index = 0; // create a copy of the data that has come in so we can safely manipuate the array var results = _.clone(item.contentsCollectionModel.collection); // walk through the list of all children for this item (both currently loaded and that to be loaded) var children = _.map(Array(item.contentsCollectionModel.info.total), function () { // if we still have data from the initial load use it if (results.length > 0) { index++; return _.defaults(results.shift(), childAttrs); } // otherwise add a loading object else { return _.defaults({id: _.uniqueId('tmpFile')}, childAttrs, LOADING_OBJECT) } }); // add the children to the grid data array addItems(children, parentIndex + 1); /** * Checks if we have the entire set of children and if not kicks off another request * * @returns {boolean} whether or not more data is available */ function loadMoreIfNeeded() { // if there are more items we need to request if (item.contentsCollectionModel.cacheSettings.hasOutstandingRecords) { // set our page size to the max item.contentsCollectionModel.cacheSettings.pageSize = 500; //requests the next page of data item.contentsCollectionModel.cache.getNextPage().then(onNextPage); return true; } return false; } /** * The callback for a successful load of the next page of data */ function onNextPage() { // grab the grid index of the last item we added from our children collection var scopeIndex = items.indexOf(item.contentsCollectionModel.collection[index - 1]) + 1; // loop through the new items in the collection and add our tree specific attributes var newItems = _.map(item.contentsCollectionModel.collection.slice(index), function (child) { return _.defaults(child, childAttrs); }); // set the index pointer to the end of the child collection index = item.contentsCollectionModel.collection.length; // splice our new data onto the main grid data array using the scopeIndex we calculated replaceItems(newItems, scopeIndex); // see if we need to load more loadMoreIfNeeded(); } // try to load page 2 of data (and start the lazy load chain) loadMoreIfNeeded(); }); } /** * Collapses an item by removing it's children from the grid data array * * @param item {PIXContainer} the item to collapse */ function collapse(item) { // grab the level of the parent var parentLevel = item.$$level || 0; // two variables to track the indexes of where the children start and end var index; var startIndex; // grab the index of the first child and stash it for latter index = startIndex = items.indexOf(item) + 1; // grab the first child item var nextItem = items[index]; // walk through the list until we hit a sibling of the parent (or the end of the list) while (nextItem && nextItem.$$level > parentLevel) { // incr our index to keep track of the end index++; // if this child is expanded, collapse it if (nextItem.$$expanded) { nextItem.$$expanded = false; } // grab the next item and go back around nextItem = items[index]; } // remove our children using our calculated indexes items.splice(startIndex, index - startIndex); } /** * Sorts the internal items list * * @param accessors {function[]} an array of functions to access the data for the sort * @param directions {boolean[]} an array of sort directions for each column */ function sort(accessors, directions) { // build an object of rows group by their parent var hierarchy = _.groupBy(items, function(item) { return item.$$parentId || 'root'; }); // first sort by the root elements var sorted = _.sortByOrder(hierarchy.root, accessors, directions); // remove the root from our list delete hierarchy.root; // deal with the children for (var parentId in hierarchy) { // grab the list of children under the current parent var children = hierarchy[parentId]; // sort the child list children = _.sortByOrder(children, accessors, directions); // find our parent in the currently sorted list var parentIndex = _.findIndex(sorted, function(item) { return item.id === parentId; }); // add our children after their parent var args = [parentIndex + 1, 0].concat(children); Array.prototype.splice.apply(sorted, args) } setItems(sorted); } /** * Initialize the data if the service has not already been initialized * @returns {*} */ function init() { return $q(function(resolve, reject) { if (isInitialized) { resolve(); } else { SessionService .whenProjectOrSessionEnded .then(destroy); $http.get(APP_SETTINGS.HOST + '/' + APP_SETTINGS.API_ROOT + '/folders/root') .success(function(data) { // hydrate the server data into a PIXFolder object root = new PIXFolder(new ItemsCollectionModel()); root.populateSelf(data); // set our page size to the maximum root.getCollectionModel().cacheSettings.pageSize = 500; // expand root folder to get it's children root.expand() .then(function() { setItems(root.contentsCollectionModel.collection); resolve(); }) .catch(function() { NotificationsService.flashWarnings($translate('FileBrowser.Error.Expand', root)); reject(); }); }) .error(function() { NotificationsService.flashWarnings($translate('FileBrowser.Error.Root', root)); reject(); }); } }); } /** * Clear the service cache */ function destroy() { $log.debug('destroying file browser cache: session ended or project changed'); root.collapse(true); root = null; items = []; isInitialized = false; } // setup our public read-only properties Object.defineProperties(this, { items : { get: function() { return items; } }, expand: { value: expand, writable: false }, collapse: { value: collapse, writable: false }, sort: { value: sort, writable: false }, init: { value: init, writable: false } }); } FileBrowserService.$inject = ["$http", "$q", "$translate", "$log", "APP_SETTINGS", "LOADING_OBJECT", "PIXFolder", "ItemsCollectionModel", "NotificationsService", "SessionService"]; angular .module('PIX.FileBrowser.Service', []) .constant('LOADING_OBJECT', { viewData: { fields: { name: 'Loading' } } }) .service('FileBrowserService', FileBrowserService); })();