Publii: update content

This commit is contained in:
Jan Rippl 2024-03-06 14:33:17 +01:00
parent 01b5bd33ea
commit 081f8ec14b
935 changed files with 87637 additions and 39 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -10,6 +10,20 @@
"name": "CZ 🇨🇿"
},
"items": [
{
"id": "https://jsem.nudista.online/pixelfedcz/",
"url": "https://jsem.nudista.online/pixelfedcz/",
"title": "Pixelfed.cz",
"summary": "<p>Registrace dnes 5. března 2024 ve 20:59...</p>\n",
"content_html": "<p>Registrace dnes 5. března 2024 ve 20:59...</p>\n\n<p> </p>",
"author": {
"name": "CZ 🇨🇿"
},
"tags": [
],
"date_published": "2024-03-05T22:13:11+01:00",
"date_modified": "2024-03-05T22:13:11+01:00"
},
{
"id": "https://jsem.nudista.online/projekt/",
"url": "https://jsem.nudista.online/projekt/",

View file

@ -3,12 +3,35 @@
<title>NoLogWeb</title>
<link href="https://jsem.nudista.online/feed.xml" rel="self" />
<link href="https://jsem.nudista.online" />
<updated>2024-03-02T15:25:44+01:00</updated>
<updated>2024-03-05T22:13:11+01:00</updated>
<author>
<name>CZ 🇨🇿</name>
</author>
<id>https://jsem.nudista.online</id>
<entry>
<title>Pixelfed.cz</title>
<author>
<name>CZ 🇨🇿</name>
</author>
<link href="https://jsem.nudista.online/pixelfedcz/"/>
<id>https://jsem.nudista.online/pixelfedcz/</id>
<updated>2024-03-05T22:13:11+01:00</updated>
<summary>
<![CDATA[
<p>Registrace dnes 5. března 2024 ve 20:59...</p>
]]>
</summary>
<content type="html">
<![CDATA[
<p>Registrace dnes 5. března 2024 ve 20:59...</p>
<p> </p>
]]>
</content>
</entry>
<entry>
<title>Projekt</title>
<author>

View file

@ -0,0 +1,12 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
quote_type = single

View file

@ -0,0 +1,49 @@
name: Bug Report
description: Report something that doesn't work correctly
body:
- type: input
id: reduced-test-case
attributes:
label: Reduced Test Case
description: >
A [reduced test case](https://css-tricks.com/reduced-test-cases/) is required.
A [debugging template](https://fullcalendar.io/reduced-test-cases) will help you get started.
placeholder: URL of reduced test case
validations:
required: true
- type: checkboxes
id: reduced-test-case-confirmation
attributes:
label: >
Do you understand that if a reduced test case is not provided,
we will intentionally delay triaging of your ticket?
options:
- label: I understand
required: true
- type: dropdown
id: connector
attributes:
label: Which connector are you using (React/Angular/etc)?
options:
- No connector (vanilla JS)
- React
- Angular
- Vue
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: >
Describe how to recreate the bug.
What do you expect to happen?
What happens instead?
validations:
required: true
- type: markdown
id: screenshot
attributes:
value: >
**Screenshot:**
If the bug is visual, drag a screenshot into the description above.

View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Getting Help
url: https://stackoverflow.com/questions/tagged/fullcalendar
about: Stuck on a tough problem? Visit the StackOverflow fullcalendar tags

View file

@ -0,0 +1,37 @@
name: Feature Request
description: Suggest an idea you want implemented
body:
- type: checkboxes
id: confirmations
attributes:
label: Checklist
options:
- label: I've already searched through [existing tickets](https://github.com/fullcalendar/fullcalendar/issues)
required: true
- label: Other people will find this feature useful
required: true
- type: dropdown
id: connector
attributes:
label: Is this feature for a specific connector (React/Angular/etc)?
options:
- No connector in particular
- React
- Angular
- Vue
- Other
validations:
required: true
- type: textarea
id: description
attributes:
label: Feature Description
description: Please describe what this feature will do.
validations:
required: true
- type: markdown
id: mockup
attributes:
value: >
**Visual Mockup:**
If you are requesting a new UI, drag some sort of mockup or screenshot into the area above.

View file

@ -0,0 +1,49 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
TZ: "America/New_York"
jobs:
ci:
name: CI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2.2.4
with:
version: 8.6.3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18.13.0
cache: 'pnpm'
- name: Setup Turbo cache
uses: actions/cache@v3
with:
path: node_modules/.cache/turbo
key: turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: turbo-${{ github.job }}-${{ github.ref_name }}-
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm run lint
- name: Build
run: pnpm run build
# - name: Test
# run: pnpm run test

11
fullcalendar-main/.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
# Package manager
node_modules
# Generated
tsconfig.json
tsconfig.tsbuildinfo
dist
# Monorepo
.turbo

7
fullcalendar-main/.npmrc Normal file
View file

@ -0,0 +1,7 @@
engine-strict = true
use-node-version = 18.13.0
prefer-workspace-packages = true
# disable so that filtered install can be used. bug:
# https://github.com/pnpm/pnpm/issues/6300
dedupe-peer-dependents = false

View file

@ -0,0 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

View file

@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
## Contributing Features
The FullCalendar project welcomes PRs for new features, but because there are so many feature requests, and because every new feature requires refinement and maintenance, each PR will be prioritized against the project's other demands and might take a while to make it to an official release.
Furthermore, each new feature should be designed as robustly as possible and be useful beyond the immediate usecase it was initially designed for. Feel free to start a ticket discussing the feature's specs before coding.
## Contributing Bugfixes
Please link to a bug ticket in the description of your PR. If a ticket doesn't exist, please create one. The ticket must contain a reduced test case.
## Contributing Locale Data
Please edit the source files in the `packages/core/locales/` directory.

View file

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021 Adam Shaw
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,73 @@
# FullCalendar
Full-sized drag & drop calendar in JavaScript
- [Project Website](https://fullcalendar.io/)
- [Documentation](https://fullcalendar.io/docs)
- [Changelog](CHANGELOG.md)
- [Support](https://fullcalendar.io/support)
- [License](LICENSE.md)
- [Roadmap](https://fullcalendar.io/roadmap)
Connectors:
- [React](https://github.com/fullcalendar/fullcalendar-react)
- [Angular](https://github.com/fullcalendar/fullcalendar-angular)
- [Vue 3](https://github.com/fullcalendar/fullcalendar-vue) |
[2](https://github.com/fullcalendar/fullcalendar-vue2)
## Bundle
The [FullCalendar Standard Bundle](bundle) is easier to install than individual plugins, though filesize will be larger. It works well with a CDN.
## Installation
Install the FullCalendar core package and any plugins you plan to use:
```sh
npm install @fullcalendar/core @fullcalendar/interaction @fullcalendar/daygrid
```
## Usage
Instantiate a Calendar with plugins and options:
```js
import { Calendar } from '@fullcalendar/core'
import interactionPlugin from '@fullcalendar/interaction'
import dayGridPlugin from '@fullcalendar/daygrid'
const calendarEl = document.getElementById('calendar')
const calendar = new Calendar(calendarEl, {
plugins: [
interactionPlugin,
dayGridPlugin
],
initialView: 'timeGridWeek',
editable: true,
events: [
{ title: 'Meeting', start: new Date() }
]
})
calendar.render()
```
## Development
You must install this repo with [PNPM](https://pnpm.io/):
```
pnpm install
```
Available scripts (via `pnpm run <script>`):
- `build` - build production-ready dist files
- `dev` - build & watch development dist files
- `test` - test headlessly
- `test:dev` - test interactively
- `lint`
- `clean`
[Info about contributing code &raquo;](CONTRIBUTING.md)

View file

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'),
}

View file

@ -0,0 +1,58 @@
# FullCalendar Standard Bundle
Easily render a full-sized drag & drop calendar with a combination of standard plugins
This `fullcalendar` package bundles these plugins:
- [@fullcalendar/core](https://github.com/fullcalendar/fullcalendar/tree/main/packages/core)
- [@fullcalendar/interaction](https://github.com/fullcalendar/fullcalendar/tree/main/packages/interaction)
- [@fullcalendar/daygrid](https://github.com/fullcalendar/fullcalendar/tree/main/packages/daygrid)
- [@fullcalendar/timegrid](https://github.com/fullcalendar/fullcalendar/tree/main/packages/timegrid)
- [@fullcalendar/list](https://github.com/fullcalendar/fullcalendar/tree/main/packages/list)
- [@fullcalendar/multimonth](https://github.com/fullcalendar/fullcalendar/tree/main/packages/multimonth)
## Usage with CDN or ZIP archive
Load the `index.global.min.js` file and use the `FullCalendar` global namespace:
```html
<!DOCTYPE html>
<html>
<head>
<script src='https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const calendarEl = document.getElementById('calendar')
const calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth'
})
calendar.render()
})
</script>
</head>
<body>
<div id='calendar'></div>
</body>
</html>
```
## Usage with NPM and ES modules
```sh
npm install fullcalendar
```
```js
import { Calendar } from 'fullcalendar'
document.addEventListener('DOMContentLoaded', function() {
const calendarEl = document.getElementById('calendar')
const calendar = new Calendar(calendarEl, {
initialView: 'dayGridMonth'
})
calendar.render()
})
```

View file

@ -0,0 +1,101 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
},
initialDate: '2023-01-12',
navLinks: true, // can click day/week names to navigate views
businessHours: true, // display business hours
editable: true,
selectable: true,
events: [
{
title: 'Business Lunch',
start: '2023-01-03T13:00:00',
constraint: 'businessHours'
},
{
title: 'Meeting',
start: '2023-01-13T11:00:00',
constraint: 'availableForMeeting', // defined below
color: '#257e4a'
},
{
title: 'Conference',
start: '2023-01-18',
end: '2023-01-20'
},
{
title: 'Party',
start: '2023-01-29T20:00:00'
},
// areas where "Meeting" must be dropped
{
groupId: 'availableForMeeting',
start: '2023-01-11T10:00:00',
end: '2023-01-11T16:00:00',
display: 'background'
},
{
groupId: 'availableForMeeting',
start: '2023-01-13T10:00:00',
end: '2023-01-13T16:00:00',
display: 'background'
},
// red areas where no events can be dropped
{
start: '2023-01-24',
end: '2023-01-28',
overlap: false,
display: 'background',
color: '#ff9f89'
},
{
start: '2023-01-06',
end: '2023-01-08',
overlap: false,
display: 'background',
color: '#ff9f89'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: {
left: 'prevYear,prev,next,nextYear today',
center: 'title',
right: 'dayGridMonth,dayGridWeek,dayGridDay'
},
initialDate: '2023-01-12',
navLinks: true, // can click day/week names to navigate views
editable: true,
dayMaxEvents: true, // allow "more" link when too many events
events: [
{
title: 'All Day Event',
start: '2023-01-01'
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var srcCalendarEl = document.getElementById('source-calendar');
var destCalendarEl = document.getElementById('destination-calendar');
var srcCalendar = new FullCalendar.Calendar(srcCalendarEl, {
editable: true,
initialDate: '2023-01-12',
events: [
{
title: 'event1',
start: '2023-01-11T10:00:00',
end: '2023-01-11T16:00:00'
},
{
title: 'event2',
start: '2023-01-13T10:00:00',
end: '2023-01-13T16:00:00'
}
],
eventLeave: function(info) {
console.log('event left!', info.event);
}
});
var destCalendar = new FullCalendar.Calendar(destCalendarEl, {
initialDate: '2023-01-12',
editable: true,
droppable: true, // will let it receive events!
eventReceive: function(info) {
console.log('event received!', info.event);
}
});
srcCalendar.render();
destCalendar.render();
});
</script>
<style>
body {
margin: 20px 0 0 20px;
font-size: 14px;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
}
#source-calendar,
#destination-calendar {
float: left;
width: 600px;
margin: 0 20px 20px 0;
}
</style>
</head>
<body>
<div id='source-calendar'></div>
<div id='destination-calendar'></div>
</body>
</html>

View file

@ -0,0 +1,149 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
/* initialize the external events
-----------------------------------------------------------------*/
var containerEl = document.getElementById('external-events-list');
new FullCalendar.Draggable(containerEl, {
itemSelector: '.fc-event',
eventData: function(eventEl) {
return {
title: eventEl.innerText.trim()
}
}
});
//// the individual way to do it
// var containerEl = document.getElementById('external-events-list');
// var eventEls = Array.prototype.slice.call(
// containerEl.querySelectorAll('.fc-event')
// );
// eventEls.forEach(function(eventEl) {
// new FullCalendar.Draggable(eventEl, {
// eventData: {
// title: eventEl.innerText.trim(),
// }
// });
// });
/* initialize the calendar
-----------------------------------------------------------------*/
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
editable: true,
droppable: true, // this allows things to be dropped onto the calendar
drop: function(arg) {
// is the "remove after drop" checkbox checked?
if (document.getElementById('drop-remove').checked) {
// if so, remove the element from the "Draggable Events" list
arg.draggedEl.parentNode.removeChild(arg.draggedEl);
}
}
});
calendar.render();
});
</script>
<style>
body {
margin-top: 40px;
font-size: 14px;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
}
#external-events {
position: fixed;
left: 20px;
top: 20px;
width: 150px;
padding: 0 10px;
border: 1px solid #ccc;
background: #eee;
text-align: left;
}
#external-events h4 {
font-size: 16px;
margin-top: 0;
padding-top: 1em;
}
#external-events .fc-event {
margin: 3px 0;
cursor: move;
}
#external-events p {
margin: 1.5em 0;
font-size: 11px;
color: #666;
}
#external-events p input {
margin: 0;
vertical-align: middle;
}
#calendar-wrap {
margin-left: 200px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='wrap'>
<div id='external-events'>
<h4>Draggable Events</h4>
<div id='external-events-list'>
<div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'>
<div class='fc-event-main'>My Event 1</div>
</div>
<div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'>
<div class='fc-event-main'>My Event 2</div>
</div>
<div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'>
<div class='fc-event-main'>My Event 3</div>
</div>
<div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'>
<div class='fc-event-main'>My Event 4</div>
</div>
<div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'>
<div class='fc-event-main'>My Event 5</div>
</div>
</div>
<p>
<input type='checkbox' id='drop-remove' />
<label for='drop-remove'>remove after drop</label>
</p>
</div>
<div id='calendar-wrap'>
<div id='calendar'></div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
height: '100%',
expandRows: true,
slotMinTime: '08:00',
slotMaxTime: '20:00',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
initialView: 'dayGridMonth',
initialDate: '2023-01-12',
navLinks: true, // can click day/week names to navigate views
editable: true,
selectable: true,
nowIndicator: true,
dayMaxEvents: true, // allow "more" link when too many events
events: [
{
title: 'All Day Event',
start: '2023-01-01',
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
html, body {
overflow: hidden; /* don't do scrollbars */
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.fc-header-toolbar {
/*
the calendar will be butting up against the edges,
but let's scoot in the header's buttons
*/
padding-top: 1em;
padding-left: 1em;
padding-right: 1em;
}
</style>
</head>
<body>
<div id='calendar-container'>
<div id='calendar'></div>
</div>
</body>
</html>

View file

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
height: 'auto',
// stickyHeaderDates: false, // for disabling
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'listMonth,listYear'
},
// customize the button names,
// otherwise they'd all just say "list"
views: {
listMonth: { buttonText: 'list month' },
listYear: { buttonText: 'list year' }
},
initialView: 'listYear',
initialDate: '2023-01-12',
navLinks: true, // can click day/week names to navigate views
editable: true,
events: [
{
title: 'repeating event 1',
daysOfWeek: [ 1, 2, 3 ],
duration: '00:30'
},
{
title: 'repeating event 2',
daysOfWeek: [ 1, 2, 3 ],
duration: '00:30'
},
{
title: 'repeating event 3',
daysOfWeek: [ 1, 2, 3 ],
duration: '00:30'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'listDay,listWeek'
},
// customize the button names,
// otherwise they'd all just say "list"
views: {
listDay: { buttonText: 'list day' },
listWeek: { buttonText: 'list week' }
},
initialView: 'listWeek',
initialDate: '2023-01-12',
navLinks: true, // can click day/week names to navigate views
editable: true,
dayMaxEvents: true, // allow "more" link when too many events
events: [
{
title: 'All Day Event',
start: '2023-01-01'
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
initialDate: '2023-01-12',
editable: true,
selectable: true,
businessHours: true,
dayMaxEvents: true, // allow "more" link when too many events
events: [
{
title: 'All Day Event',
start: '2023-01-01'
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'multiMonthYear,dayGridMonth,timeGridWeek'
},
initialView: 'multiMonthYear',
initialDate: '2023-01-12',
editable: true,
selectable: true,
dayMaxEvents: true, // allow "more" link when too many events
// multiMonthMaxColumns: 1, // guarantee single column
// showNonCurrentDates: true,
// fixedWeekCount: false,
// businessHours: true,
// weekends: false,
events: [
{
title: 'All Day Event',
start: '2023-01-01'
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1200px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridYear,dayGridMonth,timeGridWeek'
},
initialView: 'dayGridYear',
initialDate: '2023-01-12',
editable: true,
selectable: true,
dayMaxEvents: true, // allow "more" link when too many events
// businessHours: true,
// weekends: false,
events: [
{
title: 'All Day Event',
start: '2023-01-01'
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1200px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
initialDate: '2023-01-12',
initialView: 'timeGridWeek',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
height: 'auto',
navLinks: true, // can click day/week names to navigate views
editable: true,
selectable: true,
selectMirror: true,
nowIndicator: true,
events: [
{
title: 'All Day Event',
start: '2023-01-01',
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,123 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
initialDate: '2023-01-12',
navLinks: true, // can click day/week names to navigate views
selectable: true,
selectMirror: true,
select: function(arg) {
var title = prompt('Event Title:');
if (title) {
calendar.addEvent({
title: title,
start: arg.start,
end: arg.end,
allDay: arg.allDay
})
}
calendar.unselect()
},
eventClick: function(arg) {
if (confirm('Are you sure you want to delete this event?')) {
arg.event.remove()
}
},
editable: true,
dayMaxEvents: true, // allow "more" link when too many events
events: [
{
title: 'All Day Event',
start: '2023-01-01'
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<script src='../dist/index.global.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
initialDate: '2023-01-12',
initialView: 'timeGridWeek',
nowIndicator: true,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
navLinks: true, // can click day/week names to navigate views
editable: true,
selectable: true,
selectMirror: true,
dayMaxEvents: true, // allow "more" link when too many events
events: [
{
title: 'All Day Event',
start: '2023-01-01',
},
{
title: 'Long Event',
start: '2023-01-07',
end: '2023-01-10'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-09T16:00:00'
},
{
groupId: 999,
title: 'Repeating Event',
start: '2023-01-16T16:00:00'
},
{
title: 'Conference',
start: '2023-01-11',
end: '2023-01-13'
},
{
title: 'Meeting',
start: '2023-01-12T10:30:00',
end: '2023-01-12T12:30:00'
},
{
title: 'Lunch',
start: '2023-01-12T12:00:00'
},
{
title: 'Meeting',
start: '2023-01-12T14:30:00'
},
{
title: 'Happy Hour',
start: '2023-01-12T17:30:00'
},
{
title: 'Dinner',
start: '2023-01-12T20:00:00'
},
{
title: 'Birthday Party',
start: '2023-01-13T07:00:00'
},
{
title: 'Click for Google',
url: 'http://google.com/',
start: '2023-01-28'
}
]
});
calendar.render();
});
</script>
<style>
body {
margin: 40px 10px;
padding: 0;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
#calendar {
max-width: 1100px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id='calendar'></div>
</body>
</html>

View file

@ -0,0 +1,49 @@
{
"name": "fullcalendar",
"version": "6.1.11",
"title": "FullCalendar Standard Bundle",
"description": "Easily render a full-sized drag & drop calendar with a combination of standard plugins",
"homepage": "https://fullcalendar.io/docs/initialize-globals",
"dependencies": {
"@fullcalendar/core": "~6.1.11",
"@fullcalendar/daygrid": "~6.1.11",
"@fullcalendar/interaction": "~6.1.11",
"@fullcalendar/list": "~6.1.11",
"@fullcalendar/multimonth": "~6.1.11",
"@fullcalendar/timegrid": "~6.1.11"
},
"devDependencies": {
"@fullcalendar-scripts/standard": "*"
},
"scripts": {
"build": "standard-scripts pkg:build",
"clean": "standard-scripts pkg:clean",
"lint": "eslint ."
},
"type": "module",
"tsConfig": {
"extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/.tsout"
},
"include": [
"./src/**/*"
]
},
"buildConfig": {
"exports": {
".": {
"iife": true
}
},
"iifeGlobals": {
".": "FullCalendar",
"*": ""
}
},
"publishConfig": {
"directory": "./dist",
"linkDirectory": true
}
}

View file

@ -0,0 +1,5 @@
import * as Internal from '@fullcalendar/core/internal'
import * as Preact from '@fullcalendar/core/preact'
export { Internal, Preact }
export * from './index.js'

View file

@ -0,0 +1,17 @@
import { globalPlugins } from '@fullcalendar/core'
import interactionPlugin from '@fullcalendar/interaction'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import listPlugin from '@fullcalendar/list'
import multiMonthPlugin from '@fullcalendar/multimonth'
globalPlugins.push(
interactionPlugin,
dayGridPlugin,
timeGridPlugin,
listPlugin,
multiMonthPlugin,
)
export * from '@fullcalendar/core'
export * from '@fullcalendar/interaction' // for Draggable

View file

@ -0,0 +1,47 @@
{
"private": true,
"name": "@fullcalendar-monorepos/standard",
"description": "Full-sized drag & drop event calendar in JavaScript",
"version": "6.1.11",
"keywords": [
"calendar",
"event",
"full-sized",
"fullcalendar"
],
"homepage": "https://fullcalendar.io",
"bugs": "https://fullcalendar.io/reporting-bugs",
"repository": {
"type": "git",
"url": "https://github.com/fullcalendar/fullcalendar.git"
},
"license": "MIT",
"author": {
"name": "Adam Shaw",
"email": "arshaw@arshaw.com",
"url": "http://arshaw.com/"
},
"copyright": "2023 Adam Shaw",
"devDependencies": {
"@fullcalendar-scripts/standard": "*"
},
"type": "module",
"scripts": {
"postinstall": "standard-scripts postinstall",
"lint": "standard-scripts lint",
"build": "standard-scripts build",
"dev": "standard-scripts dev",
"test": "standard-scripts test",
"test:dev": "standard-scripts test --dev",
"clean": "standard-scripts clean",
"ci": "pnpm run lint && pnpm run build && pnpm run test"
},
"engines": {
"pnpm": ">=7.9.5"
},
"pnpm": {
"patchedDependencies": {
"jasmine-jquery@2.1.1": "scripts/patches/jasmine-jquery@2.1.1.patch"
}
}
}

View file

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'),
}

View file

@ -0,0 +1,46 @@
# FullCalendar Bootstrap 4 Plugin
[Bootstrap 4](https://getbootstrap.com/docs/4.6/getting-started/introduction/) theme for [FullCalendar](https://fullcalendar.io)
> For [Bootstrap 5](https://getbootstrap.com/), use the [@fullcalendar/bootstrap5](https://github.com/fullcalendar/fullcalendar/tree/main/packages/bootstrap5) package
## Installation
First, ensure the necessary Bootstrap packages are installed:
```sh
npm install bootstrap@4 @fortawesome/fontawesome-free
```
Then, install the FullCalendar core package, the Bootstrap plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)):
```sh
npm install @fullcalendar/core @fullcalendar/bootstrap @fullcalendar/daygrid
```
## Usage
Instantiate a Calendar with the necessary plugins and options:
```js
import { Calendar } from '@fullcalendar/core'
import bootstrapPlugin from '@fullcalendar/bootstrap'
import dayGridPlugin from '@fullcalendar/daygrid'
// import third-party stylesheets directly from your JS
import 'bootstrap/dist/css/bootstrap.css'
import '@fortawesome/fontawesome-free/css/all.css' // needs additional webpack config!
const calendarEl = document.getElementById('calendar')
const calendar = new Calendar(calendarEl, {
plugins: [
bootstrapPlugin,
dayGridPlugin
],
themeSystem: 'bootstrap', // important!
initialView: 'dayGridMonth'
})
calendar.render()
```

View file

@ -0,0 +1,50 @@
{
"name": "@fullcalendar/bootstrap",
"version": "6.1.11",
"title": "FullCalendar Bootstrap 4 Plugin",
"description": "Bootstrap 4 theme for FullCalendar",
"keywords": [
"bootstrap",
"bootstrap4"
],
"homepage": "https://fullcalendar.io/docs/bootstrap4",
"peerDependencies": {
"@fullcalendar/core": "~6.1.11"
},
"devDependencies": {
"@fullcalendar/core": "~6.1.11",
"@fullcalendar-scripts/standard": "*"
},
"scripts": {
"build": "standard-scripts pkg:build",
"clean": "standard-scripts pkg:clean",
"lint": "eslint ."
},
"type": "module",
"tsConfig": {
"extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/.tsout"
},
"include": [
"./src/**/*"
]
},
"buildConfig": {
"exports": {
".": {
"iife": true
},
"./internal": {}
},
"iifeGlobals": {
".": "FullCalendar.Bootstrap",
"./internal": "FullCalendar.Bootstrap.Internal"
}
},
"publishConfig": {
"directory": "./dist",
"linkDirectory": true
}
}

View file

@ -0,0 +1,37 @@
import { Theme } from '@fullcalendar/core/internal'
class BootstrapTheme extends Theme {
}
BootstrapTheme.prototype.classes = {
root: 'fc-theme-bootstrap', // TODO: compute this off of registered theme name
table: 'table-bordered', // don't attache the `table` class. we only want the borders, not any layout
tableCellShaded: 'table-active',
buttonGroup: 'btn-group',
button: 'btn btn-primary',
buttonActive: 'active',
popover: 'popover',
popoverHeader: 'popover-header',
popoverContent: 'popover-body',
}
BootstrapTheme.prototype.baseIconClass = 'fa'
BootstrapTheme.prototype.iconClasses = {
close: 'fa-times',
prev: 'fa-chevron-left',
next: 'fa-chevron-right',
prevYear: 'fa-angle-double-left',
nextYear: 'fa-angle-double-right',
}
BootstrapTheme.prototype.rtlIconClasses = {
prev: 'fa-chevron-right',
next: 'fa-chevron-left',
prevYear: 'fa-angle-double-right',
nextYear: 'fa-angle-double-left',
}
BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome' // TODO: make TS-friendly. move the option-processing into this plugin
BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'bootstrapFontAwesome'
BootstrapTheme.prototype.iconOverridePrefix = 'fa-'
export { BootstrapTheme }

View file

@ -0,0 +1,12 @@
.fc-theme-bootstrap {
& a:not([href]) {
color: inherit; // natural color for navlinks
}
& .fc-more-link:hover {
text-decoration: none;
}
}

View file

@ -0,0 +1,8 @@
import { globalPlugins } from '@fullcalendar/core'
import plugin from './index.js'
import * as Internal from './internal.js'
globalPlugins.push(plugin)
export { plugin as default, Internal }
export * from './index.js'

View file

@ -0,0 +1,10 @@
import { createPlugin, PluginDef } from '@fullcalendar/core'
import { BootstrapTheme } from './BootstrapTheme.js'
import './index.css'
export default createPlugin({
name: '<%= pkgName %>',
themeClasses: {
bootstrap: BootstrapTheme,
},
}) as PluginDef

View file

@ -0,0 +1,3 @@
import './index.css'
export { BootstrapTheme } from './BootstrapTheme.js'

View file

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'),
}

View file

@ -0,0 +1,44 @@
# FullCalendar Bootstrap 5 Plugin
[Bootstrap 5](https://getbootstrap.com/) theme for [FullCalendar](https://fullcalendar.io)
## Installation
First, ensure the necessary Bootstrap packages are installed:
```sh
npm install bootstrap@5 bootstrap-icons
```
Then, install the FullCalendar core package, the Bootstrap plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)):
```sh
npm install @fullcalendar/core @fullcalendar/bootstrap5 @fullcalendar/daygrid
```
## Usage
Instantiate a Calendar with the necessary plugins and options:
```js
import { Calendar } from '@fullcalendar/core'
import bootstrap5Plugin from '@fullcalendar/bootstrap5'
import dayGridPlugin from '@fullcalendar/daygrid'
// import bootstrap stylesheets directly from your JS
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-icons/font/bootstrap-icons.css' // needs additional webpack config!
const calendarEl = document.getElementById('calendar')
const calendar = new Calendar(calendarEl, {
plugins: [
bootstrap5Plugin,
dayGridPlugin
],
themeSystem: 'bootstrap5', // important!
initialView: 'dayGridMonth'
})
calendar.render()
```

View file

@ -0,0 +1,50 @@
{
"name": "@fullcalendar/bootstrap5",
"version": "6.1.11",
"title": "FullCalendar Bootstrap 5 Plugin",
"description": "Bootstrap 5 theme for FullCalendar",
"keywords": [
"bootstrap",
"bootstrap5"
],
"homepage": "https://fullcalendar.io/docs/bootstrap5",
"peerDependencies": {
"@fullcalendar/core": "~6.1.11"
},
"devDependencies": {
"@fullcalendar/core": "~6.1.11",
"@fullcalendar-scripts/standard": "*"
},
"scripts": {
"build": "standard-scripts pkg:build",
"clean": "standard-scripts pkg:clean",
"lint": "eslint ."
},
"type": "module",
"tsConfig": {
"extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/.tsout"
},
"include": [
"./src/**/*"
]
},
"buildConfig": {
"exports": {
".": {
"iife": true
},
"./internal": {}
},
"iifeGlobals": {
".": "FullCalendar.Bootstrap5",
"./internal": "FullCalendar.Bootstrap5.Internal"
}
},
"publishConfig": {
"directory": "./dist",
"linkDirectory": true
}
}

View file

@ -0,0 +1,37 @@
import { Theme } from '@fullcalendar/core/internal'
export class BootstrapTheme extends Theme {
}
BootstrapTheme.prototype.classes = {
root: 'fc-theme-bootstrap5',
tableCellShaded: 'fc-theme-bootstrap5-shaded',
buttonGroup: 'btn-group',
button: 'btn btn-primary',
buttonActive: 'active',
popover: 'popover',
popoverHeader: 'popover-header',
popoverContent: 'popover-body',
}
BootstrapTheme.prototype.baseIconClass = 'bi'
BootstrapTheme.prototype.iconClasses = {
close: 'bi-x-lg',
prev: 'bi-chevron-left',
next: 'bi-chevron-right',
prevYear: 'bi-chevron-double-left',
nextYear: 'bi-chevron-double-right',
}
BootstrapTheme.prototype.rtlIconClasses = {
prev: 'bi-chevron-right',
next: 'bi-chevron-left',
prevYear: 'bi-chevron-double-right',
nextYear: 'bi-chevron-double-left',
}
// wtf
BootstrapTheme.prototype.iconOverrideOption = 'buttonIcons' // TODO: make TS-friendly
BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'icon'
BootstrapTheme.prototype.iconOverridePrefix = 'bi-'
export { Theme }

View file

@ -0,0 +1,25 @@
.fc-theme-bootstrap5 {
& a:not([href]) {
color: inherit;
text-decoration: inherit;
}
& .fc-list,
& .fc-scrollgrid,
& td,
& th {
border: 1px solid var(--bs-gray-400);
}
// HACK: reapply core styles after highe-precedence border statement above
& .fc-scrollgrid {
border-right-width: 0;
border-bottom-width: 0;
}
}
.fc-theme-bootstrap5-shaded {
background-color: var(--bs-gray-200);
}

View file

@ -0,0 +1,8 @@
import { globalPlugins } from '@fullcalendar/core'
import plugin from './index.js'
import * as Internal from './internal.js'
globalPlugins.push(plugin)
export { plugin as default, Internal }
export * from './index.js'

View file

@ -0,0 +1,10 @@
import { createPlugin, PluginDef } from '@fullcalendar/core'
import { BootstrapTheme } from './BootstrapTheme.js'
import './index.css'
export default createPlugin({
name: '<%= pkgName %>',
themeClasses: {
bootstrap5: BootstrapTheme,
},
}) as PluginDef

View file

@ -0,0 +1,3 @@
import './index.css'
export { BootstrapTheme } from './BootstrapTheme.js'

View file

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'),
}

View file

@ -0,0 +1,44 @@
# FullCalendar Core
FullCalendar core package for rendering a calendar
## Installation
This package is never used alone. Use it with least one plugin (like [daygrid](https://fullcalendar.io/docs/month-view)):
```sh
npm install @fullcalendar/core @fullcalendar/daygrid
```
## Usage
First, ensure there's a DOM element for your calendar to render into:
```html
<body>
<div id='calendar'></div>
</body>
```
Then, instantiate a Calendar object with [options](https://fullcalendar.io/docs#toc) and call its `render` method:
```js
import { Calendar } from '@fullcalendar/core'
import dayGridPlugin from '@fullcalendar/daygrid'
const calendarEl = document.getElementById('calendar')
const calendar = new Calendar(calendarEl, {
plugins: [
dayGridPlugin
// any other plugins
],
initialView: 'dayGridMonth',
weekends: false,
events: [
{ title: 'Meeting', start: new Date() }
]
})
calendar.render()
```

View file

@ -0,0 +1,58 @@
{
"name": "@fullcalendar/core",
"version": "6.1.11",
"title": "FullCalendar Core",
"description": "FullCalendar core package for rendering a calendar",
"dependencies": {
"preact": "~10.12.1"
},
"devDependencies": {
"@fullcalendar-scripts/standard": "*",
"globby": "^13.1.2",
"handlebars": "^4.1.2"
},
"scripts": {
"build": "standard-scripts pkg:build",
"clean": "standard-scripts pkg:clean",
"lint": "eslint ."
},
"type": "module",
"tsConfig": {
"extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/.tsout"
},
"include": [
"./src/**/*"
]
},
"buildConfig": {
"exports": {
".": {
"iife": true
},
"./preact": {},
"./internal": {},
"./locales-all": {
"iife": true,
"generator": "./scripts/generate-locales-all.js"
},
"./locales/*": {
"iife": true,
"iifeGenerator": "./scripts/generate-locale-iife.js"
}
},
"iifeGlobals": {
".": "FullCalendar",
"./preact": "FullCalendar.Preact",
"./internal": "FullCalendar.Internal",
"preact": "",
"preact/compat": ""
}
},
"publishConfig": {
"directory": "./dist",
"linkDirectory": true
}
}

View file

@ -0,0 +1,21 @@
import { join as joinPaths, basename } from 'path'
import { fileURLToPath } from 'url'
import { readFile } from 'fs/promises'
import handlebars from 'handlebars'
const thisPkgDir = joinPaths(fileURLToPath(import.meta.url), '../..')
const templatePath = joinPaths(thisPkgDir, 'src/locales/global.js.tpl')
export function getWatchPaths() {
return [templatePath, templatePath]
}
export default async function(config) {
const localeCode = basename(config.entryAlias)
const templateText = await readFile(templatePath, 'utf8')
const template = handlebars.compile(templateText)
const code = template({ localeCode })
return code
}

View file

@ -0,0 +1,27 @@
import { join as joinPaths } from 'path'
import { fileURLToPath } from 'url'
import { readFile } from 'fs/promises'
import { globby } from 'globby'
import handlebars from 'handlebars'
const thisPkgDir = joinPaths(fileURLToPath(import.meta.url), '../..')
const templatePath = joinPaths(thisPkgDir, 'src/locales-all.js.tpl')
const localesDir = joinPaths(thisPkgDir, 'src/locales')
export function getWatchPaths() {
return [
templatePath,
localesDir,
]
}
export default async function() {
const localeFilenames = await globby('*.ts', { cwd: localesDir })
const localeCodes = localeFilenames.map((filename) => filename.replace(/\.ts$/, ''))
const templateText = await readFile(templatePath, 'utf8')
const template = handlebars.compile(templateText)
const code = template({ localeCodes })
return code
}

View file

@ -0,0 +1,156 @@
import { CalendarOptions } from './options.js'
import { DelayedRunner } from './util/DelayedRunner.js'
import { CalendarDataManager } from './reducers/CalendarDataManager.js'
import { Action } from './reducers/Action.js'
import { CalendarData } from './reducers/data-types.js'
import { CalendarRoot } from './CalendarRoot.js'
import { CalendarContent } from './CalendarContent.js'
import { createElement, render, flushSync } from './preact.js'
import { isArraysEqual } from './util/array.js'
import { CssDimValue } from './scrollgrid/util.js'
import { applyStyleProp } from './util/dom-manip.js'
import { RenderId } from './content-inject/RenderId.js'
import { CalendarImpl } from './api/CalendarImpl.js'
import { ensureElHasStyles } from './styleUtils.js'
export class Calendar extends CalendarImpl {
el: HTMLElement
private currentData: CalendarData
private renderRunner: DelayedRunner
private isRendering = false
private isRendered = false
private currentClassNames: string[] = []
private customContentRenderId = 0
constructor(el: HTMLElement, optionOverrides: CalendarOptions = {}) {
super()
ensureElHasStyles(el)
this.el = el
this.renderRunner = new DelayedRunner(this.handleRenderRequest)
new CalendarDataManager({ // eslint-disable-line no-new
optionOverrides,
calendarApi: this,
onAction: this.handleAction,
onData: this.handleData,
})
}
private handleAction = (action: Action) => {
// actions we know we want to render immediately
switch (action.type) {
case 'SET_EVENT_DRAG':
case 'SET_EVENT_RESIZE':
this.renderRunner.tryDrain()
}
}
private handleData = (data: CalendarData) => {
this.currentData = data
this.renderRunner.request(data.calendarOptions.rerenderDelay)
}
private handleRenderRequest = () => {
if (this.isRendering) {
this.isRendered = true
let { currentData } = this
flushSync(() => {
render(
<CalendarRoot options={currentData.calendarOptions} theme={currentData.theme} emitter={currentData.emitter}>
{(classNames, height, isHeightAuto, forPrint) => {
this.setClassNames(classNames)
this.setHeight(height)
return (
<RenderId.Provider value={this.customContentRenderId}>
<CalendarContent
isHeightAuto={isHeightAuto}
forPrint={forPrint}
{...currentData}
/>
</RenderId.Provider>
)
}}
</CalendarRoot>,
this.el,
)
})
} else if (this.isRendered) {
this.isRendered = false
render(null, this.el)
this.setClassNames([])
this.setHeight('')
}
}
render() {
let wasRendering = this.isRendering
if (!wasRendering) {
this.isRendering = true
} else {
this.customContentRenderId += 1
}
this.renderRunner.request()
if (wasRendering) {
this.updateSize()
}
}
destroy(): void {
if (this.isRendering) {
this.isRendering = false
this.renderRunner.request()
}
}
updateSize(): void {
flushSync(() => {
super.updateSize()
})
}
batchRendering(func): void {
this.renderRunner.pause('batchRendering')
func()
this.renderRunner.resume('batchRendering')
}
pauseRendering() { // available to plugins
this.renderRunner.pause('pauseRendering')
}
resumeRendering() { // available to plugins
this.renderRunner.resume('pauseRendering', true)
}
resetOptions(optionOverrides, changedOptionNames?: string[]) {
this.currentDataManager.resetOptions(optionOverrides, changedOptionNames)
}
private setClassNames(classNames: string[]) {
if (!isArraysEqual(classNames, this.currentClassNames)) {
let { classList } = this.el
for (let className of this.currentClassNames) {
classList.remove(className)
}
for (let className of classNames) {
classList.add(className)
}
this.currentClassNames = classNames
}
}
private setHeight(height: CssDimValue) {
applyStyleProp(this.el, 'height', height)
}
}

View file

@ -0,0 +1,289 @@
import { ViewContextType, buildViewContext } from './ViewContext.js'
import { ViewSpec } from './structs/view-spec.js'
import { ViewProps } from './View.js'
import { Toolbar } from './Toolbar.js'
import { DateProfileGenerator, DateProfile } from './DateProfileGenerator.js'
import { rangeContainsMarker } from './datelib/date-range.js'
import { memoize } from './util/memoize.js'
import { DateMarker } from './datelib/marker.js'
import { CalendarData } from './reducers/data-types.js'
import { ViewPropsTransformerClass } from './plugin-system-struct.js'
import { createElement, createRef, Fragment, VNode } from './preact.js'
import { ViewHarness } from './ViewHarness.js'
import {
Interaction,
InteractionSettingsInput,
InteractionClass,
parseInteractionSettings,
interactionSettingsStore,
} from './interactions/interaction.js'
import { DateComponent } from './component/DateComponent.js'
import { EventClicking } from './interactions/EventClicking.js'
import { EventHovering } from './interactions/EventHovering.js'
import { getNow } from './reducers/current-date.js'
import { CalendarInteraction } from './calendar-utils.js'
import { DelayedRunner } from './util/DelayedRunner.js'
import { PureComponent } from './vdom-util.js'
import { getUniqueDomId } from './util/dom-manip.js'
export interface CalendarContentProps extends CalendarData {
forPrint: boolean
isHeightAuto: boolean
}
export class CalendarContent extends PureComponent<CalendarContentProps> {
private buildViewContext = memoize(buildViewContext)
private buildViewPropTransformers = memoize(buildViewPropTransformers)
private buildToolbarProps = memoize(buildToolbarProps)
private headerRef = createRef<Toolbar>()
private footerRef = createRef<Toolbar>()
private interactionsStore: { [componentUid: string]: Interaction[] } = {}
private calendarInteractions: CalendarInteraction[]
// eslint-disable-next-line
state = {
viewLabelId: getUniqueDomId(),
}
/*
renders INSIDE of an outer div
*/
render() {
let { props } = this
let { toolbarConfig, options } = props
let toolbarProps = this.buildToolbarProps(
props.viewSpec,
props.dateProfile,
props.dateProfileGenerator,
props.currentDate,
getNow(props.options.now, props.dateEnv), // TODO: use NowTimer????
props.viewTitle,
)
let viewVGrow = false
let viewHeight: string | number = ''
let viewAspectRatio: number | undefined
if (props.isHeightAuto || props.forPrint) {
viewHeight = ''
} else if (options.height != null) {
viewVGrow = true
} else if (options.contentHeight != null) {
viewHeight = options.contentHeight
} else {
viewAspectRatio = Math.max(options.aspectRatio, 0.5) // prevent from getting too tall
}
let viewContext = this.buildViewContext(
props.viewSpec,
props.viewApi,
props.options,
props.dateProfileGenerator,
props.dateEnv,
props.theme,
props.pluginHooks,
props.dispatch,
props.getCurrentData,
props.emitter,
props.calendarApi,
this.registerInteractiveComponent,
this.unregisterInteractiveComponent,
)
let viewLabelId = (toolbarConfig.header && toolbarConfig.header.hasTitle)
? this.state.viewLabelId
: undefined
return (
<ViewContextType.Provider value={viewContext}>
{toolbarConfig.header && (
<Toolbar
ref={this.headerRef}
extraClassName="fc-header-toolbar"
model={toolbarConfig.header}
titleId={viewLabelId}
{...toolbarProps}
/>
)}
<ViewHarness
liquid={viewVGrow}
height={viewHeight}
aspectRatio={viewAspectRatio}
labeledById={viewLabelId}
>
{this.renderView(props)}
{this.buildAppendContent()}
</ViewHarness>
{toolbarConfig.footer && (
<Toolbar
ref={this.footerRef}
extraClassName="fc-footer-toolbar"
model={toolbarConfig.footer}
titleId=""
{...toolbarProps}
/>
)}
</ViewContextType.Provider>
)
}
componentDidMount() {
let { props } = this
this.calendarInteractions = props.pluginHooks.calendarInteractions
.map((CalendarInteractionClass) => new CalendarInteractionClass(props))
window.addEventListener('resize', this.handleWindowResize)
let { propSetHandlers } = props.pluginHooks
for (let propName in propSetHandlers) {
propSetHandlers[propName](props[propName], props)
}
}
componentDidUpdate(prevProps: CalendarContentProps) {
let { props } = this
let { propSetHandlers } = props.pluginHooks
for (let propName in propSetHandlers) {
if (props[propName] !== prevProps[propName]) {
propSetHandlers[propName](props[propName], props)
}
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleWindowResize)
this.resizeRunner.clear()
for (let interaction of this.calendarInteractions) {
interaction.destroy()
}
this.props.emitter.trigger('_unmount')
}
buildAppendContent(): VNode {
let { props } = this
let children = props.pluginHooks.viewContainerAppends.map(
(buildAppendContent) => buildAppendContent(props),
)
return createElement(Fragment, {}, ...children)
}
renderView(props: CalendarContentProps) {
let { pluginHooks } = props
let { viewSpec } = props
let viewProps: ViewProps = {
dateProfile: props.dateProfile,
businessHours: props.businessHours,
eventStore: props.renderableEventStore, // !
eventUiBases: props.eventUiBases,
dateSelection: props.dateSelection,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize,
isHeightAuto: props.isHeightAuto,
forPrint: props.forPrint,
}
let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers)
for (let transformer of transformers) {
Object.assign(
viewProps,
transformer.transform(viewProps, props),
)
}
let ViewComponent = viewSpec.component
return (
<ViewComponent {...viewProps} />
)
}
// Component Registration
// -----------------------------------------------------------------------------------------------------------------
registerInteractiveComponent = (component: DateComponent<any>, settingsInput: InteractionSettingsInput) => {
let settings = parseInteractionSettings(component, settingsInput)
let DEFAULT_INTERACTIONS: InteractionClass[] = [
EventClicking,
EventHovering,
]
let interactionClasses: InteractionClass[] = DEFAULT_INTERACTIONS.concat(
this.props.pluginHooks.componentInteractions,
)
let interactions = interactionClasses.map((TheInteractionClass) => new TheInteractionClass(settings))
this.interactionsStore[component.uid] = interactions
interactionSettingsStore[component.uid] = settings
}
unregisterInteractiveComponent = (component: DateComponent<any>) => {
let listeners = this.interactionsStore[component.uid]
if (listeners) {
for (let listener of listeners) {
listener.destroy()
}
delete this.interactionsStore[component.uid]
}
delete interactionSettingsStore[component.uid]
}
// Resizing
// -----------------------------------------------------------------------------------------------------------------
resizeRunner = new DelayedRunner(() => {
this.props.emitter.trigger('_resize', true) // should window resizes be considered "forced" ?
this.props.emitter.trigger('windowResize', { view: this.props.viewApi })
})
handleWindowResize = (ev: UIEvent) => {
let { options } = this.props
if (
options.handleWindowResize &&
ev.target === window // avoid jqui events
) {
this.resizeRunner.request(options.windowResizeDelay)
}
}
}
function buildToolbarProps(
viewSpec: ViewSpec,
dateProfile: DateProfile,
dateProfileGenerator: DateProfileGenerator,
currentDate: DateMarker,
now: DateMarker,
title: string,
) {
// don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid
let todayInfo = dateProfileGenerator.build(now, undefined, false) // TODO: need `undefined` or else INFINITE LOOP for some reason
let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false)
let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false)
return {
title,
activeButton: viewSpec.type,
navUnit: viewSpec.singleUnit,
isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now),
isPrevEnabled: prevInfo.isValid,
isNextEnabled: nextInfo.isValid,
}
}
// Plugin
// -----------------------------------------------------------------------------------------------------------------
function buildViewPropTransformers(theClasses: ViewPropsTransformerClass[]) {
return theClasses.map((TheClass) => new TheClass())
}

View file

@ -0,0 +1,17 @@
import { DateEnv } from './datelib/env.js'
import { BaseOptionsRefined, CalendarListeners } from './options.js'
import { PluginHooks } from './plugin-system-struct.js'
import { Emitter } from './common/Emitter.js'
import { Action } from './reducers/Action.js'
import { CalendarImpl } from './api/CalendarImpl.js'
import { CalendarData } from './reducers/data-types.js'
export interface CalendarContext {
dateEnv: DateEnv
options: BaseOptionsRefined // does not have calendar-specific properties. aims to be compatible with ViewOptionsRefined
pluginHooks: PluginHooks
emitter: Emitter<CalendarListeners>
dispatch(action: Action): void
getCurrentData(): CalendarData
calendarApi: CalendarImpl
}

View file

@ -0,0 +1,70 @@
import { ComponentChildren, flushSync } from './preact.js'
import { BaseComponent } from './vdom-util.js'
import { CssDimValue } from './scrollgrid/util.js'
import { CalendarOptions, CalendarListeners } from './options.js'
import { Theme } from './theme/Theme.js'
import { getCanVGrowWithinCell } from './util/table-styling.js'
import { Emitter } from './common/Emitter.js'
export interface CalendarRootProps {
options: CalendarOptions
theme: Theme
emitter: Emitter<CalendarListeners>
children: (classNames: string[], height: CssDimValue, isHeightAuto: boolean, forPrint: boolean) => ComponentChildren
}
interface CalendarRootState {
forPrint: boolean
}
export class CalendarRoot extends BaseComponent<CalendarRootProps, CalendarRootState> {
state = {
forPrint: false,
}
render() {
let { props } = this
let { options } = props
let { forPrint } = this.state
let isHeightAuto = forPrint || options.height === 'auto' || options.contentHeight === 'auto'
let height = (!isHeightAuto && options.height != null) ? options.height : ''
let classNames: string[] = [
'fc',
forPrint ? 'fc-media-print' : 'fc-media-screen',
`fc-direction-${options.direction}`,
props.theme.getClass('root'),
]
if (!getCanVGrowWithinCell()) {
classNames.push('fc-liquid-hack')
}
return props.children(classNames, height, isHeightAuto, forPrint)
}
componentDidMount() {
let { emitter } = this.props
emitter.on('_beforeprint', this.handleBeforePrint)
emitter.on('_afterprint', this.handleAfterPrint)
}
componentWillUnmount() {
let { emitter } = this.props
emitter.off('_beforeprint', this.handleBeforePrint)
emitter.off('_afterprint', this.handleAfterPrint)
}
handleBeforePrint = () => {
flushSync(() => {
this.setState({ forPrint: true })
})
}
handleAfterPrint = () => {
flushSync(() => {
this.setState({ forPrint: false })
})
}
}

View file

@ -0,0 +1,455 @@
import { DateMarker, startOfDay, addDays } from './datelib/marker.js'
import { Duration, createDuration, asRoughDays, asRoughMs, greatestDurationDenominator } from './datelib/duration.js'
import {
DateRange,
OpenDateRange,
constrainMarkerToRange,
intersectRanges,
rangeContainsMarker,
rangesIntersect,
parseRange,
DateRangeInput,
} from './datelib/date-range.js'
import { DateEnv, DateInput } from './datelib/env.js'
import { computeVisibleDayRange } from './util/date.js'
import { getNow } from './reducers/current-date.js'
import { CalendarImpl } from './api/CalendarImpl.js'
export interface DateProfile {
currentDate: DateMarker
isValid: boolean
validRange: OpenDateRange // dates in past/present/future the user can interact with
renderRange: DateRange // dates that get rendered (even if they're completely blank)
activeRange: DateRange | null // dates where content is rendered
currentRange: DateRange // dates for current interval (TODO: include slotMinTime/slotMaxTime?)
currentRangeUnit: string
isRangeAllDay: boolean
dateIncrement: Duration
slotMinTime: Duration
slotMaxTime: Duration
}
export interface DateProfileGeneratorProps extends DateProfileOptions {
dateProfileGeneratorClass: DateProfileGeneratorClass // not used by DateProfileGenerator itself
duration: Duration
durationUnit: string
usesMinMaxTime: boolean
dateEnv: DateEnv
calendarApi: CalendarImpl
}
export interface DateProfileOptions {
slotMinTime: Duration
slotMaxTime: Duration
showNonCurrentDates?: boolean
dayCount?: number
dateAlignment?: string
dateIncrement?: Duration
hiddenDays?: number[]
weekends?: boolean
nowInput?: DateInput | (() => DateInput)
validRangeInput?: DateRangeInput | ((this: CalendarImpl, nowDate: Date) => DateRangeInput)
visibleRangeInput?: DateRangeInput | ((this: CalendarImpl, nowDate: Date) => DateRangeInput)
fixedWeekCount?: boolean
}
export type DateProfileGeneratorClass = {
new(props: DateProfileGeneratorProps): DateProfileGenerator
}
export class DateProfileGenerator { // only publicly used for isHiddenDay :(
nowDate: DateMarker
isHiddenDayHash: boolean[]
constructor(protected props: DateProfileGeneratorProps) {
this.nowDate = getNow(props.nowInput, props.dateEnv)
this.initHiddenDays()
}
/* Date Range Computation
------------------------------------------------------------------------------------------------------------------*/
// Builds a structure with info about what the dates/ranges will be for the "prev" view.
buildPrev(currentDateProfile: DateProfile, currentDate: DateMarker, forceToValid?: boolean): DateProfile {
let { dateEnv } = this.props
let prevDate = dateEnv.subtract(
dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
currentDateProfile.dateIncrement,
)
return this.build(prevDate, -1, forceToValid)
}
// Builds a structure with info about what the dates/ranges will be for the "next" view.
buildNext(currentDateProfile: DateProfile, currentDate: DateMarker, forceToValid?: boolean): DateProfile {
let { dateEnv } = this.props
let nextDate = dateEnv.add(
dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
currentDateProfile.dateIncrement,
)
return this.build(nextDate, 1, forceToValid)
}
// Builds a structure holding dates/ranges for rendering around the given date.
// Optional direction param indicates whether the date is being incremented/decremented
// from its previous value. decremented = -1, incremented = 1 (default).
build(currentDate: DateMarker, direction?, forceToValid = true): DateProfile {
let { props } = this
let validRange: DateRange
let currentInfo
let isRangeAllDay
let renderRange: DateRange
let activeRange: DateRange
let isValid
validRange = this.buildValidRange()
validRange = this.trimHiddenDays(validRange)
if (forceToValid) {
currentDate = constrainMarkerToRange(currentDate, validRange)
}
currentInfo = this.buildCurrentRangeInfo(currentDate, direction)
isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit)
renderRange = this.buildRenderRange(
this.trimHiddenDays(currentInfo.range),
currentInfo.unit,
isRangeAllDay,
)
renderRange = this.trimHiddenDays(renderRange)
activeRange = renderRange
if (!props.showNonCurrentDates) {
activeRange = intersectRanges(activeRange, currentInfo.range)
}
activeRange = this.adjustActiveRange(activeRange)
activeRange = intersectRanges(activeRange, validRange) // might return null
// it's invalid if the originally requested date is not contained,
// or if the range is completely outside of the valid range.
isValid = rangesIntersect(currentInfo.range, validRange)
// HACK: constrain to render-range so `currentDate` is more useful to view rendering
if (!rangeContainsMarker(renderRange, currentDate)) {
currentDate = renderRange.start
}
return {
currentDate,
// constraint for where prev/next operations can go and where events can be dragged/resized to.
// an object with optional start and end properties.
validRange,
// range the view is formally responsible for.
// for example, a month view might have 1st-31st, excluding padded dates
currentRange: currentInfo.range,
// name of largest unit being displayed, like "month" or "week"
currentRangeUnit: currentInfo.unit,
isRangeAllDay,
// dates that display events and accept drag-n-drop
// will be `null` if no dates accept events
activeRange,
// date range with a rendered skeleton
// includes not-active days that need some sort of DOM
renderRange,
// Duration object that denotes the first visible time of any given day
slotMinTime: props.slotMinTime,
// Duration object that denotes the exclusive visible end time of any given day
slotMaxTime: props.slotMaxTime,
isValid,
// how far the current date will move for a prev/next operation
dateIncrement: this.buildDateIncrement(currentInfo.duration),
// pass a fallback (might be null) ^
}
}
// Builds an object with optional start/end properties.
// Indicates the minimum/maximum dates to display.
// not responsible for trimming hidden days.
buildValidRange(): OpenDateRange {
let input = this.props.validRangeInput
let simpleInput = typeof input === 'function'
? input.call(this.props.calendarApi, this.nowDate)
: input
return this.refineRange(simpleInput) ||
{ start: null, end: null } // completely open-ended
}
// Builds a structure with info about the "current" range, the range that is
// highlighted as being the current month for example.
// See build() for a description of `direction`.
// Guaranteed to have `range` and `unit` properties. `duration` is optional.
buildCurrentRangeInfo(date: DateMarker, direction) {
let { props } = this
let duration = null
let unit = null
let range = null
let dayCount
if (props.duration) {
duration = props.duration
unit = props.durationUnit
range = this.buildRangeFromDuration(date, direction, duration, unit)
} else if ((dayCount = this.props.dayCount)) {
unit = 'day'
range = this.buildRangeFromDayCount(date, direction, dayCount)
} else if ((range = this.buildCustomVisibleRange(date))) {
unit = props.dateEnv.greatestWholeUnit(range.start, range.end).unit
} else {
duration = this.getFallbackDuration()
unit = greatestDurationDenominator(duration).unit
range = this.buildRangeFromDuration(date, direction, duration, unit)
}
return { duration, unit, range }
}
getFallbackDuration(): Duration {
return createDuration({ day: 1 })
}
// Returns a new activeRange to have time values (un-ambiguate)
// slotMinTime or slotMaxTime causes the range to expand.
adjustActiveRange(range: DateRange) {
let { dateEnv, usesMinMaxTime, slotMinTime, slotMaxTime } = this.props
let { start, end } = range
if (usesMinMaxTime) {
// expand active range if slotMinTime is negative (why not when positive?)
if (asRoughDays(slotMinTime) < 0) {
start = startOfDay(start) // necessary?
start = dateEnv.add(start, slotMinTime)
}
// expand active range if slotMaxTime is beyond one day (why not when negative?)
if (asRoughDays(slotMaxTime) > 1) {
end = startOfDay(end) // necessary?
end = addDays(end, -1)
end = dateEnv.add(end, slotMaxTime)
}
}
return { start, end }
}
// Builds the "current" range when it is specified as an explicit duration.
// `unit` is the already-computed greatestDurationDenominator unit of duration.
buildRangeFromDuration(date: DateMarker, direction, duration: Duration, unit) {
let { dateEnv, dateAlignment } = this.props
let start: DateMarker
let end: DateMarker
let res
// compute what the alignment should be
if (!dateAlignment) {
let { dateIncrement } = this.props
if (dateIncrement) {
// use the smaller of the two units
if (asRoughMs(dateIncrement) < asRoughMs(duration)) {
dateAlignment = greatestDurationDenominator(dateIncrement).unit
} else {
dateAlignment = unit
}
} else {
dateAlignment = unit
}
}
// if the view displays a single day or smaller
if (asRoughDays(duration) <= 1) {
if (this.isHiddenDay(start)) {
start = this.skipHiddenDays(start, direction)
start = startOfDay(start)
}
}
function computeRes() {
start = dateEnv.startOf(date, dateAlignment)
end = dateEnv.add(start, duration)
res = { start, end }
}
computeRes()
// if range is completely enveloped by hidden days, go past the hidden days
if (!this.trimHiddenDays(res)) {
date = this.skipHiddenDays(date, direction)
computeRes()
}
return res
}
// Builds the "current" range when a dayCount is specified.
buildRangeFromDayCount(date: DateMarker, direction, dayCount) {
let { dateEnv, dateAlignment } = this.props
let runningCount = 0
let start: DateMarker = date
let end: DateMarker
if (dateAlignment) {
start = dateEnv.startOf(start, dateAlignment)
}
start = startOfDay(start)
start = this.skipHiddenDays(start, direction)
end = start
do {
end = addDays(end, 1)
if (!this.isHiddenDay(end)) {
runningCount += 1
}
} while (runningCount < dayCount)
return { start, end }
}
// Builds a normalized range object for the "visible" range,
// which is a way to define the currentRange and activeRange at the same time.
buildCustomVisibleRange(date: DateMarker) {
let { props } = this
let input = props.visibleRangeInput
let simpleInput = typeof input === 'function'
? input.call(props.calendarApi, props.dateEnv.toDate(date))
: input
let range = this.refineRange(simpleInput)
if (range && (range.start == null || range.end == null)) {
return null
}
return range
}
// Computes the range that will represent the element/cells for *rendering*,
// but which may have voided days/times.
// not responsible for trimming hidden days.
buildRenderRange(currentRange: DateRange, currentRangeUnit, isRangeAllDay) {
return currentRange
}
// Compute the duration value that should be added/substracted to the current date
// when a prev/next operation happens.
buildDateIncrement(fallback): Duration {
let { dateIncrement } = this.props
let customAlignment
if (dateIncrement) {
return dateIncrement
}
if ((customAlignment = this.props.dateAlignment)) {
return createDuration(1, customAlignment)
}
if (fallback) {
return fallback
}
return createDuration({ days: 1 })
}
refineRange(rangeInput: DateRangeInput | undefined): DateRange | null {
if (rangeInput) {
let range = parseRange(rangeInput, this.props.dateEnv)
if (range) {
range = computeVisibleDayRange(range)
}
return range
}
return null
}
/* Hidden Days
------------------------------------------------------------------------------------------------------------------*/
// Initializes internal variables related to calculating hidden days-of-week
initHiddenDays() {
let hiddenDays = this.props.hiddenDays || [] // array of day-of-week indices that are hidden
let isHiddenDayHash = [] // is the day-of-week hidden? (hash with day-of-week-index -> bool)
let dayCnt = 0
let i
if (this.props.weekends === false) {
hiddenDays.push(0, 6) // 0=sunday, 6=saturday
}
for (i = 0; i < 7; i += 1) {
if (
!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)
) {
dayCnt += 1
}
}
if (!dayCnt) {
throw new Error('invalid hiddenDays') // all days were hidden? bad.
}
this.isHiddenDayHash = isHiddenDayHash
}
// Remove days from the beginning and end of the range that are computed as hidden.
// If the whole range is trimmed off, returns null
trimHiddenDays(range: DateRange): DateRange | null {
let { start, end } = range
if (start) {
start = this.skipHiddenDays(start)
}
if (end) {
end = this.skipHiddenDays(end, -1, true)
}
if (start == null || end == null || start < end) {
return { start, end }
}
return null
}
// Is the current day hidden?
// `day` is a day-of-week index (0-6), or a Date (used for UTC)
isHiddenDay(day) {
if (day instanceof Date) {
day = day.getUTCDay()
}
return this.isHiddenDayHash[day]
}
// Incrementing the current day until it is no longer a hidden day, returning a copy.
// DOES NOT CONSIDER validRange!
// If the initial value of `date` is not a hidden day, don't do anything.
// Pass `isExclusive` as `true` if you are dealing with an end date.
// `inc` defaults to `1` (increment one day forward each time)
skipHiddenDays(date: DateMarker, inc = 1, isExclusive = false) {
while (
this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]
) {
date = addDays(date, inc)
}
return date
}
}

View file

@ -0,0 +1,94 @@
import { DateMarker, addMs, startOfDay, addDays } from './datelib/marker.js'
import { createDuration } from './datelib/duration.js'
import { ViewContext, ViewContextType } from './ViewContext.js'
import { ComponentChildren, Component } from './preact.js'
import { DateRange } from './datelib/date-range.js'
import { getNow } from './reducers/current-date.js'
export interface NowTimerProps {
unit: string // TODO: add type of unit
children: (now: DateMarker, todayRange: DateRange) => ComponentChildren
}
interface NowTimerState {
nowDate: DateMarker
todayRange: DateRange
}
export class NowTimer extends Component<NowTimerProps, NowTimerState> {
static contextType: any = ViewContextType
context: ViewContext // do this for all components that use the context!!!
initialNowDate: DateMarker
initialNowQueriedMs: number
timeoutId: any
constructor(props: NowTimerProps, context: ViewContext) {
super(props, context)
this.initialNowDate = getNow(context.options.now, context.dateEnv)
this.initialNowQueriedMs = new Date().valueOf()
this.state = this.computeTiming().currentState
}
render() {
let { props, state } = this
return props.children(state.nowDate, state.todayRange)
}
componentDidMount() {
this.setTimeout()
}
componentDidUpdate(prevProps: NowTimerProps) {
if (prevProps.unit !== this.props.unit) {
this.clearTimeout()
this.setTimeout()
}
}
componentWillUnmount() {
this.clearTimeout()
}
private computeTiming() {
let { props, context } = this
let unroundedNow = addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs)
let currentUnitStart = context.dateEnv.startOf(unroundedNow, props.unit)
let nextUnitStart = context.dateEnv.add(currentUnitStart, createDuration(1, props.unit))
let waitMs = nextUnitStart.valueOf() - unroundedNow.valueOf()
// there is a max setTimeout ms value (https://stackoverflow.com/a/3468650/96342)
// ensure no longer than a day
waitMs = Math.min(1000 * 60 * 60 * 24, waitMs)
return {
currentState: { nowDate: currentUnitStart, todayRange: buildDayRange(currentUnitStart) } as NowTimerState,
nextState: { nowDate: nextUnitStart, todayRange: buildDayRange(nextUnitStart) } as NowTimerState,
waitMs,
}
}
private setTimeout() {
let { nextState, waitMs } = this.computeTiming()
this.timeoutId = setTimeout(() => {
this.setState(nextState, () => {
this.setTimeout()
})
}, waitMs)
}
private clearTimeout() {
if (this.timeoutId) {
clearTimeout(this.timeoutId)
}
}
}
function buildDayRange(date: DateMarker): DateRange { // TODO: make this a general util
let start = startOfDay(date)
let end = addDays(start, 1)
return { start, end }
}

View file

@ -0,0 +1,53 @@
import { Duration } from './datelib/duration.js'
import { Emitter } from './common/Emitter.js'
import { CalendarListeners } from './options.js'
export interface ScrollRequest {
time?: Duration
[otherProp: string]: any
}
export type ScrollRequestHandler = (request: ScrollRequest) => boolean
export class ScrollResponder {
queuedRequest: ScrollRequest
constructor(
private execFunc: ScrollRequestHandler,
private emitter: Emitter<CalendarListeners>,
private scrollTime: Duration,
private scrollTimeReset: boolean,
) {
emitter.on('_scrollRequest', this.handleScrollRequest)
this.fireInitialScroll()
}
detach() {
this.emitter.off('_scrollRequest', this.handleScrollRequest)
}
update(isDatesNew: boolean) {
if (isDatesNew && this.scrollTimeReset) {
this.fireInitialScroll() // will drain
} else {
this.drain()
}
}
private fireInitialScroll() {
this.handleScrollRequest({
time: this.scrollTime,
})
}
private handleScrollRequest = (request: ScrollRequest) => {
this.queuedRequest = Object.assign({}, this.queuedRequest || {}, request)
this.drain()
}
private drain() {
if (this.queuedRequest && this.execFunc(this.queuedRequest)) {
this.queuedRequest = null
}
}
}

View file

@ -0,0 +1,67 @@
import { createElement } from './preact.js'
import { BaseComponent } from './vdom-util.js'
import { ToolbarModel, ToolbarWidget } from './toolbar-struct.js'
import { ToolbarSection, ToolbarContent } from './ToolbarSection.js'
export interface ToolbarProps extends ToolbarContent {
extraClassName: string // wish this could be array, but easier for pureness
model: ToolbarModel
titleId: string
}
export class Toolbar extends BaseComponent<ToolbarProps> {
render() {
let { model, extraClassName } = this.props
let forceLtr = false
let startContent
let endContent
let sectionWidgets = model.sectionWidgets
let centerContent = sectionWidgets.center
if (sectionWidgets.left) {
forceLtr = true
startContent = sectionWidgets.left
} else {
startContent = sectionWidgets.start
}
if (sectionWidgets.right) {
forceLtr = true
endContent = sectionWidgets.right
} else {
endContent = sectionWidgets.end
}
let classNames = [
extraClassName || '',
'fc-toolbar',
forceLtr ? 'fc-toolbar-ltr' : '',
]
return (
<div className={classNames.join(' ')}>
{this.renderSection('start', startContent || [])}
{this.renderSection('center', centerContent || [])}
{this.renderSection('end', endContent || [])}
</div>
)
}
renderSection(key: string, widgetGroups: ToolbarWidget[][]) {
let { props } = this
return (
<ToolbarSection
key={key}
widgetGroups={widgetGroups}
title={props.title}
navUnit={props.navUnit}
activeButton={props.activeButton}
isTodayEnabled={props.isTodayEnabled}
isPrevEnabled={props.isPrevEnabled}
isNextEnabled={props.isNextEnabled}
titleId={props.titleId}
/>
)
}
}

View file

@ -0,0 +1,74 @@
import { createElement, VNode } from './preact.js'
import { BaseComponent } from './vdom-util.js'
import { ToolbarWidget } from './toolbar-struct.js'
export interface ToolbarContent {
title: string
titleId: string
navUnit: string
activeButton: string
isTodayEnabled: boolean
isPrevEnabled: boolean
isNextEnabled: boolean
}
export interface ToolbarSectionProps extends ToolbarContent {
widgetGroups: ToolbarWidget[][]
}
export class ToolbarSection extends BaseComponent<ToolbarSectionProps> {
render(): any {
let children = this.props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup))
return createElement('div', { className: 'fc-toolbar-chunk' }, ...children)
}
renderWidgetGroup(widgetGroup: ToolbarWidget[]): any {
let { props } = this
let { theme } = this.context
let children: VNode[] = []
let isOnlyButtons = true
for (let widget of widgetGroup) {
let { buttonName, buttonClick, buttonText, buttonIcon, buttonHint } = widget
if (buttonName === 'title') {
isOnlyButtons = false
children.push(
<h2 className="fc-toolbar-title" id={props.titleId}>{props.title}</h2>,
)
} else {
let isPressed = buttonName === props.activeButton
let isDisabled =
(!props.isTodayEnabled && buttonName === 'today') ||
(!props.isPrevEnabled && buttonName === 'prev') ||
(!props.isNextEnabled && buttonName === 'next')
let buttonClasses = [`fc-${buttonName}-button`, theme.getClass('button')]
if (isPressed) {
buttonClasses.push(theme.getClass('buttonActive'))
}
children.push(
<button
type="button"
title={typeof buttonHint === 'function' ? buttonHint(props.navUnit) : buttonHint}
disabled={isDisabled}
aria-pressed={isPressed}
className={buttonClasses.join(' ')}
onClick={buttonClick}
>
{buttonText || (buttonIcon ? <span className={buttonIcon} role="img" /> : '')}
</button>,
)
}
}
if (children.length > 1) {
let groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || ''
return createElement('div', { className: groupClassName }, ...children)
}
return children[0]
}
}

View file

@ -0,0 +1,38 @@
import { DateProfile } from './DateProfileGenerator.js'
import { EventStore } from './structs/event-store.js'
import { EventUiHash } from './component/event-ui.js'
import { sliceEventStore, EventRenderRange } from './component/event-rendering.js'
import { DateSpan } from './structs/date-span.js'
import { EventInteractionState } from './interactions/event-interaction-state.js'
import { Duration } from './datelib/duration.js'
export interface ViewProps {
dateProfile: DateProfile
businessHours: EventStore
eventStore: EventStore
eventUiBases: EventUiHash
dateSelection: DateSpan | null
eventSelection: string
eventDrag: EventInteractionState | null
eventResize: EventInteractionState | null
isHeightAuto: boolean
forPrint: boolean
}
// HELPERS
/*
if nextDayThreshold is specified, slicing is done in an all-day fashion.
you can get nextDayThreshold from context.nextDayThreshold
*/
export function sliceEvents(
props: ViewProps & { dateProfile: DateProfile, nextDayThreshold: Duration },
allDay?: boolean,
): EventRenderRange[] {
return sliceEventStore(
props.eventStore,
props.eventUiBases,
props.dateProfile.activeRange,
allDay ? props.nextDayThreshold : null,
).fg
}

View file

@ -0,0 +1,85 @@
import { CalendarImpl } from './api/CalendarImpl.js'
import { ViewImpl } from './api/ViewImpl.js'
import { Theme } from './theme/Theme.js'
import { DateEnv } from './datelib/env.js'
import { PluginHooks } from './plugin-system-struct.js'
import { createContext, Context } from './preact.js'
import { ScrollResponder, ScrollRequestHandler } from './ScrollResponder.js'
import { DateProfileGenerator } from './DateProfileGenerator.js'
import { ViewSpec } from './structs/view-spec.js'
import { CalendarData } from './reducers/data-types.js'
import { Action } from './reducers/Action.js'
import { Emitter } from './common/Emitter.js'
import { InteractionSettingsInput } from './interactions/interaction.js'
import { DateComponent } from './component/DateComponent.js'
import { CalendarContext } from './CalendarContext.js'
import { createDuration } from './datelib/duration.js'
import { ViewOptionsRefined, CalendarListeners } from './options.js'
export const ViewContextType: Context<any> = createContext<ViewContext>({} as any) // for Components
export type ResizeHandler = (force: boolean) => void
/*
it's important that ViewContext extends CalendarContext so that components that subscribe to ViewContext
can pass in their ViewContext to util functions that accept CalendarContext.
*/
export interface ViewContext extends CalendarContext {
options: ViewOptionsRefined // more specific than BaseOptionsRefined
theme: Theme
isRtl: boolean
dateProfileGenerator: DateProfileGenerator
viewSpec: ViewSpec
viewApi: ViewImpl
addResizeHandler: (handler: ResizeHandler) => void
removeResizeHandler: (handler: ResizeHandler) => void
createScrollResponder: (execFunc: ScrollRequestHandler) => ScrollResponder
registerInteractiveComponent: (component: DateComponent<any>, settingsInput: InteractionSettingsInput) => void
unregisterInteractiveComponent: (component: DateComponent<any>) => void
}
export function buildViewContext(
viewSpec: ViewSpec,
viewApi: ViewImpl,
viewOptions: ViewOptionsRefined,
dateProfileGenerator: DateProfileGenerator,
dateEnv: DateEnv,
theme: Theme,
pluginHooks: PluginHooks,
dispatch: (action: Action) => void,
getCurrentData: () => CalendarData,
emitter: Emitter<CalendarListeners>,
calendarApi: CalendarImpl,
registerInteractiveComponent: (component: DateComponent<any>, settingsInput: InteractionSettingsInput) => void,
unregisterInteractiveComponent: (component: DateComponent<any>) => void,
): ViewContext {
return {
dateEnv,
options: viewOptions,
pluginHooks,
emitter,
dispatch,
getCurrentData,
calendarApi,
viewSpec,
viewApi,
dateProfileGenerator,
theme,
isRtl: viewOptions.direction === 'rtl',
addResizeHandler(handler: ResizeHandler) {
emitter.on('_resize', handler)
},
removeResizeHandler(handler: ResizeHandler) {
emitter.off('_resize', handler)
},
createScrollResponder(execFunc: ScrollRequestHandler) {
return new ScrollResponder(
execFunc,
emitter,
createDuration(viewOptions.scrollTime),
viewOptions.scrollTimeReset,
)
},
registerInteractiveComponent,
unregisterInteractiveComponent,
}
}

View file

@ -0,0 +1,90 @@
import { BaseComponent, setRef } from './vdom-util.js'
import { ComponentChildren, Ref, createElement } from './preact.js'
import { CssDimValue } from './scrollgrid/util.js'
export interface ViewHarnessProps {
elRef?: Ref<HTMLDivElement>
labeledById?: string
liquid?: boolean
height?: CssDimValue
aspectRatio?: number
children?: ComponentChildren
}
interface ViewHarnessState {
availableWidth: number | null
}
export class ViewHarness extends BaseComponent<ViewHarnessProps, ViewHarnessState> {
el: HTMLElement
state: ViewHarnessState = {
availableWidth: null,
}
render() {
let { props, state } = this
let { aspectRatio } = props
let classNames = [
'fc-view-harness',
(aspectRatio || props.liquid || props.height)
? 'fc-view-harness-active' // harness controls the height
: 'fc-view-harness-passive', // let the view do the height
]
let height: CssDimValue = ''
let paddingBottom: CssDimValue = ''
if (aspectRatio) {
if (state.availableWidth !== null) {
height = state.availableWidth / aspectRatio
} else {
// while waiting to know availableWidth, we can't set height to *zero*
// because will cause lots of unnecessary scrollbars within scrollgrid.
// BETTER: don't start rendering ANYTHING yet until we know container width
// NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606)
paddingBottom = `${(1 / aspectRatio) * 100}%`
}
} else {
height = props.height || ''
}
return (
<div
aria-labelledby={props.labeledById}
ref={this.handleEl}
className={classNames.join(' ')}
style={{ height, paddingBottom }}
>
{props.children}
</div>
)
}
componentDidMount() {
this.context.addResizeHandler(this.handleResize)
}
componentWillUnmount() {
this.context.removeResizeHandler(this.handleResize)
}
handleEl = (el: HTMLElement | null) => {
this.el = el
setRef(this.props.elRef, el)
this.updateAvailableWidth()
}
handleResize = () => {
this.updateAvailableWidth()
}
updateAvailableWidth() {
if (
this.el && // needed. but why?
this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth
) {
this.setState({ availableWidth: this.el.offsetWidth })
}
}
}

View file

@ -0,0 +1,85 @@
import { ViewApi } from './ViewApi.js'
import { EventSourceApi } from './EventSourceApi.js'
import { EventApi } from './EventApi.js'
import {
CalendarOptions,
CalendarListeners,
DateInput,
DurationInput,
DateRangeInput,
EventSourceInput,
EventInput,
FormatterInput,
} from './structs.js'
export interface CalendarApi {
view: ViewApi
updateSize(): void
// Options
// -----------------------------------------------------------------------------------------------------------------
setOption<OptionName extends keyof CalendarOptions>(name: OptionName, val: CalendarOptions[OptionName]): void
getOption<OptionName extends keyof CalendarOptions>(name: OptionName): CalendarOptions[OptionName]
getAvailableLocaleCodes(): string[]
// Trigger
// -----------------------------------------------------------------------------------------------------------------
on<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]): void
off<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]): void
trigger<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, ...args: Parameters<CalendarListeners[ListenerName]>): void
// View
// -----------------------------------------------------------------------------------------------------------------
changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput): void
zoomTo(dateMarker: Date, viewType?: string): void
// Current Date
// -----------------------------------------------------------------------------------------------------------------
prev(): void
next(): void
prevYear(): void
nextYear(): void
today(): void
gotoDate(zonedDateInput: DateInput): void
incrementDate(deltaInput: DurationInput): void
getDate(): Date
// Date Formatting Utils
// -----------------------------------------------------------------------------------------------------------------
formatDate(d: DateInput, formatter: FormatterInput): string
formatRange(d0: DateInput, d1: DateInput, settings: any): string // TODO: settings type
formatIso(d: DateInput, omitTime?: boolean): string
// Date Selection / Event Selection / DayClick
// -----------------------------------------------------------------------------------------------------------------
select(dateOrObj: DateInput | any, endDate?: DateInput): void
unselect(): void
// Public Events API
// -----------------------------------------------------------------------------------------------------------------
addEvent(eventInput: EventInput, sourceInput?: EventSourceApi | string | boolean): EventApi | null
getEventById(id: string): EventApi | null
getEvents(): EventApi[]
removeAllEvents(): void
// Public Event Sources API
// -----------------------------------------------------------------------------------------------------------------
getEventSources(): EventSourceApi[]
getEventSourceById(id: string): EventSourceApi | null
addEventSource(sourceInput: EventSourceInput): EventSourceApi
removeAllEventSources(): void
refetchEvents(): void
// Scroll
// -----------------------------------------------------------------------------------------------------------------
scrollToTime(timeInput: DurationInput): void
}

View file

@ -0,0 +1,511 @@
import { createFormatter, FormatterInput } from '../datelib/formatting.js'
import { createDuration } from '../datelib/duration.js'
import { parseDateSpan } from '../structs/date-span.js'
import { parseEventSource } from '../structs/event-source-parse.js'
import { parseEvent } from '../structs/event-parse.js'
import { eventTupleToStore } from '../structs/event-store.js'
import { ViewSpec } from '../structs/view-spec.js'
import { PointerDragEvent } from '../interactions/pointer.js'
import { getNow } from '../reducers/current-date.js'
import { triggerDateSelect, triggerDateUnselect } from '../calendar-utils.js'
import { hashValuesToArray } from '../util/object.js'
import { CalendarDataManager } from '../reducers/CalendarDataManager.js'
import { Action } from '../reducers/Action.js'
import { EventSource } from '../structs/event-source.js'
import { eventApiToStore, buildEventApis, EventImpl } from './EventImpl.js'
import { CalendarData } from '../reducers/data-types.js'
import { CalendarApi } from './CalendarApi.js'
import { ViewImpl } from './ViewImpl.js'
import { EventSourceImpl } from './EventSourceImpl.js'
import {
CalendarOptions,
CalendarListeners,
DateInput,
DurationInput,
DateSpanInput,
DateRangeInput,
EventSourceInput,
EventInput,
} from './structs.js'
export class CalendarImpl implements CalendarApi {
currentDataManager?: CalendarDataManager // will be set by CalendarDataManager
getCurrentData(): CalendarData {
return this.currentDataManager!.getCurrentData()
}
dispatch(action: Action): void {
this.currentDataManager!.dispatch(action)
}
get view(): ViewImpl { return this.getCurrentData().viewApi }
batchRendering(callback: () => void): void { // subclasses should implement
callback()
}
updateSize(): void {
this.trigger('_resize', true)
}
// Options
// -----------------------------------------------------------------------------------------------------------------
setOption<OptionName extends keyof CalendarOptions>(name: OptionName, val: CalendarOptions[OptionName]): void {
this.dispatch({
type: 'SET_OPTION',
optionName: name,
rawOptionValue: val,
})
}
getOption<OptionName extends keyof CalendarOptions>(name: OptionName): CalendarOptions[OptionName] {
return this.currentDataManager!.currentCalendarOptionsInput[name]
}
getAvailableLocaleCodes(): string[] {
return Object.keys(this.getCurrentData().availableRawLocales)
}
// Trigger
// -----------------------------------------------------------------------------------------------------------------
on<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]): void {
let { currentDataManager } = this
if (currentDataManager.currentCalendarOptionsRefiners[handlerName]) {
currentDataManager.emitter.on(handlerName, handler)
} else {
console.warn(`Unknown listener name '${handlerName}'`)
}
}
off<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]): void {
this.currentDataManager!.emitter.off(handlerName, handler)
}
// not meant for public use
trigger<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, ...args: Parameters<CalendarListeners[ListenerName]>): void {
this.currentDataManager!.emitter.trigger(handlerName, ...args)
}
// View
// -----------------------------------------------------------------------------------------------------------------
changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput): void {
this.batchRendering(() => {
this.unselect()
if (dateOrRange) {
if ((dateOrRange as DateRangeInput).start && (dateOrRange as DateRangeInput).end) { // a range
this.dispatch({
type: 'CHANGE_VIEW_TYPE',
viewType,
})
this.dispatch({ // not very efficient to do two dispatches
type: 'SET_OPTION',
optionName: 'visibleRange',
rawOptionValue: dateOrRange,
})
} else {
let { dateEnv } = this.getCurrentData()
this.dispatch({
type: 'CHANGE_VIEW_TYPE',
viewType,
dateMarker: dateEnv.createMarker(dateOrRange as DateInput),
})
}
} else {
this.dispatch({
type: 'CHANGE_VIEW_TYPE',
viewType,
})
}
})
}
// Forces navigation to a view for the given date.
// `viewType` can be a specific view name or a generic one like "week" or "day".
// needs to change
zoomTo(dateMarker: Date, viewType?: string): void {
let state = this.getCurrentData()
let spec
viewType = viewType || 'day' // day is default zoom
spec = state.viewSpecs[viewType] || this.getUnitViewSpec(viewType)
this.unselect()
if (spec) {
this.dispatch({
type: 'CHANGE_VIEW_TYPE',
viewType: spec.type,
dateMarker,
})
} else {
this.dispatch({
type: 'CHANGE_DATE',
dateMarker,
})
}
}
// Given a duration singular unit, like "week" or "day", finds a matching view spec.
// Preference is given to views that have corresponding buttons.
private getUnitViewSpec(unit: string): ViewSpec | null {
let { viewSpecs, toolbarConfig } = this.getCurrentData()
let viewTypes = [].concat(
toolbarConfig.header ? toolbarConfig.header.viewsWithButtons : [],
toolbarConfig.footer ? toolbarConfig.footer.viewsWithButtons : [],
)
let i
let spec
for (let viewType in viewSpecs) {
viewTypes.push(viewType)
}
for (i = 0; i < viewTypes.length; i += 1) {
spec = viewSpecs[viewTypes[i]]
if (spec) {
if (spec.singleUnit === unit) {
return spec
}
}
}
return null
}
// Current Date
// -----------------------------------------------------------------------------------------------------------------
prev(): void {
this.unselect()
this.dispatch({ type: 'PREV' })
}
next(): void {
this.unselect()
this.dispatch({ type: 'NEXT' })
}
prevYear(): void {
let state = this.getCurrentData()
this.unselect()
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.addYears(state.currentDate, -1),
})
}
nextYear(): void {
let state = this.getCurrentData()
this.unselect()
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.addYears(state.currentDate, 1),
})
}
today(): void {
let state = this.getCurrentData()
this.unselect()
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: getNow(state.calendarOptions.now, state.dateEnv),
})
}
gotoDate(zonedDateInput: DateInput): void {
let state = this.getCurrentData()
this.unselect()
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.createMarker(zonedDateInput),
})
}
incrementDate(deltaInput: DurationInput): void {
let state = this.getCurrentData()
let delta = createDuration(deltaInput)
if (delta) { // else, warn about invalid input?
this.unselect()
this.dispatch({
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.add(state.currentDate, delta),
})
}
}
getDate(): Date {
let state = this.getCurrentData()
return state.dateEnv.toDate(state.currentDate)
}
// Date Formatting Utils
// -----------------------------------------------------------------------------------------------------------------
formatDate(d: DateInput, formatter: FormatterInput): string {
let { dateEnv } = this.getCurrentData()
return dateEnv.format(
dateEnv.createMarker(d),
createFormatter(formatter),
)
}
// `settings` is for formatter AND isEndExclusive
formatRange(d0: DateInput, d1: DateInput, settings: any): string { // TODO: settings type
let { dateEnv } = this.getCurrentData()
return dateEnv.formatRange(
dateEnv.createMarker(d0),
dateEnv.createMarker(d1),
createFormatter(settings),
settings,
)
}
formatIso(d: DateInput, omitTime?: boolean): string {
let { dateEnv } = this.getCurrentData()
return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime })
}
// Date Selection / Event Selection / DayClick
// -----------------------------------------------------------------------------------------------------------------
select(dateOrObj: DateInput | any, endDate?: DateInput): void {
let selectionInput: DateSpanInput
if (endDate == null) {
if (dateOrObj.start != null) {
selectionInput = dateOrObj as DateSpanInput
} else {
selectionInput = {
start: dateOrObj,
end: null,
}
}
} else {
selectionInput = {
start: dateOrObj,
end: endDate,
} as DateSpanInput
}
let state = this.getCurrentData()
let selection = parseDateSpan(
selectionInput,
state.dateEnv,
createDuration({ days: 1 }), // TODO: cache this?
)
if (selection) { // throw parse error otherwise?
this.dispatch({ type: 'SELECT_DATES', selection })
triggerDateSelect(selection, null, state)
}
}
unselect(pev?: PointerDragEvent): void {
let state = this.getCurrentData()
if (state.dateSelection) {
this.dispatch({ type: 'UNSELECT_DATES' })
triggerDateUnselect(pev, state)
}
}
// Public Events API
// -----------------------------------------------------------------------------------------------------------------
addEvent(eventInput: EventInput, sourceInput?: EventSourceImpl | string | boolean): EventImpl | null {
if (eventInput instanceof EventImpl) {
let def = eventInput._def
let instance = eventInput._instance
let currentData = this.getCurrentData()
// not already present? don't want to add an old snapshot
if (!currentData.eventStore.defs[def.defId]) {
this.dispatch({
type: 'ADD_EVENTS',
eventStore: eventTupleToStore({ def, instance }), // TODO: better util for two args?
})
this.triggerEventAdd(eventInput)
}
return eventInput
}
let state = this.getCurrentData()
let eventSource: EventSource<any>
if (sourceInput instanceof EventSourceImpl) {
eventSource = sourceInput.internalEventSource
} else if (typeof sourceInput === 'boolean') {
if (sourceInput) { // true. part of the first event source
[eventSource] = hashValuesToArray(state.eventSources)
}
} else if (sourceInput != null) { // an ID. accepts a number too
let sourceApi = this.getEventSourceById(sourceInput) // TODO: use an internal function
if (!sourceApi) {
console.warn(`Could not find an event source with ID "${sourceInput}"`) // TODO: test
return null
}
eventSource = sourceApi.internalEventSource
}
let tuple = parseEvent(eventInput, eventSource, state, false)
if (tuple) {
let newEventApi = new EventImpl(
state,
tuple.def,
tuple.def.recurringDef ? null : tuple.instance,
)
this.dispatch({
type: 'ADD_EVENTS',
eventStore: eventTupleToStore(tuple),
})
this.triggerEventAdd(newEventApi)
return newEventApi
}
return null
}
private triggerEventAdd(eventApi: EventImpl): void {
let { emitter } = this.getCurrentData()
emitter.trigger('eventAdd', {
event: eventApi,
relatedEvents: [],
revert: () => {
this.dispatch({
type: 'REMOVE_EVENTS',
eventStore: eventApiToStore(eventApi),
})
},
})
}
// TODO: optimize
getEventById(id: string): EventImpl | null {
let state = this.getCurrentData()
let { defs, instances } = state.eventStore
id = String(id)
for (let defId in defs) {
let def = defs[defId]
if (def.publicId === id) {
if (def.recurringDef) {
return new EventImpl(state, def, null)
}
for (let instanceId in instances) {
let instance = instances[instanceId]
if (instance.defId === def.defId) {
return new EventImpl(state, def, instance)
}
}
}
}
return null
}
getEvents(): EventImpl[] {
let currentData = this.getCurrentData()
return buildEventApis(currentData.eventStore, currentData)
}
removeAllEvents(): void {
this.dispatch({ type: 'REMOVE_ALL_EVENTS' })
}
// Public Event Sources API
// -----------------------------------------------------------------------------------------------------------------
getEventSources(): EventSourceImpl[] {
let state = this.getCurrentData()
let sourceHash = state.eventSources
let sourceApis: EventSourceImpl[] = []
for (let internalId in sourceHash) {
sourceApis.push(new EventSourceImpl(state, sourceHash[internalId]))
}
return sourceApis
}
getEventSourceById(id: string): EventSourceImpl | null {
let state = this.getCurrentData()
let sourceHash = state.eventSources
id = String(id)
for (let sourceId in sourceHash) {
if (sourceHash[sourceId].publicId === id) {
return new EventSourceImpl(state, sourceHash[sourceId])
}
}
return null
}
addEventSource(sourceInput: EventSourceInput): EventSourceImpl {
let state = this.getCurrentData()
if (sourceInput instanceof EventSourceImpl) {
// not already present? don't want to add an old snapshot
if (!state.eventSources[sourceInput.internalEventSource.sourceId]) {
this.dispatch({
type: 'ADD_EVENT_SOURCES',
sources: [sourceInput.internalEventSource],
})
}
return sourceInput
}
let eventSource = parseEventSource(sourceInput, state)
if (eventSource) { // TODO: error otherwise?
this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] })
return new EventSourceImpl(state, eventSource)
}
return null
}
removeAllEventSources(): void {
this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' })
}
refetchEvents(): void {
this.dispatch({ type: 'FETCH_EVENT_SOURCES', isRefetch: true })
}
// Scroll
// -----------------------------------------------------------------------------------------------------------------
scrollToTime(timeInput: DurationInput): void {
let time = createDuration(timeInput)
if (time) {
this.trigger('_scrollRequest', { time })
}
}
}

View file

@ -0,0 +1,45 @@
import { Dictionary } from '../options.js'
import { EventSourceApi } from './EventSourceApi.js'
import {
DateInput,
DurationInput,
FormatterInput,
} from './structs.js'
export interface EventApi {
source: EventSourceApi | null
start: Date | null
end: Date | null
startStr: string
endStr: string
id: string
groupId: string
allDay: boolean
title: string
url: string
display: string // TODO: better
startEditable: boolean
durationEditable: boolean
constraint: any // TODO: better
overlap: boolean
allow: any // TODO: better
backgroundColor: string
borderColor: string
textColor: string
classNames: string[]
extendedProps: Dictionary
setProp(name: string, val: any): void
setExtendedProp(name: string, val: any): void
setStart(startInput: DateInput, options?: { granularity?: string, maintainDuration?: boolean }): void
setEnd(endInput: DateInput | null, options?: { granularity?: string }): void
setDates(startInput: DateInput, endInput: DateInput | null, options?: { allDay?: boolean, granularity?: string }): void
moveStart(deltaInput: DurationInput): void
moveEnd(deltaInput: DurationInput): void
moveDates(deltaInput: DurationInput): void
setAllDay(allDay: boolean, options?: { maintainDuration?: boolean }): void
formatRange(formatInput: FormatterInput)
remove(): void
toPlainObject(settings?: { collapseExtendedProps?: boolean, collapseColor?: boolean }): Dictionary
toJSON(): Dictionary
}

View file

@ -0,0 +1,451 @@
import { EventDef } from '../structs/event-def.js'
import { EVENT_NON_DATE_REFINERS, EVENT_DATE_REFINERS } from '../structs/event-parse.js'
import { EventInstance } from '../structs/event-instance.js'
import { EVENT_UI_REFINERS, EventUiHash } from '../component/event-ui.js'
import { EventMutation, applyMutationToEventStore } from '../structs/event-mutation.js'
import { diffDates, computeAlignedDayRange } from '../util/date.js'
import { createDuration, durationsEqual } from '../datelib/duration.js'
import { createFormatter } from '../datelib/formatting.js'
import { CalendarContext } from '../CalendarContext.js'
import { getRelevantEvents, EventStore } from '../structs/event-store.js'
import { Dictionary } from '../options.js'
import { EventApi } from './EventApi.js'
import { EventSourceImpl } from './EventSourceImpl.js'
import {
DateInput,
DurationInput,
FormatterInput,
} from './structs.js'
export class EventImpl implements EventApi {
_context: CalendarContext
_def: EventDef
_instance: EventInstance | null
// instance will be null if expressing a recurring event that has no current instances,
// OR if trying to validate an incoming external event that has no dates assigned
constructor(context: CalendarContext, def: EventDef, instance?: EventInstance) {
this._context = context
this._def = def
this._instance = instance || null
}
/*
TODO: make event struct more responsible for this
*/
setProp(name: string, val: any): void {
if (name in EVENT_DATE_REFINERS) {
console.warn('Could not set date-related prop \'name\'. Use one of the date-related methods instead.')
// TODO: make proper aliasing system?
} else if (name === 'id') {
val = EVENT_NON_DATE_REFINERS[name](val)
this.mutate({
standardProps: { publicId: val }, // hardcoded internal name
})
} else if (name in EVENT_NON_DATE_REFINERS) {
val = EVENT_NON_DATE_REFINERS[name](val)
this.mutate({
standardProps: { [name]: val },
})
} else if (name in EVENT_UI_REFINERS) {
let ui = EVENT_UI_REFINERS[name](val)
if (name === 'color') {
ui = { backgroundColor: val, borderColor: val }
} else if (name === 'editable') {
ui = { startEditable: val, durationEditable: val }
} else {
ui = { [name]: val }
}
this.mutate({
standardProps: { ui },
})
} else {
console.warn(`Could not set prop '${name}'. Use setExtendedProp instead.`)
}
}
setExtendedProp(name: string, val: any): void {
this.mutate({
extendedProps: { [name]: val },
})
}
setStart(startInput: DateInput, options: { granularity?: string, maintainDuration?: boolean } = {}): void {
let { dateEnv } = this._context
let start = dateEnv.createMarker(startInput)
if (start && this._instance) { // TODO: warning if parsed bad
let instanceRange = this._instance.range
let startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity) // what if parsed bad!?
if (options.maintainDuration) {
this.mutate({ datesDelta: startDelta })
} else {
this.mutate({ startDelta })
}
}
}
setEnd(endInput: DateInput | null, options: { granularity?: string } = {}): void {
let { dateEnv } = this._context
let end
if (endInput != null) {
end = dateEnv.createMarker(endInput)
if (!end) {
return // TODO: warning if parsed bad
}
}
if (this._instance) {
if (end) {
let endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity)
this.mutate({ endDelta })
} else {
this.mutate({ standardProps: { hasEnd: false } })
}
}
}
setDates(startInput: DateInput, endInput: DateInput | null, options: { allDay?: boolean, granularity?: string } = {}): void {
let { dateEnv } = this._context
let standardProps = { allDay: options.allDay } as any
let start = dateEnv.createMarker(startInput)
let end
if (!start) {
return // TODO: warning if parsed bad
}
if (endInput != null) {
end = dateEnv.createMarker(endInput)
if (!end) { // TODO: warning if parsed bad
return
}
}
if (this._instance) {
let instanceRange = this._instance.range
// when computing the diff for an event being converted to all-day,
// compute diff off of the all-day values the way event-mutation does.
if (options.allDay === true) {
instanceRange = computeAlignedDayRange(instanceRange)
}
let startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity)
if (end) {
let endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity)
if (durationsEqual(startDelta, endDelta)) {
this.mutate({ datesDelta: startDelta, standardProps })
} else {
this.mutate({ startDelta, endDelta, standardProps })
}
} else { // means "clear the end"
standardProps.hasEnd = false
this.mutate({ datesDelta: startDelta, standardProps })
}
}
}
moveStart(deltaInput: DurationInput): void {
let delta = createDuration(deltaInput)
if (delta) { // TODO: warning if parsed bad
this.mutate({ startDelta: delta })
}
}
moveEnd(deltaInput: DurationInput): void {
let delta = createDuration(deltaInput)
if (delta) { // TODO: warning if parsed bad
this.mutate({ endDelta: delta })
}
}
moveDates(deltaInput: DurationInput): void {
let delta = createDuration(deltaInput)
if (delta) { // TODO: warning if parsed bad
this.mutate({ datesDelta: delta })
}
}
setAllDay(allDay: boolean, options: { maintainDuration?: boolean } = {}): void {
let standardProps = { allDay } as any
let { maintainDuration } = options
if (maintainDuration == null) {
maintainDuration = this._context.options.allDayMaintainDuration
}
if (this._def.allDay !== allDay) {
standardProps.hasEnd = maintainDuration
}
this.mutate({ standardProps })
}
formatRange(formatInput: FormatterInput): string {
let { dateEnv } = this._context
let instance = this._instance
let formatter = createFormatter(formatInput)
if (this._def.hasEnd) {
return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, {
forcedStartTzo: instance.forcedStartTzo,
forcedEndTzo: instance.forcedEndTzo,
})
}
return dateEnv.format(instance.range.start, formatter, {
forcedTzo: instance.forcedStartTzo,
})
}
mutate(mutation: EventMutation): void { // meant to be private. but plugins need access
let instance = this._instance
if (instance) {
let def = this._def
let context = this._context
let { eventStore } = context.getCurrentData()
let relevantEvents = getRelevantEvents(eventStore, instance.instanceId)
let eventConfigBase = {
'': { // HACK. always allow API to mutate events
display: '',
startEditable: true,
durationEditable: true,
constraints: [],
overlap: null,
allows: [],
backgroundColor: '',
borderColor: '',
textColor: '',
classNames: [],
},
} as EventUiHash
relevantEvents = applyMutationToEventStore(relevantEvents, eventConfigBase, mutation, context)
let oldEvent = new EventImpl(context, def, instance) // snapshot
this._def = relevantEvents.defs[def.defId]
this._instance = relevantEvents.instances[instance.instanceId]
context.dispatch({
type: 'MERGE_EVENTS',
eventStore: relevantEvents,
})
context.emitter.trigger('eventChange', {
oldEvent,
event: this,
relatedEvents: buildEventApis(relevantEvents, context, instance),
revert() {
context.dispatch({
type: 'RESET_EVENTS',
eventStore, // the ORIGINAL store
})
},
})
}
}
remove(): void {
let context = this._context
let asStore = eventApiToStore(this)
context.dispatch({
type: 'REMOVE_EVENTS',
eventStore: asStore,
})
context.emitter.trigger('eventRemove', {
event: this,
relatedEvents: [],
revert() {
context.dispatch({
type: 'MERGE_EVENTS',
eventStore: asStore,
})
},
})
}
get source(): EventSourceImpl | null {
let { sourceId } = this._def
if (sourceId) {
return new EventSourceImpl(
this._context,
this._context.getCurrentData().eventSources[sourceId],
)
}
return null
}
get start(): Date | null {
return this._instance ?
this._context.dateEnv.toDate(this._instance.range.start) :
null
}
get end(): Date | null {
return (this._instance && this._def.hasEnd) ?
this._context.dateEnv.toDate(this._instance.range.end) :
null
}
get startStr(): string {
let instance = this._instance
if (instance) {
return this._context.dateEnv.formatIso(instance.range.start, {
omitTime: this._def.allDay,
forcedTzo: instance.forcedStartTzo,
})
}
return ''
}
get endStr(): string {
let instance = this._instance
if (instance && this._def.hasEnd) {
return this._context.dateEnv.formatIso(instance.range.end, {
omitTime: this._def.allDay,
forcedTzo: instance.forcedEndTzo,
})
}
return ''
}
// computable props that all access the def
// TODO: find a TypeScript-compatible way to do this at scale
get id() { return this._def.publicId }
get groupId() { return this._def.groupId }
get allDay() { return this._def.allDay }
get title() { return this._def.title }
get url() { return this._def.url }
get display() { return this._def.ui.display || 'auto' } // bad. just normalize the type earlier
get startEditable() { return this._def.ui.startEditable }
get durationEditable() { return this._def.ui.durationEditable }
get constraint() { return this._def.ui.constraints[0] || null }
get overlap() { return this._def.ui.overlap }
get allow() { return this._def.ui.allows[0] || null }
get backgroundColor() { return this._def.ui.backgroundColor }
get borderColor() { return this._def.ui.borderColor }
get textColor() { return this._def.ui.textColor }
// NOTE: user can't modify these because Object.freeze was called in event-def parsing
get classNames() { return this._def.ui.classNames }
get extendedProps() { return this._def.extendedProps }
toPlainObject(settings: { collapseExtendedProps?: boolean, collapseColor?: boolean } = {}): Dictionary {
let def = this._def
let { ui } = def
let { startStr, endStr } = this
let res: Dictionary = {
allDay: def.allDay,
}
if (def.title) {
res.title = def.title
}
if (startStr) {
res.start = startStr
}
if (endStr) {
res.end = endStr
}
if (def.publicId) {
res.id = def.publicId
}
if (def.groupId) {
res.groupId = def.groupId
}
if (def.url) {
res.url = def.url
}
if (ui.display && ui.display !== 'auto') {
res.display = ui.display
}
// TODO: what about recurring-event properties???
// TODO: include startEditable/durationEditable/constraint/overlap/allow
if (settings.collapseColor && ui.backgroundColor && ui.backgroundColor === ui.borderColor) {
res.color = ui.backgroundColor
} else {
if (ui.backgroundColor) {
res.backgroundColor = ui.backgroundColor
}
if (ui.borderColor) {
res.borderColor = ui.borderColor
}
}
if (ui.textColor) {
res.textColor = ui.textColor
}
if (ui.classNames.length) {
res.classNames = ui.classNames
}
if (Object.keys(def.extendedProps).length) {
if (settings.collapseExtendedProps) {
Object.assign(res, def.extendedProps)
} else {
res.extendedProps = def.extendedProps
}
}
return res
}
toJSON(): Dictionary {
return this.toPlainObject()
}
}
export function eventApiToStore(eventApi: EventImpl): EventStore {
let def = eventApi._def
let instance = eventApi._instance
return {
defs: { [def.defId]: def },
instances: instance
? { [instance.instanceId]: instance }
: {},
}
}
export function buildEventApis(eventStore: EventStore, context: CalendarContext, excludeInstance?: EventInstance): EventImpl[] {
let { defs, instances } = eventStore
let eventApis: EventImpl[] = []
let excludeInstanceId = excludeInstance ? excludeInstance.instanceId : ''
for (let id in instances) {
let instance = instances[id]
let def = defs[instance.defId]
if (instance.instanceId !== excludeInstanceId) {
eventApis.push(new EventImpl(context, def, instance))
}
}
return eventApis
}

View file

@ -0,0 +1,8 @@
export interface EventSourceApi {
id: string
url: string
format: string
remove(): void
refetch(): void
}

View file

@ -0,0 +1,38 @@
import { EventSource } from '../structs/event-source.js'
import { CalendarContext } from '../CalendarContext.js'
import { EventSourceApi } from './EventSourceApi.js'
export class EventSourceImpl implements EventSourceApi {
constructor(
private context: CalendarContext,
public internalEventSource: EventSource<any>, // rename?
) {
}
remove(): void {
this.context.dispatch({
type: 'REMOVE_EVENT_SOURCE',
sourceId: this.internalEventSource.sourceId,
})
}
refetch(): void {
this.context.dispatch({
type: 'FETCH_EVENT_SOURCES',
sourceIds: [this.internalEventSource.sourceId],
isRefetch: true,
})
}
get id(): string {
return this.internalEventSource.publicId
}
get url(): string {
return this.internalEventSource.meta.url
}
get format(): string {
return this.internalEventSource.meta.format // TODO: bad. not guaranteed
}
}

View file

@ -0,0 +1,14 @@
import { CalendarApi } from './CalendarApi.js'
export interface ViewApi {
calendar: CalendarApi
type: string
title: string
activeStart: Date
activeEnd: Date
currentStart: Date
currentEnd: Date
getOption(name: string): any
}

View file

@ -0,0 +1,42 @@
import { DateEnv } from '../datelib/env.js'
import { CalendarData } from '../reducers/data-types.js'
import { CalendarApi } from './CalendarApi.js'
import { ViewApi } from './ViewApi.js'
// always represents the current view. otherwise, it'd need to change value every time date changes
export class ViewImpl implements ViewApi {
constructor(
public type: string,
private getCurrentData: () => CalendarData,
private dateEnv: DateEnv,
) {
}
get calendar(): CalendarApi {
return this.getCurrentData().calendarApi
}
get title(): string {
return this.getCurrentData().viewTitle
}
get activeStart(): Date {
return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start)
}
get activeEnd(): Date {
return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end)
}
get currentStart(): Date {
return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start)
}
get currentEnd(): Date {
return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end)
}
getOption(name: string): any {
return this.getCurrentData().options[name] // are the view-specific options
}
}

View file

@ -0,0 +1,44 @@
export type { CalendarOptions, CalendarListeners } from '../options.js'
export type { DateInput } from '../datelib/env.js'
export type { DurationInput } from '../datelib/duration.js'
export type { DateSpanInput } from '../structs/date-span.js'
export type { DateRangeInput } from '../datelib/date-range.js'
export type { EventSourceInput } from '../structs/event-source-parse.js'
export type { EventSourceFunc, EventSourceFuncArg } from '../event-sources/func-event-source.js'
export type { EventInput, EventInputTransformer } from '../structs/event-parse.js'
export type { FormatterInput } from '../datelib/formatting.js'
export type { CssDimValue } from '../scrollgrid/util.js'
export type { BusinessHoursInput } from '../structs/business-hours.js'
export type { LocaleSingularArg, LocaleInput } from '../datelib/locale.js'
export type { OverlapFunc, ConstraintInput, AllowFunc } from '../structs/constraint.js'
export type { PluginDef, PluginDefInput } from '../plugin-system-struct.js'
export type { ViewComponentType, SpecificViewContentArg, SpecificViewMountArg } from '../structs/view-config.js'
export type { ClassNamesGenerator, CustomContentGenerator, DidMountHandler, WillUnmountHandler } from '../common/render-hook.js'
export type { NowIndicatorContentArg, NowIndicatorMountArg } from '../common/NowIndicatorContainer.js'
export type { WeekNumberContentArg, WeekNumberMountArg } from '../common/WeekNumberContainer.js'
export type { MoreLinkContentArg, MoreLinkMountArg } from '../common/MoreLinkContainer.js'
export * from '../common/more-link-public-types.js'
export type {
SlotLaneContentArg, SlotLaneMountArg,
SlotLabelContentArg, SlotLabelMountArg,
AllDayContentArg, AllDayMountArg,
DayHeaderContentArg,
DayHeaderMountArg,
} from '../render-hook-misc.js'
export type { DayCellContentArg, DayCellMountArg } from '../common/DayCellContainer.js'
export type { ViewContentArg, ViewMountArg } from '../common/ViewContainer.js'
export type { EventClickArg } from '../interactions/EventClicking.js'
export type { EventHoveringArg } from '../interactions/EventHovering.js'
export type { DateSelectArg, DateUnselectArg } from '../calendar-utils.js'
export type { WeekNumberCalculation } from '../datelib/env.js'
export type { ToolbarInput, CustomButtonInput, ButtonIconsInput, ButtonTextCompoundInput } from '../toolbar-struct.js'
export type { EventContentArg, EventMountArg } from '../component/event-rendering.js'
export type { DatesSetArg } from '../dates-set.js'
export type { EventAddArg, EventChangeArg, EventDropArg, EventRemoveArg } from '../event-crud.js'
export type { ButtonHintCompoundInput } from '../toolbar-struct.js'
export type { CustomRenderingHandler, CustomRenderingStore } from '../content-inject/CustomRenderingStore.js'
export type { DateSpanApi, DatePointApi } from '../structs/date-span.js'
export type { DateSelectionApi } from '../calendar-utils.js'
// used by some args
export type { Duration } from '../datelib/duration.js'

View file

@ -0,0 +1,79 @@
import { PointerDragEvent } from './interactions/pointer.js'
import { buildDateSpanApi, DateSpanApi, DatePointApi, DateSpan } from './structs/date-span.js'
import { CalendarContext } from './CalendarContext.js'
import { ViewApi } from './api/ViewApi.js'
import { ViewImpl } from './api/ViewImpl.js'
import { DateMarker, startOfDay } from './datelib/marker.js'
export interface DateClickApi extends DatePointApi {
dayEl: HTMLElement
jsEvent: UIEvent
view: ViewApi
}
export interface DateSelectionApi extends DateSpanApi {
jsEvent: UIEvent
view: ViewApi
}
export type DatePointTransform = (dateSpan: DateSpan, context: CalendarContext) => any
export type DateSpanTransform = (dateSpan: DateSpan, context: CalendarContext) => any
export type CalendarInteraction = { destroy: () => void }
export type CalendarInteractionClass = { new(context: CalendarContext): CalendarInteraction }
export type OptionChangeHandler = (propValue: any, context: CalendarContext) => void
export type OptionChangeHandlerMap = { [propName: string]: OptionChangeHandler }
export interface DateSelectArg extends DateSpanApi {
jsEvent: MouseEvent | null
view: ViewApi
}
export function triggerDateSelect(selection: DateSpan, pev: PointerDragEvent | null, context: CalendarContext & { viewApi?: ViewImpl }) {
context.emitter.trigger('select', {
...buildDateSpanApiWithContext(selection, context),
jsEvent: pev ? pev.origEvent as MouseEvent : null, // Is this always a mouse event? See #4655
view: context.viewApi || context.calendarApi.view,
} as DateSelectArg)
}
export interface DateUnselectArg {
jsEvent: MouseEvent
view: ViewApi
}
export function triggerDateUnselect(pev: PointerDragEvent | null, context: CalendarContext & { viewApi?: ViewImpl }) {
context.emitter.trigger('unselect', {
jsEvent: pev ? pev.origEvent as MouseEvent : null, // Is this always a mouse event? See #4655
view: context.viewApi || context.calendarApi.view,
} as DateUnselectArg)
}
export function buildDateSpanApiWithContext(dateSpan: DateSpan, context: CalendarContext) {
let props = {} as DateSpanApi
for (let transform of context.pluginHooks.dateSpanTransforms) {
Object.assign(props, transform(dateSpan, context))
}
Object.assign(props, buildDateSpanApi(dateSpan, context.dateEnv))
return props
}
// Given an event's allDay status and start date, return what its fallback end date should be.
// TODO: rename to computeDefaultEventEnd
export function getDefaultEventEnd(allDay: boolean, marker: DateMarker, context: CalendarContext): DateMarker {
let { dateEnv, options } = context
let end = marker
if (allDay) {
end = startOfDay(end)
end = dateEnv.add(end, options.defaultAllDayEventDuration)
} else {
end = dateEnv.add(end, options.defaultTimedEventDuration)
}
return end
}

View file

@ -0,0 +1,117 @@
import { ComponentChild, createElement } from '../preact.js'
import { DateMarker } from '../datelib/marker.js'
import { DateRange } from '../datelib/date-range.js'
import { getDateMeta, DateMeta, getDayClassNames } from '../component/date-rendering.js'
import { createFormatter } from '../datelib/formatting.js'
import { DateFormatter } from '../datelib/DateFormatter.js'
import { formatDayString } from '../datelib/formatting-utils.js'
import { MountArg } from './render-hook.js'
import { ViewApi } from '../api/ViewApi.js'
import { BaseComponent } from '../vdom-util.js'
import { DateProfile } from '../DateProfileGenerator.js'
import { memoizeObjArg } from '../util/memoize.js'
import { Dictionary, ViewOptions } from '../options.js'
import { DateEnv } from '../datelib/env.js'
import { ContentContainer, InnerContainerFunc } from '../content-inject/ContentContainer.js'
import { ElProps, hasCustomRenderingHandler } from '../content-inject/ContentInjector.js'
export interface DayCellContentArg extends DateMeta {
date: DateMarker // localized
view: ViewApi
dayNumberText: string
[extraProp: string]: any // so can include a resource
}
export type DayCellMountArg = MountArg<DayCellContentArg>
export interface DayCellContainerProps extends Partial<ElProps> {
date: DateMarker
dateProfile: DateProfile
todayRange: DateRange
isMonthStart?: boolean
showDayNumber?: boolean // defaults to false
extraRenderProps?: Dictionary
defaultGenerator?: (renderProps: DayCellContentArg) => ComponentChild
children?: InnerContainerFunc<DayCellContentArg>
}
const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
export class DayCellContainer extends BaseComponent<DayCellContainerProps> {
refineRenderProps = memoizeObjArg(refineRenderProps)
render() {
let { props, context } = this
let { options } = context
let renderProps = this.refineRenderProps({
date: props.date,
dateProfile: props.dateProfile,
todayRange: props.todayRange,
isMonthStart: props.isMonthStart || false,
showDayNumber: props.showDayNumber,
extraRenderProps: props.extraRenderProps,
viewApi: context.viewApi,
dateEnv: context.dateEnv,
monthStartFormat: options.monthStartFormat,
})
return (
<ContentContainer
{...props /* includes children */}
elClasses={[
...getDayClassNames(renderProps, context.theme),
...(props.elClasses || []),
]}
elAttrs={{
...props.elAttrs,
...(renderProps.isDisabled ? {} : { 'data-date': formatDayString(props.date) }),
}}
renderProps={renderProps}
generatorName="dayCellContent"
customGenerator={options.dayCellContent}
defaultGenerator={props.defaultGenerator}
classNameGenerator={
// don't use custom classNames if disabled
renderProps.isDisabled ? undefined : options.dayCellClassNames
}
didMount={options.dayCellDidMount}
willUnmount={options.dayCellWillUnmount}
/>
)
}
}
export function hasCustomDayCellContent(options: ViewOptions): boolean {
return Boolean(options.dayCellContent || hasCustomRenderingHandler('dayCellContent', options))
}
// Render Props
interface DayCellRenderPropsInput {
date: DateMarker // generic
dateProfile: DateProfile
todayRange: DateRange
dateEnv: DateEnv
viewApi: ViewApi
monthStartFormat: DateFormatter
isMonthStart: boolean // defaults to false
showDayNumber?: boolean // defaults to false
extraRenderProps?: Dictionary // so can include a resource
}
function refineRenderProps(raw: DayCellRenderPropsInput): DayCellContentArg {
let { date, dateEnv, dateProfile, isMonthStart } = raw
let dayMeta = getDateMeta(date, raw.todayRange, null, dateProfile)
let dayNumberText = raw.showDayNumber ? (
dateEnv.format(date, isMonthStart ? raw.monthStartFormat : DAY_NUM_FORMAT)
) : ''
return {
date: dateEnv.toDate(date),
view: raw.viewApi,
...dayMeta,
isMonthStart,
dayNumberText,
...raw.extraRenderProps,
}
}

View file

@ -0,0 +1,65 @@
import { BaseComponent } from '../vdom-util.js'
import { DateMarker } from '../datelib/marker.js'
import { computeFallbackHeaderFormat } from './table-utils.js'
import { VNode, createElement } from '../preact.js'
import { TableDateCell } from './TableDateCell.js'
import { TableDowCell } from './TableDowCell.js'
import { NowTimer } from '../NowTimer.js'
import { DateRange } from '../datelib/date-range.js'
import { memoize } from '../util/memoize.js'
import { DateProfile } from '../DateProfileGenerator.js'
import { DateFormatter } from '../datelib/DateFormatter.js'
export interface DayHeaderProps {
dateProfile: DateProfile
dates: DateMarker[]
datesRepDistinctDays: boolean
renderIntro?: (rowKey: string) => VNode
}
export class DayHeader extends BaseComponent<DayHeaderProps> { // TODO: rename to DayHeaderTr?
createDayHeaderFormatter = memoize(createDayHeaderFormatter)
render() {
let { context } = this
let { dates, dateProfile, datesRepDistinctDays, renderIntro } = this.props
let dayHeaderFormat = this.createDayHeaderFormatter(
context.options.dayHeaderFormat,
datesRepDistinctDays,
dates.length,
)
return (
<NowTimer unit="day">
{(nowDate: DateMarker, todayRange: DateRange) => (
<tr role="row">
{renderIntro && renderIntro('day')}
{dates.map((date) => (
datesRepDistinctDays ? (
<TableDateCell
key={date.toISOString()}
date={date}
dateProfile={dateProfile}
todayRange={todayRange}
colCnt={dates.length}
dayHeaderFormat={dayHeaderFormat}
/>
) : (
<TableDowCell
key={date.getUTCDay()}
dow={date.getUTCDay()}
dayHeaderFormat={dayHeaderFormat}
/>
)
))}
</tr>
)}
</NowTimer>
)
}
}
function createDayHeaderFormatter(explicitFormat: DateFormatter, datesRepDistinctDays, dateCnt) {
return explicitFormat || computeFallbackHeaderFormat(datesRepDistinctDays, dateCnt)
}

View file

@ -0,0 +1,81 @@
import { DateProfileGenerator } from '../DateProfileGenerator.js'
import { DateMarker, addDays, diffDays } from '../datelib/marker.js'
import { DateRange } from '../datelib/date-range.js'
export interface DaySeriesSeg {
firstIndex: number
lastIndex: number
isStart: boolean
isEnd: boolean
}
export class DaySeriesModel {
cnt: number
dates: DateMarker[] // whole-day dates for each column. left to right
indices: number[] // for each day from start, the offset
constructor(range: DateRange, dateProfileGenerator: DateProfileGenerator) {
let date: DateMarker = range.start
let { end } = range
let indices: number[] = []
let dates: DateMarker[] = []
let dayIndex = -1
while (date < end) { // loop each day from start to end
if (dateProfileGenerator.isHiddenDay(date)) {
indices.push(dayIndex + 0.5) // mark that it's between indices
} else {
dayIndex += 1
indices.push(dayIndex)
dates.push(date)
}
date = addDays(date, 1)
}
this.dates = dates
this.indices = indices
this.cnt = dates.length
}
sliceRange(range: DateRange): DaySeriesSeg | null {
let firstIndex = this.getDateDayIndex(range.start) // inclusive first index
let lastIndex = this.getDateDayIndex(addDays(range.end, -1)) // inclusive last index
let clippedFirstIndex = Math.max(0, firstIndex)
let clippedLastIndex = Math.min(this.cnt - 1, lastIndex)
// deal with in-between indices
clippedFirstIndex = Math.ceil(clippedFirstIndex) // in-between starts round to next cell
clippedLastIndex = Math.floor(clippedLastIndex) // in-between ends round to prev cell
if (clippedFirstIndex <= clippedLastIndex) {
return {
firstIndex: clippedFirstIndex,
lastIndex: clippedLastIndex,
isStart: firstIndex === clippedFirstIndex,
isEnd: lastIndex === clippedLastIndex,
}
}
return null
}
// Given a date, returns its chronolocial cell-index from the first cell of the grid.
// If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
// If before the first offset, returns a negative number.
// If after the last offset, returns an offset past the last cell offset.
// Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
private getDateDayIndex(date: DateMarker) {
let { indices } = this
let dayOffset = Math.floor(diffDays(this.dates[0], date))
if (dayOffset < 0) {
return indices[0] - 1
}
if (dayOffset >= indices.length) {
return indices[indices.length - 1] + 1
}
return indices[dayOffset]
}
}

View file

@ -0,0 +1,120 @@
import { DaySeriesModel } from './DaySeriesModel.js'
import { DateRange } from '../datelib/date-range.js'
import { DateMarker } from '../datelib/marker.js'
import { Seg } from '../component/DateComponent.js'
import { Dictionary } from '../options.js'
export interface DayTableSeg extends Seg {
row: number
firstCol: number
lastCol: number
}
export interface DayTableCell {
key: string // probably just the serialized date, but could be other metadata if this col is specific to another entity
date: DateMarker
extraRenderProps?: Dictionary
extraDataAttrs?: Dictionary
extraClassNames?: string[]
extraDateSpan?: Dictionary
}
export class DayTableModel {
rowCnt: number
colCnt: number
cells: DayTableCell[][]
headerDates: DateMarker[]
private daySeries: DaySeriesModel
constructor(daySeries: DaySeriesModel, breakOnWeeks: boolean) {
let { dates } = daySeries
let daysPerRow
let firstDay
let rowCnt
if (breakOnWeeks) {
// count columns until the day-of-week repeats
firstDay = dates[0].getUTCDay()
for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow += 1) {
if (dates[daysPerRow].getUTCDay() === firstDay) {
break
}
}
rowCnt = Math.ceil(dates.length / daysPerRow)
} else {
rowCnt = 1
daysPerRow = dates.length
}
this.rowCnt = rowCnt
this.colCnt = daysPerRow
this.daySeries = daySeries
this.cells = this.buildCells()
this.headerDates = this.buildHeaderDates()
}
private buildCells() {
let rows = []
for (let row = 0; row < this.rowCnt; row += 1) {
let cells = []
for (let col = 0; col < this.colCnt; col += 1) {
cells.push(
this.buildCell(row, col),
)
}
rows.push(cells)
}
return rows
}
private buildCell(row, col): DayTableCell {
let date = this.daySeries.dates[row * this.colCnt + col]
return {
key: date.toISOString(),
date,
}
}
private buildHeaderDates() {
let dates = []
for (let col = 0; col < this.colCnt; col += 1) {
dates.push(this.cells[0][col].date)
}
return dates
}
sliceRange(range: DateRange): DayTableSeg[] {
let { colCnt } = this
let seriesSeg = this.daySeries.sliceRange(range)
let segs: DayTableSeg[] = []
if (seriesSeg) {
let { firstIndex, lastIndex } = seriesSeg
let index = firstIndex
while (index <= lastIndex) {
let row = Math.floor(index / colCnt)
let nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1)
segs.push({
row,
firstCol: index % colCnt,
lastCol: (nextIndex - 1) % colCnt,
isStart: seriesSeg.isStart && index === firstIndex,
isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex,
})
index = nextIndex
}
}
return segs
}
}

View file

@ -0,0 +1,59 @@
export interface HandlerFuncTypeHash {
[eventName: string]: (...args: any[]) => any // with all properties required
}
export class Emitter<HandlerFuncs extends HandlerFuncTypeHash> {
private handlers: { [Prop in keyof HandlerFuncs]?: HandlerFuncs[Prop][] } = {}
private options: Partial<HandlerFuncs>
private thisContext: any = null
setThisContext(thisContext) {
this.thisContext = thisContext
}
setOptions(options: Partial<HandlerFuncs>) {
this.options = options
}
on<Prop extends keyof HandlerFuncs>(type: Prop, handler: HandlerFuncs[Prop]) {
addToHash(this.handlers, type, handler)
}
off<Prop extends keyof HandlerFuncs>(type: Prop, handler?: HandlerFuncs[Prop]) {
removeFromHash(this.handlers, type, handler)
}
trigger<Prop extends keyof HandlerFuncs>(type: Prop, ...args: Parameters<HandlerFuncs[Prop]>) {
let attachedHandlers = this.handlers[type] || []
let optionHandler = this.options && this.options[type]
let handlers = [].concat(optionHandler || [], attachedHandlers)
for (let handler of handlers) {
handler.apply(this.thisContext, args)
}
}
hasHandlers(type: keyof HandlerFuncs): boolean {
return Boolean(
(this.handlers[type] && this.handlers[type].length) ||
(this.options && this.options[type]),
)
}
}
function addToHash(hash, type, handler) {
(hash[type] || (hash[type] = []))
.push(handler)
}
function removeFromHash(hash, type, handler?) {
if (handler) {
if (hash[type]) {
hash[type] = hash[type].filter((func) => func !== handler)
}
} else {
delete hash[type] // remove all handler funcs for this type
}
}

View file

@ -0,0 +1,99 @@
import { ComponentChild, createElement } from '../preact.js'
import { BaseComponent } from '../vdom-util.js'
import { Seg } from '../component/DateComponent.js'
import { EventImpl } from '../api/EventImpl.js'
import {
computeSegDraggable,
computeSegStartResizable,
computeSegEndResizable,
EventContentArg,
getEventClassNames,
setElSeg,
} from '../component/event-rendering.js'
import { ContentContainer, InnerContainerFunc } from '../content-inject/ContentContainer.js'
import { ElProps } from '../content-inject/ContentInjector.js'
export interface MinimalEventProps {
seg: Seg
isDragging: boolean // rename to isMirrorDragging? make optional?
isResizing: boolean // rename to isMirrorResizing? make optional?
isDateSelecting: boolean // rename to isMirrorDateSelecting? make optional?
isSelected: boolean
isPast: boolean
isFuture: boolean
isToday: boolean
}
export type EventContainerProps = ElProps & MinimalEventProps & {
defaultGenerator: (renderProps: EventContentArg) => ComponentChild
disableDragging?: boolean
disableResizing?: boolean
timeText: string
children?: InnerContainerFunc<EventContentArg>
}
export class EventContainer extends BaseComponent<EventContainerProps> {
el: HTMLElement
render() {
const { props, context } = this
const { options } = context
const { seg } = props
const { eventRange } = seg
const { ui } = eventRange
const renderProps: EventContentArg = {
event: new EventImpl(context, eventRange.def, eventRange.instance),
view: context.viewApi,
timeText: props.timeText,
textColor: ui.textColor,
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
isDraggable: !props.disableDragging && computeSegDraggable(seg, context),
isStartResizable: !props.disableResizing && computeSegStartResizable(seg, context),
isEndResizable: !props.disableResizing && computeSegEndResizable(seg, context),
isMirror: Boolean(props.isDragging || props.isResizing || props.isDateSelecting),
isStart: Boolean(seg.isStart),
isEnd: Boolean(seg.isEnd),
isPast: Boolean(props.isPast), // TODO: don't cast. getDateMeta does it
isFuture: Boolean(props.isFuture), // TODO: don't cast. getDateMeta does it
isToday: Boolean(props.isToday), // TODO: don't cast. getDateMeta does it
isSelected: Boolean(props.isSelected),
isDragging: Boolean(props.isDragging),
isResizing: Boolean(props.isResizing),
}
return (
<ContentContainer
{...props /* contains children */}
elRef={this.handleEl}
elClasses={[
...getEventClassNames(renderProps),
...seg.eventRange.ui.classNames,
...(props.elClasses || []),
]}
renderProps={renderProps}
generatorName="eventContent"
customGenerator={options.eventContent}
defaultGenerator={props.defaultGenerator}
classNameGenerator={options.eventClassNames}
didMount={options.eventDidMount}
willUnmount={options.eventWillUnmount}
/>
)
}
handleEl = (el: HTMLElement | null) => {
this.el = el
if (el) {
setElSeg(el, this.props.seg)
}
}
componentDidUpdate(prevProps: EventContainerProps): void {
if (this.el && this.props.seg !== prevProps.seg) {
setElSeg(this.el, this.props.seg)
}
}
}

View file

@ -0,0 +1,229 @@
import { EventImpl } from '../api/EventImpl.js'
import { Seg } from '../component/DateComponent.js'
import { DateRange } from '../datelib/date-range.js'
import { addDays, DateMarker } from '../datelib/marker.js'
import { DateProfile } from '../DateProfileGenerator.js'
import { Dictionary } from '../options.js'
import { elementClosest, getUniqueDomId } from '../util/dom-manip.js'
import { formatWithOrdinals } from '../util/misc.js'
import { createElement, Fragment, ComponentChild, RefObject } from '../preact.js'
import { BaseComponent, setRef } from '../vdom-util.js'
import { ViewApi } from '../api/ViewApi.js'
import { ViewContext, ViewContextType } from '../ViewContext.js'
import { MorePopover } from './MorePopover.js'
import { MountArg } from './render-hook.js'
import { ContentContainer, InnerContainerFunc } from '../content-inject/ContentContainer.js'
import { ElProps } from '../content-inject/ContentInjector.js'
import { createAriaClickAttrs } from '../util/dom-event.js'
export interface MoreLinkContainerProps extends Partial<ElProps> {
dateProfile: DateProfile
todayRange: DateRange
allDayDate: DateMarker | null
moreCnt: number // can't always derive from hiddenSegs. some hiddenSegs might be due to lack of dimensions
allSegs: Seg[]
hiddenSegs: Seg[]
extraDateSpan?: Dictionary
alignmentElRef?: RefObject<HTMLElement> // will use internal <a> if unspecified
alignGridTop?: boolean // for popover
forceTimed?: boolean // for popover
popoverContent: () => ComponentChild
defaultGenerator?: (renderProps: MoreLinkContentArg) => ComponentChild
children?: InnerContainerFunc<MoreLinkContentArg>
}
export interface MoreLinkContentArg {
num: number
text: string
shortText: string
view: ViewApi
}
export type MoreLinkMountArg = MountArg<MoreLinkContentArg>
interface MoreLinkContainerState {
isPopoverOpen: boolean
popoverId: string
}
export class MoreLinkContainer extends BaseComponent<MoreLinkContainerProps, MoreLinkContainerState> {
private linkEl: HTMLElement
private parentEl: HTMLElement
state = {
isPopoverOpen: false,
popoverId: getUniqueDomId(),
}
render() {
let { props, state } = this
return (
<ViewContextType.Consumer>
{(context: ViewContext) => {
let { viewApi, options, calendarApi } = context
let { moreLinkText } = options
let { moreCnt } = props
let range = computeRange(props)
let text = typeof moreLinkText === 'function' // TODO: eventually use formatWithOrdinals
? moreLinkText.call(calendarApi, moreCnt)
: `+${moreCnt} ${moreLinkText}`
let hint = formatWithOrdinals(options.moreLinkHint, [moreCnt], text)
let renderProps: MoreLinkContentArg = {
num: moreCnt,
shortText: `+${moreCnt}`, // TODO: offer hook or i18n?
text,
view: viewApi,
}
return (
<Fragment>
{Boolean(props.moreCnt) && (
<ContentContainer
elTag={props.elTag || 'a'}
elRef={this.handleLinkEl}
elClasses={[
...(props.elClasses || []),
'fc-more-link',
]}
elStyle={props.elStyle}
elAttrs={{
...props.elAttrs,
...createAriaClickAttrs(this.handleClick),
title: hint,
'aria-expanded': state.isPopoverOpen,
'aria-controls': state.isPopoverOpen ? state.popoverId : '',
}}
renderProps={renderProps}
generatorName="moreLinkContent"
customGenerator={options.moreLinkContent}
defaultGenerator={props.defaultGenerator || renderMoreLinkInner}
classNameGenerator={options.moreLinkClassNames}
didMount={options.moreLinkDidMount}
willUnmount={options.moreLinkWillUnmount}
>{props.children}</ContentContainer>
)}
{state.isPopoverOpen && (
<MorePopover
id={state.popoverId}
startDate={range.start}
endDate={range.end}
dateProfile={props.dateProfile}
todayRange={props.todayRange}
extraDateSpan={props.extraDateSpan}
parentEl={this.parentEl}
alignmentEl={
props.alignmentElRef ?
props.alignmentElRef.current :
this.linkEl
}
alignGridTop={props.alignGridTop}
forceTimed={props.forceTimed}
onClose={this.handlePopoverClose}
>
{props.popoverContent()}
</MorePopover>
)}
</Fragment>
)
}}
</ViewContextType.Consumer>
)
}
componentDidMount() {
this.updateParentEl()
}
componentDidUpdate() {
this.updateParentEl()
}
handleLinkEl = (linkEl: HTMLElement | null) => {
this.linkEl = linkEl
if (this.props.elRef) {
setRef(this.props.elRef, linkEl)
}
}
updateParentEl() {
if (this.linkEl) {
this.parentEl = elementClosest(this.linkEl, '.fc-view-harness')
}
}
handleClick = (ev: MouseEvent) => {
let { props, context } = this
let { moreLinkClick } = context.options
let date = computeRange(props).start
function buildPublicSeg(seg: Seg) {
let { def, instance, range } = seg.eventRange
return {
event: new EventImpl(context, def, instance),
start: context.dateEnv.toDate(range.start),
end: context.dateEnv.toDate(range.end),
isStart: seg.isStart,
isEnd: seg.isEnd,
}
}
if (typeof moreLinkClick === 'function') {
moreLinkClick = moreLinkClick({
date,
allDay: Boolean(props.allDayDate),
allSegs: props.allSegs.map(buildPublicSeg),
hiddenSegs: props.hiddenSegs.map(buildPublicSeg),
jsEvent: ev,
view: context.viewApi,
}) as string | undefined
}
if (!moreLinkClick || moreLinkClick === 'popover') {
this.setState({ isPopoverOpen: true })
} else if (typeof moreLinkClick === 'string') { // a view name
context.calendarApi.zoomTo(date, moreLinkClick)
}
}
handlePopoverClose = () => {
this.setState({ isPopoverOpen: false })
}
}
function renderMoreLinkInner(props: MoreLinkContentArg) {
return props.text
}
function computeRange(props: MoreLinkContainerProps): DateRange {
if (props.allDayDate) {
return {
start: props.allDayDate,
end: addDays(props.allDayDate, 1),
}
}
let { hiddenSegs } = props
return {
start: computeEarliestSegStart(hiddenSegs),
end: computeLatestSegEnd(hiddenSegs),
}
}
export function computeEarliestSegStart(segs: Seg[]): DateMarker {
return segs.reduce(pickEarliestStart).eventRange.range.start
}
function pickEarliestStart(seg0: Seg, seg1: Seg): Seg {
return seg0.eventRange.range.start < seg1.eventRange.range.start ? seg0 : seg1
}
function computeLatestSegEnd(segs: Seg[]): DateMarker {
return segs.reduce(pickLatestEnd).eventRange.range.end
}
function pickLatestEnd(seg0: Seg, seg1: Seg): Seg {
return seg0.eventRange.range.end > seg1.eventRange.range.end ? seg0 : seg1
}

View file

@ -0,0 +1,114 @@
import { DateComponent } from '../component/DateComponent.js'
import { DateRange } from '../datelib/date-range.js'
import { DateMarker } from '../datelib/marker.js'
import { DateProfile } from '../DateProfileGenerator.js'
import { Hit } from '../interactions/hit.js'
import { Dictionary } from '../options.js'
import { createElement, ComponentChildren } from '../preact.js'
import { DayCellContainer, hasCustomDayCellContent } from './DayCellContainer.js'
import { Popover } from './Popover.js'
export interface MorePopoverProps {
id: string
startDate: DateMarker
endDate: DateMarker
dateProfile: DateProfile
parentEl: HTMLElement
alignmentEl: HTMLElement
alignGridTop?: boolean
forceTimed?: boolean
todayRange: DateRange
extraDateSpan: Dictionary
children: ComponentChildren
onClose?: () => void
}
export class MorePopover extends DateComponent<MorePopoverProps> {
rootEl: HTMLElement
render() {
let { options, dateEnv } = this.context
let { props } = this
let { startDate, todayRange, dateProfile } = props
let title = dateEnv.format(startDate, options.dayPopoverFormat)
return (
<DayCellContainer
elRef={this.handleRootEl}
date={startDate}
dateProfile={dateProfile}
todayRange={todayRange}
>
{(InnerContent, renderProps, elAttrs) => (
<Popover
elRef={elAttrs.ref}
id={props.id}
title={title}
extraClassNames={
['fc-more-popover'].concat(
(elAttrs.className as (string | undefined)) || [],
)
}
extraAttrs={elAttrs /* TODO: make these time-based when not whole-day? */}
parentEl={props.parentEl}
alignmentEl={props.alignmentEl}
alignGridTop={props.alignGridTop}
onClose={props.onClose}
>
{hasCustomDayCellContent(options) && (
<InnerContent
elTag="div"
elClasses={['fc-more-popover-misc']}
/>
)}
{props.children}
</Popover>
)}
</DayCellContainer>
)
}
handleRootEl = (rootEl: HTMLElement | null) => {
this.rootEl = rootEl
if (rootEl) {
this.context.registerInteractiveComponent(this, {
el: rootEl,
useEventCenter: false,
})
} else {
this.context.unregisterInteractiveComponent(this)
}
}
queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit {
let { rootEl, props } = this
if (
positionLeft >= 0 && positionLeft < elWidth &&
positionTop >= 0 && positionTop < elHeight
) {
return {
dateProfile: props.dateProfile,
dateSpan: {
allDay: !props.forceTimed,
range: {
start: props.startDate,
end: props.endDate,
},
...props.extraDateSpan,
},
dayEl: rootEl,
rect: {
left: 0,
top: 0,
right: elWidth,
bottom: elHeight,
},
layer: 1, // important when comparing with hits from other components
}
}
return null
}
}

View file

@ -0,0 +1,47 @@
import { MountArg } from './render-hook.js'
import { DateMarker } from '../datelib/marker.js'
import { ViewContext, ViewContextType } from '../ViewContext.js'
import { createElement } from '../preact.js'
import { ViewApi } from '../api/ViewApi.js'
import { ElProps } from '../content-inject/ContentInjector.js'
import { InnerContainerFunc, ContentContainer } from '../content-inject/ContentContainer.js'
export interface NowIndicatorContainerProps extends Partial<ElProps> {
isAxis: boolean
date: DateMarker
children?: InnerContainerFunc<NowIndicatorContentArg>
}
export interface NowIndicatorContentArg {
isAxis: boolean
date: Date
view: ViewApi
}
export type NowIndicatorMountArg = MountArg<NowIndicatorContentArg>
export const NowIndicatorContainer = (props: NowIndicatorContainerProps) => (
<ViewContextType.Consumer>
{(context: ViewContext) => {
let { options } = context
let renderProps: NowIndicatorContentArg = {
isAxis: props.isAxis,
date: context.dateEnv.toDate(props.date),
view: context.viewApi,
}
return (
<ContentContainer
{...props /* includes children */}
elTag={props.elTag || 'div'}
renderProps={renderProps}
generatorName="nowIndicatorContent"
customGenerator={options.nowIndicatorContent}
classNameGenerator={options.nowIndicatorClassNames}
didMount={options.nowIndicatorDidMount}
willUnmount={options.nowIndicatorWillUnmount}
/>
)
}}
</ViewContextType.Consumer>
)

View file

@ -0,0 +1,132 @@
import { Dictionary } from '../options.js'
import { computeClippedClientRect } from '../util/dom-geom.js'
import { applyStyle, elementClosest, getEventTargetViaRoot, getUniqueDomId } from '../util/dom-manip.js'
import { createElement, ComponentChildren, Ref, createPortal } from '../preact.js'
import { BaseComponent, setRef } from '../vdom-util.js'
export interface PopoverProps {
elRef?: Ref<HTMLElement>
id: string
title: string
extraClassNames?: string[]
extraAttrs?: Dictionary
parentEl: HTMLElement
alignmentEl: HTMLElement
alignGridTop?: boolean
children?: ComponentChildren
onClose?: () => void
}
const PADDING_FROM_VIEWPORT = 10
export class Popover extends BaseComponent<PopoverProps> {
private rootEl: HTMLElement
state = {
titleId: getUniqueDomId(),
}
render(): any {
let { theme, options } = this.context
let { props, state } = this
let classNames = [
'fc-popover',
theme.getClass('popover'),
].concat(
props.extraClassNames || [],
)
return createPortal(
<div
{...props.extraAttrs}
id={props.id}
className={classNames.join(' ')}
aria-labelledby={state.titleId}
ref={this.handleRootEl}
>
<div className={'fc-popover-header ' + theme.getClass('popoverHeader')}>
<span className="fc-popover-title" id={state.titleId}>
{props.title}
</span>
<span
className={'fc-popover-close ' + theme.getIconClass('close')}
title={options.closeHint}
onClick={this.handleCloseClick}
/>
</div>
<div className={'fc-popover-body ' + theme.getClass('popoverContent')}>
{props.children}
</div>
</div>,
props.parentEl,
)
}
componentDidMount() {
document.addEventListener('mousedown', this.handleDocumentMouseDown)
document.addEventListener('keydown', this.handleDocumentKeyDown)
this.updateSize()
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleDocumentMouseDown)
document.removeEventListener('keydown', this.handleDocumentKeyDown)
}
handleRootEl = (el: HTMLElement | null) => {
this.rootEl = el
if (this.props.elRef) {
setRef(this.props.elRef, el)
}
}
// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
handleDocumentMouseDown = (ev) => {
// only hide the popover if the click happened outside the popover
const target = getEventTargetViaRoot(ev) as HTMLElement
if (!this.rootEl.contains(target)) {
this.handleCloseClick()
}
}
handleDocumentKeyDown = (ev) => {
if (ev.key === 'Escape') {
this.handleCloseClick()
}
}
handleCloseClick = () => {
let { onClose } = this.props
if (onClose) {
onClose()
}
}
private updateSize() {
let { isRtl } = this.context
let { alignmentEl, alignGridTop } = this.props
let { rootEl } = this
let alignmentRect = computeClippedClientRect(alignmentEl)
if (alignmentRect) {
let popoverDims = rootEl.getBoundingClientRect()
// position relative to viewport
let popoverTop = alignGridTop
? elementClosest(alignmentEl, '.fc-scrollgrid').getBoundingClientRect().top
: alignmentRect.top
let popoverLeft = isRtl ? alignmentRect.right - popoverDims.width : alignmentRect.left
// constrain
popoverTop = Math.max(popoverTop, PADDING_FROM_VIEWPORT)
popoverLeft = Math.min(popoverLeft, document.documentElement.clientWidth - PADDING_FROM_VIEWPORT - popoverDims.width)
popoverLeft = Math.max(popoverLeft, PADDING_FROM_VIEWPORT)
let origin = rootEl.offsetParent.getBoundingClientRect()
applyStyle(rootEl, {
top: popoverTop - origin.top,
left: popoverLeft - origin.left,
})
}
}
}

View file

@ -0,0 +1,125 @@
/*
Records offset information for a set of elements, relative to an origin element.
Can record the left/right OR the top/bottom OR both.
Provides methods for querying the cache by position.
*/
export class PositionCache {
els: HTMLElement[] // assumed to be siblings
originClientRect: ClientRect
// arrays of coordinates (from topleft of originEl)
// caller can access these directly
lefts: any
rights: any
tops: any
bottoms: any
constructor(originEl: HTMLElement, els: HTMLElement[], isHorizontal: boolean, isVertical: boolean) {
this.els = els
let originClientRect = this.originClientRect = originEl.getBoundingClientRect() // relative to viewport top-left
if (isHorizontal) {
this.buildElHorizontals(originClientRect.left)
}
if (isVertical) {
this.buildElVerticals(originClientRect.top)
}
}
// Populates the left/right internal coordinate arrays
buildElHorizontals(originClientLeft: number) {
let lefts = []
let rights = []
for (let el of this.els) {
let rect = el.getBoundingClientRect()
lefts.push(rect.left - originClientLeft)
rights.push(rect.right - originClientLeft)
}
this.lefts = lefts
this.rights = rights
}
// Populates the top/bottom internal coordinate arrays
buildElVerticals(originClientTop: number) {
let tops = []
let bottoms = []
for (let el of this.els) {
let rect = el.getBoundingClientRect()
tops.push(rect.top - originClientTop)
bottoms.push(rect.bottom - originClientTop)
}
this.tops = tops
this.bottoms = bottoms
}
// Given a left offset (from document left), returns the index of the el that it horizontally intersects.
// If no intersection is made, returns undefined.
leftToIndex(leftPosition: number) {
let { lefts, rights } = this
let len = lefts.length
let i
for (i = 0; i < len; i += 1) {
if (leftPosition >= lefts[i] && leftPosition < rights[i]) {
return i
}
}
return undefined // TODO: better
}
// Given a top offset (from document top), returns the index of the el that it vertically intersects.
// If no intersection is made, returns undefined.
topToIndex(topPosition: number) {
let { tops, bottoms } = this
let len = tops.length
let i
for (i = 0; i < len; i += 1) {
if (topPosition >= tops[i] && topPosition < bottoms[i]) {
return i
}
}
return undefined // TODO: better
}
// Gets the width of the element at the given index
getWidth(leftIndex: number) {
return this.rights[leftIndex] - this.lefts[leftIndex]
}
// Gets the height of the element at the given index
getHeight(topIndex: number) {
return this.bottoms[topIndex] - this.tops[topIndex]
}
similarTo(otherCache: PositionCache) {
return similarNumArrays(this.tops || [], otherCache.tops || []) &&
similarNumArrays(this.bottoms || [], otherCache.bottoms || []) &&
similarNumArrays(this.lefts || [], otherCache.lefts || []) &&
similarNumArrays(this.rights || [], otherCache.rights || [])
}
}
function similarNumArrays(a: number[], b: number[]): boolean {
const len = a.length
if (len !== b.length) {
return false
}
for (let i = 0; i < len; i++) {
if (Math.round(a[i]) !== Math.round(b[i])) {
return false
}
}
return true
}

View file

@ -0,0 +1,88 @@
import { createElement, Fragment } from '../preact.js'
import { BaseComponent } from '../vdom-util.js'
import { buildSegTimeText, EventContentArg, getSegAnchorAttrs } from '../component/event-rendering.js'
import { DateFormatter } from '../datelib/DateFormatter.js'
import { EventContainer } from './EventContainer.js'
import { Seg } from '../component/DateComponent.js'
import { ElRef } from '../content-inject/ContentInjector.js'
export interface StandardEventProps {
elRef?: ElRef
elClasses?: string[]
seg: Seg
isDragging: boolean // rename to isMirrorDragging? make optional?
isResizing: boolean // rename to isMirrorResizing? make optional?
isDateSelecting: boolean // rename to isMirrorDateSelecting? make optional?
isSelected: boolean
isPast: boolean
isFuture: boolean
isToday: boolean
disableDragging?: boolean // defaults false
disableResizing?: boolean // defaults false
defaultTimeFormat: DateFormatter
defaultDisplayEventTime?: boolean // default true
defaultDisplayEventEnd?: boolean // default true
}
// should not be a purecomponent
export class StandardEvent extends BaseComponent<StandardEventProps> {
render() {
let { props, context } = this
let { options } = context
let { seg } = props
let { ui } = seg.eventRange
let timeFormat = options.eventTimeFormat || props.defaultTimeFormat
let timeText = buildSegTimeText(
seg,
timeFormat,
context,
props.defaultDisplayEventTime,
props.defaultDisplayEventEnd,
)
return (
<EventContainer
{...props /* includes elRef */}
elTag="a"
elStyle={{
borderColor: ui.borderColor,
backgroundColor: ui.backgroundColor,
}}
elAttrs={getSegAnchorAttrs(seg, context)}
defaultGenerator={renderInnerContent}
timeText={timeText}
>
{(InnerContent, eventContentArg) => (
<Fragment>
<InnerContent
elTag="div"
elClasses={['fc-event-main']}
elStyle={{ color: eventContentArg.textColor }}
/>
{Boolean(eventContentArg.isStartResizable) && (
<div className="fc-event-resizer fc-event-resizer-start" />
)}
{Boolean(eventContentArg.isEndResizable) && (
<div className="fc-event-resizer fc-event-resizer-end" />
)}
</Fragment>
)}
</EventContainer>
)
}
}
function renderInnerContent(innerProps: EventContentArg) {
return (
<div className="fc-event-main-frame">
{innerProps.timeText && (
<div className="fc-event-time">{innerProps.timeText}</div>
)}
<div className="fc-event-title-container">
<div className="fc-event-title fc-sticky">
{innerProps.event.title || <Fragment>&nbsp;</Fragment>}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,88 @@
import { DateRange } from '../datelib/date-range.js'
import { getDayClassNames, getDateMeta } from '../component/date-rendering.js'
import { DateMarker } from '../datelib/marker.js'
import { createElement } from '../preact.js'
import { DateFormatter } from '../datelib/DateFormatter.js'
import { formatDayString } from '../datelib/formatting-utils.js'
import { BaseComponent } from '../vdom-util.js'
import { buildNavLinkAttrs } from './nav-link.js'
import { DateProfile } from '../DateProfileGenerator.js'
import { DayHeaderContentArg } from '../render-hook-misc.js'
import { Dictionary } from '../options.js'
import { CLASS_NAME, renderInner } from './table-cell-util.js'
import { ContentContainer } from '../content-inject/ContentContainer.js'
export interface TableDateCellProps {
date: DateMarker
dateProfile: DateProfile
todayRange: DateRange
colCnt: number
dayHeaderFormat: DateFormatter
colSpan?: number
isSticky?: boolean // TODO: get this outta here somehow
extraDataAttrs?: Dictionary
extraRenderProps?: Dictionary
}
// BAD name for this class now. used in the Header
export class TableDateCell extends BaseComponent<TableDateCellProps> {
render() {
let { dateEnv, options, theme, viewApi } = this.context
let { props } = this
let { date, dateProfile } = props
let dayMeta = getDateMeta(date, props.todayRange, null, dateProfile)
let classNames = [CLASS_NAME].concat(
getDayClassNames(dayMeta, theme),
)
let text = dateEnv.format(date, props.dayHeaderFormat)
// if colCnt is 1, we are already in a day-view and don't need a navlink
let navLinkAttrs = (!dayMeta.isDisabled && props.colCnt > 1)
? buildNavLinkAttrs(this.context, date)
: {}
let renderProps: DayHeaderContentArg = {
date: dateEnv.toDate(date),
view: viewApi,
...props.extraRenderProps,
text,
...dayMeta,
}
return (
<ContentContainer
elTag="th"
elClasses={classNames}
elAttrs={{
role: 'columnheader',
colSpan: props.colSpan,
'data-date': !dayMeta.isDisabled ? formatDayString(date) : undefined,
...props.extraDataAttrs,
}}
renderProps={renderProps}
generatorName="dayHeaderContent"
customGenerator={options.dayHeaderContent}
defaultGenerator={renderInner}
classNameGenerator={options.dayHeaderClassNames}
didMount={options.dayHeaderDidMount}
willUnmount={options.dayHeaderWillUnmount}
>
{(InnerContainer) => (
<div className="fc-scrollgrid-sync-inner">
{!dayMeta.isDisabled && (
<InnerContainer
elTag="a"
elAttrs={navLinkAttrs}
elClasses={[
'fc-col-header-cell-cushion',
props.isSticky && 'fc-sticky',
]}
/>
)}
</div>
)}
</ContentContainer>
)
}
}

View file

@ -0,0 +1,84 @@
import { getDayClassNames, DateMeta } from '../component/date-rendering.js'
import { addDays } from '../datelib/marker.js'
import { createElement } from '../preact.js'
import { DateFormatter } from '../datelib/DateFormatter.js'
import { BaseComponent } from '../vdom-util.js'
import { Dictionary } from '../options.js'
import { CLASS_NAME, renderInner } from './table-cell-util.js'
import { DayHeaderContentArg } from '../render-hook-misc.js'
import { createFormatter } from '../datelib/formatting.js'
import { ContentContainer } from '../content-inject/ContentContainer.js'
export interface TableDowCellProps {
dow: number
dayHeaderFormat: DateFormatter
colSpan?: number
isSticky?: boolean // TODO: get this outta here somehow
extraRenderProps?: Dictionary
extraDataAttrs?: Dictionary
extraClassNames?: string[]
}
const WEEKDAY_FORMAT = createFormatter({ weekday: 'long' })
export class TableDowCell extends BaseComponent<TableDowCellProps> {
render() {
let { props } = this
let { dateEnv, theme, viewApi, options } = this.context
let date = addDays(new Date(259200000), props.dow) // start with Sun, 04 Jan 1970 00:00:00 GMT
let dateMeta: DateMeta = {
dow: props.dow,
isDisabled: false,
isFuture: false,
isPast: false,
isToday: false,
isOther: false,
}
let text = dateEnv.format(date, props.dayHeaderFormat)
let renderProps: DayHeaderContentArg = { // TODO: make this public?
date,
...dateMeta,
view: viewApi,
...props.extraRenderProps,
text,
}
return (
<ContentContainer
elTag="th"
elClasses={[
CLASS_NAME,
...getDayClassNames(dateMeta, theme),
...(props.extraClassNames || []),
]}
elAttrs={{
role: 'columnheader',
colSpan: props.colSpan,
...props.extraDataAttrs,
}}
renderProps={renderProps}
generatorName="dayHeaderContent"
customGenerator={options.dayHeaderContent}
defaultGenerator={renderInner}
classNameGenerator={options.dayHeaderClassNames}
didMount={options.dayHeaderDidMount}
willUnmount={options.dayHeaderWillUnmount}
>
{(InnerContent) => (
<div className="fc-scrollgrid-sync-inner">
<InnerContent
elTag="a"
elClasses={[
'fc-col-header-cell-cushion',
props.isSticky && 'fc-sticky',
]}
elAttrs={{
'aria-label': dateEnv.format(date, WEEKDAY_FORMAT),
}}
/>
</div>
)}
</ContentContainer>
)
}
}

View file

@ -0,0 +1,51 @@
import { ViewSpec } from '../structs/view-spec.js'
import { MountArg } from './render-hook.js'
import { ComponentChildren, createElement } from '../preact.js'
import { BaseComponent } from '../vdom-util.js'
import { ViewApi } from '../api/ViewApi.js'
import { ContentContainer } from '../content-inject/ContentContainer.js'
import { ElProps } from '../content-inject/ContentInjector.js'
export interface ViewContainerProps extends Partial<ElProps> {
viewSpec: ViewSpec
children: ComponentChildren
}
export interface ViewContentArg {
view: ViewApi
}
export type ViewMountArg = MountArg<ViewContentArg>
export class ViewContainer extends BaseComponent<ViewContainerProps> {
render() {
let { props, context } = this
let { options } = context
let renderProps: ViewContentArg = { view: context.viewApi }
return (
<ContentContainer
{...props}
elTag={props.elTag || 'div'}
elClasses={[
...buildViewClassNames(props.viewSpec),
...(props.elClasses || []),
]}
renderProps={renderProps}
classNameGenerator={options.viewClassNames}
generatorName={undefined}
didMount={options.viewDidMount}
willUnmount={options.viewWillUnmount}
>
{() => props.children}
</ContentContainer>
)
}
}
export function buildViewClassNames(viewSpec: ViewSpec): string[] {
return [
`fc-${viewSpec.type}-view`,
'fc-view',
]
}

View file

@ -0,0 +1,51 @@
import { ViewContext, ViewContextType } from '../ViewContext.js'
import { DateMarker } from '../datelib/marker.js'
import { MountArg } from './render-hook.js'
import { createElement } from '../preact.js'
import { DateFormatter } from '../datelib/DateFormatter.js'
import { ElProps } from '../content-inject/ContentInjector.js'
import { ContentContainer, InnerContainerFunc } from '../content-inject/ContentContainer.js'
export interface WeekNumberContainerProps extends ElProps {
date: DateMarker
defaultFormat: DateFormatter
children?: InnerContainerFunc<WeekNumberContentArg>
}
export interface WeekNumberContentArg {
num: number
text: string
date: Date
}
export type WeekNumberMountArg = MountArg<WeekNumberContentArg>
export const WeekNumberContainer = (props: WeekNumberContainerProps) => (
<ViewContextType.Consumer>
{(context: ViewContext) => {
let { dateEnv, options } = context
let { date } = props
let format = options.weekNumberFormat || props.defaultFormat
let num = dateEnv.computeWeekNumber(date) // TODO: somehow use for formatting as well?
let text = dateEnv.format(date, format)
let renderProps: WeekNumberContentArg = { num, text, date }
return (
<ContentContainer // why isn't WeekNumberContentArg being auto-detected?
{...props /* includes children */}
renderProps={renderProps}
generatorName="weekNumberContent"
customGenerator={options.weekNumberContent}
defaultGenerator={renderInner}
classNameGenerator={options.weekNumberClassNames}
didMount={options.weekNumberDidMount}
willUnmount={options.weekNumberWillUnmount}
/>
)
}}
</ViewContextType.Consumer>
)
function renderInner(innerProps) {
return innerProps.text
}

View file

@ -0,0 +1,53 @@
import { createElement } from '../preact.js'
import { BaseComponent } from '../vdom-util.js'
import { Seg } from '../component/DateComponent.js'
import { EventContentArg } from '../component/event-rendering.js'
import { EventContainer } from './EventContainer.js'
export interface BgEventProps {
seg: Seg
isPast: boolean
isFuture: boolean
isToday: boolean
}
export class BgEvent extends BaseComponent<BgEventProps> {
render() {
let { props } = this
let { seg } = props
return (
<EventContainer
elTag="div"
elClasses={['fc-bg-event']}
elStyle={{ backgroundColor: seg.eventRange.ui.backgroundColor }}
defaultGenerator={renderInnerContent}
seg={seg}
timeText=""
isDragging={false}
isResizing={false}
isDateSelecting={false}
isSelected={false}
isPast={props.isPast}
isFuture={props.isFuture}
isToday={props.isToday}
disableDragging={true}
disableResizing={true}
/>
)
}
}
function renderInnerContent(props: EventContentArg) {
let { title } = props.event
return title && (
<div className="fc-event-title">{props.event.title}</div>
)
}
export function renderFill(fillType: string) {
return (
<div className={`fc-${fillType}`} />
)
}

View file

@ -0,0 +1,24 @@
import { EventApi } from '../api/EventApi.js'
import { ViewApi } from '../api/ViewApi.js'
export interface EventSegment {
event: EventApi
start: Date
end: Date
isStart: boolean
isEnd: boolean
}
export type MoreLinkAction = MoreLinkSimpleAction | MoreLinkHandler
export type MoreLinkSimpleAction = 'popover' | 'week' | 'day' | 'timeGridWeek' | 'timeGridDay' | string
export interface MoreLinkArg {
date: Date
allDay: boolean
allSegs: EventSegment[]
hiddenSegs: EventSegment[]
jsEvent: UIEvent
view: ViewApi
}
export type MoreLinkHandler = (arg: MoreLinkArg) => MoreLinkSimpleAction | void

View file

@ -0,0 +1,48 @@
import { createFormatter } from '../datelib/formatting.js'
import { DateMarker } from '../datelib/marker.js'
import { createAriaClickAttrs } from '../util/dom-event.js'
import { formatWithOrdinals } from '../util/misc.js'
import { ViewContext } from '../ViewContext.js'
const DAY_FORMAT = createFormatter({ year: 'numeric', month: 'long', day: 'numeric' })
const WEEK_FORMAT = createFormatter({ week: 'long' })
export function buildNavLinkAttrs(
context: ViewContext,
dateMarker: DateMarker,
viewType = 'day',
isTabbable = true,
) {
const { dateEnv, options, calendarApi } = context
let dateStr = dateEnv.format(dateMarker, viewType === 'week' ? WEEK_FORMAT : DAY_FORMAT)
if (options.navLinks) {
let zonedDate = dateEnv.toDate(dateMarker)
const handleInteraction = (ev: UIEvent) => {
let customAction =
viewType === 'day' ? options.navLinkDayClick :
viewType === 'week' ? options.navLinkWeekClick : null
if (typeof customAction === 'function') {
customAction.call(calendarApi, dateEnv.toDate(dateMarker), ev)
} else {
if (typeof customAction === 'string') {
viewType = customAction
}
calendarApi.zoomTo(dateMarker, viewType)
}
}
return {
title: formatWithOrdinals(options.navLinkHint, [dateStr, zonedDate], dateStr),
'data-navlink': '', // for legacy selectors. TODO: use className?
...(isTabbable
? createAriaClickAttrs(handleInteraction)
: { onClick: handleInteraction }
),
}
}
return { 'aria-label': dateStr }
}

View file

@ -0,0 +1,17 @@
/* eslint max-classes-per-file: off */
import { ComponentChildren } from '../preact.js'
import { ClassNamesInput } from '../util/html.js'
export type MountArg<ContentArg> = ContentArg & { el: HTMLElement }
export type DidMountHandler<TheMountArg extends { el: HTMLElement }> = (mountArg: TheMountArg) => void
export type WillUnmountHandler<TheMountArg extends { el: HTMLElement }> = (mountArg: TheMountArg) => void
export interface ObjCustomContent {
html: string
domNodes: any[]
}
export type CustomContent = ComponentChildren | ObjCustomContent
export type CustomContentGenerator<RenderProps> = CustomContent | ((renderProps: RenderProps, createElement: any) => (CustomContent | true))
export type ClassNamesGenerator<RenderProps> = ClassNamesInput | ((renderProps: RenderProps) => ClassNamesInput)

Some files were not shown because too many files have changed in this diff Show more