2

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'
        });
    }
});
Vlad Holubiev
  • 4,876
  • 7
  • 44
  • 59
  • Just one clarification, you want to append this div ympasbox to popup.html document or to webpage that the user is browsing? (I'm asking because chrome.tabs.executeScript is used primarily by content scripts, that are executed in context of webpages, and content scripts are run in separate runtime environment.) – Pawel Miech Jan 05 '14 at 12:29
  • @Pawelmhm to webpage that the user is browsing – Vlad Holubiev Jan 05 '14 at 12:37
  • Maybe ymaps is simply not loaded yet when line ymaps.ready(init) is executed? You add script tag, browser makes request to get ymap but when ymap.ready is executed it may not be loaded and ymap variable is still undefined. Later when you interact with page in console it's loaded but it's not loaded when the code of ymaps.js is executed. Try adding some other event listeners to make sure what's happening. – Pawel Miech Jan 05 '14 at 12:46
  • I also tried to wrap ymaps.ready(init) in $(document).ready(). Result was the same – Vlad Holubiev Jan 05 '14 at 12:48

3 Answers3

4

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.

Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • I don't agree with the 1st solution's being proposed as recommended, but +1 nonetheless for the thorough coverage :) – gkalpak Jan 05 '14 at 14:57
  • Through 40 min of various attemps I still don't understand how to implement first solution. Can you give me any tip? – Vlad Holubiev Jan 05 '14 at 15:19
  • @VladGolubev Do you understand the concept? Can you show your code? – Rob W Jan 05 '14 at 15:20
  • I see that `XMLHttpRequest` returns code very similar to library. You said I have to "Replace the ')` ? – Vlad Holubiev Jan 05 '14 at 15:31
  • @VladGolubev The method can concisely be described as "Eliminate the use of the ` – Rob W Jan 05 '14 at 15:40
  • @VladGolubev I'll get back to you later (in a couple of hours). I'll provide a general implementation that's not only useful for your case, but also for others. – Rob W Jan 05 '14 at 16:18
  • I'll be waiting! Thank you, Rob – Vlad Holubiev Jan 05 '14 at 16:19
  • Wow, incredible. Tnank you, I learned today a lot. 40 users of my chrome extension include me are happy:) – Vlad Holubiev Jan 06 '14 at 00:16
  • @RobW seems like "scriptTagContext" can't operate with bing maps api(http://goo.gl/OE4S1S). When I connect it, all pages where extension is executing stop loading and turn in blank documents with iframe with from source: https://accounts.google.com/o/oauth2/postmessageRelay. Google says that this link loads g+1 button. Maybe, you should look to function i() in bing maps api which appends script tag? – Vlad Holubiev Jan 06 '14 at 19:12
  • 1
    @VladGolubev Without looking at the source code, I guess that the scripts are loaded through `document.write` (based on the observation that the page turns blank). I could capture `document.write` calls, but that's a bit messy. After looking at the source code of the link you've provided, I suggest to add `runAt: "document_end"` to `chrome.tabs.executeScript` (or `"run_at": "document_end"` in the manifest file when you're declaring content scripts), because Bing Maps only uses `document.write` when the document is not ready. – Rob W Jan 06 '14 at 21:18
2

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

Community
  • 1
  • 1
gkalpak
  • 47,844
  • 8
  • 105
  • 118
  • Updating the CSP string will not solve the problem. The CSP declared in the manifest file only affects the extension pages, not content scripts. And bundling the JS file will not help much either (in this case), because the script in question inserts ` – Rob W Jan 05 '14 at 13:49
  • @RobW what do you mean under "switching to the page"? – Vlad Holubiev Jan 05 '14 at 13:55
  • @VladGolubev I have elaborated my comment in an answer. – Rob W Jan 05 '14 at 14:31
  • ExpertSystem, you helped me a lot of times and now too. I am very much in your debt! – Vlad Holubiev Jan 05 '14 at 14:34
  • @VladGolubev It might work in some cases, but it's a terrible way of writing browser extensions, because it could (and will) cause conflicts with other pages that use JavaScript objects with a similar name. (edit: was in reply to your edited comment that went like "I used script.innerText and now it works, thanks!") – Rob W Jan 05 '14 at 14:36
  • @RobW I understand, it's ugly. I am reading your answer at the moment. I am very appreciate you too. Looks like you put a lot of effort – Vlad Holubiev Jan 05 '14 at 14:38
  • 1
    @VladGolubev: I updated my answer (thx to Rob's insightful comment). I saw Rob's answer in the meantime, but I was half-way through with my edits, so I finished through. – gkalpak Jan 05 '14 at 14:54
-1

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'));
});
Sergey Okatov
  • 1,270
  • 16
  • 19
  • It's not an "easier" solution as it does not apply to extensions, since the same context problem remains. – Xan Oct 10 '16 at 12:28
  • There is no problem. This solution works fine for me. The `init` function is invoked only when ymaps object is ready and accessible. It is impossbile to get `Uncaught ReferenceError: ymaps is not defined` within `init` function. – Sergey Okatov Oct 11 '16 at 08:53
  • Where in an extension you have a `` element working? – Xan Oct 11 '16 at 08:55
  • Well, `init` function in this example must be on the same page where container is. – Sergey Okatov Oct 11 '16 at 09:04
  • Okay, good luck getting that to work in an extension. This is probably working fine in a normal webpage (I don't dispute that) but it's not a solution that fits the question. – Xan Oct 11 '16 at 09:07