kahuna/public/js/components/gr-collections-panel/gr-collections-panel.js (253 lines of code) (raw):

import angular from 'angular'; import Rx from 'rx'; import '../../util/rx'; import '../../services/panel'; import {collectionsApi} from '../../services/api/collections-api'; import {mediaApi} from '../../services/api/media-api'; import '../../directives/gr-auto-focus'; import '../../util/eq'; import '../../util/storage'; import './gr-collections-panel.css'; import {getCollection} from '../../search-query/query-syntax'; import nodeTemplate from './gr-collections-panel-node.html'; import '../../directives/gr-auto-focus'; export var grCollectionsPanel = angular.module('grCollectionsPanel', [ 'kahuna.services.panel', collectionsApi.name, mediaApi.name, 'util.rx', 'util.eq', 'gr.autoFocus', 'util.storage' ]); grCollectionsPanel.factory('collectionsTreeState', ['$window', function($window) { // TODO: Add garbage collection to state. const localStorageKey = 'collectionsTreeOpen'; const jsonStr = $window.localStorage.getItem(localStorageKey) || '[]'; // A little bit of superstition in case this was set weirdly before. let jsonArr = []; try { jsonArr = JSON.parse(jsonStr); if (!Array.isArray(jsonArr)) { jsonArr = []; $window.localStorage.setItem(localStorageKey, '[]'); } } catch (_) { // On JSON.parse fail - use default } const stateCache = new Set(jsonArr); function setState(pathId, show) { if (show) { stateCache.add(pathId); } else { stateCache.delete(pathId); } $window.localStorage.setItem(localStorageKey, JSON.stringify(Array.from(stateCache))); } function getState(pathId) { return stateCache.has(pathId); } return { setState, getState }; }]); grCollectionsPanel.controller('GrCollectionsPanelCtrl', [ '$scope', '$timeout', 'collections', 'selectedImages$', 'selectedCollections', function ($scope, $timeout, collections, selectedImages$, selectedCollections) { const ctrl = this; ctrl.$onInit = () => { ctrl.error = false; collections.getCollections().then(collections => { ctrl.collections = collections.data.children; // this will trigger the remember-scroll-top directive to return // users to their previous position on the collections panel // once the tree has been rendered $timeout(() => { $scope.$emit('gr:remember-scroll-top:apply'); }); }, () => { // TODO: More informative error handling // TODO: Stop error propagating to global error handler ctrl.error = true; }).catch(() => ctrl.error = true); ctrl.selectedImages$ = selectedImages$; ctrl.selectedCollections = selectedCollections; }; }]); grCollectionsPanel.controller('GrNodeCtrl', ['$scope', 'collections', 'subscribe$', 'inject$', 'onValChange', 'collectionsTreeState', 'storage', function($scope, collections, subscribe$, inject$, onValChange, collectionsTreeState, storage) { const ctrl = this; ctrl.$onInit = () => { try { ctrl.node.data.data.pathId; } catch (e) { console.info('unable to find pathId for node, tree failing to render to completion'); console.info(ctrl.node); } const pathId = ctrl.node.data.data.pathId; //This filter remove child nodes with missing data, preventing display errors from occurring ctrl.filterChildren = children => children.filter(node => !!node.data.data); ctrl.children = ctrl.filterChildren(ctrl.node.data.children); $scope.$watch('ctrl.node.data.children', children => { ctrl.children = ctrl.filterChildren(children); }); ctrl.saving = false; ctrl.removing = false; ctrl.deletable = false; ctrl.showChildren = collectionsTreeState.getState(pathId); ctrl.formError = null; ctrl.addChild = childName => { return collections.addChildTo(ctrl.node, childName). then($scope.clearForm). catch(e => $scope.formError = e.body && e.body.errorMessage); }; collections.isDeletable(ctrl.node).then(d => ctrl.deletable = d); ctrl.remove = () => collections.removeFromList(ctrl.node, ctrl.nodeList); ctrl.getCollectionQuery = path => getCollection(path); $scope.$watch('ctrl.showChildren', onValChange(show => { collectionsTreeState.setState(pathId, show); })); ctrl.init = function(grCollectionTreeCtrl) { const selectedImages$ = grCollectionTreeCtrl.selectedImages$; const selectedCollections = grCollectionTreeCtrl.selectedCollections; // TODO: move this somewhere sensible, we probably don't want an observable for each node. const add$ = new Rx.Subject(); const pathWithImages$ = add$.withLatestFrom(selectedImages$, (path, images) => ({path, images})); const hasImagesSelected$ = selectedImages$.map(i => i.size > 0); ctrl.addImagesToCollection = () => { ctrl.saving = true; add$.onNext(ctrl.node.data.fullPath); }; subscribe$($scope, pathWithImages$, ({path, images}) => { collections.addToCollectionUsingImageResources(images, path) .then(() => ctrl.saving = false); }); const remove$ = new Rx.Subject(); const pathToRemoveWithImages$ = remove$.withLatestFrom(selectedImages$, (path, images) => ({path, images})); ctrl.removeImagesFromCollection = () => { ctrl.removing = true; remove$.onNext(pathId); }; subscribe$($scope, pathToRemoveWithImages$, ({path, images}) => { collections.batchRemove(images, path).then(() => ctrl.removing = false); }); inject$($scope, hasImagesSelected$, ctrl, 'hasImagesSelected'); ctrl.isSelected = selectedCollections.some(col => { return angular.equals(col, pathId); }); ctrl.hasCustomSelect = !! grCollectionTreeCtrl.onSelect; ctrl.select = () => { grCollectionTreeCtrl.onSelect({$collection: ctrl.node.data.data.path}); }; ctrl.srefNonfree = () => storage.getJs("isNonFree", true) ? true : undefined; }; }; }]); grCollectionsPanel.directive('grNode', ['$parse', '$compile', function($parse, $compile) { const templateString = `<gr-nodes ng:if="ctrl.showChildren && ctrl.node.data.children.length > 0" gr:nodes="ctrl.children" ></gr-nodes>`; return { restrict: 'E', require: ['grNode', '^^grCollectionTree'], scope: { node: '=grNode', nodeList: '=grNodeList' }, template: nodeTemplate, controller: 'GrNodeCtrl', controllerAs: 'ctrl', bindToController: true, compile: function() { // Memoize the `$compile` result for performance reasons // (compile invoked once per reference, link invoked once // per use) let compiledTemplate; return function link(scope, element, attrs, [grNodeCtrl, grCollectionTreeCtrl]) { if (! compiledTemplate) { // We compile the template on the fly here as angular doesn't deal // well with recursive templates. compiledTemplate = $compile(templateString); } compiledTemplate(scope, cloned => { const container = element.find('gu-template-container'); Array.from(cloned).forEach(clone => container.append(clone)); }); grNodeCtrl.init(grCollectionTreeCtrl); //so editing can toggle - used to show add & remove collection buttons scope.grCollectionTreeCtrl = grCollectionTreeCtrl; scope.clearForm = () => { scope.active = false; scope.childName = ''; scope.formError = null; }; }; } }; }]); grCollectionsPanel.directive('grNodes', function() { return { restrict: 'E', scope: { nodes: '=grNodes' }, controller: function(){}, controllerAs: 'grNodesCtrl', bindToController: true, template: `<ul> <li ng:repeat="node in grNodesCtrl.nodes"> <gr-node class="node" gr:node="node" gr:node-list="grNodesCtrl.nodes" ></gr-node> </li> </ul>` }; }); grCollectionsPanel.directive('grCollectionTree', function() { return { restrict: 'E', scope: { nodes: '=grNodes', editing: '=?grEditing', selectedImages$: '=?grSelectedImages', selectedCollections: '=?grSelectedCollections', selectionMode: '=?grSelectionMode', onSelect: '&?grOnSelect' }, controller: function(){ const grCollectionTreeCtrl = this; if (! grCollectionTreeCtrl.selectedImages$) { grCollectionTreeCtrl.selectedImages$ = Rx.Observable.empty(); } if (! grCollectionTreeCtrl.selectedCollections) { grCollectionTreeCtrl.selectedCollections = []; } }, controllerAs: 'grCollectionTreeCtrl', bindToController: true, template: `<gr-nodes gr:nodes="grCollectionTreeCtrl.nodes"></gr-nodes>` }; }); grCollectionsPanel.directive('grDropIntoCollection', ['$timeout', '$parse', 'vndMimeTypes', 'collections', function($timeout, $parse, vndMimeTypes, collections) { const dragOverClass = 'collection-drop-drag-over'; return { restrict: 'A', link: function(scope, element, attrs) { const collectionPath = $parse(attrs.grDropIntoCollection)(scope); element.on('drop', e => { const dt = e.dataTransfer; const gridImagesData = dt.getData(vndMimeTypes.get('gridImagesData')); const gridImageData = dt.getData(vndMimeTypes.get('gridImageData')); if (gridImagesData !== '' || gridImageData !== '') { const imagesData = gridImagesData !== '' ? JSON.parse(gridImagesData) : [JSON.parse(gridImageData)]; const imageIds = imagesData.map(imageJson => imageJson.data.id); scope.dropIntoCollectionSaving = true; collections.addToCollectionUsingImageIds(imageIds, collectionPath).then(() => { scope.dropIntoCollectionSaving = false; }); } element.removeClass(dragOverClass); }); element.on('dragover', () => { element.addClass(dragOverClass); }); element.on('dragleave', () => { element.removeClass(dragOverClass); }); } }; }]);