UNPKG

19.4 kBJavaScriptView Raw
1import WUPBaseElement from "./baseElement";
2import { px2Number, styleTransform } from "./helpers/styleHelpers";
3import { getOffset } from "./popup/popupPlacements";
4const tagName = "wup-spin";
5/** Flexible animated element with ability to place over target element without position relative
6 * @tutorial Troubleshooting
7 * * when used several spin-types at once: define `--spin-1` & `--spin-2` colors manually per each spin-type
8 * @example
9 * JS/TS
10 * ```js
11 * import WUPSpinElement, { spinUseTwinDualRing } from "web-ui-pack/spinElement";
12 * spinUseTwinDualRing(WUPSpinElement); // to apply another style
13 *
14 * const el = document.body.appendChild(document.createElement('wup-spin'));
15 * el.$options.inline = false;
16 * el.$options.overflowTarget = document.body.appendChild(document.createElement('button'))
17 * ```
18 * HTML
19 * ```html
20 * <button> Loading...
21 * <!-- Default; it's equal to <wup-spin></wup-spin>-->
22 * <wup-spin inline="false" fit="true" overflowfade="true"></wup-spin>
23 * <!-- Inline + fit to parent -->
24 * <wup-spin inline fit></wup-spin>
25 * <!-- OR; it's equal to <wup-spin inline="false" fit="true" overflowfade="false"></wup-spin> -->
26 * <wup-spin overflowfade="false"></wup-spin>
27 * </button>
28 * ```
29 */
30export default class WUPSpinElement extends WUPBaseElement {
31 #ctr = this.constructor;
32 static get observedOptions() {
33 return ["inline", "overflowTarget", "overflowOffset", "overflowFade", "fit"];
34 }
35 static get observedAttributes() {
36 return ["inline", "overflowfade", "fit"];
37 }
38 static get $styleRoot() {
39 return `:root {
40 --spin-1: #ffa500;
41 --spin-2: #fff;
42 --spin-speed: 1.2s;
43 --spin-size: 3em;
44 --spin-item-size: calc(var(--spin-size) / 8);
45 --spin-fade: rgba(255,255,255,0.43);
46 }`;
47 }
48 /* c8 ignore next 3 */
49 /* istanbul ignore next 3 */
50 static get $styleApplied() {
51 return "";
52 }
53 static get $style() {
54 return `${super.$style}
55 @keyframes WUP-SPIN-1 {
56 100% { transform: rotate(360deg); }
57 }
58 :host {
59 contain: style;
60 z-index: 100;
61 width: var(--spin-size);
62 height: var(--spin-size);
63 top:0; left:0;
64 pointer-events: none;
65 }
66 :host,
67 :host>div {
68 display: inline-block;
69 box-sizing: border-box;
70 border-radius: 50%;
71 }
72 :host>div {
73 animation: WUP-SPIN-1 var(--spin-speed) linear infinite;
74 width: 100%; height: 100%;
75 left:0; top:0;
76 }
77 :host>div[fade] {
78 display: block;
79 position: absolute;
80 left:0; top:0;
81 animation: none;
82 border: none;
83 border-radius: var(--border-radius);
84 transform: none;
85 z-index: -1;
86 background: var(--spin-fade);
87 }
88 :host>div[fade]::after { content: none; }
89 ${this.$styleApplied}`;
90 }
91 static $defaults = {
92 overflowOffset: [4, 4],
93 overflowFade: true,
94 };
95 static _itemsCount = 1;
96 /** Force to update position (when options changed) */
97 $refresh() {
98 this.gotChanges([]);
99 }
100 connectedCallback() {
101 this.style.display = "none"; // required to prevent unexpected wrong-render (tied with empty timeout)
102 super.connectedCallback();
103 }
104 gotRemoved() {
105 super.gotRemoved();
106 this.#frameId && window.cancelAnimationFrame(this.#frameId);
107 this.#frameId = undefined;
108 if (this.#prevTarget?.isConnected) {
109 // otherwise removing attribute doesn't make sense
110 this.#prevTarget.removeAttribute("aria-busy");
111 this.#prevTarget = undefined;
112 }
113 }
114 gotRender() {
115 for (let i = 0; i < this.#ctr._itemsCount; ++i) {
116 this.appendChild(document.createElement("div"));
117 }
118 }
119 gotReady() {
120 this.setAttribute("aria-label", "Loading. Please wait");
121 super.gotReady();
122 }
123 #prevTarget;
124 $refFade;
125 gotChanges(propsChanged) {
126 super.gotChanges(propsChanged);
127 this._opts.inline = this.getAttr("inline", "bool");
128 this._opts.fit = this.getAttr("fit", "bool", this._opts.fit ?? !this._opts.inline);
129 this._opts.overflowFade = this.getAttr("overflowfade", "bool", this._opts.overflowFade);
130 this.style.cssText = "";
131 this.#prevRect = undefined;
132 this.#frameId && window.cancelAnimationFrame(this.#frameId);
133 this.#frameId = undefined;
134 const nextTarget = this.target;
135 if (this.#prevTarget !== nextTarget) {
136 this.#prevTarget?.removeAttribute("aria-busy");
137 nextTarget.setAttribute("aria-busy", true);
138 this.#prevTarget = nextTarget;
139 }
140 if (!this._opts.inline) {
141 if (this._opts.overflowFade && !this.$refFade) {
142 this.$refFade = this.appendChild(document.createElement("div"));
143 this.$refFade.setAttribute("fade", "");
144 const s = getComputedStyle(this.target);
145 this.$refFade.style.borderTopLeftRadius = s.borderTopLeftRadius;
146 this.$refFade.style.borderTopRightRadius = s.borderTopRightRadius;
147 this.$refFade.style.borderBottomLeftRadius = s.borderBottomLeftRadius;
148 this.$refFade.style.borderBottomRightRadius = s.borderBottomRightRadius;
149 }
150 else if (!this._opts.overflowFade && this.$refFade) {
151 this.$refFade.remove();
152 this.$refFade = undefined;
153 }
154 this.style.position = "absolute";
155 const goUpdate = () => {
156 this.#prevRect = this.updatePosition();
157 // possible if hidden by target-remove
158 this.#frameId = window.requestAnimationFrame(goUpdate);
159 };
160 goUpdate();
161 }
162 else {
163 this.style.transform = "";
164 this.style.position = "";
165 this.$refFade?.remove();
166 this.$refFade = undefined;
167 if (this._opts.fit) {
168 const goUpdate = () => {
169 this.style.display = "none";
170 const p = this.parentElement;
171 const r = { width: p.clientWidth, height: p.clientHeight, left: 0, top: 0 };
172 this.style.display = "";
173 if (this.#prevRect && this.#prevRect.width === r.width && this.#prevRect.height === r.height) {
174 return;
175 }
176 this.style.cssText = ""; // otherwise getPropertyValue is wrong
177 const ps = getComputedStyle(p);
178 const { paddingTop, paddingLeft, paddingBottom, paddingRight } = ps;
179 const innW = r.width - px2Number(paddingLeft) - px2Number(paddingRight);
180 const innH = r.height - px2Number(paddingTop) - px2Number(paddingBottom);
181 let sz = Math.min(innH, innW);
182 sz -= sz % 2; // 17px => 16px: Safari issue > placed wrong with odd width
183 const varItemSize = ps.getPropertyValue("--spin-item-size");
184 const scale = Math.min(sz / this.clientWidth, 1);
185 // styleTransform(this, "scale", scale === 1 ? "" : `${scale}`); // wrong because it doesn't affect on the layout size
186 // this.style.zoom = scale; // zoom isn't supported by FireFox
187 this.style.cssText = `--spin-size: ${sz}px; --spin-item-size: calc(${varItemSize} * ${scale})`;
188 // this.style.width = `${sz}px`;
189 // this.style.height = `${sz}px`;
190 this.#prevRect = r;
191 this.#frameId = window.requestAnimationFrame(goUpdate);
192 };
193 goUpdate();
194 }
195 }
196 }
197 get target() {
198 return (this._opts.inline ? this.parentElement : this._opts.overflowTarget || this.parentElement);
199 }
200 get hasRelativeParent() {
201 const p = this.offsetParent;
202 return !!p && getComputedStyle(p).position === "relative";
203 }
204 #prevRect;
205 #frameId;
206 /** Update position. Call this method in cases when you changed options */
207 updatePosition() {
208 const trg = this.target;
209 if (!trg.clientWidth || !trg.clientHeight) {
210 this.style.display = "none"; // hide if target is not displayed
211 return undefined;
212 }
213 this.style.display = "";
214 const r = { width: trg.offsetWidth, height: trg.offsetHeight, left: trg.offsetLeft, top: trg.offsetTop };
215 // when target has position:relative
216 if (this.offsetParent === trg) {
217 r.top = 0;
218 r.left = 0;
219 }
220 else if (!this.hasRelativeParent) {
221 const { top, left } = trg.getBoundingClientRect();
222 r.top = top;
223 r.left = left;
224 }
225 if (this.#prevRect &&
226 this.#prevRect.top === r.top &&
227 this.#prevRect.left === r.left &&
228 this.#prevRect.width === r.width &&
229 this.#prevRect.height === r.height) {
230 return this.#prevRect;
231 }
232 const offset = getOffset(this._opts.overflowOffset);
233 this.style.display = "";
234 const w = r.width - offset.left - offset.right;
235 const h = r.height - offset.top - offset.bottom;
236 const scale = this._opts.fit ? Math.min(Math.min(h, w) / this.clientWidth, 1) : 1;
237 const left = Math.round(r.left + offset.left + (w - this.clientWidth) / 2);
238 const top = Math.round(r.top + offset.top + (h - this.clientHeight) / 2);
239 styleTransform(this, "translate", `${left}px,${top}px`); // WARN: parent transform not affects on element how it works in popup
240 styleTransform(this, "scale", scale === 1 ? "" : `${scale}`);
241 if (this.$refFade) {
242 styleTransform(this.$refFade, "translate", `${r.left - left}px,${r.top - top}px`);
243 styleTransform(this.$refFade, "scale", scale === 1 || !scale ? "" : `${1 / scale}`);
244 this.$refFade.style.width = `${r.width}px`;
245 this.$refFade.style.height = `${r.height}px`;
246 }
247 return r;
248 }
249}
250spinUseRing(WUPSpinElement);
251customElements.define(tagName, WUPSpinElement);
252/** Basic function to change spinner-style */
253export function spinSetStyle(cls, itemsCount, getter) {
254 cls._itemsCount = itemsCount;
255 Object.defineProperty(cls, "$styleApplied", {
256 configurable: true,
257 get: getter,
258 });
259}
260/** Apply on class to change spinner-style */
261export function spinUseRing(cls) {
262 spinSetStyle(cls, 1, () => `:host>div {
263 border: var(--spin-item-size) solid var(--spin-1);
264 border-top-color: var(--spin-2);
265 }`);
266}
267/** Apply on class to change spinner-style */
268export function spinUseDualRing(cls) {
269 spinSetStyle(cls, 1, () => `:root { --spin-2: transparent; }
270 :host>div {
271 border: var(--spin-item-size) solid;
272 border-color: var(--spin-2) var(--spin-1) var(--spin-2) var(--spin-1);
273 }`);
274}
275/** Apply on class to change spinner-style */
276export function spinUseTwinDualRing(cls) {
277 spinSetStyle(cls, 2, () => `@keyframes WUP-SPIN-2-2 {
278 0% { transform: translate(-50%, -50%) rotate(360deg); }
279 100% { transform: translate(-50%, -50%) rotate(0deg); }
280 }
281 :root {
282 --spin-2: #b35e03;
283 --spin-item-size: max(1px, calc(var(--spin-size) / 12));
284 }
285 :host { position: relative; }
286 :host>div:nth-child(1) {
287 border: var(--spin-item-size) solid;
288 border-color: transparent var(--spin-1) transparent var(--spin-1);
289 }
290 :host>div:nth-child(2) {
291 border: var(--spin-item-size) solid;
292 border-color: var(--spin-2) transparent var(--spin-2) transparent;
293 position: absolute;
294 width: calc(100% - var(--spin-item-size) * 3);
295 height: calc(100% - var(--spin-item-size) * 3);
296 left: 50%; top: 50%;
297 transform: translate(-50%,-50%);
298 animation: WUP-SPIN-2-2 var(--spin-speed) linear infinite;
299 }`);
300}
301/** Apply on class to change spinner-style */
302export function spinUseRoller(cls) {
303 const cnt = 4;
304 spinSetStyle(cls, cnt, () => {
305 let s = "";
306 for (let i = 1; i <= cnt - 1; ++i) {
307 s += `:host>div:nth-child(${i}) { animation-delay: -0.${15 * (cnt - i)}s }
308 `;
309 }
310 return `:host { position: relative; }
311 :host>div {
312 animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
313 position: absolute;
314 border: var(--spin-item-size) solid;
315 border-color: var(--spin-1) transparent transparent transparent;
316 }
317 ${s}`;
318 });
319}
320/** Apply on class to change spinner-style */
321export function spinUseDotRoller(cls) {
322 const cnt = 7;
323 spinSetStyle(cls, cnt, () => {
324 let s = "";
325 for (let i = 1; i <= cnt; ++i) {
326 s += `:host>div:nth-child(${i}) { animation-delay: -${0.036 * i}s; }
327 :host>div:nth-child(${i})::after { transform: rotate(calc(45deg + var(--spin-step) * ${i - 1})); }
328 `;
329 }
330 return `:root { --spin-step: 24deg; }
331 :host { position: relative; }
332 :host>div {
333 animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
334 position: absolute;
335 }
336 :host>div::after {
337 content: " ";
338 display: block;
339 position: absolute;
340 left: 0;
341 top: calc(50% - var(--spin-item-size) / 2);
342 transform-origin: calc(var(--spin-size) / 2);
343 width: var(--spin-item-size);
344 height: var(--spin-item-size);
345 border-radius: 50%;
346 background: var(--spin-1);
347 }
348 ${s}`;
349 });
350}
351/** Apply on class to change spinner-style */
352export function spinUseDotRing(cls) {
353 const cnt = 10;
354 spinSetStyle(cls, cnt, () => {
355 let s = "";
356 for (let i = 1; i <= cnt; ++i) {
357 s += `:host>div:nth-child(${i})::after { animation-delay: ${0.1 * (i - 1)}s; }
358 :host>div:nth-child(${i}) { transform: translate(-50%,-50%) rotate(${(360 / cnt) * (i - 1)}deg) }
359 `;
360 }
361 return `@keyframes WUP-SPIN-2 {
362 0%,20%,80%,100% { transform: scale(1); background: var(--spin-1) }
363 50% { transform: scale(1.4); background: var(--spin-2) }
364 }
365 :root { --spin-2: #ff5200; }
366 :host { position: relative; }
367 :host>div {
368 position: absolute;
369 width: calc(100% / 1.4142135623730951);
370 height: calc(100% / 1.4142135623730951);
371 animation: none;
372 top:50%; left:50%;
373 }
374 :host>div::after {
375 animation: WUP-SPIN-2 var(--spin-speed) linear infinite;
376 content: " ";
377 display: block;
378 width: var(--spin-item-size);
379 height: var(--spin-item-size);
380 border-radius: 50%;
381 background: var(--spin-1);
382 }
383 ${s}`;
384 });
385}
386/** Apply on class to change spinner-style */
387export function spinUseSpliceRing(cls) {
388 const cnt = 12;
389 spinSetStyle(cls, cnt, () => {
390 let s = "";
391 for (let i = 1; i <= cnt; ++i) {
392 s += `:host>div:nth-child(${i}) {
393 animation-delay: -${0.1 * (cnt - i)}s;
394 transform: rotate(${(360 / cnt) * (i - 1)}deg);
395 }
396 `;
397 }
398 return `@keyframes WUP-SPIN-3 {
399 100% { opacity: 0; background: var(--spin-2); }
400 }
401 :root { --spin-item-size: calc(var(--spin-size) / 10); }
402 :host { position: relative; }
403 :host>div {
404 animation: WUP-SPIN-3 var(--spin-speed) linear infinite;
405 position: absolute;
406 width: calc(var(--spin-size) / 4);
407 height: var(--spin-item-size);
408 left: 0;
409 top: calc(50% - var(--spin-item-size) / 2);
410 transform-origin: calc(var(--spin-size) / 2);
411 background: var(--spin-1);
412 border-radius: calc(var(--spin-item-size) / 2);
413 }
414 ${s}`;
415 });
416}
417/** Apply on class to change spinner-style */
418export function spinUseHash(cls) {
419 spinSetStyle(cls, 2, () => `@keyframes WUP-SPIN-4-1 {
420 0% {
421 width: var(--spin-item-size);
422 box-shadow: var(--spin-1) var(--spin-end) var(--spin-pad2), var(--spin-1) var(--spin-start) var(--spin-pad);
423 }
424 35% {
425 width: var(--spin-size);
426 box-shadow: var(--spin-1) 0 var(--spin-pad2), var(--spin-1) 0 var(--spin-pad);
427 }
428 70% {
429 width: var(--spin-item-size);
430 box-shadow: var(--spin-1) var(--spin-start) var(--spin-pad2), var(--spin-1) var(--spin-end) var(--spin-pad);
431 }
432 100% { box-shadow: var(--spin-1) var(--spin-end) var(--spin-pad2), var(--spin-1) var(--spin-start) var(--spin-pad); }
433 }
434 @keyframes WUP-SPIN-4-2 {
435 0% {
436 height: var(--spin-item-size);
437 box-shadow: var(--spin-2) var(--spin-pad) var(--spin-end), var(--spin-2) var(--spin-pad2) var(--spin-start);
438 }
439 35% {
440 height: var(--spin-size);
441 box-shadow: var(--spin-2) var(--spin-pad) 0, var(--spin-2) var(--spin-pad2) 0;
442 }
443 70% {
444 height: var(--spin-item-size);
445 box-shadow: var(--spin-2) var(--spin-pad) var(--spin-start), var(--spin-2) var(--spin-pad2) var(--spin-end);
446 }
447 100% { box-shadow: var(--spin-2) var(--spin-pad) var(--spin-end), var(--spin-2) var(--spin-pad2) var(--spin-start); }
448 }
449 :root {
450 --spin-2: #b35e03;
451 --spin-item-size: calc(var(--spin-size) / 8);
452 --spin-end: calc((var(--spin-size) - var(--spin-item-size)) / 2);
453 --spin-start: calc((var(--spin-end)) * -1);
454 --spin-pad: calc(var(--spin-size) / 2 - var(--spin-size) / 3 + var(--spin-item-size) / 3);
455 --spin-pad2: calc(-1 * var(--spin-pad));
456 }
457 :host {
458 position: relative;
459 padding: 3px;
460 }
461 :host>div {
462 position: absolute;
463 transform: translate(-50%, -50%) rotate(165deg);
464 top:50%; left:50%;
465 width: var(--spin-item-size);
466 height: var(--spin-item-size);
467 border-radius: calc(var(--spin-item-size) / 2);
468 }
469 :host>div:nth-child(1) {
470 animation: var(--spin-speed) ease 0s infinite normal none running WUP-SPIN-4-1;
471 }
472 :host>div:nth-child(2) {
473 animation: var(--spin-speed) ease 0s infinite normal none running WUP-SPIN-4-2;
474 }`);
475}
476
\No newline at end of file