فهرست منبع

Merge branch 'ja-multi-select'

James Allen 10 سال پیش
والد
کامیت
e4960e782f

+ 25 - 16
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"

+ 1 - 0
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"

+ 58 - 4
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) =>
@@ -65,6 +71,7 @@ define [
 				@$scope.$apply () =>
 					@_deleteEntityFromScope entity
 					@recalculateDocList()
+				@$scope.$emit "entity:deleted", entity
 
 			@ide.socket.on "reciveEntityMove", (entity_id, folder_id) =>
 				entity = @findEntityById(entity_id)
@@ -78,6 +85,55 @@ 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
+		
+		multiSelectSelectedEntity: () ->
+			@findSelectedEntity()?.multiSelected = true
 
 		findSelectedEntity: () ->
 			selected = null
@@ -277,7 +333,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 +345,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
 			}
@@ -316,8 +372,6 @@ define [
 				entity.deleted = true
 				@$scope.deletedDocs.push entity
 
-			@$scope.$emit "entity:deleted", entity
-
 		_moveEntityInScope: (entity, parent_folder) ->
 			return if entity in parent_folder.children
 			@_deleteEntityFromScope(entity, moveToDeleted: false)

+ 30 - 11
services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee

@@ -2,9 +2,23 @@ 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()
+				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 =
 			name: $scope.entity.name
@@ -24,10 +38,15 @@ define [
 			$scope.startRenaming() if $scope.entity.selected
 
 		$scope.openDeleteModal = () ->
+			if ide.fileTreeManager.multiSelectedCount() > 0
+				entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
+			else
+				entities = [$scope.entity]
 			$modal.open(
 				templateUrl: "deleteEntityModalTemplate"
 				controller:  "DeleteEntityModalController"
-				scope: $scope
+				resolve:
+					entities: () -> entities
 			)
 
 		$scope.$on "delete:selected", () ->
@@ -35,18 +54,18 @@ define [
 	]
 
 	App.controller "DeleteEntityModalController", [
-		"$scope", "ide", "$modalInstance",
-		($scope,   ide,   $modalInstance) ->
+		"$scope", "ide", "$modalInstance", "entities"
+		($scope,   ide,   $modalInstance, entities) ->
 			$scope.state =
 				inflight: false
+				
+			$scope.entities = entities
 
 			$scope.delete = () ->
 				$scope.state.inflight = true
-				ide.fileTreeManager
-					.deleteEntity($scope.entity)
-					.success () ->
-						$scope.state.inflight = false
-						$modalInstance.close()
+				for entity in $scope.entities
+					ide.fileTreeManager.deleteEntity(entity)
+				$modalInstance.close()
 
 			$scope.cancel = () ->
 				$modalInstance.dismiss('cancel')

+ 8 - 4
services/web/public/coffee/ide/file-tree/controllers/FileTreeFolderController.coffee

@@ -9,11 +9,15 @@ define [
 			localStorage("folder.#{$scope.entity.id}.expanded", $scope.expanded)
 
 		$scope.onDrop = (events, ui) ->
-			source = $(ui.draggable).scope().entity
-			return if !source?
-			# clear highlight explicitely
+			if ide.fileTreeManager.multiSelectedCount()
+				entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes() 
+			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')
-			ide.fileTreeManager.moveEntity(source, $scope.entity)
 
 		$scope.orderByFoldersFirst = (entity) ->
 			# We need this here as well as in FileTreeController

+ 9 - 3
services/web/public/coffee/ide/file-tree/controllers/FileTreeRootFolderController.coffee

@@ -4,7 +4,13 @@ define [
 	App.controller "FileTreeRootFolderController", ["$scope", "ide", ($scope, ide) ->
 		rootFolder = $scope.rootFolder
 		$scope.onDrop = (events, ui) ->
-			source = $(ui.draggable).scope().entity
-			return if !source?
-			ide.fileTreeManager.moveEntity(source, rootFolder)
+			if ide.fileTreeManager.multiSelectedCount()
+				entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes() 
+			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')
 	]

+ 1 - 1
services/web/public/coffee/ide/file-tree/directives/draggable.coffee

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

+ 1 - 0
services/web/public/coffee/ide/file-tree/directives/droppable.coffee

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

+ 2 - 1
services/web/public/coffee/ide/services/ide.coffee

@@ -3,9 +3,10 @@ define [
 ], (App) ->
 	# We create and provide this as service so that we can access the global ide
 	# 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.$http = $http
+		ide.queuedHttp = queuedHttp
 
 		@recentEvents = []
 		ide.pushEvent = (type, meta = {}) =>

+ 1 - 0
services/web/public/coffee/main.coffee

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

+ 1 - 1
services/web/public/coffee/main/project-list/index.coffee

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

+ 0 - 1
services/web/public/coffee/main/project-list/queued-http.coffee → services/web/public/coffee/services/queued-http.coffee

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

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 270 - 462
services/web/public/js/libs/jquery-layout.js


+ 32 - 10
services/web/public/stylesheets/app/editor/file-tree.less

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

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است