From ead346ee5ff2fed0fdce639784ec77bc6bbacb78 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Tue, 15 Jan 2013 17:55:43 +0200 Subject: [PATCH] Refactoring. New feature: adding multiple draggables from one. --- .../js/capa/drag_and_drop/draggables.js | 1491 +++++++++-------- common/static/js/capa/drag_and_drop/main.js | 4 +- .../static/js/capa/drag_and_drop/targets.js | 9 - .../js/capa/drag_and_drop/update_input.js | 8 +- 4 files changed, 760 insertions(+), 752 deletions(-) diff --git a/common/static/js/capa/drag_and_drop/draggables.js b/common/static/js/capa/drag_and_drop/draggables.js index d95b0e706f..5065e7193b 100644 --- a/common/static/js/capa/drag_and_drop/draggables.js +++ b/common/static/js/capa/drag_and_drop/draggables.js @@ -5,189 +5,170 @@ (function (requirejs, require, define) { define(['logme', 'update_input'], function (logme, updateInput) { - return Draggables; + return { + 'init': init + }; - function Draggables(state) { - var c1; + function init(state) { + logme('Draggables.init; state = ', state); state.draggables = []; state.numDraggablesInSlider = 0; - - for (c1 = 0; c1 < state.config.draggables.length; c1 += 1) { - processDraggable(state.config.draggables[c1], c1 + 1); - } - - state.updateArrowOpacity(); state.currentMovingDraggable = null; + (function (c1) { + while (c1 < state.config.draggables.length) { + processDraggable(state, state.config.draggables[c1], c1 + 1); + c1 += 1 + } + }(0)); + + state.updateArrowOpacity(); + $(document).mousemove(function (event) { - if (state.currentMovingDraggable !== null) { - state.currentMovingDraggable.iconEl.css( + documentMouseMove(state, event); + }); + } + + function documentMouseMove(state, event) { + if (state.currentMovingDraggable !== null) { + state.currentMovingDraggable.iconEl.css( + 'left', + event.pageX - + state.baseImageEl.offset().left - + state.currentMovingDraggable.iconWidth * 0.5 + - state.currentMovingDraggable.iconElLeftOffset + ); + state.currentMovingDraggable.iconEl.css( + 'top', + event.pageY - + state.baseImageEl.offset().top - + state.currentMovingDraggable.iconHeight * 0.5 + ); + + if (state.currentMovingDraggable.labelEl !== null) { + state.currentMovingDraggable.labelEl.css( 'left', event.pageX - state.baseImageEl.offset().left - - state.currentMovingDraggable.iconWidth * 0.5 - - state.currentMovingDraggable.iconElLeftOffset + state.currentMovingDraggable.labelWidth * 0.5 + - 9 // Account for padding, border. ); - state.currentMovingDraggable.iconEl.css( + state.currentMovingDraggable.labelEl.css( 'top', event.pageY - - state.baseImageEl.offset().top - - state.currentMovingDraggable.iconHeight * 0.5 + state.baseImageEl.offset().top + + state.currentMovingDraggable.iconHeight * 0.5 + + 5 + ); + } + } + } + + function processDraggable(state, obj, objIndex) { + var draggableObj; + + logme('processDraggable; state = ', state); + + logme('Processing draggable #' + objIndex); + + draggableObj = { + 'zIndex': objIndex, + 'oldZIndex': objIndex, + 'labelEl': null, + 'hasLoaded': false, + 'inContainer': true, + 'mousePressed': false, + 'onTarget': null, + + 'state': state, + + 'mouseDown': mouseDown, + 'mouseUp': mouseUp, + 'mouseMove': mouseMove, + 'checkLandingElement': checkLandingElement, + 'removeObjIdFromTarget': removeObjIdFromTarget, + 'checkIfOnTarget': checkIfOnTarget, + 'snapToTarget': snapToTarget, + 'correctZIndexes': correctZIndexes, + 'moveBackToSlider': moveBackToSlider + }; + + draggableObj.containerEl = $( + '
' + ); + + draggableObj.containerEl.appendTo(state.sliderEl); + + if (obj.icon.length > 0) { + draggableObj.iconElBGColor = 'transparent'; + draggableObj.iconElPadding = 0; + draggableObj.iconElBorder = 'none'; + draggableObj.iconElLeftOffset = 0; + + draggableObj.iconEl = $(''); + draggableObj.iconEl.attr( + 'src', + obj.icon + ); + draggableObj.iconEl.load(function () { + draggableObj.iconWidth = this.width; + draggableObj.iconHeight = this.height; + + if (draggableObj.iconWidth >= draggableObj.iconHeight) { + draggableObj.iconWidthSmall = 60; + draggableObj.iconHeightSmall = + draggableObj.iconWidthSmall * + (draggableObj.iconHeight / draggableObj.iconWidth); + } else { + draggableObj.iconHeightSmall = 60; + draggableObj.iconWidthSmall = + draggableObj.iconHeightSmall * + (draggableObj.iconWidth / draggableObj.iconHeight); + } + + draggableObj.iconEl.css('position', 'absolute'); + + draggableObj.iconEl.css( + 'width', + draggableObj.iconWidthSmall + ); + draggableObj.iconEl.css( + 'height', + draggableObj.iconHeightSmall ); - if (state.currentMovingDraggable.labelEl !== null) { - state.currentMovingDraggable.labelEl.css( - 'left', - event.pageX - - state.baseImageEl.offset().left - - state.currentMovingDraggable.labelWidth * 0.5 - - 9 // Account for padding, border. - ); - state.currentMovingDraggable.labelEl.css( + draggableObj.iconEl.css( + 'left', + 50 - draggableObj.iconWidthSmall * 0.5 + ); + + if (obj.label.length > 0) { + draggableObj.iconEl.css('top', 5); + } else { + draggableObj.iconEl.css( 'top', - event.pageY - - state.baseImageEl.offset().top + - state.currentMovingDraggable.iconHeight * 0.5 + - 5 + 50 - draggableObj.iconHeightSmall * 0.5 ); } - } - }); - return; + draggableObj.iconEl.appendTo(draggableObj.containerEl); - function processDraggable(obj, objIndex) { - var inContainer, mousePressed, onTarget, draggableObj; - - draggableObj = { - 'zIndex': objIndex, - 'oldZIndex': objIndex, - 'labelEl': null, - 'hasLoaded': false - }; - - draggableObj.containerEl = $( - '
' - ); - - draggableObj.containerEl.appendTo(state.sliderEl); - - if (obj.icon.length > 0) { - draggableObj.iconElBGColor = 'transparent'; - draggableObj.iconElPadding = 0; - draggableObj.iconElBorder = 'none'; - draggableObj.iconElLeftOffset = 0; - - draggableObj.iconEl = $(''); - draggableObj.iconEl.attr( - 'src', - obj.icon - ); - draggableObj.iconEl.load(function () { - draggableObj.iconWidth = this.width; - draggableObj.iconHeight = this.height; - - if (draggableObj.iconWidth >= draggableObj.iconHeight) { - draggableObj.iconWidthSmall = 60; - draggableObj.iconHeightSmall = - draggableObj.iconWidthSmall * - (draggableObj.iconHeight / draggableObj.iconWidth); - } else { - draggableObj.iconHeightSmall = 60; - draggableObj.iconWidthSmall = - draggableObj.iconHeightSmall * - (draggableObj.iconWidth / draggableObj.iconHeight); - } - - draggableObj.iconEl.css('position', 'absolute'); - - draggableObj.iconEl.css( - 'width', - draggableObj.iconWidthSmall - ); - draggableObj.iconEl.css( - 'height', - draggableObj.iconHeightSmall - ); - - draggableObj.iconEl.css( - 'left', - 50 - draggableObj.iconWidthSmall * 0.5 - ); - - if (obj.label.length > 0) { - draggableObj.iconEl.css('top', 5); - } else { - draggableObj.iconEl.css( - 'top', - 50 - draggableObj.iconHeightSmall * 0.5 - ); - } - - draggableObj.iconEl.appendTo(draggableObj.containerEl); - - if (obj.label.length > 0) { - draggableObj.labelEl = $( - '
' + - obj.label + - '
' - ); - - draggableObj.labelEl.appendTo( - draggableObj.containerEl - ); - - draggableObj.labelWidth = draggableObj.labelEl.width(); - - draggableObj.labelEl.css( - 'left', - 50 - draggableObj.labelWidth * 0.5 - ); - draggableObj.labelEl.css( - 'top', - 5 + draggableObj.iconHeightSmall + 5 - ); - - draggableObj.labelEl.mousedown(mouseDown); - draggableObj.labelEl.mouseup(mouseUp); - draggableObj.labelEl.mousemove(mouseMove); - } - - draggableObj.hasLoaded = true; - }); - } else { - // To make life easier, if there is no icon, but there is a - // label, we will create a label and store it as if it was an - // icon. All the existing code will work, and the user will - // see a label instead of an icon. if (obj.label.length > 0) { - draggableObj.iconElBGColor = state.config.labelBgColor; - draggableObj.iconElPadding = 8; - draggableObj.iconElBorder = '1px solid black'; - draggableObj.iconElLeftOffset = 9; - - draggableObj.iconEl = $( + draggableObj.labelEl = $( '
' + + obj.label + + '
' + ); - function mouseDown(event) { - if (mousePressed === false) { - // So that the browser does not perform a default drag. - // If we don't do this, each drag operation will - // potentially cause the highlghting of the dragged element. - event.preventDefault(); + draggableObj.iconEl.appendTo(draggableObj.containerEl); - // If this draggable is just being dragged out of the - // container, we must perform some additional tasks. - if (inContainer === true) { - draggableObj.containerEl.hide(); - - draggableObj.iconEl.detach(); - draggableObj.iconEl.css( - 'background-color', draggableObj.iconElBGColor - ); - draggableObj.iconEl.css( - 'padding-left', draggableObj.iconElPadding - ); - draggableObj.iconEl.css( - 'padding-right', draggableObj.iconElPadding - ); - draggableObj.iconEl.css( - 'border', draggableObj.iconElBorder - ); - draggableObj.iconEl.css( - 'width', - draggableObj.iconWidth - ); - draggableObj.iconEl.css( - 'height', - draggableObj.iconHeight - ); - draggableObj.iconEl.css( - 'left', - event.pageX - - state.baseImageEl.offset().left - - draggableObj.iconWidth * 0.5 - - draggableObj.iconElLeftOffset - ); - draggableObj.iconEl.css( - 'top', - event.pageY - - state.baseImageEl.offset().top - - draggableObj.iconHeight * 0.5 - ); - draggableObj.iconEl.appendTo( - state.baseImageEl.parent() - ); - - if (draggableObj.labelEl !== null) { - draggableObj.labelEl.detach(); - draggableObj.labelEl.css( - 'background-color', state.config.labelBgColor - ); - draggableObj.labelEl.css( - 'padding-left', 8 - ); - draggableObj.labelEl.css( - 'padding-right', 8 - ); - draggableObj.labelEl.css( - 'border', '1px solid black' - ); - draggableObj.labelEl.css( - 'left', - event.pageX - - state.baseImageEl.offset().left - - draggableObj.labelWidth * 0.5 - - 9 // Account for padding, border. - ); - draggableObj.labelEl.css( - 'top', - event.pageY - - state.baseImageEl.offset().top + - draggableObj.iconHeight * 0.5 + 5 - ); - draggableObj.labelEl.appendTo( - state.baseImageEl.parent() - ); - } - - inContainer = false; - state.numDraggablesInSlider -= 1; - } - - draggableObj.oldZIndex = draggableObj.zIndex; - draggableObj.zIndex = 1000; - draggableObj.iconEl.css('z-index', '1000'); - if (draggableObj.labelEl !== null) { - draggableObj.labelEl.css('z-index', '1000'); - } - - mousePressed = true; - state.currentMovingDraggable = draggableObj; - } - } - - function mouseUp() { - if (mousePressed === true) { - state.currentMovingDraggable = null; - - checkLandingElement(); - } - } - - function mouseMove() { - if (mousePressed === true) { - // Because we have also attached a 'mousemove' event to the - // 'document' (that will do the same thing), let's tell the - // browser not to bubble up this event. The attached event - // on the 'document' will only be triggered when the mouse - // pointer leaves the draggable while it is in the middle - // of a drag operation (user moves the mouse very quickly). - event.stopPropagation(); - - draggableObj.iconEl.css( - 'left', - event.pageX - - state.baseImageEl.offset().left - - draggableObj.iconWidth * 0.5 - - draggableObj.iconElLeftOffset - ); - draggableObj.iconEl.css( - 'top', - event.pageY - - state.baseImageEl.offset().top - - draggableObj.iconHeight * 0.5 - ); - - if (draggableObj.labelEl !== null) { - draggableObj.labelEl.css( - 'left', - event.pageX - - state.baseImageEl.offset().left - - draggableObj.labelWidth * 0.5 - - 9 // Acoount for padding, border. - ); - draggableObj.labelEl.css( - 'top', - event.pageY - - state.baseImageEl.offset().top + - draggableObj.iconHeight * 0.5 + - 5 - ); - } - } - } - - // At this point the mouse was realeased, and we need to check - // where the draggable eneded up. Based on several things, we - // will either move the draggable back to the slider, or update - // the input with the user's answer (X-Y position of the draggable, - // or the ID of the target where it landed. - function checkLandingElement() { - var positionIE, targetFound; - - mousePressed = false; - positionIE = draggableObj.iconEl.position(); - - if (state.individualTargets === true) { - targetFound = false; - - checkIfOnTarget(); - - if (targetFound === true) { - correctZIndexes(); - } else { - moveBackToSlider(); - removeObjIdFromTarget(); - - state.numDraggablesInSlider += 1; - } - } else { - if ( - (positionIE.left < 0) || - ( - positionIE.left + draggableObj.iconWidth > - state.baseImageEl.width() - ) || - (positionIE.top < 0) || - ( - positionIE.top + draggableObj.iconHeight > - state.baseImageEl.height() - ) - ) { - moveBackToSlider(); - - draggableObj.x = -1; - draggableObj.y = -1; - - state.numDraggablesInSlider += 1; - } else { - correctZIndexes(); - - draggableObj.x = - positionIE.left + draggableObj.iconWidth * 0.5; - draggableObj.y = - positionIE.top + draggableObj.iconHeight * 0.5; - } - } - - state.updateArrowOpacity(); - updateInput(state); + draggableObj.iconWidth = draggableObj.iconEl.width(); + draggableObj.iconHeight = draggableObj.iconEl.height(); + draggableObj.iconWidthSmall = draggableObj.iconWidth; + draggableObj.iconHeightSmall = draggableObj.iconHeight; + draggableObj.iconEl.css( + 'left', + 50 - draggableObj.iconWidthSmall * 0.5 + ); + draggableObj.iconEl.css( + 'top', + 50 - draggableObj.iconHeightSmall * 0.5 + ); + } else { + // If no icon and no label, don't create a draggable. return; - - function removeObjIdFromTarget() { - var c1; - - if (onTarget !== null) { - for (c1 = 0; c1 < onTarget.draggable.length; c1 += 1) { - if (onTarget.draggable[c1] === draggableObj.id) { - onTarget.draggable.splice(c1, 1); - - break; - } - } - - if (onTarget.numTextEl !== null) { - onTarget.updateNumTextEl(); - } - - onTarget = null; - } - } - - // - // Determine if a draggable, after it was relased, ends up on a - // target. We do this by iterating over all of the targets, and - // for each one we check whether the draggable's center is - // within the target's dimensions. - // - // positionIE is the object as returned by - // - // draggableObj.iconEl.position() - // - function checkIfOnTarget() { - var c1, target; - - for (c1 = 0; c1 < state.targets.length; c1 += 1) { - target = state.targets[c1]; - - // If only one draggable per target is allowed, and - // the current target already has a draggable on it - // (with an ID different from the one we are checking - // against), then go to next target. - if ( - (state.config.one_per_target === true) && - (target.draggable.length === 1) && - (target.draggable[0] !== draggableObj.id) - ) { - continue; - } - - // Check if the draggable's center coordinate is within - // the target's dimensions. If not, go to next target. - if ( - positionIE.top + draggableObj.iconHeight * 0.5 < - target.offset.top - ) { - continue; - } - if ( - positionIE.top + draggableObj.iconHeight * 0.5 > - target.offset.top + target.h - ) { - continue; - } - if ( - positionIE.left + draggableObj.iconWidth * 0.5 < - target.offset.left - ) { - continue; - } - if ( - positionIE.left + draggableObj.iconWidth * 0.5 > - target.offset.left + target.w - ) { - continue; - } - - // If we got here, then our draggable is on top of a - // target. - targetFound = true; - - // If the draggable was moved from one target to - // another, then we need to remove it's ID from the - // previous target's draggables list, and add it to the - // new target's draggables list. - if ( - (onTarget !== null) && - (onTarget.id !== target.id) - ) { - removeObjIdFromTarget(); - onTarget = target; - target.draggable.push(draggableObj.id); - } - // If the draggable was moved from the slider to a - // target, remember the target, and add ID to the - // target's draggables list. - else if (onTarget === null) { - onTarget = target; - target.draggable.push(draggableObj.id); - } - - if (target.numTextEl !== null) { - target.updateNumTextEl(); - } - - // Reposition the draggable so that it's center - // coincides with the center of the target. - snapToTarget(target); - - break; - } - } - - function snapToTarget(target) { - var offset; - - offset = 0; - if (state.config.targetOutline === true) { - offset = 1; - } - - draggableObj.iconEl.css( - 'left', - target.offset.left + 0.5 * target.w - - draggableObj.iconWidth * 0.5 + offset - - draggableObj.iconElLeftOffset - ); - draggableObj.iconEl.css( - 'top', - target.offset.top + 0.5 * target.h - - draggableObj.iconHeight * 0.5 + offset - ); - - if (draggableObj.labelEl !== null) { - draggableObj.labelEl.css( - 'left', - target.offset.left + 0.5 * target.w - - draggableObj.labelWidth * 0.5 + offset - - 9 // Acoount for padding, border. - ); - draggableObj.labelEl.css( - 'top', - target.offset.top + 0.5 * target.h + - draggableObj.iconHeight * 0.5 + 5 + offset - ); - } - } - - // Go through all of the draggables subtract 1 from the z-index - // of all whose z-index is higher than the old z-index of the - // current element. After, set the z-index of the current - // element to 1 + N (where N is the number of draggables - i.e. - // the highest z-index possible). - // - // This will make sure that after releasing a draggable, it - // will be on top of all of the other draggables. Also, the - // ordering of the visibility (z-index) of the other draggables - // will not change. - function correctZIndexes() { - var draggablesInMe, c1, c2, highestZIndex; - - if (state.individualTargets === true) { - - logme('In correctZIndexes.'); - - if (onTarget.draggable.length > 0) { - logme('onTarget.draggable.length > 0'); - - draggablesInMe = []; - - for (c1 = 0; c1 < onTarget.draggable.length; c1 += 1) { - for (c2 = 0; c2 < state.draggables.length; c2 += 1) { - if ( - onTarget.draggable[c1] === - state.draggables[c2].id - ) { - draggablesInMe.push(state.draggables[c2]); - } - } - } - - highestZIndex = -10000; - - for (c1 = 0; c1 < draggablesInMe.length; c1 += 1) { - if ( - (draggablesInMe[c1].zIndex > highestZIndex) && - (draggablesInMe[c1].zIndex !== 1000) - ) { - highestZIndex = draggablesInMe[c1].zIndex; - } - } - - if (highestZIndex === -10000) { - highestZIndex = onTarget.draggable.length; - } else if (highestZIndex < draggableObj.oldZIndex) { - highestZIndex = draggableObj.oldZIndex; - } else { - for (c1 = 0; c1 < draggablesInMe.length; c1 += 1) { - logme('draggablesInMe[' + c1 + '].id = ' + draggablesInMe[c1].id); - logme('draggablesInMe[' + c1 + '].zIndex = ' + draggablesInMe[c1].zIndex); - logme('draggablesInMe[' + c1 + '].oldZIndex = ' + draggablesInMe[c1].oldZIndex); - } - - logme('----------------'); - logme('highestZIndex = ' + highestZIndex); - - for (c1 = 0; c1 < draggablesInMe.length; c1 += 1) { - draggablesInMe[c1].zIndex -= 1; - draggablesInMe[c1].oldZIndex -= 1; - - draggablesInMe[c1].iconEl.css( - 'z-index', - draggablesInMe[c1].zIndex - ); - if (draggablesInMe[c1].labelEl !== null) { - draggablesInMe[c1].labelEl.css( - 'z-index', - draggablesInMe[c1].zIndex - ); - } - } - } - } else { - logme('highestZIndex = onTarget.draggable.length'); - highestZIndex = onTarget.draggable.length; - } - - draggableObj.zIndex = highestZIndex; - draggableObj.oldZIndex = highestZIndex; - - draggableObj.iconEl.css( - 'z-index', - draggableObj.zIndex - ); - if (draggableObj.labelEl !== null) { - draggableObj.labelEl.css( - 'z-index', - draggableObj.zIndex - ); - } - } else { - for (c1 = 0; c1 < state.draggables.length; c1++) { - if ( - draggableObj.oldZIndex < - state.draggables[c1].zIndex - ) { - state.draggables[c1].zIndex -= 1; - state.draggables[c1].oldZIndex = state.draggables[c1].zIndex; - state.draggables[c1].iconEl.css( - 'z-index', - state.draggables[c1].zIndex - ); - - if (state.draggables[c1].labelEl !== null) { - state.draggables[c1].labelEl.css( - 'z-index', - state.draggables[c1].zIndex - ); - } - } - } - - draggableObj.zIndex = c1; - draggableObj.oldZIndex = c1; - draggableObj.iconEl.css('z-index', c1); - - if (draggableObj.labelEl !== null) { - draggableObj.labelEl.css('z-index', c1); - } - } - } - - // If a draggable was released in a wrong positione, we will - // move it back to the slider, placing it in the same position - // that it was dragged out of. - function moveBackToSlider() { - draggableObj.containerEl.show(); - - draggableObj.zIndex = draggableObj.oldZIndex; - - draggableObj.iconEl.detach(); - draggableObj.iconEl.css('border', 'none'); - draggableObj.iconEl.css('background-color', 'transparent'); - draggableObj.iconEl.css('padding-left', 0); - draggableObj.iconEl.css('padding-right', 0); - draggableObj.iconEl.css('z-index', draggableObj.zIndex); - draggableObj.iconEl.css( - 'width', - draggableObj.iconWidthSmall - ); - draggableObj.iconEl.css( - 'height', - draggableObj.iconHeightSmall - ); - draggableObj.iconEl.css( - 'left', - 50 - draggableObj.iconWidthSmall * 0.5 - ); - if (draggableObj.labelEl !== null) { - draggableObj.iconEl.css('top', 5); - } else { - draggableObj.iconEl.css( - 'top', - 50 - draggableObj.iconHeightSmall * 0.5 - ); - } - - draggableObj.iconEl.appendTo(draggableObj.containerEl); - - if (draggableObj.labelEl !== null) { - draggableObj.labelEl.detach(); - draggableObj.labelEl.css('border', 'none'); - draggableObj.labelEl.css('background-color', 'transparent'); - draggableObj.labelEl.css('padding-left', 0); - draggableObj.labelEl.css('padding-right', 0); - draggableObj.labelEl.css('z-index', draggableObj.zIndex); - draggableObj.labelEl.css( - 'left', - 50 - draggableObj.labelWidth * 0.5 - ); - draggableObj.labelEl.css( - 'top', - 5 + draggableObj.iconHeightSmall + 5 - ); - draggableObj.labelEl.appendTo( - draggableObj.containerEl - ); - } - - inContainer = true; - } } } + + draggableObj.iconEl.mousedown(function (event) { + draggableObj.mouseDown.call(draggableObj, event); + }); + draggableObj.iconEl.mouseup(function (event) { + draggableObj.mouseUp.call(draggableObj, event); + }); + draggableObj.iconEl.mousemove(function (event) { + draggableObj.mouseMove.call(draggableObj, event); + }); + + draggableObj.containerEl.mousedown(function (event) { + draggableObj.mouseDown.call(draggableObj, event); + }); + draggableObj.containerEl.mouseup(function (event) { + draggableObj.mouseUp.call(draggableObj, event); + }); + draggableObj.containerEl.mousemove(function (event) { + draggableObj.mouseMove.call(draggableObj, event); + }); + + draggableObj.id = obj.id; + draggableObj.x = -1; + draggableObj.y = -1; + + state.numDraggablesInSlider += 1; + + if (obj.icon.length === 0) { + draggableObj.hasLoaded = true; + } + + state.draggables.push(draggableObj); + } + + function mouseDown(event) { + if (this.mousePressed === false) { + // So that the browser does not perform a default drag. + // If we don't do this, each drag operation will + // potentially cause the highlghting of the dragged element. + event.preventDefault(); + + // If this draggable is just being dragged out of the + // container, we must perform some additional tasks. + if (this.inContainer === true) { + this.containerEl.hide(); + + this.iconEl.detach(); + this.iconEl.css( + 'background-color', this.iconElBGColor + ); + this.iconEl.css( + 'padding-left', this.iconElPadding + ); + this.iconEl.css( + 'padding-right', this.iconElPadding + ); + this.iconEl.css( + 'border', this.iconElBorder + ); + this.iconEl.css( + 'width', + this.iconWidth + ); + this.iconEl.css( + 'height', + this.iconHeight + ); + this.iconEl.css( + 'left', + event.pageX - + this.state.baseImageEl.offset().left - + this.iconWidth * 0.5 + - this.iconElLeftOffset + ); + this.iconEl.css( + 'top', + event.pageY - + this.state.baseImageEl.offset().top - + this.iconHeight * 0.5 + ); + this.iconEl.appendTo( + state.baseImageEl.parent() + ); + + if (this.labelEl !== null) { + this.labelEl.detach(); + this.labelEl.css( + 'background-color', this.state.config.labelBgColor + ); + this.labelEl.css( + 'padding-left', 8 + ); + this.labelEl.css( + 'padding-right', 8 + ); + this.labelEl.css( + 'border', '1px solid black' + ); + this.labelEl.css( + 'left', + event.pageX - + this.state.baseImageEl.offset().left - + this.labelWidth * 0.5 + - 9 // Account for padding, border. + ); + this.labelEl.css( + 'top', + event.pageY - + this.state.baseImageEl.offset().top + + this.iconHeight * 0.5 + 5 + ); + this.labelEl.appendTo( + this.state.baseImageEl.parent() + ); + } + + this.inContainer = false; + this.state.numDraggablesInSlider -= 1; + } + + this.oldZIndex = this.zIndex; + this.zIndex = 1000; + this.iconEl.css('z-index', '1000'); + if (this.labelEl !== null) { + this.labelEl.css('z-index', '1000'); + } + + this.mousePressed = true; + this.state.currentMovingDraggable = this; + } + } + + function mouseUp() { + if (this.mousePressed === true) { + this.state.currentMovingDraggable = null; + + this.checkLandingElement(); + } + } + + function mouseMove() { + if (this.mousePressed === true) { + // Because we have also attached a 'mousemove' event to the + // 'document' (that will do the same thing), let's tell the + // browser not to bubble up this event. The attached event + // on the 'document' will only be triggered when the mouse + // pointer leaves the draggable while it is in the middle + // of a drag operation (user moves the mouse very quickly). + event.stopPropagation(); + + this.iconEl.css( + 'left', + event.pageX - + this.state.baseImageEl.offset().left - + this.iconWidth * 0.5 + - this.iconElLeftOffset + ); + this.iconEl.css( + 'top', + event.pageY - + this.state.baseImageEl.offset().top - + this.iconHeight * 0.5 + ); + + if (this.labelEl !== null) { + this.labelEl.css( + 'left', + event.pageX - + this.state.baseImageEl.offset().left - + this.labelWidth * 0.5 + - 9 // Acoount for padding, border. + ); + this.labelEl.css( + 'top', + event.pageY - + this.state.baseImageEl.offset().top + + this.iconHeight * 0.5 + + 5 + ); + } + } + } + + // At this point the mouse was realeased, and we need to check + // where the draggable eneded up. Based on several things, we + // will either move the draggable back to the slider, or update + // the input with the user's answer (X-Y position of the draggable, + // or the ID of the target where it landed. + function checkLandingElement() { + var positionIE; + + this.mousePressed = false; + positionIE = this.iconEl.position(); + + if (this.state.individualTargets === true) { + if (this.checkIfOnTarget(positionIE) === true) { + this.correctZIndexes(); + } else { + this.moveBackToSlider(); + this.removeObjIdFromTarget(); + + this.state.numDraggablesInSlider += 1; + } + } else { + if ( + (positionIE.left < 0) || + ( + positionIE.left + this.iconWidth > + this.state.baseImageEl.width() + ) || + (positionIE.top < 0) || + ( + positionIE.top + this.iconHeight > + this.state.baseImageEl.height() + ) + ) { + this.moveBackToSlider(); + + this.x = -1; + this.y = -1; + + this.state.numDraggablesInSlider += 1; + } else { + this.correctZIndexes(); + + this.x = + positionIE.left + this.iconWidth * 0.5; + this.y = + positionIE.top + this.iconHeight * 0.5; + } + } + + this.state.updateArrowOpacity(); + updateInput(this.state); + } + + function removeObjIdFromTarget() { + var c1; + + if (this.onTarget !== null) { + for (c1 = 0; c1 < this.onTarget.draggable.length; c1 += 1) { + if (this.onTarget.draggable[c1] === this.id) { + this.onTarget.draggable.splice(c1, 1); + + break; + } + } + + if (this.onTarget.numTextEl !== null) { + this.onTarget.updateNumTextEl(); + } + + this.onTarget = null; + } + } + + // + // Determine if a draggable, after it was relased, ends up on a + // target. We do this by iterating over all of the targets, and + // for each one we check whether the draggable's center is + // within the target's dimensions. + // + // positionIE is the object as returned by + // + // this.iconEl.position() + // + function checkIfOnTarget(positionIE) { + var c1, target, targetFound; + + targetFound = false; + + for (c1 = 0; c1 < this.state.targets.length; c1 += 1) { + target = this.state.targets[c1]; + + // If only one draggable per target is allowed, and + // the current target already has a draggable on it + // (with an ID different from the one we are checking + // against), then go to next target. + if ( + (this.state.config.one_per_target === true) && + (target.draggable.length === 1) && + (target.draggable[0] !== this.id) + ) { + continue; + } + + // Check if the draggable's center coordinate is within + // the target's dimensions. If not, go to next target. + if ( + positionIE.top + this.iconHeight * 0.5 < + target.offset.top + ) { + continue; + } + if ( + positionIE.top + this.iconHeight * 0.5 > + target.offset.top + target.h + ) { + continue; + } + if ( + positionIE.left + this.iconWidth * 0.5 < + target.offset.left + ) { + continue; + } + if ( + positionIE.left + this.iconWidth * 0.5 > + target.offset.left + target.w + ) { + continue; + } + + // If we got here, then our draggable is on top of a + // target. + targetFound = true; + + // If the draggable was moved from one target to + // another, then we need to remove it's ID from the + // previous target's draggables list, and add it to the + // new target's draggables list. + if ( + (this.onTarget !== null) && + (this.onTarget.id !== target.id) + ) { + this.removeObjIdFromTarget(); + this.onTarget = target; + target.draggable.push(this.id); + } + // If the draggable was moved from the slider to a + // target, remember the target, and add ID to the + // target's draggables list. + else if (this.onTarget === null) { + this.onTarget = target; + target.draggable.push(this.id); + } + + if (target.numTextEl !== null) { + target.updateNumTextEl(); + } + + // Reposition the draggable so that it's center + // coincides with the center of the target. + this.snapToTarget(target); + + break; + } + + return targetFound; + } + + function snapToTarget(target) { + var offset; + + offset = 0; + if (this.state.config.targetOutline === true) { + offset = 1; + } + + this.iconEl.css( + 'left', + target.offset.left + 0.5 * target.w - + this.iconWidth * 0.5 + offset + - this.iconElLeftOffset + ); + this.iconEl.css( + 'top', + target.offset.top + 0.5 * target.h - + this.iconHeight * 0.5 + offset + ); + + if (this.labelEl !== null) { + this.labelEl.css( + 'left', + target.offset.left + 0.5 * target.w - + this.labelWidth * 0.5 + offset + - 9 // Acoount for padding, border. + ); + this.labelEl.css( + 'top', + target.offset.top + 0.5 * target.h + + this.iconHeight * 0.5 + 5 + offset + ); + } + } + + // Go through all of the draggables subtract 1 from the z-index + // of all whose z-index is higher than the old z-index of the + // current element. After, set the z-index of the current + // element to 1 + N (where N is the number of draggables - i.e. + // the highest z-index possible). + // + // This will make sure that after releasing a draggable, it + // will be on top of all of the other draggables. Also, the + // ordering of the visibility (z-index) of the other draggables + // will not change. + function correctZIndexes() { + var draggablesInMe, c1, c2, highestZIndex; + + if (this.state.individualTargets === true) { + if (this.onTarget.draggable.length > 0) { + draggablesInMe = []; + + for (c1 = 0; c1 < this.onTarget.draggable.length; c1 += 1) { + for (c2 = 0; c2 < this.state.draggables.length; c2 += 1) { + if ( + this.onTarget.draggable[c1] === + this.state.draggables[c2].id + ) { + draggablesInMe.push(this.state.draggables[c2]); + } + } + } + + highestZIndex = -10000; + + for (c1 = 0; c1 < draggablesInMe.length; c1 += 1) { + if ( + (draggablesInMe[c1].zIndex > highestZIndex) && + (draggablesInMe[c1].zIndex !== 1000) + ) { + highestZIndex = draggablesInMe[c1].zIndex; + } + } + + if (highestZIndex === -10000) { + highestZIndex = this.onTarget.draggable.length; + } else if (highestZIndex < this.oldZIndex) { + highestZIndex = this.oldZIndex; + } else { + for (c1 = 0; c1 < draggablesInMe.length; c1 += 1) { + draggablesInMe[c1].zIndex -= 1; + draggablesInMe[c1].oldZIndex -= 1; + + draggablesInMe[c1].iconEl.css( + 'z-index', + draggablesInMe[c1].zIndex + ); + if (draggablesInMe[c1].labelEl !== null) { + draggablesInMe[c1].labelEl.css( + 'z-index', + draggablesInMe[c1].zIndex + ); + } + } + } + } else { + highestZIndex = this.onTarget.draggable.length; + } + + this.zIndex = highestZIndex; + this.oldZIndex = highestZIndex; + + this.iconEl.css( + 'z-index', + this.zIndex + ); + if (this.labelEl !== null) { + this.labelEl.css( + 'z-index', + this.zIndex + ); + } + } else { + for (c1 = 0; c1 < this.state.draggables.length; c1++) { + if ( + this.oldZIndex < + this.state.draggables[c1].zIndex + ) { + this.state.draggables[c1].zIndex -= 1; + this.state.draggables[c1].oldZIndex = this.state.draggables[c1].zIndex; + this.state.draggables[c1].iconEl.css( + 'z-index', + this.state.draggables[c1].zIndex + ); + + if (this.state.draggables[c1].labelEl !== null) { + this.state.draggables[c1].labelEl.css( + 'z-index', + this.state.draggables[c1].zIndex + ); + } + } + } + + this.zIndex = c1; + this.oldZIndex = c1; + this.iconEl.css('z-index', c1); + + if (this.labelEl !== null) { + this.labelEl.css('z-index', c1); + } + } + } + + // If a draggable was released in a wrong positione, we will + // move it back to the slider, placing it in the same position + // that it was dragged out of. + function moveBackToSlider() { + this.containerEl.show(); + + this.zIndex = this.oldZIndex; + + this.iconEl.detach(); + this.iconEl.css('border', 'none'); + this.iconEl.css('background-color', 'transparent'); + this.iconEl.css('padding-left', 0); + this.iconEl.css('padding-right', 0); + this.iconEl.css('z-index', this.zIndex); + this.iconEl.css( + 'width', + this.iconWidthSmall + ); + this.iconEl.css( + 'height', + this.iconHeightSmall + ); + this.iconEl.css( + 'left', + 50 - this.iconWidthSmall * 0.5 + ); + if (this.labelEl !== null) { + this.iconEl.css('top', 5); + } else { + this.iconEl.css( + 'top', + 50 - this.iconHeightSmall * 0.5 + ); + } + + this.iconEl.appendTo(this.containerEl); + + if (this.labelEl !== null) { + this.labelEl.detach(); + this.labelEl.css('border', 'none'); + this.labelEl.css('background-color', 'transparent'); + this.labelEl.css('padding-left', 0); + this.labelEl.css('padding-right', 0); + this.labelEl.css('z-index', this.zIndex); + this.labelEl.css( + 'left', + 50 - this.labelWidth * 0.5 + ); + this.labelEl.css( + 'top', + 5 + this.iconHeightSmall + 5 + ); + this.labelEl.appendTo( + this.containerEl + ); + } + + this.inContainer = true; } }); diff --git a/common/static/js/capa/drag_and_drop/main.js b/common/static/js/capa/drag_and_drop/main.js index 24cc8cbdcb..6b1164b6df 100644 --- a/common/static/js/capa/drag_and_drop/main.js +++ b/common/static/js/capa/drag_and_drop/main.js @@ -63,7 +63,9 @@ define( Targets(state); Scroller(state); - Draggables(state); + Draggables.init(state); + + logme('After Draggables.init(state); state = ', state); // Update the input element, checking first that it is not filled with // an answer from the server. diff --git a/common/static/js/capa/drag_and_drop/targets.js b/common/static/js/capa/drag_and_drop/targets.js index 0bc246e49f..af27c186f2 100644 --- a/common/static/js/capa/drag_and_drop/targets.js +++ b/common/static/js/capa/drag_and_drop/targets.js @@ -119,15 +119,6 @@ define(['logme'], function (logme) { highestZIndex = -10000; lowestZIndex = 10000; - for (c1 = 0; c1 < draggablesInMe.length; c1 += 1) { - logme( - 'draggablesInMe[' + c1 + '].id = ' + draggablesInMe[c1].id, - 'draggablesInMe[' + c1 + '].zIndex = ' + draggablesInMe[c1].zIndex, - 'draggablesInMe[' + c1 + '].oldZIndex = ' + draggablesInMe[c1].oldZIndex - ); - } - logme('------------------'); - for (c1 = 0; c1 < draggablesInMe.length; c1 += 1) { if (draggablesInMe[c1].zIndex < lowestZIndex) { lowestZIndex = draggablesInMe[c1].zIndex; diff --git a/common/static/js/capa/drag_and_drop/update_input.js b/common/static/js/capa/drag_and_drop/update_input.js index 3b5053d5d2..14534f1240 100644 --- a/common/static/js/capa/drag_and_drop/update_input.js +++ b/common/static/js/capa/drag_and_drop/update_input.js @@ -10,6 +10,8 @@ define(['logme'], function (logme) { function updateInput(state, checkFirst) { var inputEl, stateStr, targets, draggables, c1, c2, tempObj; + logme('updateInput; state = ', state); + if (checkFirst === true) { if (checkIfHasAnswer() === true) { return; @@ -126,7 +128,7 @@ define(['logme'], function (logme) { return; } - draggable.setInContainer(false); + draggable.inContainer = false; draggable.containerEl.hide(); draggable.iconEl.detach(); @@ -196,7 +198,7 @@ define(['logme'], function (logme) { ); } - draggable.setOnTarget(target); + draggable.onTarget = target; target.draggable.push(draggableId); if (target.numTextEl !== null) { @@ -244,7 +246,7 @@ define(['logme'], function (logme) { return; } - draggable.setInContainer(false); + draggable.inContainer = false; draggable.containerEl.hide(); draggable.iconEl.detach();