├── .eslintignore
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── deployment-config
└── vercel.json
├── package.json
├── pnpm-lock.yaml
├── public
├── favicon.ico
├── index.html
├── logo.svg
└── robots.txt
├── snowpack.config.js
├── src
├── App.svelte
├── calendar-style.js
├── components
│ ├── DatePicker.svelte
│ ├── Popover.svelte
│ ├── Toolbar.svelte
│ ├── lib
│ │ ├── calendar-page.js
│ │ ├── calendar-page.spec.js
│ │ ├── calendar.js
│ │ ├── calendar.spec.js
│ │ ├── context.js
│ │ ├── context.spec.js
│ │ ├── date-manipulation.js
│ │ ├── date-manipulation.spec.js
│ │ ├── date-utils.js
│ │ ├── day-selection-validator.js
│ │ ├── day-selection-validator.spec.js
│ │ ├── event-handling.js
│ │ ├── formatter.js
│ │ ├── positioning.js
│ │ ├── sanitization.js
│ │ └── view-context.js
│ └── view
│ │ ├── View.svelte
│ │ ├── date-view
│ │ ├── DateView.svelte
│ │ ├── Month.svelte
│ │ ├── NavBar.svelte
│ │ ├── Week.svelte
│ │ ├── date-comparison.js
│ │ ├── feedback.js
│ │ └── keyboard.js
│ │ └── time-view
│ │ ├── Chevron.svelte
│ │ ├── TimeInput.svelte
│ │ ├── TimeView.svelte
│ │ ├── time-input.js
│ │ └── time-store.js
├── index.js
├── main.js
├── normalize.css
├── prettify.css
├── style.css
└── test.js
└── web-test-runner.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/index.js
2 | build
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | tags:
8 | - 'v*'
9 | pull_request:
10 | branches:
11 | - '*'
12 |
13 | env:
14 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 | - uses: actions/setup-node@v3
22 | with:
23 | node-version: 16
24 | - uses: pnpm/action-setup@v2
25 | name: Install pnpm
26 | with:
27 | version: 8
28 | run_install: true
29 | - run: npm run lint
30 | - run: npm run test:unit
31 |
32 | docs:
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v3
36 | - uses: actions/setup-node@v3
37 | with:
38 | node-version: 16
39 | - uses: pnpm/action-setup@v2
40 | name: Install pnpm
41 | with:
42 | version: 8
43 | run_install: true
44 | - name: Deploy docs
45 | run: |
46 | pnpm build
47 | cd build
48 | npx vercel -A ../deployment-config/vercel.json --prod --token ${{ secrets.VERCEL_TOKEN }}
49 | env:
50 | NOW_PROJECT_ID: 'prj_1kSWQ4j4fKqpjLgCiqAydPBQPlmB'
51 | NOW_ORG_ID: 'team_ebDKq5BBTF4figtiNJc3VRH6'
52 |
53 | publish-npm:
54 | if: startsWith(github.ref, 'refs/tags/v')
55 | needs: build
56 | runs-on: ubuntu-latest
57 | steps:
58 | - uses: actions/checkout@v3
59 | - uses: actions/setup-node@v3
60 | with:
61 | node-version: 16
62 | - uses: pnpm/action-setup@v2
63 | name: Install pnpm
64 | with:
65 | version: 8
66 | run_install: true
67 | - name: Set publishing config
68 | run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
69 | - run: pnpm publish --no-git-checks
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | web_modules
3 | node_modules
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Fred K. Schott
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ## Svelte Datepicker
10 |
11 | [](http://standardjs.com) [](https://svelte.dev) 
12 |
13 | This is a near total rewrite of the excellent [Svelte Calendar](https://github.com/6eDesign/svelte-calendar). It provides:
14 |
15 | * Calendar
16 | * Date Picker
17 | * Date Range Picker
18 | * Time Selection
19 | * Better Responsiveness
20 | * Improved theming
21 | * Context-aware theming
22 | * Toolbar to avoid awkward bindings
23 | * Works in tough situations such as inside iframes
24 |
25 | Roadmap:
26 |
27 | * Re-introduce Keyboard Support
28 | * Add code-samples to docs
29 | * Add legend for keyboard shortcuts [h for Help]
30 |
31 | ## Svelte Kit Support
32 |
33 | Due to the way dayjs is packaged, the following configuration is required to get this working with SvelteKit:
34 |
35 | ```js
36 | const config = {
37 | kit: {
38 | target: "#svelte",
39 | vite: {
40 | ssr: {
41 | noExternal: [ 'dayjs' ]
42 | }
43 | }
44 | }
45 | }
46 | ```
47 |
48 | ## Usage
49 |
50 | * See [The Documentation](https://svelte-datepicker.vercel.app) which is a work in progress.
51 | * See [Small Svelte REPL](https://svelte.dev/repl/d812e880c6934f9e9a7cf9f760eddc11?version=3.31.2) for a minimum working verison.
52 |
53 | ## Contributing
54 |
55 | ### Tests
56 |
57 | Tests written in [uvu](https://github.com/lukeed/uvu)
58 |
59 | ```bash
60 | npm run test
61 | ```
62 |
--------------------------------------------------------------------------------
/deployment-config/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "routes": [
4 | { "handle": "filesystem" },
5 | { "src": "/.*", "dest": "/index.html" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@beyonk/svelte-datepicker",
3 | "svelte": "src/main.js",
4 | "version": "13.0.4",
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "snowpack dev",
8 | "build": "snowpack build",
9 | "lint": "eslint .",
10 | "test": "web-test-runner \"src/**/*.test.js\"",
11 | "test:unit": "uvu -r esm"
12 | },
13 | "devDependencies": {
14 | "@beyonk/eslint-config": "^4.2.0",
15 | "@beyonk/snowpack-rollup-plugin": "0.0.1",
16 | "@snowpack/plugin-dotenv": "^2.0.5",
17 | "@snowpack/plugin-svelte": "^3.5.1",
18 | "@snowpack/web-test-runner-plugin": "^0.2.1",
19 | "@testing-library/svelte": "^3.0.0",
20 | "@web/test-runner": "^0.9.7",
21 | "dayjs": "^1.11.9",
22 | "eslint": "^7.14.0",
23 | "eslint-plugin-svelte3": "^2.7.3",
24 | "esm": "^3.2.25",
25 | "mockdate": "^3.0.2",
26 | "sinon": "^9.2.4",
27 | "snowpack": "^3.0.10",
28 | "svelte": "^3.59.2",
29 | "tinro": "^0.3.7",
30 | "uvu": "^0.5.1"
31 | },
32 | "eslintConfig": {
33 | "extends": "@beyonk/eslint-config/svelte"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beyonk-group/svelte-datepicker/623cc625e184abd58f91598719d9ed9f2c5935c1/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Svelte Datepicker
9 |
10 |
11 |
12 |
13 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/snowpack.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import("snowpack").SnowpackUserConfig } */
2 | module.exports = {
3 | mount: {
4 | public: '/',
5 | src: '/_dist_'
6 | },
7 | routes: [
8 | { match: 'routes', src: '.*', dest: '/index.html' }
9 | ],
10 | plugins: [
11 | '@snowpack/plugin-svelte',
12 | '@snowpack/plugin-dotenv'
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/App.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 | Open Calendar
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
43 |
44 |
45 |
46 |
47 | Svelte DatePicker Developer Documentation
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
72 |
73 |
74 | Svelte Date Range Picker
75 |
79 |
80 |
81 |
82 |
91 | Svelte Date Picker
92 |
93 |
94 | Without Time Choice
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | With Date Restriction
104 | Restrict date from the start of the year until today
105 |
106 |
110 |
111 |
112 |
113 |
114 |
115 | With Time Choice
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | With Selected Date
124 |
125 |
129 |
130 |
131 |
132 | With Minute Step
133 |
134 |
138 |
139 |
140 |
141 |
142 | With Events
143 |
144 | {
148 | firedEvents = [
149 | ...firedEvents,
150 | `Picked date ${e.detail.date} (${firedEventsValue})`
151 | ]
152 | }}
153 | on:change={e => {
154 | firedEvents = [
155 | ...firedEvents,
156 | 'Change fired'
157 | ]
158 | }}
159 | />
160 |
161 |
162 | {#each firedEvents as fired}
163 | - {fired}
164 | {:else}
165 | - Pick date to see events
166 | {/each}
167 |
168 |
169 |
170 | With Custom Button
171 |
172 |
174 |
181 |
182 |
183 | * Beach Time by merkund
184 |
185 |
186 |
187 |
188 |
197 | Svelte Range Picker
198 |
199 |
200 | Without Time Choice
201 |
202 |
203 |
204 |
205 |
206 |
207 | With Responsive Positioning
208 | Scrolls calendar into view on small screens.
209 |
210 | Button is down there to the right!
211 |
212 |
215 | Open Calendar
216 |
217 |
220 | Open Calendar
221 |
222 |
223 |
224 |
225 |
226 | Oh no! Iframes!
227 |
228 |
229 |
231 |
232 |
233 |
234 |
235 | With Date Restriction
236 | Restrict date from the start of the year until today
237 |
238 |
239 |
245 |
246 |
247 |
248 |
249 |
250 |
251 | With Time Choice
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 | With Selected Dates
260 |
261 |
266 |
267 |
268 |
269 | With Events
270 |
271 | {
276 | firedEvents = [
277 | ...firedEvents,
278 | `Picked range ${e.detail.from} to ${e.detail.to}`
279 | ]
280 | }}
281 | on:change={e => {
282 | firedEvents = [
283 | ...firedEvents,
284 | 'Change fired'
285 | ]
286 | }}
287 | />
288 |
289 |
290 | {#each firedEvents as fired}
291 | - {fired}
292 | {:else}
293 | - Pick date to see events
294 | {/each}
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
313 |
322 |
323 |
324 |
339 |
340 |
--------------------------------------------------------------------------------
/src/calendar-style.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | class CalendarStyle {
4 | constructor (overrides = {}) {
5 | this.style = ''
6 | this.buttonBackgroundColor = '#fff'
7 | this.buttonBorderColor = '#eee'
8 | this.buttonTextColor = '#333'
9 | this.buttonWidth = '300px'
10 | this.highlightColor = '#f7901e'
11 | this.passiveHighlightColor = '#FCD9B1'
12 |
13 | this.dayBackgroundColor = 'none'
14 | this.dayBackgroundColorIsNight = 'none'
15 | this.dayTextColor = '#4a4a4a'
16 | this.dayTextColorIsNight = '#4a4a4a'
17 | this.dayTextColorInRange = 'white'
18 | this.dayHighlightedBackgroundColor = '#efefef'
19 | this.dayHighlightedTextColor = '#4a4a4a'
20 |
21 | this.currentDayTextColor = '#000'
22 | this.selectedDayTextColor = 'white'
23 |
24 | this.timeNightModeTextColor = 'white'
25 | this.timeNightModeBackgroundColor = '#808080'
26 | this.timeDayModeTextColor = 'white'
27 | this.timeDayModeBackgroundColor = 'white'
28 | this.timeSelectedTextColor = '#3d4548'
29 | this.timeInputTextColor = '#3d4548'
30 | this.timeConfirmButtonColor = '#2196F3'
31 | this.timeConfirmButtonTextColor = 'white'
32 |
33 | this.toolbarBorderColor = '#888'
34 |
35 | this.contentBackground = 'white'
36 |
37 | this.monthYearTextColor = '#3d4548'
38 | this.legendTextColor = '#4a4a4a'
39 |
40 | this.datepickerWidth = 'auto'
41 |
42 | Object.entries(overrides).forEach(([ prop, value ]) => {
43 | this[prop] = value
44 | })
45 | }
46 |
47 | toWrapperStyle () {
48 | return `
49 | --button-background-color: ${this.buttonBackgroundColor};
50 | --button-border-color: ${this.buttonBorderColor};
51 | --button-text-color: ${this.buttonTextColor};
52 | --button-width: ${this.buttonWidth};
53 | --highlight-color: ${this.highlightColor};
54 | --passive-highlight-color: ${this.passiveHighlightColor};
55 |
56 | --day-background-color: ${this.dayBackgroundColor};
57 | --day-background-color-is-night: ${this.dayBackgroundColorIsNight};
58 | --day-text-color: ${this.dayTextColor};
59 | --day-text-color-in-range: ${this.dayTextColorInRange};
60 | --day-text-color-is-night: ${this.dayTextColorIsNight};
61 | --day-highlighted-background-color: ${this.dayHighlightedBackgroundColor};
62 | --day-highlighted-text-color: ${this.dayHighlightedTextColor};
63 |
64 | --current-day-text-color: ${this.currentDayTextColor};
65 | --selected-day-text-color: ${this.selectedDayTextColor};
66 |
67 | --time-night-mode-text-color: ${this.timeNightModeTextColor};
68 | --time-night-mode-background-color: ${this.timeNightModeBackgroundColor};
69 | --time-day-mode-text-color: ${this.timeDayModeTextColor};
70 | --time-day-mode-background-color: ${this.timeDayModeBackgroundColor};
71 |
72 | --time-selected-text-color: ${this.timeSelectedTextColor};
73 | --time-input-text-color: ${this.timeInputTextColor};
74 | --time-confirm-button-text-color: ${this.timeConfirmButtonTextColor};
75 | --time-confirm-button-color: ${this.timeConfirmButtonColor};
76 |
77 | --toolbar-border-color: ${this.toolbarBorderColor};
78 |
79 | --content-background: ${this.contentBackground};
80 |
81 | --month-year-text-color: ${this.monthYearTextColor};
82 | --legend-text-color: ${this.legendTextColor};
83 | --datepicker-width: ${this.datepickerWidth};
84 |
85 | ${this.style}
86 | `
87 | }
88 | }
89 |
90 | export {
91 | CalendarStyle
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/DatePicker.svelte:
--------------------------------------------------------------------------------
1 |
120 |
121 |
170 |
171 |
176 |
dispatch('close')}>
181 |
182 |
183 | {#if !trigger}
184 |
191 | {/if}
192 |
193 |
194 |
195 |
196 |
200 | {#if config.isRangePicker}
201 |
205 | {/if}
206 |
207 |
208 |
209 |
210 |
211 |
--------------------------------------------------------------------------------
/src/components/Popover.svelte:
--------------------------------------------------------------------------------
1 |
66 |
67 |
68 |
69 |
e.key === 'Enter' && doOpen()} bind:this={triggerContainer}>
70 |
71 |
72 |
73 |
86 |
87 |
88 |
174 |
--------------------------------------------------------------------------------
/src/components/Toolbar.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/lib/calendar-page.js:
--------------------------------------------------------------------------------
1 | function getCalendarPage (date, dayValidator) {
2 | const displayedRangeStart = date.startOf('month').startOf('week')
3 | const displayedRangeEnd = date.endOf('month').endOf('week').add(1, 'day')
4 |
5 | const weeks = []
6 | let currentDay = displayedRangeStart
7 | while (currentDay.isBefore(displayedRangeEnd, 'day')) {
8 | const weekOfMonth = Math.floor(currentDay.diff(displayedRangeStart, 'days') / 7)
9 | const isRequestedMonth = currentDay.isSame(date, 'month')
10 | weeks[weekOfMonth] = weeks[weekOfMonth] || { days: [], id: `${currentDay.format('YYYYMMYYYY')}${weekOfMonth}` }
11 | weeks[weekOfMonth].days.push(
12 | Object.assign({
13 | partOfMonth: isRequestedMonth,
14 | firstOfMonth: isRequestedMonth && currentDay.date() === 1,
15 | lastOfMonth: isRequestedMonth && currentDay.date() === date.daysInMonth(),
16 | day: currentDay.date(),
17 | month: currentDay.month(),
18 | year: currentDay.year(),
19 | date: currentDay
20 | }, dayValidator(currentDay))
21 | )
22 | currentDay = currentDay.add(1, 'day')
23 | }
24 |
25 | return { month: date.month(), year: date.year(), weeks }
26 | }
27 |
28 | export {
29 | getCalendarPage
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/lib/calendar-page.spec.js:
--------------------------------------------------------------------------------
1 | import { suite as Suite } from 'uvu'
2 | import assert from 'uvu/assert'
3 | import { getCalendarPage } from './calendar-page.js'
4 | import { dayjs } from './date-utils.js'
5 | import MockDate from 'mockdate'
6 |
7 | const suite = Suite('calendar-page/getCalendarPage')
8 |
9 | suite.before(() => {
10 | MockDate.set('2020-04-20')
11 | const date = dayjs()
12 | suite.ctx = {
13 | date,
14 | page: getCalendarPage(date, () => ({
15 | isInRange: true,
16 | isSelected: true,
17 | isToday: true
18 | }))
19 | }
20 | })
21 |
22 | suite.after(() => {
23 | MockDate.reset()
24 | })
25 |
26 | suite('returns calendar page month', () => {
27 | assert.equal(
28 | suite.ctx.page.month,
29 | suite.ctx.date.month()
30 | )
31 | })
32 |
33 | suite('returns calendar page year', () => {
34 | assert.equal(
35 | suite.ctx.page.year,
36 | suite.ctx.date.year()
37 | )
38 | })
39 |
40 | suite('returns calendar page weeks', () => {
41 | assert.equal(
42 | suite.ctx.page.weeks.length,
43 | 5
44 | )
45 | })
46 |
47 | suite.run()
48 |
--------------------------------------------------------------------------------
/src/components/lib/calendar.js:
--------------------------------------------------------------------------------
1 | import { dayjs } from './date-utils'
2 | import { ensureFutureMonth } from './date-manipulation.js'
3 | import { buildDaySelectionValidator } from './day-selection-validator.js'
4 | import { getCalendarPage } from './calendar-page.js'
5 |
6 | function getMonths (config) {
7 | const { start, end, selectableCallback } = config
8 | const firstMonth = start.startOf('month').startOf('day')
9 | const lastMonth = ensureFutureMonth(firstMonth, end.startOf('month').startOf('day'))
10 |
11 | const months = []
12 | const validator = buildDaySelectionValidator(start, end, selectableCallback)
13 | let date = dayjs(firstMonth)
14 | while (date.isSameOrBefore(lastMonth)) {
15 | months.push(getCalendarPage(date, validator))
16 | date = date.add(1, 'month')
17 | }
18 | return months
19 | }
20 |
21 | export {
22 | getMonths
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/lib/calendar.spec.js:
--------------------------------------------------------------------------------
1 | import { suite as Suite } from 'uvu'
2 | import assert from 'uvu/assert'
3 | import { getMonths } from './calendar.js'
4 | import { dayjs } from './date-utils.js'
5 | import MockDate from 'mockdate'
6 | import { stub } from 'sinon'
7 | import * as daySelectionValidator from './day-selection-validator.js'
8 |
9 | const suite = Suite('calendar/getMonths')
10 |
11 | suite.before(() => {
12 | MockDate.set('2020-04-20')
13 | stub(daySelectionValidator, 'buildDaySelectionValidator').returns(() => {})
14 | })
15 |
16 | suite.after(() => {
17 | MockDate.reset()
18 | daySelectionValidator.buildDaySelectionValidator.restore()
19 | })
20 |
21 | const config = {
22 | start: dayjs('2020-03-10'),
23 | end: dayjs('2020-06-25'),
24 | selectableCallback: () => {}
25 | }
26 |
27 | suite('has correct month count', () => {
28 | const months = getMonths(config)
29 |
30 | assert.equal(
31 | months.length,
32 | 4
33 | )
34 | })
35 |
36 | suite('calls day selection validator with correct arguments', () => {
37 | getMonths(config)
38 |
39 | assert.equal(
40 | daySelectionValidator.buildDaySelectionValidator.firstCall.args,
41 | [ config.start, config.end, config.selectableCallback ]
42 | )
43 | })
44 |
45 | suite.run()
46 |
--------------------------------------------------------------------------------
/src/components/lib/context.js:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store'
2 | import { createFormatter } from './formatter.js'
3 | import { getMonths } from './calendar.js'
4 | import { sanitizeInitialValue } from './sanitization.js'
5 | import { dayjs } from './date-utils.js'
6 | import { ensureFutureMonth } from './date-manipulation.js'
7 |
8 | const contextKey = {}
9 |
10 | function setup (given, config) {
11 | const today = dayjs().startOf('day')
12 |
13 | const { isDateChosen, chosen: [ preSelectedStart, preSelectedEnd ] } = sanitizeInitialValue(given, config)
14 | const selectedStartDate = writable(preSelectedStart)
15 | const selectedEndDate = writable(preSelectedEnd)
16 | const { formatter } = createFormatter(selectedStartDate, selectedEndDate, config)
17 | const component = writable('date-view')
18 |
19 | const leftDate = preSelectedStart.subtract(
20 | config.isRangePicker && preSelectedStart.isSame(config.end, 'month') ? 1 : 0, 'month'
21 | ).startOf('month')
22 | const rightDate = config.isRangePicker ? ensureFutureMonth(leftDate, preSelectedEnd).startOf('month') : null
23 |
24 | return {
25 | months: getMonths(config),
26 | component,
27 | today,
28 | selectedStartDate,
29 | selectedEndDate,
30 | leftCalendarDate: writable(leftDate),
31 | rightCalendarDate: writable(rightDate),
32 | config,
33 | shouldShakeDate: writable(false),
34 | isOpen: writable(false),
35 | isClosing: writable(false),
36 | highlighted: writable(today),
37 | formatter,
38 | isDateChosen: writable(isDateChosen),
39 | resetView: () => {
40 | component.set('date-view')
41 | },
42 | isSelectingFirstDate: writable(true)
43 | }
44 | }
45 |
46 | export {
47 | contextKey,
48 | setup
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/lib/context.spec.js:
--------------------------------------------------------------------------------
1 | import { suite as Suite } from 'uvu'
2 | import assert from 'uvu/assert'
3 | import { setup } from './context.js'
4 | import { dayjs } from './date-utils.js'
5 | import MockDate from 'mockdate'
6 | import { get } from 'svelte/store'
7 |
8 | const defaults = Suite('setup/range-picker/defaults')
9 |
10 | defaults.before(() => {
11 | MockDate.set('2020-04-20')
12 | defaults.ctx = {
13 | config: {
14 | start: dayjs().subtract(1, 'year'),
15 | end: dayjs().add(1, 'year'),
16 | isRangePicker: true
17 | }
18 | }
19 | defaults.ctx.output = setup(undefined, defaults.ctx.config)
20 | })
21 |
22 | defaults.after(() => {
23 | MockDate.reset()
24 | })
25 |
26 | defaults('has correct start date', () => {
27 | const selectedStartDate = get(defaults.ctx.output.selectedStartDate)
28 | assert.equal(selectedStartDate.toDate(), dayjs('2020-04-20').toDate())
29 | })
30 |
31 | defaults('has correct end date', () => {
32 | const selectedEndDate = get(defaults.ctx.output.selectedEndDate)
33 | assert.equal(selectedEndDate.toDate(), dayjs('2020-05-20').toDate())
34 | })
35 |
36 | defaults('has two years worth of months plus one extra', () => {
37 | assert.equal(defaults.ctx.output.months.length, 25)
38 | })
39 |
40 | defaults('has correct view component', () => {
41 | const component = get(defaults.ctx.output.component)
42 | assert.equal(component, 'date-view')
43 | })
44 |
45 | defaults('has today', () => {
46 | assert.equal(defaults.ctx.output.today, dayjs().startOf('day'))
47 | })
48 |
49 | defaults('has correct left date', () => {
50 | const leftCalendarDate = get(defaults.ctx.output.leftCalendarDate)
51 | assert.equal(leftCalendarDate.toDate(), dayjs('2020-04-20').toDate())
52 | })
53 |
54 | defaults('has correct right date', () => {
55 | const rightCalendarDate = get(defaults.ctx.output.rightCalendarDate)
56 | assert.equal(rightCalendarDate.toDate(), dayjs('2020-05-20').toDate())
57 | })
58 |
59 | defaults('has passed configuration', () => {
60 | assert.is(defaults.ctx.output.config, defaults.ctx.config)
61 | })
62 |
63 | defaults('has correct open state', () => {
64 | const state = get(defaults.ctx.output.isOpen)
65 | assert.not(state)
66 | })
67 |
68 | defaults('has correct closing state', () => {
69 | const state = get(defaults.ctx.output.isClosing)
70 | assert.not(state)
71 | })
72 |
73 | defaults('has correct highlighted day', () => {
74 | const highlightedDay = get(defaults.ctx.output.highlighted)
75 | assert.equal(highlightedDay.toDate(), dayjs().toDate())
76 | })
77 |
78 | defaults('does not have a chosen date', () => {
79 | const isDateChosen = get(defaults.ctx.output.isDateChosen)
80 | assert.not(isDateChosen)
81 | })
82 |
83 | defaults('has correct user state', () => {
84 | const isSelectingFirstDate = get(defaults.ctx.output.isSelectingFirstDate)
85 | assert.ok(isSelectingFirstDate)
86 | })
87 |
88 | defaults('has reset function', () => {
89 | assert.type(defaults.ctx.output.resetView, 'function')
90 | })
91 |
92 | defaults.run()
93 |
94 | const sameMonth = Suite('setup/date-range/selected-dates/same-month')
95 |
96 | sameMonth.before(() => {
97 | MockDate.set('2020-04-20')
98 | sameMonth.ctx = {
99 | config: {
100 | start: dayjs().subtract(1, 'year'),
101 | end: dayjs().add(1, 'year'),
102 | selected: [
103 | dayjs('2020-04-25'),
104 | dayjs('2020-04-27')
105 | ],
106 | isRangePicker: true
107 | }
108 | }
109 | sameMonth.ctx.output = setup(undefined, sameMonth.ctx.config)
110 | })
111 |
112 | sameMonth.after(() => {
113 | MockDate.reset()
114 | })
115 |
116 | sameMonth('correct left-hand month is displayed', () => {
117 | const date = get(sameMonth.ctx.output.leftCalendarDate)
118 | assert.equal(date.toDate(), dayjs('2020-04-01').toDate())
119 | })
120 |
121 | sameMonth('correct right-hand month is displayed', () => {
122 | const date = get(sameMonth.ctx.output.rightCalendarDate)
123 | assert.equal(date.toDate(), dayjs('2020-05-01').toDate())
124 | })
125 |
126 | sameMonth.run()
127 |
128 | const withSelectedEndInsideRange = Suite('setup/date-range/default-dates/selected-inside-range')
129 |
130 | withSelectedEndInsideRange.before(() => {
131 | MockDate.set('2021-02-04')
132 | withSelectedEndInsideRange.ctx = {
133 | config: {
134 | start: dayjs('2021-01-11'),
135 | end: dayjs('2021-04-18'),
136 | isRangePicker: true
137 | }
138 | }
139 | withSelectedEndInsideRange.ctx.output = setup(undefined, withSelectedEndInsideRange.ctx.config)
140 | })
141 |
142 | withSelectedEndInsideRange.after(() => {
143 | MockDate.reset()
144 | })
145 |
146 | withSelectedEndInsideRange('left-hand month is start of default selection', () => {
147 | const date = get(withSelectedEndInsideRange.ctx.output.leftCalendarDate)
148 | assert.equal(date.format('YYYY-MM-DD'), '2021-02-01')
149 | })
150 |
151 | withSelectedEndInsideRange('right hand month is next month', () => {
152 | const date = get(withSelectedEndInsideRange.ctx.output.rightCalendarDate)
153 | assert.equal(date.format('YYYY-MM-DD'), '2021-03-01')
154 | })
155 |
156 | withSelectedEndInsideRange.run()
157 |
--------------------------------------------------------------------------------
/src/components/lib/date-manipulation.js:
--------------------------------------------------------------------------------
1 | function ensureFutureMonth (firstDate, secondDate) {
2 | return firstDate.isSame(secondDate, 'month') ? secondDate.add(1, 'month') : secondDate
3 | }
4 |
5 | export {
6 | ensureFutureMonth
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/lib/date-manipulation.spec.js:
--------------------------------------------------------------------------------
1 | import { suite as Suite } from 'uvu'
2 | import assert from 'uvu/assert'
3 | import { dayjs } from './date-utils.js'
4 | import MockDate from 'mockdate'
5 | import { ensureFutureMonth } from './date-manipulation.js'
6 |
7 | const month = Suite('ensureFutureMonth')
8 |
9 | month.before(() => {
10 | MockDate.set('2020-04-20')
11 | })
12 |
13 | month.after(() => {
14 | MockDate.reset()
15 | })
16 |
17 | month('when same month', () => {
18 | const start = dayjs('2020-04-01')
19 | const end = dayjs('2020-04-30')
20 | const expected = end.add(1, 'month').month()
21 | assert.equal(
22 | ensureFutureMonth(start, end).month(),
23 | expected
24 | )
25 | })
26 |
27 | month('when future month', () => {
28 | const start = dayjs('2020-04-30')
29 | const end = dayjs('2020-05-01')
30 | const expected = end.month()
31 | assert.equal(
32 | ensureFutureMonth(start, end).month(),
33 | expected
34 | )
35 | })
36 |
37 | month('when previous month', () => {
38 | const start = dayjs('2020-06-01')
39 | const end = dayjs('2020-05-31')
40 | const expected = end.month()
41 | assert.equal(
42 | ensureFutureMonth(start, end).month(),
43 | expected
44 | )
45 | })
46 |
47 | month.run()
48 |
--------------------------------------------------------------------------------
/src/components/lib/date-utils.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import localeData from 'dayjs/plugin/localeData'
3 | import minMax from 'dayjs/plugin/minMax'
4 | import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
5 | import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
6 |
7 | dayjs.extend(localeData)
8 | dayjs.extend(minMax)
9 | dayjs.extend(isSameOrBefore)
10 | dayjs.extend(isSameOrAfter)
11 |
12 | export {
13 | dayjs
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/lib/day-selection-validator.js:
--------------------------------------------------------------------------------
1 | import { dayjs } from './date-utils'
2 |
3 | function buildDaySelectionValidator (start, end, selectableCallback) {
4 | return date => {
5 | const isInRange = date.isSameOrAfter(start, 'day') && date.isSameOrBefore(end, 'day')
6 | return {
7 | isInRange,
8 | selectable: isInRange && (!selectableCallback || selectableCallback(date.toDate())),
9 | isToday: date.isSame(dayjs(), 'day')
10 | }
11 | }
12 | }
13 |
14 | export {
15 | buildDaySelectionValidator
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/lib/day-selection-validator.spec.js:
--------------------------------------------------------------------------------
1 | import { suite as Suite } from 'uvu'
2 | import assert from 'uvu/assert'
3 | import { dayjs } from './date-utils.js'
4 | import MockDate from 'mockdate'
5 | import { buildDaySelectionValidator } from './day-selection-validator.js'
6 |
7 | const validator = Suite('buildDaySelectionValidator/no-selectable-callback')
8 |
9 | const start = dayjs('2020-07-20')
10 | const end = dayjs('2020-07-24')
11 |
12 | validator.before(() => {
13 | MockDate.set('2020-04-20')
14 | validator.ctx = {
15 | fn: buildDaySelectionValidator(start, end)
16 | }
17 | })
18 |
19 | validator.after(() => {
20 | MockDate.reset()
21 | })
22 |
23 | validator('returns a function', () => {
24 | assert.type(validator.ctx.fn, 'function')
25 | })
26 |
27 | validator('with today', () => {
28 | const given = dayjs('2020-04-20')
29 | assert.equal(
30 | validator.ctx.fn(given),
31 | {
32 | isInRange: false,
33 | selectable: false,
34 | isToday: true
35 | }
36 | )
37 | })
38 |
39 | validator('later today', () => {
40 | const given = dayjs().endOf('day')
41 | assert.equal(
42 | validator.ctx.fn(given),
43 | {
44 | isInRange: false,
45 | selectable: false,
46 | isToday: true
47 | }
48 | )
49 | })
50 |
51 | validator('with date before start', () => {
52 | assert.equal(
53 | validator.ctx.fn(start.subtract(1, 'day')),
54 | {
55 | isInRange: false,
56 | selectable: false,
57 | isToday: false
58 | }
59 | )
60 | })
61 |
62 | validator('with first day of range', () => {
63 | assert.equal(
64 | validator.ctx.fn(start),
65 | {
66 | isInRange: true,
67 | selectable: true,
68 | isToday: false
69 | }
70 | )
71 | })
72 |
73 | validator('with last day of range', () => {
74 | assert.equal(
75 | validator.ctx.fn(end),
76 | {
77 | isInRange: true,
78 | selectable: true,
79 | isToday: false
80 | }
81 | )
82 | })
83 |
84 | validator('with date after end', () => {
85 | assert.equal(
86 | validator.ctx.fn(end.add(1, 'day')),
87 | {
88 | isInRange: false,
89 | selectable: false,
90 | isToday: false
91 | }
92 | )
93 | })
94 |
95 | validator.run()
96 |
--------------------------------------------------------------------------------
/src/components/lib/event-handling.js:
--------------------------------------------------------------------------------
1 |
2 | const once = (el, evt, cb) => {
3 | if (!el) { return }
4 | function handler () {
5 | cb.apply(this, arguments)
6 | el.removeEventListener(evt, handler)
7 | }
8 | el.addEventListener(evt, handler)
9 | }
10 |
11 | export {
12 | once
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/lib/formatter.js:
--------------------------------------------------------------------------------
1 | import { derived } from 'svelte/store'
2 |
3 | function createFormatter (selectedStartDate, selectedEndDate, config) {
4 | const formatter = derived([ selectedStartDate, selectedEndDate ], ([ $selectedStartDate, $selectedEndDate ]) => {
5 | const formattedSelected = $selectedStartDate && $selectedStartDate.format(config.format)
6 | const formattedSelectedEnd = config.isRangePicker && $selectedEndDate && $selectedEndDate.format(config.format)
7 |
8 | return {
9 | formattedSelected,
10 | formattedSelectedEnd,
11 | formattedCombined: config.isRangePicker ? `${formattedSelected} - ${formattedSelectedEnd}` : formattedSelected
12 | }
13 | })
14 |
15 | return { formatter }
16 | }
17 |
18 | export {
19 | createFormatter
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/lib/positioning.js:
--------------------------------------------------------------------------------
1 | function sizes (w) {
2 | const contentWidth = [ ...w.document.body.children ].reduce((a, el) => Math.max(
3 | a, el.getBoundingClientRect().right), 0
4 | ) - w.document.body.getBoundingClientRect().x
5 |
6 | return {
7 | pageWidth: Math.min(w.document.body.scrollWidth, contentWidth),
8 | pageHeight: w.document.body.scrollHeight,
9 | viewportHeight: w.innerHeight,
10 | viewportWidth: w.innerWidth
11 | }
12 | }
13 |
14 | const dimensions = {
15 | page: {
16 | padding: 6,
17 | deadzone: 80
18 | },
19 | content: {
20 | medium: {
21 | single: {
22 | height: 410,
23 | width: 340
24 | },
25 | range: {
26 | height: 410,
27 | width: 680
28 | }
29 | },
30 | small: {
31 | single: {
32 | height: 410,
33 | width: 340
34 | },
35 | range: {
36 | height: 786,
37 | width: 340
38 | }
39 | }
40 | }
41 | }
42 |
43 | function getPosition (w, e, config) {
44 | const { isRangePicker } = config
45 | const { pageWidth, viewportHeight, viewportWidth } = sizes(w)
46 |
47 | const display = pageWidth < 480 ? 'small' : 'medium'
48 | const mode = isRangePicker ? 'range' : 'single'
49 | const { padding, deadzone } = dimensions.page
50 | const { width, height } = dimensions.content[display][mode]
51 |
52 | if (viewportHeight < (height + padding + deadzone) || viewportWidth < (width + padding)) {
53 | return {
54 | fullscreen: true,
55 | top: 0,
56 | left: 0
57 | }
58 | }
59 |
60 | let left = Math.max(padding, e.pageX - (width / 2))
61 |
62 | if ((left + width) > pageWidth) {
63 | left = (pageWidth - width) - padding
64 | }
65 |
66 | let top = Math.max(padding, e.pageY - (height / 2))
67 |
68 | const willExceedViewableArea = (top + height) > viewportHeight
69 | if (willExceedViewableArea) {
70 | top = viewportHeight - height - padding
71 | }
72 |
73 | return { top, left }
74 | }
75 |
76 | export {
77 | getPosition
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/lib/sanitization.js:
--------------------------------------------------------------------------------
1 | import { dayjs } from './date-utils'
2 |
3 | function moveDateWithinAllowedRange (date, config, isStart) {
4 | const isOutsideRange = (
5 | date.valueOf() < config.start.valueOf() ||
6 | date.valueOf() > config.end.valueOf()
7 | )
8 |
9 | if (isOutsideRange) {
10 | console.warn('Provided date', date.format(), 'is outside specified start-and-end range', config.start.format(), 'to', config.end.format())
11 | return isStart ? config.start : config.end
12 | }
13 |
14 | return date
15 | }
16 |
17 | function sanitizeInitialValue (value, config) {
18 | let isDateChosen = false
19 | let chosen
20 |
21 | if (config.isRangePicker) {
22 | const [ from, to ] = value || []
23 | isDateChosen = Boolean(from).valueOf() && Boolean(to).valueOf()
24 | chosen = isDateChosen ? value.map(dayjs) : [ dayjs.max(dayjs(), config.start), dayjs.min(dayjs().add(...config.defaultRange), config.end) ]
25 | } else {
26 | isDateChosen = Boolean(value).valueOf()
27 | chosen = [ isDateChosen ? dayjs(value) : dayjs.max(dayjs(), config.start) ]
28 | }
29 |
30 | const [ from, to ] = chosen
31 |
32 | return {
33 | isDateChosen,
34 | chosen: [
35 | moveDateWithinAllowedRange(from, config, true),
36 | ...config.isRangePicker ? [ moveDateWithinAllowedRange(to, config, false) ] : []
37 | ]
38 | }
39 | }
40 |
41 | export {
42 | sanitizeInitialValue
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/lib/view-context.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import { dayjs } from './date-utils'
4 | import DateView from '../view/date-view/DateView.svelte'
5 | import { derived } from 'svelte/store'
6 |
7 | function createMonthView (months, displayedDate) {
8 | return derived([ displayedDate ], ([ $displayedDate ]) => {
9 | let monthIndex = 0
10 |
11 | const month = $displayedDate.month()
12 | const year = $displayedDate.year()
13 | for (let i = 0; i < months.length; i += 1) {
14 | if (months[i].month === month && months[i].year === year) {
15 | monthIndex = i
16 | }
17 | }
18 |
19 | return {
20 | monthIndex,
21 | visibleMonth: months[monthIndex]
22 | }
23 | })
24 | }
25 |
26 | function createViewContext (isStart, mainContext) {
27 | const { config, months, leftCalendarDate, rightCalendarDate, selectedStartDate, selectedEndDate } = mainContext
28 | const [ date, displayedDate ] = isStart ? [ selectedStartDate, leftCalendarDate ] : [ selectedEndDate, rightCalendarDate ]
29 | const isDaytime = derived(date, $date => {
30 | if (!$date) { return true }
31 | const [ h ] = dayjs($date).format('HH:mm').split(':').map(d => parseInt(d))
32 | return h > config.morning && h < config.night
33 | })
34 |
35 | return {
36 | isStart,
37 | date,
38 | view: DateView,
39 | isDaytime,
40 | displayedDate,
41 | monthView: createMonthView(months, displayedDate)
42 | }
43 | }
44 |
45 | export {
46 | createViewContext
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/view/View.svelte:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/view/date-view/DateView.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 | registerSelection(e.detail.date)} />
7 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/view/date-view/Month.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 | {#each dayjs.weekdaysShort() as day}
25 | {day}
26 | {/each}
27 |
28 |
29 | {#each $monthView.visibleMonth.weeks as week (week.id)}
30 |
36 | {/each}
37 |
38 |
39 |
40 |
73 |
--------------------------------------------------------------------------------
/src/components/view/date-view/NavBar.svelte:
--------------------------------------------------------------------------------
1 |
62 |
63 |
64 |
65 |
72 |
75 |
82 |
83 |
84 | {#each availableMonths as monthDefinition, index}
85 |
93 | {/each}
94 |
95 |
96 |
97 |
224 |
--------------------------------------------------------------------------------
/src/components/view/date-view/Week.svelte:
--------------------------------------------------------------------------------
1 |
16 |
20 | {#each days as day}
21 |
34 |
45 |
46 | {/each}
47 |
48 |
49 |
229 |
--------------------------------------------------------------------------------
/src/components/view/date-view/date-comparison.js:
--------------------------------------------------------------------------------
1 | export function isDateBetweenSelected (a, b, c) {
2 | const start = a.startOf('day').toDate()
3 | const stop = b.startOf('day').toDate()
4 | const day = c.startOf('day').toDate()
5 | return day > start && day < stop
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/view/date-view/feedback.js:
--------------------------------------------------------------------------------
1 | let shakeHighlightTimeout
2 |
3 | function getDay (months, m, d, y) {
4 | const theMonth = months.find(aMonth => aMonth.month === m && aMonth.year === y)
5 | if (!theMonth) {
6 | return null
7 | }
8 |
9 | for (let i = 0; i < theMonth.weeks.length; i += 1) {
10 | for (let j = 0; j < theMonth.weeks[i].days.length; j += 1) {
11 | const aDay = theMonth.weeks[i].days[j]
12 | if (aDay.month === m && aDay.day === d && aDay.year === y) return aDay
13 | }
14 | }
15 | return null
16 | }
17 |
18 | function checkIfVisibleDateIsSelectable (months, date) {
19 | const proposedDay = getDay(
20 | months,
21 | date.month(),
22 | date.date(),
23 | date.year()
24 | )
25 | return proposedDay && proposedDay.selectable
26 | }
27 |
28 | function shakeDate (shouldShakeDate, date) {
29 | clearTimeout(shakeHighlightTimeout)
30 | shouldShakeDate.set(date)
31 | shakeHighlightTimeout = setTimeout(() => {
32 | shouldShakeDate.set(false)
33 | }, 700)
34 | }
35 |
36 | export {
37 | checkIfVisibleDateIsSelectable,
38 | shakeDate
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/view/date-view/keyboard.js:
--------------------------------------------------------------------------------
1 | const keyCodes = {
2 | left: 37,
3 | up: 38,
4 | right: 39,
5 | down: 40,
6 | pgup: 33,
7 | pgdown: 34,
8 | enter: 13,
9 | escape: 27,
10 | tab: 9
11 | }
12 |
13 | const keyCodesArray = Object.keys(keyCodes).map(k => keyCodes[k])
14 |
15 | function handleKeyPress (evt, incrementDayHighlighted, incrementMonth, registerSelection, close) {
16 | if (keyCodesArray.indexOf(evt.keyCode) === -1) {
17 | return false
18 | }
19 |
20 | evt.preventDefault()
21 |
22 | switch (evt.keyCode) {
23 | case keyCodes.left:
24 | return incrementDayHighlighted(-1)
25 | case keyCodes.up:
26 | return incrementDayHighlighted(-7)
27 | case keyCodes.right:
28 | return incrementDayHighlighted(1)
29 | case keyCodes.down:
30 | return incrementDayHighlighted(7)
31 | case keyCodes.pgup:
32 | return incrementMonth(-1)
33 | case keyCodes.pgdown:
34 | return incrementMonth(1)
35 | case keyCodes.escape:
36 | return close()
37 | case keyCodes.enter:
38 | return registerSelection()
39 | default:
40 | return false
41 | }
42 | }
43 |
44 | function createKeyboardHandler ({ incrementDayHighlighted, incrementMonth, registerSelection, close }) {
45 | return evt => handleKeyPress(evt, incrementDayHighlighted, incrementMonth, registerSelection, close)
46 | }
47 |
48 | export {
49 | createKeyboardHandler
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/view/time-view/Chevron.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/view/time-view/TimeInput.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 | increment('hour')} />
4 | increment('minute')} />
5 |
6 |
7 |
8 | decrement('hour')} />
9 | decrement('minute')} />
10 |
11 |
12 |
13 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/view/time-view/TimeView.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | {dayjs($date).format(config.format)}
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/view/time-view/time-input.js:
--------------------------------------------------------------------------------
1 | import { get } from 'svelte/store'
2 |
3 | function timeInput (node, store) {
4 | node.addEventListener('keydown', types)
5 | node.addEventListener('focus', resetTime)
6 | node.addEventListener('blur', attemptValuePersist)
7 |
8 | let time
9 |
10 | const unsubscribe = store.subscribe(given => {
11 | time = given.split('')
12 | syncInput()
13 | })
14 |
15 | function syncInput () {
16 | node.value = time.join('')
17 | }
18 |
19 | function resetTime () {
20 | time = []
21 | syncInput()
22 | }
23 |
24 | function persistTime () {
25 | store.set(time.join(''))
26 | syncInput()
27 | }
28 |
29 | function attemptValuePersist () {
30 | if (time.digits === 5) {
31 | persistTime()
32 | return
33 | }
34 |
35 | time = get(store).split('')
36 | syncInput()
37 | }
38 |
39 | function types (e) {
40 | e.preventDefault()
41 | const k = e.which
42 |
43 | if (k >= 48 && k <= 57) {
44 | addDigit(k)
45 | }
46 |
47 | if (k === 8) {
48 | deleteDigit()
49 | }
50 | }
51 |
52 | function deleteDigit () {
53 | time.pop()
54 | time.length === 3 && time.pop()
55 | syncInput()
56 | }
57 |
58 | function isInvalidDigit (digit) {
59 | const tooManyDigits = time.length > 4
60 | const invalidFirstDigit = time.length === 0 && ![ 0, 1, 2 ].includes(digit)
61 | const invalidSecondDigit = time.length === 1 && time[0] === 2 && digit > 3
62 | const invalidThirdDigit = time.length === 3 && digit > 5
63 | return tooManyDigits || invalidFirstDigit || invalidSecondDigit || invalidThirdDigit
64 | }
65 |
66 | function addDigit (k) {
67 | const digit = k - 48
68 | if (isInvalidDigit(digit)) { return }
69 |
70 | time.length === 2 && time.push(':')
71 | time.push(digit)
72 |
73 | if (time.length === 5) {
74 | persistTime()
75 | }
76 |
77 | syncInput()
78 | }
79 |
80 | return {
81 | destroy () {
82 | unsubscribe()
83 | node.removeEventListener('keydown', types)
84 | node.removeEventListener('focus', resetTime)
85 | node.removeEventListener('blur', attemptValuePersist)
86 | }
87 | }
88 | }
89 |
90 | export {
91 | timeInput
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/view/time-view/time-store.js:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store'
2 | import { dayjs } from '../../lib/date-utils.js'
3 |
4 | function format (h, m) {
5 | return [
6 | String(h).padStart(2, '0'),
7 | String(m).padStart(2, '0')
8 | ].join(':')
9 | }
10 |
11 | function createStore (date, config) {
12 | const time = writable(dayjs(date).format('HH:mm'))
13 |
14 | function increment (segment) {
15 | time.update(t => {
16 | let [ h, m ] = t.split(':')
17 | if (segment === 'hour' && h < 23) { ++h }
18 | if (segment === 'minute' && m < 59) {
19 | m = Math.min(59, parseInt(m) + config.minuteStep)
20 | }
21 | return format(h, m)
22 | })
23 | }
24 |
25 | function decrement (segment) {
26 | time.update(t => {
27 | let [ h, m ] = t.split(':')
28 | if (segment === 'hour' && h > 0) { --h }
29 | if (segment === 'minute' && m > 0) {
30 | m = Math.max(0, parseInt(m) - config.minuteStep)
31 | }
32 | return format(h, m)
33 | })
34 | }
35 |
36 | function set (t) {
37 | time.set(t)
38 | }
39 | return {
40 | increment,
41 | decrement,
42 | time,
43 | set
44 | }
45 | }
46 |
47 | export {
48 | createStore
49 | }
50 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte'
2 |
3 | const app = new App({
4 | target: document.body,
5 | })
6 |
7 | export default app
8 |
9 | // Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
10 | // Learn more: https://www.snowpack.dev/#hot-module-replacement
11 | if (import.meta.hot) {
12 | import.meta.hot.accept()
13 | import.meta.hot.dispose(() => {
14 | app.$destroy()
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { CalendarStyle } from './calendar-style.js'
2 | import DatePicker from './components/DatePicker.svelte'
3 |
4 | export {
5 | CalendarStyle,
6 | DatePicker
7 | }
8 |
--------------------------------------------------------------------------------
/src/normalize.css:
--------------------------------------------------------------------------------
1 | html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}
2 | body{margin:0;}
3 | article,
4 | aside,
5 | details,
6 | figcaption,
7 | figure,
8 | footer,
9 | header,
10 | hgroup,
11 | main,
12 | menu,
13 | nav,
14 | section,
15 | summary{display:block;}
16 | audio,
17 | canvas,
18 | progress,
19 | video{display:inline-block;vertical-align:baseline;}
20 | audio:not([controls]){display:none;height:0;}
21 | [hidden],
22 | template{display:none;}
23 | a{background-color:transparent;}
24 | a:active,
25 | a:hover{outline:0;}
26 | abbr[title]{border-bottom:1px dotted;}
27 | b,
28 | strong{font-weight:bold;}
29 | dfn{font-style:italic;}
30 | h1{font-size:2em;margin:0.67em 0;}
31 | mark{background:#ff0;color:#000;}
32 | small{font-size:80%;}
33 | sub,
34 | sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}
35 | sup{top:-0.5em;}
36 | sub{bottom:-0.25em;}
37 | img{border:0;}
38 | svg:not(:root){overflow:hidden;}
39 | figure{margin:1em 40px;}
40 | hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}
41 | pre{overflow:auto;}
42 | code,
43 | kbd,
44 | pre,
45 | samp{font-family:monospace, monospace;font-size:1em;}
46 | button,
47 | input,
48 | optgroup,
49 | select,
50 | textarea{color:inherit;font:inherit;margin:0;}
51 | button{overflow:visible;}
52 | button,
53 | select{text-transform:none;}
54 | button,
55 | html input[type="button"],
56 | input[type="reset"],
57 | input[type="submit"]{-webkit-appearance:button;cursor:pointer;}
58 | button[disabled],
59 | html input[disabled]{cursor:default;}
60 | button::-moz-focus-inner,
61 | input::-moz-focus-inner{border:0;padding:0;}
62 | input{line-height:normal;}
63 | input[type="checkbox"],
64 | input[type="radio"]{box-sizing:border-box;padding:0;}
65 | input[type="number"]::-webkit-inner-spin-button,
66 | input[type="number"]::-webkit-outer-spin-button{height:auto;}
67 | input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}
68 | input[type="search"]::-webkit-search-cancel-button,
69 | input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}
70 | fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}
71 | legend{border:0;padding:0;}
72 | textarea{overflow:auto;}
73 | optgroup{font-weight:bold;}
74 | table{border-collapse:collapse;border-spacing:0;}
75 | td,
76 | th{padding:0;}
--------------------------------------------------------------------------------
/src/prettify.css:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright (C) 2015 Google Inc.
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | /* Pretty printing styles. Used with prettify.js. */
19 |
20 |
21 | /* SPAN elements with the classes below are added by prettyprint. */
22 | .pln { color: #000 } /* plain text */
23 |
24 | @media screen {
25 | .str { color: #080 } /* string content */
26 | .kwd { color: #008 } /* a keyword */
27 | .com { color: #800 } /* a comment */
28 | .typ { color: #606 } /* a type name */
29 | .lit { color: #066 } /* a literal value */
30 | /* punctuation, lisp open bracket, lisp close bracket */
31 | .pun, .opn, .clo { color: #660 }
32 | .tag { color: #008 } /* a markup tag name */
33 | .atn { color: #606 } /* a markup attribute name */
34 | .atv { color: #080 } /* a markup attribute value */
35 | .dec, .var { color: #606 } /* a declaration; a variable name */
36 | .fun { color: red } /* a function name */
37 | }
38 |
39 | /* Use higher contrast and text-weight for printable form. */
40 | @media print, projection {
41 | .str { color: #060 }
42 | .kwd { color: #006; font-weight: bold }
43 | .com { color: #600; font-style: italic }
44 | .typ { color: #404; font-weight: bold }
45 | .lit { color: #044 }
46 | .pun, .opn, .clo { color: #440 }
47 | .tag { color: #006; font-weight: bold }
48 | .atn { color: #404 }
49 | .atv { color: #060 }
50 | }
51 |
52 | /* Put a border around prettyprinted code snippets. */
53 | pre.prettyprint { padding: 20px; border: 0!important; background: #f5f5f5!important;margin-bottom:14px; }
54 |
55 | /* Specify class=linenums on a pre to get line numbering */
56 | ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */
57 | li.L0,
58 | li.L1,
59 | li.L2,
60 | li.L3,
61 | li.L5,
62 | li.L6,
63 | li.L7,
64 | li.L8 { list-style-type: decimal !important }
65 | /* Alternate shading for lines */
66 | li.L1,
67 | li.L3,
68 | li.L5,
69 | li.L7,
70 | li.L9 { background: #f5f5f5!important }
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | height: 100%;
3 | }
4 |
5 | body {
6 | font-family: 'Open Sans', sans-serif;
7 | background:#eaedf2;
8 | color:#666;
9 | font-size:14px;
10 | display: flex;
11 | flex-direction: column;
12 | height: 100%;
13 | }
14 |
15 | * {
16 | -webkit-box-sizing: border-box;
17 | -moz-box-sizing: border-box;
18 | box-sizing: border-box;
19 | }
20 |
21 | a{
22 | -webkit-transition: all .3s ease;
23 | -moz-transition: all .3s ease;
24 | -o-transition: all .3s ease;
25 | transition: all .3s ease;
26 | text-decoration:none;
27 | }
28 | .content-info a{
29 | color:#22A7F0
30 | }
31 | .content-info a:hover{
32 | color:#000;
33 | }
34 | a img {
35 | border:0;
36 | }
37 |
38 | section img{
39 | display:block;
40 | max-width:100%;
41 | height:auto;
42 | margin:15px 0 15px 0;
43 | }
44 |
45 | article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary {
46 | display: block;
47 | }
48 | section{
49 | position:relative;
50 | }
51 |
52 | .container {
53 | padding-right: 15px;
54 | padding-left: 15px;
55 | margin-right: auto;
56 | margin-left: auto;
57 | width: 1200px;
58 | }
59 | .container:before, .container:after, .clearfix:before, .row:before, .clearfix:after, .row:after {
60 | display: table;
61 | content: " ";
62 | }
63 | .container:after, .clearfix:after, .row:after {
64 | clear: both;
65 | }
66 | .row {
67 | margin-right: -15px;
68 | margin-left: -15px;
69 | }
70 |
71 | .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11 {
72 | float: left;
73 | }
74 | .col-xs-1,
75 | .col-sm-1,
76 | .col-md-1,
77 | .col-lg-1,
78 | .col-xs-2,
79 | .col-sm-2,
80 | .col-md-2,
81 | .col-lg-2,
82 | .col-xs-3,
83 | .col-sm-3,
84 | .col-md-3,
85 | .col-lg-3,
86 | .col-xs-4,
87 | .col-sm-4,
88 | .col-md-4,
89 | .col-lg-4,
90 | .col-xs-5,
91 | .col-sm-5,
92 | .col-md-5,
93 | .col-lg-5,
94 | .col-xs-6,
95 | .col-sm-6,
96 | .col-md-6,
97 | .col-lg-6,
98 | .col-xs-7,
99 | .col-sm-7,
100 | .col-md-7,
101 | .col-lg-7,
102 | .col-xs-8,
103 | .col-sm-8,
104 | .col-md-8,
105 | .col-lg-8,
106 | .col-xs-9,
107 | .col-sm-9,
108 | .col-md-9,
109 | .col-lg-9,
110 | .col-xs-10,
111 | .col-sm-10,
112 | .col-md-10,
113 | .col-lg-10,
114 | .col-xs-11,
115 | .col-sm-11,
116 | .col-md-11,
117 | .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
118 | position: relative;
119 | min-height: 1px;
120 | padding-right: 15px;
121 | padding-left: 15px;
122 | }
123 | .col-lg-12 {
124 | width: 100%;
125 | }
126 | .col-lg-11 {
127 | width: 91.66666667%;
128 | }
129 | .col-lg-10 {
130 | width: 83.33333333%;
131 | }
132 | .col-lg-9 {
133 | width: 75%;
134 | }
135 | .col-lg-8 {
136 | width: 66.66666667%;
137 | }
138 | .col-lg-7 {
139 | width: 58.33333333%;
140 | }
141 | .col-lg-6 {
142 | width: 50%;
143 | }
144 | .col-lg-5 {
145 | width: 41.66666667%;
146 | }
147 | .col-lg-4 {
148 | width: 33.33333333%;
149 | }
150 | .col-lg-3 {
151 | width: 25%;
152 | }
153 | .col-lg-2 {
154 | width: 16.66666667%;
155 | }
156 | .col-lg-1 {
157 | width: 8.33333333%;
158 | }
159 | .center{
160 | text-align:center;
161 | }
162 | .left{
163 | text-align:left;
164 | }
165 | .right{
166 | text-align:right;
167 | }
168 | .btn{
169 | display:inline-block;
170 | width:160px;
171 | height:40px;
172 | line-height:38px;
173 | background:#3498db;
174 | color:#fff;
175 | font-weight:400;
176 | text-align:center;
177 | text-transform:uppercase;
178 | font-size:14px;
179 | border-bottom:2px solid #2a8bcc;
180 | }
181 | .btn:hover{
182 | background:#2a8bcc
183 | }
184 | header{
185 | margin:0 0 50px 0;
186 | padding:20px 0;
187 | background:#22A7F0
188 | }
189 | .slogan{
190 | color:#fff;
191 | font-weight:300;
192 | font-size:20px;
193 | line-height:34px;
194 | }
195 | #logo img{
196 | display:block;
197 | height:34px;
198 | }
199 |
200 | section .container{
201 | background:#fff;
202 | }
203 |
204 | .content-wrap{
205 | padding:50px 0
206 | }
207 |
208 | aside{
209 | color:#fff;
210 | float:left;
211 | padding-left:15px;
212 | width:285px;
213 | }
214 | .fixed{
215 | position:fixed;
216 | top:15px;
217 | }
218 | aside h4{
219 | font-size:20px;
220 | font-weight:400;
221 | margin:0 0 30px 0;
222 | }
223 |
224 | .menu-box{
225 | padding:20px;
226 | background:#34495e;
227 | }
228 | .menu-box ul{
229 | margin:0;
230 | padding:0;
231 | }
232 | .menu-box li{
233 | display:block;
234 | }
235 | .menu-box li a{
236 | display:block;
237 | padding:15px 20px;
238 | margin-left: -20px;
239 | margin-right: -20px;
240 | color:#fff;
241 | border-bottom:1px solid #314559;
242 | }
243 | .menu-box li a:hover, .menu-box li a.current{
244 | background:#2c3e50;
245 | }
246 | .menu-box li:last-child a{
247 | border-bottom:0;
248 | }
249 |
250 | .content-info{
251 | padding-right:15px;
252 | padding-left:315px;
253 | }
254 | .section-txt{
255 | padding-bottom:15px;
256 | margin-bottom:30px;
257 | border-bottom:1px solid #dcdcdc;
258 | }
259 | .section-txt:last-child{
260 | margin-bottom:0;
261 | padding-bottom:0;
262 | border-bottom:0;
263 | }
264 | .content-info h3{
265 | font-size:24px;
266 | font-weight:400;
267 | color:#444;
268 | margin:0 0 30px 0;
269 | }
270 | .content-info p{
271 | color:#666;
272 | line-height:24px;
273 | font-size:16px;
274 | font-weight:300;
275 | }
276 | .content-info ul{
277 | margin:0 0 14px 0;
278 | }
279 | .content-info ul li{
280 | line-height:24px;
281 | font-size:16px;
282 | font-weight:300;
283 | }
284 |
285 | .content-info iframe {
286 | width: 100%!important;
287 | height: 350px;
288 | border: 0!important;
289 | }
290 |
291 | .footer-area{
292 | margin-top:50px;
293 | padding:60px 0;
294 | background:#222;
295 | font-size:16px;
296 | line-height:24px;
297 | color:#fff;
298 | font-weight:300;
299 | }
300 |
301 | .footer-area a{
302 | color:#999;
303 | }
304 | .footer-area a:hover{
305 | color:#eee
306 | }
307 | footer{
308 | background:#111;
309 | padding:20px 0;
310 | font-weight:300;
311 | font-size:12px;
312 | }
313 |
314 | @media only screen and (max-width: 1200px) {
315 | .container{
316 | width:970px;
317 | }
318 | .hidden-md{
319 | display:none;
320 | }
321 | .col-md-12 {
322 | width: 100%;
323 | }
324 | .col-md-11 {
325 | width: 91.66666667%;
326 | }
327 | .col-md-10 {
328 | width: 83.33333333%;
329 | }
330 | .col-md-9 {
331 | width: 75%;
332 | }
333 | .col-md-8 {
334 | width: 66.66666667%;
335 | }
336 | .col-md-7 {
337 | width: 58.33333333%;
338 | }
339 | .col-md-6 {
340 | width: 50%;
341 | }
342 | .col-md-5 {
343 | width: 41.66666667%;
344 | }
345 | .col-md-4 {
346 | width: 33.33333333%;
347 | }
348 | .col-md-3 {
349 | width: 25%;
350 | }
351 | .col-md-2 {
352 | width: 16.66666667%;
353 | }
354 | .col-md-1 {
355 | width: 8.33333333%;
356 | }
357 |
358 | }
359 |
360 |
361 | @media only screen and (max-width: 992px){
362 | .container{
363 | width:750px;
364 | }
365 | .hidden-sm{
366 | display:none;
367 | }
368 | .col-sm-12 {
369 | width: 100%;
370 | }
371 | .col-sm-11 {
372 | width: 91.66666667%;
373 | }
374 | .col-sm-10 {
375 | width: 83.33333333%;
376 | }
377 | .col-sm-9 {
378 | width: 75%;
379 | }
380 | .col-sm-8 {
381 | width: 66.66666667%;
382 | }
383 | .col-sm-7 {
384 | width: 58.33333333%;
385 | }
386 | .col-sm-6 {
387 | width: 50%;
388 | }
389 | .col-sm-5 {
390 | width: 41.66666667%;
391 | }
392 | .col-sm-4 {
393 | width: 33.33333333%;
394 | }
395 | .col-sm-3 {
396 | width: 25%;
397 | }
398 | .col-sm-2 {
399 | width: 16.66666667%;
400 | }
401 | .col-sm-1 {
402 | width: 8.33333333%;
403 | }
404 | .slogan {
405 | font-size: 16px;
406 | }
407 |
408 | }
409 |
410 | @media only screen and (max-width: 768px){
411 | .container{
412 | width:100%;
413 | }
414 | .hidden-xs{
415 | display:none;
416 | }
417 | .col-xs-12 {
418 | width: 100%;
419 | }
420 | .col-xs-11 {
421 | width: 91.66666667%;
422 | }
423 | .col-xs-10 {
424 | width: 83.33333333%;
425 | }
426 | .col-xs-9 {
427 | width: 75%;
428 | }
429 | .col-xs-8 {
430 | width: 66.66666667%;
431 | }
432 | .col-xs-7 {
433 | width: 58.33333333%;
434 | }
435 | .col-xs-6 {
436 | width: 50%;
437 | }
438 | .col-xs-5 {
439 | width: 41.66666667%;
440 | }
441 | .col-xs-4 {
442 | width: 33.33333333%;
443 | }
444 | .col-xs-3 {
445 | width: 25%;
446 | }
447 | .col-xs-2 {
448 | width: 16.66666667%;
449 | }
450 | .col-xs-1 {
451 | width: 8.33333333%;
452 | }
453 | header{
454 | margin-bottom:30px;
455 | }
456 | .content-wrap {
457 | padding: 30px 0;
458 | }
459 | .slogan{
460 | text-align:center;
461 | line-height:22px;
462 | margin-bottom:15px;
463 | }
464 | #logo {
465 | text-align:center;
466 | margin-bottom:15px;
467 | }
468 | #logo img{
469 | margin:0 auto;
470 | }
471 | .btn{
472 | display:block;
473 | margin:0 auto;
474 | }
475 | aside{
476 | width:100%;
477 | float:none;
478 | padding:0 15px;
479 | margin-bottom:30px;
480 | }
481 | .content-info {
482 | padding-right: 15px;
483 | padding-left: 15px;
484 | }
485 | .content-info p, .content-info ul li{
486 | font-size:14px;
487 | line-height:22px;
488 | }
489 | .content-info h3 {
490 | font-size: 20px;
491 | }
492 | .footer-area {
493 | margin-top: 30px;
494 | padding: 50px 0;
495 | font-size:14px;
496 | }
497 | }
--------------------------------------------------------------------------------
/src/test.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte'
2 |
3 | const app = new App({
4 | target: document.body,
5 | data: {}
6 | })
7 |
8 | export default app
9 |
--------------------------------------------------------------------------------
/web-test-runner.config.js:
--------------------------------------------------------------------------------
1 | // NODE_ENV=test - Needed by "@snowpack/web-test-runner-plugin"
2 | process.env.NODE_ENV = 'test'
3 |
4 | module.exports = {
5 | plugins: [ require('@snowpack/web-test-runner-plugin')() ]
6 | }
7 |
--------------------------------------------------------------------------------