2018-11-02 17:58:04 -05:00
// ==UserScript==
// @name OSRT Staging Move Drag-n-Drop
// @namespace openSUSE/openSUSE-release-tools
// @version 0.1.0
// @description Provide staging request moving interface on staging dashboard.
// @author Jimmy Berry
// @match */project/staging_projects/*
// @require https://code.jquery.com/jquery-3.3.1.min.js
// @require https://raw.githubusercontent.com/p34eu/selectables/master/selectables.js
// @grant none
// ==/UserScript==
// Uses a combination of two sources:
// - https://www.sitepoint.com/accessible-drag-drop/ (modified slightly)
// - https://github.com/p34eu/selectables (used directly with abuse of modifier key option)
( function ( )
{
2018-11-07 22:12:18 -06:00
// Exclude not usable browsers.
if ( ! document . querySelectorAll || ! ( 'draggable' in document . createElement ( 'span' ) ) ) {
return ;
}
2018-11-02 17:58:04 -05:00
// Add explanation of trigger shortcut to legend box.
var explanation = document . createElement ( 'div' ) ;
explanation . id = 'osrt-explanation' ;
explanation . innerText = 'press ctrl+m to move requests' ;
document . querySelector ( '#legends' ) . appendChild ( explanation ) ;
window . onkeyup = function ( e ) {
if ( e . keyCode == 77 && e . ctrlKey ) {
initMoveInterface ( ) ;
}
}
// Include CSS immediately for explanation.
$ ( 'head' ) . append ( ` <style>
# osrt - explanation
{
padding : 10 px ;
background - color : # d9b200 ;
color : white ;
}
# osrt - summary
{
position : sticky ;
z - index : 10000 ;
bottom : 0 ;
left : 0 ;
width : 95 % ;
height : 100 px ;
padding : 10 px ;
background - color : black ;
color : # 18 f018 ;
white - space : pre ;
overflow : scroll ;
}
/* drop target state */
[ data - draggable = "target" ] [ aria - dropeffect = "move" ]
{
border - color : # 68 b ;
}
[ data - draggable = "target" ] [ aria - dropeffect = "move" ] : focus ,
[ data - draggable = "target" ] [ aria - dropeffect = "move" ] . dragover
{
box - shadow : 0 0 0 1 px # fff , 0 0 0 3 px # 68 b ;
}
[ data - draggable = "item" ] : focus
{
box - shadow : 0 0 0 2 px # 68 b , inset 0 0 0 1 px # ddd ;
}
[ data - draggable = "item" ] [ aria - grabbed = "true" ] ,
. color - legend . selected
{
background : # 8 ad ;
color : # fff ;
}
. color - legend . moved ,
. osrt - moved
{
background : repeating - linear - gradient (
45 deg ,
# 606 dbc ,
# 606 dbc 10 px ,
# 465298 10 px ,
# 465298 20 px
) ;
}
< / s t y l e > ` ) ;
} ) ( ) ;
var initMoveInterface = function ( ) {
// Update explanation text and add new legend entries.
function addLegend ( type )
{
var listItem = document . createElement ( 'li' ) ;
var span = document . createElement ( 'span' ) ;
span . classList . add ( type . toLowerCase ( ) ) ;
listItem . appendChild ( span ) ;
listItem . appendChild ( document . createTextNode ( type ) ) ;
document . querySelector ( 'ul.color-legend' ) . appendChild ( listItem ) ;
}
addLegend ( 'Moved' ) ;
addLegend ( 'Selected' ) ;
document . querySelector ( '#osrt-explanation' ) . innerText = 'move mode activated: drag box around requests or ctrl/shift+click requests to select and drag a request to another staging.' ;
// @resource will not work since served without proper MIME type.
$ . get ( 'https://raw.githubusercontent.com/p34eu/selectables/master/selectables.css' , function ( data , status ) {
$ ( 'head' ) . append ( '<style>' + data + '</style>' ) ;
} ) ;
// Mark the drag targets and draggable items.
// Preferable to use the tr element as target, but mouse events not handled properly.
// Avoid making expand/collapse links selectable and avoid forcing them to be expanded.
// The pointer-events changes only seem to work properly from script.
$ ( 'table.staging-dashboard td' ) . attr ( 'data-draggable' , 'target' ) ;
$ ( 'table.staging-dashboard ul.packages-list li.request:not(:has(a.staging_expand, a.staging_collapse))' ) . attr ( 'data-draggable' , 'item' ) . css ( 'pointer-events' , 'all' ) ;
// Disable mouse events on the request links as that makes it nearly impossible to drag them.
$ ( 'table.staging-dashboard ul.packages-list li.request:not(:has(a.staging_expand, a.staging_collapse)) a' ) . css ( 'pointer-events' , 'none' ) ;
// Configure selectables to play nice with drag-n-drop code.
new Selectables ( {
elements : 'ul.packages-list li[data-draggable="item"]' ,
zone : 'body' ,
start : function ( e ) {
e . osrtContinue = ( e . target . getAttribute ( 'data-draggable' ) != 'item' &&
e . target . tagName != 'A' &&
e . target . tagName != 'LABEL' &&
e . target . id != 'osrt-summary' ) ;
} ,
// Abuse key option by setting the value in start callback whic is run
// first and the value determines if drag selection is started.
key : 'osrtContinue' ,
onSelect : function ( e ) {
addSelection ( e ) ;
} ,
onDeselect : function ( e ) {
removeSelection ( e ) ;
}
} ) ;
function getStaging ( item )
{
var parent ;
if ( item . tagName == 'TD' ) {
parent = item . parentElement ;
} else {
parent = item . parentElement . parentElement . parentElement ;
}
if ( item . parentElement . classList . contains ( 'staging_collapsible' ) ) {
// An additional layer since in hidden container.
parent = parent . parentElement ;
}
return parent . querySelector ( 'div.letter a' ) . innerText ;
}
function updateSummary ( )
{
var summaryElement = document . querySelector ( 'div#osrt-summary' ) ;
if ( ! summaryElement ) {
summaryElement = document . createElement ( 'div' ) ;
summaryElement . id = 'osrt-summary' ;
document . body . appendChild ( summaryElement ) ;
}
var elements = document . querySelectorAll ( '.osrt-moved' ) ;
var summary = { } ;
var staging ;
for ( var i = 0 ; i < elements . length ; i ++ ) {
staging = getStaging ( elements [ i ] ) ;
if ( ! ( staging in summary ) ) {
summary [ staging ] = [ ] ;
}
summary [ staging ] . push ( elements [ i ] . children [ 0 ] . innerText . trim ( ) ) ;
}
var summaryText = '' ;
var pathParts = window . location . pathname . split ( '/' ) ;
var project = pathParts [ pathParts . length - 1 ] ;
for ( var key in summary ) {
staging = key ;
if ( ! isNaN ( key ) ) {
staging = 'adi:' + key ;
}
summaryText += 'osc staging -p ' + project + ' select --move ' + staging + ' ' + summary [ key ] . join ( ' ' ) + "\n" ;
}
summaryElement . innerText = summaryText ;
}
//get the collection of draggable targets and add their draggable attribute
for ( var
targets = document . querySelectorAll ( '[data-draggable="target"]' ) ,
len = targets . length ,
i = 0 ; i < len ; i ++ )
{
targets [ i ] . setAttribute ( 'aria-dropeffect' , 'none' ) ;
}
//get the collection of draggable items and add their draggable attributes
for ( var
items = document . querySelectorAll ( '[data-draggable="item"]' ) ,
len = items . length ,
i = 0 ; i < len ; i ++ )
{
items [ i ] . setAttribute ( 'draggable' , 'true' ) ;
items [ i ] . setAttribute ( 'aria-grabbed' , 'false' ) ;
items [ i ] . setAttribute ( 'tabindex' , '0' ) ;
// OSRT modification: keep track of original staging.
items [ i ] . setAttribute ( 'data-staging-origin' , getStaging ( items [ i ] ) ) ;
}
//dictionary for storing the selections data
//comprising an array of the currently selected items
//a reference to the selected items' owning container
//and a refernce to the current drop target container
var selections =
{
items : [ ] ,
owner : null ,
droptarget : null
} ;
//function for selecting an item
function addSelection ( item )
{
//if the owner reference is still null, set it to this item's parent
//so that further selection is only allowed within the same container
if ( ! selections . owner )
{
selections . owner = item . parentNode ;
}
//or if that's already happened then compare it with this item's parent
//and if they're not the same container, return to prevent selection
else if ( selections . owner != item . parentNode )
{
return ;
}
//set this item's grabbed state
item . setAttribute ( 'aria-grabbed' , 'true' ) ;
//add it to the items array
selections . items . push ( item ) ;
}
//function for unselecting an item
function removeSelection ( item )
{
//reset this item's grabbed state
item . setAttribute ( 'aria-grabbed' , 'false' ) ;
//then find and remove this item from the existing items array
for ( var len = selections . items . length , i = 0 ; i < len ; i ++ )
{
if ( selections . items [ i ] == item )
{
selections . items . splice ( i , 1 ) ;
break ;
}
}
}
//function for resetting all selections
function clearSelections ( )
{
//if we have any selected items
if ( selections . items . length )
{
//reset the owner reference
selections . owner = null ;
//reset the grabbed state on every selected item
for ( var len = selections . items . length , i = 0 ; i < len ; i ++ )
{
selections . items [ i ] . setAttribute ( 'aria-grabbed' , 'false' ) ;
}
//then reset the items array
selections . items = [ ] ;
}
}
//shorctut function for testing whether a selection modifier is pressed
function hasModifier ( e )
{
return ( e . ctrlKey || e . metaKey || e . shiftKey ) ;
}
//function for applying dropeffect to the target containers
function addDropeffects ( )
{
//apply aria-dropeffect and tabindex to all targets apart from the owner
for ( var len = targets . length , i = 0 ; i < len ; i ++ )
{
if
(
targets [ i ] != selections . owner
&&
targets [ i ] . getAttribute ( 'aria-dropeffect' ) == 'none'
)
{
targets [ i ] . setAttribute ( 'aria-dropeffect' , 'move' ) ;
targets [ i ] . setAttribute ( 'tabindex' , '0' ) ;
}
}
//remove aria-grabbed and tabindex from all items inside those containers
for ( var len = items . length , i = 0 ; i < len ; i ++ )
{
if
(
items [ i ] . parentNode != selections . owner
&&
items [ i ] . getAttribute ( 'aria-grabbed' )
)
{
items [ i ] . removeAttribute ( 'aria-grabbed' ) ;
items [ i ] . removeAttribute ( 'tabindex' ) ;
}
}
}
//function for removing dropeffect from the target containers
function clearDropeffects ( )
{
//if we have any selected items
if ( selections . items . length )
{
//reset aria-dropeffect and remove tabindex from all targets
for ( var len = targets . length , i = 0 ; i < len ; i ++ )
{
if ( targets [ i ] . getAttribute ( 'aria-dropeffect' ) != 'none' )
{
targets [ i ] . setAttribute ( 'aria-dropeffect' , 'none' ) ;
targets [ i ] . removeAttribute ( 'tabindex' ) ;
}
}
//restore aria-grabbed and tabindex to all selectable items
//without changing the grabbed value of any existing selected items
for ( var len = items . length , i = 0 ; i < len ; i ++ )
{
if ( ! items [ i ] . getAttribute ( 'aria-grabbed' ) )
{
items [ i ] . setAttribute ( 'aria-grabbed' , 'false' ) ;
items [ i ] . setAttribute ( 'tabindex' , '0' ) ;
}
else if ( items [ i ] . getAttribute ( 'aria-grabbed' ) == 'true' )
{
items [ i ] . setAttribute ( 'tabindex' , '0' ) ;
}
}
}
}
//shortcut function for identifying an event element's target container
function getContainer ( element )
{
do
{
if ( element . nodeType == 1 && element . getAttribute ( 'aria-dropeffect' ) )
{
return element ;
}
}
while ( element = element . parentNode ) ;
return null ;
}
//mousedown event to implement single selection
document . addEventListener ( 'mousedown' , function ( e )
{
//if the element is a draggable item
if ( e . target . getAttribute ( 'draggable' ) )
{
//clear dropeffect from the target containers
clearDropeffects ( ) ;
//if the multiple selection modifier is not pressed
//and the item's grabbed state is currently false
if
(
! hasModifier ( e )
&&
e . target . getAttribute ( 'aria-grabbed' ) == 'false'
)
{
//clear all existing selections
clearSelections ( ) ;
//then add this new selection
addSelection ( e . target ) ;
}
}
//else [if the element is anything else]
//and the selection modifier is not pressed
else if ( ! hasModifier ( e ) )
{
//clear dropeffect from the target containers
clearDropeffects ( ) ;
//clear all existing selections
clearSelections ( ) ;
}
//else [if the element is anything else and the modifier is pressed]
else
{
//clear dropeffect from the target containers
clearDropeffects ( ) ;
}
} , false ) ;
//mouseup event to implement multiple selection
document . addEventListener ( 'mouseup' , function ( e )
{
//if the element is a draggable item
//and the multipler selection modifier is pressed
if ( e . target . getAttribute ( 'draggable' ) && hasModifier ( e ) )
{
//if the item's grabbed state is currently true
if ( e . target . getAttribute ( 'aria-grabbed' ) == 'true' )
{
//unselect this item
removeSelection ( e . target ) ;
//if that was the only selected item
//then reset the owner container reference
if ( ! selections . items . length )
{
selections . owner = null ;
}
}
//else [if the item's grabbed state is false]
else
{
//add this additional selection
addSelection ( e . target ) ;
}
}
} , false ) ;
//dragstart event to initiate mouse dragging
document . addEventListener ( 'dragstart' , function ( e )
{
//if the element's parent is not the owner, then block this event
if ( selections . owner != e . target . parentNode )
{
e . preventDefault ( ) ;
return ;
}
//[else] if the multiple selection modifier is pressed
//and the item's grabbed state is currently false
if
(
hasModifier ( e )
&&
e . target . getAttribute ( 'aria-grabbed' ) == 'false'
)
{
//add this additional selection
addSelection ( e . target ) ;
}
//we don't need the transfer data, but we have to define something
//otherwise the drop action won't work at all in firefox
//most browsers support the proper mime-type syntax, eg. "text/plain"
//but we have to use this incorrect syntax for the benefit of IE10+
e . dataTransfer . setData ( 'text' , '' ) ;
//apply dropeffect to the target containers
addDropeffects ( ) ;
} , false ) ;
//related variable is needed to maintain a reference to the
//dragleave's relatedTarget, since it doesn't have e.relatedTarget
var related = null ;
//dragenter event to set that variable
document . addEventListener ( 'dragenter' , function ( e )
{
related = e . target ;
} , false ) ;
//dragleave event to maintain target highlighting using that variable
document . addEventListener ( 'dragleave' , function ( e )
{
//get a drop target reference from the relatedTarget
var droptarget = getContainer ( related ) ;
//if the target is the owner then it's not a valid drop target
if ( droptarget == selections . owner )
{
droptarget = null ;
}
//if the drop target is different from the last stored reference
//(or we have one of those references but not the other one)
if ( droptarget != selections . droptarget )
{
//if we have a saved reference, clear its existing dragover class
if ( selections . droptarget )
{
selections . droptarget . className =
selections . droptarget . className . replace ( / dragover/g , '' ) ;
}
//apply the dragover class to the new drop target reference
if ( droptarget )
{
droptarget . className += ' dragover' ;
}
//then save that reference for next time
selections . droptarget = droptarget ;
}
} , false ) ;
//dragover event to allow the drag by preventing its default
document . addEventListener ( 'dragover' , function ( e )
{
//if we have any selected items, allow them to be dragged
if ( selections . items . length )
{
e . preventDefault ( ) ;
}
} , false ) ;
//dragend event to implement items being validly dropped into targets,
//or invalidly dropped elsewhere, and to clean-up the interface either way
document . addEventListener ( 'dragend' , function ( e )
{
//if we have a valid drop target reference
//(which implies that we have some selected items)
if ( selections . droptarget )
{
// OSRT modification: only move if location is changing.
if ( getStaging ( selections . droptarget ) == getStaging ( selections . items [ 0 ] ) ) {
e . preventDefault ( ) ;
return ;
}
// OSRT modification: place requests back in package list.
var target = selections . droptarget . parentElement . querySelector ( 'ul.packages-list' ) ;
//append the selected items to the end of the target container
for ( var len = selections . items . length , i = 0 ; i < len ; i ++ )
{
// OSRT modification: place in package list and determine if moved from origin.
// selections.droptarget.appendChild(selections.items[i]);
target . appendChild ( selections . items [ i ] ) ;
if ( getStaging ( selections . items [ i ] ) != selections . items [ i ] . getAttribute ( 'data-staging-origin' ) )
{
selections . items [ i ] . classList . add ( 'osrt-moved' ) ;
}
else {
selections . items [ i ] . classList . remove ( 'osrt-moved' ) ;
}
}
// OSRT modification: after drag update overall summary of moves.
updateSummary ( ) ;
//prevent default to allow the action
e . preventDefault ( ) ;
}
//if we have any selected items
if ( selections . items . length )
{
//clear dropeffect from the target containers
clearDropeffects ( ) ;
//if we have a valid drop target reference
if ( selections . droptarget )
{
//reset the selections array
clearSelections ( ) ;
//reset the target's dragover class
selections . droptarget . className =
selections . droptarget . className . replace ( / dragover/g , '' ) ;
//reset the target reference
selections . droptarget = null ;
}
}
} , false ) ;
} ;