profitlich-template-toolkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/menu-toggle/MenuToggle.js +124 -0
- package/components/menu-toggle/menu-toggle.scss +14 -0
- package/components/mux-player/MuxPlayer.js +106 -0
- package/components/mux-player/mux-player.scss +3 -0
- package/package.json +34 -0
- package/scss/core/hamburger.scss +153 -0
- package/scss/core/layout.scss +239 -0
- package/scss/core/mediaqueries.scss +42 -0
- package/utils/BodyScrolled.js +31 -0
- package/utils/MediaQueries.js +56 -0
- package/utils/Vh100.js +31 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import './menu-toggle.scss';
|
|
2
|
+
|
|
3
|
+
export class MenuToggle {
|
|
4
|
+
static #instance;
|
|
5
|
+
#menuButton;
|
|
6
|
+
#menu;
|
|
7
|
+
#menuLinkClass;
|
|
8
|
+
#menuItemClass;
|
|
9
|
+
#scrollbarWidth;
|
|
10
|
+
#shiftElement;
|
|
11
|
+
#y;
|
|
12
|
+
#bodyClickHandler;
|
|
13
|
+
#resizeHandler;
|
|
14
|
+
#escapeHandler;
|
|
15
|
+
isActive;
|
|
16
|
+
|
|
17
|
+
constructor(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId) {
|
|
18
|
+
this.#menuButton = document.getElementById(menuButtonId);
|
|
19
|
+
this.#menu = document.getElementById(menuId);
|
|
20
|
+
this.#menuLinkClass = menuLinkClass;
|
|
21
|
+
this.#menuItemClass = menuItemClass;
|
|
22
|
+
this.#shiftElement = shiftElementId ? document.getElementById(shiftElementId) : null;
|
|
23
|
+
this.#scrollbarWidth = 0;
|
|
24
|
+
this.isActive = false;
|
|
25
|
+
this.#y = 0;
|
|
26
|
+
|
|
27
|
+
this.#bodyClickHandler = this.#onBodyClick.bind(this);
|
|
28
|
+
this.#resizeHandler = this.#onResize.bind(this);
|
|
29
|
+
this.#escapeHandler = this.#onEscape.bind(this);
|
|
30
|
+
|
|
31
|
+
this.#menuButton.addEventListener('click', (event) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
event.stopPropagation();
|
|
34
|
+
this.#toggleMenu();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.#menu.addEventListener('click', (event) => {
|
|
38
|
+
if (this.isActive && event.target.matches(this.#menuLinkClass)) {
|
|
39
|
+
this.#toggleMenu();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static getInstance(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId) {
|
|
45
|
+
if (!MenuToggle.#instance) {
|
|
46
|
+
if (!menuButtonId || !menuId || !menuLinkClass || !menuItemClass) {
|
|
47
|
+
throw new Error("MenuToggle muss beim ersten Aufruf mit menuButtonId, menuId, menuLinkClass und menuItemClass initialisiert werden.");
|
|
48
|
+
}
|
|
49
|
+
MenuToggle.#instance = new MenuToggle(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId);
|
|
50
|
+
}
|
|
51
|
+
return MenuToggle.#instance;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#toggleMenu() {
|
|
55
|
+
if (this.isActive) {
|
|
56
|
+
this.isActive = false;
|
|
57
|
+
document.body.removeEventListener('click', this.#bodyClickHandler);
|
|
58
|
+
window.removeEventListener('resize', this.#resizeHandler);
|
|
59
|
+
this.#setBodyAttribute('data-menu-active', 'false');
|
|
60
|
+
if (this.#shiftElement) {
|
|
61
|
+
this.#shiftElement.style.marginRight = '';
|
|
62
|
+
this.#shiftElement.style.width = '';
|
|
63
|
+
}
|
|
64
|
+
document.body.style.paddingRight = '';
|
|
65
|
+
document.body.style.top = '';
|
|
66
|
+
window.scrollTo(0, this.#y);
|
|
67
|
+
this.#toggleEscape(false);
|
|
68
|
+
} else {
|
|
69
|
+
this.isActive = true;
|
|
70
|
+
document.body.addEventListener('click', this.#bodyClickHandler);
|
|
71
|
+
window.addEventListener('resize', this.#resizeHandler);
|
|
72
|
+
this.#scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
73
|
+
this.#y = window.scrollY;
|
|
74
|
+
this.#setBodyAttribute('data-menu-active', 'true');
|
|
75
|
+
document.body.style.paddingRight = `${this.#scrollbarWidth}px`;
|
|
76
|
+
document.body.style.top = `-${this.#y}px`;
|
|
77
|
+
if (this.#shiftElement) {
|
|
78
|
+
const marginOriginal = parseFloat(window.getComputedStyle(this.#menuButton).marginRight);
|
|
79
|
+
this.#shiftElement.style.marginRight = `${marginOriginal + this.#scrollbarWidth}px`;
|
|
80
|
+
this.#adjustShiftElementWidth();
|
|
81
|
+
}
|
|
82
|
+
this.#toggleEscape(true);
|
|
83
|
+
}
|
|
84
|
+
const event = new CustomEvent('eventMenuestatus', {
|
|
85
|
+
detail: { menueStatus: this.isActive }
|
|
86
|
+
});
|
|
87
|
+
document.dispatchEvent(event);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#adjustShiftElementWidth() {
|
|
91
|
+
const contentWidth = document.documentElement.clientWidth;
|
|
92
|
+
this.#shiftElement.style.width = `${contentWidth - this.#scrollbarWidth}px`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#onBodyClick(event) {
|
|
96
|
+
if (this.isActive && !event.target.closest('.' + this.#menuItemClass)) {
|
|
97
|
+
this.#toggleMenu();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#onResize() {
|
|
102
|
+
if (this.#shiftElement) {
|
|
103
|
+
this.#adjustShiftElementWidth();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#setBodyAttribute(attr, value) {
|
|
108
|
+
document.body.setAttribute(attr, value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#toggleEscape(status) {
|
|
112
|
+
if (status) {
|
|
113
|
+
document.addEventListener('keydown', this.#escapeHandler);
|
|
114
|
+
} else {
|
|
115
|
+
document.removeEventListener('keydown', this.#escapeHandler);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#onEscape(event) {
|
|
120
|
+
if (event.key === 'Escape') {
|
|
121
|
+
this.#toggleMenu();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
body {
|
|
2
|
+
|
|
3
|
+
&[data-menu-active="true"] {
|
|
4
|
+
position: fixed;
|
|
5
|
+
width: 100%;
|
|
6
|
+
|
|
7
|
+
// Bedeckt das Menü nur einen Teil der Seite, ist also ein Teil der Seite weiterhin zu sehen,
|
|
8
|
+
// soll die Maus Interaktionen ausserhalb des Menüs ignorieren
|
|
9
|
+
.main {
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import '@mux/mux-player';
|
|
2
|
+
import './mux-player.scss';
|
|
3
|
+
|
|
4
|
+
export class MuxPlayer {
|
|
5
|
+
#lazyLoadObserver;
|
|
6
|
+
#playPauseObserver;
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.#lazyLoadObserver = new IntersectionObserver(this.#handleLazyLoad.bind(this));
|
|
10
|
+
this.#playPauseObserver = new IntersectionObserver(this.#handlePlayPause.bind(this));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
observeLazyElements() {
|
|
14
|
+
document.querySelectorAll('.mux-player').forEach(el => {
|
|
15
|
+
this.#lazyLoadObserver.observe(el);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#handleLazyLoad(entries) {
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (entry.isIntersecting) {
|
|
22
|
+
this.#lazyLoadObserver.unobserve(entry.target);
|
|
23
|
+
const container = entry.target;
|
|
24
|
+
const playbackId = container.dataset.playbackId;
|
|
25
|
+
const aspectRatio = container.dataset.aspectRatio || '16 / 9';
|
|
26
|
+
const autoplay = container.dataset.autoplay === "true";
|
|
27
|
+
|
|
28
|
+
const player = document.createElement('mux-player');
|
|
29
|
+
player.playbackId = playbackId;
|
|
30
|
+
player.streamType = 'on-demand';
|
|
31
|
+
player.playsInline = true;
|
|
32
|
+
player.style.aspectRatio = aspectRatio;
|
|
33
|
+
|
|
34
|
+
// Autoplay-Attribut am Player-Element setzen, damit es später gefunden werden kann
|
|
35
|
+
if (autoplay) {
|
|
36
|
+
player.setAttribute('data-autoplay', 'true');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const wrapper = container.closest('[data-video-wrapper]');
|
|
40
|
+
|
|
41
|
+
// Wenn ein Wrapper gefunden wird, richte den MutationObserver ein
|
|
42
|
+
if (wrapper) {
|
|
43
|
+
const visibilityObserver = new MutationObserver(() => {
|
|
44
|
+
this.#handleVisibilityChange(player);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Beobachte den gefundenen Wrapper
|
|
48
|
+
visibilityObserver.observe(wrapper, { attributes: true, childList: true, subtree: true });
|
|
49
|
+
|
|
50
|
+
// Container durch Player ersetzen BEVOR die Sichtbarkeitsprüfung
|
|
51
|
+
container.replaceWith(player);
|
|
52
|
+
|
|
53
|
+
// Führe eine initiale Prüfung durch, falls es schon sichtbar ist
|
|
54
|
+
this.#handleVisibilityChange(player);
|
|
55
|
+
} else if (autoplay) {
|
|
56
|
+
// Fallback für normale Autoplay-Videos ohne speziellen Wrapper
|
|
57
|
+
player.muted = true;
|
|
58
|
+
player.style.setProperty('--controls', 'none');
|
|
59
|
+
container.replaceWith(player);
|
|
60
|
+
|
|
61
|
+
player.addEventListener('loadeddata', () => {
|
|
62
|
+
this.#playPauseObserver.observe(player);
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
// Für Videos ohne Autoplay
|
|
66
|
+
container.replaceWith(player);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#handleVisibilityChange(player) {
|
|
73
|
+
// Prüfe direkt am Player, ob er durch CSS sichtbar ist
|
|
74
|
+
const isVisible = window.getComputedStyle(player).display !== 'none';
|
|
75
|
+
|
|
76
|
+
if (isVisible && player.hasAttribute('data-autoplay')) {
|
|
77
|
+
// Wenn der Player sichtbar ist und Autoplay haben soll
|
|
78
|
+
player.muted = true;
|
|
79
|
+
player.style.setProperty('--controls', 'none');
|
|
80
|
+
|
|
81
|
+
// Warte auf loadeddata bevor der IntersectionObserver aktiviert wird
|
|
82
|
+
if (player.readyState >= 2) { // HAVE_CURRENT_DATA oder höher
|
|
83
|
+
this.#playPauseObserver.observe(player);
|
|
84
|
+
} else {
|
|
85
|
+
player.addEventListener('loadeddata', () => {
|
|
86
|
+
this.#playPauseObserver.observe(player);
|
|
87
|
+
}, { once: true });
|
|
88
|
+
}
|
|
89
|
+
} else if (!isVisible) {
|
|
90
|
+
// Wenn der Player unsichtbar wird, pausiere ihn und stoppe die Beobachtung
|
|
91
|
+
this.#playPauseObserver.unobserve(player);
|
|
92
|
+
player.pause();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#handlePlayPause(entries) {
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const player = entry.target;
|
|
99
|
+
if (entry.isIntersecting) {
|
|
100
|
+
player.play().catch(e => console.error("Player-Fehler:", e));
|
|
101
|
+
} else {
|
|
102
|
+
player.pause();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "profitlich-template-toolkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared SCSS layout system, JS utilities and components for profitlich template repos",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./scss/core/layout": "./scss/core/layout.scss",
|
|
8
|
+
"./scss/core/mediaqueries": "./scss/core/mediaqueries.scss",
|
|
9
|
+
"./scss/core/hamburger": "./scss/core/hamburger.scss",
|
|
10
|
+
"./utils/MediaQueries": "./utils/MediaQueries.js",
|
|
11
|
+
"./utils/Vh100": "./utils/Vh100.js",
|
|
12
|
+
"./utils/BodyScrolled": "./utils/BodyScrolled.js",
|
|
13
|
+
"./components/menu-toggle/MenuToggle": "./components/menu-toggle/MenuToggle.js",
|
|
14
|
+
"./components/mux-player/MuxPlayer": "./components/mux-player/MuxPlayer.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"scss/",
|
|
18
|
+
"utils/",
|
|
19
|
+
"components/"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@mux/mux-player": ">=3.0.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"@mux/mux-player": {
|
|
26
|
+
"optional": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/profitlich-ch/profitlich-template-toolkit"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
@mixin hamburger($hamburger-breite, $hamburger-hoehe, $hamburger-line-width, $hamburger-color, $hamburger-color-active) {
|
|
2
|
+
|
|
3
|
+
// Settings
|
|
4
|
+
// ==================================================
|
|
5
|
+
$hamburger-padding-x : 15px !default;
|
|
6
|
+
$hamburger-padding-y : 15px !default;
|
|
7
|
+
$hamburger-line-gap : calc(($hamburger-hoehe - 3 * $hamburger-line-width) / 2) !default;
|
|
8
|
+
$hamburger-color : #000 !default;
|
|
9
|
+
$hamburger-layer-border-radius : 0 !default;
|
|
10
|
+
$hamburger-hover-opacity : 1 !default;
|
|
11
|
+
$hamburger-active-layer-color : $hamburger-color-active !default;
|
|
12
|
+
$hamburger-active-hover-opacity: $hamburger-hover-opacity !default;
|
|
13
|
+
$hamburger-animation-timing: 0.195s;
|
|
14
|
+
$hamburger-compression-speed: 0.12s;
|
|
15
|
+
$hamburger-turn-speed: $hamburger-animation-timing - $hamburger-compression-speed;
|
|
16
|
+
|
|
17
|
+
// To use CSS filters as the hover effect instead of opacity,
|
|
18
|
+
// set $hamburger-hover-use-filter as true and
|
|
19
|
+
// change the value of $hamburger-hover-filter accordingly.
|
|
20
|
+
$hamburger-hover-use-filter : false !default;
|
|
21
|
+
$hamburger-hover-filter : opacity(50%) !default;
|
|
22
|
+
$hamburger-active-hover-filter: $hamburger-hover-filter !default;
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// padding: $hamburger-padding-y $hamburger-padding-x;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
&:hover {
|
|
29
|
+
@if $hamburger-hover-use-filter==true {
|
|
30
|
+
filter: $hamburger-hover-filter;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@else {
|
|
34
|
+
opacity: $hamburger-hover-opacity;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
body[data-menu-active='true'] & {
|
|
39
|
+
&:hover {
|
|
40
|
+
@if $hamburger-hover-use-filter==true {
|
|
41
|
+
filter: $hamburger-active-hover-filter;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@else {
|
|
45
|
+
opacity: $hamburger-active-hover-opacity;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.hamburger-inner,
|
|
50
|
+
.hamburger-inner::before,
|
|
51
|
+
.hamburger-inner::after {
|
|
52
|
+
background-color: $hamburger-active-layer-color;
|
|
53
|
+
transition: 0s $hamburger-compression-speed linear;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.hamburger-box {
|
|
58
|
+
width: $hamburger-breite;
|
|
59
|
+
height: calc($hamburger-line-width * 3 + $hamburger-line-gap * 2);
|
|
60
|
+
display: inline-block;
|
|
61
|
+
position: relative;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.hamburger-inner {
|
|
65
|
+
display: block;
|
|
66
|
+
top: 50%;
|
|
67
|
+
margin-top: calc($hamburger-line-width / -2);
|
|
68
|
+
|
|
69
|
+
&,
|
|
70
|
+
&::before,
|
|
71
|
+
&::after {
|
|
72
|
+
width: $hamburger-breite;
|
|
73
|
+
height: $hamburger-line-width;
|
|
74
|
+
background-color: $hamburger-color;
|
|
75
|
+
border-radius: $hamburger-layer-border-radius;
|
|
76
|
+
position: absolute;
|
|
77
|
+
transition-property: transform;
|
|
78
|
+
transition-duration: 0.15s;
|
|
79
|
+
transition-timing-function: ease;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&::before,
|
|
83
|
+
&::after {
|
|
84
|
+
content: "";
|
|
85
|
+
display: block;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&::before {
|
|
89
|
+
top: calc(($hamburger-line-gap + $hamburger-line-width) * -1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&::after {
|
|
93
|
+
bottom: calc(($hamburger-line-gap + $hamburger-line-width) * -1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// Aus Basis
|
|
100
|
+
display: block;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
|
|
103
|
+
transition-property: opacity, filter;
|
|
104
|
+
transition-duration: 0.15s;
|
|
105
|
+
transition-timing-function: linear;
|
|
106
|
+
|
|
107
|
+
// Normalize (<button>)
|
|
108
|
+
font: inherit;
|
|
109
|
+
color: inherit;
|
|
110
|
+
text-transform: none;
|
|
111
|
+
background-color: transparent;
|
|
112
|
+
border: 0;
|
|
113
|
+
margin: 0;
|
|
114
|
+
overflow: visible;
|
|
115
|
+
line-height: 0;
|
|
116
|
+
|
|
117
|
+
.hamburger-inner {
|
|
118
|
+
transition-duration: $hamburger-turn-speed;
|
|
119
|
+
transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
|
120
|
+
|
|
121
|
+
&::before {
|
|
122
|
+
transition: top $hamburger-turn-speed $hamburger-compression-speed ease,
|
|
123
|
+
opacity $hamburger-turn-speed ease;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&::after {
|
|
127
|
+
transition: bottom $hamburger-turn-speed $hamburger-compression-speed ease,
|
|
128
|
+
transform $hamburger-turn-speed cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
body[data-menu-active='true'] & {
|
|
133
|
+
.hamburger-inner {
|
|
134
|
+
transform: rotate(45deg);
|
|
135
|
+
transition-delay: $hamburger-compression-speed;
|
|
136
|
+
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
137
|
+
|
|
138
|
+
&::before {
|
|
139
|
+
top: 0;
|
|
140
|
+
opacity: 0;
|
|
141
|
+
transition: top $hamburger-turn-speed ease,
|
|
142
|
+
opacity $hamburger-turn-speed $hamburger-compression-speed ease;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
&::after {
|
|
146
|
+
bottom: 0;
|
|
147
|
+
transform: rotate(-90deg);
|
|
148
|
+
transition: bottom $hamburger-turn-speed ease,
|
|
149
|
+
transform $hamburger-turn-speed $hamburger-compression-speed cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
@use "sass:list";
|
|
2
|
+
@use "sass:map";
|
|
3
|
+
@use "sass:meta";
|
|
4
|
+
@use "sass:math";
|
|
5
|
+
|
|
6
|
+
@use "config";
|
|
7
|
+
|
|
8
|
+
// Media queries get the breakpoints
|
|
9
|
+
// Breakpints get the layouts
|
|
10
|
+
// Depending on the project, only a part of the layout names are used,
|
|
11
|
+
// so there needs to be an intervention in the breakpoints to deliver a number even if a layout is not present.
|
|
12
|
+
// Example: There are only the layouts smartphone, tablet and desktop.
|
|
13
|
+
// Then tabletPortrait must deliver the value of tablet
|
|
14
|
+
|
|
15
|
+
$tabletPortrait: null;
|
|
16
|
+
@if map.get(config.$layouts, tabletPortrait) {
|
|
17
|
+
$tabletPortrait: tabletPortrait;
|
|
18
|
+
// no tabletPortrait size is defined
|
|
19
|
+
} @else {
|
|
20
|
+
@if map.get(config.$layouts, tablet) {
|
|
21
|
+
$tabletPortrait: tablet;
|
|
22
|
+
// neither tablet size is defined
|
|
23
|
+
} @else {
|
|
24
|
+
$tabletPortrait: desktop;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$bigFrom: null;
|
|
29
|
+
@if map.get(config.$layouts, big) {
|
|
30
|
+
$bigFrom: big;
|
|
31
|
+
} @else {
|
|
32
|
+
$bigFrom: 999999px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$breakpointsVariables: (
|
|
36
|
+
smartphoneTo: map.get(config.$breakpoints, $tabletPortrait) - 1px,
|
|
37
|
+
tabletPortraitFrom: map.get(config.$breakpoints, $tabletPortrait) + 0px,
|
|
38
|
+
tabletPortraitTo: map.get(config.$breakpoints, tabletLandscape) - 1px,
|
|
39
|
+
tabletLandscapeFrom:map.get(config.$breakpoints, tabletLandscape) + 0px,
|
|
40
|
+
tabletLandscapeTo: map.get(config.$breakpoints, desktop) - 1px,
|
|
41
|
+
desktopFrom: map.get(config.$breakpoints, desktop) + 0px,
|
|
42
|
+
desktopTo: map.get(config.$breakpoints, big) - 1px,
|
|
43
|
+
bigFrom: map.get(config.$breakpoints, big) + 0px,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Gibt es kein Layout big, soll die media query desktop keine max-width haben
|
|
47
|
+
$mediaqueryDesktop: null;
|
|
48
|
+
@if map.get(config.$layouts, big) {
|
|
49
|
+
$mediaqueryDesktop: (min-width: map.get($breakpointsVariables, desktopFrom), max-width: map.get($breakpointsVariables, desktopTo));
|
|
50
|
+
} @else {
|
|
51
|
+
$mediaqueryDesktop: (min-width: map.get($breakpointsVariables, desktopFrom));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$mediaqueries: (
|
|
55
|
+
smartphone: (max-width: map.get($breakpointsVariables, smartphoneTo)),
|
|
56
|
+
tablet: (min-width: map.get($breakpointsVariables, tabletPortraitFrom), max-width: map.get($breakpointsVariables, tabletLandscapeTo)),
|
|
57
|
+
tabletFrom: (min-width: map.get($breakpointsVariables, tabletPortraitFrom)),
|
|
58
|
+
tabletTo: (max-width: map.get($breakpointsVariables, tabletLandscapeTo)),
|
|
59
|
+
tabletPortraitFrom: (min-width: map.get($breakpointsVariables, tabletPortraitFrom)),
|
|
60
|
+
tabletPortrait: (min-width: map.get($breakpointsVariables, tabletPortraitFrom), max-width: map.get($breakpointsVariables, tabletPortraitTo)),
|
|
61
|
+
tabletPortraitTo: (max-width: map.get($breakpointsVariables, tabletPortraitTo)),
|
|
62
|
+
tabletLandscapeFrom:(min-width: map.get($breakpointsVariables, tabletLandscapeFrom)),
|
|
63
|
+
tabletLandscape: (min-width: map.get($breakpointsVariables, tabletLandscapeFrom), max-width: map.get($breakpointsVariables, tabletLandscapeTo)),
|
|
64
|
+
tabletLandscapeTo: (max-width: map.get($breakpointsVariables, tabletLandscapeTo)),
|
|
65
|
+
desktopFrom: (min-width: map.get($breakpointsVariables, desktopFrom)),
|
|
66
|
+
desktop: $mediaqueryDesktop,
|
|
67
|
+
desktopTo: (max-width: map.get($breakpointsVariables, desktopTo)),
|
|
68
|
+
big: (min-width: map.get($breakpointsVariables, bigFrom)),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// Font
|
|
73
|
+
@mixin font($layout, $fontSize, $lineHeight, $bold:'false') {
|
|
74
|
+
|
|
75
|
+
font-size: size($layout, $fontSize);
|
|
76
|
+
line-height: size($layout, $lineHeight);
|
|
77
|
+
|
|
78
|
+
@if ($bold != 'false') {
|
|
79
|
+
font-weight: $bold;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
// columns
|
|
86
|
+
// Calculates the width (as a fraction of 100%)
|
|
87
|
+
// Per default the function assumes that the given space does not contain column gaps;
|
|
88
|
+
// with the variable $columnGapsAddedBasis they can be specified.
|
|
89
|
+
// $columnGapsAddedBasis is to be used when the space to be calculated contains additional column gaps outside the $columnsBasis.
|
|
90
|
+
// $columnGapsAdditional is to be used when additionally to the $columns column gaps are to be added (e.g left and/or right).
|
|
91
|
+
// $shift is for adding a value to the result, for example a margin
|
|
92
|
+
// $inverse is for calculating the width as a negative number, for example for a negative shift in a stage system
|
|
93
|
+
@function columns($layout, $columns, $columnsBasis, $columnGapsAddedBasis:0, $columnGapsAdditional:0, $margin:0, $shift:0, $invers:false) {
|
|
94
|
+
$columnGap: size($layout, map.get(config.$gutter, $layout) );
|
|
95
|
+
$margin: size($layout, $margin);
|
|
96
|
+
$shift: size($layout, $shift);
|
|
97
|
+
@if ( $invers == false ) {
|
|
98
|
+
$invers: 1;
|
|
99
|
+
} @else {
|
|
100
|
+
$invers: -1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// available space
|
|
104
|
+
// minus column gaps within the space
|
|
105
|
+
// equals space for content
|
|
106
|
+
// divided by number of columns in space
|
|
107
|
+
// equals width of each column
|
|
108
|
+
// times number of desired columns
|
|
109
|
+
// equals width of desired columns
|
|
110
|
+
// plus column gaps between desired columns
|
|
111
|
+
// equals width of desired columns including gaps
|
|
112
|
+
// plus additional column gaps
|
|
113
|
+
@return calc(
|
|
114
|
+
(
|
|
115
|
+
(
|
|
116
|
+
// available space
|
|
117
|
+
100%
|
|
118
|
+
// all column gaps in the given space
|
|
119
|
+
- $margin - $columnGap * ($columnsBasis + $columnGapsAddedBasis - 1)
|
|
120
|
+
)
|
|
121
|
+
// number of columns in the given space
|
|
122
|
+
/ $columnsBasis
|
|
123
|
+
// desired columns
|
|
124
|
+
* ($columns * $invers)
|
|
125
|
+
// columns gaps between desired columns plus additional column gaps
|
|
126
|
+
// inverse to get a negative value
|
|
127
|
+
+ ($columnGap * ($columns - 1 + $columnGapsAdditional) * $invers)
|
|
128
|
+
// Shift
|
|
129
|
+
+ ($shift * $invers)
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
// Size
|
|
137
|
+
// converts a number to a size in px or vw
|
|
138
|
+
// depending on the unit set for the layout
|
|
139
|
+
// accepts single value or a list of values
|
|
140
|
+
@function size($layout, $numbers) {
|
|
141
|
+
// Check whether second argument is a number or a list
|
|
142
|
+
@if (meta.type-of($numbers) == 'list') {
|
|
143
|
+
$result-list: (); // Empty list to store results
|
|
144
|
+
|
|
145
|
+
// Iterate over each element in the list
|
|
146
|
+
@each $number in $numbers {
|
|
147
|
+
// Test if the current element is a number
|
|
148
|
+
@if (meta.type-of($number) == 'number') {
|
|
149
|
+
// add calculated value to the result list
|
|
150
|
+
$result-list: list.append($result-list, _calculate-single-size($number, $layout));
|
|
151
|
+
} @else {
|
|
152
|
+
// Error if the current element is not a number
|
|
153
|
+
@error "Listenelement '#{$number}' übergeben an size() ist keine Zahl. Alle Elemente müssen Zahlen sein.";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
@return $result-list; // Return the list of calculated sizes
|
|
157
|
+
} @else if (meta.type-of($numbers) == 'number') {
|
|
158
|
+
// If second argument is a single number, calculate and return the size
|
|
159
|
+
@return _calculate-single-size($numbers, $layout);
|
|
160
|
+
} @else {
|
|
161
|
+
// Error if the second argument is neither a number nor a list
|
|
162
|
+
@error "Das zweite Argument der Funktion size() muss eine Zahl oder eine Liste von Zahlen sein. Empfangen: #{type-of($numbers)} '#{$numbers}'.";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Round a number to specified decimal places
|
|
167
|
+
@function _decimal-round($number, $digits: 0) {
|
|
168
|
+
$n: math.pow(10, $digits);
|
|
169
|
+
@return math.div(math.round($number * $n), $n);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Helper function to calculate a single size value (px or vw)
|
|
173
|
+
// This function is used by the main size() function
|
|
174
|
+
@function _calculate-single-size($number, $layout) {
|
|
175
|
+
// Get the width and unit type from the config
|
|
176
|
+
$width: map.get(config.$layouts, $layout);
|
|
177
|
+
$unit-type: map.get(config.$units, $layout);
|
|
178
|
+
|
|
179
|
+
// Error handling for missing width or unit type
|
|
180
|
+
@if ($width == null) {
|
|
181
|
+
@error "Layout '#{$layout}' ist nicht in 'config.$layouts' definiert.";
|
|
182
|
+
}
|
|
183
|
+
@if ($unit-type == null) {
|
|
184
|
+
@error "Der Einheitentyp für Layout '#{$layout}' ist nicht in 'config.$units' definiert.";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@if ($unit-type == 'px') {
|
|
189
|
+
@return $number * 1px;
|
|
190
|
+
} @else if ($unit-type == 'vw') {
|
|
191
|
+
@return _decimal-round(math.div($number, $width) * 100, 2) * 1vw;
|
|
192
|
+
} @else {
|
|
193
|
+
@error "Nicht unterstützter Einheitstyp '#{$unit-type}' für Layout '#{$layout}'. Nur 'px' oder 'vw' werden unterstützt.";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// Distance
|
|
199
|
+
@mixin marginPadding($layout, $mode, $position, $top: 0, $right: 0, $bottom: 0, $left: 0) {
|
|
200
|
+
|
|
201
|
+
// @include mode($layout, padding/margin, all/top/right/bottom/left, 0, 0, 0, 0)
|
|
202
|
+
|
|
203
|
+
@if ( map.get(config.$units, $layout) == 'px' ) {
|
|
204
|
+
$top: $top + px;
|
|
205
|
+
$right: $right + px;
|
|
206
|
+
$bottom: $bottom + px;
|
|
207
|
+
$left: $left + px;
|
|
208
|
+
} @else {
|
|
209
|
+
|
|
210
|
+
@if meta.type-of($top) == 'number'{
|
|
211
|
+
$top: size($layout, $top);
|
|
212
|
+
}
|
|
213
|
+
@if meta.type-of($right) == 'number'{
|
|
214
|
+
$right: size($layout, $right);
|
|
215
|
+
}
|
|
216
|
+
@if meta.type-of($bottom) == 'number'{
|
|
217
|
+
$bottom: size($layout, $bottom);
|
|
218
|
+
}
|
|
219
|
+
@if meta.type-of($left) == 'number'{
|
|
220
|
+
$left: size($layout, $left);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@if $position == all {
|
|
225
|
+
#{$mode}: $top $right $bottom $left;
|
|
226
|
+
}
|
|
227
|
+
@if $position == top {
|
|
228
|
+
#{$mode}-top: $top;
|
|
229
|
+
}
|
|
230
|
+
@if $position == right {
|
|
231
|
+
#{$mode}-right: $right;
|
|
232
|
+
}
|
|
233
|
+
@if $position == bottom {
|
|
234
|
+
#{$mode}-bottom: $bottom;
|
|
235
|
+
}
|
|
236
|
+
@if $position == left {
|
|
237
|
+
#{$mode}-left: $left;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@use "layout";
|
|
2
|
+
|
|
3
|
+
@use "sass:map";
|
|
4
|
+
@use "sass:list";
|
|
5
|
+
|
|
6
|
+
// Zusammenlegen aller Meda Queries in eine
|
|
7
|
+
// https://www.sitepoint.com/sass-mixin-media-merging
|
|
8
|
+
|
|
9
|
+
@mixin mediaquery($mediaquery) {
|
|
10
|
+
// Hole die map $mediaquery aus der map $mediaqueries
|
|
11
|
+
$queries: map.get(layout.$mediaqueries, $mediaquery);
|
|
12
|
+
|
|
13
|
+
// Wenn es keine $mediaquery in der map gibt, gebe einen Fehler aus
|
|
14
|
+
@if not $queries {
|
|
15
|
+
@error "No value could be retrieved from `#{$mediaquery}`. "
|
|
16
|
+
+ "Please make sure it is defined in `$mediaqueries` map.";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Include the media mixin with $queries
|
|
20
|
+
@include media($queries) {
|
|
21
|
+
@content($mediaquery);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@mixin media($queries) {
|
|
26
|
+
// Erst wenn alle mediaqueries geschachtelt geschrieben sind,
|
|
27
|
+
// wird der @content ausgegeben
|
|
28
|
+
@if list.length($queries) == 0 {
|
|
29
|
+
@content;
|
|
30
|
+
} @else {
|
|
31
|
+
$first-key: list.nth(map.keys($queries), 1);
|
|
32
|
+
|
|
33
|
+
// Schreibt nur die mediaquery, nicht den Inhalt
|
|
34
|
+
@media ($first-key: map.get($queries, $first-key)) {
|
|
35
|
+
$queries: map.remove($queries, $first-key);
|
|
36
|
+
|
|
37
|
+
@include media($queries) {
|
|
38
|
+
@content;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* after scroll set body attribute data-scrolled to true
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class BodyScrolled {
|
|
6
|
+
static #instance;
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
document.addEventListener('scroll', function setScrolled() {
|
|
10
|
+
document.body.setAttribute('data-body-scrolled', 'true');
|
|
11
|
+
this.removeEventListener('scroll', setScrolled);
|
|
12
|
+
|
|
13
|
+
// create event
|
|
14
|
+
let event = new CustomEvent('eventBodyScrolled', {
|
|
15
|
+
detail: {
|
|
16
|
+
scrolled: true
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
// dispatch the event
|
|
20
|
+
window.dispatchEvent(event);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static getInstance() {
|
|
26
|
+
if (!BodyScrolled.#instance) {
|
|
27
|
+
BodyScrolled.#instance = new BodyScrolled();
|
|
28
|
+
}
|
|
29
|
+
return BodyScrolled.#instance;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**Media queries
|
|
2
|
+
* https://kinsta.com/blog/javamediaqueryipt-media-query/
|
|
3
|
+
* option 3 on the linked page
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class MediaQueries {
|
|
7
|
+
static #instance;
|
|
8
|
+
layout = 'desktop';
|
|
9
|
+
#breakpoints;
|
|
10
|
+
|
|
11
|
+
constructor(breakpoints) {
|
|
12
|
+
this.#breakpoints = breakpoints;
|
|
13
|
+
this.layout = 'desktop';
|
|
14
|
+
this.changeLayout();
|
|
15
|
+
this.#matchmedia();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getInstance(breakpoints) {
|
|
19
|
+
if (!MediaQueries.#instance) {
|
|
20
|
+
if (!breakpoints) {
|
|
21
|
+
throw new Error("MediaQueries muss beim ersten Aufruf mit breakpoints initialisiert werden.");
|
|
22
|
+
}
|
|
23
|
+
MediaQueries.#instance = new MediaQueries(breakpoints);
|
|
24
|
+
}
|
|
25
|
+
return MediaQueries.#instance;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#matchmedia() {
|
|
29
|
+
for (let [layout, minSize] of Object.entries(this.#breakpoints)) {
|
|
30
|
+
if (minSize) {
|
|
31
|
+
var matchmedia = window.matchMedia('(min-width: ' + minSize + 'px)');
|
|
32
|
+
matchmedia.addEventListener('change', (e) => this.changeLayout());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// media query handler function
|
|
38
|
+
changeLayout() {
|
|
39
|
+
for (let [layout, minSize] of Object.entries(this.#breakpoints)) {
|
|
40
|
+
var matchmedia = window.matchMedia('(min-width: ' + minSize + 'px)');
|
|
41
|
+
if (!matchmedia || matchmedia.matches) {
|
|
42
|
+
this.layout = layout;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
document.body.setAttribute('data-layout', this.layout);
|
|
46
|
+
|
|
47
|
+
// create event
|
|
48
|
+
let event = new CustomEvent('eventLayoutchange', {
|
|
49
|
+
detail: {
|
|
50
|
+
layout: this.layout
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// dispatch the event
|
|
54
|
+
window.dispatchEvent(event);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/utils/Vh100.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 100vh problem
|
|
3
|
+
* https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class Vh100 {
|
|
7
|
+
static #instance;
|
|
8
|
+
vh = 0;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.#calculate();
|
|
12
|
+
// We listen to the resize event
|
|
13
|
+
window.addEventListener('resize', () => {
|
|
14
|
+
this.#calculate();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#calculate() {
|
|
19
|
+
// First we get the viewport height and we multiple it by 1% to get a value for a vh unit
|
|
20
|
+
this.vh = window.innerHeight * 0.01;
|
|
21
|
+
// Then we set the value in the --vh custom property to the root of the document
|
|
22
|
+
document.documentElement.style.setProperty('--vh', `${this.vh}px`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static getInstance() {
|
|
26
|
+
if (!Vh100.#instance) {
|
|
27
|
+
Vh100.#instance = new Vh100();
|
|
28
|
+
}
|
|
29
|
+
return Vh100.#instance;
|
|
30
|
+
}
|
|
31
|
+
}
|