UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

389 lines (388 loc) 16.5 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var ESLCarouselAutoplayMixin_1; import { ExportNs } from '../../../esl-utils/environment/export-ns'; import { listen, memoize } from '../../../esl-utils/decorators'; import { parseTime } from '../../../esl-utils/misc/format'; import { CSSClassUtils } from '../../../esl-utils/dom/class'; import { ESLMediaRuleList } from '../../../esl-media-query/core'; import { ESLIntersectionTarget, ESLIntersectionEvent } from '../../../esl-event-listener/core'; import { ESLCarouselPlugin } from '../esl-carousel.plugin'; import { ESLCarouselSlideEvent } from '../../core/esl-carousel.events'; import { ESLCarouselAutoplayEvent } from './esl-carousel.autoplay.event'; const isUserReason = (reason) => reason.startsWith('user:'); /** * Autoplay plugin mixin for {@link ESLCarousel}. * Schedules slide navigation by timeout while allowed by viewport, interaction and config constraints. */ let ESLCarouselAutoplayMixin = ESLCarouselAutoplayMixin_1 = class ESLCarouselAutoplayMixin extends ESLCarouselPlugin { constructor() { super(...arguments); /** Manual stop flag */ this._stoppedByUser = false; /** Manual pause flag */ this._pausedByUser = false; /** Last known viewport intersection state */ this._inViewport = false; /** Active cycle timeout id (null if no cycle scheduled) */ this._timeout = null; /** Remaining time until the current cycle ends */ this._remaining = 0; /** Current cycle full duration */ this._cycleDuration = 0; /** Current cycle scheduling start timestamp */ this._cycleStartedAt = null; /** Last dispatch reason */ this._lastReason = 'system:idle'; /** Last dispatched state snapshot key to suppress duplicate events */ this._lastDispatchKey = ''; } /** True when a navigation timeout is currently scheduled */ get active() { return !!this._timeout; } /** True when autoplay is paused explicitly by user action and can potentially be resumed */ get paused() { return this.enabled && !this.active && this._pausedByUser; } /** Exclusive summary state for autoplay runtime */ get state() { if (!this.enabled) return 'disabled'; if (this.paused) return 'paused'; if (this.blocked) return 'blocked'; if (this.active) return 'active'; return 'idle'; } /** True when autoplay cannot run due to runtime blockers */ get blocked() { if (!this._inViewport) return true; if (this.config.trackInteraction && (this.hovered || this.focused)) return true; return this.hasActiveBlockingItems; } /** * Effective enabled state. * True when autoplay is not manually stopped and global duration is non-negative / valid. */ get enabled() { return !this._stoppedByUser && this.duration >= 0; } /** Backward-compatible manual enable / disable API */ set enabled(value) { value ? this.start() : this.stop(); } /** Global base duration in ms (raw config parsed). Negative / NaN considered as disabled */ get duration() { return parseTime(this.config.duration); } /** * Effective current slide duration. * Tries active slide attribute; falls back to global duration. * Non-positive result pauses cycle for the slide only (unless global invalid disables plugin). */ get effectiveDuration() { const { $activeSlide } = this.$host; if (!$activeSlide) return this.duration; const value = $activeSlide.getAttribute(ESLCarouselAutoplayMixin_1.SLIDE_DURATION_ATTRIBUTE); if (!value) return this.duration; const parsed = ESLMediaRuleList.parse(value, this.$host.media, parseTime); if (typeof parsed.value === 'undefined' || isNaN(parsed.value)) return this.duration; return parsed.value; } /** Remaining time of the current/paused cycle */ get remaining() { if (!this.active || this._cycleStartedAt === null) return this._remaining; return Math.max(this._remaining - (Date.now() - this._cycleStartedAt), 0); } /** Interaction scope elements used as event subscription targets (memoized) */ get $interactionScope() { const sel = this.config.interactionScope; return sel ? this.$$findAll(sel) : [this.$host]; } /** Effective interaction scope after exclusion rules are applied */ get $effectiveInteractionScope() { const exclude = this.config.interactionScopeExclude; if (!exclude) return this.$interactionScope; return this.$interactionScope.filter(($el) => !$el.matches(exclude)); } /** True if active slide contains any blocking items */ get hasActiveBlockingItems() { const { blockerSelector } = this.config; return !!blockerSelector && !!this.$$find(blockerSelector); } /** True if any scope element is hovered */ get hovered() { return this.$effectiveInteractionScope.some(($el) => $el.matches('*:hover')); } /** True if keyboard-visible focus is within scope */ get focused() { var _a; if (!((_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.matches('*:focus-visible'))) return false; return this.$effectiveInteractionScope.some(($el) => $el.matches('*:focus-within')); } /** Backward-compatible alias for runtime allowance */ get allowed() { return this.canRun; } /** Runtime predicate: autoplay may have an active timeout right now */ get canRun() { var _a; if (!this.enabled || this._pausedByUser || this.blocked) return false; if (!(this.effectiveDuration > 0)) return false; return !!((_a = this.$host) === null || _a === void 0 ? void 0 : _a.canNavigate(this.config.command)); } /** True if autoplay can be scheduled for the current slide */ get canSchedule() { var _a; const { effectiveDuration } = this; return effectiveDuration > 0 && !!((_a = this.$host) === null || _a === void 0 ? void 0 : _a.canNavigate(this.config.command)); } /** True if autoplay may be re-started automatically by runtime conditions */ get canAutoStart() { return !this._pausedByUser && !this._stoppedByUser && !this.active && this.canRun; } /** True if runtime listeners should sync blocking state right now */ get shouldProcessBlockingState() { return this.enabled || this.active || this._pausedByUser; } /** Start autoplay or resume paused cycle */ start(reason = 'user:start:call') { if (isUserReason(reason)) { this._stoppedByUser = false; this._pausedByUser = false; } if (!this.enabled || this.blocked || this._pausedByUser) return this.syncState(reason); if (this.active) return this.syncState(reason); if (!this.canSchedule) { this._cycleDuration = Math.max(this.effectiveDuration, 0); this._remaining = 0; return this.syncState(isUserReason(reason) ? reason : 'system:idle'); } const duration = this.effectiveDuration; const remaining = this._remaining > 0 ? this._remaining : duration; const total = this._cycleDuration > 0 ? this._cycleDuration : duration; this.schedule(remaining, total, reason); } /** Pause autoplay preserving remaining time when possible */ pause(reason = 'user:pause:call') { if (isUserReason(reason)) { this._stoppedByUser = false; this._pausedByUser = true; } if (this.active) this._remaining = this.remaining; this.clearTimer(); this.syncState(reason); } /** Stop autoplay and clear current cycle state */ stop(reason = 'user:stop:call') { if (isUserReason(reason)) { this._stoppedByUser = true; this._pausedByUser = false; } this.resetCycle(); this.syncState(reason); } /** Toggle autoplay state according to the specified control behaviour */ toggle(behaviour = 'stop') { if (behaviour === 'pause') { return this._pausedByUser ? this.start('user:start:control') : this.pause('user:pause:control'); } return (this._stoppedByUser || !this.enabled) ? this.start('user:start:control') : this.stop('user:stop:control'); } /** Init lifecycle hook */ onInit() { this.update(); } /** React to config changes (rebuild memoized queries, re-evaluate state) */ onConfigChange() { super.onConfigChange(); memoize.clear(this, '$interactionScope'); this.$$off(this._onBlockingEvent); this.stop('system:stop:config'); this.update(); if (!this._pausedByUser && !this._stoppedByUser && this.canRun) this.start('system:start:auto'); } /** Suspend & cleanup on disconnect */ disconnectedCallback() { this.clearTimer(); this._remaining = 0; this._cycleDuration = 0; this._cycleStartedAt = null; this.updateMarkers(); super.disconnectedCallback(); } /** Update classes and listeners and synchronize state markers */ update() { this.$$on({ auto: true }); this.syncState(); } /** Update UI markers reflecting effective autoplay state */ updateMarkers() { const { $container } = this.$host; $container && CSSClassUtils.toggle($container, this.config.containerCls, this.enabled); } /** Reset current cycle state completely */ resetCycle() { this.clearTimer(); this._remaining = 0; this._cycleDuration = 0; } /** Clear active timer only */ clearTimer() { if (this._timeout) window.clearTimeout(this._timeout); this._timeout = null; this._cycleStartedAt = null; } /** Schedule next autoplay cycle */ schedule(duration, total = duration, reason = 'system:start:auto') { this.clearTimer(); this._remaining = duration; this._cycleDuration = total; this._cycleStartedAt = Date.now(); this._timeout = window.setTimeout(() => this._onCycle(true), duration); this.syncState(reason); } /** Sync current state markers and dispatch autoplay event */ syncState(reason = this._lastReason) { this._lastReason = reason; const { enabled, paused, blocked, active, state, effectiveDuration } = this; const duration = effectiveDuration > 0 ? effectiveDuration : 0; const remaining = this.active ? this._remaining : this.remaining; const event = new ESLCarouselAutoplayEvent({ enabled, paused, blocked, active, state, duration, remaining, reason }); const dispatchKey = event.toFingerprint(); if (dispatchKey === this._lastDispatchKey) return; this._lastDispatchKey = dispatchKey; this.updateMarkers(); this.$host.dispatchEvent(event); } /** Apply blocking logic according to configured block behaviour */ syncBlockingState() { if (this.blocked) { return this.config.blockBehaviour === 'pause' ? this.pause('system:pause:block') : this.stop('system:stop:block'); } if (this.canAutoStart) return this.start('system:start:auto'); const isIdling = !this.active && this.enabled && !this._pausedByUser; this.syncState(isIdling ? 'system:idle' : this._lastReason); } /** Internal cycle handler (execute navigation then schedule next cycle if needed) */ _onCycle(exec) { return __awaiter(this, void 0, void 0, function* () { var _a; this.clearTimer(); this._remaining = 0; if (exec) yield ((_a = this.$host) === null || _a === void 0 ? void 0 : _a.goTo(this.config.command, { activator: this }).catch(console.debug)); if (this.active || !this.enabled || this._pausedByUser || this.blocked) return this.syncState(); if (this.canSchedule) { const { effectiveDuration } = this; return this.schedule(effectiveDuration, effectiveDuration, 'system:start:auto'); } this.syncState('system:idle'); }); } /** Viewport intersection listener controlling runtime allowance */ _onIntersection(e) { this._inViewport = e.isIntersecting; if (!this.shouldProcessBlockingState) return; this.syncBlockingState(); } /** Hover/focus interaction listener toggling pause state */ _onInteract() { if (!this.shouldProcessBlockingState) return; this.syncBlockingState(); } /** Slide change listener (forces cycle restart) */ _onSlideChange() { this.stop('system:stop:slide-change'); if (this.canAutoStart) this.start('system:start:auto'); } /** Subscribe to events that block autoplay */ _onBlockingEvent() { if (!this.shouldProcessBlockingState) return; this.syncBlockingState(); } }; ESLCarouselAutoplayMixin.is = 'esl-carousel-autoplay'; ESLCarouselAutoplayMixin.DEFAULT_CONFIG = { duration: 10000, command: 'slide:next', intersection: 0.25, trackInteraction: true, blockBehaviour: 'stop', blockerSelector: '::find(esl-share[active], esl-note[active])', watchEvents: 'esl:change:active' }; ESLCarouselAutoplayMixin.DEFAULT_CONFIG_KEY = 'duration'; /** Per-slide override attribute name for timeout */ ESLCarouselAutoplayMixin.SLIDE_DURATION_ATTRIBUTE = ESLCarouselAutoplayMixin_1.is + '-timeout'; __decorate([ memoize() ], ESLCarouselAutoplayMixin.prototype, "$interactionScope", null); __decorate([ listen({ inherit: true }) ], ESLCarouselAutoplayMixin.prototype, "onConfigChange", null); __decorate([ listen({ event: ESLIntersectionEvent.TYPE, target: ($this) => ESLIntersectionTarget.for($this.$host, { threshold: [$this.config.intersection] }) }) ], ESLCarouselAutoplayMixin.prototype, "_onIntersection", null); __decorate([ listen({ event: 'mouseleave mouseenter focusin focusout', target: ($this) => $this.$interactionScope, condition: ($this) => $this.config.trackInteraction }) ], ESLCarouselAutoplayMixin.prototype, "_onInteract", null); __decorate([ listen(ESLCarouselSlideEvent.AFTER) ], ESLCarouselAutoplayMixin.prototype, "_onSlideChange", null); __decorate([ listen({ event: ($this) => $this.config.watchEvents, target: document, condition: ($this) => !!$this.config.watchEvents }) ], ESLCarouselAutoplayMixin.prototype, "_onBlockingEvent", null); ESLCarouselAutoplayMixin = ESLCarouselAutoplayMixin_1 = __decorate([ ExportNs('Carousel.Autoplay') ], ESLCarouselAutoplayMixin); export { ESLCarouselAutoplayMixin };