I'm developing a new tab replacement extension for Google Chrome and I'd like to allow the user to customize the background, to do so I'm using the storage.sync API as suggested by this page.
The problem is that the style changes are applied asynchronously, so the default background (white) is briefly used during the page load resulting in unpleasing flashes.
Possible (unsatisfying) solutions are:
do not allow to change the background;
hard code a black background in the CSS (and move the problem to custom light backgrounds);
use a CSS transition (still super-ugly).
What could be an alternative approach?
Follows a minimal example.
manifest.json
{
"manifest_version": 2,
"name": "Dummy",
"version": "0.1.0",
"chrome_url_overrides": {
"newtab": "newtab.html"
},
"permissions": [
"storage"
]
}
newtab.html
<script src="/newtab.js"></script>
newtab.js
chrome.storage.sync.get({background: 'black'}, ({background}) => {
document.body.style.background = background;
});
I come up with a reasonable solution. Basically since the localStorage API is synchronous we can use it as a cache for storage.sync.
Something like this:
newtab.js
// use the value from cache
document.body.style.background = localStorage.getItem('background') || 'black';
// update the cache if the value changes from the outside (will be used the next time)
chrome.storage.sync.get({background: 'black'}, ({background}) => {
localStorage.setItem('background', background);
});
// this represents the user changing the option
function setBackground(background) {
// save to storage.sync
chrome.storage.sync.set({background}, () => {
// TODO handle error
// update the cache
localStorage.setItem('background', background);
});
}
This doesn't work 100% of the times but neither do the simple:
document.body.style.background = 'black';
So it's good enough.¹
¹ In the real extension I change the CSS variables directly and I obtain much better results than setting the element style.
Related
I am setting up some content blockers (https://developer.apple.com/documentation/safariservices/creating_a_content_blocker)
The HTML I am testing on looks something like this:
<div class="<random>">
<div>
<div>
<div>
<a class="bad" />
Now, I am wondering if I can have a CSS selector that selects on .bad but then removes the entire .random div block.
I have tried things like:
{
"action": {
"type": "css-display-none",
"selector": "div > div > div > div >a[href*='speedtest.net']"
},
"trigger": {
"url-filter": "^https?:\/+([^\/:]+\\.)?google.*[:\/]"
}
}
and div:has(a).
Nothing seems to work.
Anyone know if its possible? I can't even find anywhere that says what apple supports for this (CSS4?)
Update:: has is now supported in Safari and Safari iOS. source: caniuse
There is currently no parent selector in css even though it is one of the most requested CSS feature.
The simple reason for the lack of implementation yet is performance issues.
https://snook.ca/archives/html_and_css/css-parent-selectors
The closest thing we have to a parent selector in CSS for now is :focus-within which will match an element if the element or any of its descendants is focused. However it is of no use in your case.
The proposed implementation of a parent selector as you mentioned is :has and is part of the level 4 of CSS selectors.
https://www.w3.org/TR/selectors-4/
Although it isn't implemented yet in any browser, it is now part of the technical preview for Safari so we might get it some day.
However it is not yet part of the technical preview for iOS and that might still take a lot of time.
https://caniuse.com/css-has
Even though there is no CSS parent selector, what you are describing can be easily achieved through Javascript in your Safari extension.
However you will need to give permissions to your Safari extension to inject scripts in the web page.
The users will have to accept the permissions. If they refuse you won't be able to access the page DOM through javascript.
You can do that by going in the manifest.json of your Xcode project : AppName => Shared (Extension) => Ressources => manifest
In content_scripts you will need to add
"content_scripts": [{
"js": [ "content.js" ],
"matches": [ "<all_urls>" ]
}]
You can also select the sites you want your extension to have access to with regular expressions like you did previously.
https://developer.apple.com/documentation/safariservices/safari_web_extensions/managing_safari_web_extension_permissions
Then in the content.js file you can add your javascript code to edit the web pages as you please.
Here is a function you could use to remove the parent of the bad elements:
const removeBadElementParent = () =>{
const badElements = document.querySelectorAll('.bad-class-name')
if(badElements.length === 0) return;
badElements.forEach(element =>{
if(!element instanceof HTMLElement) return;
const parent = element.parentElement;
if(!parent instanceof HTMLElement) return;
parent.style.display = 'none'
})
}
You don't want to use it immediately but only when the DOM has loaded so you will need to call it like this:
if( document.readyState !== 'loading' ) {
removeBadElementParent();
} else {
document.addEventListener("DOMContentLoaded", function(event) {
removeBadElementParent();
});
}
However most website make changes to the DOM and might add the bad elements after the DOMContentLoaded event has fired. So you will need to add a mutation observer to check when the DOM is changed:
const onDOMMutation = (callback) =>{
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
const observer = new MutationObserver(function(mutations, observer) {
callback();
});
observer.observe(document, {
subtree: true,
attributes: true
});
}
And you can call it like this instead of calling directly the removeBadElementParent function:
if( document.readyState !== 'loading' ) {
onDOMMutation(removeBadElementParent);
} else {
document.addEventListener("DOMContentLoaded", function(event) {
onDOMMutation(removeBadElementParent);
});
}
https://developer.apple.com/documentation/safariservices/safari_app_extensions/injecting_a_script_into_a_webpage
I am writing a custom component that I would like to define other component dependencies.
The dependencies are different animations types.
Let's say they have the names "animation__x" and "animation__y"
x and y can be any name, so I am looking for something like animation__*
or /animation__\s*/
The only way I have made this work at the moment is either ensuring my component is placed after the animation components on the HTML or alternatively to force update components using this.el.updateComponents()
Neither of these solutions feels right to me.
AFRAME.registerComponent('cool-component', {
dependencies: ['animation'],
update: functions(data){
//detect available animations and do some stuff with them
let animations = Object.keys(components).filter((key) => {
return /(^animation__\w*)/.test(key);
});
//animations results in an empty array
}
});
html that is not working
<a-scene cool-component animation__x="" animation__y="" animation__z=""></a-scene>
html that is working (but its not good as I cant ensure my component is always last in the list
<a-scene animation__x="" animation__y="" animation__z="" cool-component></a-scene>
js that works, but doesnt feel write as I am using the entities internal functions
AFRAME.registerComponent('cool-component', {
dependencies: ['animation'],
update: functions(data){
this.el.updateComponents(); //<-- I DONT LIKE THIS BUT IT WORKS
//detect available animations and do some stuff with them
//now all animations are available as this.el.components
let animations = Object.keys(components).filter((key) => {
return /(^animation__\w*)/.test(key);
});
}
});
Three options:
Depend on the specific component names: dependencies: ['animation__xxx']
Make cool-component set those animations:
AFRAME.registerComponent('cool-component', {
init: functions(data){
this.el.setAttribute('animation__xxx', {...});
}
});
You can also defer cool-component logic until the entity has loaded and all the components have initialized:
init: function () {
this.el.addEvenListener(‘loaded’, this.doStuffAferComponentsLoad.bind(this));
}
More details in what cool-component is trying to accomplish will help to get a more precise answer.
I'm trying to simulate the clicking of CSS elements on my page and automatically take screenshots of the window at each stage for testing purposes. I'm using backstopJS as the CSS testing/screenshot framework. Everything seems to work fine for this first element. A modal is triggered when i click on the register link in the main header menu. but it is not generating any reference screenshotof the modal.
plz help to trigger a reference screenshot of the modal in the below given script
This is the script :
{
"viewports": [
{
"name": "desktop",
"width": 1600,
"height": 900
},
{
"name": "phone",
"width": 320,
"height": 480
},
{
"name": "tablet_v",
"width": 568,
"height": 1024
},
{
"name": "tablet_h",
"width": 1024,
"height": 768
}
],
"grabConfigs" : [
{
"testName":"vawizard"
,"url":"http://localhost/vawizard/index.html"
,"hideSelectors": [
]
,"removeSelectors": [
"header.banner--clone"
]
,"selectors":[
"#outer_wrapper header"
,".banner_box"
,".help_desk"
,".big_search_box"
,".look_specific"
,".smart_tool_box"
,"footer"
,".copyright_box"
]
}
]
}
This is the link
http://wizard.hodgesdigital.com/
Any ideas what could be causing this behavior?
BackstopJS is mainly focused on testing layout states at different screen sizes and doesn't support the testing of user interactions.
In your case I can make two recommendations. 1) You could add a state to your URL which triggers the modal when your page is loaded OR 2) you could write a custom CasperJS script to test cases like this which require some user interaction. More detail below...
Approach 1:
In the first case you could add a hash to your URL which would trigger your modal, In my experience it's common for web apps (e.g. Angular and Ember) to represent modal states in this way....
// in your BackstopJS config
"url": "http://localhost/vawizard/index.html#openModal",
// then as part of a jQuery script
$(document).ready(function(){
if ( /openModal/.test(location.hash) )
{
// Do your open modal action here
// Then trigger BackstopJS
}
});
Approach 2:
If the above is not your style there is another good option. As part of the BackstopJS install you also have a full version of CasperJS -- so I would recommend going to CasperJS.org to look at some basic examples and see if it makes sense to write your own scripts. It will be a little more time consuming in the short run but the project specific boilerplate you write now may be nice to have for future edge cases (for testing forms, other interactions etc.).
We use custom scripts for such things. e.g
module.exports = async(page, scenario, vp) => {
await require('./onReadyInfo')(page, scenario, vp);
await require('./clickAndHoverHelper')(page, scenario, vp);
await require('./onReadyWaitForImages')(page, scenario, vp);
await page.waitForSelector("div[data-social-media-source='instagram'] a[data-target='#social-media-settings']");
await page.click("div[data-social-media-source='instagram'] a[data-target='#social-media-settings']");
await page.waitForSelector(
"#checkbox-twitter", {
timeout: 6000
}
);
await page.waitForTimeout(500);
await page.evaluate(async() => {
jQuery("#checkbox-twitter + label").click();
setTimeout(() => {
jQuery('#checkbox-twitter + label').focus()
}, 500);
});
await new Promise(resolve => setTimeout(resolve, 300));
};
This will open a dialog on link click, check a checkbox and give the label focus.
Has somebody found a good way to animate state transitions?
The router immediately removes the view from the DOM. The problem with that is that I can't defer that until the end of the animation. Note: I'm using v1.0.0-pre.4.
Billy's Billing just released an Ember module that supports animated transitions.
I'll expand on Lesyk's answer. If you need to apply it to multiple views in a DRY way, you can create a customization class like this:
App.CrossfadeView = {
didInsertElement: function(){
//called on creation
this.$().hide().fadeIn(400);
},
willDestroyElement: function(){
//called on destruction
this.$().slideDown(250);
}
};
And then in your code you apply it on your various view classes. As Ember depends on jQuery you can use pretty much any jQuery animation.
App.IndexView = Ember.View.extend(App.CrossfadeView);
App.PostView = Ember.View.extend(App.CrossfadeView);
I know this is pretty old, but the best solution for this context-specific animation today is probably ember liquid fire.
It allows you to do things like this in a transition file:
export default function(){
this.transition(
this.fromRoute('people.index'),
this.toRoute('people.detail'),
this.use('toLeft'),
this.reverse('toRight')
);
};
Ran into this same requirement on my app. Tried Ember Animated Outlet, but didn't give the granularity I needed (element specific animations).
The solution that worked for me was as follows --
Change linkTo to be an action
{{#linkTo "todos"}}<button>Todos</button>{{/linkTo}}
Becomes...
<a href="#/todos" {{action "goToTodos"}}><button>Todos</button></a>
Create Method for goToTodos in current controller
App.IndexController = Ember.Controller.extend({
goToTodos: function(){
// Get Current 'this' (for lack of a better solution, as it's late)
var holdThis = this;
// Do Element Specific Animation Here
$('#something').hide(500, function(){
// Transition to New Template
holdThis.transitionToRoute('todos');
});
}
});
Finally -- To animate in elements on the Todos Template, use didInsertElement on the view
App.TodosView = Ember.View.extend({
didInsertElement: function(){
// Hide Everything
this.$().hide();
// Do Element Specific Animations Here
$('#something_else').fadeIn(500);
}
});
So far, this is the most elegant solution I've found for element specific animations on transition. If there is anything better, would love to hear!
I've found another drop-in solution that implements animations in Views: ember-animate
Example:
App.ExampleView = Ember.View.extend({
willAnimateIn : function () {
this.$().css("opacity", 0);
},
animateIn : function (done) {
this.$().fadeTo(500, 1, done);
},
animateOut : function (done) {
this.$().fadeTo(500, 0, done);
}
}
Demo: author's personal website
App.SomeView = Ember.View.extend({
didInsertElement: function(){
//called on creation
this.$().hide().fadeIn(400);
},
willDestroyElement: function(){
//called on destruction
this.$().slideDown(250)
}
});
i want to make a chrome extension on google reader and i found a problem. content script can not access to iframes. For all n, window.frames[n] = undefined. And i have this "all_frames": true in manifest.json. Or someone could tell me how to add a button under each article. Thank you!
From taking a quick look at Google Reader's rendered HTML, the only button that is in an IFRAME appears to be the Google Plus +1 button - all the other buttons are not in an IFRAME. So you don't need to worry about the IFRAME.
I'm assuming that the existing buttons are the buttons that appear underneath each article: +1, Share, Email, Keep Unread, Add Tags.
If you want to add a new button to the existing article buttons all you need to do is enumerate the DOM - specifically the "entry-actions" DIV classes and append say a new SPAN with your element/button to each article.
I suspect (but not sure) that Reader may dynamically update the DOM with new articles. If this is the case you may need to track new articles being added to the DOM so you can add your button again. To do this add an event listener for DOMNodeInserted - e.g.
document.addEventListener('DOMNodeInserted', onNodeInserted, false);
UPDATE:
The reason you can't see ".entry-actions" class is because it is added dynamically.
Here is a working very basic example. This will monitor the DOM and when it sees an entry-actions DIV that doesn't have our ".myclass" SPAN button, will add it.
You need to have jquery included in your extension for this to work. I've used jquery-1.7.1.min.js in this example. You will also need an icon file called foo.png too if you cut and paste the example.
manifest.json
{
// Required
"name": "Foo Extension",
"version": "0.0.1",
// Recommended
"description": "A plain text description",
"icons": { "48": "foo.png" },
//"default_locale": "en",
// Pick one (or none)
"browser_action": {
"default_icon": "Foo.png", // optional
"default_title": "Foo Extension" // optional; shown in tooltip
},
"permissions": [ "http://*/", "https://*/", "tabs" ],
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["jquery-1.7.1.min.js", "content_script.js" ],
"run_at": "document_idle"
}
]
}
content_script.js
var timer;
document.addEventListener('DOMNodeInserted', onNodeInserted, false);
function onNodeInserted(e)
{
if(timer) clearTimeout(timer);
timer = setTimeout("addButtons()", 250);
}
function addButtons()
{
console.log('add buttons');
var $actions = $(".entry-actions").filter(function() {
return $(this).find('.myclass').length === 0;
});
$actions.append('<span class="myclass">My button</span>');
}