1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9-->
10<link rel="import" href="../polymer/polymer.html">
11
12<script>
13(function() {
14  "use strict";
15  /**
16   * `Polymer.IronJsonpLibraryBehavior` loads a jsonp library.
17   * Multiple components can request same library, only one copy will load.
18   *
19   * Some libraries require a specific global function be defined.
20   * If this is the case, specify the `callbackName` property.
21   *
22   * You should use an HTML Import to load library dependencies
23   * when possible instead of using this element.
24   *
25   * @hero hero.svg
26   * @demo demo/index.html
27   * @polymerBehavior
28   */
29  Polymer.IronJsonpLibraryBehavior = {
30
31    properties: {
32      /**
33       * True if library has been successfully loaded
34       */
35      libraryLoaded: {
36        type: Boolean,
37        value: false,
38        notify: true,
39        readOnly: true
40      },
41      /**
42       * Not null if library has failed to load
43       */
44      libraryErrorMessage: {
45        type: String,
46        value: null,
47        notify: true,
48        readOnly: true
49      }
50      // Following properties are to be set by behavior users
51      /**
52       * Library url. Must contain string `%%callback%%`.
53       *
54       * `%%callback%%` is a placeholder for jsonp wrapper function name
55       *
56       * Ex: https://maps.googleapis.com/maps/api/js?callback=%%callback%%
57       * @property libraryUrl
58       */
59      /**
60       * Set if library requires specific callback name.
61       * Name will be automatically generated if not set.
62       * @property callbackName
63       */
64      /**
65       * name of event to be emitted when library loads. Standard is `api-load`
66       * @property notifyEvent
67       */
68      /**
69       * event with name specified in `notifyEvent` attribute
70       * will fire upon successful load2
71       * @event `notifyEvent`
72       */
73    },
74
75    observers: [
76      '_libraryUrlChanged(libraryUrl)'
77    ],
78
79    _libraryUrlChanged: function(libraryUrl) {
80      // can't load before ready because notifyEvent might not be set
81      if (this._isReady && this.libraryUrl)
82        this._loadLibrary();
83    },
84
85    _libraryLoadCallback: function(err, result) {
86      if (err) {
87        Polymer.Base._warn("Library load failed:", err.message);
88        this._setLibraryErrorMessage(err.message);
89      }
90      else {
91        this._setLibraryErrorMessage(null);
92        this._setLibraryLoaded(true);
93        if (this.notifyEvent)
94          this.fire(this.notifyEvent, result, {composed: true});
95      }
96    },
97
98    /** loads the library, and fires this.notifyEvent upon completion */
99    _loadLibrary: function() {
100      LoaderMap.require(
101        this.libraryUrl,
102        this._libraryLoadCallback.bind(this),
103        this.callbackName
104      );
105    },
106
107    ready: function() {
108      this._isReady = true;
109      if (this.libraryUrl)
110        this._loadLibrary();
111    }
112  };
113
114  /**
115   * LoaderMap keeps track of all Loaders
116   */
117  var LoaderMap = {
118    apiMap: {}, // { hash -> Loader }
119
120    /**
121     * @param {Function} notifyCallback loaded callback fn(result)
122     * @param {string} jsonpCallbackName name of jsonpcallback. If API does not provide it, leave empty. Optional.
123     */
124    require: function(url, notifyCallback, jsonpCallbackName) {
125
126      // make hashable string form url
127      var name = this.nameFromUrl(url);
128
129      // create a loader as needed
130      if (!this.apiMap[name])
131        this.apiMap[name] = new Loader(name, url, jsonpCallbackName);
132
133      // ask for notification
134      this.apiMap[name].requestNotify(notifyCallback);
135    },
136
137    nameFromUrl: function(url) {
138      return url.replace(/[\:\/\%\?\&\.\=\-\,]/g, '_') + '_api';
139    }
140  };
141
142  /** @constructor */
143  var Loader = function(name, url, callbackName) {
144    this.notifiers = [];  // array of notifyFn [ notifyFn* ]
145
146    // callback is specified either as callback name
147    // or computed dynamically if url has callbackMacro in it
148    if (!callbackName) {
149      if (url.indexOf(this.callbackMacro) >= 0) {
150        callbackName = name + '_loaded';
151        url = url.replace(this.callbackMacro, callbackName);
152      } else {
153        this.error = new Error('IronJsonpLibraryBehavior a %%callback%% parameter is required in libraryUrl');
154        // TODO(sjmiles): we should probably fallback to listening to script.load
155        return;
156      }
157    }
158    this.callbackName = callbackName;
159    window[this.callbackName] = this.success.bind(this);
160    this.addScript(url);
161  };
162
163  Loader.prototype = {
164
165    callbackMacro: '%%callback%%',
166    loaded: false,
167
168    addScript: function(src) {
169      var script = document.createElement('script');
170      script.src = src;
171      script.onerror = this.handleError.bind(this);
172      var s = document.querySelector('script') || document.body;
173      s.parentNode.insertBefore(script, s);
174      this.script = script;
175    },
176
177    removeScript: function() {
178      if (this.script.parentNode) {
179        this.script.parentNode.removeChild(this.script);
180      }
181      this.script = null;
182    },
183
184    handleError: function(ev) {
185      this.error = new Error("Library failed to load");
186      this.notifyAll();
187      this.cleanup();
188    },
189
190    success: function() {
191      this.loaded = true;
192      this.result = Array.prototype.slice.call(arguments);
193      this.notifyAll();
194      this.cleanup();
195    },
196
197    cleanup: function() {
198      delete window[this.callbackName];
199    },
200
201    notifyAll: function() {
202      this.notifiers.forEach( function(notifyCallback) {
203        notifyCallback(this.error, this.result);
204      }.bind(this));
205      this.notifiers = [];
206    },
207
208    requestNotify: function(notifyCallback) {
209      if (this.loaded || this.error) {
210        notifyCallback( this.error, this.result);
211      } else {
212        this.notifiers.push(notifyCallback);
213      }
214    }
215  };
216})();
217</script>
218
219<!--
220  Loads specified jsonp library.
221
222  Example:
223
224      <iron-jsonp-library
225        library-url="https://apis.google.com/js/plusone.js?onload=%%callback%%"
226        notify-event="api-load"
227        library-loaded="{{loaded}}"></iron-jsonp-library>
228
229  Will emit 'api-load' event when loaded, and set 'loaded' to true
230
231  Implemented by  Polymer.IronJsonpLibraryBehavior. Use it
232  to create specific library loader elements.
233
234  @demo
235-->
236<script>
237  Polymer({
238
239    is: 'iron-jsonp-library',
240
241    behaviors: [ Polymer.IronJsonpLibraryBehavior ],
242
243    properties: {
244      /**
245       * Library url. Must contain string `%%callback%%`.
246       *
247       * `%%callback%%` is a placeholder for jsonp wrapper function name
248       *
249       * Ex: https://maps.googleapis.com/maps/api/js?callback=%%callback%%
250       */
251      libraryUrl: String,
252      /**
253       * Set if library requires specific callback name.
254       * Name will be automatically generated if not set.
255       */
256      callbackName: String,
257      /**
258       * event with name specified in 'notifyEvent' attribute
259       * will fire upon successful load
260       */
261      notifyEvent: String
262      /**
263       * event with name specified in 'notifyEvent' attribute
264       * will fire upon successful load
265       * @event `notifyEvent`
266       */
267
268    }
269  });
270
271</script>
272