-
Notifications
You must be signed in to change notification settings - Fork 1
/
wc-responsive-container.js
139 lines (121 loc) · 4.33 KB
/
wc-responsive-container.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
* wc-responsive-container
* by pinkhominid
* MIT Licensed
*
* See Philip Walton's Responsive Components: a Solution to the Container Queries Problem
* https://philipwalton.com/articles/responsive-components-a-solution-to-the-container-queries-problem/
*
* TODO
* - ResizeObserver singleton
*/
const debounce = fn => {
let id;
return (...args) => {
if(id != null) clearTimeout(id);
id = setTimeout(
() => {
fn.apply(null, args);
id = null;
},
200
);
};
};
// https://www.30secondsofcode.org/blog/s/javascript-memoization
const memoize = fn => new Proxy(fn, {
cache: new Map(),
apply (target, thisArg, argsList) {
let cacheKey = argsList.toString();
if(!this.cache.has(cacheKey)) {
this.cache.set(cacheKey, target.apply(thisArg, argsList));
}
return this.cache.get(cacheKey);
}
});
const onResize = entries =>
entries.forEach(entry =>
requestAnimationFrame(() => {
// If breakpoints are defined on the observed element,
// use them. Otherwise use the defaults.
const breaksAttr = entry.target.getAttribute('breaks');
const breakpoints = breaksAttr
? memoizedJSONParse(breaksAttr)
: defaultBreakpoints;
const breaksAddList = [];
const breaksRemoveList = [];
const overflownAddList = [];
const overflownRemoveList = [];
// Update the matching breakpoints on the observed element
Object.entries(breakpoints).forEach(([breakpoint, minWidth]) => {
const gteMin = entry.contentRect.width >= minWidth;
const hasBreakClass = entry.target.classList.contains(breakpoint);
if (gteMin && !hasBreakClass) {
breaksAddList.push(breakpoint);
} else if (!gteMin && hasBreakClass) {
breaksRemoveList.push(breakpoint);
}
});
updateClassList(entry.target, breaksRemoveList, breaksAddList);
// mark overflown (in second pass)
const isOverflown = isElemOverflown(entry.target);
const hasOverflownClass = entry.target.classList.contains('overflown');
if (isOverflown && !hasOverflownClass){
overflownAddList.push('overflown');
} else if (!isOverflown && hasOverflownClass) {
overflownRemoveList.push('overflown');
}
updateClassList(entry.target, overflownRemoveList, overflownAddList);
// call event handler, if present
if (entry.target.onResize) entry.target.onResize();
})
);
const isElemOverflown = ({ clientWidth, scrollWidth }) => scrollWidth > clientWidth;
const updateClassList = (target, removeList, addList) => {
if (removeList.length) {
target.classList.remove(...removeList);
}
if (addList.length) {
target.classList.add(...addList);
}
forceChildrenLayoutRecalcIfNeeded(target);
};
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const forceChildrenLayoutRecalcIfNeeded = target => {
if (!isSafari) return;
// Safari needs this, unfortunately, to refresh it's styling
Array.from(target.children).forEach(child => {
child.style.zoom = window.getComputedStyle(child).zoom;
})
};
const defaultBreakpoints = {sm: 384, md: 576, lg: 768, xl: 960};
const roPropName = '__wc-responsive-container-ro';
const memoizedJSONParse = memoize(JSON.parse);
// Add responsive behavior to any element
export const ResponsiveElementMixin = (superclass) =>
class extends superclass {
connectedCallback() {
if(super.connectedCallback) super.connectedCallback();
if (!this[roPropName]) {
this[roPropName] = new ResizeObserver(debounce(onResize));
}
this[roPropName].observe(this);
}
disconnectedCallback() {
this[roPropName].unobserve(this);
if(super.disconnectedCallback) super.disconnectedCallback();
}
};
export class ResponsiveContainerElement extends ResponsiveElementMixin(HTMLElement) {
connectedCallback() {
super.connectedCallback();
// Default to display block if inline
// TODO: find a low specificity way w/o extra friction (shadow DOM)
if (getComputedStyle(this).display === 'inline') this.style.display = 'block';
}
}
if (self.customElements.get('wc-responsive-container')) {
self.console.warn(`'wc-responsive-container' has already been defined as a custom element`);
} else {
self.customElements.define('wc-responsive-container', ResponsiveContainerElement);
}