Commit 6af15539 by Matjaz Gregoric

Wait 500 milliseconds before starting drag on touch.

This allows users to scroll the item bank with their finger without
accidentally starting to drag items.

Mouse drag events still start without a delay, since scrolling works
differently on desktops.

I found no way to implement this with jQuery UI draggable, so I had to
reimplement drag and drop interactions, which in some ways makes it
cleaner because it doesn't bypass virtualDOM.
parent 93fbea44
......@@ -262,12 +262,9 @@ class DragAndDropBlock(
fragment = Fragment()
fragment.add_content(loader.render_template('/templates/html/drag_and_drop.html'))
css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop.css'
'public/css/drag_and_drop.css',
)
js_urls = (
'public/js/vendor/jquery-ui-1.10.4.custom.min.js',
'public/js/vendor/jquery-ui-touch-punch-0.2.3.min.js', # Makes it work on touch devices
'public/js/vendor/virtual-dom-1.3.0.min.js',
'public/js/drag_and_drop.js',
)
......@@ -357,11 +354,9 @@ class DragAndDropBlock(
fragment.add_content(loader.render_template('/templates/html/drag_and_drop_edit.html', context))
css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop_edit.css'
'public/css/drag_and_drop_edit.css',
)
js_urls = (
'public/js/vendor/jquery-ui-1.10.4.custom.min.js',
'public/js/vendor/handlebars-v1.1.2.js',
'public/js/drag_and_drop_edit.js',
)
......
......@@ -67,6 +67,7 @@
width: auto;
padding: 1%;
background-color: #ebf0f2;
position: relative;
}
/*** DRAGGABLE ITEMS ***/
......
/*! jQuery UI - v1.10.4 - 2014-07-07
* http://jqueryui.com
* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.button.css, jquery.ui.dialog.css, jquery.ui.theme.css
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url("images/ui-bg_flat_75_ffffff_40x100.png") 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_888888_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cd0a0a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px}
\ No newline at end of file
......@@ -83,6 +83,11 @@ function DragAndDropTemplates(configuration) {
// matches contrast between text color and background color:
style['outline-color'] = item.color;
}
if (item.is_dragged) {
style.position = 'absolute';
style.left = item.drag_position.left + 'px';
style.top = item.drag_position.top + 'px';
}
if (item.is_placed) {
var maxWidth = (item.widthPercent || 30) / 100;
var widthPercent = zone.width_percent / 100;
......@@ -133,13 +138,24 @@ function DragAndDropTemplates(configuration) {
itemSpinnerTemplate(item), item_content, itemSRNote, item_description
];
// Unique key for virtual dom change tracking. Key must be different for
// Placed vs Dragged vs Unplaced, or weird bugs can occur.
var key = item.value;
if (item.is_placed && item.is_dragged) {
key += '-pd';
} else if (item.is_placed) {
key += '-p';
} else if (item.is_dragged) {
key += '-d';
} else {
key += '-u';
}
return (
h(
'div.option',
{
// Unique key for virtual dom change tracking. Key must be different for
// Placed vs Unplaced, or weird bugs can occur.
key: item.value + (item.is_placed ? "-p" : "-u"),
key: key,
className: className,
attributes: attributes,
style: style
......@@ -183,7 +199,9 @@ function DragAndDropTemplates(configuration) {
var selector = ctx.display_zone_borders ? 'div.zone.zone-with-borders' : 'div.zone';
// Mark item alignment and render its placed items as children
var item_wrapper = 'div.item-wrapper.item-align.item-align-' + zone.align;
var is_item_in_zone = function(i) { return i.is_placed && (i.zone === zone.uid); };
// In assessment mode already placed items can be dragged out of their current zone.
// Only render placed items that are not currently being dragged out of the zone.
var is_item_in_zone = function(i) { return i.is_placed && !i.is_dragged && (i.zone === zone.uid); };
var items_in_zone = $.grep(ctx.items, is_item_in_zone);
var zone_description_id = zone.prefixed_uid + '-description';
if (items_in_zone.length == 0) {
......@@ -528,9 +546,25 @@ function DragAndDropTemplates(configuration) {
var problemHeader = ctx.show_problem_header ? h('h4.title1', gettext('Problem')) : null;
// Render only items in the bank here, including placeholders. Placed
// items will be rendered by zoneTemplate.
var is_item_placed = function(i) { return i.is_placed; };
var items_placed = $.grep(ctx.items, is_item_placed);
var items_in_bank = $.grep(ctx.items, is_item_placed, true);
var items_in_bank = [];
var items_dragged = [];
var items_placed = [];
ctx.items.forEach(function(item) {
if (item.is_dragged) {
items_dragged.push(item);
// Dragged items require a placeholder in the bank.
// In assessment mode, already placed items can be dragged.
if (item.is_placed) {
items_placed.push(item)
} else {
items_in_bank.push(item);
}
} else if (item.is_placed) {
items_placed.push(item);
} else {
items_in_bank.push(item);
}
});
var item_bank_properties = {
attributes: {
'role': 'group',
......@@ -543,6 +577,19 @@ function DragAndDropTemplates(configuration) {
item_bank_properties.attributes['aria-dropeffect'] = 'move';
item_bank_properties.attributes['role'] = 'button';
}
// Render items in the bank. If the item is currently being dragged, it should be
// rendered as a placeholder. All already placed items should also be rendered as
// placedholders (as the last content in the bank) to maintain original bank dimensions.
var bank_children = [];
items_in_bank.forEach(function(item) {
if (item.is_dragged) {
bank_children.push(itemPlaceholderTemplate(item, ctx));
} else {
bank_children.push(itemTemplate(item, ctx));
}
});
bank_children = bank_children.concat(renderCollection(itemPlaceholderTemplate, items_placed, ctx));
return (
h('div.themed-xblock.xblock--drag-and-drop', main_element_properties, [
problemTitle,
......@@ -553,10 +600,7 @@ function DragAndDropTemplates(configuration) {
h('p', {innerHTML: ctx.problem_html}),
]),
h('div.drag-container', {}, [
h('div.item-bank', item_bank_properties, [
renderCollection(itemTemplate, items_in_bank, ctx),
renderCollection(itemPlaceholderTemplate, items_placed, ctx)
]),
h('div.item-bank', item_bank_properties, bank_children),
h('div.target', {attributes: {'role': 'group', 'arial-label': gettext('Drop Targets')}}, [
itemFeedbackPopupTemplate(ctx),
h('div.target-img-wrapper', [
......@@ -564,6 +608,7 @@ function DragAndDropTemplates(configuration) {
]),
renderCollection(zoneTemplate, ctx.zones, ctx)
]),
h('div.dragged-items', renderCollection(itemTemplate, items_dragged, ctx)),
]),
h("div.actions-toolbar", {attributes: {'role': 'group', 'aria-label': gettext('Actions')}}, [
(ctx.show_submit_answer ? submitAnswerTemplate(ctx) : null),
......@@ -617,6 +662,11 @@ function DragAndDropBlock(runtime, element, configuration) {
var DEFAULT_ZONE_ALIGN = 'center';
// Number of miliseconds the user has to keep their finger on the item
// without moving for drag to begin.
// This allows user to scroll the container without accidentally dragging the items.
var TOUCH_DRAG_DELAY = 500;
// Keyboard accessibility
var ESC = 27;
var RET = 13;
......@@ -689,6 +739,7 @@ function DragAndDropBlock(runtime, element, configuration) {
$root.empty();
applyState();
initDraggable();
initDroppable();
// Indicate that problem is done loading
......@@ -795,7 +846,7 @@ function DragAndDropBlock(runtime, element, configuration) {
$keyboardHelpDialog.find('.modal-window').show().focus();
// Set up event handlers
$(document).on('keydown', function(evt) {
$(document).on('keydown.keyboard-help', function(evt) {
keyboardEventDispatcher(evt, focusId);
});
......@@ -813,7 +864,7 @@ function DragAndDropBlock(runtime, element, configuration) {
$keyboardHelpDialog.find('.modal-window').hide();
// Remove event handlers
$(document).off('keydown');
$(document).off('keydown.keyboard-help');
$keyboardHelpDialog.find('.modal-dismiss-button').off();
// Handle focus
......@@ -892,15 +943,9 @@ function DragAndDropBlock(runtime, element, configuration) {
/**
* Update the DOM to reflect 'state'.
*/
var applyState = function(keepDraggableInit) {
var applyState = function() {
sendFeedbackPopupEvents();
updateDOM();
if (!keepDraggableInit) {
destroyDraggable();
if (!state.finished) {
initDraggable();
}
}
};
var sendFeedbackPopupEvents = function() {
......@@ -1053,6 +1098,18 @@ function DragAndDropBlock(runtime, element, configuration) {
return false;
};
var returnItemToBank = function(item_id) {
if (!state.items[item_id]) {
// Nothing to do here, item is already in the bank.
return;
}
delete state.items[item_id];
applyState();
var url = runtime.handlerUrl(element, 'drop_item');
var data = {val: item_id, zone: null};
$.post(url, JSON.stringify(data), 'json');
};
var placeGrabbedItem = function($zone) {
var zone = String($zone.data('uid'));
var zone_align = $zone.data('zone_align');
......@@ -1066,6 +1123,11 @@ function DragAndDropBlock(runtime, element, configuration) {
}
}
if (state.items[item_id] && state.items[item_id].zone === zone) {
// Nothing to do here, item already in zone.
return;
}
var items_in_zone_count = countItemsInZone(zone, [item_id.toString()]);
if (configuration.max_items_per_zone && configuration.max_items_per_zone <= items_in_zone_count) {
state.last_action_correct = false;
......@@ -1080,11 +1142,8 @@ function DragAndDropBlock(runtime, element, configuration) {
submitting_location: true,
};
// Wrap in setTimeout to let the droppable event finish.
setTimeout(function() {
applyState();
submitLocation(item_id, zone);
}, 0);
applyState();
submitLocation(item_id, zone);
};
var countItemsInZone = function(zone, exclude_ids) {
......@@ -1101,132 +1160,285 @@ function DragAndDropBlock(runtime, element, configuration) {
var initDroppable = function() {
// Set up zones for keyboard interaction
$root.find('.zone, .item-bank').each(function() {
$root.on('keydown', '.zone, .item-bank', function(evt) {
var $zone = $(this);
$zone.on('keydown', function(evt) {
if (state.keyboard_placement_mode) {
if (isCycleKey(evt)) {
focusNextZone(evt, $zone);
} else if (isCancelKey(evt)) {
evt.preventDefault();
state.keyboard_placement_mode = false;
releaseGrabbedItems();
} else if (isActionKey(evt)) {
evt.preventDefault();
evt.stopPropagation();
state.keyboard_placement_mode = false;
if ($zone.is('.item-bank')) {
delete state.items[$selectedItem.data('value')];
} else {
placeGrabbedItem($zone);
}
releaseGrabbedItems();
}
} else if (isTabKey(evt) && !evt.shiftKey) {
// If the user just dropped an item to this zone, next TAB keypress
// should move focus to "Go to Beginning" button.
if (state.tab_to_go_to_beginning_button && canGoToBeginning()) {
evt.preventDefault();
focusGoToBeginningButton();
if (state.keyboard_placement_mode) {
if (isCycleKey(evt)) {
focusNextZone(evt, $zone);
} else if (isCancelKey(evt)) {
evt.preventDefault();
state.keyboard_placement_mode = false;
releaseGrabbedItems();
} else if (isActionKey(evt)) {
evt.preventDefault();
evt.stopPropagation();
state.keyboard_placement_mode = false;
if ($zone.is('.item-bank')) {
delete state.items[$selectedItem.data('value')];
} else {
placeGrabbedItem($zone);
}
} else if (isSpaceKey(evt)) {
// Pressing the space bar moves the page down by default in most browsers.
// That can be distracting while moving items with the keyboard, so prevent
// the default scroll from happening while a zone is focused.
releaseGrabbedItems();
}
} else if (isTabKey(evt) && !evt.shiftKey) {
// If the user just dropped an item to this zone, next TAB keypress
// should move focus to "Go to Beginning" button.
if (state.tab_to_go_to_beginning_button && canGoToBeginning()) {
evt.preventDefault();
focusGoToBeginningButton();
}
});
$zone.on('blur', function() {
delete state.tab_to_go_to_beginning_button;
});
});
// Make zones accept items that are dropped using the mouse
$root.find('.zone').droppable({
accept: '.drag-container .option',
tolerance: 'pointer',
drop: function(evt, ui) {
var $zone = $(this);
placeGrabbedItem($zone);
} else if (isSpaceKey(evt)) {
// Pressing the space bar moves the page down by default in most browsers.
// That can be distracting while moving items with the keyboard, so prevent
// the default scroll from happening while a zone is focused.
evt.preventDefault();
}
});
if (configuration.mode === DragAndDropBlock.ASSESSMENT_MODE) {
// Make item bank accept items that are returned to the bank using the mouse
$root.find('.item-bank').droppable({
accept: '.target .option',
tolerance: 'pointer',
drop: function(evt, ui) {
var $item = ui.helper;
var item_id = $item.data('value');
releaseGrabbedItems();
delete state.items[item_id];
applyState();
var url = runtime.handlerUrl(element, 'drop_item');
var data = {
val: item_id,
zone: null
};
$root.on('blur', '.zone, .item-bank', function(evt) {
delete state.tab_to_go_to_beginning_button;
});
};
$.post(url, JSON.stringify(data), 'json')
}
});
var getItemById = function(item_id) {
for (var i = 0; i < configuration.items.length; i++) {
if (configuration.items[i].id === item_id) {
return configuration.items[i];
}
}
};
var initDraggable = function() {
$root.find('.drag-container .option[draggable=true]').each(function() {
var $item = $(this);
var $container = $root.find('.drag-container');
// Allow items to be "picked up" using the keyboard
$container.on('keydown', '.option[draggable=true]', function(evt) {
if (isActionKey(evt)) {
var $item = $(this);
evt.preventDefault();
evt.stopPropagation();
state.keyboard_placement_mode = true;
grabItem($item, 'keyboard');
$selectedItem = $item;
$root.find('.target .zone').first().focus();
}
});
// Allow item to be "picked up" using the keyboard
$item.on('keydown', function(evt) {
if (isActionKey(evt)) {
evt.preventDefault();
evt.stopPropagation();
state.keyboard_placement_mode = true;
grabItem($item, 'keyboard');
$selectedItem = $item;
$root.find('.target .zone').first().focus();
// Helper functions that return pageX/pageY from either touch or mouse events.
var pageX = function(evt) {
if (evt.type === 'touchstart' || evt.type === 'touchmove') {
return evt.originalEvent.targetTouches[0].pageX;
} else {
return evt.pageX;
}
};
var pageY = function(evt) {
if (evt.type === 'touchstart' || evt.type === 'touchmove') {
return evt.originalEvent.targetTouches[0].pageY;
} else {
return evt.pageY;
}
};
var isValidDropTarget = function(element) {
var valid = element.classList.contains('zone');
if (!valid && configuration.mode === DragAndDropBlock.ASSESSMENT_MODE) {
// In assessment mode, items can be dropped back into the item bank.
valid = element.classList.contains('item-bank');
}
return valid;
};
// If the drag ended on a valid target zone, returns the zone, otherwise returns null;
var getTargetZone = function(evt) {
var $zone = null;
var x, y;
if (evt.type === 'mouseup') {
x = evt.clientX;
y = evt.clientY;
} else {
x = evt.originalEvent.changedTouches[0].clientX;
y = evt.originalEvent.changedTouches[0].clientY;
}
// We have to temporarily hide the dragged item so that we can get the
// to the element below it.
var $dragged_items_container = $container.find('.dragged-items');
$dragged_items_container.hide();
var element = document.elementFromPoint(x, y);
while (element) {
if (isValidDropTarget(element)) {
$zone = $(element);
break;
} else {
element = element.parentElement;
}
}
$dragged_items_container.show();
return $zone;
};
var onDragStart = function(interaction_type, $item, drag_origin) {
var item_id = $item.data('value');
var item = getItemById(item_id);
var $document = $(document);
grabItem($item, 'mouse');
publishEvent({
event_type: 'edx.drag_and_drop_v2.item.picked_up',
item_id: item_id
});
// Make item draggable using the mouse
try {
$item.draggable({
addClasses: false, // don't add ui-draggable-* classes as they don't play well with virtual DOM.
containment: $root.find('.drag-container'),
cursor: 'move',
stack: $root.find('.drag-container .option'),
revert: 'invalid',
revertDuration: 150,
start: function(evt, ui) {
var $item = $(this);
// Store initial position of dragged item to be able to revert back to it on cancelled drag
// (when user drops the item onto an area that is not a droppable zone).
// The jQuery UI draggable library usually knows how to revert correctly, but our dropped items
// have a translation transform that confuses jQuery UI draggable, so we "help" it do the right
// thing by manually storing the initial position and resetting it in the 'stop' handler below.
$item.data('initial-position', {
left: $item.css('left'),
top: $item.css('top')
});
grabItem($item, 'mouse');
publishEvent({
event_type: 'edx.drag_and_drop_v2.item.picked_up',
item_id: $item.data('value'),
});
},
stop: function(evt, ui) {
// Revert to original position.
$item.css($item.data('initial-position'));
var max_left = $container.innerWidth() - $item.outerWidth();
var max_top = $container.innerHeight() - $item.outerHeight();
var item_size = {width: $item.width(), height: $item.height()};
// We need to get the item position relative to the $container.
var item_offset = $item.offset();
var container_offset = $container.offset();
var original_position = {
left: item_offset.left - container_offset.left,
top: item_offset.top - container_offset.top
};
item.drag_position = original_position;
applyState();
// Animate the item back to its original position in the bank.
var revertDrag = function() {
var revert_duration = 150;
var start_position = item.drag_position;
var start_ts = null;
var step = function(ts) {
if (!start_ts) {
start_ts = ts;
}
var progress = Math.min(1, (ts - start_ts) / revert_duration);
item.drag_position = {
left: start_position.left + (progress * (original_position.left - start_position.left)),
top: start_position.top + (progress * (original_position.top - start_position.top)),
};
if (progress === 1) {
delete item.drag_position;
releaseGrabbedItems();
} else {
applyState();
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
};
var raf_id = null;
var onDragMove = function(evt) {
evt.preventDefault();
if (raf_id) {
cancelAnimationFrame(raf_id);
}
raf_id = requestAnimationFrame(function() {
var dx = pageX(evt) - drag_origin.x;
var dy = pageY(evt) - drag_origin.y;
var left = original_position.left + dx;
var top = original_position.top + dy;
left = Math.max(0, Math.min(max_left, left));
top = Math.max(0, Math.min(max_top, top));
item.drag_position = {left: left, top: top};
applyState();
raf_id = null;
});
} catch (e) {
// Initializing the draggable will fail if draggable was already
// initialized. That's expected, ignore the exception.
};
var onDragEnd = function(evt) {
if (raf_id) {
cancelAnimationFrame(raf_id);
}
if (evt.type === 'mouseup') {
$document.off('mousemove', onDragMove);
$document.off('mouseup', onDragEnd);
} else {
$item.off('touchmove', onDragMove);
$item.off('touchend touchcancel', onDragEnd);
}
if (evt.type === 'touchcancel') {
revertDrag();
return;
}
var $zone = getTargetZone(evt);
if ($zone) {
delete item.drag_position;
if ($zone.is('.item-bank')) {
returnItemToBank(item_id);
} else {
placeGrabbedItem($zone);
}
releaseGrabbedItems();
} else {
revertDrag();
}
};
if (interaction_type === 'mouse') {
// Mouse events have to be bound to the document or they won't fire after
// the item is removed from the bank.
$document.on('mousemove', onDragMove);
$document.on('mouseup', onDragEnd);
} else {
// Touch events behave in the opposite way - they have to be bound to the item
// where the touchstart event was fired, or touchmove will not fire if the item
// gets removed from the DOM; see: https://stackoverflow.com/a/45760014/51397
$item.on('touchmove', onDragMove);
$item.on('touchend touchcancel', onDragEnd);
}
};
// Touch devices emulate mousedown events after touchstart, which would cause our drag
// start event to be handled twice.
// One way to prevent that would be to call evt.preventDefault() in the touchstart handler,
// but that would also prevent scrolling, which we do not want.
// Instead of preventing the default, we use the handled_by_touch flag, which we set to true
// in the touchstart handler (and set it back to false after the event is processed).
// If the mousedown handler sees the flag set to true, it will simply ignore the event.
var handled_by_touch = false;
// Mousedown events should start the drag immediately.
$container.on('mousedown', '.option[draggable=true]', function(evt) {
if (!handled_by_touch) {
evt.preventDefault();
var $item = $(this);
var drag_origin = {x: pageX(evt), y: pageY(evt)};
onDragStart('mouse', $(this), drag_origin);
}
});
// Touchstart events should start the event after a timeout.
// If the user starts moving the finger during the timeout, the drag should be cancelled.
// This allows users to scroll the item bank with their finger without accidentally starting
// to drag items.
$container.on('touchstart', '.option[draggable=true]', function(evt) {
handled_by_touch = true;
var $item = $(this);
var drag_origin = {x: pageX(evt), y: pageY(evt)};
var timeout_id = null;
var cancelDrag = function() {
clearTimeout(timeout_id);
// We need to reset the handled_by_touch in a timeout,
// so that it happens after the potentially emulated mousedown event.
setTimeout(function() {
handled_by_touch = false;
}, 0);
};
$item.one('touchmove touchend touchcancel', cancelDrag);
timeout_id = setTimeout(function() {
handled_by_touch = false;
$item.off('touchmove touchend touchcancel', cancelDrag);
onDragStart('touch', $item, drag_origin);
}, TOUCH_DRAG_DELAY);
});
// Prevent touchmove events fired on the dragged item causing scroll.
$container.on('touchmove', '.dragged-items .options[draggable=true]', function(evt) {
evt.preventDefault();
});
};
var grabItem = function($item, interaction_type) {
......@@ -1241,8 +1453,7 @@ function DragAndDropBlock(runtime, element, configuration) {
}
});
closePopup(false);
// applyState(true) skips destroying and initializing draggable
applyState(true);
applyState();
};
var releaseGrabbedItems = function() {
......@@ -1250,23 +1461,7 @@ function DragAndDropBlock(runtime, element, configuration) {
item.grabbed = false;
delete item.grabbed_with;
});
// applyState(true) skips destroying and initializing draggable
applyState(true);
};
var destroyDraggable = function() {
$root.find('.drag-container .option[draggable=false]').each(function() {
var $item = $(this);
$item.off();
try {
$item.draggable('destroy');
} catch (e) {
// Destroying the draggable will fail if draggable was
// not initialized in the first place. Ignore the exception.
}
});
applyState();
};
var submitLocation = function(item_id, zone) {
......@@ -1473,6 +1668,8 @@ function DragAndDropBlock(runtime, element, configuration) {
has_image: !!item.expandedImageURL,
grabbed: grabbed,
grabbed_with: item.grabbed_with,
is_dragged: Boolean(item.drag_position),
drag_position: item.drag_position,
is_placed: Boolean(item_user_state),
widthPercent: item.widthPercent, // widthPercent may be undefined (auto width)
imgNaturalWidth: item.imgNaturalWidth,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 2011–2014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
\ No newline at end of file
git+https://github.com/edx/xblock-utils.git@v1.0.4#egg=xblock-utils==1.0.4
git+https://github.com/edx/XBlock.git@xblock-0.5.0#egg=XBlock==0.5.0
-e .
......@@ -188,7 +188,6 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.assertEqual(zone.get_attribute('aria-dropeffect'), 'move')
self.assertEqual(zone.get_attribute('data-uid'), 'Zone {}'.format(zone_number))
self.assertEqual(zone.get_attribute('data-zone_align'), 'center')
self.assertIn('ui-droppable', self.get_element_classes(zone))
zone_box_percentages = box_percentages[index]
self._assert_box_percentages( # pylint: disable=star-args
'#-Zone_{}'.format(zone_number), **zone_box_percentages
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment