├── .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 |
79 |
80 |
81 |
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 | [](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 |
40 |
41 | Share
42 |
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 `