Publii: update content
This commit is contained in:
parent
c4b02d2305
commit
cace223aaf
1143 changed files with 20 additions and 87432 deletions
2
404.html
2
404.html
|
@ -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] {
|
<!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;
|
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 calendarEl = document.getElementById('calendar');
|
||||||
|
|
||||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
|
|
@ -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] {
|
<!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;
|
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 calendarEl = document.getElementById('calendar');
|
||||||
|
|
||||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
|
|
@ -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] {
|
<!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;
|
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 calendarEl = document.getElementById('calendar');
|
||||||
|
|
||||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
|
|
@ -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
|
|
|
@ -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.
|
|
|
@ -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
|
|
|
@ -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.
|
|
49
fullcalendar-main/.github/workflows/ci.yml
vendored
49
fullcalendar-main/.github/workflows/ci.yml
vendored
|
@ -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
|
|
11
fullcalendar-main/.gitignore
vendored
11
fullcalendar-main/.gitignore
vendored
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
# Package manager
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# Generated
|
|
||||||
tsconfig.json
|
|
||||||
tsconfig.tsbuildinfo
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Monorepo
|
|
||||||
.turbo
|
|
|
@ -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
|
|
5
fullcalendar-main/.vscode/extensions.json
vendored
5
fullcalendar-main/.vscode/extensions.json
vendored
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"dbaeumer.vscode-eslint"
|
|
||||||
]
|
|
||||||
}
|
|
5
fullcalendar-main/.vscode/settings.json
vendored
5
fullcalendar-main/.vscode/settings.json
vendored
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": true
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -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.
|
|
|
@ -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 »](CONTRIBUTING.md)
|
|
|
@ -1,4 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'),
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
})
|
|
||||||
```
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'
|
|
|
@ -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
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'),
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
```
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 }
|
|
|
@ -1,12 +0,0 @@
|
||||||
|
|
||||||
.fc-theme-bootstrap {
|
|
||||||
|
|
||||||
& a:not([href]) {
|
|
||||||
color: inherit; // natural color for navlinks
|
|
||||||
}
|
|
||||||
|
|
||||||
& .fc-more-link:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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'
|
|
|
@ -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
|
|
|
@ -1,3 +0,0 @@
|
||||||
import './index.css'
|
|
||||||
|
|
||||||
export { BootstrapTheme } from './BootstrapTheme.js'
|
|
|
@ -1,4 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'),
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
```
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 }
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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'
|
|
|
@ -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
|
|
|
@ -1,3 +0,0 @@
|
||||||
import './index.css'
|
|
||||||
|
|
||||||
export { BootstrapTheme } from './BootstrapTheme.js'
|
|
|
@ -1,4 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'),
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
```
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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())
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 }
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
|
|
||||||
export interface EventSourceApi {
|
|
||||||
id: string
|
|
||||||
url: string
|
|
||||||
format: string
|
|
||||||
remove(): void
|
|
||||||
refetch(): void
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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> </Fragment>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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',
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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}`} />
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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 }
|
|
||||||
}
|
|
|
@ -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)
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 {}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { createContext } from '../preact.js'
|
|
||||||
|
|
||||||
export const RenderId = createContext<number>(0)
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
Loading…
Reference in a new issue