RxJs mousemove event with condition - javascript

I'm struggling with this and I have no idea how to proceed. I want to capture mouse event only when the user stops with mousemove for certain time and it's inside specific element.
const { fromEvent } = rxjs;
const { debounceTime, tap, filter } = rxjs.operators;
const square = document.querySelectorAll("#square");
let isCursorOverSquare = true;
const move$ = fromEvent(square, "mousemove");
const enter$ = fromEvent(square, "mouseenter");
const leave$ = fromEvent(square, "mouseleave");
enter$.pipe(
tap(() => isCursorOverSquare = true)
).subscribe();
leave$.pipe(
tap(() => isCursorOverSquare = false)
).subscribe();
move$
.pipe(
debounceTime(2000),
filter(() => isCursorOverSquare)
)
.subscribe(
(e) => {
console.log(e.target);
});
#square {
width: 200px;
height: 200px;
display: block;
border: 1px solid black;
}
<script src="https://unpkg.com/rxjs#6.3.2/bundles/rxjs.umd.min.js"></script>
<div id="square"></div>
The thing I can't figure out is, how to skip the console.log, if the user moves from square to outside (i.e. handle the logic only, when user is with cursor inside the square).
EDIT:
I managed to work it, but it's not the "RxJs" way. Now I'm setting the isCursorOverSquare variable to true and false and then I use filter operator. Is there a "nicer" more reactive way, to handle this?

So if i understand your question correctly you want to:
Track all mouse movements (mousemove event stream - fromevent)
After movement stops for a certain time (debounce)
Verify it is within a bounding box (filter)
So depending on performance you can either always event the mousemoves or only start mousemove eventing after entering the square using the .switchMap() operator:
enter$
.switchMap(_ => $moves
.debounceTime(2000)
.takeUntil(leave$)
)
.subscribe(finalMouseMoveEventInSquare => {});

The issue that you have is the last mousemove event is triggered when the cursor is still in the square, but the debounce delays the observable until after the cursor has left the square. You can solve this issue by only taking the observable until the mouse has left the square. Here is the complete code for this answer:
<head>
<style>
#square {
width: 200px;
height: 200px;
display: block;
border: 1px solid black;
}
</style>
</head>
<body>
<div id="square"></div>
<script src="https://unpkg.com/rxjs#6.3.2/bundles/rxjs.umd.min.js"></script>
<script>
const { fromEvent } = rxjs;
const { debounceTime, repeat , takeUntil } = rxjs.operators;
const square = document.getElementById("square");
const move$ = fromEvent(square, "mousemove").pipe(debounceTime(2000));
const enter$ = fromEvent(square, "mouseenter");
const leave$ = fromEvent(square, "mouseleave");
move$.pipe(takeUntil(leave$), repeat()).subscribe((e) => console.log(e.target));
</script>
</body>
The repeat operator is necessary as otherwise once the mouse has left the square the first time, the observable will not be repeated when the mouse next enters the square. If your intended behaviour is for the observable to stop emitting after the mouse has left the square for the first time, feel free to remove the repeat operator. Hope this helps you, let me know if you have any questions!

Related

Invoke function after div resize is complete?

I have a resizeable div like this:
div{
resize:both;
overflow:hidden;
height:10rem;
width:10rem;
border:solid 0.5rem black;
}
<div id="item" ></div>
How do I invoke a function after it has completed resizing?
i.e. I don't want the function to continuously invoke while it's being resized,
rather I need it to invoke after the mouse has been lifted from the bottom right resizing icon
You could just check the mouseup event, and track the old v new size.
The only gotcha here is if there are other ways to resize the div other than using the mouse.
const item = document.querySelector('#item');
function getSize() {
return {
w: item.offsetWidth,
h: item.offsetHeight
}
}
let old = getSize();
item.addEventListener('mouseup', () => {
let n = getSize();
if (old.w !== n.w && old.h !== n.h) {
console.log('resized: ' + JSON.stringify(n));
old = n;
}
});
#item {
resize:both;
overflow:hidden;
height:10rem;
width:10rem;
border:solid 0.5rem black;
}
.as-console-wrapper {
max-height:50px!important;
}
<div id="item" ></div>
What I meant by combining a ResizeObserver and a debouncer is something like this:
const observerDebouncers = new WeakMap;
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
// Clear the timeout we stored in our WeakMap if any
clearTimeout( observerDebouncers.get( entry.target ) );
// Set a new timeout to dispatch an event 200 ms after this resize
// if this is followed by another resize, it will be cleared by the above.
observerDebouncers.set( entry.target, setTimeout(() => {
entry.target.dispatchEvent( new CustomEvent( 'resized' ) );
}, 200) );
})
});
const resizeable = document.querySelector( 'div' );
resizeable.addEventListener( 'resized', event => {
console.log( 'resized' );
});
// Register the resize for this element
observer.observe( resizeable );
#item {
resize:both;
overflow:hidden;
height:10rem;
width:10rem;
border:solid 0.5rem black;
}
<div id="item" ></div>
A little bit of a breakdown: observerDebouncers just contains a weakly mapped list from a target to the current timeout stored. It's weakly bound so if you ever remove the element, the timeout id is automatically lost. This means you never really have to worry about the memory consumption here becoming unwieldy.
The observer itself just creates a timeout for every resize event it receives. It also cancelled the previously stored timeout. This means that as long as there is continuous dragging, the timeout will get cancelled and the resized event delayed by 200 more milliseconds. If the resizing stops, the event will fire 200 milliseconds later. (Thats what's called a debouncer, its very common logic in JS to limit the amount of actual events being fired).
The last thing you then need to do is listen for the event and add the target to the observer and voila, a resized event.
This seems like a lot of boilerplate, but it's rather efficient. The only improvement you could make would be to register a single event handler for every target (again, weakly), which would prevent a new function being created every call. A little bit like this:
const observerDebouncers = new WeakMap;
const observerHandlers = new WeakMap;
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
// This will only happen once, so only one method per target is ever created
if( !observerHandlers.has( entry.target ) ){
observerHandlers.set( entry.target, () => {
entry.target.dispatchEvent( new CustomEvent( 'resized' ) );
});
}
// Same as before, except we reuse the method
clearTimeout( observerDebouncers.get( entry.target ) );
observerDebouncers.set( entry.target, setTimeout( observerHandlers.get( entry.target ), 200 ) );
})
});
const resizeable = document.querySelector( 'div' );
resizeable.addEventListener( 'resized', event => {
console.log( 'resized' );
});
observer.observe( resizeable );
#item {
resize:both;
overflow:hidden;
height:10rem;
width:10rem;
border:solid 0.5rem black;
}
<div id="item" ></div>

Select Cells On A Table By Dragging with svelte

I have been trying for 2 days now to integrate a table into svelte where I can select with mouseDown and dragging.
I´m new in js and svelte and did a few courses.
But at this moment is not possible for me to get this working.
Thats what i try to build in svelte: TableSelect
Hope anyone can help me.
Many thanx, Chris
It turned out slightly more complicated than suggested in my comment.
The idea is to listen to mousedown and mouseup events on the entire window (through <svelte:window>) to toggle an isDrag state on/off, listen to mouseenter events on table cells, then toggle the cells on/off only if isDrag is on.
Additionally, you will have to listen to the mousedown event on cells as well, in case the dragging is started inside a cell.
The on/off state of a single cell can be shown visually with a class:selected attribute.
<script>
let columns = new Array(5) // number of columns
let rows = new Array(3) // number of rows
let state = new Array(rows.length*columns.length).fill(false)
let isDrag = false
const beginDrag = () => {
isDrag = true
}
const endDrag = () => {
isDrag = false
}
const toggle = (r, c) => {
state[r*columns.length+c] = !state[r*columns.length+c]
}
const mouseHandler = (r, c) => (e) => {
if (isDrag || e.type === 'mousedown') {
toggle(r, c)
}
}
</script>
<style>
td {
width: 80px;
height: 80px;
background-color: pink;
}
.selected {
background-color: blue;
}
</style>
<svelte:window on:mousedown={beginDrag} on:mouseup={endDrag} />
<table>
{#each rows as _row, r}
<tr>
{#each columns as _column, c}
<td on:mousedown={mouseHandler(r, c)} on:mouseenter={mouseHandler(r, c)} class:selected="{state[r*columns.length+c]}"></td>
{/each}
</tr>
{/each}
</table>
Demo REPL

I'm trying to make a grid of clickable boxes where boxes know which other boxes they're adjacent to. JS/React

I'll post my code in a second, but I really need an approach more than a bugfix. My code works I'm just not sure the best approach for the next step.
The basic usage of my app is that users input two numbers (height/width) to specify the size of a grid. Then the app sends data to the stylesheet to fill in CSS variables which makes a CSS Grid in those dimensions. The app generates the grid with one box (a <div>) in each grid square. Each <div> will be clickable so that the user can attach certain data to it from a hardcoded library I'll manually build. Also, I want to do things that require the app to recognize all contiguous squares with identical data and perform an operation on them collectively, which means each square has to know which squares are above/below/left/right of them.
I've found examples, but only in cases where a grid is not variable in size. My grid is generated per user-inputted dimensions.
In real-world terms, I'm building a crop layout planner for Stardew Valley. Most apps I've found are just calendar apps; see what days you plant crops and what day they'll be ready. I actually want a clickable grid the user can insert corn, wheat, sunflower, etc. And other tools like scarecrows, sprinklers, etc. This means overlays which means the grid "knowing" where each square is in relation others.
The most straightforward way IMO should be to use CSS variables with grid-template-area, but this seems like it'll end up with a lot of complicated regex and I can't find examples of anyone ever doing it successfully. I can't hardcode grid area names because the grid can be a different size every render. I also can't find documentation of gird-template-area accepting logic that would simply figure it out.
The next option I think is to build in mathematical logic to for the app to calculate which squares are above and below it, which shouldn't be TOO hard because I'm making basic rectangular grids. No funny shapes. It'll be some bloated logic but it feels doable.
I'm doing this in React but there's no reason the solution can't be vanilla JS embedded in my React project. In fact it presumably would be. I just feel like there must be an easier solution or a JS/React library that does this which I haven't come across. Or maybe I've discovered the library I'm going to make.
This is the main function which is taking user input to generate the grid. I think it's straightforward enough:
function Landing() {
const [numberAcross, setNumberAcross] = useState(0);
const [numberDown, setNumberDown] = useState(0);
const getGridSize = () => {
const inputAcross = document.getElementById('boxes-across').value;
const numberAcross = parseInt(inputAcross);
setNumberAcross(numberAcross);
const inputDown = document.getElementById('boxes-down').value;
const numberDown = parseInt(inputDown);
setNumberDown(numberDown);
}
return (
<>
<label htmlFor='boxes-across'>Across (1-20):</label>
<input type='number' id='boxes-across' name='boxes-across' min='1' max='20' />
<br />
<label htmlFor='boxes-down'>Down (1-20):</label>
<input type='number' id='boxes-down' name='boxes-down' min='1' max='20' />
<br />
<button onClick={getGridSize}>Make Grid!</button>
<hr />
<div className='flexbox-center-me'>
<p>Across: {numberAcross} {typeof numberAcross}</p>
<p>Down: {numberDown} {typeof numberDown}</p>
<CropGrid numberOfColumns={numberAcross} numberOfRows={numberDown}/>
</div>
</>
);
}
export default Landing;
User input is held in state in that component then passed to this as a prop:
function CropGrid(props) {
const { numberOfColumns, numberOfRows } = props;
const squares = new Array(numberOfColumns * numberOfRows).fill(1);
const cropSquares = squares.map((square, idx) => <CropSquare squareNumber={idx} />)
document.documentElement.style.setProperty('--rows', numberOfRows);
document.documentElement.style.setProperty('--columns', numberOfColumns);
return (
<div id='crop-grid'>
{cropSquares}
</div>
);
}
export default CropGrid;
CSS to generate the grid layout:
#crop-grid {
align-self: center;
display: grid;
grid-template-columns: repeat(var(--columns), 2rem);
grid-template-rows: repeat(var(--rows), 2rem);
min-width: 1rem;
min-height: 1rem;
outline: 3px solid red;
}
And here's the individual <div> being called to render in each grid space:
function CropSquare(props) {
const {squareNumber} = props;
const idString = `crop-grid-square-${squareNumber}`;
return (
<div className='crop-grid-square' id={idString} key={idString} />
);
}
export default CropSquare;
I'd approach the task by using data attributes. I'd tell each cell what position it occupies in the grid, then query this when a cell is clicked, before using that cells row and column info to select its neighbours. You could easily store some other info here in a data attribute and use that as a means of selecting similar elements that may or may not be direct neighbours.
I guess something like this might work for you.
"use strict";
window.addEventListener('load', onLoaded, false);
function onLoaded(event)
{
let nRows = 6, nCols = 6;
for (let y=0; y<nRows; y++)
{
for (let x=0; x<nCols; x++)
{
let cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.row = y;
cell.dataset.col = x;
cell.addEventListener('click', onCellClick, false);
document.body.appendChild(cell);
}
document.body.appendChild( document.createElement('br') );
}
}
function onCellClick(event)
{
let cell = this;
let row = parseInt(cell.dataset.row), col = parseInt(cell.dataset.col);
let lastHighlighted = document.querySelectorAll('.highlight');
lastHighlighted.forEach( elem => elem.classList.remove('highlight') );
let firstDone = false;
let cssSelStr = '';
for (let y=row-1; y<row+2; y++)
{
for (let x=col-1; x<col+2; x++)
{
if (firstDone)
cssSelStr += ", ";
cssSelStr += `div[data-row="${y}"][data-col="${x}"]`;
firstDone = true;
}
}
let highlightCells = document.querySelectorAll(cssSelStr);
highlightCells.forEach( cell => cell.classList.add('highlight') );
}
.cell{
display: inline-block;
width: 32px;
height: 32px;
border: solid 1px #888;
border-radius: 6px;
margin: 4px;
}
.highlight
{
width: 30px;
height: 30px;
border-width: 2px;
background-color: #AFA;
}

Can I wait for a DOM event to resolve?

I've made a mistake. I paired my functionality to .on('click', ...) events. My system installs certain items and each item is categorized. Currently, my categories are [post, image, widgets], each having its own process and they are represented on the front-end as a list. Here's how it looks:
Each one of these, as I said, is paired to a click event. When the user clicks Install a nice loader appears, the <li> itself has stylish changes and so on.
I also happen to have a button which should allow the user to install all the items:
That's neat. Except...there is absolutely no way to do this without emulating user clicks. That's fine, but then, how can I wait for each item to complete (or not) before proceeding with the next?
How can I signal to the outside world that the install process is done?
It feels that if I use new CustomEvent, this will start to become hard to understand.
Here's some code of what I'm trying to achieve:
const installComponent = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve();
}, 1500);
});
};
$('.item').on('click', (event) => {
installComponent().then(() => {
console.log('Done with item!');
});
});
$('#install-all').on('click', (event) => {
const items = $('.item');
items.each((index, element) => {
element.click();
});
});
ul,
ol {
list-style: none;
padding: 0;
margin: 0;
}
.items {
display: flex;
flex-direction: column;
width: 360px;
}
.item {
display: flex;
justify-content: space-between;
width: 100%;
padding: 12px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 0;
}
.item h3 {
width: 80%;
}
.install-component {
width: 20%;
}
#install-all {
width: 360px;
height: 48px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul class="items">
<li class="item" data-component-name="widgets">
<h3>Widgets</h3>
<button class="install-component">Install </button>
</li>
<li class="item" data-component-name="post">
<h3>Posts</h3>
<button class="install-component">Install </button>
</li>
<li class="item" data-component-name="images">
<h3>Images</h3>
<button class="install-component">Install </button>
</li>
</ul>
<button id="install-all">Install All</button>
As you can see, all clicks are launched at the same time. There's no way to wait for whatever a click triggered to finish.
This is simple architectural problems with your application that can be solved by looking into a pattern that falls into MVC, Flux, etc.
I recommend flux a lot because it’s easy to understand and you can solve your issues by separating out your events and UI via a store and Actions.
In this case you would fire an action when clicking any of these buttons. The action could immediately update your store to set the UI into a loading state that disables clicking anything else and show the loader. The action would then process the loader which can be monitored with promises and upon completion the action would finalize by setting the loading state in the store to false and the UI can resolve to being normal again. The cool thing about the proper separation is the actions would be simple JS methods you can invoke to cause all elements to install if you so desire. Essentially, decoupling things now will make your life easier for all things.
This can sound very complicated and verbose for something as simple as click load wait finish but that’s what react, angular, flux, redux, mobx, etc are all trying to solve for you.
In this case I highly recommend examining React and Mobx with modern ECMaScript async/await to quickly make this issue and future design decisions much easier.
What you should do is to declare a variable which will store the installation if it's in progress. And it will be checked when you are trying to install before one installation is complete.
var inProgress = false;
const installComponent = () => {
inProgress = true;
return new Promise((resolve, reject) => {
if(inProgress) return;
else{
setTimeout(() => {
inProgress = false;
return resolve();
}, 1500);
}
});
};
I'd be looking to implement something like this:
let $items = $('.items .item');
let promises = new Array($items.length);
// trigger installation of the i'th component, remembering the state of that
function startInstallOnce(i) {
if (!promises[i]) {
let component = $items.get(i).data('component-name');
promises[i] = installComponent(component);
}
return promises[i];
}
// used when a single item is clicked
$items.on('click', function(ev) {
let i = $(this).index();
startInstallOnce(i);
});
// install all (remaining) components in turn
$('#install-all').on('click', function(ev) {
(function loop(i) { // async pseudo-recursive loop
if (i === components.length) return; // all done
startInstallOnce(i).then(() => loop(i + 1));
})(0);
});

Drag and Drop implemented using Rxjs not working

Trying to create a drag n drop implementation from an Rxjs course example, but its not working correctly. Some time the box is dragged back to original position some times it just get stuck. Here is the plunkr
https://plnkr.co/edit/9Nqx5qiLVwsOV7zU6Diw?p=preview
the js code:
var $drag = $('#drag');
var $document = $(document);
var $dropAreas = $('.drop-area');
var beginDrag$ = Rx.Observable.fromEvent($drag, 'mousedown');
var endDrag$ = Rx.Observable.fromEvent($document, 'mouseup');
var mouseMove$ = Rx.Observable.fromEvent($document, 'mousemove');
var currentOverArea$ = Rx.Observable.merge(
Rx.Observable.fromEvent($dropAreas, 'mouseover').map(e => $(e.target)),
Rx.Observable.fromEvent($dropAreas, 'mouseout').map(e => null)
);
var drops$ = beginDrag$
.do(e => {
e.preventDefault();
$drag.addClass('dragging');
})
.mergeMap(startEvent => {
return mouseMove$
.takeUntil(endDrag$)
.do(moveEvent => moveDrag(startEvent, moveEvent))
.last()
.withLatestFrom(currentOverArea$, (_, $area) => $area);
})
.do(() => {
$drag.removeClass('dragging')
.animate({top: 0, left: 0}, 250);
})
.subscribe( $dropArea => {
$dropAreas.removeClass('dropped');
if($dropArea) $dropArea.addClass('dropped');
});
function moveDrag(startEvent, moveEvent) {
$drag.css(
{left: moveEvent.clientX - startEvent.offsetX,
top: moveEvent.clientY - startEvent.offsetY}
);
}
If I remove the withLatestFrom operator, then dragging of div always work fine, but without this I cannot get the drop feature implemented.
Problem one: Some time the box is dragged back to original position some times it just get stuck.
Answer: you should replace order of chain, ".do" before ".withLatestFrom" like this:
const drops$ = beginDrag$
.do( e => {
e.preventDefault();
$drag.addClass('dragging');
})
.mergeMap(startEvent => {
return mouseMove$
.takeUntil(endDrag$)
.do(mouseEvent => {
moveDrag(startEvent, mouseEvent);
})
.last()
.do((x) => {
console.log("hey from last event",x);
$drag.removeClass('dragging')
.stop()
.animate({ top:0, left: 0}, 250);
}
)
.withLatestFrom(currentOverArea$, (_, $area) => {
console.log('area',$area);
return $area;
});
Problem two: drop and drag outside not working correctly.
Answer: because of mouse event causing by "pointer-events" is not clearly.
In Css File, at:
.dragable .dragging {
background: #555;
pointer-events: none;
}
This is not Enough, the "mouseout" (or "mouseleave") still working, so when you drag box and drop. it happening the same time event "mouseover" and "mouseout". So the drag area never change color.
What to do ?:
make it better by clear every mouse event from the target element. In this case, it is div#drag.dragable.dragging. Add only this to CSS and problem is solve.
div#drag.dragable.dragging {
pointer-events: none;
}
(Holly shit, it take me 8 hours to resolve this. Readmore or see Repo at: Repository
)

Categories