Hiding DOM elements with a chrome extension without causing a flicker - dom
Preface:
I am aware that there is a duplicate question out there. I am posting it again because it has no answers (and it's from 4 years ago).
General description of what I want:
I want to be able to hide a DOM-element (adding Element.style.display = "none") before the DOM is loaded into the view.
What I've tried:
Other posts point to using a MutationObserver and running it on the document element.
To ensure that we are able to hide an element before the DOM is loaded, we are to run the script containing the MutationObserver as a content_script with "run_at":"document_start".
I did all of this, and I still see a flicker (the elements appear when I load a page and then quickly disappear).
What I'm trying to do:
There's a ul which contains some li with some text on the page I inject my content_script.js into. I populate my popup.html with <text, checkbox> pairs. If the checkbox is checked, the li containing said text is visible, else it is hidden. I want it to persist between refreshes, hence the use of storage.
Things work - but there's a flicker whenever I refresh the page. The elements are there, then they're gone. I don't want them to show up in the first place!
My code:
When I detect that the DOM elements I may remove have loaded, I generate an Object that indicates whether I should hide or keep visible that specific DOM element.
I then set its Element.style.display to none or block accordingly.
/**manifest.json
...
"content_scripts": [
{
"matches": [
"some_website_url"
],
"js": [
"content_script.js"
],
"run_at": "document_start"
}
]
...
*/
///content_script.js
const mutationObserver = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (node.tagName) {
if (node.querySelector(potentially_hidden_element_selector)) {
chrome.storage.sync.get("courses", ({ courses }) => {
chrome.storage.sync.set({ "courses": generateCourseList(courses) }, () => {
const courseElements = Array.from(node.closest('ul').querySelectorAll('a[data-parent-key="mycourses"]'))
courseElements.forEach(courseElement => {
const courseName = getCourseName(courseElement)
const isVisible = courses[courseName]
updateCourseElementInSidebar(courseElement, isVisible)
})
})
})
// We found what we were looking for so stop searching
mutationObserver.disconnect()
}
}
}
}
})
mutationObserver.observe(document, { childList: true, subtree: true })
EDIT 1:
My generateCourseList method depends on the DOM elements I may try to hide - so I can't call the chrome.storage.set method before the DOM has loaded I think.
When I refresh the page, a list of courses eventually populates the DOM.
I then populate the storage's courses object based on these course elements' innerText properties. I set each of these elements' visibility to true or false based on one of two factors: if this course is already defined in the courses object, keep its visibility status, if it isn't, set it to true (visible by default).
I can't make certain DOM elements visible/hidden if I don't have reference to them though. So if I try to call generateCourseList before those specific DOM elements have loaded, I end up trying to retrieve all the course elements (document.querySelectorAll('a[data-parent-key="mycourses"]')) and get returned nothing. I end up setting courses in chrome.storage to nothing because of this chrome.storage.sync.set({ "courses": generateCourseList(courses) }....
EDIT 2:
Here is all of my code. I try to chrome.storage.sync.get as soon as I can, and I try to not depend on the result of chrome.storage.sync.set.
I try to delete the elements as soon as I can, but I'm having difficulty doing so. This is because I have difficulty knowing when the content I want to access (the course elements) have fully loaded. Previously, I was detecting when one course element was visible, and when it was, I assumed all were. This was a mistake. I was able to access the one courselement the moment it popped up, but sometimes only 4 of the 6 course elements were actually loaded. I can't hardcode this number, because it changes from person to person. I can't just tackle them one by one, because then I wouldn't know when to disconnect the MutationObserver. I used the debugger and tried to find what element is loaded soon after all 6 course elements are loaded, and that is the header#page-header.row element. I still get a flicker, though less noticeable than before.
Anything I can do to make it even less noticeable?
function start_mutation_observer() {
chrome.storage.sync.get({ 'savedCourses': {} }, ({ savedCourses }) => {
const observer = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
// The page header gets updated AFTER the courseList is updated - so once it's in the page, we know the courseElements are too
if (document.querySelector('header#page-header.row')) {
observer.disconnect()
const generatedCourses = generateCourseList(savedCourses)
const courseElements = getCourseElements()
// Set visibility of course elements
courseElements.forEach(courseElement => {
const courseName = getCourseElementTextContent(courseElement);
const isShown = generatedCourses[courseName];
setCourseElementVisibility(courseElement, isShown);
});
chrome.storage.sync.set({ 'savedCourses': generatedCourses });
return
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
// In case the content script has been injected when some of the DOM has already loaded
onMutation([{ addedNodes: [document.documentElement] }]);
});
}
function getCourseElements() {
const COURSE_ELEMENT_SELECTOR = 'ul > li > a[data-parent-key="mycourses"]'
return Array.from(document.querySelectorAll(COURSE_ELEMENT_SELECTOR))
}
function getCourseElementTextContent(courseElement) {
const COURSE_ELEMENT_TEXT_CONTAINER_SELECTOR = 'a[data-parent-key="mycourses"] > div > div > span.media-body'
return courseElement.querySelector(COURSE_ELEMENT_TEXT_CONTAINER_SELECTOR).textContent
}
function generateCourseList(savedCourses) {
// Turns [[a, b], [b,c]] into {a:b, b:c}
return Object.fromEntries(getCourseElements().map(courseElement => {
const courseName = getCourseElementTextContent(courseElement)
const isShown = savedCourses[courseName] ?? true
return [courseName, isShown]
}))
}
function setCourseElementVisibility(courseElement, isShown) {
if (isShown) {
courseElement.style.display = "block"
} else {
courseElement.style.display = "none"
}
}
start_mutation_observer()
EDIT 3:
I think it's as good as can be now. I only refresh the visibility of the course elements that were just loaded into the DOM. There's essentially no flicker now (there is a slight one, but its' the same amount of flickering without my extension).
Here is the code for the MutationObserver
function start_mutation_observer() {
let handledCourseElements = new Set()
chrome.storage.sync.get({ 'savedCourses': {} }, ({ savedCourses }) => {
const observer = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
const courseElements = getCourseElements()
const courseElementsAdded = courseElements.length > handledCourseElements.size
// If a courseElement was added, update visibility of those that weren't already processed
if (courseElementsAdded) {
const generatedCourses = generateCourseList(savedCourses)
courseElements
.filter(courseElement => !handledCourseElements.has(courseElement))
.forEach(courseElement => {
const courseName = getCourseElementTextContent(courseElement)
const courseShouldBeVisible = generatedCourses[courseName];
setCourseElementVisibility(courseElement, courseShouldBeVisible);
handledCourseElements.add(courseElement)
})
}
// The page header gets updated AFTER the courseList is updated - so once it's in the page, we know the courseElements are too
if (document.querySelector('header#page-header.row')) {
observer.disconnect()
chrome.storage.sync.set({ 'savedCourses': generateCourseList(savedCourses) });
return
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
// In case the content script has been injected when some of the DOM has already loaded
onMutation([{ addedNodes: [document.documentElement] }]);
});
}
Reading storage is slow and asynchronous, so you need to do it at the beginning:
chrome.storage.sync.get('courses', ({ courses }) => {
chrome.storage.sync.set({ 'courses': generateCourseList(courses) });
const observer = new MutationObserver(onMutation);
observer.observe(document, { childList: true, subtree: true });
onMutation([{addedNodes: [document.documentElement]}]);
function onMutation(mutations) {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (node.tagName && node.querySelector(potentially_hidden_element_selector)) {
observer.disconnect();
processNode(node, courses);
}
}
}
}
});
function processNode(node, courses) {
const courseElements = Array.from(
node.closest('ul').querySelectorAll('a[data-parent-key="mycourses"]'));
courseElements.forEach(courseElement => {
const courseName = getCourseName(courseElement);
const isVisible = courses[courseName];
updateCourseElementInSidebar(courseElement, isVisible);
});
}
Related
How to preselect checkbox in showQuickPick in vscode
I am using quickpick to display list of items and allowing user to do multiselect. I am using quickpick like this. context.subscriptions.push(vscode.commands.registerCommand('command', async () => { const list = await vscode.window.showQuickPick(filedsList, { ignoreFocusOut: true, canPickMany: true}); })); I trying to make like if user previously selected some items in the quick pick and next time use opened this quick pick I want to preselect the previously selected values. Is it feasible to do it in vscode extension development.
There are two options. In both cases you'll have to make QuickPickItems out of your AccurevOperations.cpkFieldsList as you cannot just pass strings as the first argument and use the pre-selection capability. It isn't difficult to do this, just loop through that AccurevOperations.cpkFieldsList array and create objects with the QuickPickItem properties you want, like: const arr = ["1", "2", "3"]; // const quickPickItems = arr.map(item => ( { label: item, picked: true } ) ); // but this would select them all const quickPickItems = arr.map(item => { if (Number(item) % 2 != 0) return { label: item, picked: true }; else return { label: item } }); So you would use your logic to set the items you want pre-selected to have the picked:true object property. Then const options = { canPickMany: true }; const qp = vscode.window.showQuickPick(quickPickItems, options); should show a QuickPick with your selected items. The other option is to use the createQuickPick method instead, because with that you can use its selectedItems property. So starting with your array: const arr = ["1", "2", "3"]; const quickPickItems = arr.map(item => ( { label: item } ) ); // and whatever other properties each item needs, perhaps nothing other than label const qp = vscode.window.createQuickPick(); qp.canSelectMany = true; qp.items = quickPickItems; qp.selectedItems = [quickPickItems[0], quickPickItems[2]]; // your logic here qp.show(); You will have to create an array of objects to assign to the selectedItems property. Perhaps by filtering the quickPickItems array (from above) for the labels you want pre-selected: qp.selectedItems = quickPickItems.filter(item => { return item.label === "1" || item.label === "3"; });
the object vscode::QuickPickItem has a property picked Optional flag indicating if this item is picked initially.
How can i stop this onClick event from rerendering the entire treemap echart i have?
How can i stop this onClick event from rerendering the entire treemap echart i have? I have basically a echarts treemap https://echarts.apache.org/examples/en/editor.html?c=treemap-disk as a functional component in react. I need to be able to apply filters and "grey out" certain tree nodes that dont fit the criteria. This functionality works currently but it rerenders the echart so that the user must restart from the top level and clicktheir way through all the way to the bottom level. How can i avoid the rerendering? This is a similar example i have where clicking the node displays data but also rerenders the chart losing where the node was in the map. const onChartClick = params => { if (params.treePathInfo.length === 9) { setDrawerData(params); } }; useEffect(() => { props.setDrawerData(drawerData); }, [drawerData]); const onEvents = { click: onChartClick, }; ```
you can try to put your chart on useMemo it works for me : const [dataLoaded, setdataLoaded] = useState(true); const onChartClick = params => { if (params.treePathInfo.length === 9) { setDrawerData(params); } }; useEffect(() => { props.setDrawerData(drawerData); setdataLoaded(false) }, [drawerData]); const onEvents = { click: onChartClick, }; const MemoChart = useMemo(() => <Charts option={option} onEvents={onEvents} />, [dataLoaded]);
Algolia autocomplete doesn't go to the selected URL with static sources
ANSWER GetItemURL is only used for keyboard interactions. It is not used for the mouse handling. Mouse handling is done by the rendered HTML (from the template). The simplest approach is to use a a HREF In your template, or an OnClick handler that takes you to another page! —————————- I've copied the default autocomplete code for static sources, the completion and filters work, and getItemURL is called correctly, however on click the URL does not change. I've created a sandbox you can see ithere. Default code: const { autocomplete } = window["#algolia/autocomplete-js"]; const autocomplete_id = "#autocomplete-search-box"; function CreateAutoComplete(appid, search_api_key, index_name) { console.log("CreateAutoComplete Called"); autocomplete({ container: autocomplete_id, placeholder: "Type T to get completions", getSources() { return [ { sourceId: "links", getItems({ query }) { const items = [ { label: "Twitter", url: "https://twitter.com" }, { label: "GitHub", url: "https://github.com" } ]; return items.filter(({ label }) => label.toLowerCase().includes(query.toLowerCase()) ); }, getItemUrl({ item }) { console.log("GetItemURL", item); console.log("returning", item.url); return item.url; }, templates: { item({ item }) { return item.label; } } } ]; } }); }
Remember that getItemUrl() is expecting a keyboard interaction (navigate via arrows and click enter) to navigate over to the result URL, not a click. This is working in your codesandbox, although the redirect is being blocked by Twitter/Github.
Mapbox GL JS: Style is not done loading
I have a map wher we can classically switch from one style to another, streets to satellite for example. I want to be informed that the style is loaded to then add a layer. According to the doc, I tried to wait that the style being loaded to add a layer based on a GEOJson dataset. That works perfectly when the page is loaded which fires map.on('load') but I get an error when I just change the style, so when adding layer from map.on('styledataloading'), and I even get memory problems in Firefox. My code is: mapboxgl.accessToken = 'pk.token'; var map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/streets-v10', center: [5,45.5], zoom: 7 }); map.on('load', function () { loadRegionMask(); }); map.on('styledataloading', function (styledata) { if (map.isStyleLoaded()) { loadRegionMask(); } }); $('#typeMap').on('click', function switchLayer(layer) { var layerId = layer.target.control.id; switch (layerId) { case 'streets': map.setStyle('mapbox://styles/mapbox/' + layerId + '-v10'); break; case 'satellite': map.setStyle('mapbox://styles/mapbox/satellite-streets-v9'); break; } }); function loadJSON(callback) { var xobj = new XMLHttpRequest(); xobj.overrideMimeType("application/json"); xobj.open('GET', 'regions.json', true); xobj.onreadystatechange = function () { if (xobj.readyState == 4 && xobj.status == "200") { callback(xobj.responseText); } }; xobj.send(null); } function loadRegionMask() { loadJSON(function(response) { var geoPoints_JSON = JSON.parse(response); map.addSource("region-boundaries", { 'type': 'geojson', 'data': geoPoints_JSON, }); map.addLayer({ 'id': 'region-fill', 'type': 'fill', 'source': "region-boundaries", 'layout': {}, 'paint': { 'fill-color': '#C4633F', 'fill-opacity': 0.5 }, "filter": ["==", "$type", "Polygon"] }); }); } And the error is: Uncaught Error: Style is not done loading at t._checkLoaded (mapbox-gl.js:308) at t.addSource (mapbox-gl.js:308) at e.addSource (mapbox-gl.js:390) at map.js:92 (map.addSource("region-boundaries",...) at XMLHttpRequest.xobj.onreadystatechange (map.js:63) Why do I get this error whereas I call loadRegionMask() after testing that the style is loaded?
1. Listen styledata event to solve your problem You may need to listen styledata event in your project, since this is the only standard event mentioned in mapbox-gl-js documents, see https://docs.mapbox.com/mapbox-gl-js/api/#map.event:styledata. You can use it in this way: map.on('styledata', function() { addLayer(); }); 2. Reasons why you shouldn't use other methods mentioned above setTimeout may work but is not a recommend way to solve the problem, and you would got unexpected result if your render work is heavy; style.load is a private event in mapbox, as discussed in issue https://github.com/mapbox/mapbox-gl-js/issues/7579, so we shouldn't listen to it apparently; .isStyleLoaded() works but can't be called all the time until style is full loaded, you need a listener rather than a judgement method;
Ok, this mapbox issue sucks, but I have a solution myMap.on('styledata', () => { const waiting = () => { if (!myMap.isStyleLoaded()) { setTimeout(waiting, 200); } else { loadMyLayers(); } }; waiting(); }); I mix both solutions.
I was facing a similar issue and ended up with this solution: I created a small function that would check if the style was done loading: // Check if the Mapbox-GL style is loaded. function checkIfMapboxStyleIsLoaded() { if (map.isStyleLoaded()) { return true; // When it is safe to manipulate layers } else { return false; // When it is not safe to manipulate layers } } Then whenever I swap or otherwise modify layers in the app I use the function like this: function swapLayer() { var check = checkIfMapboxStyleIsLoaded(); if (!check) { // It's not safe to manipulate layers yet, so wait 200ms and then check again setTimeout(function() { swapLayer(); }, 200); return; } // Whew, now it's safe to manipulate layers! the rest of the swapLayer logic goes here... }
Use the style.load event. It will trigger once each time a new style loads. map.on('style.load', function() { addLayer(); });
My working example: when I change style map.setStyle() I get error Uncaught Error: Style is not done loading This solved my problem Do not use map.on("load", loadTiles); instead use map.on('styledata', function() { addLayer(); }); when you change style, map.setStyle(), you must wait for setStyle() finished, then to add other layers. so far map.setStyle('xxx', callback) Does not allowed. To wait until callback, work around is use map.on("styledata" map.on("load" not work, if you change map.setStyle(). you will get error: Uncaught Error: Style is not done loading
The current style event structure is broken (at least as of Mapbox GL v1.3.0). If you check map.isStyleLoaded() in the styledata event handler, it always resolves to false: map.on('styledata', function (e) { if (map.isStyleLoaded()){ // This never happens... } } My solution is to create a new event called "style_finally_loaded" that gets fired only once, and only when the style has actually loaded: var checking_style_status = false; map.on('styledata', function (e) { if (checking_style_status){ // If already checking style status, bail out // (important because styledata event may fire multiple times) return; } else { checking_style_status = true; check_style_status(); } }); function check_style_status() { if (map.isStyleLoaded()) { checking_style_status = false; map._container.trigger('map_style_finally_loaded'); } else { // If not yet loaded, repeat check after delay: setTimeout(function() {check_style_status();}, 200); return; } }
I had the same problem, when adding real estate markers to the map. For the first time addding the markers I wait till the map turns idle. After it was added once I save this in realEstateWasInitialLoaded and just add it afterwards without any waiting. But make sure to reset realEstateWasInitialLoaded to false when changing the base map or something similar. checkIfRealEstateLayerCanBeAddedAndAdd() { /* The map must exist and real estates must be ready */ if (this.map && this.realEstates) { this.map.once('idle', () => { if (!this.realEstateWasInitialLoaded) { this.addRealEstatesLayer(); this.realEstateWasInitialLoaded = true } }) if(this.realEstateWasInitialLoaded) { this.addRealEstatesLayer(); } } },
I ended up with : map.once("idle", ()=>{ ... some function here}); In case you have a bunch of stuff you want to do , i would do something like this => add them to an array which looks like [{func: function, param: params}], then you have another function which does this: executeActions(actions) { actions.forEach((action) => { action.func(action.params); }); And at the end you have this.map.once("idle", () => { this.executeActions(actionsArray); });
I have created simple solution. Give 1 second for mapbox to load the style after you set the style and you can draw the layer map.setStyle(styleUrl); setTimeout(function(){ reDrawMapSourceAndLayer(); /// your function layer }, 1000); when you use map.on('styledataloading') it will trigger couple of time when you changes the style map.on('styledataloading', () => { const waiting = () => { if (!myMap.isStyleLoaded()) { setTimeout(waiting, 200); } else { loadMyLayers(); } }; waiting(); });
kendo-ui autocomplete extend
I'm trying to extend the kendo-ui autocomplete control: I want the search start when te user hit enter, so basically I've to check the user input on keydown event. I've tried to catch the keydown event with this code: (function($) { ui = kendo.ui, Widget = ui.Widget var ClienteText = ui.AutoComplete.extend({ init: function(element,options) { var that=this; ui.AutoComplete.fn.init.call(this, element, options); $(this).bind('keydown',function(e){ console.log(1,e); }); $(element).bind('keydown',function(e){ console.log(2,e); }); }, options: { [...list of my options...] }, _keydown: function(e) { console.log(3,e); kendo.ui.AutoComplete.fn._keydown(e); } }); ui.plugin(ClienteText); })(jQuery); None of the binded events gets called, only the _keydown, and then I'm doing something wrong and cannot call the autocomplete "normal" keydown event. I've seen a lot of examples that extend the base widget and then create a composite widget, but I'm not interested in doing that, I only want to add a functionality to an existing widget. Can someone show me what I'm doing wrong? Thank you!
What about avoiding the extend and take advantage of build in options and methods on the existing control : http://jsfiddle.net/vojtiik/Vttyq/1/ //create AutoComplete UI component var complete = $("#countries").kendoAutoComplete({ dataSource: data, filter: "startswith", placeholder: "Select country...", separator: ", ", minLength: 50 // this is to be longer than your longest char }).data("kendoAutoComplete"); $("#countries").keypress(function (e) { if (e.which == 13) { complete.options.minLength = 1; // allow search complete.search($("#countries").val()); complete.options.minLength = 50; // stop the search again } });
This code actually work: (function($) { ui = kendo.ui, ClienteText = ui.AutoComplete.extend({ init: function(element,options) { ui.AutoComplete.fn.init.call(this, element, options); $(element).bind('keydown',function(e){ var kcontrol=$(this).data('kendoClienteText'); if (e.which === 13) { kcontrol.setDataSource(datasource_clientes); kcontrol.search($(this).val()); } else { kcontrol.setDataSource(null); } }); }, options: { name: 'ClienteText', } }); ui.plugin(ClienteText); })(jQuery); but I don't know if it's the correct way to do it.