diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.jade index 0a12cece2f..fc4190be31 100644 --- a/services/web/app/views/project/editor/file-tree.jade +++ b/services/web/app/views/project/editor/file-tree.jade @@ -1,4 +1,4 @@ -aside#file-tree(ng-controller="FileTreeController").full-size +aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }").full-size .toolbar.toolbar-small.toolbar-alt(ng-if="permissions.write") a( href, @@ -27,7 +27,8 @@ aside#file-tree(ng-controller="FileTreeController").full-size href, ng-click="startRenamingSelected()", tooltip="#{translate('rename')}", - tooltip-placement="bottom" + tooltip-placement="bottom", + ng-show="multiSelectedCount == 0" ) i.fa.fa-pencil a( @@ -81,27 +82,25 @@ aside#file-tree(ng-controller="FileTreeController").full-size ) .entity .entity-name( - ng-click="select()" + ng-click="select($event)" ) //- Just a spacer to align with folders i.fa.fa-fw.toggle i.fa.fa-fw.fa-file span {{ entity.name }} - - - script(type='text/ng-template', id='entityListItemTemplate') li( - ng-class="{ 'selected': entity.selected }", + ng-class="{ 'selected': entity.selected, 'multi-selected': entity.multiSelected }", ng-controller="FileTreeEntityController" ) .entity(ng-if="entity.type != 'folder'") .entity-name( - ng-click="select()" + ng-click="select($event)" ng-dblclick="permissions.write && startRenaming()" draggable="permissions.write" + draggable-helper="draggableHelper" context-menu data-target="context-menu-{{ entity.id }}" context-menu-container="body" @@ -113,6 +112,7 @@ script(type='text/ng-template', id='entityListItemTemplate') i.fa.fa-fw.fa-file(ng-if="entity.type == 'doc'") i.fa.fa-fw.fa-image(ng-if="entity.type == 'file'") + {{ $parent.multiSelectedCount }} span( ng-hide="entity.renaming" ) {{ entity.name }} @@ -126,12 +126,11 @@ script(type='text/ng-template', id='entityListItemTemplate') on-enter="finishRenaming()" ) - span.dropdown( + span.dropdown.entity-menu-toggle( dropdown, - ng-show="entity.selected", ng-if="permissions.write" ) - a.dropdown-toggle(href, dropdown-toggle) + a.dropdown-toggle(href, dropdown-toggle, stop-propagation="click") i.fa.fa-chevron-down ul.dropdown-menu.dropdown-menu-right @@ -140,12 +139,14 @@ script(type='text/ng-template', id='entityListItemTemplate') href ng-click="startRenaming()" right-click="startRenaming()" + ng-show="!entity.multiSelected" ) #{translate("rename")} li a( href ng-click="openDeleteModal()" right-click="openDeleteModal()" + stop-propagation="click" ) #{translate("delete")} div.dropdown.context-menu( @@ -158,20 +159,23 @@ script(type='text/ng-template', id='entityListItemTemplate') href ng-click="startRenaming()" right-click="startRenaming()" + ng-show="!entity.multiSelected" ) #{translate("rename")} li a( href ng-click="openDeleteModal()" right-click="openDeleteModal()" + stop-propagation="click" ) #{translate("delete")} .entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController") .entity-name( - ng-click="select()" + ng-click="select($event)" ng-dblclick="permissions.write && startRenaming()" draggable="permissions.write" + draggable-helper="draggableHelper" droppable="permissions.write" accept=".entity-name" on-drop-callback="onDrop" @@ -194,7 +198,7 @@ script(type='text/ng-template', id='entityListItemTemplate') 'fa-folder': !expanded, \ 'fa-folder-open': expanded \ }" - ng-click="select()" + ng-click="select($event)" ) span( @@ -210,12 +214,11 @@ script(type='text/ng-template', id='entityListItemTemplate') on-enter="finishRenaming()" ) - span.dropdown( + span.dropdown.entity-menu-toggle( dropdown, ng-if="permissions.write" - ng-show="entity.selected" ) - a.dropdown-toggle(href, dropdown-toggle) + a.dropdown-toggle(href, dropdown-toggle, stop-propagation="click") i.fa.fa-chevron-down ul.dropdown-menu.dropdown-menu-right @@ -224,12 +227,14 @@ script(type='text/ng-template', id='entityListItemTemplate') href ng-click="startRenaming()" right-click="startRenaming()" + ng-show="!entity.multiSelected" ) #{translate("rename")} li a( href ng-click="openDeleteModal()" right-click="openDeleteModal()" + stop-propagation="click" ) #{translate("delete")} li.divider li @@ -261,12 +266,14 @@ script(type='text/ng-template', id='entityListItemTemplate') href ng-click="startRenaming()" right-click="startRenaming()" + ng-show="!entity.multiSelected" ) #{translate("rename")} li a( href ng-click="openDeleteModal()" right-click="openDeleteModal()" + stop-propagation="click" ) #{translate("delete")} li.divider li @@ -382,6 +389,8 @@ script(type='text/ng-template', id='deleteEntityModalTemplate') h3 #{translate("delete")} {{ entity.name }} .modal-body p !{translate("sure_you_want_to_delete")} + ul + li(ng-repeat="entity in entities") {{entity.name}} .modal-footer button.btn.btn-default( ng-disabled="state.inflight" diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 854646edfd..8a9786dd00 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -25,6 +25,7 @@ define [ "directives/onEnter" "directives/stopPropagation" "directives/rightClick" + "services/queued-http" "filters/formatDate" "main/event" "main/account-upgrade" diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index 9ea27ae62c..3792daf20d 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -19,6 +19,12 @@ define [ @recalculateDocList() @_bindToSocketEvents() + + @$scope.multiSelectedCount = 0 + + $(document).on "click", => + @clearMultiSelectedEntities() + $scope.$digest() _bindToSocketEvents: () -> @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) => @@ -78,6 +84,52 @@ define [ @ide.fileTreeManager.forEachEntity (entity) -> entity.selected = false entity.selected = true + + toggleMultiSelectEntity: (entity) -> + entity.multiSelected = !entity.multiSelected + @$scope.multiSelectedCount = @multiSelectedCount() + + multiSelectedCount: () -> + count = 0 + @forEachEntity (entity) -> + if entity.multiSelected + count++ + return count + + getMultiSelectedEntities: () -> + entities = [] + @forEachEntity (e) -> + if e.multiSelected + entities.push e + return entities + + getMultiSelectedEntityChildNodes: () -> + entities = @getMultiSelectedEntities() + paths = {} + for entity in entities + paths[@getEntityPath(entity)] = entity + prefixes = {} + for path, entity of paths + parts = path.split("/") + if parts.length <= 1 + continue + else + # Record prefixes a/b/c.tex -> 'a' and 'a/b' + for i in [1..(parts.length - 1)] + prefixes[parts.slice(0,i).join("/")] = true + child_entities = [] + for path, entity of paths + # If the path is in the prefixes, then it's a parent folder and + # should be ignore + if !prefixes[path]? + child_entities.push entity + return child_entities + + clearMultiSelectedEntities: () -> + return if @$scope.multiSelectedCount == 0 # Be efficient, this is called a lot on 'click' + @forEachEntity (entity) -> + entity.multiSelected = false + @$scope.multiSelectedCount = 0 findSelectedEntity: () -> selected = null @@ -277,7 +329,7 @@ define [ deleteEntity: (entity, callback = (error) ->) -> # We'll wait for the socket.io notification to # delete from scope. - return @ide.$http { + return @ide.queuedHttp { method: "DELETE" url: "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}" headers: @@ -289,7 +341,7 @@ define [ # since that would break the tree structure. return if @_isChildFolder(entity, parent_folder) @_moveEntityInScope(entity, parent_folder) - return @ide.$http.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", { + return @ide.queuedHttp.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", { folder_id: parent_folder.id _csrf: window.csrfToken } diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee index 9c46afc967..a1bb1da702 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee @@ -2,9 +2,19 @@ define [ "base" ], (App) -> App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) -> - $scope.select = () -> - ide.fileTreeManager.selectEntity($scope.entity) - $scope.$emit "entity:selected", $scope.entity + $scope.select = (e) -> + if e.ctrlKey or e.metaKey + e.stopPropagation() + ide.fileTreeManager.toggleMultiSelectEntity($scope.entity) + else + ide.fileTreeManager.selectEntity($scope.entity) + $scope.$emit "entity:selected", $scope.entity + + $scope.draggableHelper = () -> + if ide.fileTreeManager.multiSelectedCount() > 0 + return $("