How to efficiently store/retrieve data to/from chrome.storage.sync? - google-chrome-extension

So, I'm writing an extension to allow people to fine and save colors from images found on the web. It's going well but now I'm trying to conceptualize how I'll actually store them, and list stored items.
As far as I can tell, chrome.storage.sync() only allows for objects. Which means I'd have to do something like this:
{colors: [{colorName: 'white', colorHex: '#ffffff'}, {colorName: 'black', colorHex: '#000000'}]}
Which seems wildly inefficient, since every time I want to add or subtract a color from the favorite list, I will need to get the entire array, change the one item I want, and then store the array back. Not to mention scanning an array for a color to see if it exists or not could be very intensive on a large array.
Ultimately, I'd like to be able to do something along the lines of
colors['#fff'].name = white;
However, that doesn't seem possible.
I'd love to hear some other ideas as to what the best way to accomplish this might be.

The beauty of Javascript is that everything is loosely considered an object. Functions, arrays, and even variables can be accessed as objects.
You could create an array like this,
var colors {}
colors["#FFF"] = "white";
colors["#000"] = "black";
Or perhaps use an array of empty functions,
function color(name, hex /* ... other properties */ ) { }
var colors {
color1: color("white", "#FFF");
color2: color("black", "#000");
}
Then these colors can be accessed by
color1.name
or
color1.hex
Although, because you should use a specific 'key' value for each object in storage, perhaps that is a better way to go.
For instance,
function save_color() {
var white = "#FFF";
//key value callback
chrome.storage.sync.set({"white": white}, function() {
console.log("The value stored was: " + white);
});
}
Or, for multiple colors
function save_colors() {
var white = "#FFF";
var black = "#000";
chrome.storage.sync.set([{"white": white}, {"black": black}], function() {
console.log("The values stored are: " + white + " and " + black);
});
}
I think that may work, i haven't tried storing multiple objects using one api call before, but you should get the point. A good way to implement this may be to have an empty array that gets added to every time the user finds a color they would like to add, then periodically the extension can push the data to sync.
Once you have done a ton of testing and your sync storage is cluttered, keep track of the keys you used during development and remember to run a batch data removal. It would look something like this:
function clear_data() {
var keys = { "white", "black" };
chrome.storage.sync.remove(keys, function() {
for(var i = 0; i < keys.length; i++)
console.log("Removed Data for Key: " + key[i]);
});
}
By the way, to retrieve the value stored in sync,
function load_color() {
var color = "white";
//key callback
chrome.storage.sync.get(color, function(val) {
console.log("The value returned was: " + val);
});
}

I was unsure about this as well, so I made a small example.
manifest.json:
{
"manifest_version": 2,
"name": "Test",
"description": "Test.",
"version": "1.0",
"permissions": [
"storage"
],
"content_scripts": [
{
"matches": ["https://www.google.com/*"],
"js": ["content-script.js"]
}
]
}
content-script.js:
console.log("content script loaded")
function modifyObject() {
chrome.storage.sync.get(null, function(storageData3) {
storageData3.object.property2 = false;
chrome.storage.sync.set(storageData3, function() {
chrome.storage.sync.get(null, function(storageData4) {
console.log("after setting *only* object: " + JSON.stringify(storageData4));
});
});
});
}
// Dumb attempt at setting only property2 of "object"; will add a new top level object "property2".
function attemptToModifyProperty2() {
var toSave = { "property2": false };
chrome.storage.sync.set(toSave, function() {
chrome.storage.sync.get(null, function(storageData2) {
console.log("after attemping to set *only* property2: " + JSON.stringify(storageData2));
modifyObject();
});
});
}
function addArray() {
var toSave = { "array": [1, 2, 3] };
chrome.storage.sync.set(toSave, function() {
chrome.storage.sync.get(null, function(storageData1) {
console.log("after setting *only* array: " + JSON.stringify(storageData1));
attemptToModifyProperty2();
});
});
}
function addObject() {
var toSave = { "object": { "property1": true, "property2": true } };
chrome.storage.sync.set(toSave, function() {
chrome.storage.sync.get(null, function(storageData) {
console.log("after setting *only* object: " + JSON.stringify(storageData));
addArray();
});
});
}
chrome.storage.sync.clear();
addObject();
If you go to google.com (and log in, or change the matches in manifest.json to http), and then open the console, you'll see this output:
content script loaded
content-script.js:42 after setting *only* object: {"object":{"property1":true,"property2":true}}
content-script.js:31 after setting *only* array: {"array":[1,2,3],"object":{"property1":true,"property2":true}}
content-script.js:20 after attemping to set *only* property2: {"array":[1,2,3],"object":{"property1":true,"property2":true},"property2":false}
content-script.js:9 after setting *only* object: {"array":[1,2,3],"object":{"property1":true,"property2":false},"property2":false}
My conclusions from this were that it's only possible to set top-level objects. Even if you want to change only one property that is nested deeply within a top-level object, you will have to pass the entire object to set().

Related

Why does my for loop only goes through once when i call function inside it?

I got list of videos from API, it has list of urls fo thumbnail and i would like to combine thumbnails of each video to gif. When i loop through videos and don't generate gifs it goes through 5 times as expected, but if i include function that should generate gifs it only goes through once, without any errors. I have no idea what is happening
I'm using node.js, discord.js, get pixels and gif-encoder modules to generate thumbnails.
for(i=0;i<5;i++){
generateThumbnail(data[i].video.video_id,data[i].video.thumbs,function(){
var tags = '';
for(t=0;t<data[i].video.tags.length;t++){
tags = tags + data[i].video.tags[t].tag_name+', ';
}
fields = [
{name:data[i].video.title,
value:value},
{name:'Tags',
value:tags}
]
msg.channel.send({embed: {
color: 3447003,
thumbnail: {
"url": ""
},
fields: fields,
}});
});
}
function generateThumbnail(id,images,fn){
var pics = [];
console.log(id)
var file = require('fs').createWriteStream(id+'.gif');
var gif = new GifEncoder(images[0].width, images[0].height);
gif.pipe(file);
gif.setQuality(20);
gif.setDelay(1000);
gif.setRepeat(0)
gif.writeHeader();
for(i=0;i<images.length;i++){
pics.push(images[i].src)
}
console.log(pics)
addToGif(pics,gif);
fn()
}
var addToGif = function(images,gif, counter = 0) {
getPixels(images[counter], function(err, pixels) {
gif.addFrame(pixels.data);
gif.read();
if (counter === images.length - 1) {
gif.finish();
} else {
addToGif(images,gif, ++counter);
}
})
}
if i dont use GenerateThumbnail function it goes through 5 times as expected and everything works fine, but if i use it it goes through only once, and generated only 1 gif
Use var to declare for vars. Ie for(var i=0....
If you declare vars without var keyword, they are in the global scope. ..... and you are using another i var inside the function but now it is the same var from the outer for loop.

Add options to multiple Selectize inputs on type

I'm loading several Selectize select inputs in one page, like this:
var selectizeInput = [];
$('.select-photo-id').each(function (i) {
var selectedValue = $(this).val();
selectizeInput[i + 1] = $(this).selectize({
'maxOptions': 100,
'items': [selectedValue],
'onType': function (input) {
$.post("admin/ajax/search_photos_for_postcards",
{input: input},
function (data) {
$(this).addOption(data);
$(this).refreshOptions();
}, 'json');
}
});
});
The event onType makes a function call that returns a list of new options which I want to make available right away in the Selectize input. Is there any way to call the Selectize instance from there? As you can see from the code, I tried accessing it with $(this), but it fails. I also tried with $(this).selectize, but it's the same. Which is the correct way to do it?
I managed to fix it:
'onType': function (input) {
var $this = $(this);
$.post("admin/ajax/search_photos_for_postcards",
{input: input},
function (data) {
$this[0].addOption(data);
$this[0].refreshOptions();
}, 'json');
}
You probably want to use the load event provided by the Selectize.js API as seen in the demos. Scroll until you find "Remote Source — Github" and then click "Show Code" underneath it.

Is it possible to pass a model to a layout in Express?

I know that it is possible to pass a model to a view in express by doing something like this:
exports.locations = function(req, res){
Location.find(function(err, results) {
res.render('locations', { title: 'Locations', locations: results });
});
};
But is it possible to pass a model to my layout?
Assuming you have all (relevant) routes inside a single .js file, you could add a function like this:
function applyGlobals(pageModel) {
pageModel.myGlobalThing = "I'm always available";
pageModel.anotherGlobalThing = 8675309;
return(pageModel);
}
exports.locations = function(req, res){
Location.find(function(err, results) {
res.render('locations', applyGlobals({ title: 'Locations', locations: results }));
});
};
You could also create a more generalizable solution:
function Globalizer(baseContent) {
var theFunc = function(specificContent) {
var keys = Object.keys(baseContent);
for (var i = 0; i < keys.length; i++)
{
// the lets the page content override global content by not
// overwriting it if it exists;
if(!specificContent.hasOwnProperty(keys[i])){
specificContent[keys[i]] = baseContent[keys[i]];
}
}
return specificContent;
};
return theFunc;
};
// And use it like so.
var applyGlobals = new Globalizer({global1: 12, global2: 'otherthing'});
var pageVars = applyGlobals({item1: 'fifteen', 'item2': 15, global2: 'override'});
console.log(require('util').inspect(pageVars));
Which would emit:
{ item1: 'fifteen',
item2: 15,
global2: 'override',
global1: 12 }
Similarly, you could use one of the various mixin, extend assign or similar functions of various libraries like lodash, underscore, etc. See the doc for lodash.assign() which illustrates accomplishing the same sort of thing.
UPDATE One more way of doing it.
You might want to check out Express' app.locals documentation as well - it might work well for you.

Get previously bounded data

Assume I'm using a slightly modified version of the example code from the selection.data() API docs,
var matrix = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 8045],
[ 1013, 990, 940, 6907]
];
var tr = d3.select("body").append("table").selectAll("tr")
.data(matrix, function(d) { return d[0]; })
.enter().append("tr");
var td = tr.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.text(function(d) { return d; });
On a subsequent update of my matrix 2d array, I want to catch (and do something with...) any table cell that changes. Eg.
// updated matrix
var matrix2 = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 999999999],
[ 1013, 990, 940, 6907]
];
// bind the new data
var tr = d3.select("table").selectAll("tr")
.data(matrix2, function(d) { return d[0]; });
var cells = tr.selectAll("td")
.data(function(d) { return d; });
var updatedCells = rows.filter(function(d,i) {
// HOWTO get previously bound value for cell???
var prevCellValue = null;
return prevCellValue != d;
} );
In the update selection resulting from a join, is there a way to retrieve the previously bound value for a given selection? Once I've called selection.data(newData), it seems like I've lost the previously bound data. I can call selection.data() and temporarily store the output to a variable before binding new data to the DOM element, but it seems awkward (esp. for this 2D array example) to index the previously bound data within the anonymous function passed to, for example, the selection.filter().
(BTW, I tagged "svg" because my actual example uses SVG elements, so I previously tried this.textContent in my selection.filter() function. Unfortunately, this.textContent already had the newly bound data value for the given cell.)
EDIT: this.textContent "sort of" has the previously bound data, but it's potentially processed. I'd prefer the raw, unaltered data if possible.
D3 doesn't provide a way to get back the previously bound data. In your case, you might want to consider storing the data value in an attribute of the element it is bound to so that you can compare it later, i.e. something like
.data(...)
.attr("myvalue", function(d) { return d; });
Then you should be able to do something like
cells.filter(function(d) { return d3.select(this).attr("myvalue") != d; });

YUI3 autocomplete has images on top - How to get autocomplete to the top

My auto YUI autocomplete zindex is off. How can I force the autocomplete DIV to the top.
Below I am using a standard template for YUI:
YAHOO.util.Event.onDOMReady(function(){
YUI().use("autocomplete", "autocomplete-filters", "autocomplete-highlighters", function (Y) {
var inputNode = Y.one('#name'),
tags = [
'css',
'css3',
'douglas crockford',
'ecmascript',
'html',
'html5',
'java',
'javascript',
'json',
'node.js',
'pie',
'yui'
],
lastValue = '';
inputNode.plug(Y.Plugin.AutoComplete, {
activateFirstItem: true,
minQueryLength: 0,
queryDelay: 0,
source: tags,
resultHighlighter: 'startsWith',
resultFilters: ['startsWith']
});
// When the input node receives focus, send an empty query to display the full
// list of tag suggestions.
inputNode.on('focus', function () {
inputNode.ac.sendRequest('');
});
// When there are new AutoComplete results, check the length of the result
// list. A length of zero means the value isn't in the list, so reset it to
// the last known good value.
inputNode.ac.on('results', function (e) {
if (e.results.length) {
lastValue = inputNode.ac.get('value');
} else {
inputNode.set('value', lastValue);
}
});
// Update the last known good value after a selection is made.
inputNode.ac.after('select', function (e) {
lastValue = e.result.text;
});
});
});
Simply to put the z-index in the css. Setting via JS used to be allowed, but as of YUI 3.4.0 it's a css-only flag (https://github.com/yui/yui3/blob/master/src/autocomplete/HISTORY.md).
The relevant CSS is (adjust your z-index as necessary):
.yui3-aclist { z-index: 100; }
PS., your YAHOO. line is from YUI2, so that is quite peculiar and definitely not a standard template.
By the time your callback in the YUI().use(...) section is called, the dom should be ready. No ondomready required.

Resources