How can I trigger a mousemove only if the element is clicked first?
I'm trying to utilize this for an audio player timeline.
.player__time--bar(#mousedown="setNewCurrentPosition($event)")
.slider(role="slider" aria-valuemin="0" :aria-valuenow="currentPosition" :aria-valuemax="trackTotalDuration" aria-orientation="horizontal")
.player__time--bar-current-position(:style="{width: (100 / (trackTotalDuration / currentPosition)) + '%'}")
The method:
setNewCurrentPosition(e) {
let tag = e.target
// if the click is not on 'slider', grab div with class 'slider'
if (e.target.className === 'player__time--bar') tag = e.target.firstElementChild
else if (e.target.className === 'player__time--bar-current-position') tag = e.target.parentElement
const pos = tag.getBoundingClientRect()
const seekPos = (e.clientX - pos.left) / pos.width
this.currentPosition = parseInt(this.trackTotalDuration * seekPos)
// updates the time in the html
this.$refs.player.currentTime = this.currentPosition
},
You will want to have a mousedown listener on your element that sets a variable to indicate dragging started. Put a listener on the window to catch mouseup anywhere and unset the variable.
You can put mousemove on the element if you are only interested in dragging that happens inside the element. Otherwise you can put the mousemove listener on window so you catch it everywhere.
new Vue({
el: '#app',
data: {
dragging: false,
x: 'no',
y: 'no'
},
methods: {
startDrag() {
this.dragging = true;
this.x = this.y = 0;
},
stopDrag() {
this.dragging = false;
this.x = this.y = 'no';
},
doDrag(event) {
if (this.dragging) {
this.x = event.clientX;
this.y = event.clientY;
}
}
},
mounted() {
window.addEventListener('mouseup', this.stopDrag);
}
});
.dragstartzone {
background-color: red;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<div class="dragstartzone" #mousedown="startDrag" #mousemove="doDrag">Start dragging here</div>
<div>X: {{x}}, Y: {{y}}</div>
</div>
I ended up using the code provided by Roy J, refactoring it a bit to fit my need. Here it is
Template:
.player__time--bar(#mousedown="startDrag($event)" #mouseup="stopDrag($event)" #mousemove="doDrag($event)")
.slider(role="slider" aria-valuemin="0" :aria-valuenow="currentPosition" :aria-valuemax="trackTotalDuration" aria-orientation="horizontal")
.player__time--bar-current-position(:style="{width: (100 / (trackTotalDuration / currentPosition)) + '%'}")
Data:
data: () => ({
currentPosition: 0,
trackTotalDuration: 0,
dragging: false
}),
Methods:
startDrag() {
this.dragging = true
},
stopDrag(event) {
this.dragging = false
this.setNewCurrentPosition(event)
},
doDrag(event) {
if (this.dragging) this.setNewCurrentPosition(event)
},
setNewCurrentPosition(e) {
let tag = e.target
// if the click is not on 'slider', grab div with class 'slider'
if (e.target.className === 'player__time--bar') tag = e.target.firstElementChild
else if (e.target.className === 'player__time--bar-current-position') tag = e.target.parentElement
const pos = tag.getBoundingClientRect()
const seekPos = (e.clientX - pos.left) / pos.width
this.currentPosition = parseInt(this.trackTotalDuration * seekPos)
// updates the time in the html
this.$refs.player.currentTime = this.currentPosition
},
Related
I create a game using javascript. The code is placed at the bottom of the page. Visualize it as if the game is below a footer. The game starts when the user selects the start game button. The mechanics of the game are controlled by the arrow key. Arrow keys are used to move up, right, and left down. The problem is when I press the up or down key, the page scrolls too. How to stop this? I want the page not to scroll when the game is active. I am attaching the code of the game I create. It is a snake game.There's CSS too, but I'm not attaching it as I don't think its relevant
<div id="app" class="app">
<div class="start-screen">
<h2>🦊 </h2>
<div class="options">
<h3>Choose Difficulty</h3>
<p class="end-score"></p>
<button data-difficulty="100" class="active">Easy</button>
<button data-difficulty="75">Medium</button>
<button data-difficulty="50">Hard</button>
</div>
<button class="play-btn">Play</button>
</div>
<canvas id="board"></canvas>
<div class="score">0</div>
</div>
<script>
class SnakeGame {
constructor() {
this.$app = document.querySelector('#app');
this.$canvas = this.$app.querySelector('canvas');
this.ctx = this.$canvas.getContext('2d');
this.$startScreen = this.$app.querySelector('.start-screen');
this.$score = this.$app.querySelector('.score');
this.settings = {
canvas: {
width: 500,
height: 500,
background: 'white',
border: 'black'
},
snake: {
size: 20,
background: '#73854A',
border: '#000'
}
};
this.game = {
// "direction" (set in setUpGame())
// "nextDirection" (set in setUpGame())
// "score" (set in setUpGame())
speed: 100,
keyCodes: {
38: 'up',
40: 'down',
39: 'right',
37: 'left'
}
}
this.soundEffects = {
score: new Audio('./sounds/score.mp3'),
gameOver: new Audio('./sounds/game-over.mp3')
};
this.setUpGame();
this.init();
}
init() {
// Choose difficulty
// Rather than using "this.$startScreen.querySelectorAll('button')" and looping over the node list
// and attaching seperate event listeners on each item, it's more efficient to just listen in on the container and run a check at runtime
this.$startScreen.querySelector('.options').addEventListener('click', event => {
this.chooseDifficulty(event.target.dataset.difficulty);
});
// Play
this.$startScreen.querySelector('.play-btn').addEventListener('click', () => {
this.startGame();
});
}
chooseDifficulty(difficulty) {
if(difficulty) {
this.game.speed = difficulty;
this.$startScreen.querySelectorAll('.options button').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
}
setUpGame() {
// The snake starts off with 5 pieces
// Each piece is 30x30 pixels
// Each following piece must be n times as far from the first piece
const x = 300;
const y = 300;
this.snake = [
{ x: x, y: y },
{ x: x - this.settings.snake.size, y: y },
{ x: x - (this.settings.snake.size * 2), y: y },
{ x: x - (this.settings.snake.size * 3), y: y },
{ x: x - (this.settings.snake.size * 4), y: y }
];
this.food = {
active: false,
background: '#EC5E0B',
border: '#73AA24',
coordinates: {
x: 0,
y: 0
}
};
this.game.score = 0;
this.game.direction = 'right';
this.game.nextDirection = 'right';
}
startGame() {
// Stop the game over sound effect if a new game was restarted quickly before it could end
this.soundEffects.gameOver.pause();
this.soundEffects.gameOver.currentTime = 0;
// Reset a few things from the prior game
this.$app.classList.add('game-in-progress');
this.$app.classList.remove('game-over');
this.$score.innerText = 0;
this.generateSnake();
this.startGameInterval = setInterval(() => {
if(!this.detectCollision()) {
this.generateSnake();
} else {
this.endGame();
}
}, this.game.speed);
// Change direction
document.addEventListener('keydown', event => {
this.changeDirection(event.keyCode);
});
}
changeDirection(keyCode) {
const validKeyPress = Object.keys(this.game.keyCodes).includes(keyCode.toString()); // Only allow (up|down|left|right)
if(validKeyPress && this.validateDirectionChange(this.game.keyCodes[keyCode], this.game.direction)) {
this.game.nextDirection = this.game.keyCodes[keyCode];
}
}
// When already moving in one direction snake shouldn't be allowed to move in the opposite direction
validateDirectionChange(keyPress, currentDirection) {
return (keyPress === 'left' && currentDirection !== 'right') ||
(keyPress === 'right' && currentDirection !== 'left') ||
(keyPress === 'up' && currentDirection !== 'down') ||
(keyPress === 'down' && currentDirection !== 'up');
}
resetCanvas() {
// Full screen size
this.$canvas.width = this.settings.canvas.width;
this.$canvas.height = this.settings.canvas.height;
this.$canvas.style.border = `3px solid ${this.settings.canvas.border}`;
// Background
this.ctx.fillStyle = this.settings.canvas.background;
this.ctx.fillRect(0, 0, this.$canvas.width, this.$canvas.height);
}
generateSnake() {
let coordinate;
switch(this.game.direction) {
case 'right':
coordinate = {
x: this.snake[0].x + this.settings.snake.size,
y: this.snake[0].y
};
break;
case 'up':
coordinate = {
x: this.snake[0].x,
y: this.snake[0].y - this.settings.snake.size
};
break;
case 'left':
coordinate = {
x: this.snake[0].x - this.settings.snake.size,
y: this.snake[0].y
};
break;
case 'down':
coordinate = {
x: this.snake[0].x,
y: this.snake[0].y + this.settings.snake.size
};
}
// The snake moves by adding a piece to the beginning "this.snake.unshift(coordinate)" and removing the last piece "this.snake.pop()"
// Except when it eats the food in which case there is no need to remove a piece and the added piece will make it grow
this.snake.unshift(coordinate);
this.resetCanvas();
const ateFood = this.snake[0].x === this.food.coordinates.x && this.snake[0].y === this.food.coordinates.y;
if(ateFood) {
this.food.active = false;
this.game.score += 10;
this.$score.innerText = this.game.score;
this.soundEffects.score.play();
} else {
this.snake.pop();
}
this.generateFood();
this.drawSnake();
}
drawSnake() {
const size = this.settings.snake.size;
this.ctx.fillStyle = this.settings.snake.background;
this.ctx.strokestyle = this.settings.snake.border;
// Draw each piece
this.snake.forEach(coordinate => {
this.ctx.fillRect(coordinate.x, coordinate.y, size, size);
this.ctx.strokeRect(coordinate.x, coordinate.y, size, size);
});
this.game.direction = this.game.nextDirection;
}
generateFood() {
// If there is uneaten food on the canvas there's no need to regenerate it
if(this.food.active) {
this.drawFood(this.food.coordinates.x, this.food.coordinates.y);
return;
}
const gridSize = this.settings.snake.size;
const xMax = this.settings.canvas.width - gridSize;
const yMax = this.settings.canvas.height - gridSize;
const x = Math.round((Math.random() * xMax) / gridSize) * gridSize;
const y = Math.round((Math.random() * yMax) / gridSize) * gridSize;
// Make sure the generated coordinates do not conflict with the snake's present location
// If so recall this method recursively to try again
this.snake.forEach(coordinate => {
const foodSnakeConflict = coordinate.x == x && coordinate.y == y;
if(foodSnakeConflict) {
this.generateFood();
} else {
this.drawFood(x, y);
}
});
}
drawFood(x, y) {
const size = this.settings.snake.size;
this.ctx.fillStyle = this.food.background;
this.ctx.strokestyle = this.food.border;
this.ctx.fillRect(x, y, size, size);
this.ctx.strokeRect(x, y, size, size);
this.food.active = true;
this.food.coordinates.x = x;
this.food.coordinates.y = y;
}
detectCollision() {
// Self collison
// It's impossible for the first 3 pieces of the snake to self collide so the loop starts at 4
for(let i = 4; i < this.snake.length; i++) {
const selfCollison = this.snake[i].x === this.snake[0].x && this.snake[i].y === this.snake[0].y;
if(selfCollison) {
return true;
}
}
// Wall collison
const leftCollison = this.snake[0].x < 0;
const topCollison = this.snake[0].y < 0;
const rightCollison = this.snake[0].x > this.$canvas.width - this.settings.snake.size;
const bottomCollison = this.snake[0].y > this.$canvas.height - this.settings.snake.size;
return leftCollison || topCollison || rightCollison || bottomCollison;
}
endGame() {
this.soundEffects.gameOver.play();
clearInterval(this.startGameInterval);
this.$app.classList.remove('game-in-progress');
this.$app.classList.add('game-over');
this.$startScreen.querySelector('.options h3').innerText = 'Game Over';
this.$startScreen.querySelector('.options .end-score').innerText = `Score: ${this.game.score}`;
this.setUpGame();
}
}
const snakeGame = new SnakeGame();
</script>
Stop the page from moving up and down when the game is active
You just need to prevent the default behaviour of these keys:
// Change direction
document.addEventListener('keydown', event => {
event.preventDefault(); // add this line
this.changeDirection(event.keyCode);
});
Note that this isn't a very good idea to do it that way, as it will prevent the default behaviour of all keypresses anywhere on your page. Most notably, F5 will never refresh the browser, and if you have other elements like buttons, links, etc, the standard keypresses to activate those will stop working (turning these off is a particular problem if you want your site to be accessible to those with disabilities).
You'd be better off checking the keyCode first and only preventing default if it corresponds to one of the arrow keys - which isn't so easy to do with your code as it stands but I'd encourage you to rewrite it that way.
Even doing that presents a problem to those who are not able to use a mouse, as (depending on the position of your game interface on the screen and its size) they may still need to scroll down and will be prevented from doing this by this preventDefault behaviour. But well that's what you asked for, and I've given you the solution - but with this important caveat.
I'm using a slideshow component from our library of components which gets swipe events from a utility function that's initialized on mount. The problem I'm facing is while testing on phone I found that once you tap on a link in a slide from that slideshow, a swipe event fires along with a click.
this is the util function:
`
function initSwipeEvents(el, deltaMin = 80) {
const swipeData = {
startX: 0,
startY: 0,
endX: 0,
endY: 0,
}
let directionEvents = []
el.addEventListener("touchstart", (e) => {
const t = e.touches[0]
swipeData.startX = t.screenX
swipeData.startY = t.screenY
})
el.addEventListener("touchmove", (e) => {
const t = e.touches[0]
swipeData.endX = t.screenX
swipeData.endY = t.screenY
})
el.addEventListener("touchend", () => {
const deltaX = swipeData.endX - swipeData.startX
const deltaY = swipeData.endY - swipeData.startY
if (Math.abs(deltaX) > deltaMin) {
if (deltaX > 0) directionEvents.push("right")
else directionEvents.push("left")
}
if (Math.abs(deltaY) > deltaMin) {
if (deltaY > 0) directionEvents.push("down")
else directionEvents.push("up")
}
directionEvents.forEach((direction) =>
el.dispatchEvent(new Event(`swipe-${direction}`))
)
directionEvents = []
})
}
export default initSwipeEvents
`
This is the slideshow component:
`
<div
class="slideshow"
tabindex="0"
#swipe-down="goToPrev"
#swipe-up="goToNext"
>
<div
v-show="activeSlideLogic(i, internalIdx)"
class="slide"
>
<slot
:slide="slide"
name="slide"
/>
</div>
</div>
<script>
mounted() {
if (this.swipeEvents) initSwipeEvents(this.$el) **// swipeEvents is just a boolean prop**
},
methods:{
getLoopedIdx(idx) {
if (this.wrap)
return (idx + this.slides.length) % this.slides.length
else return _clamp(idx, 0, this.slides.length - 1)
},
goToNext() {
this.$nextTick(
() =>
(this.internalIdx = this.getLoopedIdx(this.internalIdx + 1))
)
},
goToPrev() {
this.$nextTick(
() =>
(this.internalIdx = this.getLoopedIdx(this.internalIdx - 1))
)
},
}
</script>
`
This is the slide components that is used in the slideshow slot:
<div class="slide-video">
<wp-image **// this is a custom image component, shouldn't be causing the issue**
class="image"
:image="image"
>
<nuxt-link **// this is the link I'm referencing**
class="title-meta"
:to="to"
>
<div class="title-wrapper">
<svg-play class="svg-play" />
<h2
class="title"
v-html="title"
/>
</div>
</nuxt-link>
</wp-image>
</div>
I tried to prevent the default action by doing the folowing:
`
function initSwipeEvents(el, deltaMin = 80) {
const swipeData = {
startX: 0,
startY: 0,
endX: 0,
endY: 0,
}
let directionEvents = []
el.addEventListener("touchstart", (e) => {
const t = e.touches[0]
swipeData.startX = t.screenX
swipeData.startY = t.screenY
// added
console.log(e.target);
if( e.target.matches('.title-meta') ||
e.target.matches('.title-meta *')){
console.log("this is a link");
e.preventDefault();
}
})
el.addEventListener("touchmove", (e) => {
const t = e.touches[0]
swipeData.endX = t.screenX
swipeData.endY = t.screenY
// added
console.log(e.target);
if( e.target.matches('.title-meta') ||
e.target.matches('.title-meta *')){
console.log("this is a link");
e.preventDefault();
}
})
el.addEventListener("touchend", () => {
const deltaX = swipeData.endX - swipeData.startX
const deltaY = swipeData.endY - swipeData.startY
if (Math.abs(deltaX) > deltaMin) {
if (deltaX > 0) directionEvents.push("right")
else directionEvents.push("left")
}
if (Math.abs(deltaY) > deltaMin) {
if (deltaY > 0) directionEvents.push("down")
else directionEvents.push("up")
}
directionEvents.forEach((direction) =>
el.dispatchEvent(new Event(`swipe-${direction}`))
)
directionEvents = []
})
}
export default initSwipeEvents
`
I also tried the css property, touch-action: none; on the link in the slide, to no avail
I expected to be able to click on the link without triggering the swipe event.
I'm not sure where to implement the change, whether on the util or on the slideshow it's self or if the change I implemented is correct.
Edit:
I have tried adding
goToNext(e) {
console.log(e)
this.$nextTick(
() =>
(this.internalIdx = this.getLoopedIdx(this.internalIdx + 1))
)
},
goToPrev(e) {
console.log(e)
this.$nextTick(
() =>
(this.internalIdx = this.getLoopedIdx(this.internalIdx - 1))
)
},
...on the slideshow its self, the console.log is always logging that I'm targeting the div "target: div.slideshow", even when clicking on the link.
While if I log the onWheel method, which listens to the wheel event (it isn't being imported form a utilty), I', getting the expected result.
I think the util might not have the correct scope it needs, as it;s bound to the $el, which is slideshow.
I found out the solution on my own.
Basically what was happening is that the utility function listens for the touchStart, touchMove and touchEnd events.
But if only a tap of a finger happens, there is no touchMove event firing.
TouchEnd event is subtracting touchStart and touchMove data (screenX & screenY).
So result is always bigger then deltaMin and the custom swipe event is triggered.
The solution is to take out the touchMove event and add its logic to touchEnd event.
I am using the interact.js library for drag-and-dropping elements inside a grid. Once it is inside my grid I want to be able to resize them which is working, but what is the recommend way for preventing two resizable elements from overlapping. I basically want to stop the resizable move listener when it hits another resizable element. I already know when one resizable elements hits another, But how do I prevent the user from being able to overlap them?
full function for resizing elements:
function resizeZone(target, parentContainer, minHeight, width) {
if (interact.isSet(target) == false) {
interact(target)
.resizable({
edges: { top: true, left: false, bottom: true, right: false },
listeners: {
start: function (event) {
startDataSet = event.target.dataset;
startDeltaRect = event.deltaRect;
startRect = event.rect;
},
move: function (event) {
var elementsAtDragLocation = document.elementsFromPoint(event.x0, event.client.y);
for (let elementId in elementsAtDragLocation) {
var result = elementsAtDragLocation[elementId].getAttribute('free-schedule');
var isFalseSet = (result === 'false');
if (isFalseSet) {
let dragLocationRoutineId = elementsAtDragLocation[elementId].id;
let routineId = event.target.id;
// if user drags element past other element they will overlap, because the isFalseSet check won't work =(
if (dragLocationRoutineId !== routineId) {
return;
}
}
}
let { x, y } = event.target.dataset
x = (parseFloat(x) || 0) + event.deltaRect.left
y = (parseFloat(y) || 0) + event.deltaRect.top
Object.assign(event.target.style, {
width: `${event.rect.width}px`,
height: `${event.rect.height}px`,
transform: `translate(${x}px, ${y}px)`
});
Object.assign(event.target.dataset, { x, y });
},
end: async function (event) {
let routineId = event.target.id;
ResizeTaskTrigger(routineId, event.rect.height, event.deltaRect);
},
},
modifiers: [
interact.modifiers.restrictSize({
min: { width: width, height: minHeight },
}),
interact.modifiers.restrictRect({
restriction: parentContainer
}),
],
});
}
}
Hi i'm facing the very challenging & interesting problem of scroll during selection of items with mouse drag in both direction i,e up and down
here is a screen shot
Here is my code : https://codesandbox.io/s/select-ivwq8j?file=/src/overridden/Drag-select.vue
Drag-select.vue is the file where drag selection logic is written.
which fires change when files selection gets changed.
I receive those change event here <drag-select-container #change="dragSelect($event)">
Edit 1: after IVO GELO comment
I have added inside drag() function
try{
let containerEl = document.querySelector('#wrapping_container');
let container = containerEl.getBoundingClientRect();
if(box.top > (container.top )){
containerEl.scrollTop = box.top - 50;
return true;
}
}catch(e){
console.log(e);
}
Edit code here: https://codesandbox.io/s/select-ivwq8j?file=/src/overridden/Drag-select.vue
It is very interesting and challenging problem so
Please help me thanks in advance!!
I recommend you use DragSelect js library.
Working Demo
https://codesandbox.io/s/select-forked-tnmnwk?file=/src/components/HelloWorld.vue
mounted() {
const vm = this;
const ds = new DragSelect({
selectables: document.querySelectorAll(".selectable-nodes"),
area: document.getElementById("area"),
draggability: false,
});
ds.subscribe("elementselect", function ({ item }) {
vm.selectedItems.push();
});
ds.subscribe("elementunselect", function ({ item }) {
const index = vm.selectedItems.indexOf(item.getAttribute("customAttribute"));
if (index > -1) {
vm.selectedItems.splice(index, 1);
}
});
}
I found a solution for your question. I rewrite your code in a completely different way. Here is a demo that you can test it. Actually it contains two main component. Parent component that is called "HelloCompo" and its code comes here:
HelloCompo:
<template>
<!-- This is a component that uses "MyDrag" component. -->
<div id="wrapping_container" class="hello">
<!-- Here we insert "MyDrag" component that emits custom "change" event when the selection of element is changed according to user drag. -->
<my-drag #change="dragSelect">
<div
class="item"
:class="{ selected: ( index >= minMax[1] && index <= minMax[0] ) }"
:key="item"
v-for="(item, index) in [1, 2, 3, 4,5,6,7,8,9,10,11,12,13,14,15,16]"
>
{{ item }}
</div>
</my-drag>
</div>
</template>
<script>
import MyDrag from "./MyDrag";
export default {
name: "HelloCompo",
components: {MyDrag},
data() {
return {
selectedItems: [],
};
},
computed: {
minMax: function () {
/* This computed property uses data returned by "MyDrag" component to define the maximum and minimum range to accept "selected" class. */
let max = -1;
let min = -1;
if (this.selectedItems.length > 0) {
max = Math.max(...this.selectedItems);
min = Math.min(...this.selectedItems);
}
return [max-1, min-1]
}
},
methods: {
dragSelect: function (selectedList) {
// console.log(selectedList);
/* this Method is used to set "selectedItems" data after each change in selected drag. */
this.selectedItems = selectedList;
}
},
}
</script>
<style scoped>
.item {
display: block;
width: 230px;
height: 130px;
background: orange;
margin-top: 9px;
line-height: 23px;
color: #fff;
}
.selected {
background: red !important;
}
#wrapping_container{
background:#e7e7e7;
}
</style>
And child component that is called "MyDrag":
MyDrag:
<template>
<section id="parentAll">
<!-- I used "#mousedown" and ... for calling methods instead of using all functions in mounted hook. -->
<div class="minHr" ref="container" #mousedown="startDrag" #mouseup="endDrag" #mousemove="whileDrag">
<slot></slot>
</div>
<!-- This canvas is shown only when the user is dragging on the page. -->
<canvas ref="myCanvas" v-if="showCanvas" #mouseup="endDrag" #mousemove="whileDrag"></canvas>
</section>
</template>
<script>
export default {
name: "MyDrag",
data() {
return {
dragStatus: false, // used for detecting mouse drag
childrenArr: [], // used to store the information of children of 'ref="container"' that comes from "slot"
startEvent: null, // used to detect mouse position on mousedown
endEvent: null, // used to detect mouse position on mouseup
direction: "topBottom", // used to detect the direction of dragging
selectedArr: [], // used to store the selected "divs" after dragging
heightContainer: null, // used to detect the height of 'ref="container"' dynamically
widthContainer: null, // used to detect the width of 'ref="container"' dynamically
/* These data used to draw rectangle on canvas while the user is dragging */
rect: {
startX: null,
startY: null,
w: null,
h: null
},
startDragData: {
x: null,
y: null
},
whileDragData: {
x: null,
y: null,
CLY: null
},
showCanvas: false // used to show or hide <canvas></canvas>
}
},
methods: {
childrenInfo: function () {
/* This method is called on "mounted()" hook to gather information about children of 'ref="container"' that comes from <slot></slot> */
const { container } = this.$refs;
const stylesDiv = window.getComputedStyle(container, null);
this.widthContainer = parseFloat( stylesDiv.getPropertyValue("width") );
this.heightContainer = parseFloat( stylesDiv.getPropertyValue("height") );
let children = container.childNodes;
children.forEach((item, index) => {
let childObj = {
offsetTop: item.offsetParent.offsetTop + item.offsetTop,
offsetHeight: item.offsetHeight
}
this.childrenArr.push(childObj);
})
},
startDrag: function (event) {
/* This method is called at mousedown and detect the click or right click. after that it sets some data like "showCanvas". */
if(event.button === 0) {
this.dragStatus = true;
this.startEvent = event.pageY;
this.startDragData.x = event.pageX;
this.startDragData.y = event.pageY;
this.showCanvas = false;
}
},
whileDrag: async function (event) {
/* This method is called when the user is dragging. Because I want to be confident about showing <canvas> before doing other parts of code, I used "async" function for this method. */
if (this.dragStatus) {
await this.showMethod();
console.log("dragging");
this.whileDragData.x = event.pageX;
this.whileDragData.y = event.pageY;
this.whileDragData.CLY = event.clientY
await this.canvasMethod();
} else {
this.showCanvas = false;
}
},
endDrag: function (event) {
/* This method is called at mouseup. After that it calls other methods to calculate the "divs" that were selected by user. */
if(event.button === 0) {
console.log("end drag");
this.dragStatus = false;
this.showCanvas = false;
this.endEvent = event.pageY;
this.calculateDirection();
this.calculateSelected();
}
},
showMethod: function () {
/* This method is used to set "showCanvas" data at proper time. */
this.showCanvas = true;
},
calculateDirection: function () {
/* This method is used to detect the direction of dragging. */
if (this.startEvent <= this.endEvent) {
this.direction = "topBottom";
} else {
this.direction = "bottomTop";
}
},
calculateSelected: function () {
/* This method is responsible to find out which "divs" were selected while the user was dragging. After that it emits "this.selectedArr" data to the parent component. */
this.selectedArr = [];
let endIndex = null;
let startIndex = null;
this.childrenArr.forEach( (item, index) => {
if ( (item.offsetTop < this.endEvent) && ( (item.offsetTop + item.offsetHeight) > this.endEvent) ) {
endIndex = index;
console.log(endIndex);
}
if ( (item.offsetTop < this.startEvent) && ( (item.offsetTop + item.offsetHeight) > this.startEvent) ) {
startIndex = index;
console.log(startIndex);
}
});
if( endIndex !== null ) {
if (this.direction === "topBottom") {
for (let i = startIndex; i <= endIndex; i++ ) {
this.selectedArr.push(i+1);
}
} else {
for (let i = startIndex; i >= endIndex; i-- ) {
this.selectedArr.push(i+1);
}
}
}
this.$emit("change", this.selectedArr);
},
canvasMethod: function () {
/* This method is used to show a rectangle when user drags on page. It also could understand that the user is near the top or bottom of page, and then it scrolls the page when the user is dragging. */
const { myCanvas } = this.$refs;
myCanvas.width = this.widthContainer;
myCanvas.height = this.heightContainer;
const html = document.documentElement;
let ctx = myCanvas.getContext('2d');
this.rect.startX = this.startDragData.x - myCanvas.offsetParent.offsetLeft;
this.rect.startY = this.startDragData.y - myCanvas.offsetParent.offsetTop;
this.rect.w = (this.whileDragData.x - myCanvas.offsetParent.offsetLeft) - this.rect.startX;
this.rect.h = (this.whileDragData.y - myCanvas.offsetParent.offsetTop) - this.rect.startY ;
if ( Math.abs(this.whileDragData.CLY - window.innerHeight) < 12) {
console.log("near");
html.scrollTop += 25;
}
if ( Math.abs(this.whileDragData.CLY) < 12 ) {
html.scrollTop -= 25;
}
if ( (this.whileDragData.y > (myCanvas.offsetParent.offsetTop + myCanvas.offsetHeight) - 25) || (this.whileDragData.y < myCanvas.offsetParent.offsetTop + 25) ) {
ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
}
ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
ctx.setLineDash([6]);
ctx.strokeRect(this.rect.startX, this.rect.startY, this.rect.w, this.rect.h);
},
},
mounted() {
this.childrenInfo();
}
}
</script>
<style scoped>
.minHr {
min-height: 900px;
}
#parentAll {
position: relative;
}
#parentAll canvas {
position: absolute;
top: 0;
left: 0;
}
</style>
I used a <canvas> to draw rectangle when the user is dragging. The main difference of my code with your is that it shows the selected items after the dragging process was finished. It works in both upward dragging and downward dragging and also when the user is want to continue dragging beyond the window area (scrolling).
I am adding a resize functionality following a codepen example
mentioned in the comments using MARGIN=4 (here I was not able to add the codepen link)
The codepen was the best working example which I could find.
Have added 4 elements on all the 4 edges of the box.
I Have added hostlisteners to pointerdown , pointermove and pointerup but I am stuck with executing the animate() function present in Resizable directive.
In the directive the code has mainly 3 functions doing all the calculations onDown() , calc(e), animate()
import { Directive,
ElementRef,
HostListener } from '#angular/core';
import { DraggableDirective } from './draggable.directive';
#Directive({
selector: '[appResizable]' // Attribute selector
})
export class ResizableDirective extends DraggableDirective{
constructor(public element:ElementRef){
super(element);
}
minWidth = 60;
minHeight = 40;
// Thresholds
MARGINS = 4;
//End Of whats configurable
clicked = null;
public onRightEdge; onBottomEdge; onLeftEdge; onTopEdge;
public b; x; y;
redraw = false;
e;
clickedDragging = false;
ngOnInit(){
this.animate()
}
#HostListener('dragStart', ['$event'])
onDragStart(e:PointerEvent): void{
this.clickedDragging = true;
this.onDown(e);
e.preventDefault();
}
#HostListener('dragMove', ['$event'])
onDragMove(ee): void{
if (!this.dragging || ee.pointerId !== this.pointerId) {
return;
}
if((<HTMLElement>event.srcElement).id === ('side-top')){
this.onMove(ee);
ee.preventDefault();
}
}
#HostListener('dragEnd', ['$event'])
ondragend(ev): void{
this.onUp(ev);
}
onMove(ee){
if (!this.dragging || ee.pointerId !== this.pointerId) {
return;
}
if(this.clickedDragging){
this.calc(ee);
this.e = ee;
this.redraw = true;
}
}
onDown(e){
this.calc(e);
let isResizing = this.onRightEdge || this.onBottomEdge ||
this.onLeftEdge || this.onTopEdge;
this.clicked = {
x: this.x,
y: this.y,
cx: e.clientX,
cy: e.clientY,
w: this.b.width,
h: this.b.height,
isResizing: isResizing,
onRightEdge: this.onRightEdge,
onBottomEdge: this.onBottomEdge,
onLeftEdge: this.onLeftEdge,
onTopEdge: this.onTopEdge,
}
}
calc(e){
this.b = this.element.nativeElement.getBoundingClientRect();
this.x = e.clientX - this.b.left;
this.y = e.clientY - this.b.top;
this.onRightEdge = this.x >= this.b.width - this.MARGINS;
this.onBottomEdge = this.y >= this.b.height - this.MARGINS;
this.onLeftEdge = this.x < this.MARGINS;
this.onTopEdge = this.y < this.MARGINS;
}
animate(){
requestAnimationFrame(this.animate);
if(!this.redraw)return;
this.redraw = false;
if(this.clicked && this.clicked.isResizing){
if(this.clicked.onRightEdge){
this.element.nativeElement.style.width = Math.max(this.x,
this.minWidth) + 'px';
}
if(this.clicked.onBottomEdge){
this.element.nativeElement.style.height = Math.max(this.y,
this.minHeight) + 'px';
}
if(this.clicked.onLeftEdge){
let currentWidth = Math.max(this.clicked.cx -
this.e.clientX + this.clicked.w, this.minWidth);
if(currentWidth > this.minWidth){
this.element.nativeElement.style.width = currentWidth + 'px';
this.element.nativeElement.style.left = this.e.clientX + 'px';
}
}
if (this.clicked.onTopEdge) {
var currentHeight = Math.max(this.clicked.cy -
this.e.clientY +
this.clicked.h, this.minHeight);
if (currentHeight > this.minHeight) {
this.element.nativeElement.style.height = currentHeight + 'px';
this.element.nativeElement.style.top = this.e.clientY + 'px';
}
}
return;
}
}
onUp(ev) {
this.calc(ev);
this.clicked = null;
}
}
HTML snippet on which directive has been applied
<div class="box" *ngFor="let box of dropzone1" appDroppable (dragStart)="currentBox = box" appMovable>
{{ box.dis }}
<div class="side side-h" id="side-top" (dragStart)=(e) (dragMove)=(e) (dragEnd)=(e) appResizable></div>
<div class="side side-v" id="side-right" (click)="clcikme(e)" ></div>
<div class="side side-h" id="side-bottom" (click)="clcikme(e)"></div>
<div class="side side-v" id="side-left" (click)="clcikme(e)"></div>
</div>
In the codepen example for b
this.b = this.element.nativeElement.getBoundingClientRect();
the whole element has been taken which I'll also have to do, in my case I have the border on which the directive is there
I am attaching a working stackblitz the resizable directive is present in draggable folder and used in hello component
In the stackblitz console logging of pointerdown, pointermove can also be seen.
https://stackblitz.com/edit/angular-pcpev1?file=src/app/hello.component.html
Error in console animate of undefined
Please refer to the codepen example from the comments.
Update
https://stackblitz.com/edit/angular-pcpev1?file=src%2Fapp%2Fhello.component.html
Sorry I can't fix your code because it's a mess, you just do this completely wrong, you just did two approaches I suggest in one code. Also you're leaking resize internals outside of directive, the divs need to be hidden, outside you only should use resize attribute and everything should be created inside directive.
Here, this is started for one top side:
#Directive({
selector: '[resize]'
})
class Resizable implements OnInit, OnDestroy {
private nodes: HtmlElement[] = [];
private data: {x: number, y: number, rect: ClientRect, direction: string};
constructor(#Inject(ElementRef) private element: ElementRef) {
this.mousemove = this.mousemove.bind(this);
this.mouseup = this.mouseup.bind(this);
}
mousemove(e) {
if (this.data) {
switch(this.data.direction) {
case 'top':
var offset = this.data.y - e.clientY;
var height = this.data.rect.height;
var top = this.data.rect.top;
var style = this.element.nativeElement.style;
style.height = height + offset + 'px';
style.top = top - offset + 'px';
break;
}
}
}
ngOnInit() {
var node = document.createElement('div');
node.classList.add('border-top', 'border');
this.element.nativeElement.appendChild(node);
this.nodes.push(node);
window.addEventListener('mousemove', this.mousemove);
window.addEventListener('mouseup', this.mouseup);
}
#HostListener('mousedown', ['$event'])
mousedown(e) {
if (e.target.classList.contains('border')) {
var rect = this.element.nativeElement.getBoundingClientRect();
this.data = {
x: e.clientX,
y: e.clientY,
rect,
direction: e.target.className.match(/border-([^ ]+)/)[1]
};
e.preventDefault();
} else {
delete this.data;
}
}
mouseup(e) {
delete this.data;
}
ngOnDestroy() {
this.nodes.forEach(n => n.remove());
window.removeEventListener('mousemove', this.mousemove);
window.removeEventListener('mouseup', this.mouseup);
}
}
here is CodePen demo