├── .editorconfig ├── .eslintrc ├── .gitignore ├── .stylelintrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── lerna.json ├── package.json ├── packages ├── accordion-list-element │ ├── README.md │ ├── package.json │ └── src │ │ ├── browser.js │ │ ├── example.html │ │ ├── index.css │ │ └── index.js ├── accordion-panel-element │ ├── README.md │ ├── package.json │ └── src │ │ ├── browser.js │ │ ├── example.html │ │ ├── index.css │ │ └── index.js ├── domset-svg │ ├── README.md │ ├── package.json │ └── src │ │ ├── example.html │ │ └── index.js ├── domset │ ├── README.md │ ├── package.json │ └── src │ │ ├── example.html │ │ └── index.js ├── intersection-image-element │ ├── README.md │ ├── package.json │ └── src │ │ ├── browser.js │ │ ├── example.html │ │ ├── index.css │ │ └── index.js ├── media-player-element │ ├── README.md │ ├── package.json │ └── src │ │ ├── browser.js │ │ ├── example.html │ │ ├── index.css │ │ ├── index.js │ │ └── lib │ │ ├── define-dom.js │ │ ├── define-events.js │ │ ├── define-svg.js │ │ ├── observed-attributes.js │ │ └── observed-properties.js ├── render-content-element │ ├── README.md │ ├── package.json │ └── src │ │ ├── browser.js │ │ ├── example.html │ │ └── index.js ├── social-media-player-element │ ├── README.md │ ├── package.json │ └── src │ │ ├── browser.js │ │ ├── example.html │ │ ├── index.css │ │ ├── index.js │ │ └── lib │ │ ├── define-youtube-events.js │ │ └── observed-attributes.js └── uid │ ├── README.md │ ├── package.json │ └── src │ ├── example.html │ └── index.js └── scripts ├── config ├── babel.config.js └── rollup.config.js ├── templates └── example.html └── watch.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{.*,*.{json,md,yml}}] 11 | indent_size = 2 12 | indent_style = space 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [{CONTRIBUTING.md,LICENSE.md}] 18 | indent_size = 3 19 | 20 | [{/packages/*/*.{js,map,mjs},**/node_modules/**}] 21 | charset = none 22 | end_of_line = none 23 | insert_final_newline = none 24 | trim_trailing_whitespace = none 25 | indent_style = none 26 | indent_size = none 27 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "impliedStrict": true, 12 | "sourceType": "module" 13 | }, 14 | "extends": "eslint:recommended" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | *.log* 4 | .* 5 | !.editorconfig 6 | !.eslintrc 7 | !.gitignore 8 | !.stylelintrc 9 | !.travis.yml 10 | /packages/*/*[.js,.map,.mjs] 11 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-dev" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/travis-lint 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 8 7 | 8 | install: 9 | - npm install --no-scripts 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Web Components 2 | 3 | You want to help? You rock! Now, take a moment to be sure your contributions 4 | make sense to everyone else. 5 | 6 | ## Reporting Issues 7 | 8 | Found a problem? Want a new feature? 9 | 10 | - See if your issue or idea has [already been reported]. 11 | - Provide a [reduced test case] or a [live example]. 12 | 13 | Remember, a bug is a _demonstrable problem_ caused by _our_ code. 14 | 15 | ## Submitting Pull Requests 16 | 17 | Pull requests are the greatest contributions, so be sure they are focused in 18 | scope and avoid unrelated commits. 19 | 20 | 1. To begin; [fork this project], clone your fork, and add our upstream. 21 | ```bash 22 | # Clone your fork of the repo into the current directory 23 | git clone git@github.com:YOUR_USER/web-components.git 24 | 25 | # Navigate to the newly cloned directory 26 | cd web-components 27 | 28 | # Assign the original repo to a remote called "upstream" 29 | git remote add upstream git@github.com:t7/web-components.git 30 | 31 | # Install the tools necessary for testing 32 | npm install 33 | ``` 34 | 35 | 2. Create a branch for your feature or fix: 36 | ```bash 37 | # Move into a new branch for your feature 38 | git checkout -b feature/thing 39 | ``` 40 | ```bash 41 | # Move into a new branch for your fix 42 | git checkout -b fix/something 43 | ``` 44 | 45 | 3. If your code follows our practices, then push your feature branch: 46 | ```bash 47 | # Test current code 48 | npm test 49 | ``` 50 | ```bash 51 | # Push the branch for your new feature 52 | git push origin feature/thing 53 | ``` 54 | ```bash 55 | # Or, push the branch for your update 56 | git push origin update/something 57 | ``` 58 | 59 | That’s it! Now [open a pull request] with a clear title and description. 60 | 61 | [already been reported]: https://github.com/t7/web-components/issues 62 | [fork this project]: https://github.com/t7/web-components/fork 63 | [live example]: https://codepen.io/pen 64 | [open a pull request]: https://help.github.com/articles/using-pull-requests/ 65 | [reduced test case]: https://css-tricks.com/reduced-test-cases/ 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # CC0 1.0 Universal 2 | 3 | ## Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an “owner”) of an original work of 8 | authorship and/or a database (each, a “Work”). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific works 12 | (“Commons”) that the public can reliably and without fear of later claims of 13 | infringement build upon, modify, incorporate in other works, reuse and 14 | redistribute as freely as possible in any form whatsoever and for any purposes, 15 | including without limitation commercial purposes. These owners may contribute 16 | to the Commons to promote the ideal of a free culture and the further 17 | production of creative, cultural and scientific works, or to gain reputation or 18 | greater distribution for their Work in part through the use and efforts of 19 | others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation of 22 | additional consideration or compensation, the person associating CC0 with a 23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and 25 | publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights (“Copyright and 31 | Related Rights”). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 1. the right to reproduce, adapt, distribute, perform, display, communicate, 34 | and translate a Work; 35 | 2. moral rights retained by the original author(s) and/or performer(s); 36 | 3. publicity and privacy rights pertaining to a person’s image or likeness 37 | depicted in a Work; 38 | 4. rights protecting against unfair competition in regards to a Work, 39 | subject to the limitations in paragraph 4(i), below; 40 | 5. rights protecting the extraction, dissemination, use and reuse of data in 41 | a Work; 42 | 6. database rights (such as those arising under Directive 96/9/EC of the 43 | European Parliament and of the Council of 11 March 1996 on the legal 44 | protection of databases, and under any national implementation thereof, 45 | including any amended or successor version of such directive); and 46 | 7. other similar, equivalent or corresponding rights throughout the world 47 | based on applicable law or treaty, and any national implementations 48 | thereof. 49 | 50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright 53 | and Related Rights and associated claims and causes of action, whether now 54 | known or unknown (including existing as well as future claims and causes of 55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 56 | duration provided by applicable law or treaty (including future time 57 | extensions), (iii) in any current or future medium and for any number of 58 | copies, and (iv) for any purpose whatsoever, including without limitation 59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer 60 | makes the Waiver for the benefit of each member of the public at large and 61 | to the detriment of Affirmer’s heirs and successors, fully intending that 62 | such Waiver shall not be subject to revocation, rescission, cancellation, 63 | termination, or any other legal or equitable action to disrupt the quiet 64 | enjoyment of the Work by the public as contemplated by Affirmer’s express 65 | Statement of Purpose. 66 | 67 | 3. Public License Fallback. Should any part of the Waiver for any reason be 68 | judged legally invalid or ineffective under applicable law, then the Waiver 69 | shall be preserved to the maximum extent permitted taking into account 70 | Affirmer’s express Statement of Purpose. In addition, to the extent the 71 | Waiver is so judged Affirmer hereby grants to each affected person a 72 | royalty-free, non transferable, non sublicensable, non exclusive, 73 | irrevocable and unconditional license to exercise Affirmer’s Copyright and 74 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 75 | maximum duration provided by applicable law or treaty (including future time 76 | extensions), (iii) in any current or future medium and for any number of 77 | copies, and (iv) for any purpose whatsoever, including without limitation 78 | commercial, advertising or promotional purposes (the “License”). The License 79 | shall be deemed effective as of the date CC0 was applied by Affirmer to the 80 | Work. Should any part of the License for any reason be judged legally 81 | invalid or ineffective under applicable law, such partial invalidity or 82 | ineffectiveness shall not invalidate the remainder of the License, and in 83 | such case Affirmer hereby affirms that he or she will not (i) exercise any 84 | of his or her remaining Copyright and Related Rights in the Work or (ii) 85 | assert any associated claims and causes of action with respect to the Work, 86 | in either case contrary to Affirmer’s express Statement of Purpose. 87 | 88 | 4. Limitations and Disclaimers. 89 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, 90 | surrendered, licensed or otherwise affected by this document. 91 | 2. Affirmer offers the Work as-is and makes no representations or warranties 92 | of any kind concerning the Work, express, implied, statutory or 93 | otherwise, including without limitation warranties of title, 94 | merchantability, fitness for a particular purpose, non infringement, or 95 | the absence of latent or other defects, accuracy, or the present or 96 | absence of errors, whether or not discoverable, all to the greatest 97 | extent permissible under applicable law. 98 | 3. Affirmer disclaims responsibility for clearing rights of other persons 99 | that may apply to the Work or any use thereof, including without 100 | limitation any person’s Copyright and Related Rights in the Work. 101 | Further, Affirmer disclaims responsibility for obtaining any necessary 102 | consents, permissions or other rights required for any use of the Work. 103 | 4. Affirmer understands and acknowledges that Creative Commons is not a 104 | party to this document and has no duty or obligation with respect to this 105 | CC0 or use of the Work. 106 | 107 | For more information, please see 108 | http://creativecommons.org/publicdomain/zero/1.0/. 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Components [][Web Components] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | This is a collection of [Web Components] developed at Genpact T7. 8 | 9 | ## Development 10 | 11 | To develop this project locally, you will need 12 | [Git v2 or higher](https://git-scm.com/downloads) and 13 | [Node v6 or higher](https://nodejs.org/en/download/current/) and comfortable 14 | access to a terminal or command prompt. 15 | 16 | To begin working on this project; clone this repository. 17 | 18 | ```bash 19 | git clone git@github.com:t7/web-components.git 20 | ``` 21 | 22 | After navigating to the cloned repository, install this project’s dependencies. 23 | 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | Once the dependencies are installed, you can run the project locally. 29 | 30 | ```bash 31 | npm start 32 | ``` 33 | 34 | Read [CONTRIBUTING.md](CONTRIBUTING.md) before contributing back to the project. 35 | 36 | [Web Components]: https://github.com/t7/web-components 37 | 38 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 39 | [cli-url]: https://travis-ci.org/t7/web-components 40 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 41 | [git-url]: https://github.com/t7/web-components/issues 42 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 43 | [gpr-url]: https://github.com/t7/web-components/pulls 44 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/web-components", 3 | "description": "A collection of Web Components developed at Genpact T7", 4 | "author": "TandemSeven ", 5 | "license": "CC0-1.0", 6 | "repository": "t7/web-components", 7 | "homepage": "https://github.com/t7/web-components#readme", 8 | "bugs": "https://github.com/t7/web-components/issues", 9 | "scripts": { 10 | "bootstrap": "lerna bootstrap", 11 | "docs": "jsdoc -c .jsdocrc -d .gh-pages", 12 | "postinstall": "git clone --single-branch --branch gh-pages https://github.com/t7/web-components.git .gh-pages", 13 | "prestart": "npm run bootstrap", 14 | "test": "npm run test:eclint && npm run test:eslint && npm run test:stylelint", 15 | "test:eclint": "eclint check", 16 | "test:eslint": "eslint packages/*/src/**.js --cache --ignore-path .gitignore", 17 | "test:stylelint": "stylelint packages/*/src/**.css --cache", 18 | "start": "concurrently --raw \"npx upsite .gh-pages -p 8080 -c empty\" \"lerna exec --parallel -- node ../../scripts/watch.js\"" 19 | }, 20 | "engines": { 21 | "node": ">=8.0.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.4.4", 25 | "@babel/plugin-proposal-class-properties": "^7.4.4", 26 | "@babel/preset-env": "^7.4.4", 27 | "babel-eslint": "^10.0.1", 28 | "babel-plugin-import-postcss": "^1.2.0", 29 | "concurrently": "^4.1.0", 30 | "cssnano": "^4.1.10", 31 | "eclint": "^2.8.1", 32 | "eslint": "^5.16.0", 33 | "fse": "^4.0.1", 34 | "gzip-size": "^5.1.0", 35 | "lerna": "^3.13.4", 36 | "postcss-preset-env": "^6.6.0", 37 | "rollup": "^1.10.1", 38 | "rollup-plugin-babel": "^4.3.2", 39 | "rollup-plugin-commonjs": "^9.3.4", 40 | "rollup-plugin-node-resolve": "^4.2.3", 41 | "rollup-plugin-terser": "^4.0.4", 42 | "stylelint": "^10.0.1", 43 | "stylelint-config-dev": "^4.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/accordion-list-element/README.md: -------------------------------------------------------------------------------- 1 | # Accordion List Element [][Accordion List Element] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [Accordion List Element] is a [Web Component] for containing a series of 8 | [Accordion Panel Elements] which can be controlled collectively. It will add up 9 | to 446 bytes to your project. 10 | 11 | **[Try it now](https://t7.github.io/web-components/accordion-list-element/)** 12 | 13 | ## Usage 14 | 15 | Add the Accordion List Element to your page. 16 | 17 | ```html 18 | 19 | 20 | 21 | 22 |

Content for the first accordion panel.

23 |
24 | 25 |

Content for the second accordion panel.

26 |
27 | 28 |

Content for the third accordion panel.

29 |
30 |
31 | ``` 32 | 33 | Alternatively, add the Accordion List Element to your project: 34 | 35 | ```sh 36 | npm install @t7/accordion-list-element @t7/accordion-panel-element 37 | ``` 38 | 39 | ```js 40 | import AccordionListElement from '@t7/accordion-list-element'; 41 | import AccordionPanelElement from '@t7/accordion-panel-element'; 42 | 43 | customElements.define('accordion-list', AccordionListElement); 44 | customElements.define('accordion-panel', AccordionPanelElement); 45 | ``` 46 | 47 | ```html 48 | 49 | 50 |

Content for the first accordion panel.

51 |
52 | 53 |

Content for the second accordion panel.

54 |
55 | 56 |

Content for the third accordion panel.

57 |
58 |
59 | ``` 60 | 61 | ## Attributes 62 | 63 | ### min 64 | 65 | The `min` attribute determines the minimum number of child panels that must 66 | always be open. 67 | 68 | ```html 69 | 70 | 71 | ``` 72 | 73 | ### max 74 | 75 | The `max` attribute determines the maximum number of child panels that may be 76 | open at a time. 77 | 78 | ```html 79 | 80 | 81 | ``` 82 | 83 | [Accordion List Element]: https://github.com/t7/web-components/tree/master/packages/accordion-list-element 84 | [Accordion Panel Elements]: https://github.com/t7/web-components/tree/master/packages/accordion-panel-element 85 | [Web Component]: https://github.com/t7/web-components 86 | 87 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 88 | [cli-url]: https://travis-ci.org/t7/web-components 89 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 90 | [git-url]: https://github.com/t7/web-components/issues 91 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 92 | [gpr-url]: https://github.com/t7/web-components/pulls 93 | -------------------------------------------------------------------------------- /packages/accordion-list-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/accordion-list-element", 3 | "version": "0.1.0", 4 | "description": "A Web Component for containing a series of Accordion Panel Elements which can be controlled collectively", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/accordion-list-element#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=accordion-list-element", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": { 22 | "@t7/domset": "0.1.0" 23 | }, 24 | "keywords": [ 25 | "component", 26 | "custom", 27 | "dom", 28 | "element", 29 | "web", 30 | "webcomponent" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/accordion-list-element/src/browser.js: -------------------------------------------------------------------------------- 1 | import AccordionListElement from './index.js'; 2 | 3 | customElements.define('accordion-list', AccordionListElement); 4 | 5 | export default AccordionListElement; 6 | -------------------------------------------------------------------------------- /packages/accordion-list-element/src/example.html: -------------------------------------------------------------------------------- 1 | 2 | 8 |

9 | Accordion List with a minimum of 1 panel open and a maximum 10 | of 2 panels opens. 11 |

12 | 13 | 14 |

15 | Content for the first accordion panel. 16 |

17 |
18 | 19 |

20 | Content for the second accordion panel. 21 |

22 |
23 | 24 |

25 | Content for the third accordion panel. 26 |

27 |
28 |
29 | -------------------------------------------------------------------------------- /packages/accordion-list-element/src/index.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /packages/accordion-list-element/src/index.js: -------------------------------------------------------------------------------- 1 | import $ from '@t7/domset'; 2 | import css from './index.css'; 3 | 4 | export default class AccordionListElement extends HTMLElement { 5 | constructor () { 6 | super(); 7 | 8 | $(this.attachShadow({ mode: 'open' }), null, 9 | $('style', null, css), 10 | $('slot') 11 | ); 12 | } 13 | 14 | get min () { 15 | return Number(this.getAttribute('min')) || 0; 16 | } 17 | 18 | get max () { 19 | return this.hasAttribute('max') && !isNaN(this.getAttribute('max')) ? Number(this.getAttribute('max')) : Infinity; 20 | } 21 | 22 | get panels () { 23 | return Array.prototype.filter.call(this.children, child => typeof child.showAccordion === 'function').sort((a, b) => a.lastOpened - b.lastOpened); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/accordion-panel-element/README.md: -------------------------------------------------------------------------------- 1 | # Accordion Panel Element [][Accordion Panel Element] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [Accordion Panel Element] is a [Web Component] for containing a collapsible 8 | panel with a label to toggle its visibility. It will add up to 1.04kB to your 9 | project. 10 | 11 | **[Try it now](https://t7.github.io/web-components/accordion-panel-element/)** 12 | 13 | ## Usage 14 | 15 | Add the Accordion Panel Element to your page. 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | Alternatively, add the Accordion Panel Element to your project: 22 | 23 | ```sh 24 | npm install @t7/accordion-panel-element 25 | ``` 26 | 27 | ```js 28 | import AccordionPanelElement from '@t7/accordion-panel-element'; 29 | 30 | customElements.define('accordion-panel', AccordionPanelElement); 31 | ``` 32 | 33 | ```html 34 | 35 |

Content for the first accordion panel.

36 |
37 | 38 |

Content for the second accordion panel.

39 |
40 | 41 |

Content for the third accordion panel.

42 |
43 | ``` 44 | 45 | ## Attributes 46 | 47 | ### label 48 | 49 | The `label` attribute determines the label used to toggle whether the 50 | Accordion Panel is open. 51 | 52 | ```html 53 | 54 | 55 | ``` 56 | 57 | ### open 58 | 59 | The `opens` attribute determines whether the Accordion Panel content is 60 | collapsed or visible. 61 | 62 | ```html 63 | 64 | 65 | ``` 66 | 67 | ```html 68 | 69 | 70 | ``` 71 | 72 | [Accordion Panel Element]: https://github.com/t7/web-components/tree/master/packages/accordion-panel-element 73 | [Web Component]: https://github.com/t7/web-components 74 | 75 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 76 | [cli-url]: https://travis-ci.org/t7/web-components 77 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 78 | [git-url]: https://github.com/t7/web-components/issues 79 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 80 | [gpr-url]: https://github.com/t7/web-components/pulls 81 | -------------------------------------------------------------------------------- /packages/accordion-panel-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/accordion-panel-element", 3 | "version": "0.1.0", 4 | "description": "A Web Component for containing a collapsible panel with a label to toggle its visibility", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/accordion-panel-element#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=accordion-panel-element", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": { 22 | "@t7/domset": "0.1.0" 23 | }, 24 | "keywords": [ 25 | "component", 26 | "custom", 27 | "dom", 28 | "element", 29 | "web", 30 | "webcomponent" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/accordion-panel-element/src/browser.js: -------------------------------------------------------------------------------- 1 | import AccordionPanelElement from './index.js'; 2 | 3 | customElements.define('accordion-panel', AccordionPanelElement); 4 | 5 | export default AccordionPanelElement; 6 | -------------------------------------------------------------------------------- /packages/accordion-panel-element/src/example.html: -------------------------------------------------------------------------------- 1 | 7 |

8 | 3 Panel Example 9 |

10 | 11 |

12 | Content for the first accordion panel. 13 |

14 |
15 | 16 |

17 | Content for the second accordion panel. 18 |

19 |
20 | 21 |

22 | Content for the third accordion panel. 23 |

24 |
25 | -------------------------------------------------------------------------------- /packages/accordion-panel-element/src/index.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | 5 | .control { 6 | appearance: none; 7 | background-color: transparent; 8 | border: 0; 9 | color: inherit; 10 | display: block; 11 | font: inherit; 12 | margin: 0; 13 | padding: 0; 14 | text-align: start; 15 | width: 100%; 16 | } 17 | 18 | .region { 19 | max-height: 100vh; 20 | } 21 | 22 | .region--hidden { 23 | max-height: 0; 24 | overflow: hidden; 25 | } 26 | -------------------------------------------------------------------------------- /packages/accordion-panel-element/src/index.js: -------------------------------------------------------------------------------- 1 | import $ from '@t7/domset'; 2 | import css from './index.css'; 3 | 4 | /** 5 | * @name AccordionPanelElement 6 | * @class 7 | * @extends HTMLElement 8 | * @classdesc Return a new Accordion Element. 9 | * @returns {AccordionPanelElement~Instance} 10 | */ 11 | /** 12 | * @typedef AccordionPanelElement~Instance 13 | * @property {Boolean} open - Indicates whether the {@link AccordionPanelElement} is open. 14 | * @property {Number} lastOpened - Indicates when the {@link AccordionPanelElement} was last opened by the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC. 15 | * @property {Function} showAccordion - Opens the {@link AccordionPanelElement}. 16 | * @property {Function} close - Closes the {@link AccordionPanelElement}. 17 | * @property {Function} label - Determines the label used to toggle whether the {@link AccordionPanelElement} is open. 18 | */ 19 | 20 | export default class AccordionPanelElement extends HTMLElement { 21 | constructor () { 22 | super(); 23 | 24 | const _ = mapForAccordionPanelElement.set(this, {}).get(this); 25 | 26 | $(this.attachShadow({ mode: 'open' }), null, 27 | $('style', null, css) 28 | ); 29 | 30 | _.controlElement = this.shadowRoot.appendChild($('h3', { class: 'heading' })).appendChild($('button', { class: 'control' }, '')); 31 | _.controlText = _.controlElement.lastChild; 32 | _.regionElement = this.shadowRoot.appendChild($('div', { class: 'region region--hidden', role: 'region', 'aria-hidden': true }, $('slot'))); 33 | 34 | this.lastOpened = null; 35 | 36 | _.controlElement.addEventListener('click', () => { 37 | this.open = !this.open; 38 | }); 39 | } 40 | 41 | attributeChangedCallback (name, oldValue, newValue) { 42 | const _ = mapForAccordionPanelElement.get(this); 43 | 44 | if (name === 'label' && oldValue !== newValue) { 45 | _.controlText.data = newValue; 46 | } else if (name === 'open') { 47 | const normalizedOldValue = oldValue === null ? null : ''; 48 | const normalizedNewValue = newValue === null ? null : ''; 49 | 50 | if (normalizedOldValue !== normalizedNewValue) { 51 | if (normalizedNewValue === null) { 52 | close.call(this); 53 | } else { 54 | open.call(this); 55 | } 56 | } 57 | } 58 | 59 | function close () { 60 | const _ = mapForAccordionPanelElement.get(this); 61 | 62 | const openElements = Object(Object(this.parentNode).panels).length ? this.parentNode.panels.filter(element => element.open) : []; 63 | const min = typeof Object(this.parentNode).min === 'number' ? this.parentNode.min : 0; 64 | 65 | if (openElements) { 66 | if (min > openElements.length) { 67 | return $(this, { open: true }); 68 | } 69 | } 70 | 71 | this.lastOpened = null; 72 | 73 | $(_.controlElement, { 'aria-disabled': false }); 74 | $(_.regionElement, { class: 'region region--hidden', 'aria-hidden': true }); 75 | 76 | this.dispatchEvent(new CustomEvent('close', { bubbles: true })); 77 | } 78 | 79 | function open () { 80 | const _ = mapForAccordionPanelElement.get(this); 81 | 82 | this.lastOpened = Date.now(); 83 | 84 | $(_.controlElement, { 'aria-disabled': true }); 85 | $(_.regionElement, { class: 'region', 'aria-hidden': false }); 86 | 87 | this.dispatchEvent(new CustomEvent('open', { bubbles: true })); 88 | 89 | const openElements = Object(Object(this.parentNode).panels).length ? this.parentNode.panels.filter(element => element.open) : []; 90 | const max = typeof Object(this.parentNode).max === 'number' ? this.parentNode.max : Infinity; 91 | 92 | if (openElements) { 93 | if (max < openElements.length) { 94 | const closeElement = openElements.shift(); 95 | 96 | $(closeElement, { open: false }); 97 | } 98 | } 99 | } 100 | } 101 | 102 | showAccordion () { 103 | this.open = true; 104 | } 105 | 106 | close () { 107 | this.open = false; 108 | } 109 | 110 | get label () { 111 | return this.getAttribute('label'); 112 | } 113 | 114 | set label (newValue) { 115 | if (newValue) { 116 | this.setAttribute('label', newValue); 117 | } else { 118 | this.removeAttribute('label'); 119 | } 120 | } 121 | 122 | get open () { 123 | return this.hasAttribute('open'); 124 | } 125 | 126 | set open (newValue) { 127 | if (newValue) { 128 | this.setAttribute('open', ''); 129 | } else { 130 | this.removeAttribute('open'); 131 | } 132 | } 133 | 134 | static get observedAttributes () { 135 | return ['label', 'open']; 136 | } 137 | } 138 | 139 | const mapForAccordionPanelElement = new WeakMap(); 140 | -------------------------------------------------------------------------------- /packages/domset-svg/README.md: -------------------------------------------------------------------------------- 1 | # Domset+SVG [][Domset+SVG] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [Domset+SVG] is a JavaScript function for creating DOM elements, including SVG 8 | elements. The function receives element, attribute, and child arguments and 9 | returns a DOM Element. It will add up to 345 bytes to your project. 10 | 11 | ```js 12 | domset(element, attributes, ...children); 13 | ``` 14 | 15 | **[Try it now](https://t7.github.io/web-components/domset-svg/)** 16 | 17 | --- 18 | 19 | When converting JSX to JS, [Domset+SVG] can be used to generate DOM Elements. 20 | 21 | ```jsx 22 |

Hello, World! This is generated content!

; 23 | 24 | /* becomes */ 25 | 26 | domset('h3', null, 27 | 'Hello, ', domset('strong', { title: 'Earthly Planet' }, 28 | 'World' 29 | ), '! This is generated content!' 30 | ); 31 | ``` 32 | 33 | ## Usage 34 | 35 | Add **Domset+SVG** to your page. 36 | 37 | ```html 38 | 39 | 40 | 54 | 55 | ``` 56 | 57 | Alternatively, add Domset+SVG to your project: 58 | 59 | ```sh 60 | npm install @t7/domset-svg 61 | ``` 62 | 63 | ```js 64 | import domset from '@t7/domset-svg'; 65 | 66 | // append

Hello, World!

67 | domset(document.body, null, 68 | domset('h3', null, 69 | 'Hello, ', domset('strong', { title: 'Earthly Planet' }, 70 | 'World' 71 | ), '!') 72 | ); 73 | 74 | // append 75 | domset(document.body, null, 76 | domset('svg', { viewBox: '0 0 32 32' }, 77 | domset('path', { d: 'M4 0l24 16L4 32' }) 78 | ) 79 | ); 80 | ``` 81 | 82 | ## Arguments 83 | 84 | ### id 85 | 86 | The first argument represents the Node being referenced or created. String 87 | arguments create new Elements using the string as the tag name. 88 | 89 | ```js 90 | // create

using the "h3" string 91 | domset('h3'); 92 | 93 | // create using the "svg" string 94 | domset('svg'); 95 | ``` 96 | 97 | ```js 98 | // use the created

99 | domset(document.createElement('h3')); 100 | 101 | // use the created 102 | domset(document.createElementNS('http://www.w3.org/2000/svg', 'svg')); 103 | ``` 104 | 105 | ### attributes 106 | 107 | The second argument represents the properties or attributes being assigned to 108 | the element. When a name exists on the element as a property then the property 109 | is assigned. Otherwise, the attribute is assigned. Attributes with a `null` 110 | value are removed from the element. 111 | 112 | ```js 113 | // create

using the "className" property 114 | domset('h3', { className: 'foo' }); 115 | ``` 116 | 117 | ```js 118 | // create

using the "class" attribute 119 | domset('h3', { class: 'foo' }); 120 | ``` 121 | 122 | ```js 123 | // create

with a click event using the "onclick" property 124 | domset('h3', { onclick(event) {} }); 125 | ``` 126 | 127 | ### children 128 | 129 | The third argument and all arguments afterward are children to be appended to 130 | the element. 131 | 132 | ```js 133 | // append "Hello World" as a text node to

134 | domset('h3', null, 'Hello World'); 135 | ``` 136 | 137 | ```js 138 | // append "Hello World" as 3 text nodes to

139 | domset('h3', null, 'Hello', ' ', 'World'); 140 | ``` 141 | 142 | ```js 143 | // append a new

to the fragment 144 | domset(document.createDocumentFragment(), null, domset('h3')); 145 | ``` 146 | 147 | ## Return 148 | 149 | Domset+SVG returns the element referenced or created by [element](#element). 150 | 151 | ```js 152 | // h3 is

153 | const h3 = domset('h3'); 154 | 155 | // h3ish3 is true 156 | const ish3h3 = h3 === domset(h3); 157 | ``` 158 | 159 | [Domset+SVG]: https://github.com/t7/web-components/tree/master/packages/domset-svg 160 | 161 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 162 | [cli-url]: https://travis-ci.org/t7/web-components 163 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 164 | [git-url]: https://github.com/t7/web-components/issues 165 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 166 | [gpr-url]: https://github.com/t7/web-components/pulls 167 | -------------------------------------------------------------------------------- /packages/domset-svg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/domset-svg", 3 | "version": "0.1.0", 4 | "description": "A 0.3kB JavaScript library for creating DOM+SVG elements", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/domset-svg#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=create-svg", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": {}, 22 | "keywords": [ 23 | "component", 24 | "custom", 25 | "dom", 26 | "element", 27 | "web", 28 | "webcomponent" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/domset-svg/src/example.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /packages/domset-svg/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @func domset 3 | * @desc Return a newly created DOM structure (with browser support for svgs). 4 | * @param {Node|String} [id] - The tag of the new element node being created (otherwise a new document fragment). 5 | * @param {Object} [props] - The properties or attributes being assigned to the node. 6 | * @param {...Node|String} [children] Additional nodes or text being appended to the node. 7 | * @return {HTMLElement|DocumentFragment} 8 | */ 9 | 10 | export default function domset (id, props) { 11 | const node = id == null 12 | // void ids becomes fragments 13 | ? document.createDocumentFragment() 14 | : id === Object(id) 15 | // objects are used as-is 16 | ? id 17 | : id in domset.ref 18 | // known nodes are cloned (preserving namespace, boosting performance) 19 | ? domset.ref[id].cloneNode() 20 | // new nodes are referenced then cloned 21 | : (domset.ref[id] = document.createElement(id)).cloneNode(); 22 | 23 | for (const name in props) { 24 | // conditionally set the node property (as a non-object until Firefox supports `{ viewBox: '0 0 100 100' }`) 25 | if (name in node && !(node[name] === Object(node[name]))) { 26 | node[name] = props[name]; 27 | } 28 | // conditionally remove the node attribute 29 | else if (props[name] === null && node.removeAttribute) { 30 | node.removeAttribute(name); 31 | } 32 | // conditionally set the node attribute 33 | else if (node.setAttribute) { 34 | node.setAttribute(name, props[name]); 35 | } 36 | } 37 | 38 | // conditionally assign children 39 | if (node.append) { 40 | node.append(...Array.prototype.slice.call(arguments, 2)); 41 | } 42 | 43 | return node; 44 | } 45 | 46 | // reference svg elements with the correct namespace 47 | domset.ref = ['circle', 'ellipse', 'defs', 'g', 'image', 'line', 'path', 'polygon', 'polyline', 'rect', 'svg', 'symbol', 'text', 'use'].reduce( 48 | (ref, id) => (ref[id] = document.createElementNS('http://www.w3.org/2000/svg', id), ref), 49 | {} 50 | ); 51 | -------------------------------------------------------------------------------- /packages/domset/README.md: -------------------------------------------------------------------------------- 1 | # Domset [][Domset] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [Domset] is a JavaScript function for creating DOM structures. It takes a tag 8 | name, properties or attributes, children, and returns a newly created DOM 9 | structure. It will add up to 195 bytes to your project. 10 | 11 | ```js 12 | domset(element, attributes, ...children); 13 | ``` 14 | 15 | **[Try it now](https://t7.github.io/web-components/domset/)** 16 | 17 | --- 18 | 19 | When converting JSX to JS, **Domset** can be used to generate DOM Elements. 20 | 21 | ```jsx 22 |

Hello, World! This is generated content!

; 23 | 24 | /* becomes */ 25 | 26 | domset('h3', null, 27 | 'Hello, ', domset('strong', { title: 'Earthly Planet' }, 28 | 'World' 29 | ), '! This is generated content!' 30 | ); 31 | ``` 32 | 33 | ## Usage 34 | 35 | Add **Domset** to your page. 36 | 37 | ```html 38 | 39 | 40 | 46 | 47 | ``` 48 | 49 | Alternatively, add Create to your project: 50 | 51 | ```sh 52 | npm install @t7/domset 53 | ``` 54 | 55 | ```js 56 | import domset from '@t7/domset'; 57 | 58 | // append

Hello, World!

59 | document.body.append( 60 | domset('h3', null, 'Hello, ', domset('strong', { title: 'Earthly Planet' }, 'World'), '!') 61 | ); 62 | ``` 63 | 64 | ## Arguments 65 | 66 | ### element 67 | 68 | The first argument represents the Element being referenced or created. String 69 | arguments create new Elements using the string as the tag name. 70 | 71 | ```js 72 | // create

using the "h3" string 73 | domset('h3'); 74 | ``` 75 | 76 | ```js 77 | // use the created

78 | domset(document.createElement('h3')); 79 | ``` 80 | 81 | ### attributes 82 | 83 | The second argument represents the properties or attributes being assigned to 84 | the element. When a name exists on the element as a property then the property 85 | is assigned. Otherwise, the attribute is assigned. Attributes with a `null` 86 | value are removed from the element. 87 | 88 | ```js 89 | // create

using the "className" property 90 | domset('h3', { className: 'foo' }); 91 | ``` 92 | 93 | ```js 94 | // create

using the "class" attribute 95 | domset('h3', { class: 'foo' }); 96 | ``` 97 | 98 | ```js 99 | // create

with a click event using the "onclick" property 100 | domset('h3', { onclick(event) {} }); 101 | ``` 102 | 103 | ### children 104 | 105 | The third argument and all arguments afterward are children to be appended to 106 | the element. 107 | 108 | ```js 109 | // append "Hello World" as a text node to

110 | domset('h3', null, 'Hello World'); 111 | ``` 112 | 113 | ```js 114 | // append "Hello World" as 3 text nodes to

115 | domset('h3', null, 'Hello', ' ', 'World'); 116 | ``` 117 | 118 | ```js 119 | // append a new

to the fragment 120 | domset(document.createDocumentFragment(), null, domset('h3')); 121 | ``` 122 | 123 | ## Return 124 | 125 | Create returns the element referenced or created by [element](#element). 126 | 127 | ```js 128 | // h3 is

129 | const h3 = domset('h3'); 130 | 131 | // h3ish3 is true 132 | const ish3h3 = h3 === domset(h3); 133 | ``` 134 | 135 | [Create]: https://github.com/t7/web-components/tree/master/packages/domset 136 | 137 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 138 | [cli-url]: https://travis-ci.org/t7/web-components 139 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 140 | [git-url]: https://github.com/t7/web-components/issues 141 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 142 | [gpr-url]: https://github.com/t7/web-components/pulls 143 | -------------------------------------------------------------------------------- /packages/domset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/domset", 3 | "version": "0.1.0", 4 | "description": "A 0.2kB JavaScript library for creating DOM structures", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/structure#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=structure", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": {}, 22 | "devDependencies": {}, 23 | "keywords": [ 24 | "component", 25 | "custom", 26 | "dom", 27 | "element", 28 | "web", 29 | "webcomponent" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/domset/src/example.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /packages/domset/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @func domset 3 | * @desc Return a DOM structure set with new properties, attributes, and children. 4 | * @param {String} [id] - The node being referenced or created. 5 | * @param {Object} [props] - The properties or attributes being assigned to the node. 6 | * @param {...Node|String} [children] Additional nodes or text being appended to the node. 7 | * @return {HTMLElement|DocumentFragment} 8 | */ 9 | 10 | export default function domset (id, props) { 11 | const node = id == null 12 | // void ids becomes fragments 13 | ? document.createDocumentFragment() 14 | : id === Object(id) 15 | // objects are used as-is 16 | ? id 17 | // new nodes are referenced then created 18 | : document.createElement(id); 19 | 20 | for (const name in props) { 21 | // conditionally set the node property 22 | if (name in node) { 23 | node[name] = props[name]; 24 | } 25 | // conditionally remove the node attribute 26 | else if (props[name] === null && node.removeAttribute) { 27 | node.removeAttribute(name); 28 | } 29 | // conditionally set the node attribute 30 | else if (node.setAttribute) { 31 | node.setAttribute(name, props[name]); 32 | } 33 | } 34 | 35 | // conditionally assign children 36 | if (node.append) { 37 | node.append(...Array.prototype.slice.call(arguments, 2)); 38 | } 39 | 40 | return node; 41 | } 42 | -------------------------------------------------------------------------------- /packages/intersection-image-element/README.md: -------------------------------------------------------------------------------- 1 | # Intersection Image Element [][Intersection Image Element] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [Intersection Image Element] is a [Web Component] for loading an image source 8 | after it is partially or fully visible to the viewport. It will add up to 780 9 | bytes to your project. 10 | 11 | **[Try it now](https://t7.github.io/web-components/intersection-image-element/)** 12 | 13 | ## Usage 14 | 15 | Add the Intersection Image Element to your page. 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | Alternatively, add the Intersection Image Element to your project: 22 | 23 | ```sh 24 | npm install @t7/intersection-image-element 25 | ``` 26 | 27 | ```js 28 | import IntersectionImageElement from '@t7/intersection-image-element'; 29 | 30 | customElements.define('intersection-image', IntersectionImageElement); 31 | ``` 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | ## Attributes 38 | 39 | ### src 40 | 41 | The `src` attribute determines the image URL being loaded when the element is 42 | visible. 43 | 44 | ```html 45 | 46 | 47 | ``` 48 | 49 | ### width 50 | 51 | The `width` attribute determines the intrinsic width of the image in pixels. A 52 | placeholder image using this length will preserve the aspect ratio of the 53 | element until the image URL is loaded. 54 | 55 | ```html 56 | 57 | 58 | ``` 59 | 60 | ### height 61 | 62 | The `height` attribute determines the intrinsic height of the image in pixels. 63 | 64 | ```html 65 | 66 | 67 | ``` 68 | 69 | [Intersection Image Element]: https://github.com/t7/web-components/tree/master/packages/intersection-image-element 70 | [Web Component]: https://github.com/t7/web-components 71 | 72 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 73 | [cli-url]: https://travis-ci.org/t7/web-components 74 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 75 | [git-url]: https://github.com/t7/web-components/issues 76 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 77 | [gpr-url]: https://github.com/t7/web-components/pulls 78 | -------------------------------------------------------------------------------- /packages/intersection-image-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/intersection-image-element", 3 | "version": "0.1.0", 4 | "description": "A Web Component for loading an image source after it is partially or fully visible to the viewport", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/intersection-image-element#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=intersection-image-element", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": {}, 22 | "keywords": [ 23 | "component", 24 | "custom", 25 | "dom", 26 | "element", 27 | "web", 28 | "webcomponent" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/intersection-image-element/src/browser.js: -------------------------------------------------------------------------------- 1 | import IntersectionImageElement from './index.js'; 2 | 3 | customElements.define('intersection-image', IntersectionImageElement); 4 | 5 | export default IntersectionImageElement; 6 | -------------------------------------------------------------------------------- /packages/intersection-image-element/src/example.html: -------------------------------------------------------------------------------- 1 | 14 |

Scroll down...

15 | 16 | -------------------------------------------------------------------------------- /packages/intersection-image-element/src/index.css: -------------------------------------------------------------------------------- 1 | :host, img { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /packages/intersection-image-element/src/index.js: -------------------------------------------------------------------------------- 1 | import css from './index.css'; 2 | 3 | export default class IntersectionImageElement extends HTMLElement { 4 | constructor () { 5 | super(); 6 | 7 | this.attachShadow({ mode: 'open' }); 8 | 9 | this.placeholderImage = this.shadowRoot.appendChild(document.createElement('img')); 10 | this.intersectionImage = document.createElement('img'); 11 | 12 | this.shadowRoot.appendChild(document.createElement('style')).append(css); 13 | 14 | const onComplete = event => { 15 | if (this.intersectionImage.naturalWidth) { 16 | this.shadowRoot.removeChild(this.placeholderImage); 17 | this.shadowRoot.appendChild(this.intersectionImage); 18 | } 19 | 20 | IntersectionImageElement.intersectionObserver.unobserve(this); 21 | 22 | this.dispatchEvent(new event.constructor(event.type, event)); 23 | }; 24 | 25 | this.intersectionImage.addEventListener('load', onComplete); 26 | 27 | this.intersectionImage.addEventListener('error', onComplete); 28 | } 29 | 30 | connectedCallback () { 31 | IntersectionImageElement.intersectionObserver.observe(this); 32 | } 33 | 34 | attributeChangedCallback (name, oldValue, newValue) { 35 | if (name === 'src' && this.complete) { 36 | this.shadowRoot.removeChild(this.intersectionImage); 37 | this.shadowRoot.appendChild(this.placeholderImage); 38 | 39 | IntersectionImageElement.intersectionObserver.observe(this); 40 | } else if (name === 'width' || name === 'height') { 41 | if (name === 'width') { 42 | this.placeholderImage.width = this.intersectionImage.width = newValue; 43 | } else { 44 | this.placeholderImage.height = this.intersectionImage.height = newValue; 45 | } 46 | 47 | this.placeholderImage.src = `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="${this.width}" height="${this.height}"%3E%3C/svg%3E`; 48 | } 49 | } 50 | 51 | get complete () { 52 | return Boolean(this.intersectionImage.src) && this.intersectionImage.complete; 53 | } 54 | 55 | get height () { 56 | return this.complete ? this.intersectionImage.height : this.placeholderImage.height; 57 | } 58 | 59 | set height (newValue) { 60 | if (newValue === null) { 61 | this.removeAttribute('height'); 62 | } else { 63 | this.setAttribute('height', newValue); 64 | } 65 | } 66 | 67 | get src () { 68 | return this.getAttribute('src'); 69 | } 70 | 71 | set src (newValue) { 72 | const value = newValue === null ? null : String(newValue); 73 | 74 | if (value === null) { 75 | this.removeAttribute('src'); 76 | } else if (value !== this.src) { 77 | this.setAttribute('src', value); 78 | } 79 | } 80 | 81 | get width () { 82 | return this.complete ? this.intersectionImage.width : this.placeholderImage.width; 83 | } 84 | 85 | set width (newValue) { 86 | if (newValue === null) { 87 | this.removeAttribute('width'); 88 | } else { 89 | this.setAttribute('width', newValue); 90 | } 91 | } 92 | 93 | static intersectionObserverCallback (entries) { 94 | for (const entry of entries) { 95 | if (entry.isIntersecting) { 96 | entry.target.intersectionImage.src = entry.target.getAttribute('src'); 97 | } 98 | } 99 | } 100 | 101 | static get observedAttributes () { 102 | return ['src', 'width', 'height']; 103 | } 104 | 105 | static intersectionObserverInit = {}; 106 | 107 | static intersectionObserver = new IntersectionObserver(IntersectionImageElement.intersectionObserverCallback, IntersectionImageElement.intersectionObserverInit); 108 | } 109 | -------------------------------------------------------------------------------- /packages/media-player-element/README.md: -------------------------------------------------------------------------------- 1 | # Media Player Element [][Media Player Element] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [Media Player Element] is a [Web Component] for creating tiny, responsive, 8 | international, accessible, easily customizable media players. It will add up to 9 | 3.83 kB to your project. 10 | 11 | **[Try it now](https://t7.github.io/web-components/media-player-element/)** 12 | 13 | --- 14 | 15 | [Media Player Element] can be controlled with any pointer or keyboard, whether 16 | it’s to play, pause, move across the timeline, mute, unmute, adjust the volume, 17 | enter or leave fullscreen, or download the source. 18 | 19 |

20 | Diagram of Media Player 21 |

22 | 23 | [Media Player Element] is designed for developers who want complete visual 24 | control over the component. It’s also for developers who want to hack at or 25 | extend the player without any fuss. The player itself does all the heavy 26 | lifting; semantic markup, accessibility management, language, fullscreen, text 27 | direction, providing pointer-agnostic scrubbable timelines, and lots of other 28 | cool sounding stuff. 29 | 30 |

31 | Diagram of Time Slider 32 |

33 | 34 | ## Usage 35 | 36 | Add the Media Player Element to your page. 37 | 38 | ```html 39 | 40 | ``` 41 | 42 | Alternatively, add the Media Player Element to your project: 43 | 44 | ```sh 45 | npm install @t7/media-player-element 46 | ``` 47 | 48 | ```js 49 | import MediaPlayerElement from '@t7/media-player-element'; 50 | 51 | customElements.define('media-player', MediaPlayerElement); 52 | ``` 53 | 54 | --- 55 | 56 | This component is still being developed. 57 | 58 | [Media Player Element]: https://github.com/t7/web-components/tree/master/packages/media-player-element 59 | [Web Component]: https://github.com/t7/web-components 60 | 61 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 62 | [cli-url]: https://travis-ci.org/t7/web-components 63 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 64 | [git-url]: https://github.com/t7/web-components/issues 65 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 66 | [gpr-url]: https://github.com/t7/web-components/pulls 67 | -------------------------------------------------------------------------------- /packages/media-player-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/media-player-element", 3 | "version": "0.1.0", 4 | "description": "A Web Component for creating tiny, responsive, international, accessible, easily customizable media players", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/media-player-element#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=media-player-element", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": { 22 | "@t7/domset": "0.1.0" 23 | }, 24 | "keywords": [ 25 | "component", 26 | "custom", 27 | "dom", 28 | "element", 29 | "web", 30 | "webcomponent" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/media-player-element/src/browser.js: -------------------------------------------------------------------------------- 1 | import MediaPlayerElement from './index.js'; 2 | 3 | customElements.define('media-player', MediaPlayerElement); 4 | 5 | export default MediaPlayerElement; 6 | -------------------------------------------------------------------------------- /packages/media-player-element/src/example.html: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | Diagram of Media Player 22 | 23 |
24 |
25 |
26 |

Audio with Poster

27 | 28 |
29 | -------------------------------------------------------------------------------- /packages/media-player-element/src/index.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --player-enter-color: color-mod(#9999ff alpha(25%)); 3 | --player-back-fullscreen-color: color-mod(#000000 alpha(75%)); 4 | --player-fill-fullscreen-color: #ffffff; 5 | --player-range-color: #cccccc; 6 | --player-meter-color: #0088dd; 7 | 8 | display: block; 9 | font: 300 100%/1.5 "Helvetica Neue", sans-serif; 10 | } 11 | 12 | .media-toolbar { 13 | align-items: center; 14 | cursor: default; 15 | direction: ltr; 16 | display: flex; 17 | flex-wrap: wrap; 18 | 19 | @nest :host(:fullscreen) & { 20 | background-color: var(--player-back-fullscreen-color); 21 | color: var(--player-fill-fullscreen-color); 22 | inset-block-end: 0; 23 | inset-inline: 0; 24 | opacity: .8; 25 | position: absolute; 26 | } 27 | } 28 | 29 | .media-hidden { 30 | display: none; 31 | } 32 | 33 | .media-frame { 34 | position: relative; 35 | 36 | & iframe { 37 | height: 100%; 38 | inset: 0; 39 | position: absolute; 40 | width: 100%; 41 | } 42 | } 43 | 44 | .media-media { 45 | background-color: #000000; 46 | display: block; 47 | margin-inline: auto; 48 | position: relative; 49 | width: 100%; 50 | 51 | &.audio { 52 | display: none; 53 | } 54 | 55 | @nest :host(:fullscreen) & { 56 | height: 100vh; 57 | width: 100vw; 58 | } 59 | } 60 | 61 | .media-control, .media-slider { 62 | background-color: transparent; 63 | border-style: none; 64 | color: inherit; 65 | font: inherit; 66 | margin: 0; 67 | overflow: visible; 68 | padding: 0; 69 | -webkit-tap-highlight-color: transparent; /* stylelint-disable-line property-no-vendor-prefix */ 70 | -webkit-touch-callout: none; /* stylelint-disable-line property-no-vendor-prefix */ 71 | -webkit-user-select: none; /* stylelint-disable-line property-no-vendor-prefix */ 72 | } 73 | 74 | .media-slider { 75 | height: 2.5em; 76 | padding: .625em .5em; 77 | 78 | &:focus { 79 | background-color: var(--player-enter-color); 80 | } 81 | } 82 | 83 | .media-time { 84 | flex-grow: 1; 85 | flex-shrink: 1; 86 | } 87 | 88 | .media-volume { 89 | flex-basis: 5em; 90 | } 91 | 92 | .media-range { 93 | background-color: var(--player-range-color); 94 | display: block; 95 | font-size: 75%; 96 | height: 1em; 97 | width: 100%; 98 | } 99 | 100 | .media-meter { 101 | background-color: var(--player-meter-color); 102 | display: block; 103 | height: 100%; 104 | overflow: hidden; 105 | width: 100%; 106 | } 107 | 108 | .media-text { 109 | font-size: 75%; 110 | padding-inline: .5em; 111 | white-space: nowrap; 112 | width: 2.5em; 113 | } 114 | 115 | .media-control { 116 | font-size: 75%; 117 | line-height: 1; 118 | padding: 1.16667em; 119 | text-decoration: none; 120 | 121 | &:matches(:hover, :focus) { 122 | background-color: var(--player-enter-color); 123 | } 124 | } 125 | 126 | .media-symbol { 127 | display: block; 128 | fill: currentColor; 129 | height: 1em; 130 | width: 1em; 131 | 132 | &[hidden="true"] { 133 | display: none; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/media-player-element/src/index.js: -------------------------------------------------------------------------------- 1 | import observedAttributes from './lib/observed-attributes.js'; 2 | import observedProperties from './lib/observed-properties.js'; 3 | import defineDOM from './lib/define-dom.js'; 4 | import defineEvents from './lib/define-events.js'; 5 | 6 | export default class MediaPlayer extends HTMLElement { 7 | constructor () { 8 | super(); 9 | 10 | const _ = this.__INTERNALS = { 11 | root: this.attachShadow({ mode: 'closed' }), 12 | self: this, 13 | super: MediaPlayer, 14 | mediaType: 'audio', 15 | timeDir: 'ltr', 16 | volumeDir: 'ltr' 17 | }; 18 | 19 | defineDOM(_); 20 | defineEvents(_); 21 | } 22 | 23 | attributeChangedCallback (name, oldValue, newValue) { 24 | if (oldValue !== newValue) { 25 | if (name === 'volume') { 26 | this.__INTERNALS.videoElement.volume = Number(newValue); 27 | } else if (observedAttributes.includes(name)) { 28 | this.__INTERNALS.videoElement.setAttribute(name, newValue); 29 | } 30 | } 31 | } 32 | 33 | static get observedAttributes () { 34 | return observedAttributes; 35 | } 36 | 37 | static lang = { 38 | currentTime: 'current time', 39 | download: 'download', 40 | enterFullscreen: 'enter full screen', 41 | leaveFullscreen: 'leave full screen', 42 | minutes: 'minutes', 43 | mute: 'mute', 44 | play: 'play', 45 | player: 'media player', 46 | pause: 'pause', 47 | remainingTime: 'remaining time', 48 | seconds: 'seconds', 49 | unmute: 'unmute', 50 | volume: 'volume' 51 | }; 52 | } 53 | 54 | observedProperties.forEach(name => { 55 | Object.defineProperty(MediaPlayer.prototype, name, { 56 | get () { 57 | return typeof this.__INTERNALS.videoElement[name] === 'function' ? this.__INTERNALS.videoElement[name].bind(this.__INTERNALS.videoElement) : this.__INTERNALS.videoElement[name]; 58 | }, 59 | set (newValue) { 60 | this.__INTERNALS.videoElement[name] = newValue; 61 | } 62 | }) 63 | }); 64 | -------------------------------------------------------------------------------- /packages/media-player-element/src/lib/define-dom.js: -------------------------------------------------------------------------------- 1 | import $ from '@t7/domset'; 2 | import $svg from './define-svg.js'; 3 | import css from '../index.css'; 4 | 5 | export default function (_) { 6 | // style 7 | _.styleElement = $('style', null, css); 8 | 9 | // media 10 | _.videoElement = $('video', { class: 'media-media audio' }); 11 | 12 | // play/pause toggle 13 | _.playSymbol = $svg('play'); 14 | _.pauseSymbol = $svg('pause'); 15 | _.playButton = $('button', { class: 'media-control media-play' }, _.playSymbol, _.pauseSymbol); 16 | 17 | // time slider 18 | _.timeMeter = $('span', { class: 'media-meter media-time-meter' }); 19 | _.timeRange = $('span', { class: 'media-range media-time-range' }, _.timeMeter); 20 | _.timeButton = $('button', { class: 'media-slider media-time', role: 'slider', 'aria-label': _.super.lang.currentTime, 'data-dir': _.timeDir }, _.timeRange); 21 | 22 | // current time text 23 | _.currentTimeSpan = $('span', { class: 'media-text media-current-time', role: 'timer', 'aria-label': _.super.lang.currentTime }, ''); 24 | _.currentTimeText = _.currentTimeSpan.lastChild; 25 | 26 | // remaining time text 27 | _.remainingTimeSpan = $('span', { class: 'media-text media-remaining-time', role: 'timer', 'aria-label': _.super.lang.remainingTime }, ''); 28 | _.remainingTimeText = _.remainingTimeSpan.lastChild; 29 | 30 | // mute/unmute toggle 31 | _.muteSymbol = $svg('mute'); 32 | _.unmuteSymbol = $svg('unmute'); 33 | _.muteButton = $('button', { class: 'media-control media-mute' }, _.muteSymbol, _.unmuteSymbol); 34 | 35 | // volume slider 36 | _.volumeMeter = $('span', { class: 'media-meter media-volume-meter' }); 37 | _.volumeRange = $('span', { class: 'media-range media-volume-range' }, _.volumeMeter); 38 | _.volumeButton = $('button', { class: 'media-slider media-volume', role: 'slider', 'aria-label': _.super.lang.volume, 'data-dir': _.volumeDir }, _.volumeRange); 39 | 40 | // download button 41 | _.downloadSymbol = $svg('download'); 42 | _.downloadButton = $('button', { class: 'media-control media-download', 'aria-label': _.super.lang.download }, _.downloadSymbol); 43 | 44 | // fullscreen button 45 | _.enterFullscreenSymbol = $svg('enterFullscreen'); 46 | _.leaveFullscreenSymbol = $svg('leaveFullscreen'); 47 | _.fullscreenButton = $('button', { class: 'media-control media-fullscreen' }, _.enterFullscreenSymbol, _.leaveFullscreenSymbol); 48 | 49 | // player toolbar 50 | _.toolbarElement = $('div', 51 | { class: 'media-toolbar', role: 'toolbar', 'aria-label': _.super.lang.player }, 52 | _.playButton, _.muteButton, _.volumeButton, _.currentTimeSpan, _.timeButton, _.remainingTimeSpan, _.downloadButton, _.fullscreenButton 53 | ); 54 | 55 | // player 56 | _.playerElement = $('div', { class: 'media-player', role: 'region', 'aria-label': _.super.lang.player }, _.styleElement, _.videoElement, _.toolbarElement); 57 | 58 | _.root.append(_.playerElement); 59 | } 60 | -------------------------------------------------------------------------------- /packages/media-player-element/src/lib/define-events.js: -------------------------------------------------------------------------------- 1 | import $ from '@t7/domset'; 2 | 3 | export default function (_) { 4 | // fullscreen api 5 | const fullscreenchange = 'onfullscreenchange' in _.self ? 'fullscreenchange' : 'onwebkitfullscreenchange' in _.self ? 'webkitfullscreenchange' : 'onMSFullscreenChange' in _.self ? 'MSFullscreenChange' : 'fullscreenchange'; 6 | const requestFullscreen = _.self.requestFullscreen || _.self.webkitRequestFullscreen || _.self.msRequestFullscreen; 7 | const fullscreenElement = () => _.self.ownerDocument.fullscreenElement || _.self.ownerDocument.webkitFullscreenElement || _.self.ownerDocument.msFullscreenElement; 8 | const exitFullscreen = () => (_.self.ownerDocument.exitFullscreen || _.self.ownerDocument.webkitCancelFullScreen || _.self.ownerDocument.msExitFullscreen).call(_.self.ownerDocument); 9 | 10 | // when the play control is clicked 11 | _.onPlayClick = () => { 12 | _.videoElement[_.videoElement.paused ? 'play' : 'pause'](); 13 | }; 14 | 15 | // when the time control 16 | _.onTimeClick = event => { 17 | // handle click if clicked without pointer 18 | if (!event.pointerType && !event.detail) { 19 | _.onPlayClick(event); 20 | } 21 | } 22 | 23 | // click from mute control 24 | _.onMuteClick = () => { 25 | _.videoElement.muted = !_.videoElement.muted; 26 | }; 27 | 28 | // click from volume control 29 | _.onVolumeClick = event => { 30 | // handle click if clicked without pointer 31 | if (!event.pointerType && !event.detail) { 32 | _.onMuteClick(event); 33 | } 34 | }; 35 | 36 | // click from download control 37 | _.onDownloadClick = () => { 38 | const a = _.root.appendChild($('a', { download: '', href: _.videoElement.src })); 39 | 40 | a.click(); 41 | 42 | _.root.removeChild(a); 43 | }; 44 | 45 | // click from fullscreen control 46 | _.onFullscreenClick = () => { 47 | if (requestFullscreen) { 48 | if (_.self === fullscreenElement()) { 49 | // exit fullscreen 50 | exitFullscreen(); 51 | } else { 52 | // enter fullscreen 53 | requestFullscreen.call(_.self); 54 | } 55 | } else if (_.videoElement.webkitSupportsFullscreen) { 56 | // iOS allows fullscreen of the video itself 57 | if (_.videoElement.webkitDisplayingFullscreen) { 58 | // exit ios fullscreen 59 | _.videoElement.webkitExitFullscreen(); 60 | } else { 61 | // enter ios fullscreen 62 | _.videoElement.webkitEnterFullscreen(); 63 | } 64 | 65 | _.onFullscreenChange(); 66 | } 67 | }; 68 | 69 | // keydown from play control or current time control 70 | _.onTimeKeydown = event => { 71 | const { keyCode, shiftKey } = event; 72 | 73 | // 37: LEFT, 38: UP, 39: RIGHT, 40: DOWN 74 | if (37 <= keyCode && 40 >= keyCode) { 75 | event.preventDefault(); 76 | 77 | const isLTR = /^(btt|ltr)$/.test(_.timeDir); 78 | const offset = 37 === keyCode || 39 === keyCode ? keyCode - 38 : keyCode - 39; 79 | 80 | _.videoElement.currentTime = Math.max(0, Math.min(_.duration, _.currentTime + offset * (isLTR ? 1 : -1) * (shiftKey ? 10 : 1))); 81 | 82 | _.onTimeChange(); 83 | } 84 | }; 85 | 86 | // keydown from mute control or volume control 87 | _.onVolumeKeydown = event => { 88 | const { keyCode, shiftKey } = event; 89 | 90 | // 37: LEFT, 38: UP, 39: RIGHT, 40: DOWN 91 | if (37 <= keyCode && 40 >= keyCode) { 92 | event.preventDefault(); 93 | 94 | const isLTR = /^(btt|ltr)$/.test(_.volumeDir); 95 | const offset = 37 === keyCode || 39 === keyCode ? keyCode - 38 : isLTR ? 39 - keyCode : keyCode - 39; 96 | 97 | _.videoElement.volume = Math.max(0, Math.min(1, _.videoElement.volume + offset * (isLTR ? 0.1 : -0.1) * (shiftKey ? 1 : 0.2))); 98 | } 99 | }; 100 | 101 | // when the play state changes 102 | _.onPlayChange = event => { 103 | if (_.paused !== _.videoElement.paused) { 104 | _.paused = _.videoElement.paused; 105 | 106 | $(_.playButton, { 'aria-label': _.paused ? _.super.lang.play : _.super.lang.pause }); 107 | $(_.playSymbol, { hidden: !_.paused }); 108 | $(_.pauseSymbol, { hidden: _.paused }); 109 | 110 | clearInterval(_.interval); 111 | 112 | if (!_.paused) { 113 | // listen for time changes every 30th of a second 114 | _.interval = setInterval(_.onTimeChange, 34); 115 | } 116 | 117 | // dispatch new "playchange" event 118 | _.self.dispatchEvent(new CustomEvent('playchange')); 119 | 120 | if (event) { 121 | _.self.dispatchEvent(cloneEvent(event)); 122 | } 123 | } 124 | }; 125 | 126 | // when the time changes 127 | _.onTimeChange = event => { 128 | if (_.currentTime !== _.videoElement.currentTime || _.duration !== _.videoElement.duration) { 129 | _.currentTime = _.videoElement.currentTime; 130 | _.duration = _.videoElement.duration || 0; 131 | 132 | const currentTimePercentage = _.currentTime / _.duration; 133 | const currentTimeCode = timeToTimecode(_.currentTime); 134 | const remainingTimeCode = timeToTimecode(_.duration - Math.floor(_.currentTime)); 135 | 136 | if (currentTimeCode !== _.currentTimeText.nodeValue) { 137 | _.currentTimeText.nodeValue = currentTimeCode; 138 | 139 | $(_.currentTimeSpan, { title: `${timeToAural(_.currentTime, _.super.lang.minutes, _.super.lang.seconds)}` }); 140 | } 141 | 142 | if (remainingTimeCode !== _.remainingTimeText.nodeValue) { 143 | _.remainingTimeText.nodeValue = remainingTimeCode; 144 | 145 | $(_.remainingTimeSpan, { title: `${timeToAural(_.duration - _.currentTime, _.super.lang.minutes, _.super.lang.seconds)}` }); 146 | } 147 | 148 | $(_.timeButton, { 'aria-valuenow': _.currentTime, 'aria-valuemin': 0, 'aria-valuemax': _.duration }); 149 | 150 | const dirIsInline = /^(ltr|rtl)$/i.test(_.timeDir); 151 | const axisProp = dirIsInline ? 'width' : 'height'; 152 | 153 | _.timeMeter.style[axisProp] = `${currentTimePercentage * 100}%`; 154 | 155 | const mediaType = _.videoElement.poster || _.videoElement.videoWidth ? 'video' : 'audio'; 156 | 157 | if (_.mediaType !== mediaType) { 158 | if (_.mediaType) { 159 | _.videoElement.classList.remove(_.mediaType); 160 | } 161 | 162 | _.mediaType = mediaType; 163 | 164 | _.videoElement.classList.add(mediaType); 165 | } 166 | 167 | if (event) { 168 | _.self.dispatchEvent(cloneEvent(event)); 169 | } 170 | 171 | // dispatch new "timechange" event 172 | _.self.dispatchEvent(new CustomEvent('timechange')); 173 | } 174 | }; 175 | 176 | // when media loads for the first time 177 | _.onLoadStart = event => { 178 | $(_.videoElement, { oncanplaythrough: _.onCanPlayStart }); 179 | 180 | _.onPlayChange(); 181 | _.onVolumeChange(); 182 | _.onFullscreenChange(); 183 | _.onTimeChange(); 184 | 185 | if (event) { 186 | _.self.dispatchEvent(cloneEvent(event)); 187 | } 188 | }; 189 | 190 | // when the media can play 191 | _.onCanPlayStart = event => { 192 | $(_.videoElement, { oncanplaythrough: _.onCanPlayStart }); 193 | 194 | // dispatch new "canplaystart" event 195 | _.self.dispatchEvent(new CustomEvent('canplaystart')); 196 | 197 | if (!_.videoElement.paused || _.videoElement.autoplay) { 198 | _.videoElement.play(); 199 | } 200 | 201 | if (event) { 202 | _.self.dispatchEvent(cloneEvent(event)); 203 | } 204 | }; 205 | 206 | // when the volume changes 207 | _.onVolumeChange = event => { 208 | const volumePercentage = _.videoElement.muted ? 0 : _.videoElement.volume; 209 | const isMuted = !volumePercentage; 210 | 211 | $(_.volumeButton, { 'aria-valuenow': volumePercentage, 'aria-valuemin': 0, 'aria-valuemax': 1 }); 212 | 213 | const dirIsInline = /^(ltr|rtl)$/i.test(_.volumeDir); 214 | const axisProp = dirIsInline ? 'width' : 'height'; 215 | 216 | _.volumeMeter.style[axisProp] = `${volumePercentage * 100}%`; 217 | 218 | $(_.muteButton, { 'aria-label': isMuted ? _.super.lang.unmute : _.super.lang.mute }); 219 | $(_.muteSymbol, { hidden: isMuted }); 220 | $(_.unmuteSymbol, { hidden: !isMuted }); 221 | 222 | if (event) { 223 | _.self.dispatchEvent(cloneEvent(event)); 224 | } 225 | }; 226 | 227 | // when the fullscreen state changes 228 | _.onFullscreenChange = () => { 229 | const isFullscreen = _.self === fullscreenElement(); 230 | 231 | $(_.fullscreenButton, { 'aria-label': isFullscreen ? _.super.lang.leaveFullscreen : _.super.lang.enterFullscreen }); 232 | $(_.enterFullscreenSymbol, { hidden: isFullscreen }); 233 | $(_.leaveFullscreenSymbol, { hidden: !isFullscreen }); 234 | }; 235 | 236 | $(_.videoElement, { 237 | oncanplaythrough: _.onCanPlayStart, 238 | onloadedmetadata: _.onTimeChange, 239 | onloadstart: _.onLoadStart, 240 | onpause: _.onPlayChange, 241 | onplay: _.onPlayChange, 242 | ontimeupdate: _.onTimeChange, 243 | onvolumechange: _.onVolumeChange 244 | }); 245 | 246 | $(_.playButton, { 247 | onclick: _.onPlayClick, 248 | onkeydown: _.onTimeKeydown 249 | }); 250 | 251 | $(_.timeButton, { 252 | onclick: _.onTimeClick, 253 | onkeydown: _.onTimeKeydown 254 | }); 255 | 256 | $(_.muteButton, { 257 | onclick: _.onMuteClick, 258 | onkeydown: _.onVolumeKeydown 259 | }); 260 | 261 | $(_.volumeButton, { 262 | onclick: _.onVolumeClick, 263 | onkeydown: _.onVolumeKeydown 264 | }); 265 | 266 | $(_.downloadButton, { 267 | onclick: _.onDownloadClick 268 | }) 269 | 270 | $(_.fullscreenButton, { 271 | onclick: _.onFullscreenClick 272 | }); 273 | 274 | // pointer events from time control 275 | onDrag(_.timeButton, _.timeRange, _.timeDir, percentage => { 276 | _.videoElement.currentTime = _.duration * Math.max(0, Math.min(1, percentage)); 277 | 278 | _.onTimeChange(); 279 | }); 280 | 281 | // pointer events from volume control 282 | onDrag(_.volumeButton, _.volumeRange, _.volumeDir, percentage => { 283 | _.videoElement.volume = Math.max(0, Math.min(1, percentage)); 284 | }); 285 | 286 | _.self.ownerDocument.addEventListener(fullscreenchange, _.onFullscreenChange); 287 | 288 | _.onLoadStart(); 289 | } 290 | 291 | /* Handle Drag Ranges 292 | /* ========================================================================== */ 293 | 294 | function onDrag(target, innerTarget, dir, listener) { // eslint-disable-line max-params 295 | const hasPointerEvent = undefined !== target.onpointerup; 296 | const hasTouchEvent = undefined !== target.ontouchstart; 297 | const pointerDown = hasPointerEvent ? 'pointerdown' : hasTouchEvent ? 'touchstart' : 'mousedown'; 298 | const pointerMove = hasPointerEvent ? 'pointermove' : hasTouchEvent ? 'touchmove' : 'mousemove'; 299 | const pointerUp = hasPointerEvent ? 'pointerup' : hasTouchEvent ? 'touchend' : 'mouseup'; 300 | 301 | // ... 302 | const dirIsInline = /^(ltr|rtl)$/i.test(dir); 303 | const dirIsStart = /^(ltr|ttb)$/i.test(dir); 304 | 305 | // ... 306 | const axisProp = dirIsInline ? 'clientX' : 'clientY'; 307 | 308 | let window, start, end; 309 | 310 | // on pointer down 311 | target.addEventListener(pointerDown, onpointerdown); 312 | 313 | function onpointerdown(event) { 314 | // window 315 | window = target.ownerDocument.defaultView; 316 | 317 | // client boundaries 318 | const rect = innerTarget.getBoundingClientRect(); 319 | 320 | // the container start and end coordinates 321 | start = dirIsInline ? rect.left : rect.top; 322 | end = dirIsInline ? rect.right : rect.bottom; 323 | 324 | onpointermove(event); 325 | 326 | window.addEventListener(pointerMove, onpointermove); 327 | window.addEventListener(pointerUp, onpointerup); 328 | } 329 | 330 | function onpointermove(event) { 331 | // prevent browser actions on this event 332 | event.preventDefault(); 333 | 334 | // the pointer coordinate 335 | const position = axisProp in event ? event[axisProp] : event.touches && event.touches[0] && event.touches[0][axisProp] || 0; 336 | 337 | // the percentage of the pointer along the container 338 | const percentage = (dirIsStart ? position - start : end - position) / (end - start); 339 | 340 | // call the listener with percentage 341 | listener(percentage); 342 | } 343 | 344 | function onpointerup() { 345 | window.removeEventListener(pointerMove, onpointermove); 346 | window.removeEventListener(pointerUp, onpointerup); 347 | } 348 | } 349 | 350 | /* Time To Timecode 351 | /* ====================================================================== */ 352 | 353 | function timeToTimecode(time) { 354 | return `${`0${Math.floor(time / 60)}`.slice(-2)}:${`0${Math.floor(time % 60)}`.slice(-2)}`; 355 | } 356 | 357 | /* Time To Aural 358 | /* ====================================================================== */ 359 | 360 | function timeToAural(time, langMinutes, langSeconds) { 361 | return `${Math.floor(time / 60)} ${langMinutes}, ${Math.floor(time % 60)} ${langSeconds}`; 362 | } 363 | 364 | /* Clone Event 365 | /* ====================================================================== */ 366 | 367 | function cloneEvent(event) { 368 | return Object.assign(new event.constructor(event.type, event), { 369 | relatedTarget: event.target 370 | }); 371 | } 372 | -------------------------------------------------------------------------------- /packages/media-player-element/src/lib/define-svg.js: -------------------------------------------------------------------------------- 1 | import $ from '@t7/domset'; 2 | 3 | export default function (type) { 4 | const svgElement = $(document.createElementNS(svgns, 'svg'), { class: `media-symbol media-${type}-symbol`, role: 'presentation' }, 5 | $(document.createElementNS(svgns, 'path'), { d: paths[type] }) 6 | ); 7 | 8 | svgElement.setAttribute('viewBox', '0 0 32 32'); 9 | 10 | return svgElement; 11 | } 12 | 13 | const svgns = 'http://www.w3.org/2000/svg'; 14 | const paths = { 15 | download: 'M20 2v12h4l-8 10-8-10h4V2M0 21h5v6h22v-6h5v11H0', 16 | enterFullscreen: 'M0 0h12L8 4l6 6-4 4-6-6-4 4M32 0H20l4 4-6 6 4 4 6-6 4 4M0 32V20l4 4 6-6 4 4-6 6 4 4m20 0V20l-4 4-6-6-4 4 6 6-4 4', 17 | leaveFullscreen: 'M0 4l4-4 6 6 4-4v12H2l4-4m26-6l-4-4-6 6-4-4v12h12l-4-4M0 28l4 4 6-6 4 4V18H2l4 4m26 6l-4 4-6-6-4 4V18h12l-4 4', 18 | mute: 'M0 11h6l10-8.5v27L6 21H0M22 6l10 10-10 10', 19 | pause: 'M4 0h9v32H4M28 0h-9v32h9', 20 | play: 'M4 0l24 16L4 32', 21 | unmute: 'M0 11h6l10-8.5v27L6 21H0' 22 | }; 23 | -------------------------------------------------------------------------------- /packages/media-player-element/src/lib/observed-attributes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'autoplay', 3 | 'height', 4 | 'loop', 5 | 'poster', 6 | 'src', 7 | 'volume', 8 | 'width' 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/media-player-element/src/lib/observed-properties.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'autoplay', 3 | 'height', 4 | 'loop', 5 | 'play', 6 | 'playbackRate', 7 | 'poster', 8 | 'src', 9 | 'volume', 10 | 'width' 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/render-content-element/README.md: -------------------------------------------------------------------------------- 1 | # Render Content Element [][Render Content Element] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [Render Content Element] is a [Web Component] for making content accessible to 8 | the DOM when a condition is met. It will add up to 328 bytes to your project. 9 | 10 | **[Try it now](https://t7.github.io/web-components/render-content-element/)** 11 | 12 | ## Usage 13 | 14 | Add the Render Content Element to your page. 15 | 16 | ```html 17 | 18 | 19 | 20 |

21 | This text should be accessible to the user. 22 |

23 |
24 | 25 |

26 | This text should not be accessible to the user. 27 |

28 |
29 | 30 | ``` 31 | 32 | Alternatively, add the Render Content Element to your project: 33 | 34 | ```sh 35 | npm install @t7/render-content-element 36 | ``` 37 | 38 | ```js 39 | import RenderContentElement from '@t7/render-content-element'; 40 | 41 | customElements.define('render-content', RenderContentElement); 42 | ``` 43 | 44 | ```html 45 | 46 |

47 | This text should be accessible to the user. 48 |

49 |
50 | 51 |

52 | This text should not be accessible to the user. 53 |

54 |
55 | ``` 56 | 57 | ## Attributes 58 | 59 | ### when 60 | 61 | The `when` attribute determines whether or not the contents of the element are 62 | accessible. 63 | 64 | ```html 65 | 66 | 67 | ``` 68 | 69 | ```html 70 | 71 | 72 | ``` 73 | 74 | [Render Content Element]: https://github.com/t7/web-components/tree/master/packages/render-content-element 75 | [Web Component]: https://github.com/t7/web-components 76 | 77 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 78 | [cli-url]: https://travis-ci.org/t7/web-components 79 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 80 | [git-url]: https://github.com/t7/web-components/issues 81 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 82 | [gpr-url]: https://github.com/t7/web-components/pulls 83 | -------------------------------------------------------------------------------- /packages/render-content-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/render-content-element", 3 | "version": "0.1.0", 4 | "description": "A Web Component for displaying content when a condition is met", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/render-content-element#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=render-content-element", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": {}, 22 | "keywords": [ 23 | "component", 24 | "custom", 25 | "dom", 26 | "element", 27 | "web", 28 | "webcomponent" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/render-content-element/src/browser.js: -------------------------------------------------------------------------------- 1 | import RenderContentElement from './index.js'; 2 | 3 | customElements.define('render-content', RenderContentElement); 4 | 5 | export default RenderContentElement; 6 | -------------------------------------------------------------------------------- /packages/render-content-element/src/example.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | This text should be accessible to the user. 4 |

5 |
6 | 7 |

8 | This text should not be accessible to the user. 9 |

10 |
11 | -------------------------------------------------------------------------------- /packages/render-content-element/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name RenderContentElement 3 | * @class 4 | * @extends HTMLElement 5 | * @classdesc Return a new Render Content Element. 6 | * @returns {RenderContentElement~Instance} 7 | */ 8 | /** 9 | * @typedef RenderContentElement~Instance 10 | * @property {Boolean} when - Indicates whether the {@link RenderContentElement} is accessible. 11 | */ 12 | 13 | export default class RenderContentElement extends HTMLElement { 14 | constructor () { 15 | super(); 16 | 17 | this.attachShadow({ mode: 'open' }); 18 | 19 | this.slotElement = document.createElement('slot'); 20 | } 21 | 22 | attributeChangedCallback (name, oldValue, newValue) { 23 | if (name === 'when') { 24 | const shouldBeShown = newValue !== null; 25 | 26 | if (shouldBeShown) { 27 | if (!this.slotElement.parentNode) { 28 | this.shadowRoot.appendChild(this.slotElement); 29 | } 30 | } else { 31 | if (this.slotElement.parentNode) { 32 | this.shadowRoot.removeChild(this.slotElement); 33 | } 34 | } 35 | } 36 | } 37 | 38 | get when () { 39 | return this.hasAttribute('when'); 40 | } 41 | 42 | set when (value) { 43 | if (value) { 44 | this.setAttribute('when', ''); 45 | } else { 46 | this.removeAttribute('when'); 47 | } 48 | } 49 | 50 | static get observedAttributes () { 51 | return ['when']; 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /packages/social-media-player-element/README.md: -------------------------------------------------------------------------------- 1 | # Social Media Player Element [][Social Media Player Element] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [Social Media Player Element] is a [Web Component] for creating tiny, 8 | responsive, international, accessible, easily customizable media players that 9 | also support YouTube video IDs. It will add up to 4.72 kB to your project. 10 | 11 | **[Try it now](https://t7.github.io/web-components/social-media-player-element/)** 12 | 13 | --- 14 | 15 | [Social Media Player Element] can be controlled with any pointer or keyboard, 16 | whether it’s to play, pause, move across the timeline, mute, unmute, adjust the 17 | volume, enter or leave fullscreen, or download the source. 18 | 19 |

20 | Diagram of Media Player 21 |

22 | 23 | [Social Media Player Element] is designed for developers who want complete 24 | visual control over the component. It’s also for developers who want to hack at 25 | or extend the player without any fuss. It is itself an extension of the 26 | [Media Player Element]. The player itself does all the heavy lifting; semantic 27 | markup, accessibility management, language, fullscreen, text direction, 28 | providing pointer-agnostic scrubbable timelines, and lots of other cool 29 | sounding stuff. 30 | 31 |

32 | Diagram of Time Slider 33 |

34 | 35 | ## Usage 36 | 37 | Add the Social Media Player Element to your page. 38 | 39 | ```html 40 | 41 | ``` 42 | 43 | Alternatively, add the Social Media Player Element to your project: 44 | 45 | ```sh 46 | npm install @t7/social-media-player-element 47 | ``` 48 | 49 | ```js 50 | import MediaPlayerElement from '@t7/social-media-player-element'; 51 | 52 | customElements.define('media-player', MediaPlayerElement); 53 | ``` 54 | 55 | --- 56 | 57 | This component is still being developed. 58 | 59 | [Media Player Element]: https://github.com/t7/web-components/tree/master/packages/media-player-element 60 | [Social Media Player Element]: https://github.com/t7/web-components/tree/master/packages/social-media-player-element 61 | [Web Component]: https://github.com/t7/web-components 62 | 63 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 64 | [cli-url]: https://travis-ci.org/t7/web-components 65 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 66 | [git-url]: https://github.com/t7/web-components/issues 67 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 68 | [gpr-url]: https://github.com/t7/web-components/pulls 69 | -------------------------------------------------------------------------------- /packages/social-media-player-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/social-media-player-element", 3 | "version": "0.1.0", 4 | "description": "A Web Component for creating tiny, responsive, international, accessible, easily customizable media players that also support YouTube video IDs", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/social-media-player-element#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=social-media-player-element", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": { 22 | "@t7/media-player-element": "0.1.0", 23 | "@t7/domset": "0.1.0", 24 | "@t7/uid": "0.1.0" 25 | }, 26 | "keywords": [ 27 | "component", 28 | "custom", 29 | "dom", 30 | "element", 31 | "web", 32 | "webcomponent" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/social-media-player-element/src/browser.js: -------------------------------------------------------------------------------- 1 | import SocialMediaPlayerElement from './index.js'; 2 | 3 | customElements.define('social-media-player', SocialMediaPlayerElement); 4 | 5 | export default SocialMediaPlayerElement; 6 | -------------------------------------------------------------------------------- /packages/social-media-player-element/src/example.html: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | Diagram of Media Player 22 | 23 |
24 |
25 |
26 |

27 | This page features Ten Thousand Years by Goddamn Electric Bill, and You Should Know Better by St. Lucia. 28 |

29 |
30 | -------------------------------------------------------------------------------- /packages/social-media-player-element/src/index.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --player-enter-color: color-mod(#9999ff alpha(25%)); 3 | --player-back-fullscreen-color: color-mod(#000000 alpha(75%)); 4 | --player-fill-fullscreen-color: #ffffff; 5 | --player-range-color: #cccccc; 6 | --player-meter-color: #0088dd; 7 | 8 | display: block; 9 | font: 300 100%/1.5 "Helvetica Neue", sans-serif; 10 | } 11 | 12 | .media-toolbar { 13 | align-items: center; 14 | cursor: default; 15 | direction: ltr; 16 | display: flex; 17 | flex-wrap: wrap; 18 | 19 | @nest :host(:fullscreen) & { 20 | background-color: var(--player-back-fullscreen-color); 21 | color: var(--player-fill-fullscreen-color); 22 | inset-block-end: 0; 23 | inset-inline: 0; 24 | opacity: .8; 25 | position: absolute; 26 | } 27 | } 28 | 29 | .media-hidden { 30 | display: none; 31 | } 32 | 33 | .media-frame { 34 | position: relative; 35 | 36 | & iframe { 37 | height: 100%; 38 | inset: 0; 39 | position: absolute; 40 | width: 100%; 41 | } 42 | } 43 | 44 | .media-media { 45 | background-color: #000000; 46 | display: block; 47 | margin-inline: auto; 48 | position: relative; 49 | width: 100%; 50 | 51 | &.audio { 52 | display: none; 53 | } 54 | 55 | @nest :host(:fullscreen) & { 56 | height: 100vh; 57 | width: 100vw; 58 | } 59 | } 60 | 61 | .media-control, .media-slider { 62 | background-color: transparent; 63 | border-style: none; 64 | color: inherit; 65 | font: inherit; 66 | margin: 0; 67 | overflow: visible; 68 | padding: 0; 69 | -webkit-tap-highlight-color: transparent; /* stylelint-disable-line property-no-vendor-prefix */ 70 | -webkit-touch-callout: none; /* stylelint-disable-line property-no-vendor-prefix */ 71 | -webkit-user-select: none; /* stylelint-disable-line property-no-vendor-prefix */ 72 | } 73 | 74 | .media-slider { 75 | height: 2.5em; 76 | padding: .625em .5em; 77 | 78 | &:focus { 79 | background-color: var(--player-enter-color); 80 | } 81 | } 82 | 83 | .media-time { 84 | flex-grow: 1; 85 | flex-shrink: 1; 86 | } 87 | 88 | .media-volume { 89 | flex-basis: 5em; 90 | } 91 | 92 | .media-range { 93 | background-color: var(--player-range-color); 94 | display: block; 95 | font-size: 75%; 96 | height: 1em; 97 | width: 100%; 98 | } 99 | 100 | .media-meter { 101 | background-color: var(--player-meter-color); 102 | display: block; 103 | height: 100%; 104 | overflow: hidden; 105 | width: 100%; 106 | } 107 | 108 | .media-text { 109 | font-size: 75%; 110 | padding-inline: .5em; 111 | white-space: nowrap; 112 | width: 2.5em; 113 | } 114 | 115 | .media-control { 116 | font-size: 75%; 117 | line-height: 1; 118 | padding: 1.16667em; 119 | text-decoration: none; 120 | 121 | &:matches(:hover, :focus) { 122 | background-color: var(--player-enter-color); 123 | } 124 | } 125 | 126 | .media-symbol { 127 | display: block; 128 | fill: currentColor; 129 | height: 1em; 130 | width: 1em; 131 | 132 | &[hidden="true"] { 133 | display: none; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/social-media-player-element/src/index.js: -------------------------------------------------------------------------------- 1 | import MediaPlayer from '@t7/media-player-element'; 2 | import observedAttributes from './lib/observed-attributes.js'; 3 | import defineYoutubeEvents from './lib/define-youtube-events'; 4 | 5 | export default class SocialMediaPlayer extends MediaPlayer { 6 | constructor () { 7 | super(); 8 | 9 | defineYoutubeEvents(this.__INTERNALS); 10 | } 11 | 12 | attributeChangedCallback (name, oldValue, newValue) { 13 | if (oldValue !== newValue) { 14 | if (name === 'src') { 15 | if (/^[A-Za-z0-9_-]{11}$/.test(newValue)) { 16 | return this.__INTERNALS.onYoutube(newValue); 17 | } else if (this.__INTERNALS.youtubeAPIState) { 18 | // this.__INTERNALS.onYoutubeDestroy(newValue); 19 | } 20 | 21 | this.__INTERNALS.videoElement.setAttribute(name, newValue); 22 | } else if (name === 'volume') { 23 | this.__INTERNALS.videoElement.volume = Number(newValue); 24 | } else if (observedAttributes.includes(name)) { 25 | this.__INTERNALS.videoElement.setAttribute(name, newValue); 26 | } 27 | } 28 | } 29 | 30 | static get observedAttributes () { 31 | return observedAttributes; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/social-media-player-element/src/lib/define-youtube-events.js: -------------------------------------------------------------------------------- 1 | import $ from '@t7/domset'; 2 | import uid from '@t7/uid'; 3 | 4 | export default function (_) { 5 | _.onYoutube = id => { 6 | _.youtubeID = uid(21); 7 | 8 | _.youtubePoster = $('img', { class: 'media-media video', src: `https://img.youtube.com/vi/${id}/maxresdefault.jpg`, onload: _.youtubeOnPosterLoad }); 9 | 10 | _.youtubeIframe = $('iframe', { 11 | frameBorder: 0, 12 | allowfullscreen: 1, 13 | allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', 14 | src: `https://www.youtube.com/embed/${id}?controls=0&disablekb=1&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&rel=0&showinfo=0&version=3` 15 | }); 16 | 17 | _.youtubeContainer = $('div', { class: 'media-frame' }, _.youtubePoster, _.youtubeIframe); 18 | 19 | _.youtubeAPI = json => { 20 | const message = JSON.stringify(Object.assign({ id: _.youtubeID }, json)); 21 | 22 | if (_.youtubeWindow) { 23 | _.youtubeWindow.postMessage(message, '*'); 24 | } else { 25 | setTimeout(() => { 26 | _.youtubeAPI(json); 27 | }, 200); 28 | } 29 | }; 30 | 31 | _.youtubeAPICommand = (func, args) => _.youtubeAPI({ 32 | event: 'command', 33 | func, 34 | args: args || [] 35 | }); 36 | 37 | _.youtubeAPIState = {}; 38 | 39 | _.youtubeListener = () => { 40 | _.onDownloadClick = () => { 41 | window.location = `https://www.youtube.com/watch?v=${id}`; 42 | }; 43 | 44 | $(_.downloadButton, { onclick: _.onDownloadClick }); 45 | 46 | window.addEventListener('message', event => { 47 | if (event.origin === 'https://www.youtube.com') { 48 | const data = JSON.parse(event.data); 49 | 50 | if (data.info && data.id === _.youtubeID) { 51 | Object.assign(_.youtubeAPIState, data.info); 52 | 53 | if (data.info.duration) { 54 | _.onTimeChange(); 55 | } 56 | 57 | if (data.info.muted || data.info.volume) { 58 | _.onVolumeChange(); 59 | } 60 | 61 | if (data.info.playerState) { 62 | _.onPlayChange(); 63 | } 64 | } 65 | } 66 | }); 67 | 68 | _.youtubeWindow = _.youtubeIframe.contentWindow; 69 | 70 | _.youtubeAPI({ 71 | event: 'listening' 72 | }); 73 | }; 74 | 75 | _.youtubeIframe.addEventListener('load', _.youtubeListener); 76 | 77 | Object.defineProperties(_.videoElement, { 78 | audio: { 79 | get () { 80 | return _.youtubePoster.classList.contains('audio'); 81 | }, 82 | set (newValue) { 83 | _.youtubePoster.classList.toggle('audio', !newValue); 84 | _.youtubePoster.classList.toggle('video', newValue); 85 | } 86 | }, 87 | currentTime: { 88 | get () { 89 | return _.youtubeAPIState.currentTime; 90 | }, 91 | set (newValue) { 92 | _.youtubeAPICommand('seekTo', [Number(newValue) || 0]); 93 | } 94 | }, 95 | duration: { 96 | get () { 97 | return _.youtubeAPIState.duration; 98 | } 99 | }, 100 | muted: { 101 | get () { 102 | return _.youtubeAPIState.muted; 103 | }, 104 | set (newValue) { 105 | _.youtubeAPICommand(newValue ? 'mute' : 'unMute'); 106 | } 107 | }, 108 | paused: { 109 | get () { 110 | return _.youtubeAPIState.playerState !== 1; 111 | }, 112 | set (newValue) { 113 | _.youtubeAPICommand(newValue ? 'pauseVideo' : 'playVideo'); 114 | } 115 | }, 116 | pause: { 117 | value () { 118 | this.paused = true; 119 | } 120 | }, 121 | poster: { 122 | get () { 123 | return _.youtubePoster.src; 124 | }, 125 | set (newValue) { 126 | _.youtubePoster.src = String(newValue); 127 | } 128 | }, 129 | play: { 130 | value () { 131 | this.paused = false; 132 | } 133 | }, 134 | volume: { 135 | get () { 136 | return _.youtubeAPIState.volume / 100; 137 | }, 138 | set (newValue) { 139 | _.youtubeAPICommand('setVolume', [Number(newValue * 100) || 0]); 140 | } 141 | }, 142 | setAttribute: { 143 | value (name, value) { 144 | _.videoElement[name] = value; 145 | } 146 | } 147 | }) 148 | 149 | _.videoElement.replaceWith(_.youtubeContainer); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /packages/social-media-player-element/src/lib/observed-attributes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'audio', 3 | 'autoplay', 4 | 'height', 5 | 'loop', 6 | 'poster', 7 | 'src', 8 | 'volume', 9 | 'width' 10 | ]; 11 | -------------------------------------------------------------------------------- /packages/uid/README.md: -------------------------------------------------------------------------------- 1 | # uid [][uid] 2 | 3 | [![Build Status][cli-img]][cli-url] 4 | [![Issue Tracker][git-img]][git-url] 5 | [![Pull Requests][gpr-img]][gpr-url] 6 | 7 | [uid] is a JavaScript function for creating unique URL-safe IDs. It will add up 8 | to 184 bytes to your project. 9 | 10 | ```js 11 | uid(5); // 1 second at 4000 IDs per second to have a 1% probability of at least one collision 12 | uid(11); // 4 days at 4000 IDs per second to have a 1% probability of at least one collision 13 | uid(21); // 10 million years at 4000 IDs per second to have a 1% probability of at least one collision 14 | ``` 15 | 16 | **[Try it now](https://t7.github.io/web-components/domset/)** 17 | 18 | ## Usage 19 | 20 | Add uid to your page. 21 | 22 | ```html 23 | 24 | 25 | 29 | 30 | ``` 31 | 32 | Alternatively, add uid to your project: 33 | 34 | ```sh 35 | npm install @t7/uid 36 | ``` 37 | 38 | ```js 39 | import uid from '@t7/uid'; 40 | 41 | // give a unique ID like "wAl_Hh9fYReEakFYN-7qr" 42 | document.body.id = uid(21); 43 | ``` 44 | 45 | ## Arguments 46 | 47 | ### size 48 | 49 | The first argument represents the size of the ID being generated. 50 | 51 | ```js 52 | // 1 second at 4000 IDs per second to have a 1% probability of at least one collision 53 | uid(5); 54 | ``` 55 | 56 | ```js 57 | // 4 days at 4000 IDs per second to have a 1% probability of at least one collision 58 | uid(11); 59 | ``` 60 | 61 | ```js 62 | // 10 million years at 4000 IDs per second to have a 1% probability of at least one collision 63 | uid(21); 64 | ``` 65 | 66 | ## Return 67 | 68 | uid returns a unique URL-safe ID. 69 | 70 | ```js 71 | // a unique ID like "wVC-s" 72 | uid(5); 73 | ``` 74 | 75 | ```js 76 | // a unique ID like "PEqV6F96R-4" 77 | uid(11); 78 | ``` 79 | 80 | ```js 81 | // a unique ID like "wAl_Hh9fYReEakFYN-7qr" 82 | uid(21); 83 | ``` 84 | 85 | [uid]: https://github.com/t7/web-components/tree/master/packages/uid 86 | 87 | [cli-img]: https://img.shields.io/travis/t7/web-components/master.svg 88 | [cli-url]: https://travis-ci.org/t7/web-components 89 | [git-img]: https://img.shields.io/github/issues-raw/t7/web-components.svg 90 | [git-url]: https://github.com/t7/web-components/issues 91 | [gpr-img]: https://img.shields.io/github/issues-pr-raw/t7/web-components.svg 92 | [gpr-url]: https://github.com/t7/web-components/pulls 93 | -------------------------------------------------------------------------------- /packages/uid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@t7/uid", 3 | "version": "0.1.0", 4 | "description": "A 140 byte library for generating unique URL-safe IDs", 5 | "author": "TandemSeven ", 6 | "license": "CC0-1.0", 7 | "repository": "t7/web-components", 8 | "homepage": "https://github.com/t7/web-components/tree/master/packages/uid#readme", 9 | "bugs": "https://github.com/t7/web-components/issues?q=uid", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "browser": "browser.js", 13 | "files": [ 14 | "browser.js", 15 | "index.js", 16 | "index.mjs" 17 | ], 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "dependencies": {}, 22 | "keywords": [ 23 | "component", 24 | "custom", 25 | "dom", 26 | "element", 27 | "web", 28 | "webcomponent" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/uid/src/example.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /packages/uid/src/index.js: -------------------------------------------------------------------------------- 1 | export default function uid (size) { 2 | const bytes = crypto.getRandomValues(new Uint8Array(size)); 3 | 4 | let id = ''; 5 | 6 | while (0 < size--) { 7 | id += "Uint8AraygeRdomVlus012345679BCDEFGHIJKLMNOPQSTWXYZbcfhjkpqvwxz_-"[63 & bytes[size]]; 8 | } 9 | 10 | return id; 11 | } 12 | -------------------------------------------------------------------------------- /scripts/config/babel.config.js: -------------------------------------------------------------------------------- 1 | const browserslist = require('fs').readFileSync('../../.browserslistrc', 'utf8').trim().replace(/\n+/g, ', '); 2 | const cssnano = require('cssnano'); 3 | const postcssPresetEnv = require('postcss-preset-env'); 4 | 5 | module.exports = { 6 | plugins: [ 7 | ['@babel/plugin-proposal-class-properties', { 8 | loose: true 9 | }], 10 | ['import-postcss', { 11 | plugins: [ 12 | postcssPresetEnv({ 13 | browsers: browserslist, 14 | stage: 0, 15 | features: { 16 | 'color-mod-function': { unresolved: 'warn' }, 17 | 'logical-properties-and-values': { 18 | dir: 'ltr' 19 | } 20 | } 21 | }), 22 | cssnano() 23 | ] 24 | }] 25 | ], 26 | presets: [ 27 | ['@babel/preset-env', { 28 | corejs: 3, 29 | loose: true, 30 | modules: false, 31 | targets: browserslist, 32 | useBuiltIns: 'entry' 33 | }] 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /scripts/config/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { terser } = require('rollup-plugin-terser'); 2 | const babel = require('rollup-plugin-babel'); 3 | const babelConfig = require('./babel.config') 4 | const commonjs = require('rollup-plugin-commonjs'); 5 | const nodeResolve = require('rollup-plugin-node-resolve'); 6 | 7 | module.exports = { 8 | plugins: [ 9 | nodeResolve(), 10 | commonjs({ 11 | include: 'node_modules/**' 12 | }), 13 | babel(babelConfig), 14 | terser({ 15 | compress: { 16 | unsafe: true 17 | } 18 | }), 19 | compressIIFE() 20 | ] 21 | }; 22 | 23 | function compressIIFE () { 24 | return { 25 | name: 'compress-iife', 26 | renderChunk (code, chunk, options) { 27 | if (options.format === 'iife') { 28 | const xRegExp = new RegExp(`^(?:var ${options.name}=function\\(\\){return function)(\\([\\W\\w]+})(?:}\\(\\);)$`); 29 | 30 | if (xRegExp.test(code)) { 31 | const [, body] = code.match(xRegExp); 32 | 33 | const newCode = `function ${options.name}${body}`; 34 | 35 | return newCode; 36 | } 37 | } 38 | 39 | return null; 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /scripts/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${this.componentName} 4 | 5 | 6 | 7 | 8 | 9 | 10 |

${this.componentName}

11 |

${this.description}

12 |
13 |
14 | ${this.html} 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/watch.js: -------------------------------------------------------------------------------- 1 | const config = require('./config/rollup.config.js'); 2 | const fse = require('fse'); 3 | const gzipSize = require('gzip-size'); 4 | const path = require('path'); 5 | const rollup = require('rollup'); 6 | 7 | const opts = { 8 | componentBase: path.basename(process.cwd()), 9 | componentDir: process.cwd(), 10 | componentName: path.basename(process.cwd()).replace(/(?:^|-)([a-z])/g, ($0, $1) => $1.toUpperCase()), 11 | ghpagesDir: path.join(path.dirname(__dirname), '.gh-pages'), 12 | rootDir: path.dirname(__dirname), 13 | templatesDir: path.join(__dirname, 'templates') 14 | }; 15 | 16 | function startWatcher () { 17 | let hasBrowserJS = false; 18 | 19 | try { 20 | hasBrowserJS = fse.readFileSync('src/browser.js', 'utf8'); 21 | 22 | opts.iifeName = (hasBrowserJS.match(/export default (?:(?:class|function) )?([A-z0-9_-]+)/) || [opts.componentName])[1]; 23 | } catch (error) { 24 | opts.iifeName = (fse.readFileSync('src/index.js', 'utf8').match(/export default (?:(?:class|function) )?([A-z0-9_-]+)/) || [opts.componentName])[1]; 25 | } 26 | 27 | const nodeConfig = { 28 | ...config, 29 | input: 'src/index.js', 30 | output: [ 31 | { file: 'index.js', format: 'cjs', sourcemap: false, strict: false }, 32 | { file: 'index.mjs', format: 'esm', sourcemap: false, strict: false } 33 | ], 34 | plugins: config.plugins.slice(0, -1) 35 | }; 36 | 37 | const browserConfig = { 38 | ...config, 39 | input: hasBrowserJS ? 'src/browser.js' : 'src/index.js', 40 | output: { file: 'browser.js', format: 'iife', name: opts.iifeName, sourcemap: false, strict: false } 41 | }; 42 | 43 | const watcher = rollup.watch(nodeConfig); 44 | 45 | watcher.on('event', event => { 46 | if (event.code === 'END') { 47 | Promise.all([ 48 | readPackageJson(), 49 | rollup.rollup(browserConfig).then( 50 | bundle => bundle.write(browserConfig.output) 51 | ) 52 | ]).then( 53 | ([ pkg, { output } ]) => { 54 | opts.code = output[0].code; 55 | opts.codeSize = gzipSize.sync(opts.code, { level: 9 }); 56 | opts.pkg = pkg; 57 | opts.description = `${opts.componentName} is ${opts.pkg.description[0].toLowerCase()}${opts.pkg.description.slice(1)}.`; 58 | 59 | return Promise.all([ 60 | generateExampleJs(), 61 | generateExampleFile() 62 | ]).then( 63 | () => { 64 | console.log(`browser.js is ${opts.codeSize} bytes`); // eslint-disable-line no-console 65 | } 66 | ) 67 | } 68 | ); 69 | } 70 | }); 71 | } 72 | 73 | function generateExampleJs () { 74 | const ghpagesIndexJs = path.join(opts.ghpagesDir, opts.componentBase, 'index.js'); 75 | 76 | return fse.writeFile(ghpagesIndexJs, opts.code); 77 | } 78 | 79 | function generateExampleFile () { 80 | const componentExampleFile = path.join(opts.componentDir, 'src', 'example.html'); 81 | const templatesExampleFile = path.join(opts.templatesDir, 'example.html'); 82 | const ghpagesIndexHtml = path.join(opts.ghpagesDir, opts.componentBase, 'index.html'); 83 | 84 | return Promise.all([ 85 | fse.readFile(componentExampleFile, 'utf8'), 86 | fse.readFile(templatesExampleFile, 'utf8') 87 | ]).then(([componentExampleHtml, templatesExampleHtml]) => { 88 | const templatesExampleCode = `return (\`${templatesExampleHtml}\`)`; 89 | const templateFunction = new Function(templatesExampleCode); 90 | 91 | opts.html = componentExampleHtml; 92 | 93 | const templateReturnValue = templateFunction.call(opts); 94 | 95 | return fse.writeFile(ghpagesIndexHtml, String(templateReturnValue)); 96 | }) 97 | } 98 | 99 | function readPackageJson () { 100 | return fse.readJson(path.join(opts.componentDir, 'package.json')); 101 | } 102 | 103 | startWatcher(); 104 | --------------------------------------------------------------------------------