mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-04 19:11:57 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,625 @@
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { isSameDay, isSameMonth, isToday } from 'date-fns';
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
} from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { arrowLeftIcon } from './icons.js';
|
||||
import { datePickerStyle } from './style.js';
|
||||
import { clamp, getMonthMatrix, toDate } from './utils.js';
|
||||
|
||||
const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
export interface DateCell {
|
||||
date: Date;
|
||||
label: string;
|
||||
isToday: boolean;
|
||||
notCurrentMonth: boolean;
|
||||
selected?: boolean;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
type NavActionArg = {
|
||||
action: () => void;
|
||||
disable?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Date picker
|
||||
*/
|
||||
export class DatePicker extends WithDisposable(LitElement) {
|
||||
static override styles = datePickerStyle;
|
||||
|
||||
/** current active month */
|
||||
private _cursor = new Date();
|
||||
|
||||
private _maxYear = 2099;
|
||||
|
||||
private _minYear = 1970;
|
||||
|
||||
get _cardStyle() {
|
||||
return {
|
||||
'--cell-size': `${this.size}px`,
|
||||
'--gap-h': `${this.gapH}px`,
|
||||
'--gap-v': `${this.gapV}px`,
|
||||
'min-width': `${this.cardWidth}px`,
|
||||
'min-height': `${this.cardHeight}px`,
|
||||
padding: `${this.padding}px`,
|
||||
};
|
||||
}
|
||||
|
||||
get cardHeight() {
|
||||
const rowNum = 7;
|
||||
return this.size * rowNum + this.padding * 2 + this.gapV * (rowNum - 1) - 2;
|
||||
}
|
||||
|
||||
get cardWidth() {
|
||||
const colNum = 7;
|
||||
return this.size * colNum + this.padding * 2 + this.gapH * (colNum - 1);
|
||||
}
|
||||
|
||||
get date() {
|
||||
return this._cursor.getDate();
|
||||
}
|
||||
|
||||
get day() {
|
||||
return this._cursor.getDay();
|
||||
}
|
||||
|
||||
get dayLabel() {
|
||||
return days[this.day];
|
||||
}
|
||||
|
||||
get minHeight() {
|
||||
const rowNum = this._matrix.length;
|
||||
return this.size * rowNum + this.padding * 2 + this.gapV * (rowNum - 1) - 2;
|
||||
}
|
||||
|
||||
get month() {
|
||||
return this._cursor.getMonth();
|
||||
}
|
||||
|
||||
get monthLabel() {
|
||||
return months[this.month];
|
||||
}
|
||||
|
||||
get year() {
|
||||
return this._cursor.getFullYear();
|
||||
}
|
||||
|
||||
get yearLabel() {
|
||||
return this.year;
|
||||
}
|
||||
|
||||
/** Cell */
|
||||
private _cellRenderer(cell: DateCell) {
|
||||
const classes = classMap({
|
||||
interactive: true,
|
||||
'date-cell': true,
|
||||
'date-cell--today': cell.isToday,
|
||||
'date-cell--not-curr-month': cell.notCurrentMonth,
|
||||
'date-cell--selected': !!cell.selected,
|
||||
});
|
||||
const dateRaw = `${cell.date.getFullYear()}-${cell.date.getMonth()}-${cell.date.getDate()}(${cell.date.getDay()})`;
|
||||
return html`<button
|
||||
tabindex=${cell.tabIndex ?? -1}
|
||||
aria-label=${dateRaw}
|
||||
data-date=${dateRaw}
|
||||
class=${classes}
|
||||
@click=${() => {
|
||||
this._onChange(cell.date);
|
||||
}}
|
||||
>
|
||||
${cell.label}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
private _dateContent() {
|
||||
return html` <div class="date-picker-header">
|
||||
<div class="date-picker-header__buttons">
|
||||
<button
|
||||
class="date-picker-header__date interactive"
|
||||
@click=${() => this.toggleMonthSelector()}
|
||||
>
|
||||
<div>${this.monthLabel}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="date-picker-header__date interactive"
|
||||
@click=${() => this.toggleYearSelector()}
|
||||
>
|
||||
<div>${this.yearLabel}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${this._navAction(
|
||||
() => this._moveMonth(-1),
|
||||
() => this._moveMonth(1),
|
||||
html`<button
|
||||
tabindex="0"
|
||||
aria-label="today"
|
||||
class="action-label interactive today"
|
||||
@click=${() => {
|
||||
this._onChange(new Date());
|
||||
}}
|
||||
>
|
||||
<span>TODAY</span>
|
||||
</button>`
|
||||
)}
|
||||
</div>
|
||||
${this._dayHeaderRenderer()}
|
||||
<div class="date-picker-weeks">
|
||||
${this._matrix.map(
|
||||
week =>
|
||||
html`<div class="date-picker-week">
|
||||
${week.map(cell => this._cellRenderer(cell))}
|
||||
</div>`
|
||||
)}
|
||||
</div>
|
||||
${this.onClear
|
||||
? html`<div class="date-picker-footer">
|
||||
<button
|
||||
tabindex="0"
|
||||
aria-label="clear"
|
||||
class="footer-button interactive"
|
||||
@click=${() => this.onClear?.()}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>`
|
||||
: nothing}`;
|
||||
}
|
||||
|
||||
/** Week header */
|
||||
private _dayHeaderRenderer() {
|
||||
return html`<div class="days-header">
|
||||
${days.map(day => html`<div class="date-cell">${day}</div>`)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _getMatrix() {
|
||||
this._matrix = getMonthMatrix(this._cursor).map(row => {
|
||||
return row.map(date => {
|
||||
const tabIndex = isSameDay(date, this._cursor) ? 0 : -1;
|
||||
return {
|
||||
date,
|
||||
label: date.getDate().toString(),
|
||||
isToday: isToday(date),
|
||||
notCurrentMonth: !isSameMonth(date, this._cursor),
|
||||
selected: this.value ? isSameDay(date, toDate(this.value)) : false,
|
||||
tabIndex,
|
||||
} satisfies DateCell;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _getYearMatrix() {
|
||||
// every decade has 12 years
|
||||
const no = Math.floor((this._yearCursor - this._minYear) / 12);
|
||||
const decade = no * 12;
|
||||
const start = this._minYear + decade;
|
||||
const end = start + 12;
|
||||
this._yearMatrix = Array.from(
|
||||
{ length: end - start },
|
||||
(_, i) => start + i
|
||||
).filter(v => v >= this._minYear && v <= this._maxYear);
|
||||
}
|
||||
|
||||
private _modeDecade(offset: number) {
|
||||
this._yearCursor = clamp(
|
||||
this._minYear,
|
||||
this._maxYear,
|
||||
this._yearCursor + offset
|
||||
);
|
||||
this._getYearMatrix();
|
||||
}
|
||||
|
||||
private _monthContent() {
|
||||
return html` <div class="date-picker-header">
|
||||
<button
|
||||
class="date-picker-header__date interactive"
|
||||
@click=${() => this.toggleMonthSelector()}
|
||||
>
|
||||
<div>${this._monthPickYearCursor}</div>
|
||||
</button>
|
||||
|
||||
${this._navAction(
|
||||
{
|
||||
action: () => this._monthPickYearCursor--,
|
||||
disable: this._monthPickYearCursor <= this._minYear,
|
||||
},
|
||||
{
|
||||
action: () => this._monthPickYearCursor++,
|
||||
disable: this._monthPickYearCursor >= this._maxYear,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div class="date-picker-month">
|
||||
${months.map((month, index) => {
|
||||
const isActive = this.value
|
||||
? isSameMonth(
|
||||
this.value,
|
||||
new Date(this._monthPickYearCursor, index, 1)
|
||||
)
|
||||
: false;
|
||||
const classes = classMap({
|
||||
'month-cell': true,
|
||||
interactive: true,
|
||||
active: isActive,
|
||||
});
|
||||
return html`<button
|
||||
tabindex=${this._monthCursor === index ? 0 : -1}
|
||||
aria-label=${month}
|
||||
class=${classes}
|
||||
@click=${() => {
|
||||
this._cursor.setMonth(index);
|
||||
this._cursor.setFullYear(this._monthPickYearCursor);
|
||||
this._mode = 'date';
|
||||
this._getMatrix();
|
||||
}}
|
||||
>
|
||||
${month}
|
||||
</button>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _moveMonth(offset: number) {
|
||||
this._cursor.setMonth(this._cursor.getMonth() + offset);
|
||||
this._getMatrix();
|
||||
}
|
||||
|
||||
/** Actions */
|
||||
private _navAction(
|
||||
prev: NavActionArg | NavActionArg['action'],
|
||||
curr: NavActionArg | NavActionArg['action'],
|
||||
slot?: TemplateResult
|
||||
) {
|
||||
const onPrev = typeof prev === 'function' ? prev : prev.action;
|
||||
const onNext = typeof curr === 'function' ? curr : curr.action;
|
||||
const prevDisable = typeof prev === 'function' ? false : prev.disable;
|
||||
const nextDisable = typeof curr === 'function' ? false : curr.disable;
|
||||
const classes = classMap({
|
||||
'date-picker-header__action': true,
|
||||
'with-slot': !!slot,
|
||||
});
|
||||
return html`<div class=${classes}>
|
||||
<button
|
||||
aria-label="previous month"
|
||||
class="date-picker-small-action interactive left"
|
||||
@click=${onPrev}
|
||||
?disabled=${prevDisable}
|
||||
>
|
||||
${arrowLeftIcon}
|
||||
</button>
|
||||
${slot ?? nothing}
|
||||
<button
|
||||
aria-label="next month"
|
||||
class="date-picker-small-action interactive right"
|
||||
@click=${onNext}
|
||||
?disabled=${nextDisable}
|
||||
>
|
||||
${arrowLeftIcon}
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _onChange(date: Date, emit = true) {
|
||||
this._cursor = date;
|
||||
this.value = date.getTime();
|
||||
this._getMatrix();
|
||||
emit && this.onChange?.(date);
|
||||
}
|
||||
|
||||
private _switchMode<T>(map: Record<typeof this._mode, T>) {
|
||||
return (map[this._mode] as T) ?? nothing;
|
||||
}
|
||||
|
||||
private _yearContent() {
|
||||
const startYear = this._yearMatrix[0];
|
||||
const endYear = this._yearMatrix[this._yearMatrix.length - 1];
|
||||
return html`<div class="date-picker-header">
|
||||
<button
|
||||
class="date-picker-header__date interactive"
|
||||
@click=${() => this.toggleYearSelector()}
|
||||
>
|
||||
<div>${startYear}-${endYear}</div>
|
||||
</button>
|
||||
${this._navAction(
|
||||
{
|
||||
action: () => this._modeDecade(-12),
|
||||
disable: startYear <= this._minYear,
|
||||
},
|
||||
{
|
||||
action: () => this._modeDecade(12),
|
||||
disable: endYear >= this._maxYear,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div class="date-picker-year">
|
||||
${this._yearMatrix.map(year => {
|
||||
const isActive = year === this._cursor.getFullYear();
|
||||
const classes = classMap({
|
||||
'year-cell': true,
|
||||
interactive: true,
|
||||
active: isActive,
|
||||
});
|
||||
return html`<button
|
||||
tabindex=${this._yearCursor === year ? 0 : -1}
|
||||
aria-label=${year}
|
||||
class=${classes}
|
||||
@click=${() => {
|
||||
this._cursor.setFullYear(year);
|
||||
this._mode = 'date';
|
||||
this._getMatrix();
|
||||
}}
|
||||
>
|
||||
${year}
|
||||
</button>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
closeMonthSelector() {
|
||||
this._mode = 'date';
|
||||
}
|
||||
|
||||
closeYearSelector() {
|
||||
this._mode = 'date';
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.value) this._cursor = toDate(this.value);
|
||||
this._getMatrix();
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
this._disposables.addFromEvent(
|
||||
this,
|
||||
'keydown',
|
||||
e => {
|
||||
e.stopPropagation();
|
||||
const directions = new Set([
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
]);
|
||||
if (directions.has(e.key) && this.isDateCellFocused()) {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.key === 'ArrowLeft') {
|
||||
this._cursor.setDate(this._cursor.getDate() - 1);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
this._cursor.setDate(this._cursor.getDate() + 1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
this._cursor.setDate(this._cursor.getDate() - 7);
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
this._cursor.setDate(this._cursor.getDate() + 7);
|
||||
}
|
||||
this._getMatrix();
|
||||
setTimeout(this.focusDateCell.bind(this));
|
||||
}
|
||||
|
||||
if (directions.has(e.key) && this.isMonthCellFocused()) {
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowLeft') {
|
||||
this._monthCursor = (this._monthCursor - 1 + 12) % 12;
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
this._monthCursor = (this._monthCursor + 1) % 12;
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
this._monthCursor = (this._monthCursor - 3 + 12) % 12;
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
this._monthCursor = (this._monthCursor + 3) % 12;
|
||||
}
|
||||
setTimeout(this.focusMonthCell.bind(this));
|
||||
}
|
||||
|
||||
if (directions.has(e.key) && this.isYearCellFocused()) {
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowLeft') {
|
||||
this._modeDecade(-1);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
this._modeDecade(1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
this._modeDecade(-3);
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
this._modeDecade(3);
|
||||
}
|
||||
setTimeout(this.focusYearCell.bind(this));
|
||||
}
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
setTimeout(() => {
|
||||
const focused = this.shadowRoot?.activeElement as HTMLElement;
|
||||
const firstEl = this.shadowRoot?.querySelector('button');
|
||||
|
||||
// check if focus the last element, then focus the first element
|
||||
if (!e.shiftKey && !focused) firstEl?.focus();
|
||||
// check if focused element is inside current date-picker
|
||||
if (e.shiftKey && !this.shadowRoot?.contains(focused))
|
||||
this.focusDateCell();
|
||||
});
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
this.onEscape?.(toDate(this.value));
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus on date-cell
|
||||
*/
|
||||
focusDateCell() {
|
||||
const lastEl = this.shadowRoot?.querySelector(
|
||||
'button.date-cell[tabindex="0"]'
|
||||
) as HTMLElement;
|
||||
lastEl?.focus();
|
||||
}
|
||||
|
||||
focusMonthCell() {
|
||||
const lastEl = this.shadowRoot?.querySelector(
|
||||
'button.month-cell[tabindex="0"]'
|
||||
) as HTMLElement;
|
||||
lastEl?.focus();
|
||||
}
|
||||
|
||||
focusYearCell() {
|
||||
const lastEl = this.shadowRoot?.querySelector(
|
||||
'button.year-cell[tabindex="0"]'
|
||||
) as HTMLElement;
|
||||
lastEl?.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* check if date-cell is focused
|
||||
* @returns
|
||||
*/
|
||||
isDateCellFocused() {
|
||||
const focused = this.shadowRoot?.activeElement as HTMLElement;
|
||||
return focused?.classList.contains('date-cell');
|
||||
}
|
||||
|
||||
isMonthCellFocused() {
|
||||
const focused = this.shadowRoot?.activeElement as HTMLElement;
|
||||
return focused?.classList.contains('month-cell');
|
||||
}
|
||||
|
||||
isYearCellFocused() {
|
||||
const focused = this.shadowRoot?.activeElement as HTMLElement;
|
||||
return focused?.classList.contains('year-cell');
|
||||
}
|
||||
|
||||
openMonthSelector() {
|
||||
this._monthCursor = this.month;
|
||||
this._monthPickYearCursor = this.year;
|
||||
this._mode = 'month';
|
||||
}
|
||||
|
||||
openYearSelector() {
|
||||
this._yearCursor = clamp(this._minYear, this._maxYear, this.year);
|
||||
this._mode = 'year';
|
||||
this._getYearMatrix();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const classes = classMap({
|
||||
'date-picker': true,
|
||||
[`date-picker--mode-${this._mode}`]: true,
|
||||
popup: this.popup,
|
||||
});
|
||||
const wrapperStyle = styleMap({
|
||||
'min-height': `${this.minHeight}px`,
|
||||
});
|
||||
return html`<div style=${wrapperStyle} class="date-picker-height-wrapper">
|
||||
<div class=${classes} style=${styleMap(this._cardStyle)}>
|
||||
${this._switchMode({
|
||||
date: this._dateContent(),
|
||||
month: this._monthContent(),
|
||||
year: this._yearContent(),
|
||||
})}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
toggleMonthSelector() {
|
||||
if (this._mode === 'month') this.closeMonthSelector();
|
||||
else this.openMonthSelector();
|
||||
}
|
||||
|
||||
toggleYearSelector() {
|
||||
if (this._mode === 'year') this.closeYearSelector();
|
||||
else this.openYearSelector();
|
||||
}
|
||||
|
||||
override updated(_changedProperties: PropertyValues): void {
|
||||
if (_changedProperties.has('value')) {
|
||||
// this._getMatrix();
|
||||
if (this.value) this._onChange(toDate(this.value), false);
|
||||
else this._getMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
/** date matrix */
|
||||
@property({ attribute: false })
|
||||
private accessor _matrix: DateCell[][] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
private accessor _mode: 'date' | 'month' | 'year' = 'date';
|
||||
|
||||
/** web-accessibility for month select */
|
||||
@property({ attribute: false })
|
||||
private accessor _monthCursor = 0;
|
||||
|
||||
@property({ attribute: false })
|
||||
private accessor _monthPickYearCursor = 0;
|
||||
|
||||
@property({ attribute: false })
|
||||
private accessor _yearCursor = 0;
|
||||
|
||||
@property({ attribute: false })
|
||||
private accessor _yearMatrix: number[] = [];
|
||||
|
||||
/** horizontal gap between cells in px */
|
||||
@property({ type: Number })
|
||||
accessor gapH = 10;
|
||||
|
||||
/** vertical gap between cells in px */
|
||||
@property({ type: Number })
|
||||
accessor gapV = 8;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onChange: ((value: Date) => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onClear: (() => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onEscape: ((value: Date) => void) | undefined = undefined;
|
||||
|
||||
/** card padding in px */
|
||||
@property({ type: Number })
|
||||
accessor padding = 20;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor popup: boolean = false;
|
||||
|
||||
/** cell size in px */
|
||||
@property({ type: Number })
|
||||
accessor size = 28;
|
||||
|
||||
/** Checked date timestamp */
|
||||
@property({ type: Number })
|
||||
accessor value: number | undefined = undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'date-picker': DatePicker;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { svg } from 'lit';
|
||||
|
||||
export const arrowLeftIcon = svg`<svg width="7" height="10" viewBox="0 0 7 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.10861 0.391387C6.35269 0.635464 6.35269 1.03119 6.10861 1.27527L2.38388 5L6.10861 8.72472C6.35269 8.9688 6.35269 9.36453 6.10861 9.6086C5.86453 9.85268 5.4688 9.85268 5.22473 9.6086L1.05806 5.44194C0.813981 5.19786 0.813981 4.80213 1.05806 4.55805L5.22473 0.391387C5.4688 0.147309 5.86453 0.147309 6.10861 0.391387Z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DatePicker } from './date-picker.js';
|
||||
|
||||
export * from './date-picker.js';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('date-picker', DatePicker);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import { css } from 'lit';
|
||||
|
||||
export const datePickerStyle = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
gap: var(--gap-v);
|
||||
font-family: var(--affine-font-family);
|
||||
}
|
||||
|
||||
.popup.date-picker {
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--affine-menu-shadow);
|
||||
}
|
||||
|
||||
/* small action */
|
||||
|
||||
.date-picker-small-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.interactive.date-picker-small-action,
|
||||
.interactive.action-label.today {
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
|
||||
.date-picker-small-action:hover {
|
||||
color: var(--affine-icon-hover-color);
|
||||
background: var(--affine-icon-hover-background);
|
||||
}
|
||||
|
||||
.date-picker-small-action.left > svg {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.date-picker-small-action.right > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.date-picker-small-action.down > svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* action-header */
|
||||
|
||||
.date-picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.date-picker-header__buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.date-picker-header__date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-weight: 600;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.date-picker-header__date > div {
|
||||
padding: 0px 4px;
|
||||
}
|
||||
|
||||
.date-picker-header__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
|
||||
.date-picker-header__action.with-slot {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.date-picker-header__action .action-label {
|
||||
font-size: 12px;
|
||||
padding: 0px 4px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.23s ease;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.date-picker-header__action .action-label > span {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/** days header */
|
||||
|
||||
.days-header {
|
||||
display: flex;
|
||||
gap: var(--gap-h);
|
||||
}
|
||||
|
||||
.days-header > div {
|
||||
color: var(--affine-text-secondary-color);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/** week */
|
||||
|
||||
.date-picker-weeks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-v);
|
||||
}
|
||||
|
||||
.date-picker-week {
|
||||
display: flex;
|
||||
gap: var(--gap-h);
|
||||
}
|
||||
|
||||
/** cell */
|
||||
|
||||
.date-cell {
|
||||
width: var(--cell-size);
|
||||
height: var(--cell-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.date-cell[data-date] {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.date-cell.date-cell--not-curr-month {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.date-cell.date-cell--today {
|
||||
color: var(--affine-primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.date-cell.date-cell--selected {
|
||||
background: var(--affine-primary-color);
|
||||
color: var(--affine-pure-white);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/** interactive */
|
||||
|
||||
.interactive {
|
||||
cursor: pointer;
|
||||
/* transition:
|
||||
background 0.23s ease,
|
||||
color 0.23s ease; */
|
||||
user-select: none;
|
||||
position: relative;
|
||||
border: none;
|
||||
background-color: unset;
|
||||
font-family: var(--affine-font-family);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
/* --hover */
|
||||
|
||||
.interactive::after,
|
||||
.interactive::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
transition: background 0.23s ease;
|
||||
}
|
||||
|
||||
.interactive::after {
|
||||
opacity: 1;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.interactive:hover::after {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
/* --focus */
|
||||
|
||||
.interactive::before {
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
box-shadow: 0 0 0 3px var(--affine-primary-color);
|
||||
}
|
||||
|
||||
/* .interactive:active, */
|
||||
|
||||
.interactive:focus-visible {
|
||||
outline: none;
|
||||
outline: 1px solid var(--affine-primary-color);
|
||||
}
|
||||
|
||||
/* .interactive:active::before, */
|
||||
|
||||
.interactive:focus-visible::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/** disabled */
|
||||
|
||||
.interactive[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/** Month Select */
|
||||
|
||||
.date-picker-month {
|
||||
--btn-width: 36px;
|
||||
}
|
||||
|
||||
.date-picker-year {
|
||||
--btn-width: 46px;
|
||||
}
|
||||
|
||||
.date-picker-month,
|
||||
.date-picker-year {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, var(--btn-width));
|
||||
gap: 18px 32px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.date-picker-month button,
|
||||
.date-picker-year button {
|
||||
height: 34px;
|
||||
width: fit-content;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--btn-width);
|
||||
}
|
||||
|
||||
.date-picker-month button.active,
|
||||
.date-picker-year button.active {
|
||||
color: var(--affine-primary-color);
|
||||
/* background: var(--affine-primary-color); */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.date-picker .date-picker-header {
|
||||
padding: 0px;
|
||||
transition: padding 0.23s ease;
|
||||
}
|
||||
|
||||
.date-picker--mode-month,
|
||||
.date-picker--mode-year {
|
||||
gap: 26px;
|
||||
}
|
||||
|
||||
.date-picker--mode-month .date-picker-header,
|
||||
.date-picker--mode-year .date-picker-header {
|
||||
/* padding: 0 10px; */
|
||||
}
|
||||
|
||||
.date-picker-footer {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--affine-border-color);
|
||||
}
|
||||
|
||||
.footer-button {
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
color: var(--affine-text-secondary-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--affine-font-sm);
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.footer-button:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,73 @@
|
||||
export type MaybeDate = Date | string | number | undefined;
|
||||
|
||||
/**
|
||||
* @deprecated should not use raw string to represent timestamp
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
function _isTimestampString(str: string) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given date to Date object
|
||||
* @param date
|
||||
* @returns
|
||||
*/
|
||||
export function toDate(date?: MaybeDate) {
|
||||
// TODO: handle invalid date
|
||||
if (date instanceof Date) return date;
|
||||
if (typeof date === 'string' && _isTimestampString(date)) date = +date;
|
||||
return date ? new Date(date) : new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* get the first day of the month of the given date
|
||||
* @param maybeDate
|
||||
*/
|
||||
export function getFirstDayOfMonth(maybeDate: MaybeDate) {
|
||||
const date = toDate(maybeDate);
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the last day of the month of the given date
|
||||
* @param maybeDate
|
||||
* @example
|
||||
* getLastDayOfMonth('2021-01-01') // 2021-01-31
|
||||
*/
|
||||
export function getLastDayOfMonth(maybeDate: MaybeDate) {
|
||||
const date = toDate(maybeDate);
|
||||
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
||||
}
|
||||
|
||||
export function getMonthMatrix(maybeDate: MaybeDate) {
|
||||
const date = toDate(maybeDate);
|
||||
const firstDayOfMonth = getFirstDayOfMonth(date);
|
||||
const lastDayOfMonth = getLastDayOfMonth(date);
|
||||
const firstDayOfFirstWeek = new Date(firstDayOfMonth);
|
||||
firstDayOfFirstWeek.setDate(
|
||||
firstDayOfMonth.getDate() - firstDayOfMonth.getDay()
|
||||
);
|
||||
const lastDayOfLastWeek = new Date(lastDayOfMonth);
|
||||
lastDayOfLastWeek.setDate(
|
||||
lastDayOfMonth.getDate() + (6 - lastDayOfMonth.getDay())
|
||||
);
|
||||
const matrix = [];
|
||||
let week = [];
|
||||
const day = new Date(firstDayOfFirstWeek);
|
||||
while (day <= lastDayOfLastWeek) {
|
||||
week.push(new Date(day));
|
||||
if (week.length === 7) {
|
||||
matrix.push(week);
|
||||
week = [];
|
||||
}
|
||||
day.setDate(day.getDate() + 1);
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
export function clamp(num1: number, num2: number, value: number) {
|
||||
const [min, max] = [num1, num2].sort();
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
Reference in New Issue
Block a user