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