Repository: viktorlarsson/vue-tiny-slider Branch: master Commit: 2da850e964f0 Files: 16 Total size: 33.7 KB Directory structure: gitextract_1wuhc6zo/ ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo/ │ ├── index.html │ └── main.ts ├── dist/ │ └── index.js ├── package.json ├── src/ │ ├── index.d.ts │ └── index.js ├── tests/ │ ├── index.test.js │ ├── ssr.test.js │ └── types-check.ts ├── tsconfig.json ├── vite.config.js └── vitest.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: [master] pull_request: branches: [master] jobs: test: runs-on: ubuntu-latest env: # Opt actions/checkout, actions/setup-node, etc. onto Node 24 now. # Becomes the default on 2026-06-02; this silences the deprecation # warning until then. FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true strategy: fail-fast: false matrix: node-version: [22.x, 24.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm - run: npm ci - run: npm run typecheck - run: npm test - run: npm run build ================================================ FILE: .gitignore ================================================ node_modules/ npm-debug.log # Demo debug screenshots — regenerated as needed. demo/*.png # Bun build output for the demo (deployed to Vercel). dist-demo/ # Keep any npm auth tokens out of the tree. .npmrc ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Viktor Sarström Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # tiny-slider 2.0 for Vue Wrapper for Tiny slider for all purposes by [ganlanyuan](https://github.com/ganlanyuan/tiny-slider) in Vue. [Live demo →](https://vue-tiny-slider-blush.vercel.app/) [![version](https://img.shields.io/npm/v/vue-tiny-slider.svg)](https://www.npmjs.com/package/vue-tiny-slider) [![downloads](https://img.shields.io/npm/dt/vue-tiny-slider.svg)](https://www.npmjs.com/package/vue-tiny-slider) [![downloads per week](https://img.shields.io/npm/dw/vue-tiny-slider.svg)](https://www.npmjs.com/package/vue-tiny-slider) ## Table of Contents * [Compatibility](#compatibility) * [Install](#install) * [Use](#use) * [Styling](#styling) * [Options](#options) * [Methods](#methods) * [How to use the methods](#how-to-use-the-methods) * [Nuxt 3 SSR](#nuxt-3-ssr) * [Todos](#todo) * [Collaborators](#collaborators) * [License](#license) * [Cheerios <3](#cheerios-3) ## Compatibility | `vue-tiny-slider` | Vue | | ----------------- | -------- | | `^1.0.0` | Vue 3.x | | `^0.1.x` | Vue 2.x | If you're on Vue 2, pin to `vue-tiny-slider@^0.1`. ## Install `npm install vue-tiny-slider` ## Use **Globally (Vue 3):** ````javascript import { createApp } from 'vue'; import VueTinySlider from 'vue-tiny-slider'; import App from './App.vue'; createApp(App) .component('tiny-slider', VueTinySlider) .mount('#app'); ```` **Or locally inside a single component:** ````javascript import VueTinySlider from 'vue-tiny-slider'; export default { components: { 'tiny-slider': VueTinySlider } } ```` ````html
Slider item #1
Slider item #2
Slider item #3
Slider item #4
Slider item #5
Slider item #6
```` ## Styling SCSS ````scss @import 'tiny-slider/src/tiny-slider'; ```` CDN ````html ```` ## Options ```` auto-init items mode gutter edge-padding fixed-width slide-by controls controls-text controls-container nav nav-container arrow-keys speed autoplay autoplay-timeout autoplay-direction autoplay-text autoplay-hover-pause autoplay-button autoplay-button-output autoplay-reset-on-visibility animate-in animate-out animate-normal animate-delay loop rewind auto-height responsive lazyload touch mouse-drag nested freezable disable on-init center lazy-load-selector prevent-action-when-running prevent-scroll-on-touch nav-options auto-width ```` For more detailed information about the options, see the [Tiny-slider documentation (Options)](https://github.com/ganlanyuan/tiny-slider#options). ## Methods ````getInfo```` ````goTo```` ````destroy```` ### How to use the methods To be able to use the methods, you need to use ref on the component. Ref is used to register a reference to an element or a child component. ``` ``` ``` import VueTinySlider from 'vue-tiny-slider'; export default { ..., methods: { getInfo: function(event) { this.$refs.tinySlider.slider.getInfo(); } } } ``` For more detailed information about the methods, see the [Tiny-slider documentation (Methods)](https://github.com/ganlanyuan/tiny-slider#methods). ## Nuxt 3 SSR Tiny-slider touches the DOM, so the component must only render on the client. 1. `npm install vue-tiny-slider` 2. Create `plugins/vue-tiny-slider.client.js` (the `.client` suffix makes it client-only): ```js import VueTinySlider from 'vue-tiny-slider'; export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.component('VueTinySlider', VueTinySlider); }); ``` 3. Wrap the slider in `` where you use it: ```html
#1
#2
#3
``` ## Todo * ~~Add demo link from a fiddle or something similar~~ * Better handling of the responsive-settings * Add Custom Events * ~~Add Methods~~ ## Collaborators * [Morgan Eklund](https://github.com/rymdkapten) * [Viktor Sarström](https://github.com/viktorlarsson) ## License This project is available under the MIT license. ## Cheerios <3 * Fixed broken demo link, @VitorLuizC * Moved tiny-slider from devDependencies to dependencies, @TitanFighter * Added nav position to props, @Irsanarisandy * Got it to work with NuxtJS SSR, @ilbonte ================================================ FILE: demo/index.html ================================================ vue-tiny-slider — local demo
================================================ FILE: demo/main.ts ================================================ import { createApp, h } from 'vue'; // Pull from local source so the demo always exercises the working tree. import VueTinySlider from '../src/index.js'; createApp({ components: { VueTinySlider }, render() { return h('div', { class: 'wrapper' }, [ h('h1', 'vue-tiny-slider — local demo'), h('p', { class: 'lede' }, 'Drag, swipe, or click the controls.'), h( VueTinySlider, { mouseDrag: true, loop: false, items: '2', gutter: 20, controlsPosition: 'bottom', navPosition: 'bottom' }, { default: () => [ h('div', { class: 'slide' }, 'Slide 1'), h('div', { class: 'slide' }, 'Slide 2'), h('div', { class: 'slide' }, 'Slide 3'), h('div', { class: 'slide' }, 'Slide 4'), h('div', { class: 'slide' }, 'Slide 5'), h('div', { class: 'slide' }, 'Slide 6') ] } ) ]); } }).mount('#app'); ================================================ FILE: dist/index.js ================================================ (function(e,t){typeof exports==`object`&&typeof module<`u`?module.exports=t(require(`vue`)):typeof define==`function`&&define.amd?define([`vue`],t):(e=typeof globalThis<`u`?globalThis:e||self,e[`vue-tiny-slider`]=t(e.Vue))})(this,function(e){var t=typeof Node<`u`?Node:Object,n={eventsList:[`indexChanged`,`transitionStart`,`transitionEnd`,`newBreakpointStart`,`newBreakpointEnd`,`touchStart`,`touchMove`,`touchEnd`,`dragStart`,`dragMove`,`dragEnd`],props:{mode:{type:[String],default:`carousel`},autoInit:{type:[Boolean],default:!0},axis:{type:[String],validator:e=>e===`horizontal`||e===`vertical`},items:{type:[String,Number],default:1},gutter:{type:[String,Number],default:0},edgePadding:{type:[String,Number],default:0},fixedWidth:{type:[String,Boolean,Number],default:!1},viewportMax:{type:[String,Boolean,Number],default:!1},swipeAngle:{type:[Boolean,Number],default:15},slideBy:{type:[String,Number],default:1},controls:{type:[String,Boolean],default:!0},controlsPosition:{type:[String],validator:e=>e===`top`||e===`bottom`,default:`top`},controlsText:{type:[Array],default:()=>[`prev`,`next`]},controlsContainer:{type:[Boolean,t,String],default:!1},prevButton:{type:[t,String,Boolean],default:!1},nextButton:{type:[t,String,Boolean],default:!1},nav:{type:[Boolean],default:!0},navPosition:{type:[String],default:`top`},navContainer:{type:[Boolean,t,String],default:!1},navAsThumbnails:{type:[Boolean],default:!1},arrowKeys:{type:[Boolean],default:!1},speed:{type:[String,Number],default:300},autoplay:{type:[Boolean],default:!1},autoplayTimeout:{type:[Number],default:5e3},autoplayDirection:{type:[String],default:`forward`,validator:e=>e===`forward`||e===`backward`},autoplayText:{type:[Array],default:()=>[`start`,`stop`]},autoplayHoverPause:{type:[Boolean],default:!1},autoplayButton:{type:[Boolean,t,String],default:!1},autoplayButtonOutput:{type:[Boolean],default:!0},autoplayResetOnVisibility:{type:[Boolean],default:!0},animateIn:{type:[String],default:`tns-fadeIn`},animateOut:{type:[String],default:`tns-fadeOut`},animateNormal:{type:[String],default:`tns-normal`},animateDelay:{type:[String,Number,Boolean],default:!1},loop:{type:[Boolean],default:!0},rewind:{type:[Boolean],default:!1},autoHeight:{type:[Boolean],default:!1},responsive:{type:[Boolean,Object],default:!1},lazyload:{type:[Boolean],default:!1},touch:{type:[Boolean],default:!0},mouseDrag:{type:[Boolean],default:!1},nested:{type:[String,Boolean],default:!1,validator:e=>e===`inner`||e===`outer`||e===!1},freezable:{type:[Boolean],default:!0},disable:{type:[Boolean],default:!1},startIndex:{type:[Number],default:0},onInit:{type:[Function,Boolean],default:!1},center:{type:Boolean,default:!1},lazyLoadSelector:{type:String,default:`.tns-lazy-img`},preventActionWhenRunning:{type:Boolean,default:!1},autoWidth:{type:Boolean,default:!1},preventScrollOnTouch:{type:[String,Boolean],default:!1,validator:e=>e===`auto`||e===`force`||e===!1},useLocalStorage:{type:[Boolean],default:!0}},mounted:function(){this.autoInit&&this.init()},beforeUnmount:function(){this.slider&&this.slider.destroy()},methods:{$_vueTinySlider_subscribeTo(e){this.slider.events.on(e,t=>{this.$emit(e,t)})},$_vueTinySlider_subscribeToAll(){this.$options.eventsList.forEach(this.$_vueTinySlider_subscribeTo)},goTo:function(e){this.slider.goTo(e)},rebuild:function(){this.slider=this.slider.rebuild(),this.$emit(`rebuild`)},getInfo:function(){this.$emit(`getInfo`,this.slider.getInfo(),this.slider)},destroy:function(){this.slider.destroy()},init:async function(){var{tns:e}=await import(`tiny-slider/dist/tiny-slider.js`),t={container:this.$el,axis:this.axis,items:parseInt(this.items),mode:this.mode,gutter:this.gutter,edgePadding:this.edgePadding,fixedWidth:this.fixedWidth?parseInt(this.fixedWidth,10):this.fixedWidth,viewportMax:this.viewportMax,slideBy:this.slideBy,controls:this.controls,controlsPosition:this.controlsPosition,controlsText:this.controlsText,controlsContainer:this.controlsContainer,prevButton:this.prevButton,nextButton:this.nextButton,nav:this.nav,navPosition:this.navPosition,navContainer:this.navContainer,navAsThumbnails:this.navAsThumbnails,arrowKeys:this.arrowKeys,speed:this.speed,autoplay:this.autoplay,autoplayTimeout:this.autoplayTimeout,autoplayDirection:this.autoplayDirection,autoplayText:this.autoplayText,autoplayHoverPause:this.autoplayHoverPause,autoplayButton:this.autoplayButton,autoplayButtonOutput:this.autoplayButtonOutput,autoplayResetOnVisibility:this.autoplayResetOnVisibility,animateIn:this.animateIn,animateOut:this.animateOut,animateNormal:this.animateNormal,animateDelay:this.animateDelay,loop:this.loop,rewind:this.rewind,autoHeight:this.autoHeight,responsive:this.responsive,lazyload:this.lazyload,touch:this.touch,mouseDrag:this.mouseDrag,nested:this.nested,freezable:this.freezable,disable:this.disable,onInit:this.onInit,swipeAngle:this.swipeAngle,startIndex:this.startIndex,center:this.center,lazyLoadSelector:this.lazyLoadSelector,preventActionWhenRunning:this.preventActionWhenRunning,preventScrollOnTouch:this.preventScrollOnTouch,autoWidth:this.autoWidth,useLocalStorage:this.useLocalStorage};r(t),this.slider=e(t),this.$emit(`init`),this.$_vueTinySlider_subscribeToAll()}},render:function(){return(0,e.h)(`div`,this.$slots.default?this.$slots.default():[])}};function r(e){for(var t in e)e.hasOwnProperty(t)&&e[t]===void 0&&delete e[t]}return n}); //# sourceMappingURL=index.js.map ================================================ FILE: package.json ================================================ { "name": "vue-tiny-slider", "version": "1.1.0", "description": "Vanilla javascript slider for all purposes created by ganlanyuan in Vue.", "main": "./dist/index.js", "types": "./src/index.d.ts", "files": [ "dist", "src", "README.md", "LICENSE" ], "scripts": { "watch": "vite build --watch", "build": "vite build", "test": "vitest run", "typecheck": "tsc", "demo": "bun --hot demo/index.html", "demo:build": "bun build demo/index.html --outdir=dist-demo --minify" }, "repository": { "type": "git", "url": "git+https://github.com/viktorlarsson/vue-tiny-slider.git" }, "keywords": [ "javascript", "vue", "tiny-slider", "slider", "carousel" ], "author": "Viktor Sarström & Morgan Eklund", "license": "MIT", "bugs": { "url": "https://github.com/viktorlarsson/vue-tiny-slider/issues" }, "homepage": "https://github.com/viktorlarsson/vue-tiny-slider#readme", "dependencies": { "tiny-slider": "^2.9.4" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.6", "@vue/test-utils": "^2.4.6", "happy-dom": "^20.8.9", "typescript": "^6.0.2", "vite": "^8.0.8", "vitest": "^4.1.4", "vue": "^3.5.32" } } ================================================ FILE: src/index.d.ts ================================================ import type { DefineComponent } from 'vue'; import type { TinySliderSettings, TinySliderInstance, TinySliderInfo, SilderEvent } from 'tiny-slider'; export type { TinySliderSettings, TinySliderInstance, TinySliderInfo, SilderEvent }; /** * Props accepted by the `` component. * * Inherits every tiny-slider option (except `container`, which the wrapper * sets to the component's root element) and adds a handful of wrapper- * specific props. */ export interface VueTinySliderProps extends Partial> { /** * Automatically call `init()` in the mounted hook. * Set to `false` to call `init()` manually via `$refs`. * @default true */ autoInit?: boolean; /** * Position of the prev/next controls relative to the slider. * @default "top" */ controlsPosition?: 'top' | 'bottom'; /** * Maximum viewport width (in px) considered for responsive breakpoints. * @default false */ viewportMax?: number | string | boolean; } /** * The underlying tiny-slider instance and wrapper methods exposed on the * component instance (available through `$refs.yourRef`). */ export interface VueTinySliderInstance { /** The underlying tiny-slider instance once `init()` has resolved. */ slider: TinySliderInstance | null; /** Initialize (or re-initialize) the slider. Returns a Promise because * tiny-slider is lazy-loaded to keep the module SSR-safe. */ init(): Promise; /** Go to a specific slide by index or keyword. */ goTo(target: number | 'next' | 'prev' | 'first' | 'last'): void; /** Rebuild the slider. Emits `rebuild`. */ rebuild(): void; /** Emits `getInfo` with the current slider state and instance. */ getInfo(): void; /** Destroy the underlying tiny-slider instance. */ destroy(): void; } declare const VueTinySlider: DefineComponent; export default VueTinySlider; ================================================ FILE: src/index.js ================================================ import { h } from 'vue'; // `Node` is a browser DOM global; fall back to Object on the server so that // defining the component doesn't throw during SSR module evaluation. var NodeType = typeof Node !== 'undefined' ? Node : Object; var VueTinySlider = { eventsList: [ 'indexChanged', 'transitionStart', 'transitionEnd', 'newBreakpointStart', 'newBreakpointEnd', 'touchStart', 'touchMove', 'touchEnd', 'dragStart', 'dragMove', 'dragEnd' ], props: { mode: { type: [String], default: 'carousel' }, autoInit: { type: [Boolean], default: true }, axis: { type: [String], validator: value => { return value === 'horizontal' || value === 'vertical'; } }, items: { type: [String, Number], default: 1 }, gutter: { type: [String, Number], default: 0 }, edgePadding: { type: [String, Number], default: 0 }, fixedWidth: { type: [String, Boolean, Number], default: false }, viewportMax: { type: [String, Boolean, Number], default: false }, swipeAngle: { type: [Boolean, Number], default: 15 }, slideBy: { type: [String, Number], default: 1 }, controls: { type: [String, Boolean], default: true }, controlsPosition: { type: [String], validator: value => { return value === 'top' || value === 'bottom'; }, default: 'top' }, controlsText: { type: [Array], default: () => ['prev', 'next'] }, controlsContainer: { type: [Boolean, NodeType, String], default: false }, prevButton: { type: [NodeType, String, Boolean], default: false }, nextButton: { type: [NodeType, String, Boolean], default: false }, nav: { type: [Boolean], default: true }, navPosition: { type: [String], default: "top" }, navContainer: { type: [Boolean, NodeType, String], default: false }, navAsThumbnails: { type: [Boolean], default: false }, arrowKeys: { type: [Boolean], default: false }, speed: { type: [String, Number], default: 300 }, autoplay: { type: [Boolean], default: false }, autoplayTimeout: { type: [Number], default: 5000 }, autoplayDirection: { type: [String], default: 'forward', validator: value => { return value === 'forward' || value === 'backward'; } }, autoplayText: { type: [Array], default: () => ['start', 'stop'] }, autoplayHoverPause: { type: [Boolean], default: false }, autoplayButton: { type: [Boolean, NodeType, String], default: false, }, autoplayButtonOutput: { type: [Boolean], default: true }, autoplayResetOnVisibility: { type: [Boolean], default: true, }, animateIn: { type: [String], default: 'tns-fadeIn' }, animateOut: { type: [String], default: 'tns-fadeOut' }, animateNormal: { type: [String], default: 'tns-normal' }, animateDelay: { type: [String, Number, Boolean], default: false }, loop: { type: [Boolean], default: true }, rewind: { type: [Boolean], default: false }, autoHeight: { type: [Boolean], default: false }, responsive: { type: [Boolean, Object], default: false }, lazyload: { type: [Boolean], default: false }, touch: { type: [Boolean], default: true }, mouseDrag: { type: [Boolean], default: false }, nested: { type: [String, Boolean], default: false, validator: value => { return value === 'inner' || value === 'outer' || value === false; } }, freezable: { type: [Boolean], default: true }, disable: { type: [Boolean], default: false }, startIndex: { type: [Number], default: 0 }, onInit: { type: [Function, Boolean], default: false }, center: { type: Boolean, default: false }, lazyLoadSelector: { type: String, default: '.tns-lazy-img' }, preventActionWhenRunning: { type: Boolean, default: false }, autoWidth: { type: Boolean, default: false }, preventScrollOnTouch: { type: [String, Boolean], default: false, validator: value => { return value === 'auto' || value === 'force' || value === false; } }, useLocalStorage: { type: [Boolean], default: true } }, mounted: function () { if(this.autoInit) { // init() is async (lazy-loads tiny-slider); swallow the returned // promise — consumers who need ready-state should use @init event. this.init(); } }, beforeUnmount: function() { if(this.slider) { this.slider.destroy(); } }, methods: { $_vueTinySlider_subscribeTo (eventName) { this.slider.events.on(eventName, (info) => { this.$emit(eventName, info); }); }, $_vueTinySlider_subscribeToAll () { this.$options.eventsList.forEach(this.$_vueTinySlider_subscribeTo) }, goTo: function(value) { this.slider.goTo(value); }, rebuild: function() { this.slider = this.slider.rebuild(); this.$emit('rebuild'); }, getInfo: function() { this.$emit('getInfo', this.slider.getInfo(), this.slider); }, destroy: function() { this.slider.destroy(); }, init: async function() { // Lazy-import tiny-slider so the module's top-level `document` / // `window` access never runs during SSR (mounted only fires on the // client, so this import only happens client-side). // // We import the pre-built `dist/tiny-slider.js` (single-file CJS // bundle) rather than the raw `src/tiny-slider`. The ESM source // exports an unbound `requestAnimationFrame` reference; modern // bundlers turn the import into namespace-object access // (`mod.raf(...)`) which sets `this = mod` and trips an // "Illegal invocation" inside the browser API on every pan/drag. // The CJS bundle keeps `raf(...)` as a bare call, so it works. // The dist path is explicit because some bundlers (Bun) prefer // the ESM `src/` even when `main` points at the dist bundle. var { tns } = await import('tiny-slider/dist/tiny-slider.js'); var settings = { container: this.$el, axis: this.axis, items: parseInt(this.items), mode: this.mode, gutter: this.gutter, edgePadding: this.edgePadding, fixedWidth: !this.fixedWidth ? this.fixedWidth : parseInt(this.fixedWidth, 10), viewportMax: this.viewportMax, slideBy: this.slideBy, controls: this.controls, controlsPosition: this.controlsPosition, controlsText: this.controlsText, controlsContainer: this.controlsContainer, prevButton: this.prevButton, nextButton: this.nextButton, nav: this.nav, navPosition: this.navPosition, navContainer: this.navContainer, navAsThumbnails: this.navAsThumbnails, arrowKeys: this.arrowKeys, speed: this.speed, autoplay: this.autoplay, autoplayTimeout: this.autoplayTimeout, autoplayDirection: this.autoplayDirection, autoplayText: this.autoplayText, autoplayHoverPause: this.autoplayHoverPause, autoplayButton: this.autoplayButton, autoplayButtonOutput: this.autoplayButtonOutput, autoplayResetOnVisibility: this.autoplayResetOnVisibility, animateIn: this.animateIn, animateOut: this.animateOut, animateNormal: this.animateNormal, animateDelay: this.animateDelay, loop: this.loop, rewind: this.rewind, autoHeight: this.autoHeight, responsive: this.responsive, lazyload: this.lazyload, touch: this.touch, mouseDrag: this.mouseDrag, nested: this.nested, freezable: this.freezable, disable: this.disable, onInit: this.onInit, swipeAngle: this.swipeAngle, startIndex: this.startIndex, center: this.center, lazyLoadSelector: this.lazyLoadSelector, preventActionWhenRunning: this.preventActionWhenRunning, preventScrollOnTouch: this.preventScrollOnTouch, autoWidth: this.autoWidth, useLocalStorage: this.useLocalStorage }; removeUndefinedProps(settings); this.slider = tns(settings); // Emit init event this.$emit('init'); // Subscribe to all kind of tiny-slider events this.$_vueTinySlider_subscribeToAll(); }, }, render: function(){ return h('div', this.$slots.default ? this.$slots.default() : []); } }; function removeUndefinedProps(obj) { for (var prop in obj) { if (obj.hasOwnProperty(prop) && obj[prop] === undefined) { delete obj[prop]; } } } export default VueTinySlider; ================================================ FILE: tests/index.test.js ================================================ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; // Capture the mock so each test can inspect/drive it. const tnsMock = vi.fn(); vi.mock('tiny-slider/dist/tiny-slider.js', () => ({ tns: (...args) => tnsMock(...args) })); import VueTinySlider from '../src/index.js'; function makeFakeSlider(overrides = {}) { return { events: { on: vi.fn() }, goTo: vi.fn(), rebuild: vi.fn(function () { return this; }), getInfo: vi.fn(() => ({ index: 0 })), destroy: vi.fn(), ...overrides }; } // init() is async (lazy-imports tiny-slider), so after mount we flush // microtasks before assertions that depend on the slider being ready. async function mountSlider(props = {}, slots = undefined) { const wrapper = mount(VueTinySlider, { props, slots: slots ?? { default: '
A
B
' } }); await flushPromises(); return wrapper; } beforeEach(() => { tnsMock.mockReset(); tnsMock.mockImplementation(() => makeFakeSlider()); }); describe('VueTinySlider', () => { it('renders default slot children into a
', async () => { const wrapper = await mountSlider(); expect(wrapper.element.tagName).toBe('DIV'); expect(wrapper.findAll('.s')).toHaveLength(2); }); it('calls tns() on mount when autoInit is true (default)', async () => { await mountSlider(); expect(tnsMock).toHaveBeenCalledTimes(1); }); it('does NOT call tns() on mount when autoInit is false', async () => { await mountSlider({ autoInit: false }); expect(tnsMock).not.toHaveBeenCalled(); }); it('passes the component root element as container', async () => { const wrapper = await mountSlider(); const settings = tnsMock.mock.calls[0][0]; expect(settings.container).toBe(wrapper.element); }); it('forwards key props to tns(), coercing items to int', async () => { await mountSlider({ items: '3', mode: 'gallery', loop: false, mouseDrag: true }); const settings = tnsMock.mock.calls[0][0]; expect(settings.items).toBe(3); expect(settings.mode).toBe('gallery'); expect(settings.loop).toBe(false); expect(settings.mouseDrag).toBe(true); }); it('fixedWidth passes through as false when falsy, or parsed int when set', async () => { await mountSlider(); expect(tnsMock.mock.calls[0][0].fixedWidth).toBe(false); tnsMock.mockClear(); await mountSlider({ fixedWidth: '250' }); expect(tnsMock.mock.calls[0][0].fixedWidth).toBe(250); }); it('strips undefined props before calling tns()', async () => { // `axis` has no default, so it will be undefined unless set. await mountSlider(); const settings = tnsMock.mock.calls[0][0]; expect('axis' in settings).toBe(false); }); it('emits init after tns() is created', async () => { const wrapper = await mountSlider(); expect(wrapper.emitted('init')).toBeTruthy(); expect(wrapper.emitted('init')).toHaveLength(1); }); it('subscribes to every tiny-slider event in eventsList and re-emits them', async () => { const fake = makeFakeSlider(); tnsMock.mockImplementationOnce(() => fake); const wrapper = await mountSlider(); const expected = VueTinySlider.eventsList; expect(fake.events.on).toHaveBeenCalledTimes(expected.length); const subscribedNames = fake.events.on.mock.calls.map(c => c[0]); expect(subscribedNames).toEqual(expected); // Simulate tiny-slider firing indexChanged; component should $emit it. const indexChangedHandler = fake.events.on.mock.calls.find( c => c[0] === 'indexChanged' )[1]; indexChangedHandler({ index: 4 }); expect(wrapper.emitted('indexChanged')).toBeTruthy(); expect(wrapper.emitted('indexChanged')[0]).toEqual([{ index: 4 }]); }); it('goTo(value) delegates to the underlying slider', async () => { const fake = makeFakeSlider(); tnsMock.mockImplementationOnce(() => fake); const wrapper = await mountSlider(); wrapper.vm.goTo(2); expect(fake.goTo).toHaveBeenCalledWith(2); }); it('rebuild() replaces slider and emits rebuild', async () => { const fake = makeFakeSlider(); tnsMock.mockImplementationOnce(() => fake); const wrapper = await mountSlider(); wrapper.vm.rebuild(); expect(fake.rebuild).toHaveBeenCalled(); expect(wrapper.emitted('rebuild')).toBeTruthy(); }); it('getInfo() emits getInfo with payload and slider', async () => { const info = { index: 7 }; const fake = makeFakeSlider({ getInfo: vi.fn(() => info) }); tnsMock.mockImplementationOnce(() => fake); const wrapper = await mountSlider(); wrapper.vm.getInfo(); const emitted = wrapper.emitted('getInfo'); expect(emitted).toBeTruthy(); expect(emitted[0][0]).toBe(info); expect(emitted[0][1]).toBe(fake); }); it('destroy() calls slider.destroy', async () => { const fake = makeFakeSlider(); tnsMock.mockImplementationOnce(() => fake); const wrapper = await mountSlider(); wrapper.vm.destroy(); expect(fake.destroy).toHaveBeenCalled(); }); it('destroys the slider when the component is unmounted', async () => { const fake = makeFakeSlider(); tnsMock.mockImplementationOnce(() => fake); const wrapper = await mountSlider(); wrapper.unmount(); expect(fake.destroy).toHaveBeenCalled(); }); it('controlsText default is [prev, next]', async () => { await mountSlider(); expect(tnsMock.mock.calls[0][0].controlsText).toEqual(['prev', 'next']); }); }); ================================================ FILE: tests/ssr.test.js ================================================ // @vitest-environment node // Regression guard: importing the component in a pure Node context // (no window, no document, no DOM globals) must NOT throw. This catches // top-level browser references leaking back into the module eval path. import { describe, it, expect } from 'vitest'; describe('SSR safety', () => { it('component module can be imported in Node without DOM globals', async () => { const mod = await import('../src/index.js'); expect(mod.default).toBeTypeOf('object'); expect(mod.default.props).toBeTypeOf('object'); }); it('does not reference window/document at module-eval time', async () => { // Assert absence of the browser globals before import — proving // the happy-dom env isn't leaking into this test. expect(typeof window).toBe('undefined'); expect(typeof document).toBe('undefined'); // Re-import is a no-op (cached), but keeps the assertion meaningful. await import('../src/index.js'); }); }); ================================================ FILE: tests/types-check.ts ================================================ // Smoke-check the published types from a consumer perspective. // Run: npx tsc --noEmit tests/types-check.ts (or let CI do it) import VueTinySlider, { type VueTinySliderProps, type VueTinySliderInstance, type TinySliderInfo, type SilderEvent } from '../src/index.js'; // Default export is a Vue component. const _component = VueTinySlider; // Prop interface accepts documented options. const _props: VueTinySliderProps = { items: 3, gutter: 20, loop: false, mouseDrag: true, autoInit: false, controlsPosition: 'bottom', controlsText: ['prev', 'next'], mode: 'carousel', autoplay: true, preventScrollOnTouch: 'auto' }; // Instance shape available via $refs. const _fakeRef: VueTinySliderInstance = { slider: null, init: () => Promise.resolve(), goTo: (_t: number | 'next' | 'prev' | 'first' | 'last') => {}, rebuild: () => {}, getInfo: () => {}, destroy: () => {} }; // Event name type is re-exported from tiny-slider. const _evt: SilderEvent = 'indexChanged'; const _info: TinySliderInfo = {} as TinySliderInfo; export { _component, _props, _fakeRef, _evt, _info }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, "esModuleInterop": true, "noEmit": true }, "include": ["src/**/*.d.ts", "tests/types-check.ts"] } ================================================ FILE: vite.config.js ================================================ import { defineConfig } from 'vite'; import path from 'path'; export default defineConfig({ build: { lib: { entry: path.resolve(__dirname, 'src/index.js'), name: 'vue-tiny-slider', fileName: () => 'index.js', formats: ['umd'] }, rollupOptions: { // Keep vue and tiny-slider as runtime externals. tiny-slider must // not be inlined because it touches `document`/`window` at module // eval time — see SSR notes in README. external: ['vue', /^tiny-slider(\/|$)/], output: { globals: { vue: 'Vue', 'tiny-slider': 'tns' } } }, sourcemap: true } }); ================================================ FILE: vitest.config.js ================================================ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'happy-dom', globals: false, include: ['tests/**/*.test.js'] } });