Add bulk action drag UI

This commit is contained in:
Felix Niklas 2017-01-06 18:05:08 +01:00
parent dda715be1b
commit 3b8ce3bd6d
5 changed files with 656 additions and 12 deletions

View file

@ -1,13 +1,372 @@
class App.TicketOverview extends App.Controller
className: 'overviews'
activeFocus: 'nav'
mouse:
x: null
y: null
elements:
'.js-batch-overlay': 'batchOverlay'
'.js-batch-overlay-backdrop': 'batchOverlayBackdrop'
'.js-batch-cancel': 'batchCancel'
'.js-batch-macro-circle': 'batchMacroCircle'
'.js-batch-assign-circle': 'batchAssignCircle'
'.js-batch-assign': 'batchAssign'
'.js-batch-macro': 'batchMacro'
events:
'mousedown .item': 'startDragItem'
'mouseenter .js-batch-overlay-entry': 'highlightBatchEntry'
'mouseleave .js-batch-overlay-entry': 'unhighlightBatchEntry'
constructor: ->
super
@render()
startDragItem: (event) ->
@grabbedItem = $(event.currentTarget)
offset = @grabbedItem.offset()
@batchDragger = $(App.view('ticket_overview/batch_dragger')())
@grabbedItemClone = @grabbedItem.clone()
@grabbedItemClone.data('offset', @grabbedItem.offset())
@grabbedItemClone.addClass('batch-dragger-item js-main-item')
@batchDragger.append @grabbedItemClone
@batchDragger.data
startX: event.pageX
startY: event.pageY
dx: Math.min(event.pageX - offset.left, 180)
dy: event.pageY - offset.top
moved: false
$(document).on 'mousemove.item', @dragItem
$(document).one 'mouseup.item', @endDragItem
# TODO: fire @cancelDrag on ESC
dragItem: (event) =>
event.preventDefault()
pos = @batchDragger.data()
threshold = 3
x = event.pageX - pos.dx
y = event.pageY - pos.dy
dir = if event.pageY > pos.startY then 1 else -1
if !pos.moved
if Math.abs(event.pageX - pos.startX) > threshold or Math.abs(event.pageY - pos.startY) > threshold
@batchDragger.data 'moved', true
# check grabbed items batch checkbox to make sure its checked
# (could be grabbed without checking the checkbox it)
@grabbedItemWasntChecked = !@grabbedItem.find('[name="bulk"]').prop('checked')
@grabbedItem.find('[name="bulk"]').prop('checked', true)
@grabbedItemClone.find('[name="bulk"]').prop('checked', true)
additionalItems = @el.find('[name="bulk"]:checked').parents('.item').not(@grabbedItem)
additionalItemsClones = additionalItems.clone()
@draggedItems = @grabbedItemClone.add(additionalItemsClones)
# store offsets for later use
additionalItemsClones.each (i, item) -> $(@).data('offset', additionalItems.eq(i).offset())
@batchDragger.prepend additionalItemsClones.addClass('batch-dragger-item').get().reverse()
if(additionalItemsClones.length)
@batchDragger.find('.js-batch-dragger-count').text(@draggedItems.length)
$('#app').append @batchDragger
@draggedItems.each (i, item) =>
dx = $(item).data('offset').left - $(item).offset().left - x
dy = $(item).data('offset').top - $(item).offset().top - y
$.Velocity.hook item, 'translateX', "#{dx}px"
$.Velocity.hook item, 'translateY', "#{dy}px"
@moveDraggedItems(-dir)
@mouseY = event.pageY
@showBatchOverlay()
else
return
$.Velocity.hook @batchDragger, 'translateX', "#{x}px"
$.Velocity.hook @batchDragger, 'translateY', "#{y}px"
endDragItem: (event) =>
$(document).off 'mousemove.item'
$(document).off 'mouseup.item'
pos = @batchDragger.data()
if !@hoveredBatchEntry
@cleanUpDrag()
return
$.Velocity.hook @batchDragger, 'transformOriginX', "#{pos.dx}px"
$.Velocity.hook @batchDragger, 'transformOriginY', "#{pos.dy}px"
@hoveredBatchEntry.velocity
properties:
scale: 1.1
options:
duration: 200
complete: =>
@hoveredBatchEntry.velocity "reverse",
duration: 200
complete: =>
# clean scale
@hoveredBatchEntry.removeAttr('style')
@cleanUpDrag(true)
@performBatchAction @hoveredBatchEntry.attr('data-action')
@hoveredBatchEntry = null
@batchDragger.velocity
properties:
scale: 0
options:
duration: 200
cancelDrag: ->
$(document).off 'mousemove.item'
$(document).off 'mouseup.item'
@cleanUpDrag()
cleanUpDrag: (success) ->
@hideBatchOverlay()
$('.batch-dragger').remove()
if @grabbedItemWasntChecked
@grabbedItem.find('[name="bulk"]').prop('checked', false)
if success
# uncheck all checked items
@el.find('[name="bulk"]:checked').prop('checked', false)
@el.find('[name="bulk_all"]').prop('checked', false)
moveDraggedItems: (dir) ->
@draggedItems.velocity
properties:
translateX: 0
translateY: (i) => dir * i * @batchDragger.height()/2
options:
easing: 'ease-in-out'
duration: 300
@batchDragger.find('.js-batch-dragger-count').velocity
properties:
translateY: if dir < 0 then 0 else -@batchDragger.height()+8
options:
easing: 'ease-in-out'
duration: 300
performBatchAction: (action) ->
console.log "perform action #{action} on checked items"
showBatchOverlay: ->
@batchOverlay.show()
@batchOverlayBackdrop.velocity { opacity: [1, 0] }, { duration: 500 }
@batchOverlayShown = true
$(document).on 'mousemove.batchoverlay', @controlBatchOverlay
hideBatchOverlay: ->
$(document).off 'mousemove.batchoverlay'
@batchOverlayShown = false
@batchOverlayBackdrop.velocity { opacity: [0, 1] }, { duration: 300, queue: false }
@hideBatchCircles =>
@batchOverlay.hide()
if @batchAssignShown
@hideBatchAssign()
if @batchMacroShown
@hideBatchMacro()
controlBatchOverlay: (event) =>
# store to detect if the mouse is hovering a drag-action entry
# after an animation ended -> @highlightBatchEntryAtMousePosition
@mouse.x = event.pageX
@mouse.y = event.pageY
if event.pageY <= window.innerHeight/5*2
mouseInArea = 'top'
else if event.pageY > window.innerHeight/5*2 && event.pageY <= window.innerHeight/5*3
mouseInArea = 'middle'
else
mouseInArea = 'bottom'
switch mouseInArea
when 'top'
if !@batchMacroShown
@hideBatchCircles()
@showBatchMacro()
@moveDraggedItems(1)
when 'middle'
if @batchAssignShown
@hideBatchAssign()
if @batchMacroShown
@hideBatchMacro()
if !@batchCirclesShown
@showBatchCircles()
when 'bottom'
if !@batchAssignShown
@hideBatchCircles()
@showBatchAssign()
@moveDraggedItems(-1)
showBatchCircles: ->
@batchCirclesShown = true
@batchMacroCircle.velocity
properties:
translateY: [0, '-150%']
opacity: [1, 0]
options:
easing: [1,-.55,.2,1.37]
duration: 500
visibility: 'visible'
delay: 200
@batchAssignCircle.velocity
properties:
translateY: [0, '150%']
opacity: [1, 0]
options:
easing: [1,-.55,.2,1.37]
duration: 500
visibility: 'visible'
delay: 200
hideBatchCircles: (callback) ->
@batchMacroCircle.velocity
properties:
translateY: ['-150%', 0]
opacity: [0, 1]
options:
duration: 300
visibility: 'hidden'
queue: false
@batchAssignCircle.velocity
properties:
translateY: ['150%', 0]
opacity: [0, 1]
options:
duration: 300
complete: callback
visibility: 'hidden'
queue: false
@batchCirclesShown = false
showBatchAssign: ->
return if !@batchOverlayShown # user might have dropped the item already
@batchAssignShown = true
@batchAssign.velocity
properties:
translateY: [0, '100%']
opacity: [1, 0]
options:
easing: [1,-.55,.2,1.37]
duration: 500
visibility: 'visible'
complete: @highlightBatchEntryAtMousePosition
delay: if @batchCirclesShown then 0 else 200
@batchCancel.css
top: 0
bottom: 'auto'
@batchCancel.velocity
properties:
translateY: [0, '100%']
opacity: [1, 0]
options:
easing: [1,-.55,.2,1.37]
duration: 500
visibility: 'visible'
delay: if @batchCirclesShown then 0 else 200
hideBatchAssign: ->
@batchAssign.velocity
properties:
translateY: ['100%', 0]
opacity: [0, 1]
options:
duration: 300
visibility: 'hidden'
queue: false
@batchCancel.velocity
properties:
translateY: ['100%', 0]
opacity: [0, 1]
options:
duration: 300
visibility: 'hidden'
queue: false
@batchAssignShown = false
showBatchMacro: ->
return if !@batchOverlayShown # user might have dropped the item already
@batchMacroShown = true
@batchMacro.velocity
properties:
translateY: [0, '-100%']
opacity: [1, 0]
options:
easing: [1,-.55,.2,1.37]
duration: 500
visibility: 'visible'
complete: @highlightBatchEntryAtMousePosition
delay: if @batchCirclesShown then 0 else 200
@batchCancel.css
top: 'auto'
bottom: 0
@batchCancel.velocity
properties:
translateY: [0, '-100%']
opacity: [1, 0]
options:
easing: [1,-.55,.2,1.37]
duration: 500
visibility: 'visible'
delay: if @batchCirclesShown then 0 else 200
hideBatchMacro: ->
@batchMacro.velocity
properties:
translateY: ['-100%', 0]
opacity: [0, 1]
options:
duration: 300
visibility: 'hidden'
queue: false
@batchCancel.velocity
properties:
translateY: ['-100%', 0]
opacity: [0, 1]
options:
duration: 300
visibility: 'hidden'
queue: false
@batchMacroShown = false
highlightBatchEntryAtMousePosition: =>
entryAtPoint = $(document.elementFromPoint(@mouse.x, @mouse.y)).closest('.js-batch-overlay-entry')
if(entryAtPoint.length)
@hoveredBatchEntry = entryAtPoint.addClass('is-hovered')
highlightBatchEntry: (event) ->
@hoveredBatchEntry = $(event.currentTarget).addClass('is-hovered')
unhighlightBatchEntry: (event) ->
@hoveredBatchEntry = null
$(event.currentTarget).removeClass('is-hovered')
render: ->
elLocal = $(App.view('ticket_overview')())
elLocal = $(App.view('ticket_overview/index')())
@navBarControllerVertical = new Navbar
el: elLocal.find('.overview-header')
@ -397,10 +756,10 @@ class Table extends App.Controller
)
table = $(table)
table.delegate('[name="bulk_all"]', 'click', (e) ->
if $(e.target).attr('checked')
$(e.target).closest('table').find('[name="bulk"]').attr('checked', true)
if $(e.currentTarget).prop('checked')
$(e.currentTarget).closest('table').find('[name="bulk"]').prop('checked', true)
else
$(e.target).closest('table').find('[name="bulk"]').attr('checked', false)
$(e.currentTarget).closest('table').find('[name="bulk"]').prop('checked', false)
)
@$('.table-overview').append(table)
else
@ -440,6 +799,25 @@ class Table extends App.Controller
@bulkForm.hide()
else
@bulkForm.show()
if @lastChecked && e.shiftKey
# check items in a row
currentItem = $(e.currentTarget).parents('.item')
lastCheckedItem = @lastChecked.parents('.item')
items = currentItem.parent().children()
if currentItem.index() > lastCheckedItem.index()
# current item is below last checked item
startId = lastCheckedItem.index()
endId = currentItem.index()
else
# current item is above last checked item
startId = currentItem.index()
endId = lastCheckedItem.index()
items.slice(startId+1, endId).find('[name="bulk"]').prop('checked', (-> !@checked))
@lastChecked = $(e.currentTarget)
callbackIconHeader = (headers) ->
attribute =
name: 'icon'
@ -523,8 +901,8 @@ class Table extends App.Controller
# deselect bulk_all if one item is uncheck observ
@$('.table-overview').delegate('[name="bulk"]', 'click', (e) ->
if !$(e.target).attr('checked')
$(e.target).parents().find('[name="bulk_all"]').attr('checked', false)
if !$(e.target).prop('checked')
$(e.target).parents().find('[name="bulk_all"]').prop('checked', false)
)
getSelected: ->
@ -540,7 +918,7 @@ class Table extends App.Controller
ticketId = $(element).val()
for ticketIdSelected in ticketIDs
if ticketIdSelected is ticketId
$(element).attr('checked', true)
$(element).prop('checked', true)
)
viewmode: (e) =>

View file

@ -1,5 +0,0 @@
<div class="sidebar"></div>
<div class="main flex">
<div class="overview-header"></div>
<div class="overview-table"></div>
</div>

View file

@ -0,0 +1,3 @@
<div class="batch-dragger zIndex-10">
<div class="batch-dragger-counter js-batch-dragger-count"></div>
</div>

View file

@ -0,0 +1,50 @@
<div class="sidebar"></div>
<div class="main flex">
<div class="overview-header"></div>
<div class="overview-table"></div>
</div>
<div class="batch-overlay js-batch-overlay">
<div class="batch-overlay-backdrop js-batch-overlay-backdrop"></div>
<div class="batch-overlay-cancel js-batch-cancel">
<%- @T('drag here to cancel') %>
</div>
<div class="batch-overlay-circle batch-overlay-circle--top js-batch-macro-circle">
<div class="batch-overlay-circle-label"><%- @T('run macro') %></div>
<%- @Icon('arrow-up') %>
</div>
<div class="batch-overlay-circle batch-overlay-circle--bottom js-batch-assign-circle">
<%- @Icon('arrow-down') %>
<div class="batch-overlay-circle-label"><%- @T('assign tickets') %></div>
</div>
<div class="batch-overlay-assign batch-overlay-box js-batch-assign">
<div class="batch-overlay-assign-entry js-batch-overlay-entry" data-action="assign">
<span class="avatar size-80 avatar--unique" style="background-position: -100px -150px;">HH</span>
<div class="batch-overlay-assign-entry-name">Hans Huber</div>
</div>
<div class="batch-overlay-assign-entry js-batch-overlay-entry" data-action="assign">
<span class="avatar avatar--organization size-80 avatar--unique" style="background-position: -200px -60px;">
<%- @Icon('organization') %>
</span>
<div class="batch-overlay-assign-entry-name">Zammad</div>
<div class="batch-overlay-assign-entry-detail">3 Personen</div>
</div>
<div class="batch-overlay-assign-entry js-batch-overlay-entry" data-action="assign">
<span class="avatar size-80 avatar--unique" style="background-position: -30px -10px;">FD</span>
<div class="batch-overlay-assign-entry-name">Felicity Dickens</div>
</div>
</div>
<div class="batch-overlay-macro batch-overlay-box js-batch-macro">
<div class="batch-overlay-macro-entry js-batch-overlay-entry" data-action="macro">
<div class="batch-overlay-macro-entry-name">Close</div>
</div>
<div class="batch-overlay-macro-entry js-batch-overlay-entry" data-action="macro">
<div class="batch-overlay-macro-entry-name">Close &amp; Tag as Spam</div>
</div>
<div class="batch-overlay-macro-entry js-batch-overlay-entry" data-action="macro">
<div class="batch-overlay-macro-entry-name">Close &amp; Reply we're on Holidays</div>
</div>
<div class="batch-overlay-macro-entry js-batch-overlay-entry" data-action="macro">
<div class="batch-overlay-macro-entry-name">Escalate to 2nd level</div>
</div>
</div>
</div>

View file

@ -3580,6 +3580,23 @@ footer {
}
}
&--organization {
display: flex;
align-items: center;
justify-content: center;
.icon-organization {
fill: currentColor;
}
&.size-80 {
.icon-organization {
width: 32px;
height: 32px;
}
}
}
.icon-logo {
width: 100%;
height: 100%;
@ -8321,6 +8338,207 @@ output {
margin: 20px 0 32px;
}
.batch-overlay {
@extend .fit;
z-index: 1;
color: white;
text-transform: uppercase;
text-align: center;
letter-spacing: 0.07em;
font-size: 0.95em;
line-height: 1.3;
display: none;
will-change: display;
cursor: grabbing;
overflow: hidden;
&-backdrop {
@extend .fit;
background: hsla(231,20%,8%,.8);
opacity: 0;
will-change: opacity;
}
&-circle {
margin: 35px auto;
background: hsl(207,7%,29%);
border-radius: 100%;
border: 4px solid white;
width: 140px;
height: 140px;
padding: 20px 0;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
position: absolute;
left: 0;
right: 0;
will-change: transform, opacity;
visibility: hidden;
&--top {
top: 0;
}
&--bottom {
bottom: 0;
}
.icon {
fill: currentColor;
opacity: 1;
}
&-label {
width: 50%;
margin: 10px 0;
}
}
&-cancel {
background: hsla(0,0%,100%,.21);
background-clip: padding-box;
border: 2px dashed hsla(0,0%,100%,.3);
border-radius: 8px;
padding: 28px;
margin: 50px 200px;
position: absolute;
top: 0;
left: 0;
right: 0;
visibility: hidden;
will-change: opacity;
}
&-box {
background: hsl(232,9%,17%);
display: flex;
flex-wrap: wrap;
width: 100%;
padding: 37px 25px;
position: absolute;
visibility: hidden;
will-change: opacity, transition;
}
&-assign {
padding-bottom: 87px; // 37px + 50px
bottom: -50px; // extra space for bounce animation
&-entry {
padding: 13px;
&.is-hovered {
.avatar {
border-color: $highlight-color;
transform: scale(1.05);
}
}
.avatar {
border: 4px solid hsl(231,5%,30%);
margin-bottom: 10px;
box-sizing: content-box;
transition: transform 120ms;
}
&-detail {
color: gray;
}
}
}
&-macro {
padding-top: 87px; // 37px + 50px
top: -50px; // extra space for bounce animation
&-entry {
margin: 13px;
border: 4px solid hsl(231,5%,30%);
background: hsl(233,9%,24%);
border-radius: 100%;
height: 120px;
width: 120px;
padding: 13px 13px 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9em;
&.is-hovered {
border-color: $highlight-color;
transform: scale(1.05);
}
}
}
}
.batch-dragger {
position: absolute;
left: 0;
top: 0;
pointer-events: none;
width: 250px;
height: 40px;
will-change: transform;
&-item {
position: absolute;
left: 0;
width: 100%;
background: hsl(200,100%,91%);
border-radius: 4px;
display: flex;
align-items: center;
padding: 11px 0 9px 11px;
box-shadow: 0 0 10px hsla(0,0%,0%,.28);
will-change: transform;
a {
color: inherit;
}
td {
display: block;
padding: 0 12px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
flex-shrink: 0;
&:nth-child(3) {
flex-shrink: 1;
}
&:nth-child(n+4) {
display: none;
}
}
}
&-counter {
position: absolute;
right: -8px;
bottom: -8px;
width: 25px;
height: 25px;
border-radius: 99px;
z-index: 1;
color: white;
background: $highlight-color;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px hsla(0,0%,0%,.28);
will-change: transform;
&:empty {
display: none;
}
}
}
/*
----------------