How to check if element exists & keep track of it's mutations once found using mutationObserver - mutation-observers

First & foremost excuse my ignorance as im still trying to wrap my head aroud mutationObservers
I'm creating a chrome extension that detects certain words from an ordered list. when content script runs at first the ordered list element isn't rendered so I used a mutationObserver to capture it when it's created however as I scroll through the page the ordered list keeps increasing in size via API calls, how can I keep track of the new results rendered under the ordered list?
What i've done so far
const waitForElm = async (selector) => {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
};
waitForElm("ol.artdeco-list").then((elm) => {
console.log("Element is ready");
// Tried to create a new mutationObserver here.
});
I've tried creating another mutationObserver once promise was resolved on the same element that was found "ol.artdeco-list", but that didn't seem to work...
Any advice??

Related

Showing multiple search results on leaflet map [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
https://www.website.ro/harta this is the map that uses leaflet.js and https://github.com/stefanocudini/leaflet-search, but i can search only single locations.
https://www.website.ro/public/ajax?q=electri
If i search for "electri" it has 3 locations, i want to show them when i hit enter, not to show "Not found".
Already searched on google, stackoverflow, didnt found similar answer/problem.
This can be done with careful use of the options that leaflet-search provides. First, let's create an array that will hold the potential results, and a featureLayer to render any results that show up:
const results = [];
var resultsLayer = L.featureGroup();
Now we can overwrite the buildTip option as a function which does pretty much what it does already by default, but pushes the results to an array as well:
var controlSearch = new L.Control.Search({
...options,
// hijack buildtip function, push results to array
buildTip: (text, loc) => {
results.push(loc); // <---- crucial line here
// the rest of this is lifted from the source code almost exactly
// so as to keep the same behavior when clicking on an option
const tip = L.DomUtil.create("div");
tip.innerHTML = text;
L.DomEvent.disableClickPropagation(tip)
.on(tip, "click", L.DomEvent.stop, controlSearch)
.on(
tip,
"click",
function (e) {
controlSearch._input.value = text;
controlSearch._handleAutoresize();
controlSearch._input.focus();
controlSearch._hideTooltip();
controlSearch._handleSubmit();
},
controlSearch
);
return tip;
},
// only move to the location if there are not multiple results
moveToLocation: results.length
? () => {}
: L.Control.Search._defaultMoveToLocation
});
Now we add an event listener to the input of the search, and if the user presses enter, and there are multiple results, the results that were pushed into the results array will be added to the resultsLayer as markers, and added to the map:
inputEl.addEventListener("keypress", function (e) {
if (e.key === "Enter" && results.length) {
markersLayer.remove();
results.forEach((result) => {
const marker = L.marker(result);
resultsLayer.addLayer(marker);
});
map.fitBounds(resultsLayer.getBounds());
}
});
Working codesandbox
Note this will likely require some cleanup work (i.e. emptying the array on new or empty searches), or readding the full data set if the search is empty, etc., but this should be enough to get you started.
Edit - Full item info
You asked in a comment how we can get the full details of an item and put that in a popup. Reading through leaflet-search's docs and source code, there doesn't seem to be any place that their code 'catches' the entire data object. The buildTip function really only needs 2 pieces of data from an item - the text to show in the tooltip, and the location it refers to. There's a bunch of TODOs regarding keeping the source data in a cache, but they're still todos.
What I would do is use the title and loc that is returned in a result to filter the original data and find its corresponding item in the original data:
const getFullItem = (title, loc) => {
return data.find((item) => item.title === title && loc.equals(item.loc));
};
We can also create a generic function to build the popup text for all the makers, and the results, so the popups are all consistent:
const buildPopupText = (item) => {
return `
<h4>Title: ${item.title}</h4>
<p>Phone: ${item.telefon}</p>
<p>more stuff from ${item.whatever}</p>
`;
};
When we hit enter and we map through the results, we'll use the result to get the original item:
inputEl.addEventListener("keypress", function (e) {
if (e.key === "Enter" && results.length) {
results.forEach((result) => {
const originalItem = getFullItem(result.text, result.loc);
const marker = L.marker(result.loc);
marker.bindPopup(buildPopupText(originalItem));
resultsLayer.addLayer(marker);
});
map.fitBounds(resultsLayer.getBounds());
}
});
So now the results popups build a popup from the originalItem, which has all the properties you'll need.
Working codesandbox

ERROR TypeError: Cannot read property '_leaflet_pos' of undefined

I have angular 10 app using ngx-leaflet and routing. I have a map component, which dynamically displays custom markers on map, based on user selection. I navigate from map component view to another component. Then I navigate back to map component. User can change date, and based on that, old layer of markers is removed and new layer of markers is loaded and shown. Everything works fine, but I always get this error:
ERROR TypeError: Cannot read property '_leaflet_pos' of undefined
at getPosition (leaflet-src.js:2450)
at NewClass._getMapPanePos (leaflet-src.js:4439)
at NewClass._moved (leaflet-src.js:4443)
at NewClass.getCenter (leaflet-src.js:3798)
at NewClass.setZoom (leaflet-src.js:3181)
at SafeSubscriber._next (map.component.ts:960)
at SafeSubscriber.__tryOrUnsub (Subscriber.js:183)
at SafeSubscriber.next (Subscriber.js:122)
at Subscriber._next (Subscriber.js:72)
at Subscriber.next (Subscriber.js:49)
I can reproduce this error only when I go back to the map component. If i stay only at the map component no error is shown. I've searched for fix, but from what I found it seems nobody really knows why is this happening and how to fix this error. I've found these two issues on GitHub dealing with the same problem in Vue.js, so I guess it's problem with leaflet itself, and not ngx-leaflet.
https://github.com/vue-leaflet/Vue2Leaflet/issues/613
https://github.com/stefanocudini/leaflet-search/issues/129
I've tried to change this:
function getPosition(el) {
// this method is only used for elements previously positioned using setPosition,
// so it's safe to cache the position for performance
return el._leaflet_pos || new Point(0, 0);
}
to this:
function getPosition(el) {
// this method is only used for elements previously positioned using setPosition,
// so it's safe to cache the position for performance
if(el){
return el._leaflet_pos || new Point(0, 0);
}
else{
return new Point(0, 0);
}
}
But then the error just looks like this:
ERROR TypeError: Cannot set property '_leaflet_pos' of undefined
at setPosition (leaflet-src.js:2433)
at NewClass._resetView (leaflet-src.js:4154)
at NewClass.setView (leaflet-src.js:3174)
at NewClass.setZoom (leaflet-src.js:3186)
at SafeSubscriber._next (map.component.ts:960)
at SafeSubscriber.__tryOrUnsub (Subscriber.js:183)
at SafeSubscriber.next (Subscriber.js:122)
at Subscriber._next (Subscriber.js:72)
at Subscriber.next (Subscriber.js:49)
at MapSubscriber._next (map.js:35)
UPDATE:
As #pk. suggested in comments ,when I don't call setZoom or call it before I remove old markers I get this error:
ERROR TypeError: Cannot read property 'appendChild' of undefined
at NewClass._initIcon (leaflet-src.js:7608)
at NewClass._initIcon (leaflet.rotatedMarker.js:23)
at NewClass.onAdd (leaflet-src.js:7460)
at NewClass._layerAdd (leaflet-src.js:6572)
at NewClass.whenReady (leaflet-src.js:4433)
at NewClass.addLayer (leaflet-src.js:6634)
at NewClass.eachLayer (leaflet-src.js:6861)
at NewClass.onAdd (leaflet-src.js:6845)
at NewClass._layerAdd (leaflet-src.js:6572)
at NewClass.whenReady (leaflet-src.js:4433)
UPDATE 2:
When I don't add new marker layer to map (this.deskLayer.addTo(map)), the error dissapears, but I want to add new markers to map...
This is what happens when user changes date:
onMapReady(map: Map) {
//listening for USER DATE CHANGE
this.userFloorService.sharedUserSelectedDate
.pipe(skip(1))
.subscribe(() => {
this.deskLayer.remove(); // first remove old desks
this.userFloorService // then get desks and reservations
.getFloor(this.floorNumber)
.subscribe( (data) => {
// create new reservations
let reservationsArr = data.records[0].reservations;
// create new DESKS
let deskMarkers = [];
data.records[0].desks.forEach((desk) => {
let deskId = desk.desk_id;
let deskMarker = marker(
[
desk.desk_coordinate.coordinates[0],
desk.desk_coordinate.coordinates[1],
],
{
title: this.getDeskTitle(desk, reservationsArr), // set desk title to RESERVED/FREE
rotationAngle: desk.desk_angle,
rotationOrigin: 'center center',
riseOnHover: true
}
).on('click', () => {
this.zone.run(() => {
this.openDeskDialog(deskMarker.options.title,deskId);
});
});
deskMarker.setIcon(this.getDeskIcon(deskMarker)); // for displaying desk icons on zoomLvl -1.5
deskMarkers.push(deskMarker);
});
this.deskLayer = layerGroup(deskMarkers); // add new desks to deskLayer
this.layersControl.overlays['Desks'] = this.deskLayer; // reassign desks in overlays for correct desk layer toggling
this.deskLayer.addTo(map);
map.setZoom(-1); // set zoom
},
error =>{
console.log(error);
}
);
});
It turned out, that these errors were happening because I used BehaviorSubject to pass data between components, and everytime I navigated out and back to the map component, new subscription to BehaviorSubject was created without destroying the old subscribtion. So destroying subscriptions everytime I navigated from map component solved it. Maybe this will help to somebody.

How can I update data within Detail table and don't loose range selection and filters?

I have latest enterprise React agGrid table with Master/Detail grid. My data is fetched on the client every 5 seconds and then put immutably to the redux store. React grid component is using deltaRowDataMode={true} props and deltaRowDataMode: true in Detail options.
My master grid performs normally as expected: if I have range selected, grid would keep selection after the data updates, so would filters and visibility menu would be still opened. But Detail grid behaves differently: on data refresh selections are being removed, visibility menu closes and grid jumps if filters were changed.
I've read in docs that when I open Detail grid it's being created from scratch, but in my case I don't close Detail. Anywhere I've tried keepDetailRows=true flag, which solved problems with jumping on update and selection loss, but Detail grid doesn't update data now.
It seems there are only two possible options according to the docs https://www.ag-grid.com/javascript-grid-master-detail/#changing-data-refresh. The first one is a detail table redraws everytime a data in a master row changes and the second one is a detail row doesn't changes at all if a flag suppressRefresh is enabled. Strange decision, awful beahviour...
Update.
Hello again. I found a coupe of solutions.
The first one is to use a detailCellRendererParams in table's options and set suppressRefresh to true. It gives an opportunity to use getDetailGridInfo to get detail-table's api.
While the detail-table's refreshing is disabled, using detailGridInfo allows to set a new data to a detail-table.
useEffect(() => {
const api = gridApiRef;
api && api.forEachNode(node => {
const { detailNode, expanded } = node;
if (detailNode && expanded) {
const detailGridInfo = api.getDetailGridInfo(detailNode.id);
const rowData = detailNode.data.someData; // your nested data
if (detailGridInfo) {
detailGridInfo.api.setRowData(rowData);
}
}
});
}, [results]);
The second one is to use a custom cellRenderer, wicth is much more flexible and allows to use any content inside a cellRenderer.
In table's options set detailCellRenderer: 'yourCustomCellRendereForDetailTable.
In yourCustomCellRendereForDetailTable you can use
this.state = {
rowData: [],
}
Every cellRenderer has a refresh metod which can be used as follow.
refresh(params) {
const newData = [ ...params.data.yourSomeData];
const oldData = this.state.rowData;
if (newData.length !== oldData.length) {
this.setState({
rowData: newData,
});
}
if (newData.length === oldData.length) {
if (newData.some((elem, index) => {
return !isEqual(elem, oldData[index]);
})) {
this.setState({
rowData: newData,
});
}
}
return true;
}
Using method refresh this way gives a fully customizable approach of using a detailCellRenderer.
Note. To get a better performance with using an immutable data like a redux it needs to set immutableData to true in both main and detail tables.

Vue/Vuex/GSAP-Animation: Add DOM elements to store

In a Vue project, I am looking for a way to save DOM elements to the store. Those elements shall then be animated with GSAP.
Unfortunately, there is a bit of a problem with when the DOM is ready (so I can use document.querySelector) and when Vue's transition system is firing.
the store has about this structure:
const store = new Vuex.Store({
state: {
settings: {
ui: {
btns: {
cBtnNavMain: {
el: document.querySelector('.c-btn__nav--main') // does not work, because DOM is not yet ready
[...]
}
}
}
}
},
mutations: {
// Add DOM element later via a mutation?
addDomElementToStore: (state, obj) => {
console.log("MUTATION addDomElementToStore, obj =", obj) // shows `null`
obj.el = obj.domEl
}
}
}
Then, in App.vue (loaded by main.js) there is basically this script:
created() {
console.log("App.vue created")
}
mounted() {
console.log("App.vue created")
}
methods: {
beforeEnter(el) {
console.log("BeforeEnter called, obj")
// This could be the place to add DOM to the store, but how?
// I tried a mutations like this (see above in store.mutations):
this.$store.commit('addDomElementToSettings', {
el: this.$store.state.settings.ui.btns.cNavMainBtn.el,
domEl: document.querySelector('.c-btn__nav--main')
})
// ...but won't work though, the console shows, obj params are empty
}
[...]
}
The console result shows something like this:
App.vue created
BeforeEnter called // <-- beginEnter before mounted called!
MUTATION addDomElementToStore, obj = {el: null, domElem: null}
App.vue mounted
Since "beforeEnter" is called after created, but BEFORE mounted (which is, where the DOM would be easily accessible), the DOM is not really accessible yet, it seems. So I thought, I use "beforeEnter" to assign new DOM elements to store.settings using a mutation. But that doesn't work at all - and GSAP eventually has do DOM elements to animate on.
What do I have to do to get my DOM elements saves to the settings, so that I do not have to use document.querySelector all the time, when I want to address a DOM element with GSAP?
Why don't try you using the mounted() lifecycle method to fire the mutation that stores DOM element selections in vuex state? That way you know something exists to select.

RxJs Observable with infinite scroll OR how to combine Observables

I have a table which uses infinite scroll to load more results and append them, when the user reaches the bottom of the page.
At the moment I have the following code:
var currentPage = 0;
var tableContent = Rx.Observable.empty();
function getHTTPDataPageObservable(pageNumber) {
return Rx.Observable.fromPromise($http(...));
}
function init() {
reset();
}
function reset() {
currentPage = 0;
tableContent = Rx.Observable.empty();
appendNextPage();
}
function appendNextPage() {
if(currentPage == 0) {
tableContent = getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; });
} else {
tableContent = tableContent.combineLatest(
getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; }),
function(o1, o2) {
return o1.concat(o2);
}
)
}
}
There's one major problem:
Everytime appendNextPage is called, I get a completely new Observable which then triggers all prior HTTP calls again and again.
A minor problem is, that this code is ugly and it looks like it's too much for such a simple use case.
Questions:
How to solve this problem in a nice way?
Is is possible to combine those Observables in a different way, without triggering the whole stack again and again?
You didn't include it but I'll assume that you have some way of detecting when the user reaches the bottom of the page. An event that you can use to trigger new loads. For the sake of this answer I'll say that you have defined it somewhere as:
const nextPage = fromEvent(page, 'nextpage');
What you really want to be doing is trying to map this to a stream of one directional flow rather than sort of using the stream as a mutable object. Thus:
const pageStream = nextPage.pipe(
//Always trigger the first page to load
startWith(0),
//Load these pages asynchronously, but keep them in order
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
//One option of how to join the pages together
scan((pages, p) => ([...pages, p]), [])
)
;
If you need reset functionality I would suggest that you also consider wrapping that whole stream to trigger the reset.
resetPages.pipe(
// Used for the "first" reset when the page first loads
startWith(0),
//Anytime there is a reset, restart the internal stream.
switchMapTo(
nextPage.pipe(
startWith(0),
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
scan((pages, p) => ([...pages, p]), [])
)
).subscribe(x => /*Render page content*/);
As you can see, by refactoring to nest the logic into streams we can remove the global state that was floating around before
You can use Subject and separate the problem you are solving into 2 observables. One is for scrolling events , and the other is for retrieving data. For example:
let scrollingSubject = new Rx.Subject();
let dataSubject = new Rx.Subject();
//store the data that has been received back from server to check if a page has been
// received previously
let dataList = [];
scrollingSubject.subscribe(function(page) {
dataSubject.onNext({
pageNumber: page,
pageData: [page + 10] // the data from the server
});
});
dataSubject.subscribe(function(data) {
console.log('Received data for page ' + data.pageNumber);
dataList.push(data);
});
//scroll to page 1
scrollingSubject.onNext(1);
//scroll to page 2
scrollingSubject.onNext(2);
//scroll to page 3
scrollingSubject.onNext(3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.all.js"></script>