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 }; 236 237 function SocialLikes(container, options) { 238 this.container = container; 239 this.options = options; 240 this.init(); 241 } 242 243 SocialLikes.prototype = { 244 init: function() { 245 // Add class in case of manual initialization 246 this.container.addClass(prefix); 247 248 this.single = this.container.hasClass(prefix + '_single'); 249 250 this.initUserButtons(); 251 252 this.countersLeft = 0; 253 this.number = 0; 254 this.container.on('counter.' + prefix, $.proxy(this.updateCounter, this)); 255 256 var buttons = this.container.children(); 257 258 this.makeSingleButton(); 259 260 this.buttons = []; 261 buttons.each($.proxy(function(idx, elem) { 262 var button = new Button($(elem), this.options); 263 this.buttons.push(button); 264 if (button.options.counterUrl) this.countersLeft++; 265 }, this)); 266 267 if (this.options.counters) { 268 this.timer = setTimeout($.proxy(this.appear, this), this.options.wait); 269 this.timeout = setTimeout($.proxy(this.ready, this, true), this.options.timeout); 270 } 271 else { 272 this.appear(); 273 } 274 }, 275 initUserButtons: function() { 276 if (!this.userButtonInited && window.socialLikesButtons) { 277 $.extend(true, services, socialLikesButtons); 278 } 279 this.userButtonInited = true; 280 }, 281 makeSingleButton: function() { 282 if (!this.single) return; 283 284 var container = this.container; 285 container.addClass(prefix + '_vertical'); 286 container.wrap($('<div>', {'class': prefix + '_single-w'})); 287 container.wrapInner($('<div>', {'class': prefix + '__single-container'})); 288 var wrapper = container.parent(); 289
290 // Widget 291 var widget = $('<div>', { 292 'class': getElementClassNames('widget', 'single') 293 }); 294 var button = $(template( 295 '<div class="{buttonCls}">' + 296 '<span class="{iconCls}"></span>' + 297 '{title}' + 298 '</div>', 299 { 300 buttonCls: getElementClassNames('button', 'single'), 301 iconCls: getElementClassNames('icon', 'single'), 302 title: this.options.singleTitle 303 } 304 )); 305 widget.append(button); 306 wrapper.append(widget); 307 308 widget.on('click', function() { 309 var activeClass = prefix + '__widget_active'; 310 widget.toggleClass(activeClass); 311 if (widget.hasClass(activeClass)) { 312 container.css({left: -(container.width()-widget.width())/2, top: -container.height()}); 313 showInViewport(container); 314 closeOnClick(container, function() { 315 widget.removeClass(activeClass); 316 }); 317 } 318 else { 319 container.removeClass(openClass); 320 } 321 return false; 322 }); 323 324 this.widget = widget; 325 }, 326 update: function(options) { 327 if (!options.forceUpdate && options.url === this.options.url) return; 328 329 // Reset counters 330 this.number = 0; 331 this.countersLeft = this.buttons.length; 332 if (this.widget) this.widget.find('.' + prefix + '__counter').remove(); 333 334 // Update options 335 $.extend(this.options, options); 336 for (var buttonIdx = 0; buttonIdx < this.buttons.length; buttonIdx++) { 337 this.buttons[buttonIdx].update(options); 338 } 339 }, 340 updateCounter: function(e, service, number) { 341 if (number) { 342 this.number += number; 343 if (this.single) { 344 this.getCounterElem().text(this.number); 345 } 346 } 347 348 this.countersLeft--; 349 if (this.countersLeft === 0) { 350 this.appear(); 351 this.ready(); 352 } 353 }, 354 appear: function() { 355 this.container.addClass(prefix + '_visible'); 356 }, 357 ready: function(silent) { 358 if (this.timeout) { 359 clearTimeout(this.timeout); 360 } 361 this.container.addClass(prefix + '_ready'); 362 if (!silent) { 363 this.container.trigger('ready.' + prefix, this.number); 364 } 365 }, 366 getCounterElem: function() { 367 var counterElem = this.widget.find('.' + classPrefix + 'counter_single'); 368 if (!counterElem.length) { 369 counterElem = $('<span>', { 370 'class': getElementClassNames('counter', 'single') 371 }); 372 this.widget.append(counterElem); 373 } 374 return counterElem; 375 } 376 }; 377 378 379 function Button(widget, options) { 380 this.widget = widget; 381 this.options = $.extend({}, options); 382 this.detectService(); 383 if (this.service) { 384 this.init(); 385 } 386 } 387 388 Button.prototype = { 389 init: function() { 390 this.detectParams(); 391 this.initHtml(); 392 setTimeout($.proxy(this.initCounter, this), 0); 393 }, 394 395 update: function(options) { 396 $.extend(this.options, {forceUpdate: false}, options); 397 this.widget.find('.' + prefix + '__counter').remove(); // Remove old counter 398 this.initCounter(); 399 }, 400 401 detectService: function() { 402 var service = this.widget.data('service'); 403 if (!service) { 404 // class="facebook" 405 var node = this.widget[0]; 406 var classes = node.classList || node.className.split(' '); 407 for (var classIdx = 0; classIdx < classes.length; classIdx++) { 408 var cls = classes[classIdx]; 409 if (services[cls]) { 410 service = cls; 411 break; 412 } 413 } 414 if (!service) return; 415 } 416 this.service = service; 417 $.extend(this.options, services[service]); 418 }, 419 420 detectParams: function() { 421 var data = this.widget.data(); 422 423 // Custom page counter URL or number 424 if (data.counter) { 425 var number = parseInt(data.counter, 10); 426 if (isNaN(number)) { 427 this.options.counterUrl = data.counter; 428 } 429 else { 430 this.options.counterNumber = number; 431 } 432 } 433 434 // Custom page title 435 if (data.title) { 436 this.options.title = data.title; 437 } 438 439 // Custom page URL 440 if (data.url) { 441 this.options.url = data.url; 442 } 443 }, 444 445 initHtml: function() { 446 var options = this.options; 447 var widget = this.widget; 448 449 // Old initialization HTML 450 var a = widget.find('a'); 451 if (a.length) { 452 this.cloneDataAttrs(a, widget); 453 } 454 455 // Button 456 var button = $('<span>', { 457 'class': this.getElementClassNames('button'), 458 'text': widget.text() 459 }); 460 if (options.clickUrl) { 461 var url = makeUrl(options.clickUrl, { 462 url: options.url, 463 title: options.title 464 }); 465 var link = $('<a>', { 466 href: url 467 }); 468 this.cloneDataAttrs(widget, link); 469 widget.replaceWith(link); 470 this.widget = widget = link; 471 } 472 else { 473 widget.on('click', $.proxy(this.click, this)); 474 } 475 476 widget.removeClass(this.service); 477 widget.addClass(this.getElementClassNames('widget')); 478 479 // Icon
480 button.prepend($('<span>', {'class': this.getElementClassNames('icon')})); 481 482 widget.empty().append(button); 483 widget.wrap('<div class="'+prefix+'__button_cell"/>'); 484 this.button = button; 485 }, 486 487 initCounter: function() { 488 if (this.options.counters) { 489 if (this.options.counterNumber) { 490 this.updateCounter(this.options.counterNumber); 491 } 492 else { 493 var extraOptions = { 494 counterUrl: this.options.counterUrl, 495 forceUpdate: this.options.forceUpdate 496 }; 497 counters.fetch(this.service, this.options.url, extraOptions) 498 .always($.proxy(this.updateCounter, this)); 499 } 500 } 501 }, 502 503 cloneDataAttrs: function(source, destination) { 504 var data = source.data(); 505 for (var key in data) { 506 if (data.hasOwnProperty(key)) { 507 destination.data(key, data[key]); 508 } 509 } 510 }, 511 512 getElementClassNames: function(elem) { 513 return getElementClassNames(elem, this.service); 514 }, 515 516 updateCounter: function(number) { 517 number = parseInt(number, 10) || 0; 518 519 var params = { 520 'class': this.getElementClassNames('counter'), 521 'text': number 522 }; 523 if (!number && !this.options.zeroes) { 524 params['class'] += ' ' + prefix + '__counter_empty'; 525 params.text = ''; 526 } 527 var counterElem = $('<span>', params); 528 this.widget.append(counterElem); 529 530 this.widget.trigger('counter.' + prefix, [this.service, number]); 531 }, 532 533 click: function(e) { 534 var options = this.options; 535 var process = true; 536 if ($.isFunction(options.click)) { 537 process = options.click.call(this, e); 538 } 539 if (process) { 540 var url = makeUrl(options.popupUrl, { 541 url: options.url, 542 title: options.title 543 }); 544 url = this.addAdditionalParamsToUrl(url); 545 this.openPopup(url, { 546 width: options.popupWidth, 547 height: options.popupHeight 548 }); 549 } 550 return false; 551 }, 552 553 addAdditionalParamsToUrl: function(url) { 554 var params = $.param($.extend(this.widget.data(), this.options.data)); 555 if ($.isEmptyObject(params)) return url; 556 var glue = url.indexOf('?') === -1 ? '?' : '&'; 557 return url + glue + params; 558 }, 559 560 openPopup: function(url, params) { 561 var left = Math.round(screen.width/2 - params.width/2); 562 var top = 0; 563 if (screen.height > params.height) { 564 top = Math.round(screen.height/3 - params.height/2); 565 } 566 567 var win = window.open(url, 'sl_' + this.service, 'left=' + left + ',top=' + top + ',' + 568 'width=' + params.width + ',height=' + params.height + ',personalbar=0,toolbar=0,scrollbars=1,resizable=1'); 569 if (win) { 570 win.focus(); 571 this.widget.trigger('popup_opened.' + prefix, [this.service, win]); 572 var timer = setInterval($.proxy(function() { 573 if (!win.closed) return; 574 clearInterval(timer); 575 this.widget.trigger('popup_closed.' + prefix, this.service); 576 }, this), this.options.popupCheckInterval); 577 } 578 else { 579 location.href = url; 580 } 581 } 582 }; 583 584 585 /** 586 * Helpers 587 */ 588 589 // Camelize data-attributes 590 function dataToOptions(elem) { 591 function upper(m, l) { 592 return l.toUpper(); 593 } 594 var options = {}; 595 var data = elem.data(); 596 for (var key in data) { 597 var value = data[key]; 598 if (value === 'yes') value = true; 599 else if (value === 'no') value = false;
600 options[key.replace(/-(\w)/g, upper)] = value; 601 } 602 return options; 603 } 604 605 function makeUrl(url, context) { 606 return template(url, context, encodeURIComponent); 607 } 608 609 function template(tmpl, context, filter) { 610 return tmpl.replace(/\{([^\}]+)\}/g, function(m, key) { 611 // If key doesn't exists in the context we should keep template tag as is 612 return key in context ? (filter ? filter(context[key]) : context[key]) : m; 613 }); 614 } 615 616 function getElementClassNames(elem, mod) { 617 var cls = classPrefix + elem; 618 return cls + ' ' + cls + '_' + mod; 619 } 620 621 function closeOnClick(elem, callback) { 622 function handler(e) { 623 if ((e.type === 'keydown' && e.which !== 27) || $(e.target).closest(elem).length) return; 624 elem.removeClass(openClass); 625 doc.off(events, handler); 626 if ($.isFunction(callback)) callback(); 627 } 628 var doc = $(document); 629 var events = 'click touchstart keydown'; 630 doc.on(events, handler); 631 } 632 633 function showInViewport(elem) { 634 var offset = 10; 635 if (document.documentElement.getBoundingClientRect) { 636 var left = parseInt(elem.css('left'), 10); 637 var top = parseInt(elem.css('top'), 10); 638 639 var rect = elem[0].getBoundingClientRect(); 640 if (rect.left < offset) 641 elem.css('left', offset - rect.left + left); 642 else if (rect.right > window.innerWidth - offset) 643 elem.css('left', window.innerWidth - rect.right - offset + left); 644 645 if (rect.top < offset) 646 elem.css('top', offset - rect.top + top); 647 else if (rect.bottom > window.innerHeight - offset) 648 elem.css('top', window.innerHeight - rect.bottom - offset + top); 649 } 650 elem.addClass(openClass); 651 } 652 653 654 /** 655 * Auto initialization 656 */ 657 $(function() { 658 $('.' + prefix).socialLikes(); 659 }); 660 661}));
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.