master
  1/*! Embedly jQuery - v3.1.1 - 2013-06-05
  2 * https://github.com/embedly/embedly-jquery
  3 * Copyright (c) 2013 Sean Creeley
  4 * Licensed BSD
  5 */ 
  6(function($) {
  7
  8  /*
  9   *  Util Functions
 10   */
 11
 12  // Defaults for Embedly.
 13  var defaults = {
 14    key:              null,
 15    endpoint:         'oembed',         // default endpoint is oembed (preview and objectify available too)
 16    secure:           null,            // use https endpoint vs http
 17    query:            {},
 18    method:           'replace',        // embed handling option for standard callback
 19    addImageStyles:   true,             // add style="" attribute to images for query.maxwidth and query.maxhidth
 20    wrapElement:      'div',            // standard wrapper around all returned embeds
 21    className:        'embed',          // class on the wrapper element
 22    batch:            20,                // Default Batch Size.
 23    urlRe:            null
 24  };
 25
 26  var urlRe = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
 27
 28  var none = function(obj){
 29    return obj === null || obj === undefined;
 30  };
 31  // Split a list into a bunch of batchs.
 32  var batch = function(list, split){
 33    var batches = [], current = [];
 34    $.each(list, function(i, obj){
 35      current.push(obj);
 36      if (current.length === split){
 37        batches.push(current);
 38        current = [];
 39      }
 40    });
 41    if (current.length !== 0){
 42      batches.push(current);
 43    }
 44    return batches;
 45  };
 46  // Make an argument a list
 47  var listify = function(obj){
 48    if (none(obj)){
 49      return [];
 50    } else if (!$.isArray(obj)){
 51      return [obj];
 52    }
 53    return obj;
 54  };
 55
 56  // From: http://bit.ly/T9SjVv
 57  var zip = function(arrays) {
 58    return $.map(arrays[0], function(_,i){
 59      return [$.map(arrays, function(array){return array[i];})];
 60    });
 61  };
 62
 63  /* Keeper
 64   *
 65   * alittle wrapper around Deferred that lets us keep track of
 66   * all the callbacks that we have.
 67   */
 68  var Keeper = function (len, each, after) {
 69    this.init(len, each, after);
 70  };
 71  Keeper.prototype = {
 72
 73    init: function(urls){
 74      this.urls = urls;
 75      this.count = 0;
 76      this.results = {};
 77      this._deferred = $.Deferred();
 78    },
 79    // Only 2 methods we really care about.
 80    notify : function(result) {
 81      // Store the result.
 82      this.results[result.original_url] = result;
 83      // Increase the count.
 84      this.count++;
 85      // Notify the success functions
 86      this._deferred.notify.apply(this._deferred, [result]);
 87      // If all the callbacks have completed, do your thing.
 88      if (this.count === this.urls.length){
 89        // This sorts the results in the manner in which they were added.
 90        var self = this;
 91        var results = $.map(this.urls, function(url){ return self.results[url];});
 92        this._deferred.resolve(results);
 93      }
 94      return this;
 95    },
 96    state: function() {
 97      return this._deferred.state.apply(this._deferred, arguments);
 98    }
 99  };
100  window.Keeper = Keeper;
101
102  // direct API for dealing with the
103  var API = function () {};
104  API.prototype = {
105    /*
106      For dealing directly with Embedly's API.
107
108      options: {
109        key: 'Your API key'
110        secure: false,
111        query: {
112          maxwidth: 500,
113          colors: true,
114        }
115      }
116    */
117    defaults: {},
118
119    log: function(level, message){
120      if (!none(window.console) && !none(window.console[level])){
121        window.console[level].apply(window.console, [message]);
122      }
123    },
124    // Based on the method and options, build the url,
125    build: function(method, urls, options){
126      // Technically, not great.
127      options = none(options) ? {}: options;
128      // Base method.
129
130      var secure = options.secure;
131      if (none(secure)){
132        // If the secure param was not see, use the protocol instead.
133        secure = window.location.protocol === 'https:'? true:false;
134      }
135
136      var base = (secure ? 'https': 'http') +
137        '://api.embed.ly/' + (method === 'objectify' ? '2/' : '1/') + method;
138
139      // Base Query;
140      var query = none(options.query) ? {} : options.query;
141      query.key = options.key;
142      base += '?'+$.param(query);
143
144      // Add the urls the way we like.
145      base += '&urls='+ $.map(urls, encodeURIComponent).join(',');
146
147      return base;
148    },
149    // Batch a bunch of URLS up for processing. Will split longer lists out
150    // into many batches and return the callback on each and after on done.
151    ajax: function(method, urls, options){
152
153      // Use the defaults.
154      options = $.extend({}, defaults, $.embedly.defaults, typeof options === 'object' && options);
155
156      if (none(options.key)){
157        this.log('error', 'Embedly jQuery requires an API Key. Please sign up for one at http://embed.ly');
158        return null;
159      }
160
161      // Everything is dealt with in lists.
162      urls = listify(urls);
163
164      // add a keeper that holds everything till we are good to go.
165      var keeper = new Keeper(urls);
166
167      var valid_urls = [], rejects = [], valid;
168      // Debunk the invalid urls right now.
169      $.each(urls, function(i, url){
170        valid = false;
171        // Make sure it's a URL
172        if (urlRe.test(url)){
173          valid = true;
174          // If the urlRe has been defined make sure it works.
175          if (options.urlRe !== null && options.urlRe.test && !options.urlRe.test(url)){
176            valid = false;
177          }
178        }
179        // deal with the valid urls
180        if(valid === true){
181          valid_urls.push(url);
182        } else {
183          // Notify the keeper that we have a bad url.
184          rejects.push({
185            url: url,
186            original_url: url,
187            error: true,
188            invalid: true,
189            type: 'error',
190            error_message: 'Invalid URL "'+ url+'"'
191          });
192        }
193      });
194
195      // Put everything into batches, even if these is only one.
196      var batches = batch(valid_urls, options.batch), self = this;
197
198      // Actually make those calls.
199      $.each(batches, function(i, batch){
200        $.ajax({
201          url: self.build(method, batch, options),
202          dataType: 'jsonp',
203          success: function(data){
204            // We zip together the urls and the data so we have the original_url
205            $.each(zip([batch, data]), function(i, obj){
206              var result = obj[1];
207              result.original_url = obj[0];
208              result.invalid = false;
209              keeper.notify(result);
210            });
211          }
212        });
213      });
214
215      if (rejects.length){
216        // set a short timeout so we can set up progress and done, otherwise
217        // the progress notifier will not get all the events.
218        setTimeout(function(){
219          $.each(rejects, function(i, reject){
220            keeper.notify(reject);
221          });
222        }, 1);
223      }
224
225      return keeper._deferred;
226    },
227
228    // Wrappers around ajax.
229    oembed: function(urls, options){
230      return this.ajax('oembed', urls, options);
231    },
232    preview: function(urls, options){
233      return this.ajax('preview', urls, options);
234    },
235    objectify: function(urls, options){
236      return this.ajax('objectify', urls, options);
237    },
238    extract: function(urls, options){
239      return this.ajax('extract', urls, options);
240    }
241  };
242
243  // direct API dealing directly with Embedly's Display API.
244  var ImageAPI = function () {};
245  ImageAPI.prototype = {
246
247    // Based on the method and options, build the image url,
248    build: function(method, url, options){
249      options  = $.extend({}, $.embedly.defaults, typeof options === 'object' && options);
250
251      var secure = options.secure;
252      if (none(secure)){
253        // If the secure param was not seen, use the protocol instead.
254        secure = window.location.protocol === 'https:'? true:false;
255      }
256
257      var base = (secure ? 'https': 'http') +
258        '://i.embed.ly/' + (method === 'display' ? '1/' : '1/display/') + method;
259
260      // Base Query
261      var query = none(options.query) ? {} : options.query;
262      query.key = options.key;
263      base += '?'+$.param(query);
264
265      // Add the image url
266      base += '&url='+ encodeURIComponent(url);
267
268      return base;
269    },
270    // Wrappers around build image url function.
271    display: function(url, options){
272      return this.build('display', url, options);
273    },
274    resize: function(url, options){
275      return this.build('resize', url, options);
276    },
277    fill: function(url, options){
278      return this.build('fill', url, options);
279    },
280    crop: function(url, options){
281      return this.build('crop', url, options);
282    }
283  };
284
285  var Embedly = function (element, url, options) {
286    this.init(element, url, options);
287  };
288
289  Embedly.prototype = {
290    init: function(elem, original_url, options){
291      this.elem = elem;
292      this.$elem = $(elem);
293      this.original_url = original_url;
294      this.options = options;
295      this.loaded = $.Deferred();
296
297      // Sets up some triggers.
298      var self = this;
299      this.loaded.done(function(){
300        self.$elem.trigger('loaded', [self]);
301      });
302
303      // So you can listen when the tag has been initialized;
304      this.$elem.trigger('initialized', [this]);
305    },
306    progress: function(obj){
307      $.extend(this, obj);
308
309      // if there is a custom display method, use it.
310      if (this.options.display){
311        this.options.display.apply(this.elem, [this, this.elem]);
312      }
313      // We only have a simple case for oEmbed. Everything else should be a custom
314      // success method.
315      else if(this.options.endpoint === 'oembed'){
316        this.display();
317      }
318
319      // Notifies all listeners that the data has been loaded.
320      this.loaded.resolve(this);
321    },
322    imageStyle: function(){
323      var style = [], units;
324      if (this.options.addImageStyles) {
325        if (this.options.query.maxwidth) {
326          units = isNaN(parseInt(this.options.query.maxwidth, 10)) ? '' : 'px';
327            style.push("max-width: " + (this.options.query.maxwidth)+units);
328        }
329        if (this.options.query.maxheight) {
330          units = isNaN(parseInt(this.options.query.maxheight,10)) ? '' : 'px';
331            style.push("max-height: " + (this.options.query.maxheight)+units);
332          }
333       }
334       return style.join(';');
335    },
336
337    display: function(){
338      // Ignore errors
339      if (this.type === 'error'){
340        return false;
341      }
342
343      // Image Style.
344      this.style = this.imageStyle();
345
346      var html;
347      if (this.type === 'photo'){
348        html = "<a href='" + this.original_url + "' target='_blank'>";
349        html += "<img style='" + this.style + "' src='" + this.url + "' alt='" + this.title + "' /></a>";
350      } else if (this.type === 'video' || this.type === 'rich'){
351        html = this.html;
352      } else {
353        this.title = this.title || this.url;
354        html = this.thumbnail_url ? "<img src='" + this.thumbnail_url + "' class='thumb' style='" + this.style + "'/>" : "";
355        html += "<a href='" + this.original_url + "'>" + this.title + "</a>";
356        html += this.provider_name ? "<a href='" + this.provider_url + "' class='provider'>" + this.provider_name + "</a>" : "";
357        html += this.description ? '<div class="description">' + this.description + '</div>' : '';
358      }
359
360      if (this.options.wrapElement) {
361        html = '<' + this.options.wrapElement+ ' class="' + this.options.className + '">' + html + '</' + this.options.wrapElement + '>';
362      }
363
364      this.code = html;
365      // Yay.
366      if (this.options.method === 'replace'){
367        this.$elem.replaceWith(this.code);
368      } else if (this.options.method === 'after'){
369        this.$elem.after(this.code);
370      } else if (this.options.method === 'afterParent'){
371        this.$elem.parent().after(this.code);
372      } else if (this.options.method === 'replaceParent'){
373        this.$elem.parent().replaceWith(this.code);
374      }
375      // for DOM elements we add the oembed object as a data field to that element and trigger a custom event called oembed
376      // with the custom event, developers can do any number of custom interactions with the data that is returned.
377      this.$elem.trigger('displayed', [this]);
378    }
379  };
380
381  // Sets up a generic API for use.
382  $.embedly = new API();
383
384  // Add display to it.
385  $.embedly.display = new ImageAPI();
386
387  $.fn.embedly = function ( options ) {
388    if (options === undefined || typeof options === 'object') {
389
390      // Use the defaults
391      options = $.extend({}, defaults, $.embedly.defaults, typeof options === 'object' && options);
392
393      // Kill these early.
394      if (none(options.key)){
395        $.embedly.log('error', 'Embedly jQuery requires an API Key. Please sign up for one at http://embed.ly');
396        return this.each($.noop);
397      }
398      // Keep track of the nodes we are working on so we can add them to the
399      // progress events.
400      var nodes = {};
401
402      // Create the node.
403      var create = function (elem){
404        if (!$.data($(elem), 'embedly')) {
405          var url = $(elem).attr('href');
406
407          var node = new Embedly(elem, url, options);
408          $.data(elem, 'embedly', node);
409
410          if (nodes.hasOwnProperty(url)){
411            nodes[url].push(node);
412          } else {
413            nodes[url] = [node];
414          }
415        }
416      };
417
418      // Find everything with a URL on it.
419      var elems = this.each(function () {
420        if ( !none($(this).attr('href')) ){
421          create(this);
422        } else {
423          $(this).find('a').each(function(){
424            if ( ! none($(this).attr('href')) ){
425              create(this);
426            }
427          });
428        }
429      });
430
431      // set up the api call.
432      var deferred = $.embedly.ajax(options.endpoint,
433        $.map(nodes, function(value, key) {return key;}),
434        options)
435        .progress(function(obj){
436          $.each(nodes[obj.original_url], function(i, node){
437            node.progress(obj);
438          });
439        });
440
441      if (options.progress){
442        deferred.progress(options.progress);
443      }
444      if (options.done){
445        deferred.done(options.done);
446      }
447      return elems;
448    }
449  };
450
451  // Custom selector.
452  $.expr[':'].embedly = function(elem) {
453    return ! none($(elem).data('embedly'));
454  };
455
456  // Use with selector to find img tags with data-src attribute
457  // e.g. <img data-src="http://embed.ly/static/images/logo.png"></img>
458  $.fn.display = function (endpoint, options) {
459
460    // default to display
461    if (none(endpoint)) {
462      endpoint = 'display';
463    }
464
465    if (options === undefined || typeof options === 'object') {
466
467      // Use the defaults
468      options = $.extend({}, defaults, $.embedly.defaults, typeof options === 'object' && options);
469
470      // Key Check.
471      if (none(options.key)){
472        $.embedly.log('error', 'Embedly jQuery requires an API Key. Please sign up for one at http://embed.ly/display');
473        return this.each($.noop);
474      }
475
476      // Create the node for all elements
477      var create = function (elem){
478        var $elem = $(elem);
479        if (!$elem.data('display')) {
480          var url = $elem.data('src') || $elem.attr('href');
481
482          var data = {
483            original_url : url,
484            url : $.embedly.display.build(endpoint, url, options)
485          };
486
487          $elem.data('display', data);
488          $elem.trigger('initialized', [elem]);
489
490          var html = "<img src='" + data.url + "' />";
491          if ($elem.is('a')){
492            $elem.append(html);
493          }else {
494            $elem.replaceWith(html);
495          }
496        }
497      };
498      var doCreate = function(elem){
499        if (none($(elem).data('src')) && none($(elem).attr('href'))){
500          return false;
501        }
502        return true;
503      };
504
505      // Find every image or a tag with a data-src attribute
506      var elems = this.each(function () {
507        if ( doCreate(this) ){
508          create(this);
509        } else {
510          $(this).find('img,a').each(function(){
511            if ( doCreate(this) ){
512              create(this);
513            }
514          });
515        }
516      });
517
518      return elems;
519    }
520  };
521
522  // Custom selector.
523  $.expr[':'].display = function(elem) {
524    return ! none($(elem).data('display'));
525  };
526
527}(jQuery, window));