Vlad Holubiev
Vlad Holubiev

Reputation: 5154

Can't connect API to Chrome extension

I am developing chrome extension. I want to connect some API to current tab after click on button in popup.html. I use this code in popup.js:

$('button').click(function() {
    chrome.tabs.executeScript({
        file: 'js/ymaps.js'
    }, function() {});
});

In ymaps.js I use following code to connect API to current tab:

var script = document.createElement('script');
script.src = "http://api-maps.yandex.ru/2.0-stable/?load=package.standard&lang=ru-RU";
document.getElementsByTagName('head')[0].appendChild(script);

This API is needed to use Yandex Maps. So, after that code I create <div> where map should be placed:

$('body').append('<div id="ymapsbox"></div>');

And this simple code only loads map to created <div>:

ymaps.ready(init);//Waits DOM loaded and run function
var myMap;
function init() {
    myMap = new ymaps.Map("ymapsbox", {
        center: [55.76, 37.64],
        zoom: 7
    });
}

I think, everything is clear, and if you are still reading, I'll explain what is the problem. When I click on button in my popup.html I get in Chrome's console Uncaught ReferenceError: ymaps is not defined. Seems like api library isn't connected. BUT! When I manually type in console ymaps - I get list of available methods, so library is connected. So why when I call ymaps-object from executed .js-file I get such an error?

UPD: I also tried to wrap ymaps.ready(init) in $(document).ready() function:

$(document).ready(function() {
    ymaps.ready(init);
})

But error is still appearing. Man below said that api library maybe isn't loaded yet. But this code produces error too.

   setTimeout(function() {
        ymaps.ready(init);
    }, 1500);

I even tried to do such a way...

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo) {
    if (changeInfo.status == "complete") {
        chrome.tabs.executeScript({
            file: 'js/gmm/yandexmaps.js'
        });
    }
});

Upvotes: 2

Views: 1666

Answers (3)

Sergey Okatov
Sergey Okatov

Reputation: 1420

There is a much easier solutioin from Yandex itself.

// div-container of the map
<div id="YMapsID" style="width: 450px; height: 350px;"></div>

<script type="text/javascript">
    var myMap;
    function init (ymaps) {
        myMap = new ymaps.Map("YMapsID", {
            center: [55.87, 37.66],
            zoom: 10
        });
        ...
    }
</script>

// Just after API is loaded the function init will be invoked
// On that moment the container will be ready for usage
<script src="https://...?load=package.full&lang=ru_RU&onload=init">

Update

To work this properly you must be sure that init has been ready to the moment of Yandex-scirpt is loaded. This is possible in the following ways.

  1. You place init on the html page.

  2. You initiate loading Yandex-script from the same script where init is placed.

  3. You create a dispatcher on the html page which catches the ready events from both components.

And you also need to check that your container is created to the moment of Yandex-script is loaded.

Update 2

Sometimes it happens that init script is loaded later than Yandex-lib. In this case it is worth checking:

if(typeof ymaps !== 'undefined' && typeof ymaps.Map !== 'undefined') {
    initMap();
}

Also I came across a problem with positioning of the map canvas, when it is shifted in respect to the container. This may happen, for example, when the container is in a fading modal window. In this case the best is to invoke a window resize event:

$('#modal').on('shown.bs.modal', function (e) {
    window.dispatchEvent(new Event('resize'));
});

Upvotes: -1

Rob W
Rob W

Reputation: 348992

ymaps is not defined because you're trying to use it in the content script, while the library is loaded in the context of the page (via the <script> tag).

Usually, you can solve the problem by loading the library as a content script, e.g.

chrome.tabs.executeScript({
    file: 'library.js'
}, function() {
    chrome.tabs.executeScript({
        file: 'yourscript.js'
    });
});

However, this will not solve your problem, because your library loads more external scripts in <script> tags. Consequently, part of the library is only visible to scripts within the web page (and not to the content script, because of the separate script execution environments).

Solution 1: Intercept <script> tags and run them as a content script.

Get scriptTagContext.js from https://github.com/Rob--W/chrome-api/tree/master/scriptTagContext, and load it before your other content scripts. This module solves your problem by changing the execution environment of <script> (created within the content script) to the content script.

chrome.tabs.executeScript({
    file: 'scriptTagContext.js'
}, function() {
    chrome.tabs.executeScript({
        file: 'js/ymaps.js'
    });
});

See Rob--W/chrome-api/scriptTagContext/README.md for documentation.
See the first revision of this answer for the explanation of the concept behind the solution.

Solution 2: Run in the page's context

If you -somehow- do not want to use the previous solution, then there's another option to get the code to run. I strongly recommend against this method, because it might (and will) cause conflicts in other pages. Nevertheless, for completeness:

Run all code in the context of the page, by inserting the content scripts via <script> tags in the page (or at least, the parts of the extension that use the external library). This will only work if you do not use any of the Chrome extension APIs, because your scripts will effectively run with the limited privileges of the web page.

For example, the code from your question would be restructed as follows:

var script = document.createElement('script');
script.src = "http://api-maps.yandex.ru/2.0-stable/?load=package.standard&lang=ru-RU";
script.onload = function() {
    var script = document.createElement('script');
    script.textContent = '(' + function() {
        // Runs in the context of your page
        ymaps.ready(init);//Waits DOM loaded and run function
        var myMap;
        function init() {
            myMap = new ymaps.Map("ymapsbox", {
                center: [55.76, 37.64],
                zoom: 7
            });
        }
    } + ')()';
    document.head.appendChild(script);
};
document.head.appendChild(script);

This is just one of the many ways to switch the execution context of your script to the page. Read Building a Chrome Extension - Inject code in a page using a Content script to learn more about the other possible options.

Upvotes: 4

gkalpak
gkalpak

Reputation: 48212

This is not a timing issue, rather an "execution environment"-related issue.

You inject the script into the web-page's JS context (inserting the script tag into head), but try to call ymaps from the content script's JS context. Yet, content-scripts "live" in an isolated world and have no access to the JS context of the web-page (take a look at the docs).

EDIT (thx to Rob's comment)

Usually, you are able to bundle a copy of the library and inject it as a content script as well. In your perticular case, this won't help either, since the library itself inserts script tags into to load dependencies.


Possible solutions:

Depending on your exact requirements, you could:

  1. Instead of inserting the map into the web-page, you could display (and let the user interact with) it in a popup window or new tab. You will provide an HTML file to be loaded in this new window/tab containing the library (either referencing a bundled copy of the file or using a CDN after relaxing the default Content Security Policy - the former is the recommended way).

  2. Modify the external library (i.e. to eliminate insertion of script tags). I would advise against it, since this method introduces additional maintainance "costs" (e.g. you need to repeat the process every time the library is updated).

  3. Inject all code into the web-page's context.
    Possible pitfall: Mess up the web-pages JS, e.g. overwriting already defined variables/functions. Also, this method will become increasingly complex if you need to interact with chrome.* APIs (which will not be available to the web-page's JS context, so you'll need to device a proprietary message passing mechanism, e.g. using custom events).

Yet, if you only need to execute some simple initialization code, this is a viable alternative:

E.g.:

ymaps.js:

function initMap() {
    ymaps.ready(init);//Waits DOM loaded and run function
    var myMap;
    function init() {
        myMap = new ymaps.Map("ymapsbox", {
            center: [55.76, 37.64],
            zoom: 7
        });
    }
}

$('body').append('<div id="ymapsbox"></div>');
var script1 = document.createElement('script');
script1.src = 'http://api-maps.yandex.ru/2.0-stable/?load=package.standard&lang=ru-RU';
script1.addEventListener('load', function() {
    var script2 = document.createElement('script');
    var script2.textContent = '(' + initMap + ')()';
    document.head.appendChild(script2);
});
document.head.appendChild(script1);

Rob already pointed to this great resource on the subject:
Building a Chrome Extension - Inject code in a page using a Content script

Upvotes: 2

Related Questions