Merge branch 'ja-multi-select'

This commit is contained in:
James Allen 2016-02-12 11:56:10 +00:00
commit e4960e782f
14 changed files with 1125 additions and 1947 deletions

View file

@ -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") .toolbar.toolbar-small.toolbar-alt(ng-if="permissions.write")
a( a(
href, href,
@ -27,7 +27,8 @@ aside#file-tree(ng-controller="FileTreeController").full-size
href, href,
ng-click="startRenamingSelected()", ng-click="startRenamingSelected()",
tooltip="#{translate('rename')}", tooltip="#{translate('rename')}",
tooltip-placement="bottom" tooltip-placement="bottom",
ng-show="multiSelectedCount == 0"
) )
i.fa.fa-pencil i.fa.fa-pencil
a( a(
@ -81,27 +82,25 @@ aside#file-tree(ng-controller="FileTreeController").full-size
) )
.entity .entity
.entity-name( .entity-name(
ng-click="select()" ng-click="select($event)"
) )
//- Just a spacer to align with folders //- Just a spacer to align with folders
i.fa.fa-fw.toggle i.fa.fa-fw.toggle
i.fa.fa-fw.fa-file i.fa.fa-fw.fa-file
span {{ entity.name }} span {{ entity.name }}
script(type='text/ng-template', id='entityListItemTemplate') script(type='text/ng-template', id='entityListItemTemplate')
li( li(
ng-class="{ 'selected': entity.selected }", ng-class="{ 'selected': entity.selected, 'multi-selected': entity.multiSelected }",
ng-controller="FileTreeEntityController" ng-controller="FileTreeEntityController"
) )
.entity(ng-if="entity.type != 'folder'") .entity(ng-if="entity.type != 'folder'")
.entity-name( .entity-name(
ng-click="select()" ng-click="select($event)"
ng-dblclick="permissions.write && startRenaming()" ng-dblclick="permissions.write && startRenaming()"
draggable="permissions.write" draggable="permissions.write"
draggable-helper="draggableHelper"
context-menu context-menu
data-target="context-menu-{{ entity.id }}" data-target="context-menu-{{ entity.id }}"
context-menu-container="body" 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-file(ng-if="entity.type == 'doc'")
i.fa.fa-fw.fa-image(ng-if="entity.type == 'file'") i.fa.fa-fw.fa-image(ng-if="entity.type == 'file'")
{{ $parent.multiSelectedCount }}
span( span(
ng-hide="entity.renaming" ng-hide="entity.renaming"
) {{ entity.name }} ) {{ entity.name }}
@ -126,12 +126,11 @@ script(type='text/ng-template', id='entityListItemTemplate')
on-enter="finishRenaming()" on-enter="finishRenaming()"
) )
span.dropdown( span.dropdown.entity-menu-toggle(
dropdown, dropdown,
ng-show="entity.selected",
ng-if="permissions.write" ng-if="permissions.write"
) )
a.dropdown-toggle(href, dropdown-toggle) a.dropdown-toggle(href, dropdown-toggle, stop-propagation="click")
i.fa.fa-chevron-down i.fa.fa-chevron-down
ul.dropdown-menu.dropdown-menu-right ul.dropdown-menu.dropdown-menu-right
@ -140,12 +139,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href href
ng-click="startRenaming()" ng-click="startRenaming()"
right-click="startRenaming()" right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href href
ng-click="openDeleteModal()" ng-click="openDeleteModal()"
right-click="openDeleteModal()" right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")} ) #{translate("delete")}
div.dropdown.context-menu( div.dropdown.context-menu(
@ -158,20 +159,23 @@ script(type='text/ng-template', id='entityListItemTemplate')
href href
ng-click="startRenaming()" ng-click="startRenaming()"
right-click="startRenaming()" right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href href
ng-click="openDeleteModal()" ng-click="openDeleteModal()"
right-click="openDeleteModal()" right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")} ) #{translate("delete")}
.entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController") .entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController")
.entity-name( .entity-name(
ng-click="select()" ng-click="select($event)"
ng-dblclick="permissions.write && startRenaming()" ng-dblclick="permissions.write && startRenaming()"
draggable="permissions.write" draggable="permissions.write"
draggable-helper="draggableHelper"
droppable="permissions.write" droppable="permissions.write"
accept=".entity-name" accept=".entity-name"
on-drop-callback="onDrop" on-drop-callback="onDrop"
@ -194,7 +198,7 @@ script(type='text/ng-template', id='entityListItemTemplate')
'fa-folder': !expanded, \ 'fa-folder': !expanded, \
'fa-folder-open': expanded \ 'fa-folder-open': expanded \
}" }"
ng-click="select()" ng-click="select($event)"
) )
span( span(
@ -210,12 +214,11 @@ script(type='text/ng-template', id='entityListItemTemplate')
on-enter="finishRenaming()" on-enter="finishRenaming()"
) )
span.dropdown( span.dropdown.entity-menu-toggle(
dropdown, dropdown,
ng-if="permissions.write" 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 i.fa.fa-chevron-down
ul.dropdown-menu.dropdown-menu-right ul.dropdown-menu.dropdown-menu-right
@ -224,12 +227,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href href
ng-click="startRenaming()" ng-click="startRenaming()"
right-click="startRenaming()" right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href href
ng-click="openDeleteModal()" ng-click="openDeleteModal()"
right-click="openDeleteModal()" right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")} ) #{translate("delete")}
li.divider li.divider
li li
@ -261,12 +266,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href href
ng-click="startRenaming()" ng-click="startRenaming()"
right-click="startRenaming()" right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href href
ng-click="openDeleteModal()" ng-click="openDeleteModal()"
right-click="openDeleteModal()" right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")} ) #{translate("delete")}
li.divider li.divider
li li
@ -382,6 +389,8 @@ script(type='text/ng-template', id='deleteEntityModalTemplate')
h3 #{translate("delete")} {{ entity.name }} h3 #{translate("delete")} {{ entity.name }}
.modal-body .modal-body
p !{translate("sure_you_want_to_delete")} p !{translate("sure_you_want_to_delete")}
ul
li(ng-repeat="entity in entities") {{entity.name}}
.modal-footer .modal-footer
button.btn.btn-default( button.btn.btn-default(
ng-disabled="state.inflight" ng-disabled="state.inflight"

View file

@ -25,6 +25,7 @@ define [
"directives/onEnter" "directives/onEnter"
"directives/stopPropagation" "directives/stopPropagation"
"directives/rightClick" "directives/rightClick"
"services/queued-http"
"filters/formatDate" "filters/formatDate"
"main/event" "main/event"
"main/account-upgrade" "main/account-upgrade"

View file

@ -19,6 +19,12 @@ define [
@recalculateDocList() @recalculateDocList()
@_bindToSocketEvents() @_bindToSocketEvents()
@$scope.multiSelectedCount = 0
$(document).on "click", =>
@clearMultiSelectedEntities()
$scope.$digest()
_bindToSocketEvents: () -> _bindToSocketEvents: () ->
@ide.socket.on "reciveNewDoc", (parent_folder_id, doc) => @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) =>
@ -65,6 +71,7 @@ define [
@$scope.$apply () => @$scope.$apply () =>
@_deleteEntityFromScope entity @_deleteEntityFromScope entity
@recalculateDocList() @recalculateDocList()
@$scope.$emit "entity:deleted", entity
@ide.socket.on "reciveEntityMove", (entity_id, folder_id) => @ide.socket.on "reciveEntityMove", (entity_id, folder_id) =>
entity = @findEntityById(entity_id) entity = @findEntityById(entity_id)
@ -78,6 +85,55 @@ define [
@ide.fileTreeManager.forEachEntity (entity) -> @ide.fileTreeManager.forEachEntity (entity) ->
entity.selected = false entity.selected = false
entity.selected = true 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
multiSelectSelectedEntity: () ->
@findSelectedEntity()?.multiSelected = true
findSelectedEntity: () -> findSelectedEntity: () ->
selected = null selected = null
@ -277,7 +333,7 @@ define [
deleteEntity: (entity, callback = (error) ->) -> deleteEntity: (entity, callback = (error) ->) ->
# We'll wait for the socket.io notification to # We'll wait for the socket.io notification to
# delete from scope. # delete from scope.
return @ide.$http { return @ide.queuedHttp {
method: "DELETE" method: "DELETE"
url: "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}" url: "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}"
headers: headers:
@ -289,7 +345,7 @@ define [
# since that would break the tree structure. # since that would break the tree structure.
return if @_isChildFolder(entity, parent_folder) return if @_isChildFolder(entity, parent_folder)
@_moveEntityInScope(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 folder_id: parent_folder.id
_csrf: window.csrfToken _csrf: window.csrfToken
} }
@ -316,8 +372,6 @@ define [
entity.deleted = true entity.deleted = true
@$scope.deletedDocs.push entity @$scope.deletedDocs.push entity
@$scope.$emit "entity:deleted", entity
_moveEntityInScope: (entity, parent_folder) -> _moveEntityInScope: (entity, parent_folder) ->
return if entity in parent_folder.children return if entity in parent_folder.children
@_deleteEntityFromScope(entity, moveToDeleted: false) @_deleteEntityFromScope(entity, moveToDeleted: false)

View file

@ -2,9 +2,23 @@ define [
"base" "base"
], (App) -> ], (App) ->
App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) -> App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
$scope.select = () -> $scope.select = (e) ->
ide.fileTreeManager.selectEntity($scope.entity) if e.ctrlKey or e.metaKey
$scope.$emit "entity:selected", $scope.entity e.stopPropagation()
initialMultiSelectCount = ide.fileTreeManager.multiSelectedCount()
ide.fileTreeManager.toggleMultiSelectEntity($scope.entity) == 0
if initialMultiSelectCount == 0
# On first multi selection, also include the current active/open file.
ide.fileTreeManager.multiSelectSelectedEntity()
else
ide.fileTreeManager.selectEntity($scope.entity)
$scope.$emit "entity:selected", $scope.entity
$scope.draggableHelper = () ->
if ide.fileTreeManager.multiSelectedCount() > 0
return $("<strong style='z-index:100'>#{ide.fileTreeManager.multiSelectedCount()} Files</strong>")
else
return $("<strong style='z-index:100'>#{$scope.entity.name}</strong>")
$scope.inputs = $scope.inputs =
name: $scope.entity.name name: $scope.entity.name
@ -24,10 +38,15 @@ define [
$scope.startRenaming() if $scope.entity.selected $scope.startRenaming() if $scope.entity.selected
$scope.openDeleteModal = () -> $scope.openDeleteModal = () ->
if ide.fileTreeManager.multiSelectedCount() > 0
entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
else
entities = [$scope.entity]
$modal.open( $modal.open(
templateUrl: "deleteEntityModalTemplate" templateUrl: "deleteEntityModalTemplate"
controller: "DeleteEntityModalController" controller: "DeleteEntityModalController"
scope: $scope resolve:
entities: () -> entities
) )
$scope.$on "delete:selected", () -> $scope.$on "delete:selected", () ->
@ -35,18 +54,18 @@ define [
] ]
App.controller "DeleteEntityModalController", [ App.controller "DeleteEntityModalController", [
"$scope", "ide", "$modalInstance", "$scope", "ide", "$modalInstance", "entities"
($scope, ide, $modalInstance) -> ($scope, ide, $modalInstance, entities) ->
$scope.state = $scope.state =
inflight: false inflight: false
$scope.entities = entities
$scope.delete = () -> $scope.delete = () ->
$scope.state.inflight = true $scope.state.inflight = true
ide.fileTreeManager for entity in $scope.entities
.deleteEntity($scope.entity) ide.fileTreeManager.deleteEntity(entity)
.success () -> $modalInstance.close()
$scope.state.inflight = false
$modalInstance.close()
$scope.cancel = () -> $scope.cancel = () ->
$modalInstance.dismiss('cancel') $modalInstance.dismiss('cancel')

View file

@ -9,11 +9,15 @@ define [
localStorage("folder.#{$scope.entity.id}.expanded", $scope.expanded) localStorage("folder.#{$scope.entity.id}.expanded", $scope.expanded)
$scope.onDrop = (events, ui) -> $scope.onDrop = (events, ui) ->
source = $(ui.draggable).scope().entity if ide.fileTreeManager.multiSelectedCount()
return if !source? entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
# clear highlight explicitely else
entities = [$(ui.draggable).scope().entity]
for dropped_entity in entities
ide.fileTreeManager.moveEntity(dropped_entity, $scope.entity)
$scope.$digest()
# clear highlight explicitly
$('.file-tree-inner .droppable-hover').removeClass('droppable-hover') $('.file-tree-inner .droppable-hover').removeClass('droppable-hover')
ide.fileTreeManager.moveEntity(source, $scope.entity)
$scope.orderByFoldersFirst = (entity) -> $scope.orderByFoldersFirst = (entity) ->
# We need this here as well as in FileTreeController # We need this here as well as in FileTreeController

View file

@ -4,7 +4,13 @@ define [
App.controller "FileTreeRootFolderController", ["$scope", "ide", ($scope, ide) -> App.controller "FileTreeRootFolderController", ["$scope", "ide", ($scope, ide) ->
rootFolder = $scope.rootFolder rootFolder = $scope.rootFolder
$scope.onDrop = (events, ui) -> $scope.onDrop = (events, ui) ->
source = $(ui.draggable).scope().entity if ide.fileTreeManager.multiSelectedCount()
return if !source? entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
ide.fileTreeManager.moveEntity(source, rootFolder) else
entities = [$(ui.draggable).scope().entity]
for dropped_entity in entities
ide.fileTreeManager.moveEntity(dropped_entity, rootFolder)
$scope.$digest()
# clear highlight explicitly
$('.file-tree-inner .droppable-hover').removeClass('droppable-hover')
] ]

View file

@ -9,6 +9,6 @@ define [
element.draggable element.draggable
delay: 250 delay: 250
opacity: 0.7 opacity: 0.7
helper: "clone"
scroll: true scroll: true
helper: scope.$eval(attrs.draggableHelper)
} }

View file

@ -9,6 +9,7 @@ define [
element.droppable element.droppable
greedy: true greedy: true
hoverClass: "droppable-hover" hoverClass: "droppable-hover"
tolerance: "pointer"
accept: attrs.accept accept: attrs.accept
drop: scope.$eval(attrs.onDropCallback) drop: scope.$eval(attrs.onDropCallback)
} }

View file

@ -3,9 +3,10 @@ define [
], (App) -> ], (App) ->
# We create and provide this as service so that we can access the global ide # We create and provide this as service so that we can access the global ide
# from within other parts of the angular app. # from within other parts of the angular app.
App.factory "ide", ["$http", "$modal", ($http, $modal) -> App.factory "ide", ["$http", "queuedHttp", "$modal", ($http, queuedHttp, $modal) ->
ide = {} ide = {}
ide.$http = $http ide.$http = $http
ide.queuedHttp = queuedHttp
@recentEvents = [] @recentEvents = []
ide.pushEvent = (type, meta = {}) => ide.pushEvent = (type, meta = {}) =>

View file

@ -25,6 +25,7 @@ define [
"directives/onEnter" "directives/onEnter"
"directives/selectAll" "directives/selectAll"
"directives/maxHeight" "directives/maxHeight"
"services/queued-http"
"filters/formatDate" "filters/formatDate"
"__MAIN_CLIENTSIDE_INCLUDES__" "__MAIN_CLIENTSIDE_INCLUDES__"
], () -> ], () ->

View file

@ -3,6 +3,6 @@ define [
"main/project-list/modal-controllers" "main/project-list/modal-controllers"
"main/project-list/tag-controllers" "main/project-list/tag-controllers"
"main/project-list/notifications-controller" "main/project-list/notifications-controller"
"main/project-list/queued-http"
"main/project-list/left-hand-menu-promo-controller" "main/project-list/left-hand-menu-promo-controller"
"services/queued-http"
], () -> ], () ->

View file

@ -1,7 +1,6 @@
define [ define [
"base" "base"
], (App) -> ], (App) ->
App.factory "queuedHttp", ($http, $q) -> App.factory "queuedHttp", ($http, $q) ->
pendingRequests = [] pendingRequests = []
inflight = false inflight = false

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,8 @@ aside#file-tree {
font-size: 0.8rem; font-size: 0.8rem;
margin: 0; margin: 0;
padding: (@line-height-computed / 4) 0; padding: (@line-height-computed / 4) 0;
overflow-x: hidden;
height: 100%;
ul { ul {
margin-left: (@line-height-computed / 2); margin-left: (@line-height-computed / 2);
@ -46,7 +48,7 @@ aside#file-tree {
line-height: 1.6; line-height: 1.6;
} }
&.droppable-hover { &.droppable-hover {
background-color: @file-tree-droppable-background-color; background-color: fade(@file-tree-droppable-background-color, 60%);
} }
} }
@ -66,16 +68,13 @@ aside#file-tree {
font-size: 0.7rem; font-size: 0.7rem;
color: @gray color: @gray
} }
&.selected { &.multi-selected {
> .entity > .entity-name { > .entity > .entity-name {
color: @link-color; background-color: lighten(@brand-info, 40%);
border-right: 4px solid @link-color; &:hover {
font-weight: bold; background-color: lighten(@brand-info, 30%);
i.fa-folder-open, i.fa-folder, i.fa-file, i.fa-image, i.fa-file-pdf-o {
color: @link-color;
} }
padding-right: 32px;
} }
} }
@ -97,11 +96,34 @@ aside#file-tree {
width: 100%; width: 100%;
} }
} }
> .entity > .entity-name {
.entity-menu-toggle {
display: none;
}
}
}
}
&:not(.multi-selected) {
ul.file-tree-list li.selected {
> .entity > .entity-name {
color: @link-color;
border-right: 4px solid @link-color;
font-weight: bold;
padding-right: 32px;
i.fa-folder-open, i.fa-folder, i.fa-file, i.fa-image, i.fa-file-pdf-o {
color: @link-color;
}
.entity-menu-toggle {
display: inline;
}
}
} }
} }
ul.droppable-hover { ul.droppable-hover {
background-color: @file-tree-droppable-background-color; background-color: fade(@file-tree-droppable-background-color, 60%);
} }
} }