├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .stylelint.json ├── .vscode ├── extensions.json └── settings.json ├── build.json ├── changelog.md ├── demo ├── error.html ├── index.html └── src │ ├── js │ ├── app.js │ └── loadSprite.js │ └── less │ ├── app.less │ ├── components │ ├── base.less │ ├── buttons.less │ ├── error.less │ ├── examples.less │ └── type.less │ ├── lib │ ├── fontface.less │ ├── mixins.less │ └── normalize.less │ └── variables.less ├── deploy.json ├── dist ├── app.css ├── app.js ├── app.js.map ├── shr.css ├── shr.js ├── shr.js.map ├── shr.mjs ├── shr.mjs.map └── shr.svg ├── gulpfile.js ├── license.md ├── package.json ├── readme.md ├── shr.code-workspace ├── src ├── js │ ├── config │ │ ├── constants.js │ │ └── defaults.js │ ├── shr.js │ └── utils │ │ ├── ajax.js │ │ ├── console.js │ │ ├── css.js │ │ ├── elements.js │ │ ├── is.js │ │ ├── numbers.js │ │ ├── objects.js │ │ ├── storage.js │ │ └── urls.js ├── sass │ ├── button.scss │ ├── mixins.scss │ ├── settings.scss │ └── shr.scss └── sprite │ ├── shr-facebook.svg │ ├── shr-github.svg │ ├── shr-google.svg │ ├── shr-pinterest.svg │ ├── shr-twitter.svg │ └── shr-youtube.svg └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # See editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb-base", "prettier"], 4 | "plugins": ["simple-import-sort", "import"], 5 | "env": { 6 | "browser": true, 7 | "es6": true 8 | }, 9 | "rules": { 10 | "padding-line-between-statements": [ 11 | "error", 12 | { 13 | "blankLine": "never", 14 | "prev": ["singleline-const", "singleline-let", "singleline-var"], 15 | "next": ["singleline-const", "singleline-let", "singleline-var"] 16 | } 17 | ], 18 | "sort-imports": "off", 19 | "import/order": "off", 20 | "simple-import-sort/sort": "error" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | docs/index.dev.html 4 | credentials.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.stylelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"], 3 | "extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"], 4 | "rules": { 5 | "selector-class-pattern": null, 6 | "selector-no-qualifying-type": [ 7 | true, 8 | { 9 | "ignore": ["attribute", "class"] 10 | } 11 | ], 12 | "string-no-newline": null, 13 | "indentation": 4, 14 | "string-quotes": "single", 15 | "max-nesting-depth": 2, 16 | "plugin/selector-bem-pattern": { 17 | "preset": "bem", 18 | "componentName": "(([a-z0-9]+(?!-$)-?)+)", 19 | "componentSelectors": { 20 | "initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$" 21 | }, 22 | "ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "dbaeumer.vscode-eslint", 7 | "wix.vscode-import-cost", 8 | "esbenp.prettier-vscode", 9 | "shinnn.stylelint", 10 | "wayou.vscode-todo-highlight" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /build.json: -------------------------------------------------------------------------------- 1 | { 2 | "js": { 3 | "shr.js": { 4 | "src": "./src/js/shr.js", 5 | "dist": "./dist/", 6 | "formats": ["es", "umd"] 7 | }, 8 | "app.js": { 9 | "src": "./demo/src/js/app.js", 10 | "dist": "./dist/", 11 | "formats": ["iife"] 12 | } 13 | }, 14 | "css": { 15 | "app.css": { 16 | "src": "./demo/src/less/app.less", 17 | "dist": "./dist/" 18 | }, 19 | "shr.css": { 20 | "src": "./src/sass/shr.scss", 21 | "dist": "./dist/" 22 | } 23 | }, 24 | "sprite": { 25 | "shr.svg": { 26 | "src": "./src/sprite/*.svg", 27 | "dist": "./dist" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # v2.0.2 4 | 5 | - Fixed a bug with handling NaN counts 6 | - Fixed a bug with `displayZero` default 7 | - Fixed a bug with increment on click not working as expected 8 | - Clean up 9 | 10 | # v2.0.1 11 | 12 | - Fixed a bug with numbers over 1 million not formatting correctly 13 | 14 | # v2.0.0 15 | 16 | - Complete re-write in ES6 17 | - YouTube subscribe button added 18 | - Google+ removed 19 | - New API methods 20 | - New design 21 | 22 | ## v1.1.0 23 | 24 | - Added `increment` option for on click to add 1 to the count (if supported). This _assumes_ the share was successful of course 25 | - Cleaned up the default CSS for the buttons etc and freshened the styles slightly. Should fix issues such as #1 26 | - Fixed bug with `before` count option not displaying correctly 27 | - Removed the need for a countainer around the button and count (beware, this may result in wrapping) 28 | 29 | ## v1.0.5 30 | 31 | - Fix for local storage - thanks @danfoley 32 | - Default to formatted numbers - e.g. 1K rather than 1000 33 | 34 | ## v1.0.4 35 | 36 | - Small bug fix 37 | 38 | ## v1.0.3 39 | 40 | - Small bug fix 41 | 42 | ## v1.0.2 43 | 44 | - Security fix 45 | 46 | ## v1.0.1 47 | 48 | - Bug fix related to debugging 49 | 50 | ## v1.0.0 51 | 52 | - Repo move 53 | 54 | ## v0.1.9 55 | 56 | - Bug fix 57 | 58 | ## v0.1.8 59 | 60 | - Fix for Twitter removal of count 61 | 62 | ## v0.1.7 63 | 64 | - Made GitHub token optional 65 | 66 | ## v0.1.6 67 | 68 | - Fixes for IE9 69 | - Fixed toLocaleString() for old IE 70 | - Added local storage caching 71 | - Added support for GitHub (stars, forks, etc) 72 | - Jazzed up docs 73 | -------------------------------------------------------------------------------- /demo/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Doh. Looks like something went wrong. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 26 | 33 | 40 | 41 | 42 | 43 |
44 |

Doh.

45 |

Looks like something went wrong.

46 | Back to shr.one 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shr - Simple, clean, and customizable social sharing buttons 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 44 | 51 | 58 | 59 | 60 | 61 | 69 | 70 | 71 | 72 |
73 |

Share

74 |

75 | Simple, clean, and customizable social sharing buttons by 76 | @sam_potts 77 |

78 |
79 | 80 |
81 |
82 | 87 | Star 88 | 89 | 90 | 97 | 98 | 105 | 106 | 111 | Pin it 112 | 113 |
114 |
115 | 116 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /demo/src/js/app.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Docs example 3 | // ========================================================================== 4 | 5 | import Shr from '../../../src/js/shr'; 6 | import loadSprite from './loadSprite'; 7 | 8 | document.addEventListener('DOMContentLoaded', () => { 9 | Shr.setup('.js-shr', { 10 | debug: true, 11 | tokens: { 12 | youtube: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c', 13 | }, 14 | }); 15 | 16 | loadSprite('../dist/shr.svg'); 17 | }); 18 | -------------------------------------------------------------------------------- /demo/src/js/loadSprite.js: -------------------------------------------------------------------------------- 1 | export default function loadSprite(url) { 2 | const xhr = new XMLHttpRequest(); 3 | 4 | xhr.open('GET', url, true); 5 | 6 | xhr.onload = () => { 7 | const container = document.createElement('div'); 8 | container.setAttribute('hidden', ''); 9 | container.innerHTML = xhr.responseText; 10 | document.body.insertBefore(container, document.body.childNodes[0]); 11 | }; 12 | 13 | xhr.send(); 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/less/app.less: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // HTML5 Video Player Demo Page 3 | // ========================================================================== 4 | 5 | // CSS Reset 6 | @import "lib/normalize"; 7 | 8 | // Mixins 9 | @import "lib/mixins"; 10 | 11 | // Variables 12 | @import "variables"; 13 | 14 | // Base layout 15 | @import "components/base"; 16 | 17 | // Type 18 | @import "lib/fontface"; 19 | @import "components/type"; 20 | 21 | // Examples 22 | @import "components/examples"; 23 | @import "components/buttons"; 24 | 25 | // Error 26 | @import "components/error"; -------------------------------------------------------------------------------- /demo/src/less/components/base.less: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Base layout 3 | // ========================================================================== 4 | 5 | // BORDER-BOX ALL THE THINGS! 6 | // http://paulirish.com/2012/box-sizing-border-box-ftw/ 7 | *, 8 | *::after, 9 | *::before { 10 | box-sizing: border-box; 11 | } 12 | 13 | // Hidden 14 | [hidden] { 15 | display: none; 16 | } 17 | 18 | // Base 19 | html { 20 | font-size: 100%; 21 | height: 100%; 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | body { 27 | width: 100%; 28 | padding: @padding-base; 29 | background: @body-background; 30 | font-family: @font-family; 31 | .font-size(); 32 | .font-smoothing(on); 33 | line-height: @line-height; 34 | text-align: center; 35 | color: @body-text-color; 36 | } 37 | 38 | // Header 39 | header { 40 | padding-bottom: @padding-base; 41 | 42 | p { 43 | .font-size(18); 44 | 45 | &:last-child { 46 | margin: 0; 47 | } 48 | } 49 | 50 | @media (min-width: @screen-sm) { 51 | padding-bottom: (@padding-base * 3); 52 | } 53 | } 54 | 55 | // Sections 56 | section { 57 | padding-bottom: @padding-base; 58 | 59 | @media (min-width: @screen-sm) { 60 | padding-bottom: (@padding-base * 2); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /demo/src/less/components/buttons.less: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Buttons 3 | // ========================================================================== 4 | 5 | .btn { 6 | display: inline-flex; 7 | padding: ceil(@padding-base / 1.75) ceil(@padding-base * 1.25); 8 | 9 | .button-styles(@button-background); 10 | border-radius: 4px; 11 | transition: all 0.3s ease; 12 | 13 | color: @button-text-color; 14 | text-decoration: none; 15 | .font-size(); 16 | font-weight: 600; 17 | 18 | &:focus { 19 | outline: 0; 20 | } 21 | 22 | &:hover, 23 | &:focus { 24 | border: 0; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/src/less/components/error.less: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Errors (AWS pages) 3 | // ========================================================================== 4 | 5 | // Error page 6 | html.error, 7 | .error body { 8 | height: 100%; 9 | } 10 | .error body { 11 | width: 100%; 12 | display: table; 13 | table-layout: fixed; 14 | } 15 | .error main { 16 | display: table-cell; 17 | width: 100%; 18 | vertical-align: middle; 19 | } -------------------------------------------------------------------------------- /demo/src/less/components/examples.less: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Examples 3 | // ========================================================================== 4 | .examples { 5 | margin-bottom: @padding-base; 6 | text-shadow: none; 7 | 8 | .shr-button { 9 | margin-top: (@padding-base / 4); 10 | margin-bottom: (@padding-base / 4); 11 | } 12 | 13 | @media (min-width: @screen-sm) { 14 | margin-bottom: (@padding-base * 3); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/less/components/type.less: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Typography 3 | // ========================================================================== 4 | 5 | // Headings 6 | h1 { 7 | letter-spacing: -.025em; 8 | margin: 0 0 (@padding-base / 2); 9 | line-height: 1.2; 10 | .font-size(64); 11 | color: @color-headings; 12 | 13 | span { 14 | font-weight: @font-weight-regular; 15 | color: lighten(@body-background, 5%); 16 | } 17 | } 18 | 19 | // Paragraph and small 20 | p, 21 | small { 22 | margin: 0 0 @padding-base; 23 | } 24 | small { 25 | display: block; 26 | padding: 0 (@padding-base / 2); 27 | .font-size(@font-size-small); 28 | } 29 | 30 | // Links 31 | a { 32 | text-decoration: none; 33 | color: @link-color; 34 | transition: all 0.3s ease; 35 | 36 | &:hover, 37 | &:focus { 38 | border-bottom: 1px solid; 39 | } 40 | 41 | &:focus { 42 | .tab-focus(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/src/less/lib/fontface.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-display: swap; 3 | font-family: 'Gordita'; 4 | font-style: normal; 5 | font-weight: @font-weight-light; 6 | src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'), 7 | url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff'); 8 | } 9 | 10 | @font-face { 11 | font-display: swap; 12 | font-family: 'Gordita'; 13 | font-style: normal; 14 | font-weight: @font-weight-regular; 15 | src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'), 16 | url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff'); 17 | } 18 | 19 | @font-face { 20 | font-display: swap; 21 | font-family: 'Gordita'; 22 | font-style: normal; 23 | font-weight: @font-weight-medium; 24 | src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'), 25 | url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff'); 26 | } 27 | 28 | @font-face { 29 | font-display: swap; 30 | font-family: 'Gordita'; 31 | font-style: normal; 32 | font-weight: @font-weight-bold; 33 | src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'), 34 | url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff'); 35 | } 36 | 37 | @font-face { 38 | font-display: swap; 39 | font-family: 'Gordita'; 40 | font-style: normal; 41 | font-weight: @font-weight-black; 42 | src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'), 43 | url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff'); 44 | } 45 | -------------------------------------------------------------------------------- /demo/src/less/lib/mixins.less: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Mixins 3 | // ========================================================================== 4 | 5 | // Contain floats: nicolasgallagher.com/micro-clearfix-hack/ 6 | // --------------------------------------- 7 | .clearfix() { 8 | zoom: 1; 9 | &:before, 10 | &:after { 11 | content: ""; 12 | display: table; 13 | } 14 | &:after { 15 | clear: both; 16 | } 17 | } 18 | 19 | // Webkit-style focus 20 | // --------------------------------------- 21 | .tab-focus() { 22 | // Default 23 | outline: thin dotted @gray-dark; 24 | // Webkit 25 | outline-offset: 1px; 26 | } 27 | 28 | // Use rems for font sizing 29 | // Leave at 100%/16px 30 | // --------------------------------------- 31 | .font-size(@font-size: @font-size-base) { 32 | @rem: round((@font-size / 16), 3); 33 | font-size: (@font-size * 1px); 34 | font-size: ~"@{rem}rem"; 35 | } 36 | 37 | // Font smoothing 38 | // --------------------------------------- 39 | .font-smoothing(@mode: on) when(@mode = on) { 40 | -moz-osx-font-smoothing: grayscale; 41 | -webkit-font-smoothing: antialiased; 42 | } 43 | .font-smoothing(@mode: on) when(@mode = off) { 44 | -moz-osx-font-smoothing: auto; 45 | -webkit-font-smoothing: subpixel-antialiased; 46 | } 47 | 48 | // Button styling 49 | // --------------------------------------- 50 | .button-styles(@background-color: @shr-button-base-bg-color, @focus-color: fade(@background-color, 50%)) { 51 | background-color: @background-color; 52 | border: 0; 53 | box-shadow: 0 1px 1px fade(#000, 5%); 54 | 55 | &:hover, 56 | &:focus { 57 | background-color: darken(@background-color, 3%); 58 | } 59 | 60 | &:focus { 61 | box-shadow: 0 0 0 3px @focus-color; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /demo/src/less/lib/normalize.less: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.3 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /** 8 | * Correct `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 8/9. 28 | */ 29 | 30 | audio, 31 | canvas, 32 | video { 33 | display: inline-block; 34 | } 35 | 36 | /** 37 | * Prevent modern browsers from displaying `audio` without controls. 38 | * Remove excess height in iOS 5 devices. 39 | */ 40 | 41 | audio:not([controls]) { 42 | display: none; 43 | height: 0; 44 | } 45 | 46 | /** 47 | * Address `[hidden]` styling not present in IE 8/9. 48 | * Hide the `template` element in IE, Safari, and Firefox < 22. 49 | */ 50 | 51 | [hidden], 52 | template { 53 | display: none; 54 | } 55 | 56 | /* ========================================================================== 57 | Base 58 | ========================================================================== */ 59 | 60 | /** 61 | * 1. Set default font family to sans-serif. 62 | * 2. Prevent iOS text size adjust after orientation change, without disabling 63 | * user zoom. 64 | */ 65 | 66 | html { 67 | font-family: sans-serif; /* 1 */ 68 | -ms-text-size-adjust: 100%; /* 2 */ 69 | -webkit-text-size-adjust: 100%; /* 2 */ 70 | } 71 | 72 | /** 73 | * Remove default margin. 74 | */ 75 | 76 | body { 77 | margin: 0; 78 | } 79 | 80 | /* ========================================================================== 81 | Links 82 | ========================================================================== */ 83 | 84 | /** 85 | * Remove the gray background color from active links in IE 10. 86 | */ 87 | 88 | a { 89 | background: transparent; 90 | } 91 | 92 | /** 93 | * Address `outline` inconsistency between Chrome and other browsers. 94 | */ 95 | 96 | a:focus { 97 | outline: thin dotted; 98 | } 99 | 100 | /** 101 | * Improve readability when focused and also mouse hovered in all browsers. 102 | */ 103 | 104 | a:active, 105 | a:hover { 106 | outline: 0; 107 | } 108 | 109 | /* ========================================================================== 110 | Typography 111 | ========================================================================== */ 112 | 113 | /** 114 | * Address variable `h1` font-size and margin within `section` and `article` 115 | * contexts in Firefox 4+, Safari 5, and Chrome. 116 | */ 117 | 118 | h1 { 119 | font-size: 2em; 120 | margin: 0.67em 0; 121 | } 122 | 123 | /** 124 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 125 | */ 126 | 127 | abbr[title] { 128 | border-bottom: 1px dotted; 129 | } 130 | 131 | /** 132 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 133 | */ 134 | 135 | b, 136 | strong { 137 | font-weight: bold; 138 | } 139 | 140 | /** 141 | * Address styling not present in Safari 5 and Chrome. 142 | */ 143 | 144 | dfn { 145 | font-style: italic; 146 | } 147 | 148 | /** 149 | * Address differences between Firefox and other browsers. 150 | */ 151 | 152 | hr { 153 | -moz-box-sizing: content-box; 154 | box-sizing: content-box; 155 | height: 0; 156 | } 157 | 158 | /** 159 | * Address styling not present in IE 8/9. 160 | */ 161 | 162 | mark { 163 | background: #ff0; 164 | color: #000; 165 | } 166 | 167 | /** 168 | * Correct font family set oddly in Safari 5 and Chrome. 169 | */ 170 | 171 | code, 172 | kbd, 173 | pre, 174 | samp { 175 | font-family: monospace, serif; 176 | font-size: 1em; 177 | } 178 | 179 | /** 180 | * Improve readability of pre-formatted text in all browsers. 181 | */ 182 | 183 | pre { 184 | white-space: pre-wrap; 185 | } 186 | 187 | /** 188 | * Set consistent quote types. 189 | */ 190 | 191 | q { 192 | quotes: "\201C" "\201D" "\2018" "\2019"; 193 | } 194 | 195 | /** 196 | * Address inconsistent and variable font size in all browsers. 197 | */ 198 | 199 | small { 200 | font-size: 80%; 201 | } 202 | 203 | /** 204 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 205 | */ 206 | 207 | sub, 208 | sup { 209 | font-size: 75%; 210 | line-height: 0; 211 | position: relative; 212 | vertical-align: baseline; 213 | } 214 | 215 | sup { 216 | top: -0.5em; 217 | } 218 | 219 | sub { 220 | bottom: -0.25em; 221 | } 222 | 223 | /* ========================================================================== 224 | Embedded content 225 | ========================================================================== */ 226 | 227 | /** 228 | * Remove border when inside `a` element in IE 8/9. 229 | */ 230 | 231 | img { 232 | border: 0; 233 | } 234 | 235 | /** 236 | * Correct overflow displayed oddly in IE 9. 237 | */ 238 | 239 | svg:not(:root) { 240 | overflow: hidden; 241 | } 242 | 243 | /* ========================================================================== 244 | Figures 245 | ========================================================================== */ 246 | 247 | /** 248 | * Address margin not present in IE 8/9 and Safari 5. 249 | */ 250 | 251 | figure { 252 | margin: 0; 253 | } 254 | 255 | /* ========================================================================== 256 | Forms 257 | ========================================================================== */ 258 | 259 | /** 260 | * Define consistent border, margin, and padding. 261 | */ 262 | 263 | fieldset { 264 | border: 1px solid #c0c0c0; 265 | margin: 0 2px; 266 | padding: 0.35em 0.625em 0.75em; 267 | } 268 | 269 | /** 270 | * 1. Correct `color` not being inherited in IE 8/9. 271 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 272 | */ 273 | 274 | legend { 275 | border: 0; /* 1 */ 276 | padding: 0; /* 2 */ 277 | } 278 | 279 | /** 280 | * 1. Correct font family not being inherited in all browsers. 281 | * 2. Correct font size not being inherited in all browsers. 282 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 283 | */ 284 | 285 | button, 286 | input, 287 | select, 288 | textarea { 289 | font-family: inherit; /* 1 */ 290 | font-size: 100%; /* 2 */ 291 | margin: 0; /* 3 */ 292 | } 293 | 294 | /** 295 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 296 | * the UA stylesheet. 297 | */ 298 | 299 | button, 300 | input { 301 | line-height: normal; 302 | } 303 | 304 | /** 305 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 306 | * All other form control elements do not inherit `text-transform` values. 307 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 308 | * Correct `select` style inheritance in Firefox 4+ and Opera. 309 | */ 310 | 311 | button, 312 | select { 313 | text-transform: none; 314 | } 315 | 316 | /** 317 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 318 | * and `video` controls. 319 | * 2. Correct inability to style clickable `input` types in iOS. 320 | * 3. Improve usability and consistency of cursor style between image-type 321 | * `input` and others. 322 | */ 323 | 324 | button, 325 | html input[type="button"], /* 1 */ 326 | input[type="reset"], 327 | input[type="submit"] { 328 | -webkit-appearance: button; /* 2 */ 329 | cursor: pointer; /* 3 */ 330 | } 331 | 332 | /** 333 | * Re-set default cursor for disabled elements. 334 | */ 335 | 336 | button[disabled], 337 | html input[disabled] { 338 | cursor: default; 339 | } 340 | 341 | /** 342 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 343 | * 2. Remove excess padding in IE 8/9/10. 344 | */ 345 | 346 | input[type="checkbox"], 347 | input[type="radio"] { 348 | box-sizing: border-box; /* 1 */ 349 | padding: 0; /* 2 */ 350 | } 351 | 352 | /** 353 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 354 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 355 | * (include `-moz` to future-proof). 356 | */ 357 | 358 | input[type="search"] { 359 | -webkit-appearance: textfield; /* 1 */ 360 | -moz-box-sizing: content-box; 361 | -webkit-box-sizing: content-box; /* 2 */ 362 | box-sizing: content-box; 363 | } 364 | 365 | /** 366 | * Remove inner padding and search cancel button in Safari 5 and Chrome 367 | * on OS X. 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Remove inner padding and border in Firefox 4+. 377 | */ 378 | 379 | button::-moz-focus-inner, 380 | input::-moz-focus-inner { 381 | border: 0; 382 | padding: 0; 383 | } 384 | 385 | /** 386 | * 1. Remove default vertical scrollbar in IE 8/9. 387 | * 2. Improve readability and alignment in all browsers. 388 | */ 389 | 390 | textarea { 391 | overflow: auto; /* 1 */ 392 | vertical-align: top; /* 2 */ 393 | } 394 | 395 | /* ========================================================================== 396 | Tables 397 | ========================================================================== */ 398 | 399 | /** 400 | * Remove most spacing between table cells. 401 | */ 402 | 403 | table { 404 | border-collapse: collapse; 405 | border-spacing: 0; 406 | } -------------------------------------------------------------------------------- /demo/src/less/variables.less: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Variables 3 | // ========================================================================== 4 | 5 | // Colors 6 | @blue: #3498db; 7 | @green: #4cb953; 8 | @gray-dark: #343f4a; 9 | @gray: #55646b; 10 | @gray-light: #cbd0d3; 11 | @gray-lighter: #dbe3e8; 12 | @off-white: #f2f5f7; 13 | 14 | // Background 15 | @body-background: #1e252b; 16 | @body-text-color: #fff; 17 | 18 | @color-headings: @green; 19 | 20 | // Fonts 21 | @font-family: Gordita, system-ui, BlinkMacSystemFont, -apple-system, Avenir, 'Helvetica Neue', 'Segoe UI', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 22 | @line-height: 1.5; 23 | @font-size-base: 15; 24 | @font-size-small: 13; 25 | @font-weight-light: 300; 26 | @font-weight-regular: 400; 27 | @font-weight-medium: 500; 28 | @font-weight-bold: 600; 29 | @font-weight-black: 900; 30 | 31 | // Elements 32 | @link-color: @green; 33 | @button-background: @green; 34 | @button-text-color: #fff; 35 | 36 | // Spacing 37 | @padding-base: 20px; 38 | 39 | // Breakpoints 40 | @screen-sm: 480px; 41 | @screen-md: 768px; 42 | 43 | // Radii 44 | @border-radius-base: 4px; 45 | 46 | // Examples 47 | @example-width-audio: 520px; 48 | @example-width-video: 1200px; 49 | -------------------------------------------------------------------------------- /deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "cdn": { 3 | "bucket": "cdn-shr", 4 | "domain": "cdn.shr.one", 5 | "region": "us-east-1" 6 | }, 7 | "demo": { 8 | "bucket": "shr.one", 9 | "domain": "shr.one", 10 | "region": "us-east-1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dist/app.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}a{background:0 0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}hr{box-sizing:content-box;height:0}mark{background:#ff0;color:#000}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}*,::after,::before{box-sizing:border-box}[hidden]{display:none}html{font-size:100%;height:100%;display:flex;align-items:center}body{width:100%;padding:20px;background:#1e252b;font-family:Gordita,system-ui,BlinkMacSystemFont,-apple-system,Avenir,'Helvetica Neue','Segoe UI',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:15px;font-size:.938rem;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;line-height:1.5;text-align:center;color:#fff}header{padding-bottom:20px}header p{font-size:18px;font-size:1.125rem}header p:last-child{margin:0}@media (min-width:480px){header{padding-bottom:60px}}section{padding-bottom:20px}@media (min-width:480px){section{padding-bottom:40px}}@font-face{font-display:swap;font-family:Gordita;font-style:normal;font-weight:300;src:url(https://cdn.plyr.io/static/fonts/gordita-light.woff2) format('woff2'),url(https://cdn.plyr.io/static/fonts/gordita-light.woff) format('woff')}@font-face{font-display:swap;font-family:Gordita;font-style:normal;font-weight:400;src:url(https://cdn.plyr.io/static/fonts/gordita-regular.woff2) format('woff2'),url(https://cdn.plyr.io/static/fonts/gordita-regular.woff) format('woff')}@font-face{font-display:swap;font-family:Gordita;font-style:normal;font-weight:500;src:url(https://cdn.plyr.io/static/fonts/gordita-medium.woff2) format('woff2'),url(https://cdn.plyr.io/static/fonts/gordita-medium.woff) format('woff')}@font-face{font-display:swap;font-family:Gordita;font-style:normal;font-weight:600;src:url(https://cdn.plyr.io/static/fonts/gordita-bold.woff2) format('woff2'),url(https://cdn.plyr.io/static/fonts/gordita-bold.woff) format('woff')}@font-face{font-display:swap;font-family:Gordita;font-style:normal;font-weight:900;src:url(https://cdn.plyr.io/static/fonts/gordita-black.woff2) format('woff2'),url(https://cdn.plyr.io/static/fonts/gordita-black.woff) format('woff')}h1{letter-spacing:-.025em;margin:0 0 10px;line-height:1.2;font-size:64px;font-size:4rem;color:#4cb953}h1 span{font-weight:400;color:#28323a}p,small{margin:0 0 20px}small{display:block;padding:0 10px;font-size:13px;font-size:.813rem}a{text-decoration:none;color:#4cb953;transition:all .3s ease}a:focus,a:hover{border-bottom:1px solid}a:focus{outline:thin dotted #343f4a;outline-offset:1px}.examples{margin-bottom:20px;text-shadow:none}.examples .shr-button{margin-top:5px;margin-bottom:5px}@media (min-width:480px){.examples{margin-bottom:60px}}.btn{display:inline-flex;padding:12px 25px;background-color:#4cb953;border:0;box-shadow:0 1px 1px rgba(0,0,0,.05);border-radius:4px;transition:all .3s ease;color:#fff;text-decoration:none;font-size:15px;font-size:.938rem;font-weight:600}.btn:focus,.btn:hover{background-color:#45b14c}.btn:focus{box-shadow:0 0 0 3px rgba(76,185,83,.5)}.btn:focus{outline:0}.btn:focus,.btn:hover{border:0}.error body,html.error{height:100%}.error body{width:100%;display:table;table-layout:fixed}.error main{display:table-cell;width:100%;vertical-align:middle} -------------------------------------------------------------------------------- /dist/app.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function t(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function e(t,e){for(var n=0;n this.updateDisplay(data))\n .catch(() => {});\n\n this.listeners(true);\n\n this.elements.trigger.shr = this;\n }\n\n /**\n * Destroy the current instance\n * @returns {Void}\n */\n destroy() {\n this.listeners(false);\n\n // TODO: Remove the count and unwrap\n }\n\n /**\n * Setup event listeners\n * @returns {Void}\n */\n listeners(toggle = false) {\n const method = toggle ? 'addEventListener' : 'removeEventListener';\n\n this.elements.trigger[method]('click', event => this.share(event), false);\n }\n\n /**\n * Gets the href from the trigger link\n * @returns {String} The href attribute from the link\n */\n get href() {\n if (!is.element(this.elements.trigger)) {\n return null;\n }\n\n return this.elements.trigger.href;\n }\n\n /**\n * Gets the network for this instance\n * @returns {String} The network name in lowercase\n */\n get network() {\n if (!is.element(this.elements.trigger)) {\n return null;\n }\n\n const { networks } = this.config;\n\n return Object.keys(networks).find(n => getDomain(this.href) === networks[n].domain);\n }\n\n /**\n * Gets the config for the specified network\n * @returns {Object} The config options\n */\n get networkConfig() {\n if (is.empty(this.network)) {\n return null;\n }\n\n return this.config.networks[this.network];\n }\n\n /**\n * Gets the URL or ID we are geting the share count for\n * @returns {String} The target ID or URL we're sharing\n */\n get target() {\n if (is.empty(this.network)) {\n return null;\n }\n\n const url = new URL(this.href);\n\n switch (this.network) {\n case 'facebook':\n return url.searchParams.get('u');\n\n case 'github':\n return url.pathname.substring(1);\n\n case 'youtube':\n return url.pathname.split('/').pop();\n\n default:\n return url.searchParams.get('url');\n }\n }\n\n /**\n * Gets for the URL for the JSONP endpoint\n * @returns {String} The URL for the JSONP endpoint\n */\n get apiUrl() {\n if (is.empty(this.network)) {\n return null;\n }\n\n const { tokens } = this.config;\n\n switch (this.network) {\n case 'github':\n return this.networkConfig.url(this.target, tokens.github);\n\n case 'youtube':\n return this.networkConfig.url(this.target, tokens.youtube);\n\n default:\n return this.networkConfig.url(encodeURIComponent(this.target));\n }\n }\n\n /**\n * Initiate the share process\n * This must be user triggered or the popup will be blocked\n * @param {Event} event The user input event\n * @returns {Void}\n */\n share(event) {\n this.openPopup(event);\n\n const { increment } = this.config.count;\n\n this.getCount()\n .then(data => this.updateDisplay(data, increment))\n .catch(() => {});\n }\n\n /**\n * Displays a pop up window to share on the requested social network.\n * @param {Event} event - Event that triggered the popup window.\n * @returns {Void}\n */\n openPopup(event) {\n // Only popup if we need to...\n if (is.empty(this.network) || !this.networkConfig.popup) {\n return;\n }\n\n // Prevent the link opening\n if (is.event(event)) {\n event.preventDefault();\n }\n\n // Set variables for the popup\n const size = this.networkConfig.popup;\n const { width, height } = size;\n const name = `shr-popup--${this.network}`;\n\n // If window already exists, just focus it\n if (this.popup && !this.popup.closed) {\n this.popup.focus();\n\n this.console.log('Popup re-focused.');\n } else {\n // Get position\n const left = window.screenLeft !== undefined ? window.screenLeft : window.screen.left;\n const top = window.screenTop !== undefined ? window.screenTop : window.screen.top;\n // Open in the centre of the screen\n const x = window.screen.width / 2 - width / 2 + left;\n const y = window.screen.height / 2 - height / 2 + top;\n\n // Open that window\n this.popup = window.open(this.href, name, `top=${y},left=${x},width=${width},height=${height}`);\n\n // Determine if the popup was blocked\n const blocked = !this.popup || this.popup.closed || !is.boolean(this.popup.closed);\n\n // Focus new window\n if (!blocked) {\n this.popup.focus();\n\n // Nullify opener to prevent \"tab nabbing\"\n // https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/\n // this.popup.opener = null;\n\n this.console.log('Popup opened.');\n } else {\n this.console.error('Popup blocked.');\n }\n }\n }\n\n /**\n * Get the count for the url from API\n * @param {Boolean} useCache Whether to use the local storage cache or not\n * @returns {Promise}\n */\n getCount(useCache = true) {\n return new Promise((resolve, reject) => {\n // Format the JSONP endpoint\n const url = this.apiUrl;\n\n // If there's an endpoint. For some social networks, you can't\n // get the share count (like Twitter) so we won't have any data. The link\n // will be to share it, but you won't get a count of how many people have.\n\n if (is.empty(url)) {\n reject(new Error(`No URL available for ${this.network}.`));\n return;\n }\n\n // Check cache first\n if (useCache) {\n const cached = this.storage.get(this.target);\n\n if (!is.empty(cached) && Object.keys(cached).includes(this.network)) {\n const count = cached[this.network];\n resolve(is.number(count) ? count : 0);\n this.console.log(`getCount for '${this.target}' for '${this.network}' resolved from cache.`);\n return;\n }\n }\n\n // When we get here, this means the cached counts are not valid,\n // or don't exist. We will call the API if the URL is available\n // at this point.\n\n // Runs a GET request on the URL\n getJSONP(url)\n .then(data => {\n let count = 0;\n const custom = this.elements.trigger.getAttribute('data-shr-display');\n\n // Get value based on config\n if (!is.empty(custom)) {\n count = data[custom];\n } else {\n count = this.networkConfig.shareCount(data);\n }\n\n // Default to zero for undefined\n if (is.empty(count)) {\n count = 0;\n } else {\n // Parse\n count = parseInt(count, 10);\n\n // Handle NaN\n if (!is.number(count)) {\n count = 0;\n }\n }\n\n // Cache in local storage\n this.storage.set({\n [this.target]: {\n [this.network]: count,\n },\n });\n\n resolve(count);\n })\n .catch(reject);\n });\n }\n\n /**\n * Display the count\n * @param {Number} input - The count returned from the share count API\n * @param {Boolean} increment - Determines if we should increment the count or not\n * @returns {Void}\n */\n updateDisplay(input, increment = false) {\n const { count, wrapper } = this.config;\n // If we're incrementing (e.g. on click)\n const number = increment ? input + 1 : input;\n // Standardize position\n const position = count.position.toLowerCase();\n\n // Only display if there's a count\n if (number > 0 || count.displayZero) {\n const isAfter = position === 'after';\n const round = unit => Math.round((number / unit) * 10) / 10;\n let label = formatNumber(number);\n\n // Format to 1K, 1M, etc\n if (count.format) {\n if (number > 1000000) {\n label = `${round(1000000)}M`;\n } else if (number > 1000) {\n label = `${round(1000)}K`;\n }\n }\n\n // Update or insert\n if (is.element(this.elements.count)) {\n this.elements.count.textContent = label;\n } else {\n // Add wrapper\n wrap(\n this.elements.trigger,\n createElement('span', {\n class: wrapper.className,\n }),\n );\n\n // Create count display\n this.elements.count = createElement(\n 'span',\n {\n class: `${count.className} ${count.className}--${position}`,\n },\n label,\n );\n\n // Insert count display\n this.elements.trigger.insertAdjacentElement(isAfter ? 'afterend' : 'beforebegin', this.elements.count);\n }\n }\n }\n\n /**\n * Setup multiple instances\n * @param {String|Element|NodeList|Array} target\n * @param {Object} options\n * @returns {Array} - An array of instances\n */\n static setup(target, options = {}) {\n let targets = null;\n\n if (is.string(target)) {\n targets = Array.from(document.querySelectorAll(target));\n } else if (is.element(target)) {\n targets = [target];\n } else if (is.nodeList(target)) {\n targets = Array.from(target);\n } else if (is.array(target)) {\n targets = target.filter(is.element);\n }\n\n if (is.empty(targets)) {\n return null;\n }\n\n const config = Object.assign({}, defaults, options);\n\n if (is.string(target) && config.watch) {\n // Create an observer instance\n const observer = new MutationObserver(mutations => {\n Array.from(mutations).forEach(mutation => {\n Array.from(mutation.addedNodes).forEach(node => {\n if (!is.element(node) || !matches(node, target)) {\n return;\n }\n\n // eslint-disable-next-line no-unused-vars\n const share = new Shr(node, config);\n });\n });\n });\n\n // Pass in the target node, as well as the observer options\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n }\n\n return targets.map(t => new Shr(t, options));\n }\n}\n\nexport default Shr;\n","// ==========================================================================\n// Type checking utils\n// ==========================================================================\n\nconst getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\nconst isNullOrUndefined = input => input === null || typeof input === 'undefined';\nconst isObject = input => getConstructor(input) === Object;\nconst isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\nconst isString = input => getConstructor(input) === String;\nconst isBoolean = input => getConstructor(input) === Boolean;\nconst isFunction = input => getConstructor(input) === Function;\nconst isArray = input => Array.isArray(input);\nconst isNodeList = input => instanceOf(input, NodeList);\nconst isElement = input => instanceOf(input, Element);\nconst isEvent = input => instanceOf(input, Event);\nconst isEmpty = input =>\n isNullOrUndefined(input) ||\n ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\n (isObject(input) && !Object.keys(input).length);\n\nexport default {\n nullOrUndefined: isNullOrUndefined,\n object: isObject,\n number: isNumber,\n string: isString,\n boolean: isBoolean,\n function: isFunction,\n array: isArray,\n nodeList: isNodeList,\n element: isElement,\n event: isEvent,\n empty: isEmpty,\n};\n","import is from '../utils/is';\n\n/**\n * Constants. These are uneditable by the user. These will get merged into\n * the global config after the user defaults so the user can't overwrite these\n * values.\n *\n * @typedef {Object} constants\n * @type {Object}\n *\n * @property {Object} facebook - The settings for Facebook within Shr.\n * @property {function} facebook.url - The method that returns the API Url to get the share count for Facebook.\n * @property {function} facebook.shareCount - The method that extracts the number we need from the data returned from the API for Facebook.\n *\n * @property {Object} twitter - The settings for Twitter within Shr.\n * @property {function} twitter.url - The method that returns the API Url to get the share count for Twitter.\n * @property {function} twitter.shareCount - The method that extracts the number we need from the data returned from the API for Twitter.\n *\n * @property {Object} pinterest - The settings for Pinterest within Shr.\n * @property {function} pinterest.url - The method that returns the API Url to get the share count for Pinterest.\n * @property {function} pinterest.shareCount - The method that extracts the number we need from the data returned from the API for Pinterest.\n *\n * @property {Object} github - The settings for GitHub within Shr.\n * @property {function} github.url - The method that returns the API Url to get the share count for GitHub.\n * @property {function} github.shareCount - The method that extracts the number we need from the data returned from the API for GitHub.\n */\n\nconst constants = {\n facebook: {\n domain: 'facebook.com',\n url: url => `https://graph.facebook.com/?id=${url}&fields=og_object{engagement}`,\n shareCount: data => data.og_object.engagement.count,\n popup: {\n width: 640,\n height: 360,\n },\n },\n\n twitter: {\n domain: 'twitter.com',\n url: () => null,\n shareCount: () => null,\n popup: {\n width: 640,\n height: 240,\n },\n },\n\n pinterest: {\n domain: 'pinterest.com',\n url: url => `https://widgets.pinterest.com/v1/urls/count.json?url=${url}`,\n shareCount: data => data.count,\n popup: {\n width: 830,\n height: 700,\n },\n },\n\n github: {\n domain: 'github.com',\n url: (path, token) => `https://api.github.com/repos/${path}${is.string(token) ? `?access_token=${token}` : ''}`,\n shareCount: data => data.data.stargazers_count,\n },\n\n youtube: {\n domain: 'youtube.com',\n url: (id, key) => `https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${id}&key=${key}`,\n shareCount: data => {\n if (!is.empty(data.error)) {\n return null;\n }\n\n const [first] = data.items;\n\n if (is.empty(first)) {\n return null;\n }\n\n return first.statistics.subscriberCount;\n },\n },\n};\n\nexport default constants;\n","/**\n * Default Shr Config. All variables, settings and states are stored here\n * and global. These are the defaults. The user can edit these at will when\n * initializing Shr.\n *\n * @typedef {Object} defaults\n * @type {Object}\n *\n * @property {Boolean} debug - The flag for if we debug Shr or not. By defaul this is false.\n\n * @property {Object} wrapper The object containing the settings for the wrapper that's added.\n * @property {String} wrapper.className Classname for the wrapper.\n\n * @property {Object} count - The object containing the settings for the count.\n * @property {String} count.className - Classname for the share count.\n * @property {Boolean} count.displayZero - Determines if we display zero values.\n * @property {Boolean} count.format - Display 1000 as 1K, 1000000 as 1M, etc\n * @property {String} count.position - Inject the count before or after the link in the DOM\n * @property {Boolean} count.increment - Determines if we increment the count on click. This assumes the share is valid.\n *\n * @property {Object} tokens - The object containing authentication tokens.\n * @property {Object} tokens.github The optional authentication tokens for GitHub (to prevent rate limiting).\n * @property {String} tokens.youtube The public key you need to get the subscriber count for YouTube.\n *\n * @property {Object} storage - The object containing the settings for local storage.\n * @property {Boolean} storage.enabled - Determines if local storage is enabled for the browser or not.\n * @property {String} storage.key - The key that the storage will use to access Shr data.\n * @property {Number} storage.ttl - The time to live for the local storage values if available.\n*/\n\n/**\n *\n */\n\nconst defaults = {\n debug: false,\n wrapper: {\n className: 'shr',\n },\n count: {\n className: 'shr__count',\n displayZero: false,\n format: true,\n position: 'after',\n increment: true,\n },\n tokens: {\n github: '',\n youtube: '',\n },\n storage: {\n enabled: true,\n key: 'shr',\n ttl: 300000,\n },\n};\n\nexport default defaults;\n","/**\n * Makes the JSONP request to get the social network to get the share count.\n *\n * @param {string} url - The URL of the of the sharing API.\n * @param {function} callback - The callback funciton once the API completes the request.\n */\nexport function getJSONP(url) {\n return new Promise((resolve, reject) => {\n // Generate a random callback\n const name = `jsonp_callback_${Math.round(100000 * Math.random())}`;\n // Create a faux script\n const script = document.createElement('script');\n\n // Handle errors\n script.addEventListener('error', error => reject(error));\n\n // Cleanup to prevent memory leaks and hit original callback\n window[name] = data => {\n delete window[name];\n document.body.removeChild(script);\n resolve(data);\n };\n\n // Add callback to URL\n const src = new URL(url);\n src.searchParams.set('callback', name);\n\n // Set src and load\n script.setAttribute('src', src.toString());\n\n // Inject to the body\n document.body.appendChild(script);\n });\n}\n\nexport default { getJSONP };\n","// ==========================================================================\n// Console wrapper\n// ==========================================================================\n\nconst noop = () => {};\n\nexport default class Console {\n constructor(enabled = false) {\n this.enabled = window.console && enabled;\n\n if (this.enabled) {\n this.log('Debugging enabled');\n }\n }\n\n get log() {\n return this.enabled\n ? Function.prototype.bind.call(console.log, console) // eslint-disable-line no-console\n : noop;\n }\n\n get warn() {\n return this.enabled\n ? Function.prototype.bind.call(console.warn, console) // eslint-disable-line no-console\n : noop;\n }\n\n get error() {\n return this.enabled\n ? Function.prototype.bind.call(console.error, console) // eslint-disable-line no-console\n : noop;\n }\n}\n","// Element matches a selector\nexport function matches(element, selector) {\n const prototype = { Element };\n\n function match() {\n return Array.from(document.querySelectorAll(selector)).includes(this);\n }\n\n const method =\n prototype.matches ||\n prototype.webkitMatchesSelector ||\n prototype.mozMatchesSelector ||\n prototype.msMatchesSelector ||\n match;\n\n return method.call(element, selector);\n}\n\nexport default { matches };\n","import is from './is';\n\n/**\n * Wrap one or more HTMLElement in wrapper container\n * @param {HTMLElement[]} elements\n * @param {HTMLElement} wrapper\n * @returns {Void}\n */\nexport function wrap(elements, wrapper) {\n // Convert `elements` to an array, if necessary.\n const targets = elements.length ? elements : [elements];\n\n // Loops backwards to prevent having to clone the wrapper on the\n // first element (see `child` below).\n Array.from(targets)\n .reverse()\n .forEach((element, index) => {\n const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\n // Cache the current parent and sibling.\n const parent = element.parentNode;\n const sibling = element.nextSibling;\n\n // Wrap the element (is automatically removed from its current\n // parent).\n child.appendChild(element);\n\n // If the element had a sibling, insert the wrapper before\n // the sibling to maintain the HTML structure; otherwise, just\n // append it to the parent.\n if (sibling) {\n parent.insertBefore(child, sibling);\n } else {\n parent.appendChild(child);\n }\n });\n}\n\n/**\n * Set HTMLElement attributes\n * @param {HTMLElement} element\n * @param {Object} attributes\n * @returns {Void}\n */\nexport function setAttributes(element, attributes) {\n if (!is.element(element) || is.empty(attributes)) {\n return;\n }\n\n // Assume null and undefined attributes should be left out,\n // Setting them would otherwise convert them to \"null\" and \"undefined\"\n Object.entries(attributes)\n .filter(([, value]) => !is.nullOrUndefined(value))\n .forEach(([key, value]) => element.setAttribute(key, value));\n}\n\n/**\n * Create a HTMLElement\n * @param {String} type - Type of element to create\n * @param {Object} attributes - Object of HTML attributes\n * @param {String} text - Sets the text content\n * @returns {HTMLElement}\n */\nexport function createElement(type, attributes, text) {\n // Create a new \n const element = document.createElement(type);\n\n // Set all passed attributes\n if (is.object(attributes)) {\n setAttributes(element, attributes);\n }\n\n // Add text node\n if (is.string(text)) {\n element.innerText = text;\n }\n\n // Return built element\n return element;\n}\n\nexport default { wrap };\n","export function formatNumber(number) {\n // Work out whether decimal separator is . or , for localised numbers\n const decimalSeparator = /\\./.test((1.1).toLocaleString()) ? '.' : ',';\n // Round n to an integer and present\n const regex = new RegExp(`\\\\${decimalSeparator}\\\\d+$`);\n\n return Math.round(number)\n .toLocaleString()\n .replace(regex, '');\n}\n\nexport default { formatNumber };\n","// ==========================================================================\n// Object utils\n// ==========================================================================\n\nimport is from './is';\n\n// Deep extend destination object with N more objects\nexport function extend(target = {}, ...sources) {\n if (!sources.length) {\n return target;\n }\n\n const source = sources.shift();\n\n if (!is.object(source)) {\n return target;\n }\n\n Object.keys(source).forEach(key => {\n if (is.object(source[key])) {\n if (!Object.keys(target).includes(key)) {\n Object.assign(target, { [key]: {} });\n }\n\n extend(target[key], source[key]);\n } else {\n Object.assign(target, { [key]: source[key] });\n }\n });\n\n return extend(target, ...sources);\n}\n\nexport default { extend };\n","// ==========================================================================\n// Plyr storage\n// ==========================================================================\n\nimport is from './is';\nimport { extend } from './objects';\n\nclass Storage {\n constructor(key, ttl, enabled = true) {\n this.enabled = enabled && Storage.supported;\n this.key = key;\n this.ttl = ttl;\n }\n\n // Check for actual support (see if we can use it)\n static get supported() {\n try {\n if (!('localStorage' in window)) {\n return false;\n }\n\n const test = '___test';\n\n // Try to use it (it might be disabled, e.g. user is in private mode)\n // see: https://github.com/sampotts/plyr/issues/131\n window.localStorage.setItem(test, test);\n window.localStorage.removeItem(test);\n\n return true;\n } catch (e) {\n return false;\n }\n }\n\n get(key) {\n if (!Storage.supported || !this.enabled) {\n return null;\n }\n\n const store = window.localStorage.getItem(this.key);\n\n if (is.empty(store)) {\n return null;\n }\n\n // Check TTL\n const ttl = window.localStorage.getItem(`${this.key}_ttl`);\n\n if (is.empty(ttl) || ttl < Date.now()) {\n return null;\n }\n\n const json = JSON.parse(store);\n\n return is.string(key) && key.length ? json[key] : json;\n }\n\n set(object) {\n // Bail if we don't have localStorage support or it's disabled\n if (!Storage.supported || !this.enabled) {\n return;\n }\n\n // Can only store objectst\n if (!is.object(object)) {\n return;\n }\n\n // Get current storage\n let storage = this.get();\n\n // Default to empty object\n if (is.empty(storage)) {\n storage = {};\n }\n\n // Update the working copy of the values\n extend(storage, object);\n\n // Update storage and TTL record\n window.localStorage.setItem(this.key, JSON.stringify(storage));\n window.localStorage.setItem(`${this.key}_ttl`, Date.now() + this.ttl);\n }\n}\n\nexport default Storage;\n","export function getDomain(href) {\n const url = new URL(href);\n let domain = url.hostname;\n const parts = domain.split('.');\n const { length } = parts;\n\n // Extract the root domain\n if (length > 2) {\n domain = `${parts[length - 2]}.${parts[length - 1]}`;\n\n // Check to see if it's using a Country Code Top Level Domain (ccTLD) (i.e. \".me.uk\")\n if (parts[length - 2].length === 2 && parts[length - 1].length === 2) {\n domain = `${parts[length - 3]}.${domain}`;\n }\n }\n\n return domain;\n}\n\nexport default { getDomain };\n"]} -------------------------------------------------------------------------------- /dist/shr.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Gulp build script 3 | // ========================================================================== 4 | 5 | const path = require('path'); 6 | const gulp = require('gulp'); 7 | // ------------------------------------ 8 | // CSS 9 | // ------------------------------------ 10 | const less = require('gulp-less'); 11 | const sass = require('gulp-sass'); 12 | const clean = require('gulp-clean-css'); 13 | const prefix = require('gulp-autoprefixer'); 14 | // ------------------------------------ 15 | // JavaScript 16 | // ------------------------------------ 17 | const terser = require('gulp-terser'); 18 | const rollup = require('gulp-better-rollup'); 19 | const babel = require('rollup-plugin-babel'); 20 | const commonjs = require('rollup-plugin-commonjs'); 21 | const resolve = require('rollup-plugin-node-resolve'); 22 | const sourcemaps = require('gulp-sourcemaps'); 23 | // ------------------------------------ 24 | // Images 25 | // ------------------------------------ 26 | const svgstore = require('gulp-svgstore'); 27 | const imagemin = require('gulp-imagemin'); 28 | // ------------------------------------ 29 | // Utils 30 | // ------------------------------------ 31 | const plumber = require('gulp-plumber'); 32 | const rename = require('gulp-rename'); 33 | const replace = require('gulp-replace'); 34 | const size = require('gulp-size'); 35 | const ansi = require('ansi-colors'); 36 | const log = require('fancy-log'); 37 | const del = require('del'); 38 | const through = require('through2'); 39 | // ------------------------------------ 40 | // Deployment 41 | // ------------------------------------ 42 | const aws = require('aws-sdk'); 43 | const publish = require('gulp-awspublish'); 44 | const FastlyPurge = require('fastly-purge'); 45 | // ------------------------------------ 46 | // Configs 47 | // ------------------------------------ 48 | const pkg = require('./package.json'); 49 | const build = require('./build.json'); 50 | const deploy = require('./deploy.json'); 51 | // ------------------------------------ 52 | // Get info from package 53 | // ------------------------------------ 54 | const { browserslist, version } = pkg; 55 | 56 | // Get AWS config 57 | Object.values(deploy).forEach(target => { 58 | Object.assign(target, { 59 | publisher: publish.create({ 60 | region: target.region, 61 | params: { 62 | Bucket: target.bucket, 63 | }, 64 | credentials: new aws.SharedIniFileCredentials({ profile: 'shr' }), 65 | }), 66 | }); 67 | }); 68 | 69 | const root = __dirname; 70 | const paths = { 71 | shr: { 72 | // Source paths 73 | src: { 74 | sass: path.join(root, 'src/sass/**/*'), 75 | js: path.join(root, 'src/js/**/*'), 76 | sprite: path.join(root, 'src/sprite/*.svg'), 77 | }, 78 | }, 79 | demo: { 80 | // Source paths 81 | src: { 82 | less: path.join(root, 'demo/src/less/**/*'), 83 | js: path.join(root, 'demo/src/js/**/*'), 84 | sprite: path.join(root, 'demo/src/sprite/**/*'), 85 | }, 86 | // Docs 87 | root: path.join(root, 'demo/'), 88 | }, 89 | upload: [path.join(root, 'dist/**')], 90 | }; 91 | 92 | // Task arrays 93 | const tasks = { 94 | css: [], 95 | js: [], 96 | sprite: [], 97 | }; 98 | 99 | // Babel config 100 | const babelrc = { 101 | babelrc: false, 102 | presets: [ 103 | '@babel/env', 104 | [ 105 | 'minify', 106 | { 107 | builtIns: false, // Temporary fix for https://github.com/babel/minify/issues/904 108 | }, 109 | ], 110 | ], 111 | }; 112 | 113 | // Size plugin 114 | const sizeOptions = { showFiles: true, gzip: true }; 115 | 116 | // Clean out /dist 117 | gulp.task('clean', done => { 118 | del(paths.upload.map(dir => path.join(dir, '*'))); 119 | done(); 120 | }); 121 | 122 | // JavaScript 123 | const namespace = 'Shr'; 124 | 125 | Object.entries(build.js).forEach(([filename, entry]) => { 126 | entry.formats.forEach(format => { 127 | const name = `js:${filename}:${format}`; 128 | tasks.js.push(name); 129 | 130 | gulp.task(name, () => 131 | gulp 132 | .src(entry.src) 133 | .pipe(plumber()) 134 | .pipe(sourcemaps.init()) 135 | .pipe( 136 | rollup( 137 | { 138 | plugins: [resolve(), commonjs(), babel(babelrc)], 139 | }, 140 | { 141 | name: namespace, 142 | format, 143 | }, 144 | ), 145 | ) 146 | .pipe(terser()) 147 | .pipe( 148 | rename({ 149 | extname: `.${format === 'es' ? 'mjs' : 'js'}`, 150 | }), 151 | ) 152 | .pipe(size(sizeOptions)) 153 | .pipe(sourcemaps.write('')) 154 | .pipe(gulp.dest(entry.dist)), 155 | ); 156 | }); 157 | }); 158 | 159 | // CSS 160 | Object.entries(build.css).forEach(([filename, entry]) => { 161 | const name = `css:${filename}`; 162 | tasks.css.push(name); 163 | 164 | gulp.task(name, () => 165 | gulp 166 | .src(entry.src) 167 | .pipe(plumber()) 168 | .pipe(path.extname(entry.src) === '.less' ? less() : sass()) 169 | .pipe( 170 | prefix(browserslist, { 171 | cascade: false, 172 | }), 173 | ) 174 | .pipe(clean()) 175 | .pipe(size(sizeOptions)) 176 | .pipe(gulp.dest(entry.dist)), 177 | ); 178 | }); 179 | 180 | // SVG Sprite 181 | Object.entries(build.sprite).forEach(([filename, entry]) => { 182 | const name = `sprite:${filename}`; 183 | tasks.sprite.push(name); 184 | 185 | gulp.task(name, () => 186 | gulp 187 | .src(entry.src) 188 | .pipe(plumber()) 189 | .pipe( 190 | imagemin([ 191 | imagemin.svgo({ 192 | plugins: [{ removeViewBox: false }], 193 | }), 194 | ]), 195 | ) 196 | .pipe(svgstore()) 197 | .pipe(rename({ basename: path.parse(filename).name })) 198 | .pipe(size(sizeOptions)) 199 | .pipe(gulp.dest(entry.dist)), 200 | ); 201 | }); 202 | 203 | // Watch for file changes 204 | gulp.task('watch', () => { 205 | // Core 206 | gulp.watch(paths.shr.src.js, gulp.parallel(...tasks.js)); 207 | gulp.watch(paths.shr.src.sass, gulp.parallel(...tasks.css)); 208 | gulp.watch(paths.shr.src.sprite, gulp.parallel(...tasks.sprite)); 209 | 210 | // Demo 211 | gulp.watch(paths.demo.src.js, gulp.parallel(...tasks.js)); 212 | gulp.watch(paths.demo.src.less, gulp.parallel(...tasks.css)); 213 | }); 214 | 215 | // Default gulp task 216 | gulp.task('default', gulp.series('clean', gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite), 'watch')); 217 | 218 | // Publish a version to CDN and demo 219 | // -------------------------------------------- 220 | // Get deployment config 221 | let credentials = {}; 222 | try { 223 | credentials = require('./credentials.json'); //eslint-disable-line 224 | } catch (e) { 225 | // Do nothing 226 | } 227 | // Some options 228 | const maxAge = 31536000; // seconds 1 year 229 | const headers = { 230 | cdn: { 231 | 'Cache-Control': `max-age=${maxAge}`, 232 | }, 233 | demo: { 234 | 'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0', 235 | }, 236 | }; 237 | 238 | const regex = 239 | '(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?'; 240 | const semver = new RegExp(`v${regex}`, 'gi'); 241 | const cdnpath = new RegExp(`${deploy.cdn.domain}/${regex}`, 'gi'); 242 | const versionPath = `https://${deploy.cdn.domain}/${version}`; 243 | const localpath = new RegExp('(../)?dist', 'gi'); 244 | 245 | // Publish version to CDN bucket 246 | gulp.task('cdn', () => { 247 | const { domain, publisher } = deploy.cdn; 248 | 249 | if (!publisher) { 250 | throw new Error('No publisher instance. Check AWS configuration.'); 251 | } 252 | 253 | log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`); 254 | 255 | // Upload to CDN 256 | return gulp 257 | .src(paths.upload) 258 | .pipe( 259 | size({ 260 | showFiles: true, 261 | gzip: true, 262 | }), 263 | ) 264 | .pipe( 265 | rename(p => { 266 | // eslint-disable-next-line no-param-reassign 267 | p.dirname = p.dirname.replace('.', version); 268 | }), 269 | ) 270 | .pipe(publisher.publish(headers.cdn)) 271 | .pipe(publish.reporter()); 272 | }); 273 | 274 | // Replace versioned files in readme.md 275 | gulp.task('demo:readme', () => { 276 | const { domain } = deploy.cdn; 277 | 278 | return gulp 279 | .src([`${root}/readme.md`]) 280 | .pipe(replace(cdnpath, `${domain}/${version}`)) 281 | .pipe(gulp.dest(root)); 282 | }); 283 | 284 | // Replace versions in shr.js 285 | gulp.task('demo:src', () => 286 | gulp 287 | .src(path.join(root, 'src/js/shr.js')) 288 | .pipe(replace(semver, `v${version}`)) 289 | .pipe(gulp.dest(path.join(root, 'src/js/'))), 290 | ); 291 | 292 | // Replace versions in shr.js 293 | gulp.task('demo:svg', () => { 294 | const { domain, publisher } = deploy.cdn; 295 | 296 | if (!publisher) { 297 | throw new Error('No publisher instance. Check AWS configuration.'); 298 | } 299 | 300 | return gulp 301 | .src(path.join(root, 'dist/app.js')) 302 | .pipe(replace(localpath, `https://${domain}/${version}`)) 303 | .pipe( 304 | rename(p => { 305 | // eslint-disable-next-line no-param-reassign 306 | p.dirname = p.dirname.replace('.', version); 307 | }), 308 | ) 309 | .pipe(publisher.publish(headers.cdn)) 310 | .pipe(publish.reporter()); 311 | }); 312 | 313 | // Replace local file paths with remote paths in demo 314 | // e.g. "../dist/shr.js" to "https://cdn.shr.one/x.x.x/shr.js" 315 | gulp.task('demo:paths', () => { 316 | const { publisher } = deploy.demo; 317 | const { domain } = deploy.cdn; 318 | 319 | if (!publisher) { 320 | throw new Error('No publisher instance. Check AWS configuration.'); 321 | } 322 | 323 | return gulp 324 | .src([`*.html`, `src/js/app.js`].map(p => path.join(paths.demo.root, p))) 325 | .pipe(replace(localpath, `https://${domain}/${version}`)) 326 | .pipe(publisher.publish(headers.demo)) 327 | .pipe(publish.reporter()); 328 | }); 329 | 330 | // Upload error.html to cdn (as well as demo site) 331 | gulp.task('demo:error', () => { 332 | const { publisher } = deploy.demo; 333 | const { domain } = deploy.cdn; 334 | 335 | if (!publisher) { 336 | throw new Error('No publisher instance. Check AWS configuration.'); 337 | } 338 | 339 | return gulp 340 | .src([`${paths.demo.root}error.html`]) 341 | .pipe(replace(localpath, `https://${domain}/${version}`)) 342 | .pipe(publisher.publish(headers.demo)) 343 | .pipe(publish.reporter()); 344 | }); 345 | 346 | // Purge the fastly cache incase any 403/404 are cached 347 | gulp.task('purge', () => { 348 | if (!Object.keys(credentials).includes('fastly')) { 349 | throw new Error('Fastly credentials required to purge cache.'); 350 | } 351 | 352 | const { fastly } = credentials; 353 | const list = []; 354 | 355 | return gulp 356 | .src(paths.upload) 357 | .pipe( 358 | through.obj((file, enc, cb) => { 359 | if (file.stat.isFile()) { 360 | const filename = file.path.split('/').pop(); 361 | list.push(`${versionPath}/${filename}`); 362 | } 363 | 364 | cb(null); 365 | }), 366 | ) 367 | .on('end', () => { 368 | const purge = new FastlyPurge(fastly.token); 369 | 370 | list.forEach(url => { 371 | log(`Purging ${ansi.cyan(url)}...`); 372 | 373 | purge.url(url, (error, result) => { 374 | if (error) { 375 | log.error(error); 376 | } else if (result) { 377 | log(result); 378 | } 379 | }); 380 | }); 381 | }); 382 | }); 383 | 384 | // Publish to Demo bucket 385 | gulp.task('demo', gulp.parallel('demo:readme', 'demo:src', 'demo:svg', 'demo:paths', 'demo:error')); 386 | 387 | // Do everything 388 | gulp.task( 389 | 'deploy', 390 | gulp.series('clean', gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite), 'cdn', 'demo', 'purge'), 391 | ); 392 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Sam Potts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shr-buttons", 3 | "version": "2.0.3", 4 | "description": "Simple, customizable sharing buttons", 5 | "homepage": "http://shr.one", 6 | "author": "Sam Potts ", 7 | "license": "MIT", 8 | "main": "dist/shr.js", 9 | "module": "dist/shr.mjs", 10 | "jsnext:main": "dist/shr.mjs", 11 | "browser": "dist/shr.js", 12 | "sass": "src/sass/shr.scss", 13 | "style": "dist/shr.css", 14 | "browserslist": "> 1%", 15 | "keywords": [ 16 | "Facebook", 17 | "Twitter", 18 | "Pinterest", 19 | "YouTube", 20 | "Sharing", 21 | "Share Buttons" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/sampotts/shr.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/sampotts/shr/issues" 29 | }, 30 | "scripts": { 31 | "lint": "eslint src/js", 32 | "lint:fix": "eslint --fix src/js", 33 | "deploy": "yarn lint && gulp deploy" 34 | }, 35 | "dependencies": {}, 36 | "devDependencies": { 37 | "ansi-colors": "^4.0.1", 38 | "aws-sdk": "^2.479.0", 39 | "@babel/core": "^7.4.5", 40 | "@babel/preset-env": "^7.4.5", 41 | "babel-eslint": "^10.0.2", 42 | "babel-preset-minify": "^0.5.0", 43 | "del": "^4.1.1", 44 | "eslint": "^5.16.0", 45 | "eslint-config-airbnb-base": "^13.1.0", 46 | "eslint-config-prettier": "^5.0.0", 47 | "eslint-plugin-import": "^2.17.3", 48 | "eslint-plugin-simple-import-sort": "^4.0.0", 49 | "fancy-log": "^1.3.3", 50 | "fastly-purge": "^1.0.1", 51 | "gulp": "^4.0.2", 52 | "gulp-autoprefixer": "^6.1.0", 53 | "gulp-awspublish": "^4.0.0", 54 | "gulp-better-rollup": "^4.0.1", 55 | "gulp-clean-css": "^4.2.0", 56 | "gulp-imagemin": "^6.0.0", 57 | "gulp-less": "^4.0.1", 58 | "gulp-plumber": "^1.2.1", 59 | "gulp-rename": "^1.4.0", 60 | "gulp-replace": "^1.0.0", 61 | "gulp-sass": "^4.0.2", 62 | "gulp-size": "^3.0.0", 63 | "gulp-sourcemaps": "^2.6.5", 64 | "gulp-svgstore": "^7.0.1", 65 | "gulp-terser": "^1.2.0", 66 | "prettier-eslint": "^9.0.0", 67 | "prettier-stylelint": "^0.4.2", 68 | "rollup": "^1.15.6", 69 | "rollup-plugin-babel": "^4.3.2", 70 | "rollup-plugin-commonjs": "^10.0.0", 71 | "rollup-plugin-node-resolve": "^5.0.3", 72 | "stylelint-config-prettier": "^5.2.0", 73 | "stylelint-config-recommended": "^2.2.0", 74 | "stylelint-config-sass-guidelines": "^6.0.0", 75 | "stylelint-order": "^3.0.0", 76 | "stylelint-scss": "^3.8.0", 77 | "stylelint-selector-bem-pattern": "^2.1.0", 78 | "through2": "^3.0.1" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Shr 2 | 3 | Simple, clean, customizable sharing buttons. 4 | 5 | [Donate to support Shr](#donate) - [Checkout the demo](https://shr.one) 6 | 7 | [![Image of Shr](https://cdn.shr.one/shr.png)](https://shr.one) 8 | 9 | ## Why? 10 | 11 | The default share buttons used by the social networks are not only ugly to look at (sorry, they just are) but they usually depend on iframes, are slow and generally heavy. That led to me creating shr (short for share). 12 | 13 | ## Features 14 | 15 | - **Accessible** - built using progressive enhancement 16 | - **Lightweight** - just 3KB minified and gzipped 17 | - **Customisable** - make the buttons and count look how you want with the markup you want 18 | - **Semantic** - uses the _right_ elements. There's no ``s as buttons type hacks 19 | - **Fast** - uses local storage to cache results to keep things fast 20 | - **No dependencies** - written in "vanilla" ES6 JavaScript 21 | 22 | ## Changelog 23 | 24 | Check out [the changelog](changelog.md) 25 | 26 | ## Setup 27 | 28 | To set up Shr, you first must include the JavaScript lib and optionally the CSS and SVG sprite if you want icons on your buttons. 29 | 30 | ### 1. HTML 31 | 32 | Here's an example for a Facebook button, see [HTML section](#HTML) below for other examples. 33 | 34 | ```html 35 | 43 | ``` 44 | 45 | This markup assumes you're using the SVG sprite (which is optional) and the default CSS. If you're not using either of these then you can omit the `shr__*` classNames completely and the ``. The `href` attribute value is used to determine the type of network. It is also used as the fallback so must be valid. 46 | 47 | Once Shr has been initialized on a button and data has been fetched, it is manipulated as below: 48 | 49 | ```html 50 | 51 | 59 | 888 60 | 61 | ``` 62 | 63 | - The outer `` is a wrapper so that we can prevent the count wrapping under the button and just looking odd 64 | - The count `` is used as the bubble for the current count for share, star or subscriber, etc 65 | - The className for both of these elements can be changed in [options](#options) 66 | 67 | ### 2. JavaScript 68 | 69 | There are two ways you can get up and running with JavaScript: 70 | 71 | #### via the npm package 72 | 73 | If you're using npm/yarn to manage your dependencies, you can add `shr-buttons`: 74 | 75 | ```bash 76 | npm install --save shr-buttons 77 | ``` 78 | 79 | and then in your JavaScript app: 80 | 81 | ```javascript 82 | import Shr from 'shr-buttons'; 83 | ``` 84 | 85 | #### via a ` 91 | ``` 92 | 93 | Alternatively add the script to your main app bundle. 94 | 95 | ##### Initialize 96 | 97 | You can initialise a single button using the constructor: 98 | 99 | ```javascript 100 | const button = new Shr('.js-shr', { ...options }); 101 | ``` 102 | 103 | This will setup all elements that match the `.js-shr` selector. The first argument must be either a: 104 | 105 | - CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector), 106 | - a [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) 107 | - a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) 108 | - an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) 109 | 110 | This will return an instance that you can use for API calls later. 111 | 112 | More information about the `options` can be found in [options section](#options) below. 113 | 114 | Alternatively, an easier way if you have multiple buttons is to use the static method: 115 | 116 | ```javascript 117 | const buttons = Shr.setup('.js-shr', { ...options }); 118 | ``` 119 | 120 | This will return an array of instances it setup. 121 | 122 | _Note_: `Shr.setup` will also look for mutations of the DOM and and matching elements will also be setup if they are injected into the DOM after initial setup. 123 | 124 | ### 3. CSS _(optional)_ 125 | 126 | You don't have to use the Shr CSS. You're free to style the buttons how you like. You can either include the SASS in your build or use the CDN hosted CSS in your ``: 127 | 128 | ```html 129 | 130 | ``` 131 | 132 | ### 4. SVG Sprite (_optional_) 133 | 134 | Ir you want to display the icons for each social network as per the demo, you can use the included [SVG sprite](https://css-tricks.com/svg-sprites-use-better-icon-fonts/). If you already have a sprite system, then you can include the SVG icons as-is. Otherwise, you can use something like [sprite.js](https://gist.github.com/sampotts/15adab33ff3af87f902db0253f0df8dd). 135 | 136 | ## API 137 | 138 | A few useful methods are exposed. To call an API method, you need a reference to the instance. This is returned from `Shr.setup` or your call the the constructor (`new Shr`), e.g.: 139 | 140 | ```javascript 141 | const button = new Shr('.js-shr-facebook', { ...options }); 142 | 143 | button 144 | .getCount() 145 | .then(count => { 146 | // Do something with count 😎 147 | }) 148 | .catch(error => { 149 | // Something went wrong 😢 150 | }); 151 | ``` 152 | 153 | | Method | Parameters | Description | 154 | | ------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 155 | | `getCount()` | - | Returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that will either resolve with a count or an error. | 156 | | `openPopup(event)` | `Event` | Open the associated dialog. This will be blocked unless it is as a result of user input. We'd suggest calling this as the callback for `addEventListener` or similar and passing the relevant event. | 157 | 158 | --- 159 | 160 | The following needs revision. 161 | 162 | --- 163 | 164 | ## Options 165 | 166 | There are a ton of options that ship with Shr. These allow you to customize the library to your needs. 167 | 168 | ### debug 169 | 170 | _Default_: `false` 171 | _Type_: Boolean 172 | 173 | If you are are just debugging Shr in a development environment, you can turn on debugging when you setup. 174 | 175 | ```javascript 176 | Shr.setup('.js-shr', { 177 | debug: true, 178 | }); 179 | ``` 180 | 181 | ### count.className 182 | 183 | _Default_: `shr__count` 184 | _Type_: String 185 | 186 | When adding the share count element to the screen, this is the className used to style it. 187 | 188 | ```javascript 189 | Shr.setup('.js-shr', { 190 | count: { 191 | className: 'your-class-name', 192 | }, 193 | }); 194 | ``` 195 | 196 | ### count.displayZero 197 | 198 | _Default_: `false` 199 | _Type_: Boolean 200 | 201 | Sometimes your URL has not been shared yet. You can choose whether or not you want to display 0 shares or not. Some APIs don't allow for the actual count, so those APIs will just be a link to share (such as Twitter) and won't show the count if this is turned on or not. 202 | 203 | ```javascript 204 | Shr.setup('.js-shr', { 205 | count: { 206 | displayZero: 'your-class-name', 207 | }, 208 | }); 209 | ``` 210 | 211 | ### count.format 212 | 213 | _Default_: `true` 214 | _Type_: Boolean 215 | 216 | By default, Shr shortens the amount of shares to an easier to read number. Say you have a URL that went viral and you have over 1 million shares. By default, Shr shows this as 1 M. You can, however, turn this off and show the exact amount of shares. 217 | 218 | ```javascript 219 | Shr.setup('.js-shr', { 220 | count: { 221 | format: false, 222 | }, 223 | }); 224 | ``` 225 | 226 | ### count.position 227 | 228 | _Default_: `after` 229 | _Type_: Enum (`'before'` or `'after'`) 230 | 231 | By default, the number of shares shows up after the social icon. This means it's to the right of the icon. You can change this to be before the social icon (the left of the icon) by setting this value to `before`. 232 | 233 | ```javascript 234 | Shr.setup('.js-shr', { 235 | count: { 236 | position: 'before', 237 | }, 238 | }); 239 | ``` 240 | 241 | ### count.increment 242 | 243 | _Default_: `true` 244 | _Type_: Boolean 245 | 246 | When the user clicks the share icon, we automatically update the count. However, this is assuming that the user went through with the sharing. This is for speed and reactivity. If you don't want this behavior, you can set this value to false. 247 | 248 | ```javascript 249 | Shr.setup('.js-shr', { 250 | count: { 251 | increment: false, 252 | }, 253 | }); 254 | ``` 255 | 256 | ### storage 257 | 258 | _Default_: 259 | 260 | ```javascript 261 | { 262 | enabled: true, 263 | key: 'shr', 264 | ttl: 300000, 265 | } 266 | ``` 267 | 268 | _Type_: 269 | 270 | ```javascript 271 | { 272 | enabled: Boolean, // Whether storage is supported 273 | key: String, // Key to be used in local storage - (NOTE: can't contain special characters or whitespace) 274 | ttl: Number, // Seconds to keep the storage valid 275 | } 276 | ``` 277 | 278 | To save requests and speed up your site, Shr saves all of the values used to local storage. The two keys you can set are the `key` of the local storage and the time to live (AKA: how long do you want these values to last before we refresh). These can be customized to your liking in the setup like. 279 | 280 | ```javascript 281 | Shr.setup('.js-shr', { 282 | storage: { 283 | key: `yourkey`, 284 | ttl: 100, 285 | }, 286 | }); 287 | ``` 288 | 289 | ## HTML 290 | 291 | Shr provides a ton of networks that can be used on your site. Each button has certain attributes that need to be defined in order for Shr to operate efficiently and effictively. Below are descriptions and an example of each button and how to use it. 292 | 293 | ### Twitter 294 | 295 | This button allows you to tweet a URL on Twitter. A count is not available for this button due to [Twitter deciding to remove the API endpoint](https://twittercommunity.com/t/a-new-design-for-tweet-and-follow-buttons/52791), for whatever reason. 296 | 297 | ```html 298 | 306 | ``` 307 | 308 | There are 3 variables you can add to your Twitter share: 309 | 310 | 1. `text` - This is the text of your tweet. Makes ure it's properly URL encoded. 311 | 2. `url` - This is the URL you wish to share via Twitter. The URL needs to be properly encoded as well. 312 | 3. `via` - This is who is sharing the tweet (your username) 313 | 314 | ### Pinterest 315 | 316 | This button allows you to post a pin to Pinterest. 317 | 318 | ```html 319 | 324 | 325 | Pin it 326 | 327 | ``` 328 | 329 | There are 3 variables you can add to your Pinterest pin: 330 | 331 | 1. `url` - This is the URL you wish to pin on Pinterest. Make sure it's properly URL encoded. 332 | 2. `media` - This is the URL to an image you wish to pin. Make sure it's properly URL encoded. 333 | 3. `description` - This is a URL encoded description for your pin. 334 | 335 | ### Facebook 336 | 337 | This button allows you to share on Facebook. 338 | 339 | ```html 340 | 348 | ``` 349 | 350 | When entering your URL for Facebook, make sure it's properly URL encoded! The number of shares will appear next to the button for the URL you are sharing. 351 | 352 | ### GitHub 353 | 354 | This button allows you to star a repo on GitHub and shows the current number of stars for the project. 355 | 356 | ```html 357 | 358 | Star 359 | 360 | ``` 361 | 362 | ### YouTube 363 | 364 | This button allows you to subscribe to a channel on YouTube and shows the current number of subscribers. 365 | 366 | ```html 367 | 368 | Subscribe 369 | 370 | ``` 371 | 372 | ## Browser support 373 | 374 | Shr is supported in all modern browsers and IE11. 375 | 376 | ## Issues 377 | 378 | If you find anything weird with Shr, please let us know using the GitHub issues tracker. 379 | 380 | ## Author 381 | 382 | Shr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me](http://sampotts.me) with generous help from: 383 | 384 | - [@danpastori](https://github.com/danpastori) 385 | - [@danfoley](https://github.com/danfoley) 386 | - ...and other [contributors](https://github.com/sampotts/shr/graphs/contributors) 387 | 388 | ## Donate 389 | 390 | Shr costs money to run, not my time (I donate that for free) but domains, hosting and more. Any help is appreciated... [Donate to support Shr](https://www.paypal.me/pottsy/20usd) 391 | 392 | ## Thanks 393 | 394 | ![Fastly](https://www.fastly.com/sites/all/themes/custom/fastly2016/logo.png)(https://www.fastly.com/) 395 | 396 | Thanks to [Fastly](https://www.fastly.com/) for providing the CDN services. 397 | 398 | ## Copyright and License 399 | 400 | [The MIT license](license.md). 401 | -------------------------------------------------------------------------------- /shr.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "search.exclude": { 9 | "**/node_modules": true, 10 | "**/dist": true 11 | }, 12 | 13 | // Linting 14 | "stylelint.enable": true, 15 | "css.validate": false, 16 | "scss.validate": false, 17 | "less.validate": false, 18 | "javascript.validate.enable": false, 19 | 20 | // Prettier 21 | "prettier.eslintIntegration": true, 22 | "prettier.stylelintIntegration": true, 23 | 24 | // Formatting 25 | "editor.tabSize": 4, 26 | "editor.insertSpaces": true, 27 | "editor.formatOnSave": true, 28 | "editor.codeActionsOnSave": { 29 | "source.organizeImports": true 30 | }, 31 | 32 | // Trim on save 33 | "files.trimTrailingWhitespace": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/js/config/constants.js: -------------------------------------------------------------------------------- 1 | import is from '../utils/is'; 2 | 3 | /** 4 | * Constants. These are uneditable by the user. These will get merged into 5 | * the global config after the user defaults so the user can't overwrite these 6 | * values. 7 | * 8 | * @typedef {Object} constants 9 | * @type {Object} 10 | * 11 | * @property {Object} facebook - The settings for Facebook within Shr. 12 | * @property {function} facebook.url - The method that returns the API Url to get the share count for Facebook. 13 | * @property {function} facebook.shareCount - The method that extracts the number we need from the data returned from the API for Facebook. 14 | * 15 | * @property {Object} twitter - The settings for Twitter within Shr. 16 | * @property {function} twitter.url - The method that returns the API Url to get the share count for Twitter. 17 | * @property {function} twitter.shareCount - The method that extracts the number we need from the data returned from the API for Twitter. 18 | * 19 | * @property {Object} pinterest - The settings for Pinterest within Shr. 20 | * @property {function} pinterest.url - The method that returns the API Url to get the share count for Pinterest. 21 | * @property {function} pinterest.shareCount - The method that extracts the number we need from the data returned from the API for Pinterest. 22 | * 23 | * @property {Object} github - The settings for GitHub within Shr. 24 | * @property {function} github.url - The method that returns the API Url to get the share count for GitHub. 25 | * @property {function} github.shareCount - The method that extracts the number we need from the data returned from the API for GitHub. 26 | */ 27 | 28 | const constants = { 29 | facebook: { 30 | domain: 'facebook.com', 31 | url: url => `https://graph.facebook.com/?id=${url}&fields=og_object{engagement}`, 32 | shareCount: data => data.og_object.engagement.count, 33 | popup: { 34 | width: 640, 35 | height: 360, 36 | }, 37 | }, 38 | 39 | twitter: { 40 | domain: 'twitter.com', 41 | url: () => null, 42 | shareCount: () => null, 43 | popup: { 44 | width: 640, 45 | height: 240, 46 | }, 47 | }, 48 | 49 | pinterest: { 50 | domain: 'pinterest.com', 51 | url: url => `https://widgets.pinterest.com/v1/urls/count.json?url=${url}`, 52 | shareCount: data => data.count, 53 | popup: { 54 | width: 830, 55 | height: 700, 56 | }, 57 | }, 58 | 59 | github: { 60 | domain: 'github.com', 61 | url: (path, token) => `https://api.github.com/repos/${path}${is.string(token) ? `?access_token=${token}` : ''}`, 62 | shareCount: data => data.data.stargazers_count, 63 | }, 64 | 65 | youtube: { 66 | domain: 'youtube.com', 67 | url: (id, key) => `https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${id}&key=${key}`, 68 | shareCount: data => { 69 | if (!is.empty(data.error)) { 70 | return null; 71 | } 72 | 73 | const [first] = data.items; 74 | 75 | if (is.empty(first)) { 76 | return null; 77 | } 78 | 79 | return first.statistics.subscriberCount; 80 | }, 81 | }, 82 | }; 83 | 84 | export default constants; 85 | -------------------------------------------------------------------------------- /src/js/config/defaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default Shr Config. All variables, settings and states are stored here 3 | * and global. These are the defaults. The user can edit these at will when 4 | * initializing Shr. 5 | * 6 | * @typedef {Object} defaults 7 | * @type {Object} 8 | * 9 | * @property {Boolean} debug - The flag for if we debug Shr or not. By defaul this is false. 10 | 11 | * @property {Object} wrapper The object containing the settings for the wrapper that's added. 12 | * @property {String} wrapper.className Classname for the wrapper. 13 | 14 | * @property {Object} count - The object containing the settings for the count. 15 | * @property {String} count.className - Classname for the share count. 16 | * @property {Boolean} count.displayZero - Determines if we display zero values. 17 | * @property {Boolean} count.format - Display 1000 as 1K, 1000000 as 1M, etc 18 | * @property {String} count.position - Inject the count before or after the link in the DOM 19 | * @property {Boolean} count.increment - Determines if we increment the count on click. This assumes the share is valid. 20 | * 21 | * @property {Object} tokens - The object containing authentication tokens. 22 | * @property {Object} tokens.github The optional authentication tokens for GitHub (to prevent rate limiting). 23 | * @property {String} tokens.youtube The public key you need to get the subscriber count for YouTube. 24 | * 25 | * @property {Object} storage - The object containing the settings for local storage. 26 | * @property {Boolean} storage.enabled - Determines if local storage is enabled for the browser or not. 27 | * @property {String} storage.key - The key that the storage will use to access Shr data. 28 | * @property {Number} storage.ttl - The time to live for the local storage values if available. 29 | */ 30 | 31 | /** 32 | * 33 | */ 34 | 35 | const defaults = { 36 | debug: false, 37 | wrapper: { 38 | className: 'shr', 39 | }, 40 | count: { 41 | className: 'shr__count', 42 | displayZero: false, 43 | format: true, 44 | position: 'after', 45 | increment: true, 46 | }, 47 | tokens: { 48 | github: '', 49 | youtube: '', 50 | }, 51 | storage: { 52 | enabled: true, 53 | key: 'shr', 54 | ttl: 300000, 55 | }, 56 | }; 57 | 58 | export default defaults; 59 | -------------------------------------------------------------------------------- /src/js/shr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Shr.js 3 | * @version 2.0.2 4 | * @author Sam Potts 5 | * @license MIT 6 | */ 7 | 8 | import constants from './config/constants'; 9 | import defaults from './config/defaults'; 10 | import { getJSONP } from './utils/ajax'; 11 | import Console from './utils/console'; 12 | import { matches } from './utils/css'; 13 | import { createElement, wrap } from './utils/elements'; 14 | import is from './utils/is'; 15 | import { formatNumber } from './utils/numbers'; 16 | import { extend } from './utils/objects'; 17 | import Storage from './utils/storage'; 18 | import { getDomain } from './utils/urls'; 19 | 20 | class Shr { 21 | /** 22 | * Setup a new instance 23 | * @param {String|Element} target 24 | * @param {Object} options 25 | */ 26 | constructor(target, options) { 27 | this.elements = { 28 | count: null, 29 | trigger: null, 30 | popup: null, 31 | }; 32 | 33 | if (is.element(target)) { 34 | // An Element is passed, use it directly 35 | this.elements.trigger = target; 36 | } else if (is.string(target)) { 37 | // A CSS Selector is passed, fetch it from the DOM 38 | this.elements.trigger = document.querySelector(target); 39 | } 40 | 41 | if (!is.element(this.elements.trigger) || !is.empty(this.elements.trigger.shr)) { 42 | return; 43 | } 44 | 45 | this.config = extend({}, defaults, options, { networks: constants }); 46 | 47 | this.console = new Console(this.config.debug); 48 | 49 | this.storage = new Storage(this.config.storage.key, this.config.storage.ttl, this.config.storage.enabled); 50 | 51 | this.getCount() 52 | .then(data => this.updateDisplay(data)) 53 | .catch(() => {}); 54 | 55 | this.listeners(true); 56 | 57 | this.elements.trigger.shr = this; 58 | } 59 | 60 | /** 61 | * Destroy the current instance 62 | * @returns {Void} 63 | */ 64 | destroy() { 65 | this.listeners(false); 66 | 67 | // TODO: Remove the count and unwrap 68 | } 69 | 70 | /** 71 | * Setup event listeners 72 | * @returns {Void} 73 | */ 74 | listeners(toggle = false) { 75 | const method = toggle ? 'addEventListener' : 'removeEventListener'; 76 | 77 | this.elements.trigger[method]('click', event => this.share(event), false); 78 | } 79 | 80 | /** 81 | * Gets the href from the trigger link 82 | * @returns {String} The href attribute from the link 83 | */ 84 | get href() { 85 | if (!is.element(this.elements.trigger)) { 86 | return null; 87 | } 88 | 89 | return this.elements.trigger.href; 90 | } 91 | 92 | /** 93 | * Gets the network for this instance 94 | * @returns {String} The network name in lowercase 95 | */ 96 | get network() { 97 | if (!is.element(this.elements.trigger)) { 98 | return null; 99 | } 100 | 101 | const { networks } = this.config; 102 | 103 | return Object.keys(networks).find(n => getDomain(this.href) === networks[n].domain); 104 | } 105 | 106 | /** 107 | * Gets the config for the specified network 108 | * @returns {Object} The config options 109 | */ 110 | get networkConfig() { 111 | if (is.empty(this.network)) { 112 | return null; 113 | } 114 | 115 | return this.config.networks[this.network]; 116 | } 117 | 118 | /** 119 | * Gets the URL or ID we are geting the share count for 120 | * @returns {String} The target ID or URL we're sharing 121 | */ 122 | get target() { 123 | if (is.empty(this.network)) { 124 | return null; 125 | } 126 | 127 | const url = new URL(this.href); 128 | 129 | switch (this.network) { 130 | case 'facebook': 131 | return url.searchParams.get('u'); 132 | 133 | case 'github': 134 | return url.pathname.substring(1); 135 | 136 | case 'youtube': 137 | return url.pathname.split('/').pop(); 138 | 139 | default: 140 | return url.searchParams.get('url'); 141 | } 142 | } 143 | 144 | /** 145 | * Gets for the URL for the JSONP endpoint 146 | * @returns {String} The URL for the JSONP endpoint 147 | */ 148 | get apiUrl() { 149 | if (is.empty(this.network)) { 150 | return null; 151 | } 152 | 153 | const { tokens } = this.config; 154 | 155 | switch (this.network) { 156 | case 'github': 157 | return this.networkConfig.url(this.target, tokens.github); 158 | 159 | case 'youtube': 160 | return this.networkConfig.url(this.target, tokens.youtube); 161 | 162 | default: 163 | return this.networkConfig.url(encodeURIComponent(this.target)); 164 | } 165 | } 166 | 167 | /** 168 | * Initiate the share process 169 | * This must be user triggered or the popup will be blocked 170 | * @param {Event} event The user input event 171 | * @returns {Void} 172 | */ 173 | share(event) { 174 | this.openPopup(event); 175 | 176 | const { increment } = this.config.count; 177 | 178 | this.getCount() 179 | .then(data => this.updateDisplay(data, increment)) 180 | .catch(() => {}); 181 | } 182 | 183 | /** 184 | * Displays a pop up window to share on the requested social network. 185 | * @param {Event} event - Event that triggered the popup window. 186 | * @returns {Void} 187 | */ 188 | openPopup(event) { 189 | // Only popup if we need to... 190 | if (is.empty(this.network) || !this.networkConfig.popup) { 191 | return; 192 | } 193 | 194 | // Prevent the link opening 195 | if (is.event(event)) { 196 | event.preventDefault(); 197 | } 198 | 199 | // Set variables for the popup 200 | const size = this.networkConfig.popup; 201 | const { width, height } = size; 202 | const name = `shr-popup--${this.network}`; 203 | 204 | // If window already exists, just focus it 205 | if (this.popup && !this.popup.closed) { 206 | this.popup.focus(); 207 | 208 | this.console.log('Popup re-focused.'); 209 | } else { 210 | // Get position 211 | const left = window.screenLeft !== undefined ? window.screenLeft : window.screen.left; 212 | const top = window.screenTop !== undefined ? window.screenTop : window.screen.top; 213 | // Open in the centre of the screen 214 | const x = window.screen.width / 2 - width / 2 + left; 215 | const y = window.screen.height / 2 - height / 2 + top; 216 | 217 | // Open that window 218 | this.popup = window.open(this.href, name, `top=${y},left=${x},width=${width},height=${height}`); 219 | 220 | // Determine if the popup was blocked 221 | const blocked = !this.popup || this.popup.closed || !is.boolean(this.popup.closed); 222 | 223 | // Focus new window 224 | if (!blocked) { 225 | this.popup.focus(); 226 | 227 | // Nullify opener to prevent "tab nabbing" 228 | // https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/ 229 | // this.popup.opener = null; 230 | 231 | this.console.log('Popup opened.'); 232 | } else { 233 | this.console.error('Popup blocked.'); 234 | } 235 | } 236 | } 237 | 238 | /** 239 | * Get the count for the url from API 240 | * @param {Boolean} useCache Whether to use the local storage cache or not 241 | * @returns {Promise} 242 | */ 243 | getCount(useCache = true) { 244 | return new Promise((resolve, reject) => { 245 | // Format the JSONP endpoint 246 | const url = this.apiUrl; 247 | 248 | // If there's an endpoint. For some social networks, you can't 249 | // get the share count (like Twitter) so we won't have any data. The link 250 | // will be to share it, but you won't get a count of how many people have. 251 | 252 | if (is.empty(url)) { 253 | reject(new Error(`No URL available for ${this.network}.`)); 254 | return; 255 | } 256 | 257 | // Check cache first 258 | if (useCache) { 259 | const cached = this.storage.get(this.target); 260 | 261 | if (!is.empty(cached) && Object.keys(cached).includes(this.network)) { 262 | const count = cached[this.network]; 263 | resolve(is.number(count) ? count : 0); 264 | this.console.log(`getCount for '${this.target}' for '${this.network}' resolved from cache.`); 265 | return; 266 | } 267 | } 268 | 269 | // When we get here, this means the cached counts are not valid, 270 | // or don't exist. We will call the API if the URL is available 271 | // at this point. 272 | 273 | // Runs a GET request on the URL 274 | getJSONP(url) 275 | .then(data => { 276 | let count = 0; 277 | const custom = this.elements.trigger.getAttribute('data-shr-display'); 278 | 279 | // Get value based on config 280 | if (!is.empty(custom)) { 281 | count = data[custom]; 282 | } else { 283 | count = this.networkConfig.shareCount(data); 284 | } 285 | 286 | // Default to zero for undefined 287 | if (is.empty(count)) { 288 | count = 0; 289 | } else { 290 | // Parse 291 | count = parseInt(count, 10); 292 | 293 | // Handle NaN 294 | if (!is.number(count)) { 295 | count = 0; 296 | } 297 | } 298 | 299 | // Cache in local storage 300 | this.storage.set({ 301 | [this.target]: { 302 | [this.network]: count, 303 | }, 304 | }); 305 | 306 | resolve(count); 307 | }) 308 | .catch(reject); 309 | }); 310 | } 311 | 312 | /** 313 | * Display the count 314 | * @param {Number} input - The count returned from the share count API 315 | * @param {Boolean} increment - Determines if we should increment the count or not 316 | * @returns {Void} 317 | */ 318 | updateDisplay(input, increment = false) { 319 | const { count, wrapper } = this.config; 320 | // If we're incrementing (e.g. on click) 321 | const number = increment ? input + 1 : input; 322 | // Standardize position 323 | const position = count.position.toLowerCase(); 324 | 325 | // Only display if there's a count 326 | if (number > 0 || count.displayZero) { 327 | const isAfter = position === 'after'; 328 | const round = unit => Math.round((number / unit) * 10) / 10; 329 | let label = formatNumber(number); 330 | 331 | // Format to 1K, 1M, etc 332 | if (count.format) { 333 | if (number > 1000000) { 334 | label = `${round(1000000)}M`; 335 | } else if (number > 1000) { 336 | label = `${round(1000)}K`; 337 | } 338 | } 339 | 340 | // Update or insert 341 | if (is.element(this.elements.count)) { 342 | this.elements.count.textContent = label; 343 | } else { 344 | // Add wrapper 345 | wrap( 346 | this.elements.trigger, 347 | createElement('span', { 348 | class: wrapper.className, 349 | }), 350 | ); 351 | 352 | // Create count display 353 | this.elements.count = createElement( 354 | 'span', 355 | { 356 | class: `${count.className} ${count.className}--${position}`, 357 | }, 358 | label, 359 | ); 360 | 361 | // Insert count display 362 | this.elements.trigger.insertAdjacentElement(isAfter ? 'afterend' : 'beforebegin', this.elements.count); 363 | } 364 | } 365 | } 366 | 367 | /** 368 | * Setup multiple instances 369 | * @param {String|Element|NodeList|Array} target 370 | * @param {Object} options 371 | * @returns {Array} - An array of instances 372 | */ 373 | static setup(target, options = {}) { 374 | let targets = null; 375 | 376 | if (is.string(target)) { 377 | targets = Array.from(document.querySelectorAll(target)); 378 | } else if (is.element(target)) { 379 | targets = [target]; 380 | } else if (is.nodeList(target)) { 381 | targets = Array.from(target); 382 | } else if (is.array(target)) { 383 | targets = target.filter(is.element); 384 | } 385 | 386 | if (is.empty(targets)) { 387 | return null; 388 | } 389 | 390 | const config = Object.assign({}, defaults, options); 391 | 392 | if (is.string(target) && config.watch) { 393 | // Create an observer instance 394 | const observer = new MutationObserver(mutations => { 395 | Array.from(mutations).forEach(mutation => { 396 | Array.from(mutation.addedNodes).forEach(node => { 397 | if (!is.element(node) || !matches(node, target)) { 398 | return; 399 | } 400 | 401 | // eslint-disable-next-line no-unused-vars 402 | const share = new Shr(node, config); 403 | }); 404 | }); 405 | }); 406 | 407 | // Pass in the target node, as well as the observer options 408 | observer.observe(document.body, { 409 | childList: true, 410 | subtree: true, 411 | }); 412 | } 413 | 414 | return targets.map(t => new Shr(t, options)); 415 | } 416 | } 417 | 418 | export default Shr; 419 | -------------------------------------------------------------------------------- /src/js/utils/ajax.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes the JSONP request to get the social network to get the share count. 3 | * 4 | * @param {string} url - The URL of the of the sharing API. 5 | * @param {function} callback - The callback funciton once the API completes the request. 6 | */ 7 | export function getJSONP(url) { 8 | return new Promise((resolve, reject) => { 9 | // Generate a random callback 10 | const name = `jsonp_callback_${Math.round(100000 * Math.random())}`; 11 | // Create a faux script 12 | const script = document.createElement('script'); 13 | 14 | // Handle errors 15 | script.addEventListener('error', error => reject(error)); 16 | 17 | // Cleanup to prevent memory leaks and hit original callback 18 | window[name] = data => { 19 | delete window[name]; 20 | document.body.removeChild(script); 21 | resolve(data); 22 | }; 23 | 24 | // Add callback to URL 25 | const src = new URL(url); 26 | src.searchParams.set('callback', name); 27 | 28 | // Set src and load 29 | script.setAttribute('src', src.toString()); 30 | 31 | // Inject to the body 32 | document.body.appendChild(script); 33 | }); 34 | } 35 | 36 | export default { getJSONP }; 37 | -------------------------------------------------------------------------------- /src/js/utils/console.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Console wrapper 3 | // ========================================================================== 4 | 5 | const noop = () => {}; 6 | 7 | export default class Console { 8 | constructor(enabled = false) { 9 | this.enabled = window.console && enabled; 10 | 11 | if (this.enabled) { 12 | this.log('Debugging enabled'); 13 | } 14 | } 15 | 16 | get log() { 17 | return this.enabled 18 | ? Function.prototype.bind.call(console.log, console) // eslint-disable-line no-console 19 | : noop; 20 | } 21 | 22 | get warn() { 23 | return this.enabled 24 | ? Function.prototype.bind.call(console.warn, console) // eslint-disable-line no-console 25 | : noop; 26 | } 27 | 28 | get error() { 29 | return this.enabled 30 | ? Function.prototype.bind.call(console.error, console) // eslint-disable-line no-console 31 | : noop; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/js/utils/css.js: -------------------------------------------------------------------------------- 1 | // Element matches a selector 2 | export function matches(element, selector) { 3 | const prototype = { Element }; 4 | 5 | function match() { 6 | return Array.from(document.querySelectorAll(selector)).includes(this); 7 | } 8 | 9 | const method = 10 | prototype.matches || 11 | prototype.webkitMatchesSelector || 12 | prototype.mozMatchesSelector || 13 | prototype.msMatchesSelector || 14 | match; 15 | 16 | return method.call(element, selector); 17 | } 18 | 19 | export default { matches }; 20 | -------------------------------------------------------------------------------- /src/js/utils/elements.js: -------------------------------------------------------------------------------- 1 | import is from './is'; 2 | 3 | /** 4 | * Wrap one or more HTMLElement in wrapper container 5 | * @param {HTMLElement[]} elements 6 | * @param {HTMLElement} wrapper 7 | * @returns {Void} 8 | */ 9 | export function wrap(elements, wrapper) { 10 | // Convert `elements` to an array, if necessary. 11 | const targets = elements.length ? elements : [elements]; 12 | 13 | // Loops backwards to prevent having to clone the wrapper on the 14 | // first element (see `child` below). 15 | Array.from(targets) 16 | .reverse() 17 | .forEach((element, index) => { 18 | const child = index > 0 ? wrapper.cloneNode(true) : wrapper; 19 | // Cache the current parent and sibling. 20 | const parent = element.parentNode; 21 | const sibling = element.nextSibling; 22 | 23 | // Wrap the element (is automatically removed from its current 24 | // parent). 25 | child.appendChild(element); 26 | 27 | // If the element had a sibling, insert the wrapper before 28 | // the sibling to maintain the HTML structure; otherwise, just 29 | // append it to the parent. 30 | if (sibling) { 31 | parent.insertBefore(child, sibling); 32 | } else { 33 | parent.appendChild(child); 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * Set HTMLElement attributes 40 | * @param {HTMLElement} element 41 | * @param {Object} attributes 42 | * @returns {Void} 43 | */ 44 | export function setAttributes(element, attributes) { 45 | if (!is.element(element) || is.empty(attributes)) { 46 | return; 47 | } 48 | 49 | // Assume null and undefined attributes should be left out, 50 | // Setting them would otherwise convert them to "null" and "undefined" 51 | Object.entries(attributes) 52 | .filter(([, value]) => !is.nullOrUndefined(value)) 53 | .forEach(([key, value]) => element.setAttribute(key, value)); 54 | } 55 | 56 | /** 57 | * Create a HTMLElement 58 | * @param {String} type - Type of element to create 59 | * @param {Object} attributes - Object of HTML attributes 60 | * @param {String} text - Sets the text content 61 | * @returns {HTMLElement} 62 | */ 63 | export function createElement(type, attributes, text) { 64 | // Create a new 65 | const element = document.createElement(type); 66 | 67 | // Set all passed attributes 68 | if (is.object(attributes)) { 69 | setAttributes(element, attributes); 70 | } 71 | 72 | // Add text node 73 | if (is.string(text)) { 74 | element.innerText = text; 75 | } 76 | 77 | // Return built element 78 | return element; 79 | } 80 | 81 | export default { wrap }; 82 | -------------------------------------------------------------------------------- /src/js/utils/is.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Type checking utils 3 | // ========================================================================== 4 | 5 | const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null); 6 | const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor); 7 | const isNullOrUndefined = input => input === null || typeof input === 'undefined'; 8 | const isObject = input => getConstructor(input) === Object; 9 | const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input); 10 | const isString = input => getConstructor(input) === String; 11 | const isBoolean = input => getConstructor(input) === Boolean; 12 | const isFunction = input => getConstructor(input) === Function; 13 | const isArray = input => Array.isArray(input); 14 | const isNodeList = input => instanceOf(input, NodeList); 15 | const isElement = input => instanceOf(input, Element); 16 | const isEvent = input => instanceOf(input, Event); 17 | const isEmpty = input => 18 | isNullOrUndefined(input) || 19 | ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) || 20 | (isObject(input) && !Object.keys(input).length); 21 | 22 | export default { 23 | nullOrUndefined: isNullOrUndefined, 24 | object: isObject, 25 | number: isNumber, 26 | string: isString, 27 | boolean: isBoolean, 28 | function: isFunction, 29 | array: isArray, 30 | nodeList: isNodeList, 31 | element: isElement, 32 | event: isEvent, 33 | empty: isEmpty, 34 | }; 35 | -------------------------------------------------------------------------------- /src/js/utils/numbers.js: -------------------------------------------------------------------------------- 1 | export function formatNumber(number) { 2 | // Work out whether decimal separator is . or , for localised numbers 3 | const decimalSeparator = /\./.test((1.1).toLocaleString()) ? '.' : ','; 4 | // Round n to an integer and present 5 | const regex = new RegExp(`\\${decimalSeparator}\\d+$`); 6 | 7 | return Math.round(number) 8 | .toLocaleString() 9 | .replace(regex, ''); 10 | } 11 | 12 | export default { formatNumber }; 13 | -------------------------------------------------------------------------------- /src/js/utils/objects.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Object utils 3 | // ========================================================================== 4 | 5 | import is from './is'; 6 | 7 | // Deep extend destination object with N more objects 8 | export function extend(target = {}, ...sources) { 9 | if (!sources.length) { 10 | return target; 11 | } 12 | 13 | const source = sources.shift(); 14 | 15 | if (!is.object(source)) { 16 | return target; 17 | } 18 | 19 | Object.keys(source).forEach(key => { 20 | if (is.object(source[key])) { 21 | if (!Object.keys(target).includes(key)) { 22 | Object.assign(target, { [key]: {} }); 23 | } 24 | 25 | extend(target[key], source[key]); 26 | } else { 27 | Object.assign(target, { [key]: source[key] }); 28 | } 29 | }); 30 | 31 | return extend(target, ...sources); 32 | } 33 | 34 | export default { extend }; 35 | -------------------------------------------------------------------------------- /src/js/utils/storage.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Plyr storage 3 | // ========================================================================== 4 | 5 | import is from './is'; 6 | import { extend } from './objects'; 7 | 8 | class Storage { 9 | constructor(key, ttl, enabled = true) { 10 | this.enabled = enabled && Storage.supported; 11 | this.key = key; 12 | this.ttl = ttl; 13 | } 14 | 15 | // Check for actual support (see if we can use it) 16 | static get supported() { 17 | try { 18 | if (!('localStorage' in window)) { 19 | return false; 20 | } 21 | 22 | const test = '___test'; 23 | 24 | // Try to use it (it might be disabled, e.g. user is in private mode) 25 | // see: https://github.com/sampotts/plyr/issues/131 26 | window.localStorage.setItem(test, test); 27 | window.localStorage.removeItem(test); 28 | 29 | return true; 30 | } catch (e) { 31 | return false; 32 | } 33 | } 34 | 35 | get(key) { 36 | if (!Storage.supported || !this.enabled) { 37 | return null; 38 | } 39 | 40 | const store = window.localStorage.getItem(this.key); 41 | 42 | if (is.empty(store)) { 43 | return null; 44 | } 45 | 46 | // Check TTL 47 | const ttl = window.localStorage.getItem(`${this.key}_ttl`); 48 | 49 | if (is.empty(ttl) || ttl < Date.now()) { 50 | return null; 51 | } 52 | 53 | const json = JSON.parse(store); 54 | 55 | return is.string(key) && key.length ? json[key] : json; 56 | } 57 | 58 | set(object) { 59 | // Bail if we don't have localStorage support or it's disabled 60 | if (!Storage.supported || !this.enabled) { 61 | return; 62 | } 63 | 64 | // Can only store objectst 65 | if (!is.object(object)) { 66 | return; 67 | } 68 | 69 | // Get current storage 70 | let storage = this.get(); 71 | 72 | // Default to empty object 73 | if (is.empty(storage)) { 74 | storage = {}; 75 | } 76 | 77 | // Update the working copy of the values 78 | extend(storage, object); 79 | 80 | // Update storage and TTL record 81 | window.localStorage.setItem(this.key, JSON.stringify(storage)); 82 | window.localStorage.setItem(`${this.key}_ttl`, Date.now() + this.ttl); 83 | } 84 | } 85 | 86 | export default Storage; 87 | -------------------------------------------------------------------------------- /src/js/utils/urls.js: -------------------------------------------------------------------------------- 1 | export function getDomain(href) { 2 | const url = new URL(href); 3 | let domain = url.hostname; 4 | const parts = domain.split('.'); 5 | const { length } = parts; 6 | 7 | // Extract the root domain 8 | if (length > 2) { 9 | domain = `${parts[length - 2]}.${parts[length - 1]}`; 10 | 11 | // Check to see if it's using a Country Code Top Level Domain (ccTLD) (i.e. ".me.uk") 12 | if (parts[length - 2].length === 2 && parts[length - 1].length === 2) { 13 | domain = `${parts[length - 3]}.${domain}`; 14 | } 15 | } 16 | 17 | return domain; 18 | } 19 | 20 | export default { getDomain }; 21 | -------------------------------------------------------------------------------- /src/sass/button.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Button styling 3 | // ========================================================================== 4 | 5 | .shr, 6 | .shr__button { 7 | display: inline-flex; 8 | align-items: center; 9 | vertical-align: middle; 10 | margin-top: ceil($shr-padding-base / 4); 11 | margin-bottom: ceil($shr-padding-base / 4); 12 | white-space: nowrap; 13 | } 14 | 15 | // Cancel out margin for wrapped buttons 16 | .shr .shr__button { 17 | margin-top: 0; 18 | margin-bottom: 0; 19 | } 20 | 21 | // Adjacent buttons 22 | .shr + .shr, 23 | .shr__button + .shr__button, 24 | .shr + .shr__button, 25 | .shr__button + .shr { 26 | margin-left: ceil($shr-padding-base / 2); 27 | } 28 | 29 | // Shared 30 | .shr__button, 31 | .shr__count { 32 | vertical-align: middle; 33 | border-radius: $shr-button-border-radius; 34 | font-weight: $shr-font-weight; 35 | box-shadow: transparentize($color: #000, $amount: 0.1); 36 | } 37 | 38 | // Buttons 39 | .shr__button { 40 | padding: ceil($shr-padding-base / 4) ceil($shr-padding-base / 2); 41 | @include shr-button-styles(); 42 | transition: all 0.3s ease; 43 | color: $shr-button-text-color; 44 | text-decoration: none; 45 | user-select: none; 46 | border: 0; 47 | 48 | // Focus styles are handled in the mixin 49 | &:focus { 50 | outline: 0; 51 | } 52 | 53 | &:hover, 54 | &:focus { 55 | border: 0; 56 | } 57 | 58 | // Icons 59 | svg { 60 | display: inline-block; 61 | fill: currentColor; 62 | width: 16px; 63 | height: 16px; 64 | margin-right: ceil($shr-padding-base / 2); 65 | } 66 | 67 | // Network specific styles 68 | &--facebook { 69 | @include shr-button-styles($shr-button-facebook-bg-color); 70 | } 71 | 72 | &--twitter { 73 | @include shr-button-styles($shr-button-twitter-bg-color); 74 | } 75 | 76 | &--pinterest { 77 | @include shr-button-styles($shr-button-pinterest-bg-color); 78 | } 79 | 80 | &--google { 81 | @include shr-button-styles($shr-button-google-bg-color); 82 | } 83 | 84 | &--github { 85 | @include shr-button-styles($shr-button-github-bg-color, darken($shr-button-github-bg-color, 10%)); 86 | } 87 | 88 | &--youtube { 89 | @include shr-button-styles($shr-button-youtube-bg-color); 90 | } 91 | } 92 | 93 | // Count bubble 94 | .shr__count { 95 | display: inline-block; 96 | position: relative; 97 | padding: ($shr-padding-base / 4) ($shr-padding-base / 3); 98 | background: $shr-button-count-bg; 99 | text-align: center; 100 | min-width: 32px; 101 | color: $shr-button-count-text-color; 102 | 103 | // The arrow 104 | &::before { 105 | content: ""; 106 | position: absolute; 107 | width: $shr-button-count-arrow-size; 108 | height: $shr-button-count-arrow-size; 109 | top: 50%; 110 | margin-top: -($shr-button-count-arrow-size / 2); 111 | background: inherit; 112 | } 113 | } 114 | 115 | .shr__count--after { 116 | margin-left: ($shr-padding-base / 2); 117 | 118 | &::before { 119 | left: 2px; 120 | transform: rotate(-45deg) translate(-50%, -50%); 121 | } 122 | } 123 | 124 | .shr__count--before { 125 | margin-right: ($shr-padding-base / 2); 126 | 127 | &::before { 128 | right: 2px; 129 | transform: rotate(135deg) translate(-50%, -50%); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/sass/mixins.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Mixins 3 | // ========================================================================== 4 | 5 | // Button styling 6 | // --------------------------------------- 7 | @mixin shr-button-styles($background-color: $shr-button-base-bg-color, $focus-color: transparentize($background-color, 0.66)){ 8 | background-color: $background-color; 9 | @include contrast-text-color($background-color); 10 | 11 | &:hover, 12 | &:focus { 13 | background-color: darken($background-color, 3%); 14 | } 15 | 16 | &:focus { 17 | box-shadow: 0 0 0 3px $focus-color; 18 | } 19 | } 20 | 21 | // Contrast helpers 22 | // --------------------------------------- 23 | @mixin contrast-text-color($background: ""){ 24 | @if (lightness($background) >= 65%){ 25 | color: #333; 26 | } @else { 27 | color: #fff; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/sass/settings.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Shr.js SASS settings 3 | // https://github.com/sampotts/shr 4 | // ========================================================================== 5 | 6 | // Base 7 | $shr-padding-base: 20px !default; 8 | $shr-font-smoothing: on !default; 9 | $shr-line-height: 1.5 !default; 10 | $shr-font-weight: 500 !default; 11 | 12 | // Button 13 | $shr-button-border-radius: 4px !default; 14 | $shr-button-bg-color: #3498db !default; 15 | $shr-button-text-color: #fff !default; 16 | 17 | $shr-button-base-bg-color: #3b5998 !default; 18 | $shr-button-facebook-bg-color: #3c569a !default; 19 | $shr-button-twitter-bg-color: #4baaf4 !default; 20 | $shr-button-google-bg-color: #df4f3f !default; 21 | $shr-button-pinterest-bg-color: #cb2026 !default; 22 | $shr-button-github-bg-color: #f5f5f5 !default; 23 | $shr-button-youtube-bg-color: #ff0000 !default; 24 | 25 | // Count 26 | $shr-button-count-bg: #fff !default; 27 | $shr-button-count-text-color: #55646b !default; 28 | $shr-button-count-arrow-size: 8px !default; 29 | -------------------------------------------------------------------------------- /src/sass/shr.scss: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Shr.js styles 3 | // https://github.com/sampotts/shr 4 | // ========================================================================== 5 | 6 | @import "settings"; 7 | @import "mixins"; 8 | @import "button"; 9 | -------------------------------------------------------------------------------- /src/sprite/shr-facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Facebook 4 | 5 | -------------------------------------------------------------------------------- /src/sprite/shr-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | GitHub 4 | 10 | 11 | -------------------------------------------------------------------------------- /src/sprite/shr-google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google+ 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/sprite/shr-pinterest.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pinterest 4 | 9 | 10 | -------------------------------------------------------------------------------- /src/sprite/shr-twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Twitter 4 | 8 | 9 | -------------------------------------------------------------------------------- /src/sprite/shr-youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | YouTube 4 | 5 | 6 | --------------------------------------------------------------------------------