Completely lost on how to save extension popup window content
Asked Answered
D

2

6

I'm pretty much lost on how to make the added contents of my popup window not disappear every time I open a new link or click it "away". I've read about content script, background script and the like but I don't honestly know how to implement that into my own source code. Below is my popup.html, popup.js and my manifest.js file.

{
    "manifest_version": 2,
    "name": "URL_save",
    "description": "This extension saves an URL and renames the title to the user's wishes and hyperlink the title.",
    "version": "0.1",

    "browser_action": {
        "default_icon": "/img/icon.png",
        "default_popup": "popup.html",
        "default_title": "See your saved websites!"
    },

    "permissions": [
        "tabs"
    ]
}

popup html:

<html>
  <head>
    <title>Your articles</title>
    <link href="/css/style.css" rel="stylesheet"/>
    <script src="/js/underscore-min.js"></script>
    <script src="/js/popup.js"></script>
  </head>
  <body>
    <div id="div">No content yet! Click the button to add the link of the current website!</div>
    <div><ul id="list"></ul></div>
    <br/>
    <button id="button">Add link!</button>
  </body>
</html>

popup.js:

// global variables
var url;

// event listener for the button inside popup window
document.addEventListener('DOMContentLoaded', function() {
    var button = document.getElementById('button');
    button.addEventListener('click', function() {
        addLink();
    });
});

// fetch the URL of the current tab, add inside the window
function addLink() {
// store info in the the queryInfo object as per: 
//   https://developer.chrome.com/extensions/tabs#method-query
    var queryInfo = {
    currentWindow: true,
    active: true
    };

    chrome.tabs.query(queryInfo, function(tabs) {
        // tabs is an array so fetch the first (and only) object-elemnt in tab
        // put URL propery of tab in another variable as per: 
        //   https://developer.chrome.com/extensions/tabs#type-Tab
        url = tabs[0].url;

        // format html
        var html = '<li><a href=' + url + " target='_blank'>" + url + '</a><br/></li>';

        // change the text message
        document.getElementById("div").innerHTML = "<h2>Saved pages</h2>";

        // get to unordered list and create space for new list item 
        var list = document.getElementById("list");
        var newcontent = document.createElement('LI');
        newcontent.innerHTML = html;

        // while loop to remember previous content and append the new ones
        while (newcontent.firstChild) {
            list.appendChild(newcontent.firstChild);
        }
    });
}

In this images you see what happens when I first add a link but then close (only) the popup window, opening it again:

After adding the current URL:
before

After closing and re-opening the popup:
after

Drainpipe answered 22/12, 2016 at 13:40 Comment(1)
Currently this question is a bit too broad to provide complete code answers. There are choices (e.g. if you want to store the URLs only for this session, across Chrome shutdown/restart, or across all machines using this profile) that you need to make to limit the answer set in order to get this down to a specific answer of how to store, and restore, your data.Sparse
S
25

Similar to a web page, the popup's (or an options/settings page's) scope is created when it is shown and destroyed when it is no longer visible. This means that there is no state stored within the popup itself between the times that it is shown. Any information which you desire to persist after the popup is destroyed, you will need to store somewhere else. Thus, you will need to use JavaScript to store any state which you desire to have be the same the next time the popup is opened. Each time the popup is opened, you will need to retrieve that information and restore it to the DOM. The two most commonly used places are a StorageAreaMDN, or the background page.

Where you store the information will depend on how long you want the data you store to persist, and where you want the data to be seen.

The general locations where you could store data include (other possibilities exist, but the followin are the most common):

  • The background page if you want the data to exist only until Chrome is closed. It will not exist once Chrome is restarted. You can send the data to the background page through a couple/few different methods, including message passingMDN, or directly changing values on the background pageMDN. Data stored in the StorageArea (the two options below) is also available to the background page, and content scripts.
  • chrome.storage.localMDN if you want the data to persist on the local machine across Chrome being closed and restarted.
  • chrome.storage.syncMDN if you want the data shared with all instances of Chrome which use the current Chrome account/profile. The data will also persist until changed. It will be available through Chrome being closed and restarted. It will be available on other machines using the same profile.
  • window.localStorage: Prior to the existence of chrome.storage it was popular to store data for the extension in window.localStorage. While this will still work, it is generally preferred to use chrome.storage.

One of the advantages of using a chrome.storage StorageAreaMDN is that the data is directly available to all portions of your extension without the need to pass the data as a message.1

Your current code

Currently your code is not storing the URLs that are entered anywhere other than in the DOM of the popup. You will need to establish a data structure (e.g. an array) in which you store the list of URLs. This data can then be stored into one of the storage locations mentioned above.

Google's example on the Options documentation page2, MDN shows storing chrome.storage.sync and restoring values into the DOM when the options page is displayed. The code used in this example can for the options page can work exactly as-is for a popup by just defining its HTML page as the default_popup for a browser_action. There are many other examples available.

Unfortunately, without more specifics from you as to what you desire, it is difficult to give you specific code. However, couple of suggestions to head in the direction you need to go are:

  • Refactor your code so you have a separate function that you call with a URL as a parameter which just adds this URL to the list you have in the DOM (e.g. addUrlToDom(url)). This function will be used when the user adds a URL and when the URLs are restored when the page loads.
  • Store your list of URLs in an array (e.g. urlList). This array will be what you save into the storage location outside of your popup. You will read this array from that storage location in your DOMContentLoaded handler and use the refactored addUrlToDom() function to add each value. Restoring it into the DOM could look something like:

    urlList.forEach(function(url){
        addUrlToDom(url);
    });
    

Storing your data in chrome.storage.local

Assuming you want to store the URLs on the local machine across Chrome shutdown/restart (i.e. use chrome.storage.local), you code could look something like:

manifest.json changes to permissions only:

    "permissions": [
        "tabs",
        "storage"
    ]

popup.js:

// global variables
var urlList=[];

document.addEventListener('DOMContentLoaded', function() {
    getUrlListAndRestoreInDom();
    // event listener for the button inside popup window
    document.getElementById('button').addEventListener('click', addLink);
});

// fetch the URL of the current tab, add inside the window
function addLink() {
    chrome.tabs.query({currentWindow: true,active: true}, function(tabs) {
        // tabs is an array so fetch the first (and only) object-element in tab
        var url = tabs[0].url;
        if(urlList.indexOf(url) === -1){
            //Don't add duplicates
            addUrlToListAndSave(url);
            addUrlToDom(url);
        }
    });
}

function getUrlListAndRestoreInDom(){
    chrome.storage.local.get({urlList:[]},function(data){
        urlList = data.urlList;
        urlList.forEach(function(url){
            addUrlToDom(url);
        });
    });
}

function addUrlToDom(url){
    // change the text message
    document.getElementById("div").innerHTML = "<h2>Saved pages</h2>";

    //Inserting HTML text here is a bad idea, as it has potential security holes when
    //  including content not sourced entirely from within your extension (e.g. url).
    //  Inserting HTML text is fine if it is _entirely_ sourced from within your
    //  extension.
    /*
    // format HTML
    var html = '<li><a href=' + url + " target='_blank'>" + url + '</a></li>';
    //Add URL to DOM
    document.getElementById("list").insertAdjacentHTML('beforeend',html);
    */
    //Build the new DOM elements programatically instead:
    var newLine = document.createElement('li');
    var newLink = document.createElement('a');
    newLink.textContent = url;
    newLink.setAttribute('href',url);
    newLink.setAttribute('target','_blank');
    newLine.appendChild(newLink);
    document.getElementById("list").appendChild(newLine);
}

function addUrlToListAndSave(url){
    if(urlList.indexOf(url) === -1){
        //URL is not already in list
        urlList.push(url);
        saveUrlList();
    }
}

function saveUrlList(callback){
    chrome.storage.local.set({urlList},function(){
        if(typeof callback === 'function'){
            //If there was no callback provided, don't try to call it.
            callback();
        }
    });
}

  1. The exception to this is scripts which you insert into the page context. The page context is something you will probably not be running scripts in. To do so you have to use a content script (where your StorageAreaMDN data is directly available) to insert a <script> tag into the DOM of a web page. This can be a bit complex, any you probably don't need to be concerned about it. It is mentioned here merely because there is a possible exception to the statement that the StorageAreaMDN data is available to all areas of your extension.
  2. The example in the Chrome documenation works just fine on Firefox. Yes, Firefox supports both chrome.*, using callbacks, and browser.*, using promises.
Sparse answered 22/12, 2016 at 18:43 Comment(20)
Wow... You are truly amazing without doubt. You really are a genius. There's so much I need to learn more about, thank you for pointing me to the right direction and even giving me better code! I appreciate your help you helped me much more than I could (and would) ask for! I'm really glad, thank you!Drainpipe
However I do have one question, in the last function "saveUrlList" it takes as an argument a callback. But in the "addUrlToListAndSave" function the "saveUrlList" doesn't receive any arguments. Isn't a callback function defined as follows: "hey (whatever name) function, call me when you did your work and give me (optionally) the output of your work". And then the previous function, say chrome.storage.local.set calls the nameless function back. But I didn't understand why the last function is needed and how that works. on developer.chrome.com/extensions/storage I found it a bit vague.Drainpipe
Yes, a callback is usually a function (CB) which is passed to another function (S) with the intention that CB be called once the action that S is supposed to do is complete. In this code, saveUrlList() has the capability to accept an optional callback which will be called once the urlList is saved. Using if(typeof callback === 'function'){ tests to see that a callback function was passed as callback. If no function was passed, then we don't try to call it. addUrlToListAndSave has nothing which must be done after the save is complete, so no callback function is passed. (cont')Sparse
(cont') The entirety of the if statement block in saveUrlList() could be removed. It is not needed for the current functionality, but might be needed in the future. Having the capability in the code was mostly intended to allow it if needed in any additional code (e.g. the Options example I linked in the answer uses a set() callback to inform the user that the options have been changed). Using the callback may, or may not, be desired, but you will probably want to use saveUrlList() when you implement a way to remove URLs from the list (which does not inherently need the capability).Sparse
I see, thank you! I pretty much understand all of the code now. But I don't get this sentence about chrome.storage.local.set of the developer.chrome site: "An object which gives each key/value pair to update storage with. Any other key/value pairs in storage will not be affected." Do they mean (in our context) that when you provide an object (inside is the urlList array) that the urls inside of it are not set into the storage if they already exist there? I also don't get the: "chrome.storage.local.get( {urlList : [] }, ...." part of the second function. Why must the array be inside object?Drainpipe
That sentence is saying that if you have 100 different named values (keys) in the StorageArea, you can update as few as you want without affecting the others. The entire urlList array is one item (it has one key "urlList"). The empty array in chrome.storage.local.get( {urlList : [] } provides the default value in case nothing is yet stored in "urlList" (e.g. when first installed). If not provided, we would have to check for undefined values prior to using urlList, as urlList.forEach() would throw an error if you were trying to use the non-existent forEach method of undefined.Sparse
This made me wonder now, let's say that the DOM is first installed. The getUrlListAndRestoreInDom function uses a empty array as default value (like you say) and then calls the forEach to the array (which is empty). Afterwards the addUrlToDom function gets called. This function should change the popup-window's title. How come it doesn't get immediately changed by this function since it get's called. Or does the forEach-function stop executing on an empty array? I tried to look on the internet but only found this: -Note: forEach() does not execute the function for array elements without values.Drainpipe
@Kobrajunior, In that situation, given that the array has no elements in it, the function inside forEach() is not executed. Array.prototype.forEach() is executed once for each existing element. An empty array, [] has no elements, thus there are no elements on which to execute the forEach() function.Sparse
thank you very much for the time you spent on me! I wish you happy holidays! "The empty array in chrome.storage.local.get( {urlList : [] } provides the default value in case nothing is yet stored in "urlList" (e.g. when first installed)." This was a statement of your previous comment. But if nothing is stored than it is already empty instead of undefined, right? Because when I removed the [] and just put {urlList} than the code also worked normally, or was that just luck?Drainpipe
@Kobrajunior, And happy holidays to you too. No. I was talking about what is stored in chrome.storage.local with the key urlList. If nothing has ever been stored there, then it is undefined, unless you provide a default value. An empty array is a clearly defined value. Unless we tell it, how would it know that we want urlList to be an Array? It worked because you had already installed and used the extension previously. Thus, a value had already been stored there. The default value is only returned if nothing has yet been stored in that location (or the value was deleted).Sparse
Haven't we defined the urlList to be an global (empty) array in the first line? Or does that global statement mean that is undefined, not empty (like local variables in C-programming). Or does chrome.storage.local simply not have the permission to read this global statement.Drainpipe
@Kobrajunior, It is defined as that until we unconditionally assign urlList = data.urlList;. After that point, it does not matter what we had as the value in the var urlList line. We could test for an undefined value and not assign it if it is undefined, but it is easier to just supply the default value to be returned by chrome.storage.local.get().Sparse
alright thank you very much! I've got a lot to learn it seems haha. I appreciate all of your help and time, especially since I consumed a lot of it. Again, thank you.Drainpipe
I've gone pretty far with my extension now but I was wondering, is it possible to delete a single URL if someone clicks on a button (clear all URL's was easy). I created buttons together with the URL's when the URL's got added to the DOM. Afterwards I've put an event listener to the buttons with the action to delete the specific URL but it just doesn't get removed, does chrome-extension support this feature?Drainpipe
@Drainpipe You could certainly delete a single URL. I would expect you would want to do so. Without the code (e.g. in a new question), it is difficult to just say how to do so. However, in addition to the DOM manipulation, it will most likely involve using .splice().Sparse
I have read the rules thoroughly today and I have question. I'm not sure if this will also be seen as a violation: "What if I have a question, someone answered it, but the answer doesn't work and the person who answered that question admits that it doesn't work. Can I then ask the same question (similar in spirit) again?" I think I have done so and I'm not sure if that is wrong. The rules only states that you aren't allowed to ask the same question again but it didn't state that you aren't allowed to ask the question again if the previous answer wasn't correct.Drainpipe
I am asking this because I don't want to violate the rules or anything like that. I tried my best to make the questions different (one about the event listener specific and the other one about deleting one URL).Drainpipe
Also, I don't want to be rude or anything so you can ignore the rest of this comment if you want but could you maybe look at the latest question (#41347140). I've tried my best and changed a lot of things but it seems to get worse every time try to change it. If you don't have time or something like that it's okay than I just stop where I am at now.Drainpipe
@Kobrajunior, Just FYI: I'm not intentionally ignoring you. I just have other things happening in real life at the moment and have not put time into Stack Exchange. I'll try to put some time in and take a look at your questions in detail.Sparse
I thank you very much for your time and I appreciate it all. The question has been answered (next time I'm going to be more patient). I wish you happy new year in advance.Drainpipe
U
0

A popup reloads it's document every time it's closed/reopened. Instead, you should use a background page to preserve state.

General way to do this is:

  1. communicate from popup to background to do something. I.e.: do an ajax request and store the result in a plain javascript object.
  2. do a get from the background page, to get the javascript object. Even though you close the popup, state is preserved in the background page.
Unveil answered 22/12, 2016 at 17:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.