├── .gitignore ├── LICENSE ├── README.md ├── combined.woff2 ├── combined.woff2.base64 ├── fontawesome ├── fa-brands-400.ttf ├── fa-brands-400.woff2 ├── fa-regular-400.ttf ├── fa-regular-400.woff2 ├── fa-solid-900.ttf ├── fa-solid-900.woff2 ├── fa-v4compatibility.ttf ├── fa-v4compatibility.woff2 └── merged.ttf ├── package.json ├── share-button.js ├── share.html └── subset.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .d8_history 3 | .vscode 4 | npm-debug.log 5 | .env 6 | package-lock.json 7 | test/*.log 8 | node_modules/* 9 | web_modules/* 10 | urls.json 11 | out/* 12 | tmp/* 13 | build/* 14 | *.code-workspace 15 | 16 | # Local Netlify folder 17 | .netlify 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alex Russell 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | A performance-first web component for content sharing. Initially developed for (and you can see it used on) [infrequently.org](https://infrequently.org/) 4 | 5 | Provides: 6 | 7 | - Sharing via [Web Share API](https://web.dev/web-share/) as well as direct link copy and toot/tweet buttons 8 | - Themeing through CSS variables and Web Component `::part()`s 9 | - Tiny footprint; a single file, ~4KiB gzipped, and traced to death by someone who knows their way around a browser 10 | 11 | ## Using/Configuring 12 | 13 | See `share.html` for examples. 14 | 15 | Basic use is to import the (single) script file (no dependencies), prefereably with modern syntax so as to avoid browsers that can't support it: 16 | 17 | ```html 18 | 21 | 24 | 25 | ``` 26 | 27 | Styling is handled via CSS `::part()` directives and variables, e.g.: 28 | 29 | ```css 30 | share-button.blue { 31 | --color: darkblue; 32 | --hover-bg-color: rgba(53, 49, 181, 0.219); 33 | --transition-duration: 0.2s; 34 | --hover-scale: 1.5; 35 | } 36 | 37 | share-button.never-tweet::part(tweet-button) { 38 | display: none; 39 | } 40 | ``` 41 | 42 | The important `::part()`s are: 43 | 44 | - `share-button` 45 | - `tweet-button` 46 | - `toot-button` 47 | - `copy-link-button` 48 | 49 | ## Building/Hacking 50 | 51 | This thing is a super bare-bones, no-frills, no-frameworks. 52 | 53 | Building requires a functional linux environment (WSL and Crostini work great) with [`fonttools`](https://github.com/fonttools/fonttools) in your `PATH`. On Debian-derived distros, a quick `sudo apt install fonttools` should be enough. 54 | 55 | The design works by: 56 | 57 | - Subsetting from the checked-in [fontawesome](https://fontawesome.com/) glyphs; see `subset.sh` to add a glyph. 58 | - The resulting combined font is then subsetted, which considerably reduces resulting binary size 59 | - That subset is then base64 encoded and dropt on disk at `combined.woff2` 60 | - We then manually copy this into the URL injected by `share-button.js` (yes, yes, I know) 61 | - This is then loaded in a singleton way as the script loads. This might cause a global style recalc! Caveat emptor. 62 | - When instantiated, the web components reply on this font for their glyphs. 63 | 64 | The care taken to minimize dependencies for the component (no framework) and binary size means the whole shebang should come down the wire in less than 4KiB, making it suitable for inlining in aggressive setups. -------------------------------------------------------------------------------- /combined.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/combined.woff2 -------------------------------------------------------------------------------- /combined.woff2.base64: -------------------------------------------------------------------------------- 1 | d09GMgABAAAAAAasAAoAAAAAC3QAAAZiAwcFAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAXAqPCIwFATYCJAMgCxIABCAFgzQHIBsuCQgeg+PuYl6YDJb/ilvkESr/n3ez7kuAtgSrIhaqoWIwKkrHPKxWRhTtiNLSMZOv3TNiXp0bG4XAIFMaIboZndlQqjWq6UThIe7d+yAVjrnO07Us8EUBrmV5eSCJz6n778dm5TdEG1G8ETINSqWUfw8RTSKeRCsJtrXg3v06bbdQ16Lb3OsDdi1iiJ6jb8yvPgASekA9SAFqTUMBNPM5n6DnrxIDKYsDQ8PXAw2Kndrg6GXcx890sXIXQBcxAAgAqCFtBcADAAQWBEABCdoDaNDAAo/Lb+fCjxU4VvP1ubIy8Ln6herJ6pHq4eqmqr2cCgS2Rd4haBJzzPoTsEp8I/VrRWDAggXTzhAVm1SridPaIdOqtNWt2+tQA6JjWVsanagXSgulrDcv1flMvrhS586qt7I21T6YvC49bAgkELnJIy+OWwoisF0oS91Q93DOlm5werfFY1jBuIMqWfswhVLhtmdIjF/GKalE6yV2ISSwB8s5+XEcoWsjgchDbq2XK1kc+qVZbvkAJB00BFLXuaweJKcMI5zRC3ed0vtNGDbGowTFzjfVNre7LuPutNudHjb4pBLtM1iI1qfsTwwaD7U418VDKzMqqu2y78korR5KKxOGrTAQPQvuoFTX7qanG590Fsc9cVrEKtvqrqLJqWp70kJI2kLFyes4m7rB4qHQXiXcnowK40rrSup8r8FAgicflj4Ly6usMQa1FSkldj0ZqYIL2Zfp/byIQQKIyJMgl+8Uykz3l+E4r5YOPV2oc1NOzuFzIY7VjbzWrh/bAUre5Wbtg52X28tTlsUrgGtCWk+Cm9gkN7BGHuhPCmZUpHFd1wlEZTqG/NkBJYVCxxqJAqnrdCBfsoI2GFgcdwEKGWUXOevWEnm5W13ULmLcUnlxgodudyUwHMzKIufYy82lQll62NLtVhcircuQWJO/veX5pprfeYzrCBG56x5uerp6HC2Nhfs+RqulipT19nKIQmcqS4b/KZqhJa9gvkZcHG20ra5ziCup123sQRCJVrDJqVqYRdsb4nKJqRvKyJ3RjSrgtL1c7xdYcwOjt/9ftiSuqXtYXly7s8t9ogZMNKVMnHiin69QJ0ijLNbcnPQ3C7p3Ysrk5NJ/jy0cjn1lbQGieCIpCGg72+dwHD327yQ5ubXyzoLuG3tOrtUalSroC33tsXKlKIqqwiKlCGVRoUqEFdDNaXg46TY6t3XvNWEgrIkOj52BMNThQLT+PbTbcLAd6rvFOS7pSPxd0jBD6P3/rsePn8ZJOG+RyQCgmb6RrVHDzAaZ/akHmrq2KltihnfJkhWiXH282ahRMgat+ocFxR3XzNENWKOCu/Tf1t226r2TkuPFfo+xFeofwPMn6DsBgHLihAkTlfT9xfvhUe4Rm1UfMbLu5cWZptUASECDO1t5n6+TLOLi1bsOlln/6NRtuEzGtpyZQ2s3HpFPvt6v9ZBNPRTfnp0vMh45WKUyQfi/yV0+fQd2oKbmbpPXOkGm5bHgqFis5qG6y9ftf+rkjniM+vcbZO5dlRZqjCj1jt8a/ki5UBUWqiRQ/Fs8E/s1mv1FM+BTqv1+dRY03kZ08SJFFXXquP6e2KlhBxsvYUBqbgMAEEDRNhAAADHG6ip5jG2r6smVuw7AIWoPlcNxtz3uBhyq+ohPd4jfHe3Ro/aoPvAJoqoQKFRNQJ9In6pVGxcmPBZMlRjgbpfdtq24BsCu78/ef1f3eS7XVLZDfROchDm1PpBia5fepLp2PNN813w8NJqX1gRqTAY7kw5rEDIZZ0zbdnwqgbjtWG44bxtWUIB/eP6fJhEykYSRClModFWd8CKQ3/VcjZrnXK4IbSHyZSiMKlKc4lKiNL7JvgIXAAAEsBFS/P7+ZPVT1/4UyTEAgIci9zL/HE/lq3IXs5AughCJuAgB15eTcU+EN4OvmWAkiZwDGDrqI7CMqQ92VkQMBCcjviGt8iJQtYsi0BMWJIkC4ASWkFATp5CBCmOFLJoyc1aCCqUYzPGXksEppKIRLgqYxnhGMIzhTIRHYBB2eJriYmygsgFTGMKEbmtjGBIO8lTVnQ0ZzwDGMphZPB2lhzGJ0QzoN6dVz5gFBrOppoP5dAVy45nAiKpBnmrKWEUyueRRwzA6OpS/VR7hxvOTqy3XhL+RzZ4/qJuv+qjystwsDqoqlwgAAA== 2 | -------------------------------------------------------------------------------- /fontawesome/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/fa-brands-400.ttf -------------------------------------------------------------------------------- /fontawesome/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/fa-brands-400.woff2 -------------------------------------------------------------------------------- /fontawesome/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/fa-regular-400.ttf -------------------------------------------------------------------------------- /fontawesome/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/fa-regular-400.woff2 -------------------------------------------------------------------------------- /fontawesome/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/fa-solid-900.ttf -------------------------------------------------------------------------------- /fontawesome/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/fa-solid-900.woff2 -------------------------------------------------------------------------------- /fontawesome/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /fontawesome/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /fontawesome/merged.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slightlyoff/share-button/c62877cd1a5bd99d6dfda676fb349e3e94cf92c3/fontawesome/merged.ttf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@slightlyoff/share-button", 3 | "description": "A web component for sharing.", 4 | "author": "Alex Russell ", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:slightlyoff/share-button.git" 8 | }, 9 | "scripts": { 10 | "subset": "./subset.sh" 11 | }, 12 | "files": [ 13 | "share-button.js" 14 | ], 15 | "license": "MIT", 16 | "version": "0.4.1", 17 | "main": "share-button.js", 18 | "module": "share-button.js", 19 | "keywords": [ 20 | "web components", 21 | "web share" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /share-button.js: -------------------------------------------------------------------------------- 1 | // One-time global addition of fonts to the parent document. This prevents 2 | // repeated definitions as well as the node bloat of SVG. 3 | let srcUrl = `url(data:font/woff2;base64,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)`; 4 | if (!document.fonts.check("1rem share-button-combined")) { 5 | 6 | var font = new FontFace( 7 | "share-button-combined", 8 | srcUrl, 9 | { style: 'normal', weight: '400' } 10 | ); 11 | await font.load(); 12 | document.fonts.add(font); 13 | } 14 | 15 | let ua = navigator.userAgent; 16 | let styleEl = document.createElement("style"); 17 | styleEl.innerText = ` 18 | @font-face { 19 | font-family: "share-button-combined"; 20 | font-display: block; 21 | src: ${srcUrl} format("woff2"); 22 | } 23 | `; 24 | document.head.appendChild(styleEl); 25 | 26 | let _styleMap = new Map(); 27 | let addStyles = (doc, styles) => { 28 | let s = _styleMap.get(styles); 29 | if (!s) { 30 | try { 31 | s = { 32 | type: "CSS", 33 | value: new CSSStyleSheet() 34 | } 35 | s.value.replaceSync(styles); 36 | } catch(e) { 37 | s = { 38 | type: "sheet", 39 | value: styles 40 | }; 41 | } 42 | _styleMap.set(styles, s); 43 | } 44 | switch(s.type) { 45 | case "sheet": 46 | let sheet = doc.createElement("style"); 47 | sheet.textContent = s.value; 48 | doc.appendChild(sheet); 49 | break; 50 | case "CSS": 51 | doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, s.value]; 52 | break; 53 | }; 54 | } 55 | 56 | 57 | class ShareButton extends HTMLElement { 58 | 59 | static fontUrl = srcUrl; 60 | 61 | static styles = ` 62 | :host { 63 | --transition-duration: 0.3s; 64 | --hover-scale: 1.2; 65 | --color: currentColor; 66 | --hover-bg-color: rgba(0, 0, 0, 18%); 67 | } 68 | 69 | :host > button { 70 | all: unset; 71 | 72 | font-family: share-button-combined; 73 | color: var(--color); 74 | 75 | display: inline-block; 76 | padding: 0.5rem 0.75rem; 77 | border-radius: 0.2rem; 78 | cursor: pointer; 79 | will-change: transform; 80 | transition: all var(--transition-duration, 0.3s); 81 | } 82 | 83 | :host > button:focus { 84 | outline: 2px solid currentColor; 85 | outline-style: auto; 86 | } 87 | 88 | :host > button:hover { 89 | background: var(--hover-bg-color); 90 | transform: scale(var(--hover-scale)); 91 | } 92 | `; 93 | 94 | static template = (() => { 95 | // TODO: add an RSS button (icon ) 96 | document.body.insertAdjacentHTML("beforeend", ` 97 | `); 173 | return document.body.lastElementChild; 174 | })(); 175 | 176 | static get observedAttributes() { 177 | return [ 178 | "url", 179 | "title", 180 | "text", 181 | "image", 182 | ]; 183 | } 184 | 185 | constructor() { 186 | super(); 187 | let shadow = this.attachShadow({ mode: "open" }); 188 | this.url = ""; 189 | this.title = ""; 190 | this.text = ""; 191 | this.image = ""; 192 | } 193 | 194 | attributeChangedCallback(name, oldValue, newValue) { 195 | if(ShareButton.observedAttributes.includes(name) && 196 | oldValue !== newValue) { 197 | this[name] = newValue; 198 | } 199 | } 200 | 201 | #$(selector) { 202 | return Array.from(this.shadowRoot.querySelectorAll(selector)); 203 | } 204 | 205 | #$$(id) { 206 | return this.shadowRoot.getElementById(id); 207 | } 208 | 209 | #imageDataFile = null; 210 | 211 | async #getImageDataFile() { 212 | 213 | if (!this.image) { return; } 214 | 215 | try { 216 | if (!this.#imageDataFile) { 217 | let name = (new URL(this.image)).pathname.split("/").pop(); 218 | let response = await fetch(this.image); 219 | let imageData = await response.blob(); 220 | 221 | this.#imageDataFile = new File( 222 | [imageData], 223 | name, 224 | { 225 | type: imageData.type, 226 | lastModified: new Date().getTime() 227 | } 228 | ); 229 | } 230 | return this.#imageDataFile; 231 | } catch(e) { 232 | console.log(e); 233 | } 234 | } 235 | 236 | #success(evt) { 237 | // TODO: fire an event or trigger a CSS change to animate success 238 | // TODO: event logging for analytics 239 | } 240 | 241 | get _url() { 242 | return (this.url || window.location); 243 | } 244 | 245 | get _title() { 246 | return (this.title || document.title); 247 | } 248 | 249 | get _fullText() { 250 | return `"${this._title}${ 251 | (this.text ? (': '+this.text) : "" ) 252 | }"\n\n`; 253 | } 254 | 255 | // Web Share 256 | async share(evt) { 257 | let shareData = { 258 | url: this._url, 259 | title: this._title, 260 | text: this.text 261 | }; 262 | 263 | if (this.image) { 264 | let file = await this.#getImageDataFile(); 265 | shareData.files = [ file ]; 266 | } 267 | 268 | try { 269 | await navigator.share(shareData); 270 | this.dispatchEvent(new CustomEvent("share", { 271 | detail: shareData, 272 | bubbles: true, 273 | composed: true 274 | })); 275 | this.#success(evt); 276 | } catch(e) { 277 | console.error("share-button share failed:", e); 278 | } 279 | } 280 | 281 | // *sigh* 282 | async tweet(evt) { 283 | let url = new URL("/intent/tweet", "https://twitter.com"); 284 | url.searchParams.set("url", this._url); 285 | url.searchParams.set("text", this._fullText); 286 | window.open(url.toString(), "twitterShare", "popup,noopener"); 287 | this.#success(evt); 288 | } 289 | 290 | async copyLink(evt) { 291 | try { 292 | navigator.clipboard.writeText(`${this._fullText}${this._url}`); 293 | this.#success(evt); 294 | } catch(e) { 295 | console.error(e); 296 | } 297 | } 298 | 299 | // Mastodon 300 | async toot(evt) { 301 | let instance = this.#$$("instance").value; 302 | let url = new URL("/share", instance); 303 | url.searchParams.set("url", this._url); 304 | url.searchParams.set("text", this._fullText); 305 | window.open(url.toString(), "tootShare", "popup,noopener"); 306 | this.#success(evt); 307 | } 308 | 309 | openTootDialog(evt) { this.#$$("toot-prompt").showModal(); } 310 | 311 | closeTootDialog(evt) { this.#$$("toot-prompt").close(); } 312 | 313 | // LI sharing 314 | promote(evt) { 315 | // https://www.linkedin.com/sharing/share-offsite/?url={url} 316 | let url = new URL("/sharing/share-offsite", "https://www.linkedin.com"); 317 | // LI seems to support posting either URLs, *or* text, so we go w/ URL 318 | url.searchParams.set("url", this._url); 319 | // url.searchParams.set("text", this._fullText); 320 | window.open(url.toString(), "liShare", "popup,noopener"); 321 | this.#success(evt); 322 | } 323 | 324 | // Bsky 325 | skeet(evt) { 326 | // https://bsky.app/intent/compose?text=... 327 | let url = new URL("/intent/compose", "https://bsky.app"); 328 | url.searchParams.set("text", `${this._fullText} ${this._url}`); 329 | window.open(url.toString(), "skeetShare", "popup,noopener"); 330 | this.#success(evt); 331 | } 332 | 333 | connectedCallback() { 334 | this.wireElements(); 335 | } 336 | 337 | #wired = false; 338 | wireElements() { 339 | // Prevent memory leaks 340 | if (this.#wired) { return; } 341 | this.#wired = true; 342 | 343 | let sr = this.shadowRoot; 344 | let listen = (id, evt, method) => { 345 | let m = (typeof method == "string") ? 346 | this[method].bind(this) : 347 | method; 348 | this.#$$(id).addEventListener(evt, m); 349 | }; 350 | 351 | addStyles(sr, ShareButton.styles); 352 | 353 | sr.appendChild( 354 | ShareButton.template.content.cloneNode(true) 355 | ); 356 | 357 | if (navigator.share) { 358 | listen("share", "click", "share"); 359 | } else { 360 | this.#$$("share").style.display = "none"; 361 | } 362 | 363 | listen("tweet", "click", "tweet"); 364 | listen("toot", "click", "openTootDialog"); 365 | listen("copy", "click", "copyLink"); 366 | listen("toot-form", "submit", "toot"); 367 | listen("cancel", "click", "closeTootDialog"); 368 | listen("promote", "click", "promote"); 369 | listen("skeet", "click", "skeet"); 370 | } 371 | } 372 | customElements.define("share-button", ShareButton); 373 | 374 | export default ShareButton; -------------------------------------------------------------------------------- /share.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | 29 | 30 | 31 |

Default

32 | 36 | 37 | 38 |

Never Tweet

39 | 43 | 44 | 45 |

With Fallback Links

46 | 50 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /subset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MERGED="./fontawesome/merged.ttf" 4 | COMBINED="./combined.woff2" 5 | 6 | # TODO: to add/update an icon, you first have to clobber $MERGED. Automate. 7 | # if [ ! -f $MERGED ] 8 | # then 9 | # fonttools merge --output-file=$MERGED ./fontawesome/*.ttf 10 | # fi 11 | 12 | # Merge every time to avoid stale update bugs 13 | fonttools merge --output-file=$MERGED ./fontawesome/*.ttf 14 | 15 | # Share, Tweet, Link, Toot, LI Promote, Skeet, RSS 16 | pyftsubset $MERGED --unicodes="U+F14D,U+F099,U+F0C1,U+F4F6,U+F08C,U+E671,U+F09E" \ 17 | --flavor=woff2 --output-file=$COMBINED 18 | 19 | base64 $COMBINED \ 20 | | (readarray -t TXT; IFS=''; echo "${TXT[*]}") \ 21 | > "${COMBINED}.base64" --------------------------------------------------------------------------------