From 6193dac2cfd4469153b5672441aadb88ea92d119 Mon Sep 17 00:00:00 2001 From: Tom Klingenberg <352517+ktomk@users.noreply.github.com> Date: Fri, 21 May 2021 10:26:11 +0200 Subject: [PATCH] Calendar new views (#57) * touch-up month-view weekday-headers the month view has a scroll-bar, the header needs some right-margin. the 15px is taken from the week- and day-view which has a scroll-bar and that right-margin for the header (div.rbc-time-header style attribute). * dnd: update week-wrapper updating WeekWrapper.js to the upstream version of Nov, 6th 2020. [1] fixes crashes on drop into all-day in the week view of the calendar. this is the upstream version, just with a different semicolon-style and one typo fix. [1]: https://github.com/jquense/react-big-calendar/blob/34aec3a64d018ec0f9dce780a6d4eeb03692c9fd/src/addons/dragAndDrop/WeekWrapper.js * dnd: fix drop on time-gutter previously dropping on the time-gutter leaves calendar in drag state. fix is to add the ability of specific drop targets and cancel/reset drag- and-drop if the drop was not on an allowed drop target. streamlined with [1]. [1]: https://github.com/jquense/react-big-calendar/pull/1901 * add week, day and agenda view add three of the rbc views to the calendar: 1. week; 2. day; 3. agenda - row details by single-click on week and day events - background color for day/week view (non-full-day events) - css: reset day-event border - css: bootstrap needs button style reset (time-header) - css: center agenda no events text * show week-of-year show the week-number in the top-left corner of the timer-gutter. display format is weekOfYearFormat (localizer), which is decorated as the default moment localizer does not have it. * all-/cross- day event basically, given the end-date is undefined|null|"", the event is all-day. additionally an event is displayed in the all-day slot if it is across multiple days. previously there was no need to handle all- or cross-days to this extend for any calendar event as only the original week view was in use and additionally the year view. now this is needed as the week and day views are active and they display event with time(s) and within the all-/cross-day slot on top. this is handling events with a bogus end-date gracefully: in case an end-date is before the start-date, the start-date is leading and the end-date of the event will be the events start-date plus a fixed amount of time (e.g. one hour). this is useful for events with date and time (with minute precision). and an offset of 12 hours is necessary to display the event on the correct day within all views, incl. the year view. * all-/cross- day: drag-and-drop with the changes to the event model and the new views, dragging and dropping events have more features now, like turning an event into an all-day event. row updates are done more specifically to the columns that actually need changes. the events collection is updated specifically, too. previously it was not updated and re-created after row changes did propagate. when there are full-day-dates due to start date without minute precision (all day events), and the end date is also w/o minute precision, then it needs to be noon (12:00) not start of day (00:00) as otherwise the day would not count as end (the year view seems most picky). it is only necessary to modify the end-date to make it into the all-day slot if it is on the same day. * create new events by selecting creating on the same-day slot a time-range creates a new event. if the events start date can not be edited (e.g. formula), the selection is prevented (handleSelecting() implementation). NOTE: intentionally new events are without a title (cdb, rdb) * minor fix redundant variable * update code: 1. adjust some styles; 2. update translation; 3. fix move event if the end date comes from the formula column; 4. check whether the container in the WeekWrapper is valid. Co-authored-by: renjie-run --- public/locale/de/calendar.json | 8 +- public/locale/en/calendar.json | 2 +- public/locale/fr/calendar.json | 2 +- public/locale/ru/calendar.json | 52 +++++++++++++ public/locale/zh_CN/calendar.json | 22 +++--- src/Calendar.js | 12 +++ src/ReactBigCalendar.js | 99 ++++++++++++++++++++++--- src/addons/dragAndDrop/WeekWrapper.js | 79 ++++++++++---------- src/app.js | 5 ++ src/components/grid-views/Agenda.js | 8 +- src/components/grid-views/Month.js | 3 +- src/components/header/TimeGridHeader.js | 2 +- src/components/selection/Selection.js | 19 ++++- src/components/time/TimeGridEvent.js | 18 +++++ src/css/react-big-calendar.css | 42 +++++++++-- src/locale/lang/de.js | 8 +- src/locale/lang/en.js | 2 +- src/locale/lang/fr.js | 2 +- src/locale/lang/zh_CN.js | 22 +++--- src/model/event.js | 34 +++++++-- 20 files changed, 336 insertions(+), 105 deletions(-) create mode 100644 public/locale/ru/calendar.json diff --git a/public/locale/de/calendar.json b/public/locale/de/calendar.json index 95c767d..986f93c 100644 --- a/public/locale/de/calendar.json +++ b/public/locale/de/calendar.json @@ -7,15 +7,15 @@ "Color_From": "Farbe", "Unnamed_record": "Neuer Eintrag", "more": "mehr", - ".rbc.localizer.moment.dateFormat": "DD", - ".rbc.localizer.moment.dayFormat": "DD ddd", + ".rbc.localizer.moment.dateFormat": "DD.", + ".rbc.localizer.moment.dayFormat": "ddd, DD.", ".rbc.localizer.moment.weekdayFormat": "ddd", ".rbc.localizer.moment.monthFormat": "MMMM", ".rbc.localizer.moment.yearHeaderFormat": "YYYY", ".rbc.localizer.moment.dayHeaderFormat": "dddd, DD. MMMM", - ".rbc.localizer.moment.agendaDateFormat": "ddd MMM DD", + ".rbc.localizer.moment.agendaDateFormat": "ddd, DD. MMMM", ".rbc.localizer.moment.yearMonthWeekdayFormat": "ddd", - ".rbc.localizer.moment.yearMonthDateFormat": "DD", + ".rbc.localizer.moment.yearMonthDateFormat": "D", ".rbc.localizer.moment.weekOfYearFormat": "WW", ".rbc.messages.date": "Datum", ".rbc.messages.time": "Zeit", diff --git a/public/locale/en/calendar.json b/public/locale/en/calendar.json index 24be39e..5cbae4a 100644 --- a/public/locale/en/calendar.json +++ b/public/locale/en/calendar.json @@ -15,7 +15,7 @@ ".rbc.localizer.moment.dayHeaderFormat": "MM-dd-DD", ".rbc.localizer.moment.agendaDateFormat": "ddd MMM DD", ".rbc.localizer.moment.yearMonthWeekdayFormat": "dd", - ".rbc.localizer.moment.yearMonthDateFormat": "DD", + ".rbc.localizer.moment.yearMonthDateFormat": "D", ".rbc.localizer.moment.weekOfYearFormat": "WW", ".rbc.messages.date": "Date", ".rbc.messages.time": "Time", diff --git a/public/locale/fr/calendar.json b/public/locale/fr/calendar.json index 95de689..5ef25fa 100644 --- a/public/locale/fr/calendar.json +++ b/public/locale/fr/calendar.json @@ -15,7 +15,7 @@ ".rbc.localizer.moment.dayHeaderFormat": "ddd MMM DD", ".rbc.localizer.moment.agendaDateFormat": "ddd MMM DD", ".rbc.localizer.moment.yearMonthWeekdayFormat": "dd", - ".rbc.localizer.moment.yearMonthDateFormat": "DD", + ".rbc.localizer.moment.yearMonthDateFormat": "D", ".rbc.localizer.moment.weekOfYearFormat": "WW", ".rbc.messages.date": "Date", ".rbc.messages.time": "Heure", diff --git a/public/locale/ru/calendar.json b/public/locale/ru/calendar.json new file mode 100644 index 0000000..cc21692 --- /dev/null +++ b/public/locale/ru/calendar.json @@ -0,0 +1,52 @@ +{ + "Calendar": "Календарь", + "Submit": "Сохранить", + "Cancel": "Отмена", + "Start_Date": "Поле дата начала", + "End_Date_Optional": "Поле дата окончания (необязательно)", + "Color_From": "Цвет поля", + "Unnamed_record": "Нет имени", + "more": "больше", + ".rbc.localizer.moment.dateFormat": "DD", + ".rbc.localizer.moment.dayFormat": "DD ddd", + ".rbc.localizer.moment.weekdayFormat": "ддд", + ".rbc.localizer.moment.monthFormat": "МММ", + ".rbc.localizer.moment.yearHeaderFormat": "ГГГГ", + ".rbc.localizer.moment.dayHeaderFormat": "ММ-дд-ДД", + ".rbc.localizer.moment.agendaDateFormat": "ddd MMM DD", + ".rbc.localizer.moment.yearMonthWeekdayFormat": "дд", + ".rbc.localizer.moment.yearMonthDateFormat": "D", + ".rbc.localizer.moment.weekOfYearFormat": "WW", + ".rbc.messages.date": "Дата", + ".rbc.messages.time": "Время", + ".rbc.messages.event": "Событие", + ".rbc.messages.allDay": "Весь день", + ".rbc.messages.week": "Неделя", + ".rbc.messages.work_week": "Рабочая неделя", + ".rbc.messages.day": "День", + ".rbc.messages.month": "Месяц", + ".rbc.messages.previous": "Назад", + ".rbc.messages.next": "Вперед", + ".rbc.messages.yesterday": "Вчера", + ".rbc.messages.tomorrow": "завтрашний день", + ".rbc.messages.today": "Сегодня", + ".rbc.messages.agenda": "Повестка дня", + ".rbc.messages.year": "Год", + ".rbc.messages.noEventsInRange": "В этом диапазоне событий нет.", + "No_date_field_to_place_records_on_the_calendar": "Нет поля с датой для размещения записей в календаре.", + "There_are_no_records": "Запись отсутствует.", + "Default_View": "Вид по умолчанию", + "New_View": "Новый Вид", + "Rename_View": "Переименовать Вид", + "Delete_View": "Удалить Вид", + "Name": "Имя", + "Name_is_required": "Имя обязательно", + "Settings": "Настройки", + "Table": "Таблица", + "View": "Вид", + "Title": "Поле заголовка", + "No_options": "Нет опций", + "Not_used": "Не используется", + "Choose_the_time_range": "Выберите временной диапазон", + "Out_of_range": "Можно выбрать не более 12 месяцев." +} diff --git a/public/locale/zh_CN/calendar.json b/public/locale/zh_CN/calendar.json index 142abf7..d32aaaf 100644 --- a/public/locale/zh_CN/calendar.json +++ b/public/locale/zh_CN/calendar.json @@ -15,24 +15,24 @@ ".rbc.localizer.moment.dayHeaderFormat": "MMMDD日 星期dd", ".rbc.localizer.moment.agendaDateFormat": "ddd MMM DD", ".rbc.localizer.moment.yearMonthWeekdayFormat": "dd", - ".rbc.localizer.moment.yearMonthDateFormat": "DD", + ".rbc.localizer.moment.yearMonthDateFormat": "D", ".rbc.localizer.moment.weekOfYearFormat": "WW", - ".rbc.messages.date": "Date", - ".rbc.messages.time": "Time", - ".rbc.messages.event": "Event", - ".rbc.messages.allDay": "All Day", + ".rbc.messages.date": "日期", + ".rbc.messages.time": "时间", + ".rbc.messages.event": "事件", + ".rbc.messages.allDay": "全天", ".rbc.messages.week": "周", - ".rbc.messages.work_week": "Work Week", + ".rbc.messages.work_week": "工作周", ".rbc.messages.day": "日", ".rbc.messages.month": "月", - ".rbc.messages.previous": "Back", - ".rbc.messages.next": "Next", - ".rbc.messages.yesterday": "Yesterday", - ".rbc.messages.tomorrow": "Tomorrow", + ".rbc.messages.previous": "返回", + ".rbc.messages.next": "下一步", + ".rbc.messages.yesterday": "昨天", + ".rbc.messages.tomorrow": "明天", ".rbc.messages.today": "今天", ".rbc.messages.agenda": "议程", ".rbc.messages.year": "年", - ".rbc.messages.noEventsInRange": "There are no events in this range.", + ".rbc.messages.noEventsInRange": "此范围内没有事件。", "No_date_field_to_place_records_on_the_calendar": "没有用于将记录放置在日历上的日期字段。", "There_are_no_records": "没有记录。", "Default_View": "默认视图", diff --git a/src/Calendar.js b/src/Calendar.js index ed9bfa3..e5db23d 100644 --- a/src/Calendar.js +++ b/src/Calendar.js @@ -749,6 +749,18 @@ class Calendar extends React.Component { }) { let names = viewNames(views); const msgs = message(messages); + + components[CALENDAR_VIEWS.WEEK] = { + timeGutterHeader: (props) => { + const { localizer, range } = props; + return ( +
+ {localizer.format(range[1], 'weekOfYearFormat')} +
+ ); + } + }; + return { viewNames: names, localizer: mergeWithDefaults(localizer, culture, formats, msgs), diff --git a/src/ReactBigCalendar.js b/src/ReactBigCalendar.js index 7d4a245..bd9265d 100644 --- a/src/ReactBigCalendar.js +++ b/src/ReactBigCalendar.js @@ -32,7 +32,7 @@ const propTypes = { getRowById: PropTypes.func, }; -const calendarViews = [CALENDAR_VIEWS.YEAR, CALENDAR_VIEWS.MONTH]; +const calendarViews = [CALENDAR_VIEWS.YEAR, CALENDAR_VIEWS.MONTH, CALENDAR_VIEWS.WEEK, CALENDAR_VIEWS.DAY, CALENDAR_VIEWS.AGENDA]; const localizer = momentLocalizer(moment); class ReactBigCalendar extends React.Component { @@ -160,15 +160,14 @@ class ReactBigCalendar extends React.Component { const { key: endDateColumnKey, name: endDateColumnName, type: endDateColumnType } = endDateColumn || {}; const title = this.getEventTitle(rawRow, titleColumnType, titleColumnName); const date = startDateColumnType === 'formula' ? rawRow[startDateColumnName] : row[startDateColumnKey]; - // start date must exist and valid. + // start date must exist and be valid. if (!date || !isValidDateObject(new Date(date))) { return null; } const endDate = this.getEventEndDate(rawRow, row, endDateColumnType, endDateColumnName, endDateColumnKey, date); - // Invalid event: - // 1. invalid end date - // 2. duration less than 0 between end date with start date. - if (endDate && (!isValidDateObject(new Date(endDate)) || moment(endDate).isBefore(date))) { + // end date if exists must be valid + // NOTE: end date might be before start date, this is delegated to TableEvent() + if (endDate && !isValidDateObject(new Date(endDate))) { return null; } const eventColors = TableEvent.getColors({row, colorColumn, optionColors, highlightColors}); @@ -179,6 +178,8 @@ class ReactBigCalendar extends React.Component { this.props.onRowExpand(row, this.props.activeTable); } + onSelectEvent = ({row}) => this.onRowExpand(row); + onInsertRow = (rowData) => { let { activeTable, activeView, onInsertRow, rows } = this.props; let row_id = rows.length > 0 ? rows[rows.length - 1]._id : ''; @@ -190,10 +191,47 @@ class ReactBigCalendar extends React.Component { return moment(date).format(targetFormat); } + /** + * create new event row + * + * @param {Date} start + * @param {Date} end + */ + createEvent = ({ start, end }) => { + const { CellType, activeTable, appendRow, setting: {settings} } = this.props; + const startDateColumnName = settings[SETTING_KEY.COLUMN_START_DATE]; + const endDateColumnName = settings[SETTING_KEY.COLUMN_END_DATE]; + const startDateColumn = this.getDateColumn(startDateColumnName); + const endDateColumn = endDateColumnName ? this.getDateColumn(endDateColumnName) : null; + if (startDateColumn.type !== CellType.DATE) { + return; + } + const rowData = {}; + const startDateFormat = startDateColumn.data && startDateColumn.data.format; + const startDateMinutePrecision = startDateFormat && startDateFormat.indexOf('HH:mm') > -1; + rowData[startDateColumn.name] = this.getFormattedDate(start, startDateFormat); + if (startDateMinutePrecision) { + switch (endDateColumn.type) { + case CellType.DATE: + if (endDateColumn !== startDateColumn) { + rowData[endDateColumn.name] = this.getFormattedDate(end, endDateColumn.data && endDateColumn.data.format); + } + break; + case CellType.DURATION: + rowData[endDateColumn.name] = (Math.abs(end - start) / 1000).toFixed(); + break; + default: + break; + } + } + appendRow(activeTable, rowData); + } + moveEvent = ({ event, start, end, isAllDay: droppedOnAllDaySlot }) => { + const { events } = this.state; + const idx = events.indexOf(event); let updatedData = {}; - let { activeTable, modifyRow, setting, CellType } = this.props; - const { settings = {} } = setting; + let { activeTable, modifyRow, setting: {settings}, CellType, getRowById} = this.props; const startDateColumnName = settings[SETTING_KEY.COLUMN_START_DATE]; const endDateColumnName = settings[SETTING_KEY.COLUMN_END_DATE]; let startDateColumn = this.getDateColumn(startDateColumnName); @@ -204,19 +242,55 @@ class ReactBigCalendar extends React.Component { return; } const startDateFormat = data && data.format; + const startDateMinutePrecision = startDateFormat && startDateFormat.indexOf('HH:mm') > -1; updatedData[startDateColumn.name] = this.getFormattedDate(start, startDateFormat); + if (!droppedOnAllDaySlot && startDateMinutePrecision && event.allDay) { + event.allDay = false; + end = new Date(start.valueOf()); + end.setHours(end.getHours() + TableEvent.FIXED_PERIOD_OF_TIME_IN_HOURS); + } + if (droppedOnAllDaySlot && startDateMinutePrecision) { + // an event can only be made all-day if it has a true end-date field when its start-date is with + // time (with minute precision) [if an event is across two days, it is also displayed on top] + if (endDateColumn && (endDateColumn !== startDateColumn) && endDateColumn.type === CellType.DATE) { + const startEndSameDay = moment(start).format('YYYY-MM-DD') === moment(end).format('YYYY-MM-DD'); + if (startEndSameDay) { + end = moment(end).add(1, 'day').startOf('day').toDate(); + } + } + } } if (endDateColumn) { const { type, data } = endDateColumn; if (type === CellType.FORMULA) { - return; + end = event.end; // the end date get from the formula column is read only. } - if (type !== CellType.DURATION) { + if (type === CellType.DATE) { const endDateFormat = data && data.format; updatedData[endDateColumn.name] = this.getFormattedDate(end, endDateFormat); } } modifyRow(activeTable, event.row, updatedData); + + const updatedEvent = { ...event, start, end, ...{row: getRowById(activeTable, event.row._id)}}; + const nextEvents = [...events]; + nextEvents.splice(idx, 1, updatedEvent); + this.setState({ + events: nextEvents + }); + } + + handleSelectSlot = ({ action, start, end}) => { + if (action === 'select') { + this.createEvent({start, end}); + } + } + + handleSelecting = ({ start, end }) => { + const { CellType, setting: {settings} } = this.props; + const startDateColumnName = settings[SETTING_KEY.COLUMN_START_DATE]; + const startDateColumn = this.getDateColumn(startDateColumnName); + return startDateColumn.type === CellType.DATE; } render() { @@ -235,9 +309,12 @@ class ReactBigCalendar extends React.Component { onSelectView={this.onSelectView} defaultDate={new Date()} onRowExpand={this.onRowExpand} + onSelectEvent={this.onSelectEvent} onInsertRow={this.onInsertRow} hideViewSettingPanel={this.props.hideViewSettingPanel} - selectable={true} + selectable + onSelectSlot={this.handleSelectSlot} + onSelecting={this.handleSelecting} onEventDrop={this.moveEvent} isExporting={this.props.isExporting} exportedMonths={this.props.exportedMonths} diff --git a/src/addons/dragAndDrop/WeekWrapper.js b/src/addons/dragAndDrop/WeekWrapper.js index 1b945c3..75b9d80 100644 --- a/src/addons/dragAndDrop/WeekWrapper.js +++ b/src/addons/dragAndDrop/WeekWrapper.js @@ -9,8 +9,6 @@ import Selection, { getBoundsForNode } from '../../components/selection/Selectio import EventRow from '../../components/rows/EventRow'; import { dragAccessors } from './common'; -import DraggingLayer from './dragging-layer'; - const propTypes = {}; const eventTimes = (event, accessors) => { @@ -140,6 +138,8 @@ class WeekWrapper extends React.Component { const { accessors, slotMetrics: metrics } = this.props; let { start, end } = eventTimes(event, accessors); + let originalStart = start; + let originalEnd = end; let rowBox = getBoundsForNode(node); let cursorInRow = pointInBox(rowBox, point); @@ -147,13 +147,8 @@ class WeekWrapper extends React.Component { if (direction === 'RIGHT') { if (cursorInRow) { if (metrics.last < start) return this.reset(); - // add min - end = dates.add( - metrics.getDateForSlot( - getSlotAtX(rowBox, point.x, false, metrics.slots) - ), - 1, - 'day' + end = metrics.getDateForSlot( + getSlotAtX(rowBox, point.x, false, metrics.slots) ); } else if ( dates.inRange(start, metrics.first, metrics.last) || @@ -164,10 +159,12 @@ class WeekWrapper extends React.Component { this.setState({ segment: null }); return; } - - end = dates.max(end, dates.add(start, 1, 'day')); + end = dates.merge(end, accessors.end(event)); + if (dates.lt(end, start)) { + end = originalEnd; + } } else if (direction === 'LEFT') { - // inbetween Row + // in-between Row if (cursorInRow) { if (metrics.first > end) return this.reset(); @@ -183,8 +180,10 @@ class WeekWrapper extends React.Component { this.reset(); return; } - - start = dates.min(dates.add(end, -1, 'day'), start); + start = dates.merge(start, accessors.start(event)); + if (dates.gt(start, end)) { + start = originalStart; + } } this.update(event, start, end); @@ -193,8 +192,16 @@ class WeekWrapper extends React.Component { _selectable = () => { let node = findDOMNode(this).closest('.rbc-month-row, .rbc-allday-cell'); let container = node.closest('.rbc-month-view, .rbc-time-view'); + if (!container) { + return; + } + let allowedTargets = container.querySelectorAll( + '.rbc-day-slot, .rbc-allday-cell' + ); - let selector = (this._selector = new Selection(() => container)); + let selector = (this._selector = new Selection(() => container, { + allowedTargets: () => allowedTargets, + })); selector.on('beforeSelect', point => { const { isAllDay } = this.props; @@ -211,7 +218,6 @@ class WeekWrapper extends React.Component { const bounds = getBoundsForNode(node); const { dragAndDropAction } = this.context.draggable; - this.setState({box}); if (dragAndDropAction.action === 'move') this.handleMove(box, bounds); if (dragAndDropAction.action === 'resize') this.handleResize(box, bounds); }); @@ -220,8 +226,13 @@ class WeekWrapper extends React.Component { selector.on('select', point => { const bounds = getBoundsForNode(node); - if (!this.state.segment || !pointInBox(bounds, point)) return; - this.handleInteractionEnd(); + if (!this.state.segment) return; + + if (!pointInBox(bounds, point)) { + this.reset(); + } else { + this.handleInteractionEnd(); + } }); selector.on('dropFromOutside', point => { @@ -273,33 +284,23 @@ class WeekWrapper extends React.Component { render() { const { children, accessors } = this.props; - let { segment, box } = this.state; - - const { interacting, currentEventEl } = this.context.draggable.dragAndDropAction; + let { segment } = this.state; return (
{children} {segment && ( - - - {interacting && - - } - + )}
); diff --git a/src/app.js b/src/app.js index f428c7e..9da3ef7 100644 --- a/src/app.js +++ b/src/app.js @@ -394,6 +394,10 @@ class App extends React.Component { this.dtable.modifyRow(table, row, updated); } + appendRow = (table, rowData) => { + this.dtable.appendRow(table, rowData); + } + render() { let { isLoading, showDialog, plugin_settings, selectedViewIdx, rows, @@ -439,6 +443,7 @@ class App extends React.Component { columns={columns} rows={rows} getRowById={this.dtable.getRowById} + appendRow={this.appendRow} modifyRow={this.modifyRow} setting={currentSetting} CellType={this.cellType} diff --git a/src/components/grid-views/Agenda.js b/src/components/grid-views/Agenda.js index df8fe4c..7ed4296 100755 --- a/src/components/grid-views/Agenda.js +++ b/src/components/grid-views/Agenda.js @@ -11,6 +11,7 @@ import { navigate } from '../../constants'; import intl from 'react-intl-universal'; class Agenda extends React.Component { + constructor(props) { super(props); this.headerRef = React.createRef(); @@ -171,17 +172,14 @@ class Agenda extends React.Component { let isOverflowing = this.contentRef.current.scrollHeight > this.contentRef.current.clientHeight; - let widths = this._widths || []; this._widths = [ getWidth(firstRow.children[0]), getWidth(firstRow.children[1]) ]; - if (widths[0] !== this._widths[0] || widths[1] !== this._widths[1]) { - this.dateColRef.current.style.width = this._widths[0] + 'px'; - this.timeColRef.current.style.width = this._widths[1] + 'px'; - } + this.dateColRef.current.style.width = this._widths[0] + 'px'; + this.timeColRef.current.style.width = this._widths[1] + 'px'; if (isOverflowing) { addClass(header, 'rbc-header-overflowing'); diff --git a/src/components/grid-views/Month.js b/src/components/grid-views/Month.js index 7e01a69..1deb413 100755 --- a/src/components/grid-views/Month.js +++ b/src/components/grid-views/Month.js @@ -127,8 +127,7 @@ class MonthView extends React.Component { let renderedRowsCount = getRenderedRowsCount(monthRowsHeight); let overRowsCount = scrollTop / MONTH_ROW_HEIGHT; let fract = overRowsCount - Math.trunc(overRowsCount); - let overDatesCount = Math.ceil(overRowsCount) - 1; - let visibleStartIndex = overDatesCount; + let visibleStartIndex = Math.ceil(overRowsCount) - 1; if (isNextMonth(date, allWeeksStartDates, visibleStartIndex)) { this.isScrolling = false; let nextMonthDate = getNextMonthDate(allWeeksStartDates, visibleStartIndex); diff --git a/src/components/header/TimeGridHeader.js b/src/components/header/TimeGridHeader.js index 8ade7e1..d4988fc 100755 --- a/src/components/header/TimeGridHeader.js +++ b/src/components/header/TimeGridHeader.js @@ -141,7 +141,7 @@ class TimeGridHeader extends React.Component { className='rbc-label rbc-time-header-gutter' style={{ width, minWidth: width, maxWidth: width }} > - {TimeGutterHeader && } + {TimeGutterHeader && } {resources.map(([id, resource], idx) => ( diff --git a/src/components/selection/Selection.js b/src/components/selection/Selection.js index e495618..76f4aec 100755 --- a/src/components/selection/Selection.js +++ b/src/components/selection/Selection.js @@ -39,11 +39,12 @@ const clickInterval = 250; class Selection { - constructor(node, { global = false, longPressThreshold = 250 } = {}) { + constructor(node, { global = false, longPressThreshold = 250, allowedTargets = () => [] } = {}) { this.isDetached = false; this.container = node; this.globalMouse = !node || global; this.longPressThreshold = longPressThreshold; + this.getAllowedTargets = allowedTargets; this._listeners = Object.create(null); this._handleInitialEvent = this._handleInitialEvent.bind(this); this._handleMoveEvent = this._handleMoveEvent.bind(this); @@ -311,6 +312,7 @@ class Selection { if (!this._initialEventData) return; let inRoot = !this.container || contains(this.container(), e.target); + let inAllowedTarget = this._checkIfInAllowedTarget(e); let bounds = this._selectRect; let click = this.isClick(pageX, pageY); @@ -320,7 +322,7 @@ class Selection { return this.emit('reset'); } - if (!inRoot) { + if (!inRoot || !inAllowedTarget) { return this.emit('reset'); } @@ -332,6 +334,19 @@ class Selection { if (!click) return this.emit('select', bounds); } + _checkIfInAllowedTarget(e) { + let allowedTargets = this.getAllowedTargets(); + if (!allowedTargets || !allowedTargets.length) return true; + let inAllowedTarget = false; + allowedTargets.forEach(n => { + if (contains(n, e.target)) { + inAllowedTarget = true; + } + }); + + return inAllowedTarget; + } + _handleClickEvent(e) { const { pageX, pageY, clientX, clientY } = getEventCoordinates(e); const now = new Date().getTime(); diff --git a/src/components/time/TimeGridEvent.js b/src/components/time/TimeGridEvent.js index 87d60d7..0a1f46b 100755 --- a/src/components/time/TimeGridEvent.js +++ b/src/components/time/TimeGridEvent.js @@ -5,6 +5,23 @@ function stringifyPercent(v) { return typeof v === 'string' ? v : v + '%'; } +/** + * duplication from EventCell.getRbcEventStyle for colored style of week/day events + * + * @see EventCell.getRbcEventStyle + * @param props + * @return {{borderLeft: string, color, background}} + */ +const getRbcEventStyle = (props) => { + const { event } = props; + const { bgColor, highlightColor, textColor } = event; + return { + background: bgColor, + borderLeft: highlightColor && `3px solid ${highlightColor}`, + color: textColor, + }; +}; + /* eslint-disable react/prop-types */ function TimeGridEvent(props) { const { @@ -45,6 +62,7 @@ function TimeGridEvent(props) { onClick={onClick} onDoubleClick={onDoubleClick} style={{ + ...getRbcEventStyle(props), ...userProps.style, top: stringifyPercent(top), [rtl ? 'right' : 'left']: stringifyPercent(xOffset), diff --git a/src/css/react-big-calendar.css b/src/css/react-big-calendar.css index 46e32ba..0966ca7 100644 --- a/src/css/react-big-calendar.css +++ b/src/css/react-big-calendar.css @@ -93,8 +93,11 @@ button.rbc-input::-moz-focus-inner { text-decoration: none; } +/* maximum event rows: 10 */ .rbc-row-content { position: relative; + max-height: 250px; + overflow-y: auto; user-select: none; -webkit-user-select: none; z-index: 4; @@ -260,6 +263,7 @@ button.rbc-input::-moz-focus-inner { padding: 0 12px; border-radius: 5px; background: transparent; + white-space: nowrap; } .rbc-btn-group .view-type-list .rbc-view-type:hover { @@ -665,7 +669,7 @@ button.rbc-input::-moz-focus-inner { } .rbc-day-slot .rbc-event { - border: 1px solid #265985; + border: 0 solid #265985; display: flex; max-height: 100%; min-height: 20px; @@ -772,6 +776,14 @@ button.rbc-input::-moz-focus-inner { white-space: nowrap; } +.rbc-time-view .rbc-time-header-gutter .rbc-week-of-year { + height: 100%; + width: 100%; + text-align: center; + vertical-align: middle; + line-height: 62px +} + .rbc-time-view .rbc-allday-cell { box-sizing: content-box; width: 100%; @@ -799,10 +811,6 @@ button.rbc-input::-moz-focus-inner { flex-direction: row; } -.rbc-time-header.rbc-overflowing { - border-right: 1px solid #ddd; -} - .rbc-rtl .rbc-time-header.rbc-overflowing { border-right-width: 0; border-left: 1px solid #ddd; @@ -816,6 +824,19 @@ button.rbc-input::-moz-focus-inner { border-bottom: 1px solid #ddd; } +.rbc-time-header-cell .rbc-header + .rbc-header { + border-left: 1px solid #ddd; +} + +.rbc-time-header-cell .rbc-header button{ + width: 100%; + padding: 0; + border: 0; + cursor: pointer; + background: none; + outline: 0; +} + .rbc-time-header-cell-single-day { display: none; } @@ -1022,3 +1043,14 @@ button.rbc-input::-moz-focus-inner { border-radius: 50%; background-color: #F25041; } + +.rbc-agenda-empty { + flex-grow: 0.618; + display: flex; + align-items: center; + justify-content: center; +} + +.rbc-agenda-content { + overflow-y: auto; +} diff --git a/src/locale/lang/de.js b/src/locale/lang/de.js index 700328b..2a9bc2f 100644 --- a/src/locale/lang/de.js +++ b/src/locale/lang/de.js @@ -7,15 +7,15 @@ const de = { 'Color_From': 'Farbe', 'Unnamed_record': 'Neuer Eintrag', 'more': 'mehr', - '.rbc.localizer.moment.dateFormat': 'DD', - '.rbc.localizer.moment.dayFormat': 'DD ddd', + '.rbc.localizer.moment.dateFormat': 'DD.', + '.rbc.localizer.moment.dayFormat': 'ddd, DD.', '.rbc.localizer.moment.weekdayFormat': 'ddd', '.rbc.localizer.moment.monthFormat': 'MMMM', '.rbc.localizer.moment.yearHeaderFormat': 'YYYY', '.rbc.localizer.moment.dayHeaderFormat': 'dddd, DD. MMMM', - '.rbc.localizer.moment.agendaDateFormat': 'ddd MMM DD', + '.rbc.localizer.moment.agendaDateFormat': 'ddd, DD. MMMM', '.rbc.localizer.moment.yearMonthWeekdayFormat': 'ddd', - '.rbc.localizer.moment.yearMonthDateFormat': 'DD', + '.rbc.localizer.moment.yearMonthDateFormat': 'D', '.rbc.localizer.moment.weekOfYearFormat': 'WW', '.rbc.messages.date': 'Datum', '.rbc.messages.time': 'Zeit', diff --git a/src/locale/lang/en.js b/src/locale/lang/en.js index fa7fa6a..353cc43 100644 --- a/src/locale/lang/en.js +++ b/src/locale/lang/en.js @@ -15,7 +15,7 @@ const en = { '.rbc.localizer.moment.dayHeaderFormat': 'MM-dd-DD', '.rbc.localizer.moment.agendaDateFormat': 'ddd MMM DD', '.rbc.localizer.moment.yearMonthWeekdayFormat': 'dd', - '.rbc.localizer.moment.yearMonthDateFormat': 'DD', + '.rbc.localizer.moment.yearMonthDateFormat': 'D', '.rbc.localizer.moment.weekOfYearFormat': 'WW', '.rbc.messages.date': 'Date', '.rbc.messages.time': 'Time', diff --git a/src/locale/lang/fr.js b/src/locale/lang/fr.js index c310d8b..2cda8dd 100644 --- a/src/locale/lang/fr.js +++ b/src/locale/lang/fr.js @@ -15,7 +15,7 @@ const fr = { '.rbc.localizer.moment.dayHeaderFormat': 'ddd MMM DD', '.rbc.localizer.moment.agendaDateFormat': 'ddd MMM DD', '.rbc.localizer.moment.yearMonthWeekdayFormat': 'dd', - '.rbc.localizer.moment.yearMonthDateFormat': 'DD', + '.rbc.localizer.moment.yearMonthDateFormat': 'D', '.rbc.localizer.moment.weekOfYearFormat': 'WW', '.rbc.messages.date': 'Date', '.rbc.messages.time': 'Heure', diff --git a/src/locale/lang/zh_CN.js b/src/locale/lang/zh_CN.js index eaa5463..1893a32 100644 --- a/src/locale/lang/zh_CN.js +++ b/src/locale/lang/zh_CN.js @@ -15,24 +15,24 @@ const zh_CN = { '.rbc.localizer.moment.dayHeaderFormat': 'MMMDD日 星期dd', '.rbc.localizer.moment.agendaDateFormat': 'ddd MMM DD', '.rbc.localizer.moment.yearMonthWeekdayFormat': 'dd', - '.rbc.localizer.moment.yearMonthDateFormat': 'DD', + '.rbc.localizer.moment.yearMonthDateFormat': 'D', '.rbc.localizer.moment.weekOfYearFormat': 'WW', - '.rbc.messages.date': 'Date', - '.rbc.messages.time': 'Time', - '.rbc.messages.event': 'Event', - '.rbc.messages.allDay': 'All Day', + '.rbc.messages.date': '日期', + '.rbc.messages.time': '时间', + '.rbc.messages.event': '事件', + '.rbc.messages.allDay': '全天', '.rbc.messages.week': '周', - '.rbc.messages.work_week': 'Work Week', + '.rbc.messages.work_week': '工作周', '.rbc.messages.day': '日', '.rbc.messages.month': '月', - '.rbc.messages.previous': 'Back', - '.rbc.messages.next': 'Next', - '.rbc.messages.yesterday': 'Yesterday', - '.rbc.messages.tomorrow': 'Tomorrow', + '.rbc.messages.previous': '返回', + '.rbc.messages.next': '下一步', + '.rbc.messages.yesterday': '昨天', + '.rbc.messages.tomorrow': '明天', '.rbc.messages.today': '今天', '.rbc.messages.agenda': '议程', '.rbc.messages.year': '年', - '.rbc.messages.noEventsInRange': 'There are no events in this range.', + '.rbc.messages.noEventsInRange': '此范围内没有事件。', 'No_date_field_to_place_records_on_the_calendar': '没有用于将记录放置在日历上的日期字段。', 'There_are_no_records': '没有记录。', 'Default_View': '默认视图', diff --git a/src/model/event.js b/src/model/event.js index 1df1824..49738a0 100644 --- a/src/model/event.js +++ b/src/model/event.js @@ -81,18 +81,27 @@ * given the date-in-row is without minute precision, when representing * the start date of an event, then the event all-day is true. * - * otherwise all-day is the defined-all-day. + * given the date-in-row is with minute precision, when representing the + * start date of an event and there is no end-date-in-row, then the + * event all-day is true, as well. + * + * otherwise all-day is false * * @see TableEvent.constructor() * * @param {Date|undefined} eventStart * @param {string|undefined} rowDate - * @param {boolean|undefined} defAllDay + * @param {string|undefined} rowEndDate */ -const allDayImplementation = (eventStart, rowDate) => { +const allDayImplementation = (eventStart, rowDate, rowEndDate) => { + // rowDate is DATE, not DATETIME if (eventStart && rowDate && (eventStart.toISOString().slice(0, 10) === rowDate)) { return true; } + // rowDate is DATETIME, not DATE + if (eventStart && rowDate && !rowEndDate) { + return false; + } return false; }; @@ -109,12 +118,25 @@ const allDayImplementation = (eventStart, rowDate) => { * @param {Date|undefined} eventStart * @param {boolean|undefined} eventAllDay * @param {string|undefined} rowDate + * @return {Date|undefined} */ const endImplementation = (eventStart, eventAllDay, rowDate) => { - let end = rowDate ? new Date(rowDate) : eventStart; - if (eventAllDay !== true && !rowDate) { + let end; + if (rowDate) { + end = new Date(rowDate); + // given the date-in-row is without minute precision + if (end.toISOString().slice(0, 10) === rowDate) { + end.setHours(12); + end.setMinutes(0); + } + } else { + end = eventStart; + } + if ((eventAllDay !== true) && eventStart && (rowDate === undefined || end <= eventStart)) { end = new Date(+eventStart); end.setHours(eventStart.getHours() + TableEvent.FIXED_PERIOD_OF_TIME_IN_HOURS); + } else if (eventStart && (rowDate === undefined || end <= eventStart)) { + end = new Date(+eventStart); } return end; }; @@ -178,7 +200,7 @@ export default class TableEvent { /* 2/2: React-Big-Calendar event properties */ this.title = object.title || null; this.start = object.date && new Date(object.date); - this.allDay = allDayImplementation(this.start, object.date); + this.allDay = allDayImplementation(this.start, object.date, object.endDate); this.end = endImplementation(this.start, this.allDay, object.endDate); } }