1/** 2 * Social Likes 3 * http://sapegin.github.com/social-likes 4 * 5 * Sharing buttons for Russian and worldwide social networks. 6 * 7 * @requires jQuery 8 * @author Artem Sapegin 9 * @copyright 2014 Artem Sapegin (sapegin.me) 10 * @license MIT 11 */ 12 13/*global define:false, socialLikesButtons:false */ 14 15(function(factory) { // Try to register as an anonymous AMD module 16 if (typeof define === 'function' && define.amd) { 17 define(['jquery'], factory); 18 } 19 else { 20 factory(jQuery); 21 } 22}(function($, undefined) { 23 24 'use strict'; 25 26 var prefix = 'social-likes'; 27 var classPrefix = prefix + '__'; 28 var openClass = prefix + '_opened'; 29 var protocol = location.protocol === 'https:' ? 'https:' : 'http:'; 30 var isHttps = protocol === 'https:'; 31 32 33 /** 34 * Buttons 35 */ 36 var services = { 37 facebook: { 38 // https://developers.facebook.com/docs/reference/fql/link_stat/ 39 counterUrl: 'https://graph.facebook.com/fql?q=SELECT+total_count+FROM+link_stat+WHERE+url%3D%22{url}%22&callback=?', 40 convertNumber: function(data) { 41 return data.data[0].total_count; 42 }, 43 popupUrl: 'https://www.facebook.com/sharer/sharer.php?u={url}', 44 popupWidth: 600, 45 popupHeight: 500 46 }, 47 twitter: { 48 counterUrl: 'https://cdn.api.twitter.com/1/urls/count.json?url={url}&callback=?', 49 convertNumber: function(data) { 50 return data.count; 51 }, 52 popupUrl: 'https://twitter.com/intent/tweet?url={url}&text={title}', 53 popupWidth: 600, 54 popupHeight: 450, 55 click: function() { 56 // Add colon to improve readability 57 if (!/[\.\?:\-ââ]\s*$/.test(this.options.title)) this.options.title += ':'; 58 return true; 59 } 60 }, 61 mailru: { 62 counterUrl: protocol + '//connect.mail.ru/share_count?url_list={url}&callback=1&func=?', 63 convertNumber: function(data) { 64 for (var url in data) { 65 if (data.hasOwnProperty(url)) { 66 return data[url].shares; 67 } 68 } 69 }, 70 popupUrl: protocol + '//connect.mail.ru/share?share_url={url}&title={title}', 71 popupWidth: 550, 72 popupHeight: 360 73 }, 74 vkontakte: { 75 counterUrl: 'https://vk.com/share.php?act=count&url={url}&index={index}', 76 counter: function(jsonUrl, deferred) { 77 var options = services.vkontakte; 78 if (!options._) { 79 options._ = []; 80 if (!window.VK) window.VK = {}; 81 window.VK.Share = { 82 count: function(idx, number) { 83 options._[idx].resolve(number); 84 } 85 }; 86 } 87 88 var index = options._.length; 89 options._.push(deferred); 90 $.getScript(makeUrl(jsonUrl, {index: index})) 91 .fail(deferred.reject); 92 }, 93 popupUrl: protocol + '//vk.com/share.php?url={url}&title={title}', 94 popupWidth: 550, 95 popupHeight: 330 96 }, 97 odnoklassniki: { 98 // HTTPS not supported 99 counterUrl: isHttps ? undefined : 'http://connect.ok.ru/dk?st.cmd=extLike&ref={url}&uid={index}', 100 counter: function(jsonUrl, deferred) { 101 var options = services.odnoklassniki; 102 if (!options._) { 103 options._ = []; 104 if (!window.ODKL) window.ODKL = {}; 105 window.ODKL.updateCount = function(idx, number) { 106 options._[idx].resolve(number); 107 }; 108 } 109 110 var index = options._.length; 111 options._.push(deferred); 112 $.getScript(makeUrl(jsonUrl, {index: index})) 113 .fail(deferred.reject); 114 }, 115 popupUrl: 'http://connect.ok.ru/dk?st.cmd=WidgetSharePreview&service=odnoklassniki&st.shareUrl={url}', 116 popupWidth: 550, 117 popupHeight: 360 118 }, 119 plusone: { 120 // HTTPS not supported yet: http://clubs.ya.ru/share/1499 121 counterUrl: isHttps ? undefined : 'http://share.yandex.ru/gpp.xml?url={url}', 122 counter: function(jsonUrl, deferred) { 123 var options = services.plusone; 124 if (options._) {
125 // Reject all counters except the first because Yandex Share counter doesnât return URL 126 deferred.reject(); 127 return; 128 } 129 130 if (!window.services) window.services = {}; 131 window.services.gplus = { 132 cb: function(number) { 133 if (typeof number === 'string') { 134 number = number.replace(/\D/g, ''); 135 } 136 options._.resolve(parseInt(number, 10)); 137 } 138 }; 139 140 options._ = deferred; 141 $.getScript(makeUrl(jsonUrl)) 142 .fail(deferred.reject); 143 }, 144 popupUrl: 'https://plus.google.com/share?url={url}', 145 popupWidth: 700, 146 popupHeight: 500 147 }, 148 pinterest: { 149 counterUrl: protocol + '//api.pinterest.com/v1/urls/count.json?url={url}&callback=?', 150 convertNumber: function(data) { 151 return data.count; 152 }, 153 popupUrl: protocol + '//pinterest.com/pin/create/button/?url={url}&description={title}', 154 popupWidth: 630, 155 popupHeight: 270 156 } 157 }; 158 159 160 /** 161 * Counters manager 162 */ 163 var counters = { 164 promises: {}, 165 fetch: function(service, url, extraOptions) { 166 if (!counters.promises[service]) counters.promises[service] = {}; 167 var servicePromises = counters.promises[service]; 168 169 if (!extraOptions.forceUpdate && servicePromises[url]) { 170 return servicePromises[url]; 171 } 172 else { 173 var options = $.extend({}, services[service], extraOptions); 174 var deferred = $.Deferred(); 175 var jsonUrl = options.counterUrl && makeUrl(options.counterUrl, {url: url}); 176 177 if (jsonUrl && $.isFunction(options.counter)) { 178 options.counter(jsonUrl, deferred); 179 } 180 else if (options.counterUrl) { 181 $.getJSON(jsonUrl) 182 .done(function(data) { 183 try { 184 var number = data; 185 if ($.isFunction(options.convertNumber)) { 186 number = options.convertNumber(data); 187 } 188 deferred.resolve(number); 189 } 190 catch (e) { 191 deferred.reject(); 192 } 193 }) 194 .fail(deferred.reject); 195 } 196 else { 197 deferred.reject(); 198 } 199 200 servicePromises[url] = deferred.promise(); 201 return servicePromises[url]; 202 } 203 } 204 }; 205 206 207 /** 208 * jQuery plugin 209 */ 210 $.fn.socialLikes = function(options) { 211 return this.each(function() { 212 var elem = $(this); 213 var instance = elem.data(prefix); 214 if (instance) { 215 if ($.isPlainObject(options)) { 216 instance.update(options); 217 } 218 } 219 else { 220 instance = new SocialLikes(elem, $.extend({}, $.fn.socialLikes.defaults, options, dataToOptions(elem))); 221 elem.data(prefix, instance); 222 } 223 }); 224 }; 225 226 $.fn.socialLikes.defaults = { 227 url: window.location.href.replace(window.location.hash, ''), 228 title: document.title, 229 counters: true, 230 zeroes: false, 231 wait: 500, // Show buttons only after counters are ready or after this amount of time 232 timeout: 10000, // Show counters after this amount of time even if they arenât ready 233 popupCheckInterval: 500, 234 singleTitle: 'Share', 235 initHtml: true 236 }; 237 238 function SocialLikes(container, options) { 239 this.container = container; 240 this.options = options; 241 this.init(); 242 } 243 244 SocialLikes.prototype = { 245 init: function() { 246 // Add class in case of manual initialization 247 this.container.addClass(prefix); 248 249 this.single = this.container.hasClass(prefix + '_single'); 250 251 this.initUserButtons(); 252 253 this.countersLeft = 0; 254 this.number = 0; 255 this.container.on('counter.' + prefix, $.proxy(this.updateCounter, this)); 256 257 var buttons = this.container.children(); 258 259 this.makeSingleButton(); 260 261 this.buttons = []; 262 buttons.each($.proxy(function(idx, elem) { 263 var button = new Button($(elem), this.options); 264 this.buttons.push(button); 265 if (button.options.counterUrl) this.countersLeft++; 266 }, this)); 267 268 if (this.options.counters) { 269 this.timer = setTimeout($.proxy(this.appear, this), this.options.wait); 270 this.timeout = setTimeout($.proxy(this.ready, this, true), this.options.timeout); 271 } 272 else { 273 this.appear(); 274 } 275 }, 276 initUserButtons: function() { 277 if (!this.userButtonInited && window.socialLikesButtons) { 278 $.extend(true, services, socialLikesButtons); 279 } 280 this.userButtonInited = true; 281 }, 282 makeSingleButton: function() { 283 if (!this.single) return; 284 285 var container = this.container; 286 container.addClass(prefix + '_vertical'); 287 container.wrap($('<div>', {'class': prefix + '_single-w'})); 288 container.wrapInner($('<div>', {'class': prefix + '__single-container'})); 289 var wrapper = container.parent(); 290
291 // Widget 292 var widget = $('<div>', { 293 'class': getElementClassNames('widget', 'single') 294 }); 295 var button = $(template( 296 '<div class="{buttonCls}">' + 297 '<span class="{iconCls}"></span>' + 298 '{title}' + 299 '</div>', 300 { 301 buttonCls: getElementClassNames('button', 'single'), 302 iconCls: getElementClassNames('icon', 'single'), 303 title: this.options.singleTitle 304 } 305 )); 306 widget.append(button); 307 wrapper.append(widget); 308 309 widget.on('click', function() { 310 var activeClass = prefix + '__widget_active'; 311 widget.toggleClass(activeClass); 312 if (widget.hasClass(activeClass)) { 313 container.css({left: -(container.width()-widget.width())/2, top: -container.height()}); 314 showInViewport(container); 315 closeOnClick(container, function() { 316 widget.removeClass(activeClass); 317 }); 318 } 319 else { 320 container.removeClass(openClass); 321 } 322 return false; 323 }); 324 325 this.widget = widget; 326 }, 327 update: function(options) { 328 if (!options.forceUpdate && options.url === this.options.url) return; 329 330 // Reset counters 331 this.number = 0; 332 this.countersLeft = this.buttons.length; 333 if (this.widget) this.widget.find('.' + prefix + '__counter').remove(); 334 335 // Update options 336 $.extend(this.options, options); 337 for (var buttonIdx = 0; buttonIdx < this.buttons.length; buttonIdx++) { 338 this.buttons[buttonIdx].update(options); 339 } 340 }, 341 updateCounter: function(e, service, number) { 342 if (number) { 343 this.number += number; 344 if (this.single) { 345 this.getCounterElem().text(this.number); 346 } 347 } 348 349 this.countersLeft--; 350 if (this.countersLeft === 0) { 351 this.appear(); 352 this.ready(); 353 } 354 }, 355 appear: function() { 356 this.container.addClass(prefix + '_visible'); 357 }, 358 ready: function(silent) { 359 if (this.timeout) { 360 clearTimeout(this.timeout); 361 } 362 this.container.addClass(prefix + '_ready'); 363 if (!silent) { 364 this.container.trigger('ready.' + prefix, this.number); 365 } 366 }, 367 getCounterElem: function() { 368 var counterElem = this.widget.find('.' + classPrefix + 'counter_single'); 369 if (!counterElem.length) { 370 counterElem = $('<span>', { 371 'class': getElementClassNames('counter', 'single') 372 }); 373 this.widget.append(counterElem); 374 } 375 return counterElem; 376 } 377 }; 378 379 380 function Button(widget, options) { 381 this.widget = widget; 382 this.options = $.extend({}, options); 383 this.detectService(); 384 if (this.service) { 385 this.init(); 386 } 387 } 388 389 Button.prototype = { 390 init: function() { 391 this.detectParams(); 392 if (this.options.initHtml) this.initHtml(); 393 else this.widget.on('click', $.proxy(this.click, this)); 394 setTimeout($.proxy(this.initCounter, this), 0); 395 }, 396 397 update: function(options) { 398 $.extend(this.options, {forceUpdate: false}, options); 399 this.widget.find('.' + prefix + '__counter').remove(); // Remove old counter 400 this.initCounter(); 401 }, 402 403 detectService: function() { 404 var service = this.widget.data('service'); 405 if (!service) { 406 // class="facebook" 407 var node = this.widget[0]; 408 var classes = node.classList || node.className.split(' '); 409 for (var classIdx = 0; classIdx < classes.length; classIdx++) { 410 var cls = classes[classIdx]; 411 if (services[cls]) { 412 service = cls; 413 break; 414 } 415 } 416 if (!service) return; 417 } 418 this.service = service; 419 $.extend(this.options, services[service]); 420 }, 421 422 detectParams: function() { 423 var data = this.widget.data(); 424 425 // Custom page counter URL or number 426 if (data.counter) { 427 var number = parseInt(data.counter, 10); 428 if (isNaN(number)) { 429 this.options.counterUrl = data.counter; 430 } 431 else { 432 this.options.counterNumber = number; 433 } 434 } 435 436 // Custom page title 437 if (data.title) { 438 this.options.title = data.title; 439 } 440 441 // Custom page URL 442 if (data.url) { 443 this.options.url = data.url; 444 } 445 }, 446 447 initHtml: function() { 448 var options = this.options; 449 var widget = this.widget; 450 451 // Old initialization HTML 452 var a = widget.find('a'); 453 if (a.length) { 454 this.cloneDataAttrs(a, widget); 455 } 456 457 // Button 458 var button = $('<span>', { 459 'class': this.getElementClassNames('button'), 460 'text': widget.text() 461 }); 462 if (options.clickUrl) { 463 var url = makeUrl(options.clickUrl, { 464 url: options.url, 465 title: options.title 466 }); 467 var link = $('<a>', { 468 href: url 469 }); 470 this.cloneDataAttrs(widget, link); 471 widget.replaceWith(link); 472 this.widget = widget = link; 473 } 474 else { 475 widget.on('click', $.proxy(this.click, this)); 476 } 477 478 widget.removeClass(this.service); 479 widget.addClass(this.getElementClassNames('widget')); 480 481 // Icon
482 button.prepend($('<span>', {'class': this.getElementClassNames('icon')})); 483 484 widget.empty().append(button); 485 this.button = button; 486 }, 487 488 initCounter: function() { 489 if (this.options.counters) { 490 if (this.options.counterNumber) { 491 this.updateCounter(this.options.counterNumber); 492 } 493 else { 494 var extraOptions = { 495 counterUrl: this.options.counterUrl, 496 forceUpdate: this.options.forceUpdate 497 }; 498 counters.fetch(this.service, this.options.url, extraOptions) 499 .always($.proxy(this.updateCounter, this)); 500 } 501 } 502 }, 503 504 cloneDataAttrs: function(source, destination) { 505 var data = source.data(); 506 for (var key in data) { 507 if (data.hasOwnProperty(key)) { 508 destination.data(key, data[key]); 509 } 510 } 511 }, 512 513 getElementClassNames: function(elem) { 514 return getElementClassNames(elem, this.service); 515 }, 516 517 updateCounter: function(number) { 518 number = parseInt(number, 10) || 0; 519 520 var params = { 521 'class': this.getElementClassNames('counter'), 522 'text': number 523 }; 524 if (!number && !this.options.zeroes) { 525 params['class'] += ' ' + prefix + '__counter_empty'; 526 params.text = ''; 527 } 528 var counterElem = $('<span>', params); 529 this.widget.append(counterElem); 530 531 this.widget.trigger('counter.' + prefix, [this.service, number]); 532 }, 533 534 click: function(e) { 535 var options = this.options; 536 var process = true; 537 if ($.isFunction(options.click)) { 538 process = options.click.call(this, e); 539 } 540 if (process) { 541 var url = makeUrl(options.popupUrl, { 542 url: options.url, 543 title: options.title 544 }); 545 url = this.addAdditionalParamsToUrl(url); 546 this.openPopup(url, { 547 width: options.popupWidth, 548 height: options.popupHeight 549 }); 550 } 551 return false; 552 }, 553 554 addAdditionalParamsToUrl: function(url) { 555 var params = $.param($.extend(this.widget.data(), this.options.data)); 556 if ($.isEmptyObject(params)) return url; 557 var glue = url.indexOf('?') === -1 ? '?' : '&'; 558 return url + glue + params; 559 }, 560 561 openPopup: function(url, params) { 562 var left = Math.round(screen.width/2 - params.width/2); 563 var top = 0; 564 if (screen.height > params.height) { 565 top = Math.round(screen.height/3 - params.height/2); 566 } 567 568 var win = window.open(url, 'sl_' + this.service, 'left=' + left + ',top=' + top + ',' + 569 'width=' + params.width + ',height=' + params.height + ',personalbar=0,toolbar=0,scrollbars=1,resizable=1'); 570 if (win) { 571 win.focus(); 572 this.widget.trigger('popup_opened.' + prefix, [this.service, win]); 573 var timer = setInterval($.proxy(function() { 574 if (!win.closed) return; 575 clearInterval(timer); 576 this.widget.trigger('popup_closed.' + prefix, this.service); 577 }, this), this.options.popupCheckInterval); 578 } 579 else { 580 location.href = url; 581 } 582 } 583 }; 584 585 586 /** 587 * Helpers 588 */ 589 590 // Camelize data-attributes 591 function dataToOptions(elem) { 592 function upper(m, l) { 593 return l.toUpper(); 594 } 595 var options = {}; 596 var data = elem.data(); 597 for (var key in data) { 598 var value = data[key]; 599 if (value === 'yes') value = true; 600 else if (value === 'no') value = false;
601 options[key.replace(/-(\w)/g, upper)] = value; 602 } 603 return options; 604 } 605 606 function makeUrl(url, context) { 607 return template(url, context, encodeURIComponent); 608 } 609 610 function template(tmpl, context, filter) { 611 return tmpl.replace(/\{([^\}]+)\}/g, function(m, key) { 612 // If key doesn't exists in the context we should keep template tag as is 613 return key in context ? (filter ? filter(context[key]) : context[key]) : m; 614 }); 615 } 616 617 function getElementClassNames(elem, mod) { 618 var cls = classPrefix + elem; 619 return cls + ' ' + cls + '_' + mod; 620 } 621 622 function closeOnClick(elem, callback) { 623 function handler(e) { 624 if ((e.type === 'keydown' && e.which !== 27) || $(e.target).closest(elem).length) return; 625 elem.removeClass(openClass); 626 doc.off(events, handler); 627 if ($.isFunction(callback)) callback(); 628 } 629 var doc = $(document); 630 var events = 'click touchstart keydown'; 631 doc.on(events, handler); 632 } 633 634 function showInViewport(elem) { 635 var offset = 10; 636 if (document.documentElement.getBoundingClientRect) { 637 var left = parseInt(elem.css('left'), 10); 638 var top = parseInt(elem.css('top'), 10); 639 640 var rect = elem[0].getBoundingClientRect(); 641 if (rect.left < offset) 642 elem.css('left', offset - rect.left + left); 643 else if (rect.right > window.innerWidth - offset) 644 elem.css('left', window.innerWidth - rect.right - offset + left); 645 646 if (rect.top < offset) 647 elem.css('top', offset - rect.top + top); 648 else if (rect.bottom > window.innerHeight - offset) 649 elem.css('top', window.innerHeight - rect.bottom - offset + top); 650 } 651 elem.addClass(openClass); 652 } 653 654 655 /** 656 * Auto initialization 657 */ 658 $(function() { 659 $('.' + prefix).socialLikes(); 660 }); 661 662}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.