From 7d5a47da53251c9a595e629acd5d1b2b787ed89e Mon Sep 17 00:00:00 2001 From: David Jerleke Date: Wed, 25 Dec 2024 23:06:30 +0100 Subject: [PATCH] Implement #202. --- .../src/components/Animations.ts | 16 ++-- .../embla-carousel/src/components/Axis.ts | 2 +- .../src/components/DragHandler.ts | 13 ++- .../src/components/DragTracker.ts | 12 ++- .../src/components/EmblaCarousel.ts | 86 ++++++++++------- .../embla-carousel/src/components/Engine.ts | 30 +++--- .../src/components/NodeHandler.ts | 94 +++++++++++++++++++ .../src/components/NodeRects.ts | 33 ------- .../embla-carousel/src/components/Options.ts | 4 +- .../src/components/OptionsHandler.ts | 20 +++- .../src/components/ResizeHandler.ts | 13 ++- .../src/components/ScrollSnaps.ts | 2 +- .../src/components/SlideSizes.ts | 10 +- .../src/components/SlidesHandler.ts | 7 +- .../src/components/SlidesInView.ts | 8 +- .../src/components/SlidesToScroll.ts | 2 +- .../src/components/SsrHandler.ts | 94 +++++++++++++++++++ .../src/components/Translate.ts | 17 ++-- 18 files changed, 331 insertions(+), 132 deletions(-) create mode 100644 packages/embla-carousel/src/components/NodeHandler.ts delete mode 100644 packages/embla-carousel/src/components/NodeRects.ts create mode 100644 packages/embla-carousel/src/components/SsrHandler.ts diff --git a/packages/embla-carousel/src/components/Animations.ts b/packages/embla-carousel/src/components/Animations.ts index bf86c3be6..f73e63600 100644 --- a/packages/embla-carousel/src/components/Animations.ts +++ b/packages/embla-carousel/src/components/Animations.ts @@ -6,7 +6,7 @@ export type AnimationsUpdateType = (engine: EngineType) => void export type AnimationsRenderType = (engine: EngineType, alpha: number) => void export type AnimationsType = { - init: () => void + init: (ownerWindow: WindowType) => void destroy: () => void start: () => void stop: () => void @@ -15,19 +15,21 @@ export type AnimationsType = { } export function Animations( - ownerDocument: Document, - ownerWindow: WindowType, update: () => void, render: (alpha: number) => void ): AnimationsType { const documentVisibleHandler = EventStore() const fixedTimeStep = 1000 / 60 + let windowInstance: WindowType let lastTimeStamp: number | null = null let accumulatedTime = 0 let animationId = 0 - function init(): void { + function init(ownerWindow: WindowType): void { + const ownerDocument = ownerWindow.document + windowInstance = ownerWindow + documentVisibleHandler.add(ownerDocument, 'visibilitychange', () => { if (ownerDocument.hidden) reset() }) @@ -59,17 +61,17 @@ export function Animations( render(alpha) if (animationId) { - animationId = ownerWindow.requestAnimationFrame(animate) + animationId = windowInstance.requestAnimationFrame(animate) } } function start(): void { if (animationId) return - animationId = ownerWindow.requestAnimationFrame(animate) + animationId = windowInstance.requestAnimationFrame(animate) } function stop(): void { - ownerWindow.cancelAnimationFrame(animationId) + windowInstance.cancelAnimationFrame(animationId) lastTimeStamp = null accumulatedTime = 0 animationId = 0 diff --git a/packages/embla-carousel/src/components/Axis.ts b/packages/embla-carousel/src/components/Axis.ts index 0504392f1..96deb13c2 100644 --- a/packages/embla-carousel/src/components/Axis.ts +++ b/packages/embla-carousel/src/components/Axis.ts @@ -1,4 +1,4 @@ -import { NodeRectType } from './NodeRects' +import { NodeRectType } from './NodeHandler' export type AxisOptionType = 'x' | 'y' export type AxisDirectionOptionType = 'ltr' | 'rtl' diff --git a/packages/embla-carousel/src/components/DragHandler.ts b/packages/embla-carousel/src/components/DragHandler.ts index a4e78b8ce..3a5066f21 100644 --- a/packages/embla-carousel/src/components/DragHandler.ts +++ b/packages/embla-carousel/src/components/DragHandler.ts @@ -21,7 +21,7 @@ import { } from './utils' export type DragHandlerType = { - init: () => void + init: (windowInstance: WindowType) => void destroy: () => void pointerDown: () => boolean } @@ -30,8 +30,6 @@ export function DragHandler( active: boolean, axis: AxisType, rootNode: HTMLElement, - ownerDocument: Document, - ownerWindow: WindowType, target: Vector1DType, dragTracker: DragTrackerType, location: Vector1DType, @@ -58,6 +56,8 @@ export function DragHandler( const freeForceBoost = { mouse: 500, touch: 600 } const baseSpeed = dragFree ? 43 : 25 + let ownerDocument: Document + let ownerWindow: WindowType let isMoving = false let startScroll = 0 let startCross = 0 @@ -66,9 +66,14 @@ export function DragHandler( let preventClick = false let isMouse = false - function init(): void { + function init(windowInstance: WindowType): void { if (!active) return + ownerDocument = windowInstance.document + ownerWindow = windowInstance + + dragTracker.init(windowInstance) + const node = rootNode initEvents .add(node, 'dragstart', (evt) => evt.preventDefault(), nonPassiveEvent) diff --git a/packages/embla-carousel/src/components/DragTracker.ts b/packages/embla-carousel/src/components/DragTracker.ts index aebd3ca54..9f9a8e4c8 100644 --- a/packages/embla-carousel/src/components/DragTracker.ts +++ b/packages/embla-carousel/src/components/DragTracker.ts @@ -5,21 +5,24 @@ type PointerCoordType = keyof Touch | keyof MouseEvent export type PointerEventType = TouchEvent | MouseEvent export type DragTrackerType = { + init: (windowInstance: WindowType) => void pointerDown: (evt: PointerEventType) => number pointerMove: (evt: PointerEventType) => number pointerUp: (evt: PointerEventType) => number readPoint: (evt: PointerEventType, evtAxis?: AxisOptionType) => number } -export function DragTracker( - axis: AxisType, - ownerWindow: WindowType -): DragTrackerType { +export function DragTracker(axis: AxisType): DragTrackerType { const logInterval = 170 + let ownerWindow: WindowType let startEvent: PointerEventType let lastEvent: PointerEventType + function init(windowInstance: WindowType): void { + ownerWindow = windowInstance + } + function readTime(evt: PointerEventType): number { return evt.timeStamp } @@ -57,6 +60,7 @@ export function DragTracker( } const self: DragTrackerType = { + init, pointerDown, pointerMove, pointerUp, diff --git a/packages/embla-carousel/src/components/EmblaCarousel.ts b/packages/embla-carousel/src/components/EmblaCarousel.ts index 320816d28..ed5f24a3b 100644 --- a/packages/embla-carousel/src/components/EmblaCarousel.ts +++ b/packages/embla-carousel/src/components/EmblaCarousel.ts @@ -3,11 +3,13 @@ import { EventStore } from './EventStore' import { WatchHandler, WatchHandlerType } from './WatchHandler' import { EventHandler, EventHandlerType } from './EventHandler' import { defaultOptions, EmblaOptionsType, OptionsType } from './Options' +import { NodeHandler, NodeHandlerType } from './NodeHandler' import { OptionsHandler } from './OptionsHandler' import { PluginsHandler } from './PluginsHandler' import { EmblaPluginsType, EmblaPluginType } from './Plugins' -import { isNumber, isString, WindowType } from './utils' import { ScrollToDirectionType } from './ScrollTo' +import { isNumber } from './utils' +import { SsrHandler, SsrHandlerType } from './SsrHandler' export type EmblaCarouselType = { canScrollNext: () => boolean @@ -43,16 +45,17 @@ export type EmblaCarouselType = { jump?: boolean, direction?: ScrollToDirectionType ) => void + ssrStyles: () => string } function EmblaCarousel( root: HTMLElement, userOptions?: EmblaOptionsType, - userPlugins?: EmblaPluginType[] + userPlugins?: EmblaPluginType[], + headless?: boolean ): EmblaCarouselType { - const ownerDocument = root.ownerDocument - const ownerWindow = ownerDocument.defaultView - const optionsHandler = OptionsHandler(ownerWindow) + const isSsr = !!headless + const optionsHandler = OptionsHandler() const pluginsHandler = PluginsHandler(optionsHandler) const mediaHandlers = EventStore() const watchHandler = WatchHandler() @@ -64,43 +67,33 @@ function EmblaCarousel( let destroyed = false let engine: EngineType + let nodeHandler: NodeHandlerType + let ssrHandler: SsrHandlerType let optionsBase = mergeOptions(defaultOptions, EmblaCarousel.globalOptions) let options = mergeOptions(optionsBase) let pluginList: EmblaPluginType[] = [] let pluginApis: EmblaPluginsType - let container: HTMLElement let slides: HTMLElement[] - function storeElements(): void { - const { container: userContainer, slides: userSlides } = options - - const customContainer = isString(userContainer) - ? root.querySelector(userContainer) - : userContainer - container = (customContainer || root.children[0]) - - const customSlides = isString(userSlides) - ? container.querySelectorAll(userSlides) - : userSlides - slides = [].slice.call(customSlides || container.children) - } - - function createEngine(options: OptionsType): EngineType { + function createEngine( + options: OptionsType, + container: HTMLElement, + slides: HTMLElement[] + ): EngineType { const engine = Engine( root, container, slides, - ownerDocument, - ownerWindow, options, + nodeHandler, eventHandler, watchHandler ) if (options.loop && !engine.slideLooper.canLoop()) { const optionsWithoutLoop = Object.assign({}, options, { loop: false }) - return createEngine(optionsWithoutLoop) + return createEngine(optionsWithoutLoop, container, slides) } return engine } @@ -111,13 +104,28 @@ function EmblaCarousel( ): void { if (destroyed) return + nodeHandler = NodeHandler(root, isSsr) + const { ownerWindow } = nodeHandler + + optionsHandler.init(ownerWindow) optionsBase = mergeOptions(optionsBase, withOptions) options = optionsAtMedia(optionsBase) pluginList = withPlugins || pluginList - storeElements() + const nodes = nodeHandler.getNodes(options) + container = nodes.container + slides = nodes.slides + engine = createEngine(options, container, slides) - engine = createEngine(options) + ssrHandler = SsrHandler( + isSsr, + container, + engine.axis, + nodeHandler, + optionsBase, + mergeOptions, + createEngine + ) optionsMediaQueries([ optionsBase, @@ -125,18 +133,22 @@ function EmblaCarousel( ]).forEach((query) => mediaHandlers.add(query, 'change', reActivate)) if (!options.active) return + if (!ownerWindow) return + if (isSsr) return engine.translate.to(engine.location.get()) - engine.animation.init() - engine.slidesInView.init() - engine.slideFocus.init() - engine.resizeHandler.init() - engine.slidesHandler.init() + engine.animation.init(ownerWindow) + engine.resizeHandler.init(ownerWindow) + engine.slidesInView.init(ownerWindow) + engine.slidesHandler.init(ownerWindow) engine.eventHandler.init(self) engine.watchHandler.init(self) + engine.slideFocus.init() if (engine.options.loop) engine.slideLooper.loop() - if (container.offsetParent && slides.length) engine.dragHandler.init() + if (container.offsetParent && slides.length) { + engine.dragHandler.init(ownerWindow) + } pluginApis = pluginsHandler.init(self, pluginList) } @@ -180,6 +192,7 @@ function EmblaCarousel( direction?: ScrollToDirectionType ): void { if (!options.active || destroyed) return + engine.scrollBody .useBaseFriction() .useDuration(jump === true ? 0 : options.duration) @@ -245,6 +258,10 @@ function EmblaCarousel( return pluginApis } + function ssrStyles(): string { + return ssrHandler.getStyles() + } + function internalEngine(): EngineType { return engine } @@ -286,11 +303,12 @@ function EmblaCarousel( slideNodes, slidesInView, slidesNotInView, - snapList + snapList, + ssrStyles } activate(userOptions, userPlugins) - setTimeout(() => eventHandler.emit('init', null), 0) + setTimeout(() => eventHandler.emit('init', null), 0) // TODO: Won't work in SSR return self } diff --git a/packages/embla-carousel/src/components/Engine.ts b/packages/embla-carousel/src/components/Engine.ts index 212bc51b2..803a47a62 100644 --- a/packages/embla-carousel/src/components/Engine.ts +++ b/packages/embla-carousel/src/components/Engine.ts @@ -12,7 +12,7 @@ import { DragTracker } from './DragTracker' import { EventHandlerType } from './EventHandler' import { EventStore, EventStoreType } from './EventStore' import { LimitType } from './Limit' -import { NodeRectType, NodeRects } from './NodeRects' +import { NodeRectType } from './NodeHandler' import { OptionsType } from './Options' import { PercentOfView, PercentOfViewType } from './PercentOfView' import { ResizeHandler, ResizeHandlerType } from './ResizeHandler' @@ -33,15 +33,15 @@ import { SlidesInView, SlidesInViewType } from './SlidesInView' import { SlideSizes } from './SlideSizes' import { SlidesToScroll, SlidesToScrollType } from './SlidesToScroll' import { Translate, TranslateType } from './Translate' -import { arrayKeys, arrayLast, arrayLastIndex, WindowType } from './utils' +import { arrayKeys, arrayLast, arrayLastIndex } from './utils' import { Vector1D, Vector1DType } from './Vector1d' import { WatchHandlerType } from './WatchHandler' +import { NodeHandlerType } from './NodeHandler' export type EngineType = { - ownerDocument: Document - ownerWindow: WindowType eventHandler: EventHandlerType watchHandler: WatchHandlerType + contentSize: number axis: AxisType animation: AnimationsType scrollBounds: ScrollBoundsType @@ -80,9 +80,8 @@ export function Engine( root: HTMLElement, container: HTMLElement, slides: HTMLElement[], - ownerDocument: Document, - ownerWindow: WindowType, options: OptionsType, + nodeHandler: NodeHandlerType, eventHandler: EventHandlerType, watchHandler: WatchHandlerType ): EngineType { @@ -108,9 +107,8 @@ export function Engine( // Measurements const pixelTolerance = 2 - const nodeRects = NodeRects() - const containerRect = nodeRects.measure(container) - const slideRects = slides.map(nodeRects.measure) + const containerRect = nodeHandler.getRect(container) + const slideRects = slides.map(nodeHandler.getRect) const axis = Axis(scrollAxis, direction) const viewSize = axis.measureSize(containerRect) const percentOfView = PercentOfView(viewSize) @@ -123,7 +121,7 @@ export function Engine( slideRects, slides, readEdgeGap, - ownerWindow + nodeHandler ) const slidesToScroll = SlidesToScroll( axis, @@ -211,8 +209,6 @@ export function Engine( } const animation = Animations( - ownerDocument, - ownerWindow, () => update(engine), (alpha: number) => render(engine, alpha) ) @@ -278,10 +274,9 @@ export function Engine( // Engine const engine: EngineType = { - ownerDocument, - ownerWindow, eventHandler, containerRect, + contentSize, slideRects, animation, axis, @@ -289,10 +284,8 @@ export function Engine( draggable, axis, root, - ownerDocument, - ownerWindow, target, - DragTracker(axis, ownerWindow), + DragTracker(axis), location, animation, scrollTo, @@ -321,10 +314,9 @@ export function Engine( container, eventHandler, watchHandler, - ownerWindow, slides, axis, - nodeRects + nodeHandler ), scrollBody, scrollBounds: ScrollBounds( diff --git a/packages/embla-carousel/src/components/NodeHandler.ts b/packages/embla-carousel/src/components/NodeHandler.ts new file mode 100644 index 000000000..e14b9ce17 --- /dev/null +++ b/packages/embla-carousel/src/components/NodeHandler.ts @@ -0,0 +1,94 @@ +import { OptionsType } from './Options' +import { isString, WindowType } from './utils' + +export type NodeRectType = { + top: number + right: number + bottom: number + left: number + width: number + height: number +} + +type NodesType = { + container: HTMLElement + slides: HTMLElement[] +} + +export type NodeHandlerType = { + ownerWindow: WindowType | null + getNodes: (options: OptionsType) => NodesType + getRect: (node: HTMLElement) => NodeRectType +} + +export function NodeHandler( + root: HTMLElement, + isSsr: boolean +): NodeHandlerType { + const ownerDocument = isSsr ? null : root.ownerDocument + const ownerWindow = ownerDocument + ? ownerDocument.defaultView + : null + + function getRect(node: HTMLElement): NodeRectType { + const { offsetTop, offsetLeft, offsetWidth, offsetHeight } = node + const offset: NodeRectType = { + top: offsetTop, + right: offsetLeft + offsetWidth, + bottom: offsetTop + offsetHeight, + left: offsetLeft, + width: offsetWidth, + height: offsetHeight + } + return offset + } + + function createSsrNode( + offsetLeft: number, + offsetTop: number, + offsetWidth: number, + offsetHeight: number + ): HTMLElement { + return { offsetLeft, offsetTop, offsetWidth, offsetHeight } + } + + function getBrowserNodes(options: OptionsType): NodesType { + const { container: userContainer, slides: userSlides } = options + + const containerNode = isString(userContainer) + ? root.querySelector(userContainer) + : userContainer + const container = (containerNode || root.children[0]) + + const slideNodes = isString(userSlides) + ? container.querySelectorAll(userSlides) + : userSlides + const slides = Array.from(slideNodes || container.children) + + return { container, slides } + } + + function getSsrNodes(options: OptionsType): NodesType { + const container = createSsrNode(0, 0, 100, 100) + let startOffset = 0 + + const slides = options.ssr.map((size) => { + const slide = createSsrNode(startOffset, startOffset, size, size) + startOffset += size + return slide + }) + + return { container, slides } + } + + function getNodes(options: OptionsType): NodesType { + return isSsr ? getSsrNodes(options) : getBrowserNodes(options) + } + + const self: NodeHandlerType = { + ownerWindow, + getNodes, + getRect + } + return self +} diff --git a/packages/embla-carousel/src/components/NodeRects.ts b/packages/embla-carousel/src/components/NodeRects.ts deleted file mode 100644 index 3b5664cc5..000000000 --- a/packages/embla-carousel/src/components/NodeRects.ts +++ /dev/null @@ -1,33 +0,0 @@ -export type NodeRectType = { - top: number - right: number - bottom: number - left: number - width: number - height: number -} - -export type NodeRectsType = { - measure: (node: HTMLElement) => NodeRectType -} - -export function NodeRects(): NodeRectsType { - function measure(node: HTMLElement): NodeRectType { - const { offsetTop, offsetLeft, offsetWidth, offsetHeight } = node - const offset: NodeRectType = { - top: offsetTop, - right: offsetLeft + offsetWidth, - bottom: offsetTop + offsetHeight, - left: offsetLeft, - width: offsetWidth, - height: offsetHeight - } - - return offset - } - - const self: NodeRectsType = { - measure - } - return self -} diff --git a/packages/embla-carousel/src/components/Options.ts b/packages/embla-carousel/src/components/Options.ts index 938f441a5..8ac14ea36 100644 --- a/packages/embla-carousel/src/components/Options.ts +++ b/packages/embla-carousel/src/components/Options.ts @@ -34,6 +34,7 @@ export type OptionsType = CreateOptionsType<{ resize: boolean focus: boolean slideChanges: boolean + ssr: number[] }> export const defaultOptions: OptionsType = { @@ -56,7 +57,8 @@ export const defaultOptions: OptionsType = { draggable: true, resize: true, focus: true, - slideChanges: true + slideChanges: true, + ssr: [] } export type EmblaOptionsType = Partial diff --git a/packages/embla-carousel/src/components/OptionsHandler.ts b/packages/embla-carousel/src/components/OptionsHandler.ts index 8f635fc09..30e40bc6d 100644 --- a/packages/embla-carousel/src/components/OptionsHandler.ts +++ b/packages/embla-carousel/src/components/OptionsHandler.ts @@ -1,9 +1,11 @@ +import { NodeHandlerType } from './NodeHandler' import { LooseOptionsType, CreateOptionsType } from './Options' import { objectKeys, objectsMergeDeep, WindowType } from './utils' type OptionsType = Partial> export type OptionsHandlerType = { + init: (windowInstance: NodeHandlerType['ownerWindow']) => void mergeOptions: ( optionsA: TypeA, optionsB?: TypeB @@ -12,7 +14,14 @@ export type OptionsHandlerType = { optionsMediaQueries: (optionsList: OptionsType[]) => MediaQueryList[] } -export function OptionsHandler(ownerWindow: WindowType): OptionsHandlerType { +export function OptionsHandler(): OptionsHandlerType { + let windowInstance: WindowType + + function init(ownerWindow: NodeHandlerType['ownerWindow']): void { + if (!ownerWindow) return + windowInstance = ownerWindow + } + function mergeOptions( optionsA: TypeA, optionsB?: TypeB @@ -21,9 +30,11 @@ export function OptionsHandler(ownerWindow: WindowType): OptionsHandlerType { } function optionsAtMedia(options: Type): Type { + if (!windowInstance) return options + const optionsAtMedia = options.breakpoints || {} const matchedMediaOptions = objectKeys(optionsAtMedia) - .filter((media) => ownerWindow.matchMedia(media).matches) + .filter((media) => windowInstance.matchMedia(media).matches) .map((media) => optionsAtMedia[media]) .reduce((a, mediaOption) => mergeOptions(a, mediaOption), {}) @@ -31,13 +42,16 @@ export function OptionsHandler(ownerWindow: WindowType): OptionsHandlerType { } function optionsMediaQueries(optionsList: OptionsType[]): MediaQueryList[] { + if (!windowInstance) return [] + return optionsList .map((options) => objectKeys(options.breakpoints || {})) .reduce((acc, mediaQueries) => acc.concat(mediaQueries), []) - .map(ownerWindow.matchMedia) + .map(windowInstance.matchMedia) } const self: OptionsHandlerType = { + init, mergeOptions, optionsAtMedia, optionsMediaQueries diff --git a/packages/embla-carousel/src/components/ResizeHandler.ts b/packages/embla-carousel/src/components/ResizeHandler.ts index efcd228c8..e82f75c1f 100644 --- a/packages/embla-carousel/src/components/ResizeHandler.ts +++ b/packages/embla-carousel/src/components/ResizeHandler.ts @@ -2,11 +2,11 @@ import { AxisType } from './Axis' import { EmblaCarouselType } from './EmblaCarousel' import { EventHandlerType } from './EventHandler' import { WatchHandlerType } from './WatchHandler' -import { NodeRectsType } from './NodeRects' +import { NodeHandlerType } from './NodeHandler' import { mathAbs, WindowType } from './utils' export type ResizeHandlerType = { - init: () => void + init: (ownerWindow: WindowType) => void destroy: () => void } @@ -15,10 +15,9 @@ export function ResizeHandler( container: HTMLElement, eventHandler: EventHandlerType, watchHandler: WatchHandlerType, - ownerWindow: WindowType, slides: HTMLElement[], axis: AxisType, - nodeRects: NodeRectsType + nodeHandler: NodeHandlerType ): ResizeHandlerType { const observeNodes = [container].concat(slides) let resizeObserver: ResizeObserver @@ -27,16 +26,16 @@ export function ResizeHandler( let destroyed = false function readSize(node: HTMLElement): number { - return axis.measureSize(nodeRects.measure(node)) + return axis.measureSize(nodeHandler.getRect(node)) } - function init(): void { + function init(ownerWindow: WindowType): void { if (!active) return containerSize = readSize(container) slideSizes = slides.map(readSize) - resizeObserver = new ResizeObserver((entries) => { + resizeObserver = new ownerWindow.ResizeObserver((entries) => { watchHandler.emit('resize', entries, onResize) }) diff --git a/packages/embla-carousel/src/components/ScrollSnaps.ts b/packages/embla-carousel/src/components/ScrollSnaps.ts index ef5f5f9f4..d9cb576b8 100644 --- a/packages/embla-carousel/src/components/ScrollSnaps.ts +++ b/packages/embla-carousel/src/components/ScrollSnaps.ts @@ -1,6 +1,6 @@ import { AlignmentType } from './Alignment' import { AxisType } from './Axis' -import { NodeRectType } from './NodeRects' +import { NodeRectType } from './NodeHandler' import { SlidesToScrollType } from './SlidesToScroll' import { arrayLast, mathAbs } from './utils' diff --git a/packages/embla-carousel/src/components/SlideSizes.ts b/packages/embla-carousel/src/components/SlideSizes.ts index eaf2c1f3b..bc8c784f2 100644 --- a/packages/embla-carousel/src/components/SlideSizes.ts +++ b/packages/embla-carousel/src/components/SlideSizes.ts @@ -1,6 +1,7 @@ import { AxisType } from './Axis' -import { NodeRectType } from './NodeRects' -import { arrayIsLastIndex, arrayLast, mathAbs, WindowType } from './utils' +import { NodeHandlerType } from './NodeHandler' +import { NodeRectType } from './NodeHandler' +import { arrayIsLastIndex, arrayLast, mathAbs } from './utils' export type SlideSizesType = { slideSizes: number[] @@ -15,8 +16,9 @@ export function SlideSizes( slideRects: NodeRectType[], slides: HTMLElement[], readEdgeGap: boolean, - ownerWindow: WindowType + nodeHandler: NodeHandlerType ): SlideSizesType { + const { ownerWindow } = nodeHandler const { measureSize, startEdge, endEdge } = axis const withEdgeGap = slideRects[0] && readEdgeGap const startGap = measureStartGap() @@ -31,7 +33,7 @@ export function SlideSizes( } function measureEndGap(): number { - if (!withEdgeGap) return 0 + if (!withEdgeGap || !ownerWindow) return 0 const style = ownerWindow.getComputedStyle(arrayLast(slides)) return parseFloat(style.getPropertyValue(`margin-${endEdge}`)) } diff --git a/packages/embla-carousel/src/components/SlidesHandler.ts b/packages/embla-carousel/src/components/SlidesHandler.ts index 61716e401..58a264cbb 100644 --- a/packages/embla-carousel/src/components/SlidesHandler.ts +++ b/packages/embla-carousel/src/components/SlidesHandler.ts @@ -1,9 +1,10 @@ import { EmblaCarouselType } from './EmblaCarousel' import { EventHandlerType } from './EventHandler' import { WatchHandlerType } from './WatchHandler' +import { WindowType } from './utils' export type SlidesHandlerType = { - init: () => void + init: (ownerWindow: WindowType) => void destroy: () => void } @@ -16,10 +17,10 @@ export function SlidesHandler( let mutationObserver: MutationObserver let destroyed = false - function init(): void { + function init(ownerWindow: WindowType): void { if (!active) return - mutationObserver = new MutationObserver((mutations) => { + mutationObserver = new ownerWindow.MutationObserver((mutations) => { watchHandler.emit('slideschanged', mutations, onSlidesChange) }) diff --git a/packages/embla-carousel/src/components/SlidesInView.ts b/packages/embla-carousel/src/components/SlidesInView.ts index 5e778e94e..e66bb7b74 100644 --- a/packages/embla-carousel/src/components/SlidesInView.ts +++ b/packages/embla-carousel/src/components/SlidesInView.ts @@ -1,5 +1,5 @@ import { EventHandlerType } from './EventHandler' -import { objectKeys } from './utils' +import { objectKeys, WindowType } from './utils' type IntersectionEntryMapType = { [key: number]: IntersectionObserverEntry @@ -8,7 +8,7 @@ type IntersectionEntryMapType = { export type SlidesInViewOptionsType = IntersectionObserverInit['threshold'] export type SlidesInViewType = { - init: () => void + init: (ownerWindow: WindowType) => void destroy: () => void get: (inView?: boolean) => number[] } @@ -25,8 +25,8 @@ export function SlidesInView( let intersectionObserver: IntersectionObserver let destroyed = false - function init(): void { - intersectionObserver = new IntersectionObserver( + function init(ownerWindow: WindowType): void { + intersectionObserver = new ownerWindow.IntersectionObserver( (entries) => { if (destroyed) return diff --git a/packages/embla-carousel/src/components/SlidesToScroll.ts b/packages/embla-carousel/src/components/SlidesToScroll.ts index aa81eee82..7bad59f89 100644 --- a/packages/embla-carousel/src/components/SlidesToScroll.ts +++ b/packages/embla-carousel/src/components/SlidesToScroll.ts @@ -1,5 +1,5 @@ import { AxisType } from './Axis' -import { NodeRectType } from './NodeRects' +import { NodeRectType } from './NodeHandler' import { arrayKeys, arrayLast, diff --git a/packages/embla-carousel/src/components/SsrHandler.ts b/packages/embla-carousel/src/components/SsrHandler.ts new file mode 100644 index 000000000..6d67dbbb9 --- /dev/null +++ b/packages/embla-carousel/src/components/SsrHandler.ts @@ -0,0 +1,94 @@ +import { Axis, AxisType } from './Axis' +import { EngineType } from './Engine' +import { NodeHandlerType } from './NodeHandler' +import { OptionsType } from './Options' +import { OptionsHandlerType } from './OptionsHandler' +import { Translate } from './Translate' +import { mathSign } from './utils' + +// TODO: Add selectors from slides and container options to make it dynamic +// TODO: Enable SSR for library wrappers like React, Vue etc. +// TODO: Remove init event with timeout in EmblaCarousel.ts which won't work with SSR (deprecated) +// TODO: Enable SSR for plugins? + +export type SsrHandlerType = { + getStyles: () => string +} + +export function SsrHandler( + isSsr: boolean, + container: HTMLElement, + axis: AxisType, + nodeHandler: NodeHandlerType, + optionsBase: OptionsType, + mergeOptions: OptionsHandlerType['mergeOptions'], + createEngine: ( + options: OptionsType, + container: HTMLElement, + slides: HTMLElement[] + ) => EngineType +): SsrHandlerType { + const translate = Translate(axis, container, '%') + + function getNormalizedEngine(options: OptionsType): EngineType { + const normalizedOptions = mergeOptions(options, { direction: 'ltr' }) + const { slides, container } = nodeHandler.getNodes(normalizedOptions) + return createEngine(normalizedOptions, container, slides) + } + + function createStyles(options: OptionsType): string { + const { direction } = Axis(options.axis, options.direction) + const { location, slideLooper, contentSize } = getNormalizedEngine(options) + const loopPoints = options.loop ? slideLooper.loopPoints : [] + const containerSsr = direction(location.get()) + + return ` + .embla__container { + transform: ${translate.get(containerSsr)}; + } + ${loopPoints.reduce((acc, loopPoint) => { + const { index } = loopPoint + const sign = mathSign(loopPoint.target()) + const size = options.ssr[index] + + if (!sign || !size) return acc + + const slideSsr = direction((contentSize / size) * 100 * sign) + + return ` + ${acc} + .embla__slide:nth-child(${index + 1}) { + transform: ${translate.get(slideSsr)}; + } + ` + }, '')} + ` + } + + function getStyles(): string { + if (!isSsr) return '' + if (!optionsBase.ssr.length) return '' + + const optionBreakpoints = optionsBase.breakpoints + + return ` + ${createStyles(optionsBase)} + + ${Object.keys(optionBreakpoints).reduce((acc, key) => { + const optionsAtMedia = mergeOptions(optionsBase, optionBreakpoints[key]) + + return ` + ${acc} + @media ${key} { + ${createStyles(optionsAtMedia)} + } + ` + }, '')} + ` //.replace(/\s+/g, '') + } + + const self: SsrHandlerType = { + getStyles + } + return self +} diff --git a/packages/embla-carousel/src/components/Translate.ts b/packages/embla-carousel/src/components/Translate.ts index 4ccda61b4..484b45d6d 100644 --- a/packages/embla-carousel/src/components/Translate.ts +++ b/packages/embla-carousel/src/components/Translate.ts @@ -5,23 +5,27 @@ export type TranslateType = { clear: () => void to: (target: number) => void toggleActive: (active: boolean) => void + get: (n: number) => string } export function Translate( axis: AxisType, - container: HTMLElement + container: HTMLElement, + unit?: 'px' | '%' ): TranslateType { - const translate = axis.scroll === 'x' ? x : y + const getTranslate = axis.scroll === 'x' ? x : y const containerStyle = container.style + const transformUnit = unit || 'px' + let previousTarget: number | null = null let disabled = false function x(n: number): string { - return `translate3d(${n}px,0px,0px)` + return `translate3d(${n}${transformUnit},0px,0px)` } function y(n: number): string { - return `translate3d(0px,${n}px,0px)` + return `translate3d(0px,${n}${transformUnit},0px)` } function to(target: number): void { @@ -30,7 +34,7 @@ export function Translate( const newTarget = roundToTwoDecimals(axis.direction(target)) if (newTarget === previousTarget) return - containerStyle.transform = translate(newTarget) + containerStyle.transform = getTranslate(newTarget) previousTarget = newTarget } @@ -47,7 +51,8 @@ export function Translate( const self: TranslateType = { clear, to, - toggleActive + toggleActive, + get: getTranslate } return self }