Skip to content

Commit

Permalink
Add and migrate to Rollup version of scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
mahozad committed Mar 13, 2022
1 parent 7267bec commit 7f02c99
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-new-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,4 @@ jobs:
uses: softprops/action-gh-release@v1
with:
body_path: changelog.txt
files: theme-switch.min.js
files: dist/theme-switch.min.js
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ and [this library](https://github.com/GoogleChromeLabs/dark-mode-toggle).

## Use it in your page

Download the [theme-switch.min.js](theme-switch.min.js) file and reference it at the top of your HTML:
Download the [theme-switch.min.js](dist/theme-switch.min.js) file and reference it at the top of your HTML:

```html
<!DOCTYPE html>
Expand Down Expand Up @@ -119,7 +119,7 @@ In your *angular.json* file at the root of your project update the `scripts` pro
```json
"scripts": [
{
"input": "node_modules/@mahozad/theme-switch/theme-switch.min.js",
"input": "node_modules/@mahozad/theme-switch/dist/theme-switch.min.js",
"inject": false,
"bundleName": "theme-switch"
}
Expand Down
2 changes: 1 addition & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Theme switch demo</title>
<script src="../theme-switch.js"></script>
<script src="../dist/theme-switch.js"></script>
<link rel="stylesheet" href="styles.css">
<script src="scripts.js" defer></script>
</head>
Expand Down
270 changes: 270 additions & 0 deletions dist/theme-switch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
'use strict';

/*
* NOTE: Do not use this script as an ES6 module.
* ES6 modules are deferred and we don't want that because
* we want the user previous theme selection to be applied
* as soon as possible (before the page is rendered).
* */

/*
* There are two types of modules mostly used in JavaScript.
* One is created by Node.js and is used inside the Node environment
* and has been available for a long time. It is called CommonJS.
* Another is the standard native JavaScript modules introduced in ES6.
*
* The Node variant (CommonJS) uses `module.exports` (or simply `exports`) and
* `require()` to export and import scripts, functions, variables, etc.
* Browsers do not know about `exports` or `require` functions and throw error
* because they are objects and functions created just in Node environment and set globally.
* If you want to use this type of module in browsers, you should bundle the files
* (merge all of them into a single JS file which eliminates the need for exports and require)
* with tools like babel, webpack, rollup, etc.
*
* ES6 modules use `export` and `import` keywords for the same purpose.
*
* Example Node modules:
*
* my-calculator.js
* const PI = 3.14;
* function calculate() {}
* modules.exports.calculate = calculate;
* modules.exports.PI = PI;
*
* main.js
* const calculator = require("my-calculator");
* const perimeter = 2 * calculator.PI;
* const result = calculator.calculate();
*
* Example ES6 modules:
*
* my-calculator.js
* export const PI = 3.14;
* export function calculate() {}
*
* main.js
* import { calculate, PI } from "my-calculator.js";
* const perimeter = 2 * PI;
* const result = calculate();
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
* and https://stackoverflow.com/a/9901097/8583692
* */

/*
* Minify the script either through command line with babel:
* - babel theme-switch.js --source-maps --out-file result.min.js
* or with babel-minify (also has an alias called minify) which is useful if
* you don't already use babel (as a preset) or want to run minification standalone.
* Note that it does not take into account babel.config.json settings.
* - babel-minify (or minify) theme-switch.js --mangle --no-comments --out-file result.min.js`
* Or automate it with IntelliJ file watcher
* - babel
* + program: $ProjectFileDir$\node_modules\.bin\babel
* + arguments: $FilePathRelativeToProjectRoot$ --out-file $FileNameWithoutExtension$.min.js --source-maps
* Note that setting "sourceMaps": "true" in babel.config.json does not work because of
* this bug: https://github.com/babel/babel/issues/5261 ("sourceMaps": "inline" works, however)
* - UglifyJS
* - YUI compressor (seems to be deprecated and removed in newer versions of IntelliJ)
*
* Babel preset-env inserts a semicolon at the start of the minified file.
* See why: https://stackoverflow.com/q/1873983/8583692
* */

// TODO: extract Jest configuration to a jest.config.js file
// TODO: Add an attribute so the user can define key name stored in localstorage
// TODO: Make the component customizable by adding custom attributes:
// See https://medium.com/technofunnel/creating-passing-data-to-html-custom-elements-using-attributes-bfd9aa759fd4

/*
* NOTE: To avoid name collisions if another script declares variables or functions with the same name
* as ours (i.e. defining them in the global scope) and browsers complaining about identifiers
* being redeclared, we wrap all our code in a closure or IIFE (sort of creating a namespace for it).
* ES6 now supports block scope as well (simply wrapping the whole code in {}).
* I am now using the babel-plugin-iife-wrap plugin to wrap the whole result (minified)
* code in an IIFE.
* For examples, see these libraries:
* - https://github.com/highlightjs/highlight.js/blob/main/src/highlight.js
* - https://github.com/jashkenas/underscore/blob/master/underscore.js
* See
* - https://www.w3schools.com/js/js_scope.asp
* - https://github.com/jhnns/rewire/issues/136#issuecomment-380829197
* - https://stackoverflow.com/a/32750216/8583692
* - https://stackoverflow.com/q/8228281/8583692
* - https://stackoverflow.com/q/881515/8583692
* - https://stackoverflow.com/q/39388777/8583692
* - https://stackoverflow.com/a/47207686/8583692
* We could also do something like these libraries:
* - https://github.com/juliangarnier/anime/blob/master/build.js
* - https://github.com/mrdoob/three.js/
* - https://github.com/moment/moment
* - https://github.com/floating-ui/floating-ui
* */

const ELEMENT_NAME = "theme-switch";
const ICON_SIZE = 24 /* px */;
const ICON_COLOR = "#000";
const THEME_KEY = "theme";
const THEME_AUTO = "auto";
const THEME_DARK = "dark";
const THEME_LIGHT = "light";
const THEME_VALUES = [THEME_AUTO, THEME_DARK, THEME_LIGHT];
const THEME_DEFAULT = THEME_LIGHT;
const THEME_ATTRIBUTE = "data-theme";
const COLOR_SCHEME_DARK = "(prefers-color-scheme: dark)";
const CUSTOM_EVENT_NAME = "themeToggle";
// circleRadius, raysOpacity, eclipseCenterX, letterOffset
const ICON_INITIAL_STATE_FOR_AUTO = [10, 0, 33, 0];
const ICON_INITIAL_STATE_FOR_DARK = [10, 0, 20, 1];
const ICON_INITIAL_STATE_FOR_LIGHT = [5, 1, 33, 1];

class ThemeSwitchElement extends HTMLElement {
shadowRoot;
static counter = 0; // See https://stackoverflow.com/a/43116254/8583692
identifier = ThemeSwitchElement.counter++;

constructor() {
super();
// See https://stackoverflow.com/q/2305654/8583692
this.shadowRoot = this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = generateIcon(...getInitialStateForIcon());
// Add the click listener to the top-most parent (the custom element itself)
// so the padding etc. on the element be also clickable
this.shadowRoot.host.addEventListener("click", this.onClick);
// If another theme switch in page toggled, update my icon too
document.addEventListener(CUSTOM_EVENT_NAME, event => {
if (event.detail.originId !== this.identifier) {
this.adaptToTheme();
}
});
// Create some CSS to apply to the shadow DOM
// See https://css-tricks.com/styling-a-web-component/
const style = document.createElement("style");
style.textContent = generateStyle();
this.shadowRoot.append(style);
}

onClick() {
const oldTheme = getUserThemeSelection();
this.toggleTheme(oldTheme);
const newTheme = getUserThemeSelection();
const event = this.createEvent(oldTheme, newTheme);
this.dispatchEvent(event);
}

// See https://stackoverflow.com/a/53804106/8583692
createEvent(oldTheme, newTheme) {
return new CustomEvent(CUSTOM_EVENT_NAME, {
detail: {
originId: this.identifier,
oldState: oldTheme,
newState: newTheme
},
bubbles: true,
composed: true,
cancelable: false
});
}

// See https://stackoverflow.com/q/48316611
toggleTheme(currentTheme) {
if (currentTheme === THEME_AUTO) {
localStorage.setItem(THEME_KEY, THEME_LIGHT);
this.animateThemeButtonIconToLight();
} else if (currentTheme === THEME_DARK) {
localStorage.setItem(THEME_KEY, THEME_AUTO);
this.animateThemeButtonIconToAuto();
} else /* if (theme === THEME_LIGHT) */ {
localStorage.setItem(THEME_KEY, THEME_DARK);
this.animateThemeButtonIconToDark();
}
updateTheme();
}

adaptToTheme() {
const theme = getUserThemeSelection();
if (theme === THEME_AUTO) {
this.animateThemeButtonIconToAuto();
} else if (theme === THEME_DARK) {
this.animateThemeButtonIconToDark();
} else /* if (theme === THEME_LIGHT) */ {
this.animateThemeButtonIconToLight();
}
}

animateThemeButtonIconToLight() {
this.shadowRoot.getElementById("letter-anim-hide").beginElement();
this.shadowRoot.getElementById("core-anim-shrink").beginElement();
this.shadowRoot.getElementById("rays-anim-rotate").beginElement();
this.shadowRoot.getElementById("rays-anim-show").beginElement();
}

animateThemeButtonIconToAuto() {
this.shadowRoot.getElementById("eclipse-anim-go").beginElement();
this.shadowRoot.getElementById("letter-anim-show").beginElement();
}

animateThemeButtonIconToDark() {
this.shadowRoot.getElementById("rays-anim-hide").beginElement();
this.shadowRoot.getElementById("core-anim-enlarge").beginElement();
this.shadowRoot.getElementById("eclipse-anim-come").beginElement();
}
}

function generateIcon(circleRadius, raysOpacity, eclipseCenterX, letterOffset) {
return ` <button id="theme-switch"> <svg viewBox="0 0 24 24" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"> <defs> <mask id="mask"> <rect width="100%" height="100%" fill="#fff"/> <circle id="eclipse" r="10" cx="${eclipseCenterX}" cy="6"> <animate id="eclipse-anim-come" fill="freeze" attributeName="cx" to="20" dur="300ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines="0.37, 0, 0.63, 1"/> <animate id="eclipse-anim-go" fill="freeze" attributeName="cx" to="33" dur="300ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines="0.37, 0, 0.63, 1"/> </circle> <path id="letter" fill="none" stroke="#000" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" pathLength="1" stroke-dasharray="1 1" stroke-dashoffset="${letterOffset}" d="m8 16.5 4-9 4 9-1-2h-6"> <animate id="letter-anim-show" fill="freeze" attributeName="stroke-dashoffset" to="0" dur="400ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines=".67,.27,.55,.9"/> <animate id="letter-anim-hide" fill="freeze" attributeName="stroke-dashoffset" to="1" dur="15ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines="0.37, 0, 0.63, 1"/> </path> </mask> </defs> <g id="visible-content" mask="url(#mask)"> <g id="rays" opacity="${raysOpacity}" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round"> <animate id="rays-anim-hide" fill="freeze" attributeName="opacity" to="0" dur="100ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines="0.37, 0, 0.63, 1"/> <animate id="rays-anim-show" fill="freeze" attributeName="opacity" to="1" dur="300ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines="0.37, 0, 0.63, 1"/> <animateTransform id="rays-anim-rotate" attributeName="transform" attributeType="XML" type="rotate" from="-25 12 12" to="0 12 12" dur="300ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines="0.37, 0, 0.63, 1"/> <path d="m12 1v3"/> <path d="m23 12h-3"/> <path d="m19.778 4.2218-2.121 2.1213"/> <path d="m19.778 19.778-2.121-2.121"/> <path d="m4.222 19.778 2.121-2.121"/> <path d="m4.222 4.222 2.121 2.121"/> <path d="m4 12h-3"/> <path d="m12 20v3"/> </g> <circle id="circle" r="${circleRadius}" cx="12" cy="12"> <animate id="core-anim-enlarge" fill="freeze" attributeName="r" to="10" dur="300ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines="0.37, 0, 0.63, 1"/> <animate id="core-anim-shrink" fill="freeze" attributeName="r" to="5" dur="300ms" begin="indefinite" calcMode="spline" keyTimes="0; 1" keySplines="0.37, 0, 0.63, 1"/> </circle> </g> </svg> </button> `;
}

function generateStyle() {
return `:host{display:flex;width:var(--dummy-variable,${ICON_SIZE}px);aspect-ratio:1/1;cursor:pointer}:host([hidden]){display:none}button{padding:0;border:none;background:0 0;display:flex;cursor:pointer}#circle{fill:var(--theme-switch-icon-color,${ICON_COLOR})}#rays{stroke:var(--theme-switch-icon-color,${ICON_COLOR})}`;
}

updateTheme();
window.customElements.define(ELEMENT_NAME, ThemeSwitchElement);
window
.matchMedia(COLOR_SCHEME_DARK)
.addEventListener("change", updateTheme);

function updateTheme() {
let theme = getUserThemeSelection();
if (theme === THEME_AUTO) theme = getSystemTheme();
document.documentElement.setAttribute(THEME_ATTRIBUTE, theme);
}

function getUserThemeSelection() {
const userSelection = localStorage.getItem(THEME_KEY);
return THEME_VALUES.includes(userSelection) ? userSelection : THEME_DEFAULT;
}

function getSystemTheme() {
const isDark = window.matchMedia(COLOR_SCHEME_DARK).matches;
return isDark ? THEME_DARK : THEME_LIGHT;
}

function getInitialStateForIcon() {
const theme = getUserThemeSelection();
if (theme === THEME_AUTO) {
return ICON_INITIAL_STATE_FOR_AUTO;
} else if (theme === THEME_DARK) {
return ICON_INITIAL_STATE_FOR_DARK;
} else /* if (theme === THEME_LIGHT) */ {
return ICON_INITIAL_STATE_FOR_LIGHT;
}
}

// Export for tests run by npm (no longer needed; kept for future reference)
// See https://stackoverflow.com/q/63752210/8583692
// and https://stackoverflow.com/a/54680602/8583692
// and https://stackoverflow.com/q/43042889/8583692
// and https://stackoverflow.com/a/1984728/8583692
// if (typeof module !== "undefined") {
// module.exports = {
// updateTheme,
// toggleTheme,
// getSystemTheme,
// getInitialStateForIcon,
// animateThemeButtonIconToAuto,
// animateThemeButtonIconToDark,
// animateThemeButtonIconToLight
// };
// }
2 changes: 2 additions & 0 deletions dist/theme-switch.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7f02c99

Please sign in to comment.