refactor: daily notes (#1901)
This commit is contained in:
@@ -3,7 +3,7 @@ import { CalendarActivity } from './CalendarActivity'
|
|||||||
import CalendarDay from './CalendarDay'
|
import CalendarDay from './CalendarDay'
|
||||||
import { CalendarDays, CalendarDaysLeap, CalendarDaysOfTheWeek } from './Constants'
|
import { CalendarDays, CalendarDaysLeap, CalendarDaysOfTheWeek } from './Constants'
|
||||||
import { createActivityRecord, dateToDateOnlyString, isLeapYear, getStartDayOfMonth } from './CalendarUtilts'
|
import { createActivityRecord, dateToDateOnlyString, isLeapYear, getStartDayOfMonth } from './CalendarUtilts'
|
||||||
import { isDateInSameDay } from '@/Utils/DateUtils'
|
import { areDatesInSameDay } from '@/Utils/DateUtils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activities: CalendarActivity[]
|
activities: CalendarActivity[]
|
||||||
@@ -29,10 +29,11 @@ const Calendar: FunctionComponent<Props> = ({ activities, startDate, onDateSelec
|
|||||||
}, [startDate])
|
}, [startDate])
|
||||||
|
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const days = isLeapYear(year) ? CalendarDaysLeap : CalendarDays
|
const dayBundle = isLeapYear(year) ? CalendarDaysLeap : CalendarDays
|
||||||
|
const days = Array(dayBundle[month] + (startDay - 1)).fill(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-300 ${className} border-left border-right border border-neutral`}>
|
<div className={`w-300 ${className} min-h-[210px]`}>
|
||||||
<div className="mr-auto ml-auto w-70">
|
<div className="mr-auto ml-auto w-70">
|
||||||
<div className="flex w-full flex-wrap">
|
<div className="flex w-full flex-wrap">
|
||||||
{CalendarDaysOfTheWeek.map((d) => (
|
{CalendarDaysOfTheWeek.map((d) => (
|
||||||
@@ -42,23 +43,24 @@ const Calendar: FunctionComponent<Props> = ({ activities, startDate, onDateSelec
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-wrap">
|
<div className="flex w-full flex-wrap">
|
||||||
{Array(days[month] + (startDay - 1))
|
{days.map((_, index) => {
|
||||||
.fill(null)
|
const dayIndex = index - (startDay - 2)
|
||||||
.map((_, index) => {
|
const date = new Date(year, month, dayIndex)
|
||||||
const d = index - (startDay - 2)
|
const day = date.getDate()
|
||||||
const date = new Date(year, month, d)
|
const activities = activityMap[dateToDateOnlyString(date)] || []
|
||||||
const activities = activityMap[dateToDateOnlyString(date)] || []
|
const isTemplate = selectedDay && areDatesInSameDay(selectedDay, date)
|
||||||
return (
|
const type = activities.length > 0 ? 'item' : isTemplate ? 'template' : 'empty'
|
||||||
<CalendarDay
|
return (
|
||||||
key={index}
|
<CalendarDay
|
||||||
day={d}
|
isLastMonth={dayIndex <= 0}
|
||||||
isToday={isDateInSameDay(date, today)}
|
key={index}
|
||||||
activities={activities}
|
day={day}
|
||||||
onClick={() => onDateSelect(date)}
|
isToday={areDatesInSameDay(date, today)}
|
||||||
hasPendingEntry={selectedDay && isDateInSameDay(selectedDay, date)}
|
onClick={() => onDateSelect(date)}
|
||||||
/>
|
type={type}
|
||||||
)
|
/>
|
||||||
})}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +1,37 @@
|
|||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
import { CalendarActivity } from './CalendarActivity'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
day: number
|
day: number
|
||||||
activities: CalendarActivity[]
|
|
||||||
isToday: boolean
|
isToday: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
hasPendingEntry?: boolean
|
type: 'empty' | 'item' | 'template'
|
||||||
|
isLastMonth: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalendarDay: FunctionComponent<Props> = ({ day, activities = [], hasPendingEntry, isToday, onClick }) => {
|
const CalendarDay: FunctionComponent<Props> = ({ day, type, isToday, onClick, isLastMonth }) => {
|
||||||
const hasActivity = day > 0 && activities.length > 0
|
let classNames = ''
|
||||||
const todayClassNames = 'bg-danger text-danger-contrast font-bold'
|
if (isToday) {
|
||||||
const hasActivityClassNames = 'bg-danger-light text-danger font-bold'
|
classNames += 'bg-danger text-danger-contrast font-bold'
|
||||||
const defaultClassNames = 'bg-transparent hover:bg-contrast'
|
} else if (isLastMonth) {
|
||||||
const hasPendingEntryNames = 'bg-contrast'
|
classNames += 'text-passive-3'
|
||||||
|
} else {
|
||||||
|
if (type === 'empty') {
|
||||||
|
classNames += 'bg-transparent hover:bg-contrast'
|
||||||
|
} else if (type === 'item') {
|
||||||
|
classNames += 'bg-danger-light text-danger font-bold'
|
||||||
|
} else {
|
||||||
|
classNames += 'bg-contrast'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-7 w-[14.2%] p-0.5">
|
<div className="h-7 w-[14.2%] p-0.5">
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${classNames} flex h-full w-full cursor-pointer items-center justify-center rounded`}
|
||||||
!hasActivity && !isToday ? defaultClassNames : ''
|
|
||||||
} flex h-full w-full cursor-pointer items-center justify-center rounded ${
|
|
||||||
isToday ? todayClassNames : hasActivity ? hasActivityClassNames : ''
|
|
||||||
} ${hasPendingEntry ? hasPendingEntryNames : ''}`}
|
|
||||||
key={day}
|
key={day}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{day > 0 ? day : ''}
|
{day}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,43 +1,39 @@
|
|||||||
import { areDatesInSameMonth } from '@/Utils/DateUtils'
|
import { areDatesInSameDay, areDatesInSameMonth } from '@/Utils/DateUtils'
|
||||||
import {
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import Calendar from './Calendar'
|
import Calendar from './Calendar'
|
||||||
import { CalendarActivity, CalendarActivityType } from './CalendarActivity'
|
import { CalendarActivity, CalendarActivityType } from './CalendarActivity'
|
||||||
import { CalendarMonth } from './CalendarMonth'
|
import { CalendarMonth } from './CalendarMonth'
|
||||||
import { CalendarMonths } from './Constants'
|
import { CalendarMonths } from './Constants'
|
||||||
import { insertMonths, insertMonthsWithTarget } from './CalendarUtilts'
|
import { insertMonths, insertMonthsWithTarget } from './CalendarUtilts'
|
||||||
|
import { InfiniteScrollerInterface, InfinteScroller } from '../InfiniteScroller/InfiniteScroller'
|
||||||
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
|
import { LoggingDomain, log } from '@/Logging'
|
||||||
|
import { usePrevious } from './usePrevious'
|
||||||
|
import { isMobileScreen } from '@/Utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activityType: CalendarActivityType
|
activityType: CalendarActivityType
|
||||||
activities: CalendarActivity[]
|
activities: CalendarActivity[]
|
||||||
onDateSelect: (date: Date) => void
|
onDateSelect: (date: Date) => void
|
||||||
selectedTemplateDay?: Date
|
selectedDay?: Date
|
||||||
selectedItemDay?: Date
|
selectedDayType?: 'item' | 'template'
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InfiniteCalendarInterface = {
|
export type InfiniteCalendarInterface = {
|
||||||
changeMonth: (month: Date) => void
|
goToMonth: (month: Date) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageSize = 10
|
const PageSize = 2
|
||||||
|
|
||||||
const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||||
({ activities, onDateSelect, selectedTemplateDay, selectedItemDay }: Props, ref) => {
|
({ activities, onDateSelect, selectedDay, className }: Props, ref) => {
|
||||||
const [date, setDate] = useState(new Date())
|
|
||||||
const [month, setMonth] = useState(date.getMonth())
|
|
||||||
const [year, setYear] = useState(date.getFullYear())
|
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(true)
|
const [expanded, setExpanded] = useState(true)
|
||||||
const [scrollWidth, setScrollWidth] = useState(0)
|
const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false)
|
||||||
|
const scrollerRef = useRef<InfiniteScrollerInterface | null>(null)
|
||||||
|
const previousSelectedDay = usePrevious(selectedDay)
|
||||||
|
|
||||||
|
const [activeDate, setActiveDate] = useState(new Date())
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const [months, setMonths] = useState<CalendarMonth[]>(() => {
|
const [months, setMonths] = useState<CalendarMonth[]>(() => {
|
||||||
const base = [{ date: today }]
|
const base = [{ date: today }]
|
||||||
@@ -46,18 +42,6 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
|||||||
return base
|
return base
|
||||||
})
|
})
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
changeMonth(date: Date) {
|
|
||||||
setDate(date)
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const [firstElement, setFirstElement] = useState<HTMLDivElement | null>(null)
|
|
||||||
const [lastElement, setLastElement] = useState<HTMLDivElement | null>(null)
|
|
||||||
const [didPaginateLeft, setDidPaginateLeft] = useState(false)
|
|
||||||
const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false)
|
|
||||||
const scrollArea = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const hasMonthInList = useCallback(
|
const hasMonthInList = useCallback(
|
||||||
(date: Date): boolean => {
|
(date: Date): boolean => {
|
||||||
for (const month of months) {
|
for (const month of months) {
|
||||||
@@ -74,7 +58,38 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
|||||||
(date: Date): void => {
|
(date: Date): void => {
|
||||||
setMonths(insertMonthsWithTarget(months, date))
|
setMonths(insertMonthsWithTarget(months, date))
|
||||||
},
|
},
|
||||||
[months, setMonths],
|
[months],
|
||||||
|
)
|
||||||
|
|
||||||
|
const scrollToMonth = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
const elementId = elementIdForMonth(date)
|
||||||
|
scrollerRef.current?.scrollToElementId(elementId)
|
||||||
|
},
|
||||||
|
[scrollerRef],
|
||||||
|
)
|
||||||
|
|
||||||
|
const goToMonth = useCallback(
|
||||||
|
(month: Date) => {
|
||||||
|
if (!hasMonthInList(month)) {
|
||||||
|
insertMonthInList(month)
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LoggingDomain.DailyNotes, '[Calendar] Scrolling to month', month, 'from goToMonth')
|
||||||
|
setActiveDate(month)
|
||||||
|
scrollToMonth(month)
|
||||||
|
},
|
||||||
|
[hasMonthInList, insertMonthInList, scrollToMonth],
|
||||||
|
)
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
goToMonth(date: Date) {
|
||||||
|
goToMonth(date)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[goToMonth],
|
||||||
)
|
)
|
||||||
|
|
||||||
const resetNumberOfCalendarsToBase = useCallback(
|
const resetNumberOfCalendarsToBase = useCallback(
|
||||||
@@ -88,153 +103,63 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTemplateDay) {
|
if (selectedDay) {
|
||||||
setDate(selectedTemplateDay)
|
if (previousSelectedDay && areDatesInSameDay(previousSelectedDay, selectedDay)) {
|
||||||
|
log(LoggingDomain.DailyNotes, '[Calendar] selectedDay has changed, but is same as previous', selectedDay)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log(LoggingDomain.DailyNotes, '[Calendar] selectedDay has changed, going to month:', selectedDay)
|
||||||
|
goToMonth(selectedDay)
|
||||||
}
|
}
|
||||||
}, [selectedTemplateDay])
|
}, [selectedDay, goToMonth, previousSelectedDay])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedItemDay) {
|
|
||||||
setDate(selectedItemDay)
|
|
||||||
}
|
|
||||||
}, [selectedItemDay])
|
|
||||||
|
|
||||||
const scrollToMonth = useCallback((date: Date) => {
|
|
||||||
const elementId = elementIdForMonth(date)
|
|
||||||
const element = document.getElementById(elementId)
|
|
||||||
if (!element) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollArea.current!.scrollLeft = element.offsetLeft + -60
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
setMonth(date.getMonth())
|
|
||||||
setYear(date.getFullYear())
|
|
||||||
|
|
||||||
if (!hasMonthInList(date)) {
|
|
||||||
insertMonthInList(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToMonth(date)
|
|
||||||
}, [date, hasMonthInList, insertMonthInList, scrollToMonth])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!restoreScrollAfterExpand) {
|
if (!restoreScrollAfterExpand) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollArea.current && expanded) {
|
if (expanded) {
|
||||||
scrollToMonth(date)
|
log(
|
||||||
|
LoggingDomain.DailyNotes,
|
||||||
|
'[Calendar] Scrolling to month',
|
||||||
|
activeDate,
|
||||||
|
'from restoreScrollAfterExpand useEffect',
|
||||||
|
)
|
||||||
|
scrollToMonth(activeDate)
|
||||||
setRestoreScrollAfterExpand(false)
|
setRestoreScrollAfterExpand(false)
|
||||||
}
|
}
|
||||||
}, [expanded, scrollToMonth, date, restoreScrollAfterExpand, setRestoreScrollAfterExpand])
|
}, [expanded, scrollToMonth, activeDate, restoreScrollAfterExpand, setRestoreScrollAfterExpand])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!scrollArea.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (didPaginateLeft) {
|
|
||||||
scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollWidth
|
|
||||||
setDidPaginateLeft(false)
|
|
||||||
}
|
|
||||||
}, [months, didPaginateLeft, scrollWidth])
|
|
||||||
|
|
||||||
const paginateLeft = useCallback(() => {
|
const paginateLeft = useCallback(() => {
|
||||||
if (scrollArea.current) {
|
log(LoggingDomain.DailyNotes, '[Calendar] paginateLeft')
|
||||||
setScrollWidth(scrollArea.current.scrollWidth)
|
setMonths((prevMonths) => {
|
||||||
}
|
const copy = prevMonths.slice()
|
||||||
|
insertMonths(copy, 'front', PageSize)
|
||||||
const copy = months.slice()
|
return copy
|
||||||
insertMonths(copy, 'front', PageSize)
|
})
|
||||||
setDidPaginateLeft(true)
|
}, [setMonths])
|
||||||
setMonths(copy)
|
|
||||||
}, [months, setMonths])
|
|
||||||
|
|
||||||
const paginateRight = useCallback(() => {
|
const paginateRight = useCallback(() => {
|
||||||
const copy = months.slice()
|
log(LoggingDomain.DailyNotes, '[Calendar] paginateRight')
|
||||||
insertMonths(copy, 'end', PageSize)
|
setMonths((prevMonths) => {
|
||||||
setDidPaginateLeft(false)
|
const copy = prevMonths.slice()
|
||||||
setMonths(copy)
|
insertMonths(copy, 'end', PageSize)
|
||||||
}, [months, setMonths])
|
return copy
|
||||||
|
})
|
||||||
|
}, [setMonths])
|
||||||
|
|
||||||
const updateCurrentMonth = useCallback(
|
const onElementVisibility = useCallback(
|
||||||
(index: number) => {
|
(id: string) => {
|
||||||
const newMonth = months[index]
|
const index = months.findIndex((candidate) => elementIdForMonth(candidate.date) === id)
|
||||||
setMonth(newMonth.date.getMonth())
|
if (index >= 0) {
|
||||||
setYear(newMonth.date.getFullYear())
|
const newMonth = months[index]
|
||||||
|
log(LoggingDomain.DailyNotes, '[Calendar] Month element did become visible, setting activeDate', newMonth)
|
||||||
|
setActiveDate(newMonth.date)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[months, setMonth, setYear],
|
[months],
|
||||||
)
|
)
|
||||||
|
|
||||||
const visibilityObserver = useMemo(
|
|
||||||
() =>
|
|
||||||
new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
|
||||||
if (visibleEntry) {
|
|
||||||
const id = visibleEntry.target.id
|
|
||||||
const index = months.findIndex((candidate) => elementIdForMonth(candidate.date) === id)
|
|
||||||
updateCurrentMonth(index)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.9 },
|
|
||||||
),
|
|
||||||
[updateCurrentMonth, months],
|
|
||||||
)
|
|
||||||
|
|
||||||
const rightObserver = useMemo(
|
|
||||||
() =>
|
|
||||||
new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
if (entries[0].isIntersecting) {
|
|
||||||
paginateRight()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.5 },
|
|
||||||
),
|
|
||||||
[paginateRight],
|
|
||||||
)
|
|
||||||
|
|
||||||
const leftObserver = useMemo(
|
|
||||||
() =>
|
|
||||||
new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
if (entries[0].isIntersecting) {
|
|
||||||
paginateLeft()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 1.0 },
|
|
||||||
),
|
|
||||||
[paginateLeft],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (lastElement) {
|
|
||||||
rightObserver.observe(lastElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (lastElement) {
|
|
||||||
rightObserver.unobserve(lastElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [lastElement, rightObserver])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (firstElement) {
|
|
||||||
leftObserver.observe(firstElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (firstElement) {
|
|
||||||
leftObserver.unobserve(firstElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [firstElement, leftObserver])
|
|
||||||
|
|
||||||
const toggleVisibility = useCallback(() => {
|
const toggleVisibility = useCallback(() => {
|
||||||
setRestoreScrollAfterExpand(true)
|
setRestoreScrollAfterExpand(true)
|
||||||
|
|
||||||
@@ -254,48 +179,42 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex-shrink-0 border-b border-solid border-border">
|
<div className={'border-b border-solid border-border'}>
|
||||||
<div
|
<div
|
||||||
className="text-md flex cursor-pointer items-center justify-center py-2 px-4 text-center font-bold hover:bg-contrast"
|
|
||||||
onClick={toggleVisibility}
|
onClick={toggleVisibility}
|
||||||
|
className={classNames(
|
||||||
|
'text-md flex cursor-pointer items-center justify-center py-2 px-4',
|
||||||
|
'text-center font-bold hover:bg-contrast',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{CalendarMonths[month]} {year}
|
{CalendarMonths[activeDate.getMonth()]} {activeDate.getFullYear()}
|
||||||
</div>
|
</div>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div
|
<InfinteScroller
|
||||||
style={{ scrollBehavior: 'smooth' }}
|
paginateFront={paginateLeft}
|
||||||
ref={scrollArea}
|
paginateEnd={paginateRight}
|
||||||
id="calendar-scroller"
|
direction={'horizontal'}
|
||||||
className="flex w-full overflow-x-scroll pb-2 md:max-w-full"
|
onElementVisibility={onElementVisibility}
|
||||||
|
ref={scrollerRef}
|
||||||
|
className={className}
|
||||||
|
isMobileScreen={isMobileScreen()}
|
||||||
>
|
>
|
||||||
{months.map((month, index) => {
|
{months.map((month) => {
|
||||||
const isFirst = index === 0
|
|
||||||
const isLast = index === months.length - 1
|
|
||||||
const id = elementIdForMonth(month.date)
|
const id = elementIdForMonth(month.date)
|
||||||
return (
|
return (
|
||||||
<div
|
<div id={id} key={id}>
|
||||||
id={id}
|
|
||||||
key={id}
|
|
||||||
ref={(ref) => {
|
|
||||||
isFirst ? setFirstElement(ref) : isLast ? setLastElement(ref) : null
|
|
||||||
|
|
||||||
if (ref) {
|
|
||||||
visibilityObserver.observe(ref)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Calendar
|
<Calendar
|
||||||
key={id}
|
key={id}
|
||||||
className="mx-2"
|
className="mx-2"
|
||||||
activities={activities}
|
activities={activities}
|
||||||
onDateSelect={handleDaySelection}
|
onDateSelect={handleDaySelection}
|
||||||
startDate={month.date}
|
startDate={month.date}
|
||||||
selectedDay={selectedTemplateDay}
|
selectedDay={selectedDay}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</InfinteScroller>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export const usePrevious = <T>(value: T): T | undefined => {
|
||||||
|
const ref = useRef<T>()
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value
|
||||||
|
})
|
||||||
|
return ref.current
|
||||||
|
}
|
||||||
@@ -247,7 +247,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
<div
|
<div
|
||||||
id="items-column"
|
id="items-column"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'sn-component section app-column flex h-screen flex-col pt-safe-top md:h-full',
|
'sn-component section app-column flex h-screen flex-col overflow-hidden pt-safe-top md:h-full',
|
||||||
'xl:w-87.5 xsm-only:!w-full sm-only:!w-full',
|
'xl:w-87.5 xsm-only:!w-full sm-only:!w-full',
|
||||||
isTabletScreenSize && !isNotesListVisibleOnTablets
|
isTabletScreenSize && !isNotesListVisibleOnTablets
|
||||||
? 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0'
|
? 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0'
|
||||||
@@ -256,7 +256,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
aria-label={'Notes & Files'}
|
aria-label={'Notes & Files'}
|
||||||
ref={itemsViewPanelRef}
|
ref={itemsViewPanelRef}
|
||||||
>
|
>
|
||||||
<ResponsivePaneContent paneId={AppPaneId.Items}>
|
<ResponsivePaneContent className="overflow-hidden" paneId={AppPaneId.Items}>
|
||||||
<div id="items-title-bar" className="section-title-bar border-b border-solid border-border">
|
<div id="items-title-bar" className="section-title-bar border-b border-solid border-border">
|
||||||
<div id="items-title-bar-container">
|
<div id="items-title-bar-container">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createDailyItemsWithToday } from './CreateDailySections'
|
||||||
|
|
||||||
|
describe('create daily sections', () => {
|
||||||
|
it('createDailyItemsWithToday', () => {
|
||||||
|
const result = createDailyItemsWithToday(10)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -31,8 +31,9 @@ export const templateEntryForDate = (date: Date): DailyItemsDay => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createDailyItemsWithToday(count: number): DailyItemsDay[] {
|
export function createDailyItemsWithToday(count: number): DailyItemsDay[] {
|
||||||
const today = templateEntryForDate(new Date())
|
const items = [templateEntryForDate(new Date())]
|
||||||
return insertBlanks([today], 'end', count)
|
insertBlanks(items, 'front', count / 2 - 1)
|
||||||
|
return insertBlanks(items, 'end', count / 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
|
||||||
import { ListableContentItem } from '../Types/ListableContentItem'
|
import { ListableContentItem } from '../Types/ListableContentItem'
|
||||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
|
||||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
|
||||||
import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider'
|
||||||
import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata'
|
import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata'
|
||||||
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
|
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
|
||||||
@@ -14,6 +11,9 @@ import { SNTag } from '@standardnotes/snjs'
|
|||||||
import { CalendarActivity } from '../Calendar/CalendarActivity'
|
import { CalendarActivity } from '../Calendar/CalendarActivity'
|
||||||
import { dateToDailyDayIdentifier } from './Utils'
|
import { dateToDailyDayIdentifier } from './Utils'
|
||||||
import InfiniteCalendar, { InfiniteCalendarInterface } from '../Calendar/InfiniteCalendar'
|
import InfiniteCalendar, { InfiniteCalendarInterface } from '../Calendar/InfiniteCalendar'
|
||||||
|
import { InfiniteScrollerInterface, InfinteScroller } from '../InfiniteScroller/InfiniteScroller'
|
||||||
|
import { LoggingDomain, log } from '@/Logging'
|
||||||
|
import { isMobileScreen } from '@/Utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
itemListController: ItemListController
|
itemListController: ItemListController
|
||||||
@@ -36,12 +36,9 @@ const DailyContentList: FunctionComponent<Props> = ({
|
|||||||
const [needsSelectionReload, setNeedsSelectionReload] = useState(false)
|
const [needsSelectionReload, setNeedsSelectionReload] = useState(false)
|
||||||
const [todayItem, setTodayItem] = useState<DailyItemsDay>()
|
const [todayItem, setTodayItem] = useState<DailyItemsDay>()
|
||||||
const [selectedDay, setSelectedDay] = useState<Date>()
|
const [selectedDay, setSelectedDay] = useState<Date>()
|
||||||
const [lastElement, setLastElement] = useState<HTMLDivElement | null>(null)
|
|
||||||
const [firstElement, setFirstElement] = useState<HTMLDivElement | null>(null)
|
|
||||||
const [lastScrollHeight, setLastScrollHeight] = useState(0)
|
|
||||||
const [didPaginateTop, setDidPaginateTop] = useState(false)
|
|
||||||
const scrollArea = useRef<HTMLDivElement>(null)
|
|
||||||
const calendarRef = useRef<InfiniteCalendarInterface | null>(null)
|
const calendarRef = useRef<InfiniteCalendarInterface | null>(null)
|
||||||
|
const [lastVisibleDay, setLastVisibleDay] = useState<DailyItemsDay>()
|
||||||
|
const scrollerRef = useRef<InfiniteScrollerInterface | null>(null)
|
||||||
|
|
||||||
const [dailyItems, setDailyItems] = useState<DailyItemsDay[]>(() => {
|
const [dailyItems, setDailyItems] = useState<DailyItemsDay[]>(() => {
|
||||||
return createDailyItemsWithToday(PageSize)
|
return createDailyItemsWithToday(PageSize)
|
||||||
@@ -57,6 +54,12 @@ const DailyContentList: FunctionComponent<Props> = ({
|
|||||||
setTodayItem(dailyItems.find((item) => item.isToday) as DailyItemsDay)
|
setTodayItem(dailyItems.find((item) => item.isToday) as DailyItemsDay)
|
||||||
}, [dailyItems])
|
}, [dailyItems])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (todayItem && scrollerRef.current) {
|
||||||
|
scrollerRef.current?.scrollToElementId(todayItem.id)
|
||||||
|
}
|
||||||
|
}, [todayItem, scrollerRef])
|
||||||
|
|
||||||
const calendarActivities: CalendarActivity[] = useMemo(() => {
|
const calendarActivities: CalendarActivity[] = useMemo(() => {
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
return {
|
return {
|
||||||
@@ -67,108 +70,37 @@ const DailyContentList: FunctionComponent<Props> = ({
|
|||||||
}, [items])
|
}, [items])
|
||||||
|
|
||||||
const paginateBottom = useCallback(() => {
|
const paginateBottom = useCallback(() => {
|
||||||
const copy = dailyItems.slice()
|
log(LoggingDomain.DailyNotes, '[ContentList] paginateBottom')
|
||||||
insertBlanks(copy, 'end', PageSize)
|
setDailyItems((prev) => {
|
||||||
setDailyItems(copy)
|
const copy = prev.slice()
|
||||||
}, [dailyItems, setDailyItems])
|
insertBlanks(copy, 'end', PageSize)
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
}, [setDailyItems])
|
||||||
|
|
||||||
const paginateTop = useCallback(() => {
|
const paginateTop = useCallback(() => {
|
||||||
if (scrollArea.current) {
|
log(LoggingDomain.DailyNotes, '[ContentList] paginateTop')
|
||||||
setLastScrollHeight(scrollArea.current.scrollHeight)
|
setDailyItems((prev) => {
|
||||||
}
|
const copy = prev.slice()
|
||||||
const copy = dailyItems.slice()
|
insertBlanks(copy, 'front', PageSize)
|
||||||
insertBlanks(copy, 'front', PageSize)
|
return copy
|
||||||
setDidPaginateTop(true)
|
})
|
||||||
setDailyItems(copy)
|
}, [setDailyItems])
|
||||||
}, [dailyItems, setDailyItems, setDidPaginateTop])
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!scrollArea.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (didPaginateTop) {
|
|
||||||
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - lastScrollHeight
|
|
||||||
setDidPaginateTop(false)
|
|
||||||
}
|
|
||||||
}, [didPaginateTop, lastScrollHeight])
|
|
||||||
|
|
||||||
const onListItemDidBecomeVisible = useCallback(
|
const onListItemDidBecomeVisible = useCallback(
|
||||||
(elementId: string) => {
|
(elementId: string) => {
|
||||||
const dailyItem = dailyItems.find((candidate) => candidate.id === elementId)
|
const dailyItem = dailyItems.find((candidate) => candidate.id === elementId)
|
||||||
if (dailyItem) {
|
if (dailyItem && dailyItem !== lastVisibleDay) {
|
||||||
calendarRef?.current?.changeMonth(dailyItem.date)
|
setLastVisibleDay(dailyItem)
|
||||||
|
log(LoggingDomain.DailyNotes, '[ContentList] Item did become visible for date', dailyItem.date)
|
||||||
|
calendarRef?.current?.goToMonth(dailyItem.date)
|
||||||
|
} else {
|
||||||
|
log(LoggingDomain.DailyNotes, '[ContentList] Ignoring duplicate day visibility')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dailyItems],
|
[dailyItems, lastVisibleDay],
|
||||||
)
|
)
|
||||||
|
|
||||||
const visibilityObserver = useMemo(
|
|
||||||
() =>
|
|
||||||
new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
|
||||||
if (visibleEntry) {
|
|
||||||
onListItemDidBecomeVisible(visibleEntry.target.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.9 },
|
|
||||||
),
|
|
||||||
[onListItemDidBecomeVisible],
|
|
||||||
)
|
|
||||||
|
|
||||||
const bottomObserver = useMemo(
|
|
||||||
() =>
|
|
||||||
new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const first = entries[0]
|
|
||||||
if (first.isIntersecting) {
|
|
||||||
paginateBottom()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.5 },
|
|
||||||
),
|
|
||||||
[paginateBottom],
|
|
||||||
)
|
|
||||||
|
|
||||||
const topObserver = useMemo(
|
|
||||||
() =>
|
|
||||||
new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const first = entries[0]
|
|
||||||
if (first.isIntersecting) {
|
|
||||||
paginateTop()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.5 },
|
|
||||||
),
|
|
||||||
[paginateTop],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (lastElement) {
|
|
||||||
bottomObserver.observe(lastElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (lastElement) {
|
|
||||||
bottomObserver.unobserve(lastElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [lastElement, bottomObserver])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (firstElement) {
|
|
||||||
topObserver.observe(firstElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (firstElement) {
|
|
||||||
topObserver.unobserve(firstElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [firstElement, topObserver])
|
|
||||||
|
|
||||||
const onClickItem = useCallback(
|
const onClickItem = useCallback(
|
||||||
async (day: DailyItemsDay, item: ListableContentItem, userTriggered: boolean) => {
|
async (day: DailyItemsDay, item: ListableContentItem, userTriggered: boolean) => {
|
||||||
await onSelect(item, userTriggered)
|
await onSelect(item, userTriggered)
|
||||||
@@ -239,29 +171,29 @@ const DailyContentList: FunctionComponent<Props> = ({
|
|||||||
[onClickItem, onClickTemplate, dailyItemForDate, itemsByDateMapping],
|
[onClickItem, onClickTemplate, dailyItemForDate, itemsByDateMapping],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const hasItemsOnSelectedDay = selectedDay && itemsByDateMapping[dateToDailyDayIdentifier(selectedDay)]?.length > 0
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InfiniteCalendar
|
<InfiniteCalendar
|
||||||
activities={calendarActivities}
|
activities={calendarActivities}
|
||||||
activityType={'created'}
|
activityType={'created'}
|
||||||
onDateSelect={onCalendarSelect}
|
onDateSelect={onCalendarSelect}
|
||||||
selectedTemplateDay={selectedDay}
|
selectedDay={selectedDay}
|
||||||
selectedItemDay={selectedDay}
|
selectedDayType={!selectedDay ? undefined : hasItemsOnSelectedDay ? 'item' : 'template'}
|
||||||
ref={calendarRef}
|
ref={calendarRef}
|
||||||
|
className={'flex-column flex'}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
className={classNames(
|
<InfinteScroller
|
||||||
'infinite-scroll overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none',
|
paginateFront={paginateTop}
|
||||||
'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
|
paginateEnd={paginateBottom}
|
||||||
'md:hover:[overflow-y:_overlay]',
|
direction="vertical"
|
||||||
)}
|
onElementVisibility={onListItemDidBecomeVisible}
|
||||||
ref={scrollArea}
|
className={'flex-1'}
|
||||||
id={ElementIds.ContentList}
|
ref={scrollerRef}
|
||||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
isMobileScreen={isMobileScreen()}
|
||||||
>
|
>
|
||||||
{dailyItems.map((dailyItem, index) => {
|
{dailyItems.map((dailyItem) => {
|
||||||
const isFirst = index === 0
|
|
||||||
const isLast = index === dailyItems.length - 1
|
|
||||||
const items = itemsByDateMapping[dailyItem.id]
|
const items = itemsByDateMapping[dailyItem.id]
|
||||||
if (items) {
|
if (items) {
|
||||||
return items.map((item) => (
|
return items.map((item) => (
|
||||||
@@ -275,12 +207,6 @@ const DailyContentList: FunctionComponent<Props> = ({
|
|||||||
hidePreview={hideNotePreview}
|
hidePreview={hideNotePreview}
|
||||||
hideTags={hideTags}
|
hideTags={hideTags}
|
||||||
onClick={() => onClickItem(dailyItem, item, true)}
|
onClick={() => onClickItem(dailyItem, item, true)}
|
||||||
ref={(ref) => {
|
|
||||||
isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null
|
|
||||||
if (ref) {
|
|
||||||
visibilityObserver.observe(ref)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
@@ -291,17 +217,11 @@ const DailyContentList: FunctionComponent<Props> = ({
|
|||||||
id={dailyItem.id}
|
id={dailyItem.id}
|
||||||
key={dailyItem.dateKey}
|
key={dailyItem.dateKey}
|
||||||
onClick={() => onClickTemplate(dailyItem.date)}
|
onClick={() => onClickTemplate(dailyItem.date)}
|
||||||
ref={(ref) => {
|
|
||||||
isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null
|
|
||||||
if (ref) {
|
|
||||||
visibilityObserver.observe(ref)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</InfinteScroller>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,12 +166,9 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon })
|
void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon })
|
||||||
}, [preferences, changePreferences])
|
}, [preferences, changePreferences])
|
||||||
|
|
||||||
const setEntryMode = useCallback(
|
const toggleEntryMode = useCallback(() => {
|
||||||
(mode: 'normal' | 'daily') => {
|
void changePreferences({ entryMode: isDailyEntry ? 'normal' : 'daily' })
|
||||||
void changePreferences({ entryMode: mode })
|
}, [isDailyEntry, changePreferences])
|
||||||
},
|
|
||||||
[changePreferences],
|
|
||||||
)
|
|
||||||
|
|
||||||
const TabButton: FunctionComponent<{
|
const TabButton: FunctionComponent<{
|
||||||
label: string
|
label: string
|
||||||
@@ -211,7 +208,10 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
<h1 className="sk-h3 m-0 text-sm font-semibold">Upgrade for per-tag preferences</h1>
|
<h1 className="sk-h3 m-0 text-sm font-semibold">Upgrade for per-tag preferences</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="col-start-1 col-end-3 m-0 mt-1 text-sm">
|
<p className="col-start-1 col-end-3 m-0 mt-1 text-sm">
|
||||||
Create powerful workflows and organizational layouts with per-tag display preferences.
|
{DailyEntryModeEnabled &&
|
||||||
|
'Create powerful workflows and organizational layouts with per-tag display preferences and the all-new Daily Notebook feature.'}
|
||||||
|
{!DailyEntryModeEnabled &&
|
||||||
|
'Create powerful workflows and organizational layouts with per-tag display preferences.'}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
@@ -235,7 +235,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
{currentMode === 'tag' && <button onClick={resetTagPreferences}>Reset</button>}
|
{currentMode === 'tag' && <button onClick={resetTagPreferences}>Reset</button>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{controlsDisabled && <NoSubscriptionBanner />}
|
{!controlsDisabled && <NoSubscriptionBanner />}
|
||||||
|
|
||||||
<MenuItemSeparator />
|
<MenuItemSeparator />
|
||||||
|
|
||||||
@@ -376,26 +376,17 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
{currentMode === 'tag' && DailyEntryModeEnabled && (
|
{currentMode === 'tag' && DailyEntryModeEnabled && (
|
||||||
<>
|
<>
|
||||||
<MenuItemSeparator />
|
<MenuItemSeparator />
|
||||||
<div className="px-3 py-1 text-xs font-semibold uppercase text-text">Entry Mode</div>
|
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={controlsDisabled}
|
disabled={controlsDisabled}
|
||||||
className="py-2"
|
type={MenuItemType.SwitchButton}
|
||||||
type={MenuItemType.RadioButton}
|
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||||
onClick={() => setEntryMode('normal')}
|
|
||||||
checked={!selectedTag.preferences?.entryMode || selectedTag.preferences?.entryMode === 'normal'}
|
|
||||||
>
|
|
||||||
<div className="ml-2 flex flex-grow items-center justify-between">Normal</div>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
disabled={controlsDisabled}
|
|
||||||
className="py-2"
|
|
||||||
type={MenuItemType.RadioButton}
|
|
||||||
onClick={() => setEntryMode('daily')}
|
|
||||||
checked={isDailyEntry}
|
checked={isDailyEntry}
|
||||||
|
onChange={toggleEntryMode}
|
||||||
>
|
>
|
||||||
<div className="ml-2 flex flex-grow items-center justify-between">Daily</div>
|
<div className="flex flex-col pr-5">
|
||||||
|
<div className="text-xs font-semibold uppercase text-text">Daily Notebook</div>
|
||||||
|
<div className="mt-1">Capture new notes daily with a calendar-based experience</div>
|
||||||
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
import { LoggingDomain, log } from '@/Logging'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode
|
||||||
|
paginateFront: () => void
|
||||||
|
paginateEnd: () => void
|
||||||
|
direction: 'horizontal' | 'vertical'
|
||||||
|
onElementVisibility?: (elementId: string) => void
|
||||||
|
className?: string
|
||||||
|
isMobileScreen?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InfiniteScrollerInterface = {
|
||||||
|
scrollToElementId: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfinteScroller = forwardRef<InfiniteScrollerInterface, Props>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
paginateFront,
|
||||||
|
paginateEnd,
|
||||||
|
direction = 'vertical',
|
||||||
|
onElementVisibility,
|
||||||
|
className,
|
||||||
|
isMobileScreen,
|
||||||
|
}: Props,
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const frontSentinel = useRef<HTMLDivElement | null>(null)
|
||||||
|
const endSentinel = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [ignoreFirstFrontSentinelEvent, setIgnoreFirstFrontSentinelEvent] = useState(true)
|
||||||
|
const [needsMobilePaginationFix, setNeedsMobilePaginationFix] = useState(false)
|
||||||
|
|
||||||
|
const scrollArea = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [scrollSize, setScrollSize] = useState(0)
|
||||||
|
const [didPaginateFront, setDidPaginateFront] = useState(false)
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
scrollToElementId(id: string) {
|
||||||
|
scrollToElementId(id)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const visibilityObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
||||||
|
if (visibleEntry) {
|
||||||
|
onElementVisibility?.(visibleEntry.target.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1.0 },
|
||||||
|
),
|
||||||
|
[onElementVisibility],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const childElements = Array.from(scrollArea.current!.children)
|
||||||
|
for (const child of childElements) {
|
||||||
|
visibilityObserver.observe(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const child of childElements) {
|
||||||
|
visibilityObserver.unobserve(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [visibilityObserver, children])
|
||||||
|
|
||||||
|
const scrollToElementId = useCallback((id: string) => {
|
||||||
|
const element = document.getElementById(id)
|
||||||
|
if (!element) {
|
||||||
|
log(LoggingDomain.DailyNotes, 'Element not found', id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LoggingDomain.DailyNotes, 'Scrolling to element', id)
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: 'auto',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center',
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!scrollArea.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didPaginateFront) {
|
||||||
|
if (direction === 'vertical') {
|
||||||
|
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - scrollSize
|
||||||
|
if (isMobileScreen) {
|
||||||
|
setNeedsMobilePaginationFix(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollSize
|
||||||
|
}
|
||||||
|
setDidPaginateFront(false)
|
||||||
|
}
|
||||||
|
}, [didPaginateFront, scrollSize, direction, isMobileScreen])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
/**
|
||||||
|
* iOS Safari has an issue rendering paginated items from the top where the new
|
||||||
|
* scrolled to area is white until the user interacts with scroll again. The fix
|
||||||
|
* we apply is to re-set scrollTop to its same value to trigger a refresh.
|
||||||
|
* https://stackoverflow.com/questions/9807620
|
||||||
|
*/
|
||||||
|
if (needsMobilePaginationFix) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!scrollArea.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LoggingDomain.DailyNotes, '[InfiniteScroller] Applying mobile pagination fix')
|
||||||
|
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - scrollSize
|
||||||
|
setNeedsMobilePaginationFix(false)
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
}, [needsMobilePaginationFix, scrollSize])
|
||||||
|
|
||||||
|
const _paginateFront = useCallback(() => {
|
||||||
|
if (direction === 'vertical') {
|
||||||
|
setScrollSize(scrollArea!.current!.scrollHeight)
|
||||||
|
} else {
|
||||||
|
setScrollSize(scrollArea!.current!.scrollWidth)
|
||||||
|
}
|
||||||
|
setDidPaginateFront(true)
|
||||||
|
paginateFront()
|
||||||
|
}, [paginateFront, direction])
|
||||||
|
|
||||||
|
const _paginateEnd = useCallback(() => {
|
||||||
|
paginateEnd()
|
||||||
|
}, [paginateEnd])
|
||||||
|
|
||||||
|
const frontObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
if (ignoreFirstFrontSentinelEvent) {
|
||||||
|
log(LoggingDomain.DailyNotes, '[InfiniteScroller] Ignoring first front sentinel event')
|
||||||
|
setIgnoreFirstFrontSentinelEvent(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_paginateFront()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
),
|
||||||
|
[_paginateFront, ignoreFirstFrontSentinelEvent],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (frontSentinel.current) {
|
||||||
|
frontObserver.observe(frontSentinel.current)
|
||||||
|
}
|
||||||
|
}, [frontObserver, frontSentinel])
|
||||||
|
|
||||||
|
const endObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
_paginateEnd()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
),
|
||||||
|
[_paginateEnd],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (endSentinel.current) {
|
||||||
|
endObserver.observe(endSentinel.current)
|
||||||
|
}
|
||||||
|
}, [endObserver, endSentinel])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollArea}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
overflowY: 'scroll',
|
||||||
|
flexDirection: direction === 'vertical' ? 'column' : 'row',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 1, height: 1, backgroundColor: 'transparent' }} ref={frontSentinel}></div>
|
||||||
|
{children}
|
||||||
|
<div style={{ width: 1, height: 1, backgroundColor: 'transparent' }} ref={endSentinel}></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
17
packages/web/src/javascripts/Logging.ts
Normal file
17
packages/web/src/javascripts/Logging.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { log as utilsLog } from '@standardnotes/utils'
|
||||||
|
|
||||||
|
export enum LoggingDomain {
|
||||||
|
DailyNotes,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||||
|
[LoggingDomain.DailyNotes]: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log(domain: LoggingDomain, ...args: any[]): void {
|
||||||
|
if (!LoggingStatus[domain]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utilsLog(LoggingDomain[domain], ...args)
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ export function numHoursBetweenDates(date1: Date, date2: Date): number {
|
|||||||
return Math.abs(date1.getTime() - date2.getTime()) / 3600000
|
return Math.abs(date1.getTime() - date2.getTime()) / 3600000
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDateInSameDay(date1: Date, date2: Date): boolean {
|
export function areDatesInSameDay(date1: Date, date2: Date): boolean {
|
||||||
return date1.toLocaleDateString() === date2.toLocaleDateString()
|
return date1.toLocaleDateString() === date2.toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user