Which event do I need to capture to catching a mouse move while mousedown with Vue - mouseevent

I'm building a 2D game with a top down map using Vue3. I created a simple map editor, where you can drag each tile from a set to the map. This can get very cumbersome for a large map, thus I want to try another way. My idea is like a drawing tool, you select the "brush" and drag it over a "canvas". The only problem is, the map is built of tile div arranged in a matrix. I tried the following, but for some reason the drawing is delayed and it generates "artifacts" on other tiles (I start dragging on tile 20:4 but also tile 15:2 is updated.
<div class="tiles">
<div v-for="(row, y) in tiles" class="row"
:key="'row_' + y"
:style="{width: mapSize.x + 'px'}">
<div v-for="(tile, x) in row" class="tile"
:class="tile.background"
:key="'tile_' + x + '_' + y"
#dragover.prevent
#dragenter="onDragEnter(x, y)">
</div>
</div>
</div>
I don't have a draggable element, maybe this is the problem, but what other event could I use for a clicked move?

I would suggest that you use mousedown, mousemove, and mouseup and possibly their touch semi-equivalents (if using touch devices).
Here is an example, with comments in the code.
const colors = ["powderblue","gold","orangered","deeppink", "deepskyblue","palegreen"];
const generateTiles = (wx, wy) => {
const ret = [];
for (let y = 0; y < wy; y++) {
const row = [];
ret.push(row)
for (let x = 0; x < wx; x++) {
row.push({x,y,background: colors[0]})
}
}
return ret;
}
Vue.createApp({
el: '#app',
setup() {
const tiles = Vue.reactive(generateTiles(10,10)); // setup tiles and make reactive
const color = Vue.ref(1); // helper for managing color(s)
let mouseDown = false; // no need to make reactive
// do some stuff to the tile here
const setTileBg = (tile) => {
tile.background=colors[color.value];
}
const handlers = {
// reuse handler, used both for parent and
onMouseDown: (tile) => {
mouseDown = true;
if(tile) setTileBg(tile);
},
onMouseUp: (e) => {
mouseDown = false;
},
onMouseOver: (tile) => {
if(mouseDown) setTileBg(tile);
}
}
// adding it to window (instead of an element) allows
// capturing mouseup event even when mouse is not within
// the dom element
window.addEventListener('mouseup', handlers.onMouseUp)
return {
tiles, color, colors, ...handlers
}
}
}).mount('#app')
.tiles,select{width:200px;margin:0 auto;display:block}.tile{display:inline-block;width:20px;height:20px;border:1px solid #000;box-sizing:border-box;font-size:10px;font-family:monospace;cursor:pointer;color:rgba(0,0,0,.4);text-align:center;padding:3px}.row{display:block;height:20px;width:200px;box-sizing:content-box;user-select:none}
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<div id="app">
<div>
<select v-model="color">
<option :value="i" v-for="(c,i) in colors" :style="{'background-color':c}">{{c}}</option>
</select>
</div>
<div class="tiles">
<div v-for="(row, y) in tiles" class="row"
:key="'row_' + y"
>
<div v-for="(tile, x) in row" class="tile"
:style="{'background-color':tile.background}"
:key="'tile_' + x + '_' + y"
#mouseDown="onMouseDown(tile)"
#mouseOver="onMouseOver(tile)">
{{ y * row.length + x}}
</div>
</div>
</div>
</div>

Related

Vue.js : Range slider with two handles

I want to create a vue js components where it contains a range slider of hours with two handles.
I use vue3 + vite.js
I tried this code to implement the components but when I drag one of handles I have an error
Code :
this is the template :
<template>
<div>
<input type="range" ref="rangeInput" v-model="rangeValue" #input="updateRange"/>
<div class="range-slider">
<div class="handle" :style="{left: leftHandle + '%'}" #mousedown="startHandleDrag(1)">
{{ formatHour(rangeValue[0]) }}
</div>
<div class="handle" :style="{left: rightHandle + '%'}" #mousedown="startHandleDrag(2)">
{{ formatHour(rangeValue[1]) }}
</div>
</div>
</div>
</template>
and this is the script :
<script>
export default {
data() {
return {
rangeValue: [8, 18],
handleDragging: 0
};
},
computed: {
leftHandle() {
return this.rangeValue[0];
},
rightHandle() {
return this.rangeValue[1];
}
},
methods: {
updateRange(event) {
const value = event.target.value;
const range = this.rangeValue;
if (this.handleDragging === 1) {
range[0] = value[0];
} else if (this.handleDragging === 2) {
range[1] = value[1];
} else {
range[0] = value[0];
range[1] = value[1];
}
this.rangeValue = range;
},
startHandleDrag(handle) {
this.handleDragging = handle;
document.addEventListener("mouseup", this.stopHandleDrag);
document.addEventListener("mousemove", this.updateRange);
},
stopHandleDrag() {
this.handleDragging = 0;
document.removeEventListener("mouseup", this.stopHandleDrag);
document.removeEventListener("mousemove", this.updateRange);
},
formatHour(value) {
return value + ":00";
}
}
};
</script>
Error :
any ideas to solve it !!!
In your startHandleDrag() and stopHandleDrag(), you bind updateRange() to the mousemove event:
document.addEventListener("mousemove", this.updateRange);
There are two issues with that:
The target of the mousemove event is the element under the cursor. This can be any element, and unless it happens to be an input, it will not have a value attribute (and if it does, it will not hold an array). If you really want to use the "mousemove" event, use the cursor coordinates like pageX or pageX.
You bind it as a function pointer (addEventListener("mousemove", this.updateRange)), and when called from the listener, this will refer to element.target. To avoid this, either use an arrow function (addEventListener("mousemove", (e) => this.updateRange(e))) or bind this (addEventListener("mousemove", this.updateRange.bind(this))).
I don't fully understand what you want to do with the handles, but my guess is that adding and removing listeners is a workaround, and you actually want to make them draggable? If so, have a look at the drag event. Hope that helps!

Infinite scroll not working in old Nebular site

I have a website which is based on an old version of ngx-admin (Nebular) which i am assuming version 2.1.0.
the new nebular documentation does not seem to apply to my website, so i am trying to add an infinite loop functionality using ngx-infinite-scroll, but the scroll event is not fired.
my example which i try to apply is taken from (https://stackblitz.com/edit/ngx-infinite-scroll?file=src%2Fapp%2Fapp.module.ts)
my component:
//our root app component
import { Component } from "#angular/core";
#Component({
selector: "my-app",
styleUrls: ["./test.component.scss"],
templateUrl: "./test.component.html"
})
export class TestComponent {
array = [];
sum = 100;
throttle = 300;
scrollDistance = 1;
scrollUpDistance = 2;
direction = "";
modalOpen = false;
constructor() {
console.log("constructor!!");
this.appendItems(0, this.sum);
}
addItems(startIndex, endIndex, _method) {
for (let i = 0; i < this.sum; ++i) {
this.array[_method]([i, " ", this.generateWord()].join(""));
}
}
appendItems(startIndex, endIndex) {
this.addItems(startIndex, endIndex, "push");
}
prependItems(startIndex, endIndex) {
this.addItems(startIndex, endIndex, "unshift");
}
onScrollDown(ev) {
console.log("scrolled down!!", ev);
// add another 20 items
const start = this.sum;
this.sum += 20;
this.appendItems(start, this.sum);
this.direction = "down";
}
onUp(ev) {
console.log("scrolled up!", ev);
const start = this.sum;
this.sum += 20;
this.prependItems(start, this.sum);
this.direction = "up";
}
generateWord() {
return "Test Word";
}
toggleModal() {
this.modalOpen = !this.modalOpen;
}
}
my html:
<h1 class="title well">
ngx-infinite-scroll v-{{nisVersion}}
<section>
<small>items: {{sum}}, now triggering scroll: {{direction}}</small>
</section>
<section>
<button class="btn btn-info">Open Infinite Scroll in Modal</button>
</section>
</h1>
<div class="search-results"
infinite-scroll
[infiniteScrollDistance]="scrollDistance"
[infiniteScrollUpDistance]="scrollUpDistance"
[infiniteScrollThrottle]="throttle"
(scrolled)="onScrollDown()"
(scrolledUp)="onUp()" style="height:100%">
<p *ngFor="let i of array">
{{ i }}
</p>
</div>
Any hints how i can get the scroll event to fire?
Well, finally i got it.
the tricky part of ngx-admin was to findout that one needs to add withScroll="false" to nb-layout in the theme file.
then in the component file this is what worked for me:
#HostListener("window:scroll", ["$event"])
onWindowScroll() {
//In chrome and some browser scroll is given to body tag
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
console.log("scrolled");
}
}
How to detect scroll to bottom of html element
maybe this will help someone

Conditionally rendering icons only one time depending on property

I am trying to write a template that displays either Icon AAA or Icon BBB, depending on whether or not the item in the current iteration has a specific flag. Here is my code:
<div v-for="(item, itemIndex) in items">
<div v-if="item.hasUnreadComments">
<span>Display Icon AAA</span>
</div>
<div v-else>
<span>Display Icon BBB</span>
</div>
</div>
The issue here is that I need either icon displayed ONCE. If more than one item has it set to item.hasUnreadComments === true, Icon AAA will be displayed equally as many times, which is not what I want. Couldnt find anything in the docs and I dont want to bind it to a v-model.
Can this be done in Vue without a third data variable used as a flag?
You will have to do some sort of intermediate transformation. v-if is just a flexible, low-level directive that will hide or show an element based on a condition. It won't be able to deal directly with how you expect the data to come out.
If I'm understanding what you're asking for, you want an icon to only be visible once ever in a list. You can prefilter and use an extra "else" condition. This sounds like a use case for computed properties. You define a function that can provide a transformed version of data when you need it.
This example could be improved upon by finding a way to boil down the nested if/elses but I think this covers your use case right now:
const app = new Vue({
el: "#app",
data() {
return {
items: [{
hasUnreadComments: true
},
{
hasUnreadComments: true
},
{
hasUnreadComments: false
},
{
hasUnreadComments: true
},
{
hasUnreadComments: false
},
]
}
},
computed: {
filteredItems() {
let firstIconSeen = false;
let secondIconSeen = false;
return this.items.map(item => {
if (item.hasUnreadComments) {
if (!firstIconSeen) {
item.firstA = true;
firstIconSeen = true;
}
} else {
if (!secondIconSeen) {
item.firstB = true;
secondIconSeen = true;
}
secondIconSeen = true;
}
return item;
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, itemIndex) in filteredItems">
<div v-if="item.firstA">
<span>Display Icon AAA</span>
</div>
<div v-else-if="item.firstB">
<span>Display Icon BBB</span>
</div>
<div v-else>
<span>Display no icon</span>
</div>
</div>
</div>

Add event listeners in VueJS 2

I am trying to add event listeners to my viewmodel once VueJS is loaded. Adding event listeners works if I do not use VueJS, so I know the code is correct but they never attach in VueJS.
<div id="app">
<div name="pageContent" id="preview">
<section class="row">
<div class="columns medium-12">
<h1>This is the top content</h1>
<p>ashcbaubvdiuavduabd</p>
</div>
</section>
<section class="row">
<div class="columns medium-6">
<h1>This is left content</h1>
<p>ashcbaubvdiuavduabd</p>
</div>
<div class="columns medium-6">
<h1>This is the right content</h1>
<p>ashcbaubvdiuavduabd</p>
</div>
</section>
</div>
</div>
<script type="text/javascript">
let editorContainer = document.getElementById('preview')
let controls = document.getElementById('defaultControls')
let cmsEditor = new CmsEditor(editorContainer, controls)
var app = new Vue({
el: '#app',
data: {
editor: cmsEditor
},
mounted: function () {
// wire up our listeners
console.log('mounted')
document.oncontextmenu = function () { return false }
let rows = this.editor.EditorContainer.getElementsByTagName('section')
for (var i = 0; i < rows.length; i++) {
console.log('section ' + i + ' : ' + rows[i].innerHTML)
rows[i].addEventListener('mouseover', function () {
console.log('mouse over event')
this.editor.SetActiveRow(this)
})
rows[i].addEventListener('dblclick', function () {
this.editor.DisplayContextMenu(this)
})
}
},
methods: {
save: function () {
console.log('save')
this.editor.Save()
},
undo: function () {
console.log('undo')
this.editor.Undo()
}
}
})
</script>
Looks like you are creating the editor on elements that will be removed from the DOM. Vue uses the content of #app as it's template, compiles the template into a render function, then replaces the DOM with the results of the render function. Given that editor is created on DOM elements that are gone now, I expect the code would fail.
You probably want to move the creation of the editor into mounted, then set up your event listeners.
FWIW, I also think you have the this issue mentioned by the commenters.
I think it should be something like this:
mounted: function() {
let editorContainer = document.getElementById('preview');
let controls = document.getElementById('defaultControls');
this.editor = new CmsEditor(editorContainer, controls);
// wire up our listeners
console.log('mounted')
document.oncontextmenu = function () { return false; };
let rows = this.editor.EditorContainer.getElementsByTagName("section");
for (var i = 0; i < rows.length; i++) {
console.log("section " + i + " : " + rows[i].innerHTML);
rows[i].addEventListener('mouseover', () => {
console.log('mouse over event');
this.editor.SetActiveRow(this);
});
rows[i].addEventListener('dblclick', () => {
this.editor.DisplayContextMenu(this);
});
}
},

How to find width on components in vue.js

Html:
<ul class="nav nav-tabs nav-style">
<tabs
v-for="tabelement in tabelements" :name="tabelement":tabselected="tabelement == type ? 'active': ''" v-on:click="tabclick(tab)"
></tabs>
</ul>
JS:
Vue.component('tabs', {
template:'<li :class="tabselected">{{name}}</li>',
props:['name','tabselected']
});
I want to find the sum of width of all li in this example.
Add watch block to your script.
script
watch: {
'tabelements': function(val) {
var lis = this.$refs.ul.getElementsByTagName("li");
for (var i = 0, len = lis.length; i < len; i++) {
console.log(lis[i].clientWidth); // do something
}
console.log(this.$refs.ul.clientWidth, this.$refs.ul.scrollWidth);
}
}
if scrollWidth > clientWidth, u can show your arrows.
Updated. Explain Fiddle
template
<tabs ref="ul">
Put ref on component otherwise instance doesnot know about it
script
this.$nextTick
This function run method when dom is updated