Publii: update content

This commit is contained in:
Jan Rippl 2024-03-06 14:50:45 +01:00
parent c4b02d2305
commit cace223aaf
1143 changed files with 20 additions and 87432 deletions

View file

@ -1,6 +1,6 @@
<!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Error 404 - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="Jsem · Nudista · Online"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] {
opacity: 1;
}</style></noscript><script src="/kalendar-novy/dist/index.global.js"></script><script src="/kalendar-novy/packages/list/index.global.js"></script><script src="/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() {
}</style></noscript><script src="/kalendar/dist/index.global.js"></script><script src="/kalendar/packages/list/index.global.js"></script><script src="/kalendar/packages/core/locales/cs.global.js"></script><script src="/kalendar/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {

View file

@ -1,6 +1,6 @@
<!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Author: CZ 🇨🇿 - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="CZ 🇨🇿"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/autor/jan-rippl-cz/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] {
opacity: 1;
}</style></noscript><script src="/kalendar-novy/dist/index.global.js"></script><script src="/kalendar-novy/packages/list/index.global.js"></script><script src="/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() {
}</style></noscript><script src="/kalendar/dist/index.global.js"></script><script src="/kalendar/packages/list/index.global.js"></script><script src="/kalendar/packages/core/locales/cs.global.js"></script><script src="/kalendar/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {

View file

@ -1,6 +1,6 @@
<!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Author: SRN 🇩🇪 - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="SRN 🇩🇪"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/autor/jan-rippl-de/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] {
opacity: 1;
}</style></noscript><script src="/kalendar-novy/dist/index.global.js"></script><script src="/kalendar-novy/packages/list/index.global.js"></script><script src="/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() {
}</style></noscript><script src="/kalendar/dist/index.global.js"></script><script src="/kalendar/packages/list/index.global.js"></script><script src="/kalendar/packages/core/locales/cs.global.js"></script><script src="/kalendar/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {

View file

@ -1,12 +0,0 @@
# 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

@ -1,49 +0,0 @@
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

@ -1,5 +0,0 @@
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

@ -1,37 +0,0 @@
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

@ -1,49 +0,0 @@
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

View file

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

View file

@ -1,7 +0,0 @@
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

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,14 +0,0 @@
## 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

@ -1,73 +0,0 @@
# 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

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

View file

@ -1,58 +0,0 @@
# 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

@ -1,49 +0,0 @@
{
"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

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

View file

@ -1,17 +0,0 @@
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

@ -1,47 +0,0 @@
{
"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

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

View file

@ -1,46 +0,0 @@
# 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

@ -1,50 +0,0 @@
{
"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

@ -1,37 +0,0 @@
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

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

View file

@ -1,8 +0,0 @@
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

@ -1,10 +0,0 @@
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

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

View file

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

View file

@ -1,44 +0,0 @@
# 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

@ -1,50 +0,0 @@
{
"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

@ -1,37 +0,0 @@
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

@ -1,25 +0,0 @@
.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

@ -1,8 +0,0 @@
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

@ -1,10 +0,0 @@
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

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

View file

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

View file

@ -1,44 +0,0 @@
# 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

@ -1,58 +0,0 @@
{
"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

@ -1,21 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,156 +0,0 @@
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

@ -1,289 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,70 +0,0 @@
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

@ -1,455 +0,0 @@
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

@ -1,94 +0,0 @@
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

@ -1,53 +0,0 @@
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

@ -1,67 +0,0 @@
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

@ -1,74 +0,0 @@
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

@ -1,38 +0,0 @@
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

@ -1,85 +0,0 @@
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

@ -1,90 +0,0 @@
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

@ -1,85 +0,0 @@
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

@ -1,511 +0,0 @@
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

@ -1,45 +0,0 @@
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

@ -1,451 +0,0 @@
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

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

View file

@ -1,38 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,42 +0,0 @@
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

@ -1,44 +0,0 @@
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

@ -1,79 +0,0 @@
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

@ -1,117 +0,0 @@
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

@ -1,65 +0,0 @@
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

@ -1,81 +0,0 @@
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

@ -1,120 +0,0 @@
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

@ -1,59 +0,0 @@
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

@ -1,99 +0,0 @@
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

@ -1,229 +0,0 @@
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

@ -1,114 +0,0 @@
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

@ -1,47 +0,0 @@
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

@ -1,132 +0,0 @@
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

@ -1,125 +0,0 @@
/*
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

@ -1,88 +0,0 @@
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

@ -1,88 +0,0 @@
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

@ -1,84 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,53 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,48 +0,0 @@
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

@ -1,17 +0,0 @@
/* 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)

View file

@ -1,124 +0,0 @@
/* eslint max-classes-per-file: "off" */
/*
An object for getting/setting scroll-related information for an element.
Internally, this is done very differently for window versus DOM element,
so this object serves as a common interface.
*/
export abstract class ScrollController {
abstract getScrollTop(): number
abstract getScrollLeft(): number
abstract setScrollTop(top: number): void
abstract setScrollLeft(left: number): void
abstract getClientWidth(): number
abstract getClientHeight(): number
abstract getScrollWidth(): number
abstract getScrollHeight(): number
getMaxScrollTop() {
return this.getScrollHeight() - this.getClientHeight()
}
getMaxScrollLeft() {
return this.getScrollWidth() - this.getClientWidth()
}
canScrollVertically() {
return this.getMaxScrollTop() > 0
}
canScrollHorizontally() {
return this.getMaxScrollLeft() > 0
}
canScrollUp() {
return this.getScrollTop() > 0
}
canScrollDown() {
return this.getScrollTop() < this.getMaxScrollTop()
}
canScrollLeft() {
return this.getScrollLeft() > 0
}
canScrollRight() {
return this.getScrollLeft() < this.getMaxScrollLeft()
}
}
export class ElementScrollController extends ScrollController {
el: HTMLElement
constructor(el: HTMLElement) {
super()
this.el = el
}
getScrollTop() {
return this.el.scrollTop
}
getScrollLeft() {
return this.el.scrollLeft
}
setScrollTop(top: number) {
this.el.scrollTop = top
}
setScrollLeft(left: number) {
this.el.scrollLeft = left
}
getScrollWidth() {
return this.el.scrollWidth
}
getScrollHeight() {
return this.el.scrollHeight
}
getClientHeight() {
return this.el.clientHeight
}
getClientWidth() {
return this.el.clientWidth
}
}
export class WindowScrollController extends ScrollController {
getScrollTop() {
return window.pageYOffset
}
getScrollLeft() {
return window.pageXOffset
}
setScrollTop(n: number) {
window.scroll(window.pageXOffset, n)
}
setScrollLeft(n: number) {
window.scroll(n, window.pageYOffset)
}
getScrollWidth() {
return document.documentElement.scrollWidth
}
getScrollHeight() {
return document.documentElement.scrollHeight
}
getClientHeight() {
return document.documentElement.clientHeight
}
getClientWidth() {
return document.documentElement.clientWidth
}
}

View file

@ -1,247 +0,0 @@
import { DateRange, intersectRanges } from '../datelib/date-range.js'
import { EventStore } from '../structs/event-store.js'
import { EventUiHash } from '../component/event-ui.js'
import { sliceEventStore, EventRenderRange } from '../component/event-rendering.js'
import { DateProfile } from '../DateProfileGenerator.js'
import { Seg, EventSegUiInteractionState } from '../component/DateComponent.js' // TODO: rename EventSegUiInteractionState, move here
import { DateSpan, fabricateEventRange } from '../structs/date-span.js'
import { EventInteractionState } from '../interactions/event-interaction-state.js'
import { Duration } from '../datelib/duration.js'
import { memoize } from '../util/memoize.js'
import { DateMarker, addMs, addDays } from '../datelib/marker.js'
import { CalendarContext } from '../CalendarContext.js'
import { expandRecurring } from '../structs/recurring-event.js'
export interface SliceableProps {
dateSelection: DateSpan
businessHours: EventStore
eventStore: EventStore
eventDrag: EventInteractionState | null
eventResize: EventInteractionState | null
eventSelection: string
eventUiBases: EventUiHash
}
export interface SlicedProps<SegType extends Seg> {
dateSelectionSegs: SegType[]
businessHourSegs: SegType[]
fgEventSegs: SegType[]
bgEventSegs: SegType[]
eventDrag: EventSegUiInteractionState | null
eventResize: EventSegUiInteractionState | null
eventSelection: string
}
export abstract class Slicer<SegType extends Seg, ExtraArgs extends any[] = []> {
private sliceBusinessHours = memoize(this._sliceBusinessHours)
private sliceDateSelection = memoize(this._sliceDateSpan)
private sliceEventStore = memoize(this._sliceEventStore)
private sliceEventDrag = memoize(this._sliceInteraction)
private sliceEventResize = memoize(this._sliceInteraction)
abstract sliceRange(dateRange: DateRange, ...extraArgs: ExtraArgs): SegType[]
protected forceDayIfListItem = false // hack
sliceProps(
props: SliceableProps,
dateProfile: DateProfile,
nextDayThreshold: Duration | null,
context: CalendarContext,
...extraArgs: ExtraArgs
): SlicedProps<SegType> {
let { eventUiBases } = props
let eventSegs = this.sliceEventStore(props.eventStore, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs)
return {
dateSelectionSegs: this.sliceDateSelection(props.dateSelection, dateProfile, nextDayThreshold, eventUiBases, context, ...extraArgs),
businessHourSegs: this.sliceBusinessHours(props.businessHours, dateProfile, nextDayThreshold, context, ...extraArgs),
fgEventSegs: eventSegs.fg,
bgEventSegs: eventSegs.bg,
eventDrag: this.sliceEventDrag(props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs),
eventResize: this.sliceEventResize(props.eventResize, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs),
eventSelection: props.eventSelection,
} // TODO: give interactionSegs?
}
sliceNowDate( // does not memoize
date: DateMarker,
dateProfile: DateProfile,
nextDayThreshold: Duration | null,
context: CalendarContext,
...extraArgs: ExtraArgs
): SegType[] {
return this._sliceDateSpan(
{ range: { start: date, end: addMs(date, 1) }, allDay: false }, // add 1 ms, protect against null range
dateProfile,
nextDayThreshold,
{},
context,
...extraArgs,
)
}
private _sliceBusinessHours(
businessHours: EventStore,
dateProfile: DateProfile,
nextDayThreshold: Duration | null,
context: CalendarContext,
...extraArgs: ExtraArgs
): SegType[] {
if (!businessHours) {
return []
}
return this._sliceEventStore(
expandRecurring(
businessHours,
computeActiveRange(dateProfile, Boolean(nextDayThreshold)),
context,
),
{},
dateProfile,
nextDayThreshold,
...extraArgs,
).bg
}
private _sliceEventStore(
eventStore: EventStore,
eventUiBases: EventUiHash,
dateProfile: DateProfile,
nextDayThreshold: Duration | null,
...extraArgs: ExtraArgs
): { bg: SegType[], fg: SegType[] } {
if (eventStore) {
let rangeRes = sliceEventStore(
eventStore,
eventUiBases,
computeActiveRange(dateProfile, Boolean(nextDayThreshold)),
nextDayThreshold,
)
return {
bg: this.sliceEventRanges(rangeRes.bg, extraArgs),
fg: this.sliceEventRanges(rangeRes.fg, extraArgs),
}
}
return { bg: [], fg: [] }
}
private _sliceInteraction(
interaction: EventInteractionState,
eventUiBases: EventUiHash,
dateProfile: DateProfile,
nextDayThreshold: Duration | null,
...extraArgs: ExtraArgs
): EventSegUiInteractionState {
if (!interaction) {
return null
}
let rangeRes = sliceEventStore(
interaction.mutatedEvents,
eventUiBases,
computeActiveRange(dateProfile, Boolean(nextDayThreshold)),
nextDayThreshold,
)
return {
segs: this.sliceEventRanges(rangeRes.fg, extraArgs),
affectedInstances: interaction.affectedEvents.instances,
isEvent: interaction.isEvent,
}
}
private _sliceDateSpan(
dateSpan: DateSpan,
dateProfile: DateProfile,
nextDayThreshold: Duration | null,
eventUiBases: EventUiHash,
context: CalendarContext,
...extraArgs: ExtraArgs
): SegType[] {
if (!dateSpan) {
return []
}
let activeRange = computeActiveRange(dateProfile, Boolean(nextDayThreshold))
let activeDateSpanRange = intersectRanges(dateSpan.range, activeRange)
if (activeDateSpanRange) {
dateSpan = { ...dateSpan, range: activeDateSpanRange }
let eventRange = fabricateEventRange(dateSpan, eventUiBases, context)
let segs = this.sliceRange(dateSpan.range, ...extraArgs)
for (let seg of segs) {
seg.eventRange = eventRange
}
return segs
}
return []
}
/*
"complete" seg means it has component and eventRange
*/
private sliceEventRanges(
eventRanges: EventRenderRange[],
extraArgs: ExtraArgs,
): SegType[] {
let segs: SegType[] = []
for (let eventRange of eventRanges) {
segs.push(...this.sliceEventRange(eventRange, extraArgs))
}
return segs
}
/*
"complete" seg means it has component and eventRange
*/
private sliceEventRange(
eventRange: EventRenderRange,
extraArgs: ExtraArgs,
): SegType[] {
let dateRange = eventRange.range
// hack to make multi-day events that are being force-displayed as list-items to take up only one day
if (this.forceDayIfListItem && eventRange.ui.display === 'list-item') {
dateRange = {
start: dateRange.start,
end: addDays(dateRange.start, 1),
}
}
let segs = this.sliceRange(dateRange, ...extraArgs)
for (let seg of segs) {
seg.eventRange = eventRange
seg.isStart = eventRange.isStart && seg.isStart
seg.isEnd = eventRange.isEnd && seg.isEnd
}
return segs
}
}
/*
for incorporating slotMinTime/slotMaxTime if appropriate
TODO: should be part of DateProfile!
TimelineDateProfile already does this btw
*/
function computeActiveRange(dateProfile: DateProfile, isComponentAllDay: boolean): DateRange {
let range = dateProfile.activeRange
if (isComponentAllDay) {
return range
}
return {
start: addMs(range.start, dateProfile.slotMinTime.milliseconds),
end: addMs(range.end, dateProfile.slotMaxTime.milliseconds - 864e5), // 864e5 = ms in a day
}
}

View file

@ -1,8 +0,0 @@
import { ComponentChild } from '../preact.js'
import { DayHeaderContentArg } from '../render-hook-misc.js'
export const CLASS_NAME = 'fc-col-header-cell' // do the cushion too? no
export function renderInner(renderProps: DayHeaderContentArg): ComponentChild {
return renderProps.text
}

View file

@ -1,17 +0,0 @@
import { createFormatter } from '../datelib/formatting.js'
import { DateFormatter } from '../datelib/DateFormatter.js'
// Computes a default column header formatting string if `colFormat` is not explicitly defined
export function computeFallbackHeaderFormat(datesRepDistinctDays: boolean, dayCnt: number): DateFormatter {
// if more than one week row, or if there are a lot of columns with not much space,
// put just the day numbers will be in each cell
if (!datesRepDistinctDays || dayCnt > 10) {
return createFormatter({ weekday: 'short' }) // "Sat"
}
if (dayCnt > 1) {
return createFormatter({ weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }) // "Sat 11/12"
}
return createFormatter({ weekday: 'long' }) // "Saturday"
}

View file

@ -1,46 +0,0 @@
import { Component, ComponentChildren } from '../preact.js'
import { CalendarDataManager } from '../reducers/CalendarDataManager.js'
import { CalendarImpl } from '../api/CalendarImpl.js'
import { CalendarData } from '../reducers/data-types.js'
export interface CalendarDataProviderProps {
optionOverrides: any
calendarApi: CalendarImpl
children?: (data: CalendarData) => ComponentChildren
}
// TODO: move this to react plugin?
export class CalendarDataProvider extends Component<CalendarDataProviderProps, CalendarData> {
dataManager: CalendarDataManager
constructor(props: CalendarDataProviderProps) {
super(props)
this.dataManager = new CalendarDataManager({
optionOverrides: props.optionOverrides,
calendarApi: props.calendarApi,
onData: this.handleData,
})
}
handleData = (data: CalendarData) => {
if (!this.dataManager) { // still within initial run, before assignment in constructor
// eslint-disable-next-line react/no-direct-mutation-state
this.state = data // can't use setState yet
} else {
this.setState(data)
}
}
render() {
return this.props.children(this.state)
}
componentDidUpdate(prevProps: CalendarDataProviderProps) {
let newOptionOverrides = this.props.optionOverrides
if (newOptionOverrides !== prevProps.optionOverrides) { // prevent recursive handleData
this.dataManager.resetOptions(newOptionOverrides)
}
}
}

View file

@ -1,64 +0,0 @@
import { BaseComponent } from '../vdom-util.js'
import { EventRenderRange } from './event-rendering.js'
import { EventInstanceHash } from '../structs/event-instance.js'
import { Hit } from '../interactions/hit.js'
import { elementClosest } from '../util/dom-manip.js'
import { guid } from '../util/misc.js'
import { Dictionary } from '../options.js'
export type DateComponentHash = { [uid: string]: DateComponent<any, any> }
// NOTE: for fg-events, eventRange.range is NOT sliced,
// thus, we need isStart/isEnd
export interface Seg {
component?: DateComponent<any, any>
isStart: boolean
isEnd: boolean
eventRange?: EventRenderRange
[otherProp: string]: any // TODO: remove this. extending Seg will handle this
el?: never
// NOTE: can sometimes have start/end, which are important values :(
}
export interface EventSegUiInteractionState {
affectedInstances: EventInstanceHash
segs: Seg[]
isEvent: boolean
}
/*
an INTERACTABLE date component
PURPOSES:
- hook up to fg, fill, and mirror renderers
- interface for dragging and hits
*/
export abstract class DateComponent<Props=Dictionary, State=Dictionary> extends BaseComponent<Props, State> {
uid = guid()
// Hit System
// -----------------------------------------------------------------------------------------------------------------
prepareHits() {
}
queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit | null {
return null // this should be abstract
}
// Pointer Interaction Utils
// -----------------------------------------------------------------------------------------------------------------
isValidSegDownEl(el: HTMLElement) {
return !(this.props as any).eventDrag && // HACK
!(this.props as any).eventResize && // HACK
!elementClosest(el, '.fc-event-mirror')
}
isValidDateDownEl(el: HTMLElement) {
return !elementClosest(el, '.fc-event:not(.fc-bg-event)') &&
!elementClosest(el, '.fc-more-link') && // a "more.." link
!elementClosest(el, 'a[data-navlink]') && // a clickable nav link
!elementClosest(el, '.fc-popover') // hack
}
}

View file

@ -1,80 +0,0 @@
import { DateMarker, DAY_IDS } from '../datelib/marker.js'
import { rangeContainsMarker, DateRange } from '../datelib/date-range.js'
import { DateProfile } from '../DateProfileGenerator.js'
import { Theme } from '../theme/Theme.js'
export interface DateMeta {
dow: number
isDisabled: boolean
isOther: boolean // like, is it in the non-current "other" month
isToday: boolean
isPast: boolean
isFuture: boolean
}
export function getDateMeta(date: DateMarker, todayRange?: DateRange, nowDate?: DateMarker, dateProfile?: DateProfile): DateMeta {
return {
dow: date.getUTCDay(),
isDisabled: Boolean(dateProfile && !rangeContainsMarker(dateProfile.activeRange, date)),
isOther: Boolean(dateProfile && !rangeContainsMarker(dateProfile.currentRange, date)),
isToday: Boolean(todayRange && rangeContainsMarker(todayRange, date)),
isPast: Boolean(nowDate ? (date < nowDate) : todayRange ? (date < todayRange.start) : false),
isFuture: Boolean(nowDate ? (date > nowDate) : todayRange ? (date >= todayRange.end) : false),
}
}
export function getDayClassNames(meta: DateMeta, theme: Theme) {
let classNames: string[] = [
'fc-day',
`fc-day-${DAY_IDS[meta.dow]}`,
]
if (meta.isDisabled) {
classNames.push('fc-day-disabled')
} else {
if (meta.isToday) {
classNames.push('fc-day-today')
classNames.push(theme.getClass('today'))
}
if (meta.isPast) {
classNames.push('fc-day-past')
}
if (meta.isFuture) {
classNames.push('fc-day-future')
}
if (meta.isOther) {
classNames.push('fc-day-other')
}
}
return classNames
}
export function getSlotClassNames(meta: DateMeta, theme: Theme) {
let classNames: string[] = [
'fc-slot',
`fc-slot-${DAY_IDS[meta.dow]}`,
]
if (meta.isDisabled) {
classNames.push('fc-slot-disabled')
} else {
if (meta.isToday) {
classNames.push('fc-slot-today')
classNames.push(theme.getClass('today'))
}
if (meta.isPast) {
classNames.push('fc-slot-past')
}
if (meta.isFuture) {
classNames.push('fc-slot-future')
}
}
return classNames
}

View file

@ -1,375 +0,0 @@
import { EventDef, EventDefHash } from '../structs/event-def.js'
import { EventTuple } from '../structs/event-parse.js'
import { EventStore } from '../structs/event-store.js'
import { DateRange, invertRanges, intersectRanges, rangeContainsMarker } from '../datelib/date-range.js'
import { Duration } from '../datelib/duration.js'
import { compareByFieldSpecs, OrderSpec } from '../util/misc.js'
import { computeVisibleDayRange } from '../util/date.js'
import { Seg } from './DateComponent.js'
import { EventImpl } from '../api/EventImpl.js'
import { EventUi, EventUiHash, combineEventUis } from './event-ui.js'
import { mapHash } from '../util/object.js'
import { ViewContext } from '../ViewContext.js'
import { DateFormatter } from '../datelib/DateFormatter.js'
import { addMs, DateMarker, startOfDay } from '../datelib/marker.js'
import { ViewApi } from '../api/ViewApi.js'
import { MountArg } from '../common/render-hook.js'
import { createAriaKeyboardAttrs } from '../util/dom-event.js'
export interface EventRenderRange extends EventTuple {
ui: EventUi
range: DateRange
isStart: boolean
isEnd: boolean
}
/*
Specifying nextDayThreshold signals that all-day ranges should be sliced.
*/
export function sliceEventStore(eventStore: EventStore, eventUiBases: EventUiHash, framingRange: DateRange, nextDayThreshold?: Duration) {
let inverseBgByGroupId: { [groupId: string]: DateRange[] } = {}
let inverseBgByDefId: { [defId: string]: DateRange[] } = {}
let defByGroupId: { [groupId: string]: EventDef } = {}
let bgRanges: EventRenderRange[] = []
let fgRanges: EventRenderRange[] = []
let eventUis = compileEventUis(eventStore.defs, eventUiBases)
for (let defId in eventStore.defs) {
let def = eventStore.defs[defId]
let ui = eventUis[def.defId]
if (ui.display === 'inverse-background') {
if (def.groupId) {
inverseBgByGroupId[def.groupId] = []
if (!defByGroupId[def.groupId]) {
defByGroupId[def.groupId] = def
}
} else {
inverseBgByDefId[defId] = []
}
}
}
for (let instanceId in eventStore.instances) {
let instance = eventStore.instances[instanceId]
let def = eventStore.defs[instance.defId]
let ui = eventUis[def.defId]
let origRange = instance.range
let normalRange = (!def.allDay && nextDayThreshold) ?
computeVisibleDayRange(origRange, nextDayThreshold) :
origRange
let slicedRange = intersectRanges(normalRange, framingRange)
if (slicedRange) {
if (ui.display === 'inverse-background') {
if (def.groupId) {
inverseBgByGroupId[def.groupId].push(slicedRange)
} else {
inverseBgByDefId[instance.defId].push(slicedRange)
}
} else if (ui.display !== 'none') {
(ui.display === 'background' ? bgRanges : fgRanges).push({
def,
ui,
instance,
range: slicedRange,
isStart: normalRange.start && normalRange.start.valueOf() === slicedRange.start.valueOf(),
isEnd: normalRange.end && normalRange.end.valueOf() === slicedRange.end.valueOf(),
})
}
}
}
for (let groupId in inverseBgByGroupId) { // BY GROUP
let ranges = inverseBgByGroupId[groupId]
let invertedRanges = invertRanges(ranges, framingRange)
for (let invertedRange of invertedRanges) {
let def = defByGroupId[groupId]
let ui = eventUis[def.defId]
bgRanges.push({
def,
ui,
instance: null,
range: invertedRange,
isStart: false,
isEnd: false,
})
}
}
for (let defId in inverseBgByDefId) {
let ranges = inverseBgByDefId[defId]
let invertedRanges = invertRanges(ranges, framingRange)
for (let invertedRange of invertedRanges) {
bgRanges.push({
def: eventStore.defs[defId],
ui: eventUis[defId],
instance: null,
range: invertedRange,
isStart: false,
isEnd: false,
})
}
}
return { bg: bgRanges, fg: fgRanges }
}
export function hasBgRendering(def: EventDef) {
return def.ui.display === 'background' || def.ui.display === 'inverse-background'
}
export function setElSeg(el: HTMLElement, seg: Seg) {
(el as any).fcSeg = seg
}
export function getElSeg(el: HTMLElement): Seg | null {
return (el as any).fcSeg ||
(el.parentNode as any).fcSeg || // for the harness
null
}
// event ui computation
export function compileEventUis(eventDefs: EventDefHash, eventUiBases: EventUiHash) {
return mapHash(eventDefs, (eventDef: EventDef) => compileEventUi(eventDef, eventUiBases))
}
export function compileEventUi(eventDef: EventDef, eventUiBases: EventUiHash) {
let uis = []
if (eventUiBases['']) {
uis.push(eventUiBases[''])
}
if (eventUiBases[eventDef.defId]) {
uis.push(eventUiBases[eventDef.defId])
}
uis.push(eventDef.ui)
return combineEventUis(uis)
}
export function sortEventSegs(segs, eventOrderSpecs: OrderSpec<EventImpl>[]): Seg[] {
let objs = segs.map(buildSegCompareObj)
objs.sort((obj0, obj1) => compareByFieldSpecs(obj0, obj1, eventOrderSpecs))
return objs.map((c) => c._seg)
}
// returns a object with all primitive props that can be compared
export function buildSegCompareObj(seg: Seg) {
let { eventRange } = seg
let eventDef = eventRange.def
let range = eventRange.instance ? eventRange.instance.range : eventRange.range
let start = range.start ? range.start.valueOf() : 0 // TODO: better support for open-range events
let end = range.end ? range.end.valueOf() : 0 // "
return {
...eventDef.extendedProps,
...eventDef,
id: eventDef.publicId,
start,
end,
duration: end - start,
allDay: Number(eventDef.allDay),
_seg: seg, // for later retrieval
}
}
// other stuff
export interface EventContentArg { // for *Content handlers
event: EventImpl
timeText: string
backgroundColor: string // TODO: add other EventUi props?
borderColor: string //
textColor: string //
isDraggable: boolean
isStartResizable: boolean
isEndResizable: boolean
isMirror: boolean
isStart: boolean
isEnd: boolean
isPast: boolean
isFuture: boolean
isToday: boolean
isSelected: boolean
isDragging: boolean
isResizing: boolean
view: ViewApi // specifically for the API
}
export type EventMountArg = MountArg<EventContentArg>
export function computeSegDraggable(seg: Seg, context: ViewContext) {
let { pluginHooks } = context
let transformers = pluginHooks.isDraggableTransformers
let { def, ui } = seg.eventRange
let val = ui.startEditable
for (let transformer of transformers) {
val = transformer(val, def, ui, context)
}
return val
}
export function computeSegStartResizable(seg: Seg, context: ViewContext) {
return seg.isStart && seg.eventRange.ui.durationEditable && context.options.eventResizableFromStart
}
export function computeSegEndResizable(seg: Seg, context: ViewContext) {
return seg.isEnd && seg.eventRange.ui.durationEditable
}
export function buildSegTimeText(
seg: Seg,
timeFormat: DateFormatter,
context: ViewContext,
defaultDisplayEventTime?: boolean, // defaults to true
defaultDisplayEventEnd?: boolean, // defaults to true
startOverride?: DateMarker,
endOverride?: DateMarker,
) {
let { dateEnv, options } = context
let { displayEventTime, displayEventEnd } = options
let eventDef = seg.eventRange.def
let eventInstance = seg.eventRange.instance
if (displayEventTime == null) { displayEventTime = defaultDisplayEventTime !== false }
if (displayEventEnd == null) { displayEventEnd = defaultDisplayEventEnd !== false }
let wholeEventStart = eventInstance.range.start
let wholeEventEnd = eventInstance.range.end
let segStart = startOverride || seg.start || seg.eventRange.range.start
let segEnd = endOverride || seg.end || seg.eventRange.range.end
let isStartDay = startOfDay(wholeEventStart).valueOf() === startOfDay(segStart).valueOf()
let isEndDay = startOfDay(addMs(wholeEventEnd, -1)).valueOf() === startOfDay(addMs(segEnd, -1)).valueOf()
if (displayEventTime && !eventDef.allDay && (isStartDay || isEndDay)) {
segStart = isStartDay ? wholeEventStart : segStart
segEnd = isEndDay ? wholeEventEnd : segEnd
if (displayEventEnd && eventDef.hasEnd) {
return dateEnv.formatRange(segStart, segEnd, timeFormat, {
forcedStartTzo: startOverride ? null : eventInstance.forcedStartTzo, // nooooooooooooo, give tzo if same date
forcedEndTzo: endOverride ? null : eventInstance.forcedEndTzo,
})
}
return dateEnv.format(segStart, timeFormat, {
forcedTzo: startOverride ? null : eventInstance.forcedStartTzo, // nooooo, same
})
}
return ''
}
export function getSegMeta(seg: Seg, todayRange: DateRange, nowDate?: DateMarker) { // TODO: make arg order consistent with date util
let segRange = seg.eventRange.range
return {
isPast: segRange.end <= (nowDate || todayRange.start),
isFuture: segRange.start >= (nowDate || todayRange.end),
isToday: todayRange && rangeContainsMarker(todayRange, segRange.start),
}
}
export function getEventClassNames(props: EventContentArg) { // weird that we use this interface, but convenient
let classNames: string[] = ['fc-event']
if (props.isMirror) {
classNames.push('fc-event-mirror')
}
if (props.isDraggable) {
classNames.push('fc-event-draggable')
}
if (props.isStartResizable || props.isEndResizable) {
classNames.push('fc-event-resizable')
}
if (props.isDragging) {
classNames.push('fc-event-dragging')
}
if (props.isResizing) {
classNames.push('fc-event-resizing')
}
if (props.isSelected) {
classNames.push('fc-event-selected')
}
if (props.isStart) {
classNames.push('fc-event-start')
}
if (props.isEnd) {
classNames.push('fc-event-end')
}
if (props.isPast) {
classNames.push('fc-event-past')
}
if (props.isToday) {
classNames.push('fc-event-today')
}
if (props.isFuture) {
classNames.push('fc-event-future')
}
return classNames
}
export function buildEventRangeKey(eventRange: EventRenderRange) {
return eventRange.instance
? eventRange.instance.instanceId
: `${eventRange.def.defId}:${eventRange.range.start.toISOString()}`
// inverse-background events don't have specific instances. TODO: better solution
}
export function getSegAnchorAttrs(seg: Seg, context: ViewContext) {
let { def, instance } = seg.eventRange
let { url } = def
if (url) {
return { href: url }
}
let { emitter, options } = context
let { eventInteractive } = options
if (eventInteractive == null) {
eventInteractive = def.interactive
if (eventInteractive == null) {
eventInteractive = Boolean(emitter.hasHandlers('eventClick'))
}
}
// mock what happens in EventClicking
if (eventInteractive) {
// only attach keyboard-related handlers because click handler is already done in EventClicking
return createAriaKeyboardAttrs((ev: UIEvent) => {
emitter.trigger('eventClick', {
el: ev.target as HTMLElement,
event: new EventImpl(context, def, instance),
jsEvent: ev as MouseEvent,
view: context.viewApi,
})
})
}
return {}
}

View file

@ -1,184 +0,0 @@
import { EventStore, createEmptyEventStore } from '../structs/event-store.js'
import { EventDef } from '../structs/event-def.js'
import { EventInteractionState } from '../interactions/event-interaction-state.js'
import { mapHash } from '../util/object.js'
import { memoize } from '../util/memoize.js'
import { EventUiHash, EventUi, combineEventUis } from './event-ui.js'
import { DateSpan } from '../structs/date-span.js'
export interface SplittableProps {
businessHours: EventStore | null // is this really allowed to be null?
dateSelection: DateSpan | null
eventStore: EventStore
eventUiBases: EventUiHash
eventSelection: string
eventDrag: EventInteractionState | null
eventResize: EventInteractionState | null
}
const EMPTY_EVENT_STORE = createEmptyEventStore() // for purecomponents. TODO: keep elsewhere
export abstract class Splitter<PropsType extends SplittableProps = SplittableProps> {
private getKeysForEventDefs = memoize(this._getKeysForEventDefs)
private splitDateSelection = memoize(this._splitDateSpan)
private splitEventStore = memoize(this._splitEventStore)
private splitIndividualUi = memoize(this._splitIndividualUi)
private splitEventDrag = memoize(this._splitInteraction)
private splitEventResize = memoize(this._splitInteraction)
private eventUiBuilders = {} // TODO: typescript protection
abstract getKeyInfo(props: PropsType): { [key: string]: { ui?: EventUi, businessHours?: EventStore } }
abstract getKeysForDateSpan(dateSpan: DateSpan): string[]
abstract getKeysForEventDef(eventDef: EventDef): string[]
splitProps(props: PropsType): { [key: string]: SplittableProps } {
let keyInfos = this.getKeyInfo(props)
let defKeys = this.getKeysForEventDefs(props.eventStore)
let dateSelections = this.splitDateSelection(props.dateSelection)
let individualUi = this.splitIndividualUi(props.eventUiBases, defKeys) // the individual *bases*
let eventStores = this.splitEventStore(props.eventStore, defKeys)
let eventDrags = this.splitEventDrag(props.eventDrag)
let eventResizes = this.splitEventResize(props.eventResize)
let splitProps: { [key: string]: SplittableProps } = {}
this.eventUiBuilders = mapHash(keyInfos, (info, key) => this.eventUiBuilders[key] || memoize(buildEventUiForKey))
for (let key in keyInfos) {
let keyInfo = keyInfos[key]
let eventStore = eventStores[key] || EMPTY_EVENT_STORE
let buildEventUi = this.eventUiBuilders[key]
splitProps[key] = {
businessHours: keyInfo.businessHours || props.businessHours,
dateSelection: dateSelections[key] || null,
eventStore,
eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]),
eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '',
eventDrag: eventDrags[key] || null,
eventResize: eventResizes[key] || null,
}
}
return splitProps
}
private _splitDateSpan(dateSpan: DateSpan | null) {
let dateSpans = {}
if (dateSpan) {
let keys = this.getKeysForDateSpan(dateSpan)
for (let key of keys) {
dateSpans[key] = dateSpan
}
}
return dateSpans
}
private _getKeysForEventDefs(eventStore: EventStore) {
return mapHash(eventStore.defs, (eventDef: EventDef) => this.getKeysForEventDef(eventDef))
}
private _splitEventStore(eventStore: EventStore, defKeys): { [key: string]: EventStore } {
let { defs, instances } = eventStore
let splitStores = {}
for (let defId in defs) {
for (let key of defKeys[defId]) {
if (!splitStores[key]) {
splitStores[key] = createEmptyEventStore()
}
splitStores[key].defs[defId] = defs[defId]
}
}
for (let instanceId in instances) {
let instance = instances[instanceId]
for (let key of defKeys[instance.defId]) {
if (splitStores[key]) { // must have already been created
splitStores[key].instances[instanceId] = instance
}
}
}
return splitStores
}
private _splitIndividualUi(eventUiBases: EventUiHash, defKeys): { [key: string]: EventUiHash } {
let splitHashes: { [key: string]: EventUiHash } = {}
for (let defId in eventUiBases) {
if (defId) { // not the '' key
for (let key of defKeys[defId]) {
if (!splitHashes[key]) {
splitHashes[key] = {}
}
splitHashes[key][defId] = eventUiBases[defId]
}
}
}
return splitHashes
}
private _splitInteraction(interaction: EventInteractionState | null): { [key: string]: EventInteractionState } {
let splitStates: { [key: string]: EventInteractionState } = {}
if (interaction) {
let affectedStores = this._splitEventStore(
interaction.affectedEvents,
this._getKeysForEventDefs(interaction.affectedEvents), // can't use cached. might be events from other calendar
)
// can't rely on defKeys because event data is mutated
let mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents)
let mutatedStores = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId)
let populate = (key) => {
if (!splitStates[key]) {
splitStates[key] = {
affectedEvents: affectedStores[key] || EMPTY_EVENT_STORE,
mutatedEvents: mutatedStores[key] || EMPTY_EVENT_STORE,
isEvent: interaction.isEvent,
}
}
}
for (let key in affectedStores) {
populate(key)
}
for (let key in mutatedStores) {
populate(key)
}
}
return splitStates
}
}
function buildEventUiForKey(allUi: EventUi | null, eventUiForKey: EventUi | null, individualUi: EventUiHash | null) {
let baseParts = []
if (allUi) {
baseParts.push(allUi)
}
if (eventUiForKey) {
baseParts.push(eventUiForKey)
}
let stuff = {
'': combineEventUis(baseParts),
}
if (individualUi) {
Object.assign(stuff, individualUi)
}
return stuff
}

View file

@ -1,98 +0,0 @@
import { Constraint, AllowFunc, normalizeConstraint } from '../structs/constraint.js'
import { parseClassNames } from '../util/html.js'
import { CalendarContext } from '../CalendarContext.js'
import { RawOptionsFromRefiners, RefinedOptionsFromRefiners, identity, Identity } from '../options.js'
// TODO: better called "EventSettings" or "EventConfig"
// TODO: move this file into structs
// TODO: separate constraint/overlap/allow, because selection uses only that, not other props
export const EVENT_UI_REFINERS = {
display: String,
editable: Boolean,
startEditable: Boolean,
durationEditable: Boolean,
constraint: identity as Identity<any>, // Identity<ConstraintInput>, // circular reference. ts dies. event->constraint->event
overlap: identity as Identity<boolean>,
allow: identity as Identity<AllowFunc>,
className: parseClassNames, // will both end up as array of strings
classNames: parseClassNames, // "
color: String,
backgroundColor: String,
borderColor: String,
textColor: String,
}
const EMPTY_EVENT_UI: EventUi = {
display: null,
startEditable: null,
durationEditable: null,
constraints: [],
overlap: null,
allows: [],
backgroundColor: '',
borderColor: '',
textColor: '',
classNames: [],
}
type BuiltInEventUiRefiners = typeof EVENT_UI_REFINERS
interface EventUiRefiners extends BuiltInEventUiRefiners {
// to prevent circular reference (and give is the option for ambient modification for later)
}
export type EventUiInput = RawOptionsFromRefiners<Required<EventUiRefiners>> // Required hack
export type EventUiRefined = RefinedOptionsFromRefiners<Required<EventUiRefiners>> // Required hack
export interface EventUi {
display: string | null
startEditable: boolean | null
durationEditable: boolean | null
constraints: Constraint[]
overlap: boolean | null
allows: AllowFunc[] // crappy name to indicate plural
backgroundColor: string
borderColor: string
textColor: string,
classNames: string[]
}
export type EventUiHash = { [defId: string]: EventUi }
export function createEventUi(refined: EventUiRefined, context: CalendarContext): EventUi {
let constraint = normalizeConstraint(refined.constraint, context)
return {
display: refined.display || null,
startEditable: refined.startEditable != null ? refined.startEditable : refined.editable,
durationEditable: refined.durationEditable != null ? refined.durationEditable : refined.editable,
constraints: constraint != null ? [constraint] : [],
overlap: refined.overlap != null ? refined.overlap : null,
allows: refined.allow != null ? [refined.allow] : [],
backgroundColor: refined.backgroundColor || refined.color || '',
borderColor: refined.borderColor || refined.color || '',
textColor: refined.textColor || '',
classNames: (refined.className || []).concat(refined.classNames || []), // join singular and plural
}
}
// TODO: prevent against problems with <2 args!
export function combineEventUis(uis: EventUi[]): EventUi {
return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI)
}
function combineTwoEventUis(item0: EventUi, item1: EventUi): EventUi { // hash1 has higher precedence
return {
display: item1.display != null ? item1.display : item0.display,
startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable,
durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable,
constraints: item0.constraints.concat(item1.constraints),
overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap,
allows: item0.allows.concat(item1.allows),
backgroundColor: item1.backgroundColor || item0.backgroundColor,
borderColor: item1.borderColor || item0.borderColor,
textColor: item1.textColor || item0.textColor,
classNames: item0.classNames.concat(item1.classNames),
}
}

View file

@ -1,126 +0,0 @@
import { createElement, Component, FunctionalComponent, ComponentChildren } from '../preact.js'
import { ClassNamesGenerator } from '../common/render-hook.js'
import {
ContentInjector,
ContentGeneratorProps,
ElAttrsProps,
buildElAttrs,
ElProps,
ElAttrs,
} from './ContentInjector.js'
import { RenderId } from './RenderId.js'
import { setRef } from '../vdom-util.js'
/*
The `children` prop is a function that defines inner wrappers (ex: ResourceCell)
*/
export type ContentContainerProps<RenderProps> =
ElAttrsProps &
ContentGeneratorProps<RenderProps> & {
elTag?: string
classNameGenerator: ClassNamesGenerator<RenderProps> | undefined
didMount: ((renderProps: RenderProps & { el: HTMLElement }) => void) | undefined
willUnmount: ((renderProps: RenderProps & { el: HTMLElement }) => void) | undefined
children?: InnerContainerFunc<RenderProps>
}
export class ContentContainer<RenderProps> extends Component<ContentContainerProps<RenderProps>> {
static contextType = RenderId
didMountMisfire?: boolean
context: number
el: HTMLElement
InnerContent = InnerContentInjector.bind(undefined, this)
render() {
const { props } = this
const generatedClassNames = generateClassNames(props.classNameGenerator, props.renderProps)
if (props.children) {
const elAttrs = buildElAttrs(props, generatedClassNames, this.handleEl)
const children = props.children(this.InnerContent, props.renderProps, elAttrs)
if (props.elTag) {
return createElement(props.elTag, elAttrs, children)
} else {
return children
}
} else {
return createElement(ContentInjector<RenderProps>, {
...props,
elRef: this.handleEl,
elTag: props.elTag || 'div',
elClasses: (props.elClasses || []).concat(generatedClassNames),
renderId: this.context,
})
}
}
handleEl = (el: HTMLElement) => {
this.el = el
if (this.props.elRef) {
setRef(this.props.elRef, el)
if (el && this.didMountMisfire) {
this.componentDidMount()
}
}
}
componentDidMount(): void {
if (this.el) {
this.props.didMount?.({
...this.props.renderProps,
el: this.el,
})
} else {
this.didMountMisfire = true
}
}
componentWillUnmount(): void {
this.props.willUnmount?.({
...this.props.renderProps,
el: this.el,
})
}
}
// Inner
export type InnerContainerComponent = FunctionalComponent<ElProps>
export type InnerContainerFunc<RenderProps> = (
InnerContainer: InnerContainerComponent,
renderProps: RenderProps,
elAttrs: ElAttrs,
) => ComponentChildren
function InnerContentInjector<RenderProps>(
containerComponent: ContentContainer<RenderProps>,
props: ElProps,
) {
const parentProps = containerComponent.props
return createElement(ContentInjector<RenderProps>, {
renderProps: parentProps.renderProps,
generatorName: parentProps.generatorName,
customGenerator: parentProps.customGenerator,
defaultGenerator: parentProps.defaultGenerator,
renderId: containerComponent.context,
...props,
})
}
// Utils
function generateClassNames<RenderProps>(
classNameGenerator: ClassNamesGenerator<RenderProps> | undefined,
renderProps: RenderProps,
): string[] {
const classNames = typeof classNameGenerator === 'function' ?
classNameGenerator(renderProps) :
classNameGenerator || []
return typeof classNames === 'string' ? [classNames] : classNames
}

View file

@ -1,209 +0,0 @@
import { createElement, ComponentChild, JSX, Ref, isValidElement } from '../preact.js'
import { CustomContentGenerator } from '../common/render-hook.js'
import { BaseComponent, setRef } from '../vdom-util.js'
import { guid } from '../util/misc.js'
import { isArraysEqual } from '../util/array.js'
import { removeElement } from '../util/dom-manip.js'
import { ViewOptions } from '../options.js'
import { isNonHandlerPropsEqual, isPropsEqual } from '../util/object.js'
export type ElRef = Ref<HTMLElement>
export type ElAttrs = JSX.HTMLAttributes & JSX.SVGAttributes & { ref?: ElRef } & Record<string, any>
export interface ElAttrsProps {
elRef?: ElRef
elClasses?: string[]
elStyle?: JSX.CSSProperties
elAttrs?: ElAttrs
}
export interface ElProps extends ElAttrsProps {
elTag: string
}
export interface ContentGeneratorProps<RenderProps> {
renderProps: RenderProps
generatorName: string | undefined // for informing UI-framework if `customGenerator` is undefined
customGenerator?: CustomContentGenerator<RenderProps>
defaultGenerator?: (renderProps: RenderProps) => ComponentChild
}
export type ContentInjectorProps<RenderProps> =
ElProps &
ContentGeneratorProps<RenderProps> &
{ renderId: number }
export class ContentInjector<RenderProps> extends BaseComponent<ContentInjectorProps<RenderProps>> {
private id = guid()
private queuedDomNodes: Node[] = []
private currentDomNodes: Node[] = []
private currentGeneratorMeta: any
render() {
const { props, context } = this
const { options } = context
const { customGenerator, defaultGenerator, renderProps } = props
const attrs = buildElAttrs(props, [], this.handleEl)
let useDefault = false
let innerContent: ComponentChild | undefined
let queuedDomNodes: Node[] = []
let currentGeneratorMeta: any
if (customGenerator != null) {
const customGeneratorRes = typeof customGenerator === 'function' ?
customGenerator(renderProps, createElement) :
customGenerator
if (customGeneratorRes === true) {
useDefault = true
} else {
const isObject = customGeneratorRes && typeof customGeneratorRes === 'object' // non-null
if (isObject && ('html' in customGeneratorRes)) {
attrs.dangerouslySetInnerHTML = { __html: customGeneratorRes.html }
} else if (isObject && ('domNodes' in customGeneratorRes)) {
queuedDomNodes = Array.prototype.slice.call(customGeneratorRes.domNodes)
} else if (
isObject
? isValidElement(customGeneratorRes) // vdom node
: typeof customGeneratorRes !== 'function' // primitive value (like string or number)
) {
// use in vdom
innerContent = customGeneratorRes
} else {
// an exotic object for handleCustomRendering
currentGeneratorMeta = customGeneratorRes
}
}
} else {
useDefault = !hasCustomRenderingHandler(props.generatorName, options)
}
if (useDefault && defaultGenerator) {
innerContent = defaultGenerator(renderProps)
}
this.queuedDomNodes = queuedDomNodes
this.currentGeneratorMeta = currentGeneratorMeta
return createElement(props.elTag, attrs, innerContent)
}
componentDidMount(): void {
this.applyQueueudDomNodes()
this.triggerCustomRendering(true)
}
componentDidUpdate(): void {
this.applyQueueudDomNodes()
this.triggerCustomRendering(true)
}
componentWillUnmount(): void {
this.triggerCustomRendering(false) // TODO: different API for removal?
}
private triggerCustomRendering(isActive: boolean) {
const { props, context } = this
const { handleCustomRendering, customRenderingMetaMap } = context.options
if (handleCustomRendering) {
const generatorMeta =
this.currentGeneratorMeta ??
customRenderingMetaMap?.[props.generatorName]
if (generatorMeta) {
handleCustomRendering({
id: this.id,
isActive,
containerEl: this.base as HTMLElement,
reportNewContainerEl: this.updateElRef, // front-end framework tells us about new container els
generatorMeta,
...props,
elClasses: (props.elClasses || []).filter(isTruthy),
})
}
}
}
private handleEl = (el: HTMLElement | null) => {
const { options } = this.context
const { generatorName } = this.props
if (!options.customRenderingReplaces || !hasCustomRenderingHandler(generatorName, options)) {
this.updateElRef(el)
}
}
private updateElRef = (el: HTMLElement | null) => {
if (this.props.elRef) {
setRef(this.props.elRef, el)
}
}
private applyQueueudDomNodes() {
const { queuedDomNodes, currentDomNodes } = this
const el = this.base
if (!isArraysEqual(queuedDomNodes, currentDomNodes)) {
currentDomNodes.forEach(removeElement)
for (let newNode of queuedDomNodes) {
el.appendChild(newNode)
}
this.currentDomNodes = queuedDomNodes
}
}
}
ContentInjector.addPropsEquality({
elClasses: isArraysEqual,
elStyle: isPropsEqual,
elAttrs: isNonHandlerPropsEqual,
renderProps: isPropsEqual,
})
// Util
/*
Does UI-framework provide custom way of rendering that does not use Preact VDOM
AND does the calendar's options define custom rendering?
AKA. Should we NOT render the default content?
*/
export function hasCustomRenderingHandler(
generatorName: string | undefined,
options: ViewOptions,
): boolean {
return Boolean(
options.handleCustomRendering &&
generatorName &&
options.customRenderingMetaMap?.[generatorName],
)
}
export function buildElAttrs(
props: ElAttrsProps,
extraClassNames?: string[],
elRef?: ElRef,
): ElAttrs {
const attrs: ElAttrs = { ...props.elAttrs, ref: elRef as any }
if (props.elClasses || extraClassNames) {
attrs.className = (props.elClasses || [])
.concat(extraClassNames || [])
.concat((attrs.className as (string | undefined)) || [])
.filter(Boolean)
.join(' ')
}
if (props.elStyle) {
attrs.style = props.elStyle
}
return attrs
}
function isTruthy(val: any): boolean {
return Boolean(val)
}

View file

@ -1,39 +0,0 @@
import { Store } from './Store.js'
import { ElProps } from './ContentInjector.js'
export type CustomRenderingHandler<RenderProps> = (customRender: CustomRendering<RenderProps>) => void
export interface CustomRendering<RenderProps> extends ElProps {
id: string // TODO: need this? Map can be responsible for storing key?
isActive: boolean
containerEl: HTMLElement
reportNewContainerEl: (el: HTMLElement | null) => void
generatorName: string
generatorMeta: any // could be as simple as boolean
renderProps: RenderProps
}
/*
Subscribers will get a LIST of CustomRenderings
*/
export class CustomRenderingStore<RenderProps> extends Store<Map<string, CustomRendering<RenderProps>>> {
private map = new Map<string, CustomRendering<RenderProps>>()
// for consistent order
handle(customRendering: CustomRendering<RenderProps>): void {
const { map } = this
let updated = false
if (customRendering.isActive) {
map.set(customRendering.id, customRendering as CustomRendering<RenderProps>)
updated = true
} else if (map.has(customRendering.id)) {
map.delete(customRendering.id)
updated = true
}
if (updated) {
this.set(map)
}
}
}

View file

@ -1,3 +0,0 @@
import { createContext } from '../preact.js'
export const RenderId = createContext<number>(0)

View file

@ -1,21 +0,0 @@
export class Store<Value> {
private handlers: ((value: Value) => void)[] = []
private currentValue: Value | undefined
set(value: Value): void {
this.currentValue = value
for (let handler of this.handlers) {
handler(value)
}
}
subscribe(handler: (value: Value) => void) {
this.handlers.push(handler)
if (this.currentValue !== undefined) {
handler(this.currentValue)
}
}
}

View file

@ -1,50 +0,0 @@
import { DateMarker } from './marker.js'
import { CalendarSystem } from './calendar-system.js'
import { Locale } from './locale.js'
import { ZonedMarker, ExpandedZonedMarker, expandZonedMarker } from './zoned-marker.js'
export interface VerboseFormattingArg {
date: ExpandedZonedMarker
start: ExpandedZonedMarker
end?: ExpandedZonedMarker
timeZone: string
localeCodes: string[],
defaultSeparator: string
}
export function createVerboseFormattingArg(
start: ZonedMarker,
end: ZonedMarker,
context: DateFormattingContext,
betterDefaultSeparator?: string,
): VerboseFormattingArg {
let startInfo = expandZonedMarker(start, context.calendarSystem)
let endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null
return {
date: startInfo,
start: startInfo,
end: endInfo,
timeZone: context.timeZone,
localeCodes: context.locale.codes,
defaultSeparator: betterDefaultSeparator || context.defaultSeparator,
}
}
export type CmdFormatterFunc = (cmd: string, arg: VerboseFormattingArg) => string
export interface DateFormattingContext {
timeZone: string,
locale: Locale,
calendarSystem: CalendarSystem
computeWeekNumber: (d: DateMarker) => number
weekText: string
weekTextLong: string
cmdFormatter?: CmdFormatterFunc
defaultSeparator: string
}
export interface DateFormatter {
format(date: ZonedMarker, context: DateFormattingContext): string
formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext, betterDefaultSeparator?: string): string
}

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