Skip to content
Snippets Groups Projects
Commit 75105f20 authored by Steinwender, Tamara's avatar Steinwender, Tamara
Browse files

Convert theme switcher to a webcomponent

parent 82946d09
No related branches found
No related tags found
No related merge requests found
Showing
with 1230 additions and 0 deletions
/vendor/**
/dist/**
*.conf.js
*.config.js
\ No newline at end of file
{
"root": true,
"extends": "./../../eslint.common.json"
}
\ No newline at end of file
dist
node_modules
.idea
npm-debug.log
package-lock.json
index.html
node_modules
.idea
npm-debug.log
package-lock.json
index.html
This diff is collapsed.
# Theme-Switcher web component
This web component allows to switch between multiple predefined themes and can detect browsers default theme.
## Installation
You can install these components via npm:
```bash
npm i @dbp-toolkit/theme-switcher
```
After you have installed the theme-switcher component via npm you can use this example to get a button
that opens a dialogue with multiple themes. After you have selected a theme the class of the theme is
applied to the body of your html document. To add a style to the specific class you have to
```html
<dbp-theme-switcher
themes='[{"class": "light-theme", "icon": "sun", "name": "Light Mode"},
{"class": "dark-theme", "icon": "night", "name": "Dark Mode"}]'>
</dbp-theme-switcher>
<style>
.light-theme{
--dbp-override-base: white;
--dbp-override-base-inverted: black;
--dbp-override-text: black;
--dbp-override-text-inverted: white;
--dbp-override-text-muted: #767676;
--dbp-override-accent: #c24f68;
--dbp-override-primary-base: #2a4491;
--dbp-override-primary-text: white;
--dbp-override-primary-border: 1px solid #2a4491;
--dbp-override-secondary-base: white;
--dbp-override-secondary-text: black;
--dbp-override-secondary-border: 1px solid black;
--dbp-override-info: #2a4491;
--dbp-override-success: #188018;
--dbp-override-warning-as-text: #c15500;
--dbp-override-warning: #f99a41;
--dbp-override-danger: #de3535;
--dbp-override-border: 1px solid black;
--dbp-override-border-radius: 0px;
--dbp-override-hover-base: black;
--dbp-override-hover-text: white;
}
.dark-theme{
--dbp-override-base: #151515;
--dbp-override-base-inverted: white;
--dbp-override-text: white;
--dbp-override-text-inverted: #151515;
--dbp-override-text-muted: #666666;
--dbp-override-accent: #c24f68;
--dbp-override-primary-base: #8ca4eb;
--dbp-override-primary-text: #151515;
--dbp-override-primary-border: 1px solid #8ca4eb;
--dbp-override-secondary-base: #151515;
--dbp-override-secondary-text: white;
--dbp-override-secondary-border: 1px solid white;
--dbp-override-info: #8ca4eb;
--dbp-override-success: #7acc79;
--dbp-override-warning-as-text: #f99a41;
--dbp-override-warning: #f99a41;
--dbp-override-danger: #de3535;
--dbp-override-border: 1px solid white;
--dbp-override-border-radius: 0px;
--dbp-override-hover-base: white;
--dbp-override-hover-text: #151515;
}
</style>
<script type="module" src="node_modules/@dbp-toolkit/theme-switcher/dist/dbp-theme-switcher.js"></script>
```
Or you can include the JS files directly via CDN:
```html
<script type="module" src="https://unpkg.com/@dbp-toolkit/theme-switcher@0.0.1/dist/theme-switcher.js"></script>
```
## Theme Switcher
### Usage
```html
<dbp-theme-switcher></dbp-theme-switcher>
```
### Attributes
- `lang` (optional, default: `de`): set to `de` or `en` for German or English
- example `<dbp-file-source lang="de"></dbp-file-source>`
- `themes`: An array with objects with properties: `class` (defines the class name which is append to the body),
`icon` (defines the icon which is used for presenting the theme), `name` (defines the displayed name),
- if no or only one theme object is added, then the theme switcher would not be displayed.
- example `<dbp-theme-switcher
themes='[{"class": "light-theme", "icon": "sun", "name": "Light Mode"},
{"class": "dark-theme", "icon": "night", "name": "Dark Mode"}]'>
</dbp-theme-switcher>`
- `darkModeThemeOverride` (optional)
- Options:
- `darkModeThemeOverride` not set: the theme switcher detects if the browser uses the dark mode and add
the `dark-theme` class automatically to the body if it is in the themes array and no user preference is set
- `darkModeThemeOverride` is set: the theme switcher not detects the browser color mode
- `darkModeThemeOverride` is set with a string: e.g.: `darkModeThemeOverride="dunkles-theme`, the theme switcher
detects if the browser uses the dark mode and handles the given string as dark mode class
### Note
Where to body is defined, there the classes had to be defined!
## Local development
```bash
# get the source
git clone git@gitlab.tugraz.at:dbp/web-components/toolkit.git
cd toolkit/packages/theme-switcher
# install dependencies
yarn install
# constantly build dist/bundle.js and run a local web-server on port 8002
yarn run watch-local
# build local packages in dist directory
yarn run build
```
Jump to <http://localhost:8002> and you should get a demo page.
packages/theme-switcher/assets/favicon.ico

37.2 KiB

module.exports = {
input: [
'src/*.js',
],
output: './',
options: {
debug: false,
removeUnusedKeys: true,
func: {list: ['i18n.t', '_i18n.t']},
lngs: ['en','de'],
resource: {
loadPath: 'src/i18n/{{lng}}/{{ns}}.json',
savePath: 'src/i18n/{{lng}}/{{ns}}.json'
},
},
}
module.exports = require('../../karma.common.conf.js');
{
"name": "@dbp-toolkit/theme-switcher",
"homepage": "https://gitlab.tugraz.at/dbp/web-components/toolkit/-/tree/master/packages/theme-switcher",
"version": "0.0.1",
"main": "src/index.js",
"license": "LGPL-2.1-or-later",
"repository": {
"type": "git",
"url": "https://gitlab.tugraz.at/dbp/web-components/toolkit.git",
"directory": "packages/theme-switcher"
},
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"devDependencies": {
"@esm-bundle/chai": "^4.2.0",
"@rollup/plugin-commonjs": "^21.0.0",
"@rollup/plugin-json": "^4.0.2",
"@rollup/plugin-node-resolve": "^13.0.0",
"eslint": "^8.0.0",
"eslint-plugin-jsdoc": "^37.0.0",
"i18next-scanner": "^3.0.0",
"karma": "^6.0.0",
"karma-chrome-launcher": "^3.0.0",
"karma-firefox-launcher": "^2.1.0",
"karma-mocha": "^2.0.0",
"mocha": "^9.0.0",
"rollup": "^2.33.3",
"rollup-plugin-copy": "^3.1.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-serve": "^1.0.1",
"rollup-plugin-terser": "^7.0.2"
},
"dependencies": {
"@dbp-toolkit/common": "^0.2.2",
"@open-wc/scoped-elements": "^2.0.0",
"i18next": "^21.4.2",
"jszip": "^3.5.0",
"lit": "^2.0.0",
"material-design-icons-svg": "^3.0.0"
},
"scripts": {
"clean": "rm dist/*",
"build": "npm run build-local",
"build-local": "rollup -c",
"build-dev": "rollup -c --environment BUILD:development",
"build-prod": "rollup -c --environment BUILD:production",
"build-demo": "rollup -c --environment BUILD:demo",
"build-test": "rollup -c --environment BUILD:test",
"i18next": "i18next-scanner",
"watch": "npm run watch-local",
"watch-local": "rollup -c --watch",
"watch-dev": "rollup -c --watch --environment BUILD:development",
"test": "npm run build-test && karma start --singleRun",
"lint": "eslint ."
}
}
import glob from 'glob';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import copy from 'rollup-plugin-copy';
import {terser} from "rollup-plugin-terser";
import json from '@rollup/plugin-json';
import serve from 'rollup-plugin-serve';
import del from 'rollup-plugin-delete';
import {getPackagePath, getDistPath} from '../../rollup.utils.js';
import path from "path";
const pkg = require('./package.json');
const build = (typeof process.env.BUILD !== 'undefined') ? process.env.BUILD : 'local';
console.log("build: " + build);
let nextcloudBaseURL = 'https://cloud.tugraz.at';
let nextcloudFileURL = nextcloudBaseURL + '/apps/files/?dir=';
export default (async () => {
return {
input: (build !== 'test') ? ['src/demo.js', 'src/dbp-theme-switcher.js'] : glob.sync('test/**/*.js'),
output: {
dir: 'dist',
entryFileNames: '[name].js',
chunkFileNames: 'shared/[name].[hash].[format].js',
format: 'esm',
sourcemap: true
},
plugins: [
del({
targets: 'dist/*'
}),
resolve({browser: true}),
commonjs(),
json(),
(build !== 'local' && build !== 'test') ? terser() : false,
copy({
targets: [
{src: 'assets/index.html', dest: 'dist'},
{src: 'assets/favicon.ico', dest: 'dist'},
{src: await getPackagePath('@dbp-toolkit/common', 'assets/icons/*.svg'), dest: 'dist/' + await getDistPath('@dbp-toolkit/common', 'icons')},
],
}),
(process.env.ROLLUP_WATCH === 'true') ? serve({contentBase: 'dist', host: '127.0.0.1', port: 8002}) : false
]
};
})();
\ No newline at end of file
import * as commonUtils from "@dbp-toolkit/common/utils";
import {ThemeSwitcher} from './theme-switcher';
commonUtils.defineCustomElement('dbp-theme-switcher', ThemeSwitcher);
import {createInstance} from './i18n';
import {html, LitElement} from 'lit';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import {ThemeSwitcher} from "./theme-switcher";
import * as commonUtils from '@dbp-toolkit/common/utils';
export class ThemeSwitcherDemo extends ScopedElementsMixin(LitElement) {
constructor() {
super();
this._i18n = createInstance();
this.lang = this._i18n.language;
this.url = '';
this.selectedFiles = [];
this.selectedFilesCount = 0;
}
static get scopedElements() {
return {
'dbp-theme-switcher': ThemeSwitcher,
};
}
static get properties() {
return {
lang: { type: String }
};
}
connectedCallback() {
super.connectedCallback();
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
if (propName === "lang") {
this._i18n.changeLanguage(this.lang);
}
});
super.update(changedProperties);
}
render() {
const i18n = this._i18n;
return html`
<style>
.light-theme{
/* You must not fill out all vars, if they are not filled in, it would take an appropriate default value */
--dbp-override-base: white;
--dbp-override-base-inverted: black;
--dbp-override-text: black;
--dbp-override-text-inverted: white;
--dbp-override-text-muted: #767676;
--dbp-override-accent: #c24f68;
--dbp-override-primary-base: #2a4491;
--dbp-override-primary-text: white;
--dbp-override-primary-border: 1px solid #2a4491;
--dbp-override-secondary-base: white;
--dbp-override-secondary-text: black;
--dbp-override-secondary-border: 1px solid black;
--dbp-override-info: #2a4491;
--dbp-override-success: #188018;
--dbp-override-warning-as-text: #c15500;
--dbp-override-warning: #f99a41;
--dbp-override-danger: #de3535;
--dbp-override-border: 1px solid black;
--dbp-override-border-radius: 0px;
/* Remove hover vars if you don't want a hover effect */
--dbp-override-hover-base: black;
--dbp-override-hover-text: white;
}
.dark-theme{
/* If you don't want to use an additional theme, then delete the "themes" attribute */
/* You have to fill out all vars, if you want to use the dark theme, the default values would be the light theme. */
--dbp-override-base: #151515;
--dbp-override-base-inverted: white;
--dbp-override-text: white;
--dbp-override-text-inverted: #151515;
--dbp-override-text-muted: #666666;
--dbp-override-accent: #c24f68;
--dbp-override-primary-base: #8ca4eb;
--dbp-override-primary-text: #151515;
--dbp-override-primary-border: 1px solid #8ca4eb;
--dbp-override-secondary-base: #151515;
--dbp-override-secondary-text: white;
--dbp-override-secondary-border: 1px solid white;
--dbp-override-info: #8ca4eb;
--dbp-override-success: #7acc79;
--dbp-override-warning-as-text: #f99a41;
--dbp-override-warning: #f99a41;
--dbp-override-danger: #de3535;
--dbp-override-border: 1px solid white;
--dbp-override-border-radius: 0px;
/* Remove hover vars if you don't want a hover effect */
--dbp-override-hover-base: white;
--dbp-override-hover-text: #151515;
}
</style>
<section class="section">
<div class="content">
<h1 class="title">${i18n.t('demo-title')}</h1>
<p>${i18n.t('intro')}</p>
</div>
<div class="content">
<dbp-theme-switcher themes='[{"class": "light-theme", "icon": "sun", "name": "Light Mode"}, {"class": "dark-theme", "icon": "night", "name": "Dark Mode"}]'></dbp-theme-switcher>
</div>
</section>
`;
}
}
commonUtils.defineCustomElement('dbp-theme-switcher-demo', ThemeSwitcherDemo);
import {createInstance as _createInstance} from '@dbp-toolkit/common/i18next.js';
import de from './i18n/de/translation.json';
import en from './i18n/en/translation.json';
export function createInstance() {
return _createInstance({en: en, de: de}, 'de', 'en');
}
\ No newline at end of file
{
"demo-title": "Theme Switcher Demo",
"intro": "Mit dem Theme-Switcher können Sie zwischen unterschiedlichen Farb-Themes umschalten.",
"color-mode": "Farbmodus ändern"
}
{
"demo-title": "Theme Switcher Demo",
"intro": "With the theme-switcher you can switch between multiple themes.",
"color-mode": "Farbmodus ändern"
}
export {ThemeSwitcher} from './theme-switcher';
\ No newline at end of file
import {createInstance} from './i18n.js';
import {html, css} from 'lit';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import {AdapterLitElement, Icon} from '@dbp-toolkit/common';
import * as commonStyles from '@dbp-toolkit/common/styles';
import {classMap} from 'lit/directives/class-map.js';
export class ThemeSwitcher extends ScopedElementsMixin(AdapterLitElement) {
constructor() {
super();
this._i18n = createInstance();
this.lang = this._i18n.language;
this.themes = [];
this.boundCloseAdditionalMenuHandler = this.hideModeMenu.bind(this);
this.detectBrowserDarkMode = false;
this.darkModeClass = "dark-theme";
}
static get properties() {
return {
...super.properties,
lang: { type: String },
themes: { type: Array, attribute: "themes" },
darkModeThemeOverride: {type: String, attribute: "dark-mode-theme-override"}
};
}
static get scopedElements() {
return {
'dbp-icon': Icon
};
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
if (propName === "lang") {
this._i18n.changeLanguage(this.lang);
}
});
super.update(changedProperties);
}
connectedCallback() {
super.connectedCallback();
this.updateComplete.then(() => {
if (typeof this.darkModeThemeOverride === "undefined") {
this.detectBrowserDarkMode = true;
} else if ( this.darkModeThemeOverride === "") {
this.detectBrowserDarkMode = false;
} else {
this.detectBrowserDarkMode = true;
this.darkModeClass = this.darkModeThemeOverride;
}
this.loadTheme("light-theme");
this.detectInitialMode();
});
}
detectInitialMode() {
//look for saved modes
let prefMode = localStorage.getItem('prefered-color-mode');
if (prefMode) {
// search for prefered mode
const theme = this.themes.find(theme => theme.class === prefMode);
if (theme) {
this.loadTheme(theme.class);
}
return;
}
if (this.detectBrowserDarkMode) {
//look for browser mode
const useDark = window.matchMedia("(prefers-color-scheme: dark)");
if (useDark.matches) {
// search for dark mode
const theme = this.themes.find(theme => theme.class === this.darkModeClass);
if (theme) {
this.loadTheme(theme.class);
}
}
}
}
toggleModeMenu() {
const button = this.shadowRoot.querySelector(".mode-button");
if(!button) {
return;
}
if (button.classList.contains("active"))
button.classList.remove("active");
else
button.classList.add("active");
const menu = this.shadowRoot.querySelector("ul.extended-menu");
const menuStart = this.shadowRoot.querySelector(".mode-button");
if (menu === null || menuStart === null) {
return;
}
menu.classList.toggle('hidden');
if (!menu.classList.contains('hidden')) { // add event listener for clicking outside of menu
document.addEventListener('click', this.boundCloseAdditionalMenuHandler);
this.initateOpenAdditionalMenu = true;
}
else {
document.removeEventListener('click', this.boundCloseAdditionalMenuHandler);
}
}
hideModeMenu() {
if (this.initateOpenAdditionalMenu) {
this.initateOpenAdditionalMenu = false;
return;
}
const menu = this.shadowRoot.querySelector("ul.extended-menu");
if (menu && !menu.classList.contains('hidden'))
this.toggleModeMenu();
}
loadTheme(themeName) {
const button = this.shadowRoot.querySelector(".button-" + themeName);
const otherButtons = this.shadowRoot.querySelectorAll(".button-theme");
const body = this.shadowRoot.host.getRootNode({composed: true}).body;
if (button === null || otherButtons.length === 0 || body === null ) {
return;
}
otherButtons.forEach(button => button.classList.remove("active"));
button.classList.add("active");
if (!body.classList.contains(themeName)) {
this.themes.forEach(theme => {
body.classList.remove(theme.class);
});
body.classList.add(themeName);
}
}
saveTheme(themeName) {
//set active state
const browserModeDark = window.matchMedia("(prefers-color-scheme: dark)");
const browserModeLight = window.matchMedia("(prefers-color-scheme: light)");
if (themeName === "light-theme" && browserModeLight.matches) {
localStorage.removeItem('prefered-color-mode');
} else if (themeName === this.darkModeClass && browserModeDark.matches) {
localStorage.removeItem('prefered-color-mode');
} else {
localStorage.setItem('prefered-color-mode', themeName);
}
}
static get styles() {
return css`
${commonStyles.getThemeCSS()}
${commonStyles.getGeneralCSS()}
${commonStyles.getButtonCSS()}
mode-button, button.button {
border: none;
}
.active, .extended-menu li a.active dbp-icon {
color: var(--dbp-accent);
}
.active {
font-weight: bolder;
}
a:hover:not(.active) , .extended-menu li a:hover:not(.active) {
color: var(--dbp-hover-text);
background-color: var(--dbp-hover-base);
transition: none;
}
a {
padding: 0.3em;
display: inline-block;
text-decoration: none;
transition: background-color 0.15s, color 0.15s;
color: var(--dbp-text);
}
.extended-menu {
list-style: none;
border: var(--dbp-border);
position: absolute;
background-color: var(--dbp-base);
z-index: 1000;
border-radius: var(--dbp-border-radius);
}
.extended-menu li {
text-align: left;
min-width: 160px;
}
.extended-menu li a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 12px 15px;
w1idth: 100%;
box-sizing: border-box;
text-align: left;
color: var(--dbp-text);
background: none;
display: block
}
.icon {
margin-right: 10px;
}
`;
}
render() {
const i18n = this._i18n;
return html`
<div class="${classMap({hidden: this.themes.length <= 1})}">
<a class="mode-button" title="${i18n.t('color-mode')}"
@click="${() => {this.toggleModeMenu();}}"><dbp-icon name="contrast"></dbp-icon></a>
<ul class='extended-menu hidden'>
${this.themes.map(theme => html`
<li class="" id="${theme.class}">
<a class="button-theme button-${theme.class}" @click="${() => {this.loadTheme(theme.class); this.saveTheme(theme.class);}}" title="${theme.name}">
<dbp-icon class="icon" name="${theme.icon}"></dbp-icon> ${theme.name}
</a>
</li>
`)}
</ul>
</div>
`;
}
}
\ No newline at end of file
import {assert} from '@esm-bundle/chai';
import '../src/dbp-theme-switcher';
import '../src/demo';
suite('dbp-theme-switcher basics', () => {
let node;
setup(async () => {
node = document.createElement('dbp-theme-switcher');
document.body.appendChild(node);
await node.updateComplete;
});
teardown(() => {
node.remove();
});
test('should render', () => {
assert.isNotNull(node.shadowRoot);
});
});
suite('dbp-theme-switcher demo', () => {
let node;
setup(async () => {
node = document.createElement('dbp-theme-switcher-demo');
document.body.appendChild(node);
await node.updateComplete;
});
teardown(() => {
node.remove();
});
test('should render', () => {
assert.isNotNull(node.shadowRoot);
});
});
{
"element": "dbp-theme-switcher-demo-activity",
"module_src": "dbp-theme-switcher-demo-activity.js",
"routing_name": "theme-switcher",
"name": {
"de": "Theme-Switcher Komponente",
"en": "Theme-switcher component"
},
"short_name": {
"de": "Theme-Switcher Komponente",
"en": "Theme-switcher component"
},
"description": {
"de": "Theme Switcher Web Komponente",
"en": "Theme-Switcher web component"
},
"subscribe": "lang"
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment