├── .editorconfig
├── .github
└── ISSUE_TEMPLATE
│ └── config.yml
├── .gitignore
├── .npmrc
├── .vscode
└── extensions.json
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── karma.config.cjs
├── package.json
├── pnpm-lock.yaml
├── rollup.config.js
├── src
├── FullCalendar.ts
├── index.ts
└── options.ts
├── tests
├── DynamicEvent.vue
└── index.js
├── tsconfig.json
└── vite.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 | quote_type = single
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Bug Report
4 | url: https://github.com/fullcalendar/fullcalendar/issues/new?template=bug-report.yml
5 | about: Create a ticket on the main issue tracker. Choose the "Vue" connector
6 | - name: Feature Request
7 | url: https://github.com/fullcalendar/fullcalendar/issues/new?template=feature-request.yml
8 | about: Create a ticket on the main issue tracker. Choose the "Vue" connector
9 | - name: Getting Help
10 | url: https://stackoverflow.com/questions/tagged/fullcalendar%20vue.js
11 | about: Stuck on a tough problem? Visit the StackOverflow fullcalendar + vue.js tags
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Package manager
3 | node_modules
4 |
5 | # Generated
6 | dist
7 |
8 | # Outer monorepo
9 | .turbo
10 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict = true
2 | use-node-version = 20.14.0
3 | link-workspace-packages = true
4 | prefer-workspace-packages = true
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "Vue.volar"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Note
2 |
3 | This changelog does not mention all releases.
4 | Visit the github releases page as well as the main fullcalendar repo.
5 |
6 | ## 5.11.1 (2022-05-10)
7 |
8 | - FIX: Cannot target calendar api with several instances (#155)
9 |
10 | ## 5.7.1 (2021-06-02)
11 |
12 | - support for Vue 3
13 | - KNOWN BUG: templates within slots do not inherit App's
14 | mixins, directives, filters, and components (like #123)
15 |
16 | ## 5.4.0 (2020-11-11)
17 |
18 | - no longer expose component as 'fullcalendar' when used globally
19 | - eventContent slot doesn't properly destroy the Vue components inside (#111)
20 | - global mixins, directives, filters & components work inside slots (#105)
21 | - better compatibility with Webpack 5, deeming `resolve.fullySpecified` unnecessary ([core-5822])
22 | - dist files now include a CJS file. ESM is still used by default in most environments ([core-5929])
23 | - webpack upgrade note: use style-loader instead of vue-style-loader
24 |
25 | [core-5822]: https://github.com/fullcalendar/fullcalendar/issues/5822
26 | [core-5929]: https://github.com/fullcalendar/fullcalendar/issues/5929
27 |
28 | ## 5.2.0 (2020-07-30)
29 |
30 | - pre-built release of the Vue component (#61)
31 | - using the component through a CDN (#28)
32 | - Build errors due to missing types in main.ts (#101)
33 | - when appropriate, expose as 'fullcalendar' component, for DOM templates
34 |
35 | ## 4.3.1 (2019-08-12)
36 |
37 | - fix regression where object props wrongly forcing rerenders (#11, #34)
38 |
39 | ## 4.2.2 (2019-06-04)
40 |
41 | Emergency bugfix: event objects with Date objects wouldn't render
42 |
43 | ## 4.2.1 (2019-06-04)
44 |
45 | Fixed bugs surfaced in issue #32:
46 | - event/resource-fetching *functions* don't work
47 | - event/resource *computed properties* don't work
48 | - removed `deep-copy` as a dependency
49 |
50 | ## 4.2.0 (2019-06-02)
51 |
52 | - nested props data mutations, like events being updated,
53 | will now be rendred on the calendar (#9)
54 | - added missing props (#25, #29)
55 | - the following emitted events are now deprecated.
56 | use *props* instead. pass in a function as the prop:
57 | - `datesRender`
58 | - `datesDestroy`
59 | - `dayRender`
60 | - `eventRender`
61 | - `eventDestroy`
62 | - `viewSkeletonRender`
63 | - `viewSkeletonDestroy`
64 | - `resourceRender`
65 | Allows returning false/DOM nodes (#27)
66 | - no unnecessary rerendering of calendar caused by header/footer
67 | props being specified as literals (#11)
68 | - new dependency: fast-deep-equal
69 | automatically bundled with UMD dist
70 |
71 | ## 4.1.1 (2019-05-14)
72 |
73 | Fix missing option `googleCalendarApiKey` (#12)
74 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Adam Shaw
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # FullCalendar Vue 3 Component
3 |
4 | The official [Vue 3](https://vuejs.org/) component for [FullCalendar](https://fullcalendar.io)
5 |
6 | ## Installation
7 |
8 | Install the Vue 3 connector, the core package, and any plugins (like [daygrid](https://fullcalendar.io/docs/month-view)):
9 |
10 | ```sh
11 | npm install @fullcalendar/vue3 @fullcalendar/core @fullcalendar/daygrid
12 | ```
13 |
14 | ## Usage
15 |
16 | Render a `FullCalendar` component, supplying an [options](https://fullcalendar.io/docs#toc) object:
17 |
18 | ```vue
19 |
41 |
42 |
43 | Demo App
44 |
45 |
46 | ```
47 |
48 | You can even supply [named-slot](https://vuejs.org/guide/components/slots.html#named-slots) templates:
49 |
50 | ```vue
51 |
52 | Demo App
53 |
54 |
55 | {{ arg.timeText }}
56 | {{ arg.event.title }}
57 |
58 |
59 |
60 | ```
61 |
62 | ## Links
63 |
64 | - [Documentation](https://fullcalendar.io/docs/vue)
65 | - [Example Project](https://github.com/fullcalendar/fullcalendar-examples/tree/main/vue3)
66 |
67 | ## Development
68 |
69 | You must install this repo with [PNPM](https://pnpm.io/):
70 |
71 | ```
72 | pnpm install
73 | ```
74 |
75 | Available scripts (via `pnpm run
18 |
--------------------------------------------------------------------------------
/tests/index.js:
--------------------------------------------------------------------------------
1 | import { nextTick, defineAsyncComponent, h } from 'vue'
2 | import { createI18n } from 'vue-i18n'
3 | import { mount as _mount } from '@vue/test-utils'
4 | import FullCalendar from '../dist/index.js'
5 | import dayGridPlugin from '@fullcalendar/daygrid'
6 |
7 | const INITIAL_DATE = '2019-05-15'
8 |
9 | const DEFAULT_OPTIONS = {
10 | initialDate: INITIAL_DATE,
11 | initialView: 'dayGridMonth',
12 | timeZone: 'UTC',
13 | plugins: [ dayGridPlugin ]
14 | }
15 |
16 | let currentWrapper
17 | let currentContainerEl
18 |
19 | function mount(component, options = {}) {
20 | if (options.attachTo === undefined) {
21 | currentContainerEl = document.body.appendChild(document.createElement('div'))
22 | options = {
23 | ...options,
24 | attachTo: currentContainerEl
25 | }
26 | }
27 |
28 | currentWrapper = _mount(component, options)
29 | return currentWrapper
30 | }
31 |
32 | afterEach(function() {
33 | if (currentWrapper) {
34 | currentWrapper.unmount()
35 | currentWrapper = null
36 | }
37 | if (currentContainerEl) {
38 | currentContainerEl.remove()
39 | currentContainerEl = null
40 | }
41 | })
42 |
43 |
44 | it('renders', async () => {
45 | let wrapper = mount(FullCalendar, { propsData: { options: DEFAULT_OPTIONS } })
46 | expect(isSkeletonRendered(wrapper)).toEqual(true)
47 | })
48 |
49 | it('unmounts and calls destroy', async () => {
50 | let unmounted = false
51 | let options = {
52 | ...DEFAULT_OPTIONS,
53 | viewWillUnmount() {
54 | unmounted = true
55 | }
56 | }
57 |
58 | let wrapper = mount(FullCalendar, { propsData: { options } })
59 | wrapper.unmount()
60 | expect(unmounted).toBeTruthy()
61 | })
62 |
63 | it('handles a single prop change', async () => {
64 | let options = {
65 | ...DEFAULT_OPTIONS,
66 | weekends: true
67 | }
68 | let wrapper = mount(FullCalendar, {
69 | propsData: { options }
70 | })
71 | expect(isWeekendsRendered(wrapper)).toEqual(true)
72 |
73 | // it's easy for the component to detect this change because the whole options object changes.
74 | // a more difficult scenario is when a component updates its own nested prop.
75 | // there's a test for that below (COMPONENT_FOR_OPTION_MANIP).
76 | wrapper.setProps({
77 | options: {
78 | ...options,
79 | weekends: false // good idea to test a falsy prop
80 | }
81 | })
82 | await nextTick()
83 | expect(isWeekendsRendered(wrapper)).toEqual(false)
84 | })
85 |
86 | it('renders events with Date objects', async () => { // necessary to test copy util
87 | let wrapper = mount(FullCalendar, {
88 | propsData: {
89 | options: {
90 | ...DEFAULT_OPTIONS,
91 | events: [
92 | { title: 'event', start: new Date(INITIAL_DATE) },
93 | { title: 'event', start: new Date(INITIAL_DATE) }
94 | ]
95 | }
96 | }
97 | })
98 |
99 | expect(getRenderedEventCount(wrapper)).toEqual(2)
100 | })
101 |
102 | it('handles multiple prop changes, include event reset', async () => {
103 | let viewMountCnt = 0
104 | let eventRenderCnt = 0
105 | let options = {
106 | ...DEFAULT_OPTIONS,
107 | events: buildEvents(1),
108 | viewDidMount() {
109 | viewMountCnt++
110 | },
111 | eventContent() {
112 | eventRenderCnt++
113 | }
114 | }
115 |
116 | let wrapper = mount(FullCalendar, {
117 | propsData: { options }
118 | })
119 |
120 | expect(getRenderedEventCount(wrapper)).toEqual(1)
121 | expect(isWeekendsRendered(wrapper)).toEqual(true)
122 | expect(viewMountCnt).toEqual(1)
123 | expect(eventRenderCnt).toEqual(1)
124 |
125 | viewMountCnt = 0
126 | eventRenderCnt = 0
127 |
128 | wrapper.setProps({
129 | options: {
130 | ...options,
131 | direction: 'rtl',
132 | weekends: false,
133 | events: buildEvents(2)
134 | }
135 | })
136 |
137 | await nextTick()
138 | expect(getRenderedEventCount(wrapper)).toEqual(2)
139 | expect(isWeekendsRendered(wrapper)).toEqual(false)
140 | expect(viewMountCnt).toEqual(0)
141 | expect(eventRenderCnt).toEqual(2) // TODO: get this down to 1 (only 1 new event rendered)
142 | })
143 |
144 | it('should expose an API', async () => {
145 | let wrapper = mount(FullCalendar, { propsData: { options: DEFAULT_OPTIONS } })
146 | let calendarApi = wrapper.vm.getApi()
147 | expect(calendarApi).toBeTruthy()
148 |
149 | let newDate = new Date(Date.UTC(2000, 0, 1))
150 | calendarApi.gotoDate(newDate)
151 | expect(calendarApi.getDate().valueOf()).toEqual(newDate.valueOf())
152 | })
153 |
154 |
155 | const COMPONENT_FOR_API = {
156 | components: {
157 | FullCalendar
158 | },
159 | template: `
160 |
161 |
162 |
163 | `,
164 | data() {
165 | return {
166 | calendarOptions: DEFAULT_OPTIONS
167 | }
168 | },
169 | methods: {
170 | gotoDate(newDate) {
171 | let calendarApi = this.$refs.fullCalendar.getApi()
172 | calendarApi.gotoDate(newDate)
173 | },
174 | getDate() {
175 | let calendarApi = this.$refs.fullCalendar.getApi()
176 | return calendarApi.getDate()
177 | }
178 | }
179 | }
180 |
181 | it('should expose an API in $refs', async () => {
182 | let wrapper = mount(COMPONENT_FOR_API)
183 | let newDate = new Date(Date.UTC(2000, 0, 1))
184 |
185 | wrapper.vm.gotoDate(newDate)
186 | expect(wrapper.vm.getDate().valueOf()).toEqual(newDate.valueOf())
187 | })
188 |
189 |
190 | it('should handle multiple refs $refs', async () => {
191 | let wrapper = mount({
192 | components: {
193 | FullCalendar
194 | },
195 | template: `
196 |
197 |
198 |
199 |
200 |
201 | `,
202 | data() {
203 | return {
204 | calendarOptions0: DEFAULT_OPTIONS,
205 | calendarOptions1: DEFAULT_OPTIONS,
206 | calendarOptions2: DEFAULT_OPTIONS,
207 | }
208 | },
209 | methods: {
210 | check() {
211 | let ref0 = this.$refs.fullCalendar0.getApi()
212 | let ref1 = this.$refs.fullCalendar1.getApi()
213 | let ref2 = this.$refs.fullCalendar2.getApi()
214 | expect(ref0).not.toEqual(ref1)
215 | expect(ref1).not.toEqual(ref2)
216 | expect(ref2).not.toEqual(ref0)
217 | }
218 | }
219 | })
220 | wrapper.vm.check()
221 | })
222 |
223 |
224 | // toolbar/event non-reactivity
225 |
226 | const COMPONENT_FOR_OPTION_MANIP = {
227 | props: [ 'calendarViewDidMount', 'calendarEventContent' ],
228 | components: {
229 | FullCalendar
230 | },
231 | template: `
232 |
233 |
calendarHeight: {{ calendarOptions.height }}
234 |
235 |
236 | `,
237 | data() {
238 | return {
239 | something: 0,
240 | calendarOptions: {
241 | ...DEFAULT_OPTIONS,
242 | viewDidMount: this.calendarViewDidMount, // pass the prop
243 | eventContent: this.calendarEventContent, // pass the prop
244 | headerToolbar: buildToolbar(),
245 | events: buildEvents(1),
246 | weekends: true // needs to be initially present if we plan on changing it (a Vue concept)
247 | }
248 | }
249 | },
250 | methods: {
251 | changeSomething() {
252 | this.something++
253 | },
254 | disableWeekends() {
255 | this.calendarOptions.weekends = false
256 | }
257 | }
258 | }
259 |
260 | it('handles an object change when prop is reassigned', async () => {
261 | let wrapper = mount(COMPONENT_FOR_OPTION_MANIP)
262 | expect(isWeekendsRendered(wrapper)).toEqual(true)
263 |
264 | wrapper.vm.disableWeekends()
265 | await nextTick()
266 | expect(isWeekendsRendered(wrapper)).toEqual(false)
267 | })
268 |
269 | it('avoids rerendering unchanged toolbar/events', async () => {
270 | let viewMountCnt = 0
271 | let eventRenderCnt = 0
272 |
273 | let wrapper = mount(COMPONENT_FOR_OPTION_MANIP, {
274 | propsData: {
275 | calendarViewDidMount() {
276 | viewMountCnt++
277 | },
278 | calendarEventContent() {
279 | eventRenderCnt++
280 | }
281 | }
282 | })
283 |
284 | expect(viewMountCnt).toEqual(1)
285 | expect(eventRenderCnt).toEqual(1)
286 |
287 | viewMountCnt = 0
288 | eventRenderCnt = 0
289 |
290 | wrapper.vm.changeSomething()
291 | expect(viewMountCnt).toEqual(0)
292 | expect(eventRenderCnt).toEqual(0)
293 | })
294 |
295 |
296 | // event reactivity
297 |
298 | const COMPONENT_FOR_EVENT_MANIP = {
299 | components: {
300 | FullCalendar
301 | },
302 | template: `
303 |
304 | `,
305 | data() {
306 | return {
307 | calendarOptions: {
308 | ...DEFAULT_OPTIONS,
309 | events: buildEvents(1)
310 | }
311 | }
312 | },
313 | methods: {
314 | addEvent() {
315 | this.calendarOptions.events.push(buildEvent(1))
316 | },
317 | updateTitle(title) {
318 | this.calendarOptions.events[0].title = title
319 | }
320 | }
321 | }
322 |
323 | it('reacts to event adding', async () => {
324 | let wrapper = mount(COMPONENT_FOR_EVENT_MANIP)
325 | expect(getRenderedEventCount(wrapper)).toEqual(1)
326 |
327 | wrapper.vm.addEvent()
328 | await nextTick()
329 | expect(getRenderedEventCount(wrapper)).toEqual(2)
330 | })
331 |
332 | it('reacts to event property changes', async () => {
333 | let wrapper = mount(COMPONENT_FOR_EVENT_MANIP)
334 | expect(getFirstEventTitle(wrapper)).toEqual('event0')
335 | wrapper.vm.updateTitle('another title')
336 |
337 | await nextTick()
338 | expect(getFirstEventTitle(wrapper)).toEqual('another title')
339 | })
340 |
341 |
342 | // event reactivity with fetch function
343 |
344 | const EVENT_FUNC_COMPONENT = {
345 | components: {
346 | FullCalendar
347 | },
348 | template: `
349 |
350 | `,
351 | data() {
352 | return {
353 | calendarOptions: {
354 | ...DEFAULT_OPTIONS,
355 | events: this.fetchEvents
356 | }
357 | }
358 | },
359 | methods: {
360 | fetchEvents(fetchInfo, successCallback) {
361 | setTimeout(() => {
362 | successCallback(buildEvents(2))
363 | }, 0)
364 | }
365 | }
366 | }
367 |
368 | it('can receive an async event function', function(done) {
369 | let wrapper = mount(EVENT_FUNC_COMPONENT)
370 | setTimeout(() => {
371 | expect(getRenderedEventCount(wrapper)).toEqual(2)
372 | done()
373 | }, 100) // more than event function's setTimeout
374 | })
375 |
376 |
377 | // event reactivity with computed prop
378 |
379 | const EVENT_COMP_PROP_COMPONENT = {
380 | components: {
381 | FullCalendar
382 | },
383 | template: `
384 |
385 | `,
386 | data() {
387 | return {
388 | first: true
389 | }
390 | },
391 | computed: {
392 | calendarOptions() {
393 | return {
394 | ...DEFAULT_OPTIONS,
395 | events: this.first ? [] : buildEvents(2)
396 | }
397 | }
398 | },
399 | methods: {
400 | markNotFirst() {
401 | this.first = false
402 | }
403 | }
404 | }
405 |
406 | it('reacts to computed events prop', async () => {
407 | let wrapper = mount(EVENT_COMP_PROP_COMPONENT)
408 | expect(getRenderedEventCount(wrapper)).toEqual(0)
409 |
410 | wrapper.vm.markNotFirst()
411 | await nextTick()
412 | expect(getRenderedEventCount(wrapper)).toEqual(2)
413 | })
414 |
415 |
416 | // component with vue slots
417 |
418 | const COMPONENT_WITH_SLOTS = {
419 | components: {
420 | FullCalendar
421 | },
422 | template: `
423 |
424 |
425 | {{ arg.timeText }}
426 | {{ arg.event.title }}
427 |
428 |
429 | `,
430 | data() {
431 | return {
432 | calendarOptions: {
433 | ...DEFAULT_OPTIONS,
434 | events: buildEvents(1)
435 | }
436 | }
437 | },
438 | methods: {
439 | resetEvents() {
440 | this.calendarOptions.events = buildEvents(1)
441 | }
442 | }
443 | }
444 |
445 | it('renders and rerenders a custom slot', async () => {
446 | let wrapper = mount(COMPONENT_WITH_SLOTS)
447 | await nextTick()
448 |
449 | let eventEl = getRenderedEventEls(wrapper)[0]
450 | expect(eventEl.findAll('i').length).toEqual(1)
451 |
452 | wrapper.vm.resetEvents()
453 | await nextTick()
454 | eventEl = getRenderedEventEls(wrapper)[0]
455 | expect(eventEl.findAll('i').length).toEqual(1)
456 | })
457 |
458 | it('calls nested vue lifecycle methods when in custom content', async () => {
459 | let mountedCalled = false
460 | let beforeUnmountCalled = false
461 | let unmountedCalled = false
462 | let wrapper = mount({
463 | components: {
464 | FullCalendar,
465 | EventContent: {
466 | props: {
467 | event: { type: Object, required: true }
468 | },
469 | template: `
470 | {{ event.title }}
471 | `,
472 | mounted() {
473 | mountedCalled = true
474 | },
475 | beforeUnmount() {
476 | beforeUnmountCalled = true
477 | },
478 | unmounted() {
479 | unmountedCalled = true
480 | },
481 | }
482 | },
483 | template: `
484 |
485 |
486 |
487 |
488 |
489 | `,
490 | data() {
491 | return {
492 | calendarOptions: {
493 | ...DEFAULT_OPTIONS,
494 | events: buildEvents(1)
495 | }
496 | }
497 | }
498 | })
499 | await nextTick()
500 | expect(mountedCalled).toEqual(true)
501 |
502 | wrapper.unmount()
503 | await nextTick()
504 | expect(beforeUnmountCalled).toEqual(true)
505 | expect(unmountedCalled).toEqual(true)
506 | })
507 |
508 | // component with eventContent (two multi-day event)
509 |
510 | const COMPONENT_WITH_SLOTS_MULTIDAY_EVENTS = {
511 | components: {
512 | FullCalendar
513 | },
514 | template: `
515 |
516 |
517 | {{ arg.timeText }}
518 | {{ arg.event.title }}
519 |
520 |
521 | `,
522 | data() {
523 | return {
524 | calendarOptions: {
525 | ...DEFAULT_OPTIONS,
526 | events: [
527 | { title: 'all-day 1', start: INITIAL_DATE, end: '2019-05-18' }, // 3 day
528 | { title: 'all-day 2', start: INITIAL_DATE, end: '2019-05-17' }, // 2 day
529 | ]
530 | }
531 | }
532 | }
533 | }
534 |
535 | it('renders two multi-day events positioned correctly', async () => {
536 | let wrapper = mount(COMPONENT_WITH_SLOTS_MULTIDAY_EVENTS)
537 | await nextTick()
538 |
539 | let eventEls = getRenderedEventEls(wrapper).map((wrapper) => wrapper.element)
540 | expect(eventEls.length).toBe(2)
541 | expect(anyElsIntersect(eventEls)).toBe(false)
542 | })
543 |
544 | // component with eventContent (multi-day & timed)
545 |
546 | const COMPONENT_WITH_SLOTS_MULTIDAY_AND_TIMED = {
547 | components: {
548 | FullCalendar
549 | },
550 | template: `
551 |
552 |
553 | {{ arg.timeText }}
554 | {{ arg.event.title }}
555 |
556 |
557 | `,
558 | data() {
559 | return {
560 | calendarOptions: {
561 | ...DEFAULT_OPTIONS,
562 | events: [
563 | { title: 'all-day 1', start: INITIAL_DATE, end: '2019-05-18' }, // 3 day
564 | { title: 'all-day 2', start: INITIAL_DATE + 'T12:00:00' },
565 | ]
566 | }
567 | }
568 | }
569 | }
570 |
571 | it('renders a multi-day and timed event positioned correctly', async () => {
572 | let wrapper = mount(COMPONENT_WITH_SLOTS_MULTIDAY_AND_TIMED)
573 | await nextTick()
574 |
575 | let eventEls = getRenderedEventEls(wrapper).map((wrapper) => wrapper.element)
576 | expect(eventEls.length).toBe(2)
577 | expect(anyElsIntersect(eventEls)).toBe(false)
578 | })
579 |
580 | // component with vue slots AND custom render func
581 |
582 | const COMPONENT_WITH_SLOTS2 = {
583 | components: {
584 | FullCalendar
585 | },
586 | template: `
587 |
588 |
589 | {{ arg.timeText }}
590 | {{ arg.event.title }}
591 |
592 |
593 | `,
594 | data() {
595 | return {
596 | calendarOptions: {
597 | ...DEFAULT_OPTIONS,
598 | events: buildEvents(1),
599 | eventContent: (eventArg) => {
600 | return h('i', {}, eventArg.event.title)
601 | }
602 | }
603 | }
604 | }
605 | }
606 |
607 | it('render function can return jsx', async () => {
608 | let wrapper = mount(COMPONENT_WITH_SLOTS2)
609 | await nextTick()
610 |
611 | let eventEl = getRenderedEventEls(wrapper)[0]
612 | expect(eventEl.findAll('i').length).toEqual(1)
613 | })
614 |
615 | // component with vue slots AND custom render func that returns vanilla-js-style objects
616 |
617 | const COMPONENT_WITH_SLOTS3 = {
618 | components: {
619 | FullCalendar
620 | },
621 | template: `
622 |
623 | `,
624 | data() {
625 | return {
626 | calendarOptions: {
627 | ...DEFAULT_OPTIONS,
628 | events: buildEvents(1),
629 | eventContent: (eventArg) => {
630 | return { html: `${eventArg.event.title}` }
631 | }
632 | }
633 | }
634 | }
635 | }
636 |
637 | it('render function can returns vanilla-js-style objects', async () => {
638 | let wrapper = mount(COMPONENT_WITH_SLOTS3)
639 | await nextTick()
640 |
641 | let eventEl = getRenderedEventEls(wrapper)[0]
642 | expect(eventEl.findAll('i').length).toEqual(1)
643 | })
644 |
645 | // event rendering and did-mount hooks
646 |
647 | ;['auto', 'background'].forEach((eventDisplay) => {
648 | it(`during ${eventDisplay} custom event rendering, receives el`, async () => {
649 | let eventDidMountCalled = false
650 |
651 | mount({
652 | components: {
653 | FullCalendar
654 | },
655 | template: `
656 |
657 |
658 | {{ arg.event.title }}
659 |
660 |
661 | `,
662 | data() {
663 | return {
664 | calendarOptions: {
665 | ...DEFAULT_OPTIONS,
666 | events: [
667 | {
668 | title: 'Event 1',
669 | start: INITIAL_DATE,
670 | display: eventDisplay,
671 | },
672 | ],
673 | eventDidMount: (eventInfo) => {
674 | expect(eventInfo.el).toBeTruthy()
675 | eventDidMountCalled = true
676 | }
677 | }
678 | }
679 | }
680 | })
681 |
682 | await nextTick()
683 | expect(eventDidMountCalled).toBe(true)
684 | })
685 | })
686 |
687 | //
688 |
689 | const OTHER_COMPONENT = {
690 | template: 'other component'
691 | }
692 |
693 | const COMPONENT_USING_ROOT_OPTIONS_IN_SLOT = {
694 | components: {
695 | FullCalendar,
696 | OtherComponent: OTHER_COMPONENT
697 | },
698 | template: `
699 |
700 |
701 |
702 |
703 |
704 | `,
705 | data() {
706 | return {
707 | calendarOptions: {
708 | ...DEFAULT_OPTIONS,
709 | events: buildEvents(1)
710 | }
711 | }
712 | },
713 | }
714 |
715 | it('can use component defined in higher contexts', async () => {
716 | let wrapper = mount(COMPONENT_USING_ROOT_OPTIONS_IN_SLOT)
717 | let eventEl = getRenderedEventEls(wrapper)[0]
718 |
719 | await nextTick()
720 | expect(eventEl.findAll('i').length).toEqual(1)
721 | })
722 |
723 |
724 | it('allows plugin access for slots', async () => {
725 | let helloJp = 'こんにちは、世界'
726 | let i18n = createI18n({
727 | locale: 'ja',
728 | messages: {
729 | ja: {
730 | message: {
731 | hello: helloJp
732 | }
733 | }
734 | }
735 | })
736 | let Component = {
737 | components: {
738 | FullCalendar,
739 | },
740 | template: `
741 |
742 |
743 | {{ $t("message.hello") }}
744 |
745 |
746 | `,
747 | data() {
748 | return {
749 | calendarOptions: {
750 | ...DEFAULT_OPTIONS,
751 | events: buildEvents(1)
752 | }
753 | }
754 | },
755 | }
756 | let wrapper = mount(Component, {
757 | global: {
758 | plugins: [i18n]
759 | }
760 | })
761 |
762 | await nextTick()
763 | let eventEl = getRenderedEventEls(wrapper)[0]
764 | expect(eventEl.text()).toEqual(helloJp)
765 | })
766 |
767 |
768 | // dynamic events
769 |
770 | const DynamicEvent = defineAsyncComponent(() => import('./DynamicEvent.vue'))
771 |
772 | const COMPONENT_WITH_DYNAMIC_SLOTS = {
773 | components: {
774 | FullCalendar,
775 | DynamicEvent
776 | },
777 | template: `
778 |
779 |
780 |
781 |
782 |
783 | `,
784 | data() {
785 | return {
786 | calendarOptions: {
787 | ...DEFAULT_OPTIONS,
788 | events: buildEvents(1)
789 | }
790 | }
791 | }
792 | }
793 |
794 | // https://github.com/fullcalendar/fullcalendar-vue/issues/122
795 | it('renders dynamically imported event', (done) => {
796 | let wrapper = mount(COMPONENT_WITH_DYNAMIC_SLOTS)
797 | let eventEl = getRenderedEventEls(wrapper).at(0)
798 |
799 | setTimeout(() => {
800 | expect(eventEl.findAll('.dynamic-event').length).toEqual(1)
801 | done()
802 | }, 100)
803 | })
804 |
805 |
806 | // slots data binding
807 |
808 | it('slot rendering reacts to bound parent state', async () => {
809 | let wrapper = mount({
810 | components: {
811 | FullCalendar,
812 | },
813 | template: `
814 |
815 |
816 | Event:
817 | Event:
818 | {{ arg.event.title }}
819 |
820 |
821 | `,
822 | data() {
823 | return {
824 | isBold: false,
825 | calendarOptions: {
826 | ...DEFAULT_OPTIONS,
827 | events: buildEvents(1)
828 | }
829 | }
830 | },
831 | methods: {
832 | turnBold() {
833 | this.isBold = true
834 | }
835 | }
836 | })
837 | let eventEl = getRenderedEventEls(wrapper).at(0)
838 |
839 | await nextTick()
840 | expect(eventEl.findAll('b').length).toEqual(0)
841 | expect(eventEl.findAll('i').length).toEqual(1)
842 | wrapper.vm.turnBold()
843 |
844 | await nextTick()
845 | expect(eventEl.findAll('b').length).toEqual(1)
846 | expect(eventEl.findAll('i').length).toEqual(0)
847 | })
848 |
849 |
850 | // FullCalendar options utils
851 |
852 | function buildEvents(length) {
853 | let events = []
854 |
855 | for (let i = 0; i < length; i++) {
856 | events.push(buildEvent(i))
857 | }
858 |
859 | return events
860 | }
861 |
862 | function buildEvent(i) {
863 | return { title: 'event' + i, start: INITIAL_DATE }
864 | }
865 |
866 | function buildToolbar() {
867 | return {
868 | left: 'prev,next today',
869 | center: 'title',
870 | right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
871 | }
872 | }
873 |
874 |
875 | // DOM querying utils
876 |
877 | function isSkeletonRendered(wrapper) {
878 | return wrapper.find('.fc').exists()
879 | }
880 |
881 | function isWeekendsRendered(wrapper) {
882 | return wrapper.find('.fc-day-sat').exists()
883 | }
884 |
885 | function getRenderedEventEls(wrapper) {
886 | return wrapper.findAll('.fc-event')
887 | }
888 |
889 | function getRenderedEventCount(wrapper) {
890 | return getRenderedEventEls(wrapper).length
891 | }
892 |
893 | function getFirstEventTitle(wrapper) {
894 | return wrapper.find('.fc-event-title').text()
895 | }
896 |
897 |
898 | // DOM geometry utils
899 |
900 | function anyElsIntersect(els) {
901 | let rects = els.map((el) => el.getBoundingClientRect())
902 |
903 | for (let i = 0; i < rects.length; i += 1) {
904 | for (let j = i + 1; j < rects.length; j += 1) {
905 | if (rectsIntersect(rects[i], rects[j])) {
906 | return [els[i], els[j]]
907 | }
908 | }
909 | }
910 |
911 | return false
912 | }
913 |
914 | function rectsIntersect(rect0, rect1) {
915 | return rect0.left < rect1.right && rect0.right > rect1.left && rect0.top < rect1.bottom && rect0.bottom > rect1.top
916 | }
917 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "skipLibCheck": true,
4 | "target": "esnext",
5 | "module": "esnext",
6 | "moduleResolution": "node16",
7 | "strict": true,
8 | "declaration": true,
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "outDir": "dist"
12 | },
13 | "include": [
14 | "./src/**/*"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import vue from '@vitejs/plugin-vue'
2 | import sourcemaps from 'rollup-plugin-sourcemaps'
3 |
4 | /*
5 | Tests
6 | */
7 | export default {
8 | root: './tests/', // all other paths are relative to this
9 | define: {
10 | 'process.env.NODE_ENV': JSON.stringify('development'),
11 | },
12 | resolve: {
13 | alias: {
14 | 'vue': 'vue/dist/vue.esm-bundler.js' // for runtime vue templates
15 | }
16 | },
17 | build: {
18 | sourcemap: 'inline', // gets fed into karma-sourcemap-loader
19 | lib: {
20 | entry: 'index.js',
21 | fileName: 'index',
22 | formats: ['iife'], // produces .iife.js
23 | name: 'FullCalendarVueTests' // necessary, but not used
24 | },
25 | minify: false
26 | },
27 | plugins: [
28 | vue(),
29 | sourcemaps() // for READING sourcemaps
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------