pix-angular-filebrowser
Version:
File browser for Web2
679 lines (586 loc) • 23.9 kB
JavaScript
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);
})();