mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-26 10:00:50 +08:00
feat(scheduler): add new date & time picker
Summary: Adds a date and time picker to the new event creator Test Plan: todo Reviewers: bengotow, juan Reviewed By: bengotow, juan Differential Revision: https://phab.nylas.com/D2842
This commit is contained in:
parent
a3fe0f4d71
commit
bb318bf69c
13 changed files with 594 additions and 128 deletions
|
@ -1,11 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import moment from 'moment-timezone'
|
import moment from 'moment-timezone'
|
||||||
import {RetinaImg} from 'nylas-component-kit'
|
import {
|
||||||
|
RetinaImg,
|
||||||
|
DatePicker,
|
||||||
|
TimePicker,
|
||||||
|
TabGroupRegion,
|
||||||
|
} from 'nylas-component-kit'
|
||||||
import {PLUGIN_ID} from '../scheduler-constants'
|
import {PLUGIN_ID} from '../scheduler-constants'
|
||||||
|
|
||||||
import ProposedTimeList from './proposed-time-list'
|
import ProposedTimeList from './proposed-time-list'
|
||||||
import EventDatetimeInput from './event-datetime-input'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Utils,
|
Utils,
|
||||||
|
@ -103,28 +107,79 @@ export default class NewEventCard extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_eventStart() {
|
||||||
|
return moment.unix(this.props.event.start || moment().unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventEnd() {
|
||||||
|
return moment.unix(this.props.event.end || moment().unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangeDay = (newTimestamp) => {
|
||||||
|
const newDay = moment(newTimestamp)
|
||||||
|
const start = this._eventStart()
|
||||||
|
const end = this._eventEnd()
|
||||||
|
start.year(newDay.year())
|
||||||
|
end.year(newDay.year())
|
||||||
|
start.dayOfYear(newDay.dayOfYear())
|
||||||
|
end.dayOfYear(newDay.dayOfYear())
|
||||||
|
this.props.onChange({start: start.unix(), end: end.unix()})
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangeStartTime = (newTimestamp) => {
|
||||||
|
const newTime = moment(newTimestamp)
|
||||||
|
const start = this._eventStart()
|
||||||
|
const end = this._eventEnd()
|
||||||
|
start.hour(newTime.hour())
|
||||||
|
start.minute(newTime.minute())
|
||||||
|
let newEnd = moment(end)
|
||||||
|
if (end.isSameOrBefore(start)) {
|
||||||
|
const leftInDay = moment(start).endOf('day').diff(start)
|
||||||
|
const move = Math.min(leftInDay, moment.duration(1, 'hour').asMilliseconds());
|
||||||
|
newEnd = moment(start).add(move, 'ms')
|
||||||
|
}
|
||||||
|
this.props.onChange({start: start.unix(), end: newEnd.unix()})
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangeEndTime = (newTimestamp) => {
|
||||||
|
const newTime = moment(newTimestamp)
|
||||||
|
const start = this._eventStart()
|
||||||
|
const end = this._eventEnd()
|
||||||
|
end.hour(newTime.hour())
|
||||||
|
end.minute(newTime.minute())
|
||||||
|
let newStart = moment(start)
|
||||||
|
if (start.isSameOrAfter(end)) {
|
||||||
|
const sinceDay = end.diff(moment(end).startOf('day'))
|
||||||
|
const move = Math.min(sinceDay, moment.duration(1, 'hour').asMilliseconds());
|
||||||
|
newStart = moment(end).subtract(move, 'ms');
|
||||||
|
}
|
||||||
|
this.props.onChange({end: end.unix(), start: newStart.unix()})
|
||||||
|
}
|
||||||
|
|
||||||
_renderTimePicker() {
|
_renderTimePicker() {
|
||||||
const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID);
|
const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID);
|
||||||
if (metadata && metadata.proposals) {
|
if (metadata && metadata.proposals) {
|
||||||
return <ProposedTimeList event={this.props.event} proposals={metadata.proposals} />
|
return <ProposedTimeList event={this.props.event} proposals={metadata.proposals} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startVal = (this.props.event.start) * 1000;
|
||||||
|
const endVal = (this.props.event.end) * 1000;
|
||||||
return (
|
return (
|
||||||
<div className="row time">
|
<div className="row time">
|
||||||
{this._renderIcon("ic-eventcard-time@2x.png")}
|
{this._renderIcon("ic-eventcard-time@2x.png")}
|
||||||
<span>
|
<span>
|
||||||
<EventDatetimeInput name="start"
|
<TimePicker value={startVal} onChange={this._onChangeStartTime} />
|
||||||
value={this.props.event.start}
|
to
|
||||||
onChange={ date => this.props.onChange({start: date}) }
|
<TimePicker value={endVal} relativeTo={startVal}
|
||||||
/>
|
onChange={this._onChangeEndTime}
|
||||||
-
|
|
||||||
<EventDatetimeInput name="end"
|
|
||||||
reversed
|
|
||||||
value={this.props.event.end}
|
|
||||||
onChange={ date => this.props.onChange({end: date}) }
|
|
||||||
/>
|
/>
|
||||||
<span className="timezone">
|
<span className="timezone">
|
||||||
{moment().tz(Utils.timeZone).format("z")}
|
{moment().tz(Utils.timeZone).format("z")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
on
|
||||||
|
|
||||||
|
<DatePicker value={startVal} onChange={this._onChangeDay} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -169,6 +224,7 @@ export default class NewEventCard extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="new-event-card">
|
<div className="new-event-card">
|
||||||
|
<TabGroupRegion>
|
||||||
<div className="remove-button" onClick={this.props.onRemove}>✕</div>
|
<div className="remove-button" onClick={this.props.onRemove}>✕</div>
|
||||||
<div className="row title">
|
<div className="row title">
|
||||||
{this._renderIcon("ic-eventcard-description@2x.png")}
|
{this._renderIcon("ic-eventcard-description@2x.png")}
|
||||||
|
@ -214,6 +270,7 @@ export default class NewEventCard extends React.Component {
|
||||||
onChange={ e => this.props.onChange({description: e.target.value}) }
|
onChange={ e => this.props.onChange({description: e.target.value}) }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</TabGroupRegion>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,10 @@ import {
|
||||||
import {RetinaImg} from 'nylas-component-kit'
|
import {RetinaImg} from 'nylas-component-kit'
|
||||||
import {PLUGIN_ID, PLUGIN_NAME} from '../scheduler-constants'
|
import {PLUGIN_ID, PLUGIN_NAME} from '../scheduler-constants'
|
||||||
|
|
||||||
|
import moment from 'moment'
|
||||||
|
// moment-round upon require patches `moment` with new functions.
|
||||||
|
require('moment-round')
|
||||||
|
|
||||||
export default class SchedulerComposerButton extends React.Component {
|
export default class SchedulerComposerButton extends React.Component {
|
||||||
static displayName = "SchedulerComposerButton";
|
static displayName = "SchedulerComposerButton";
|
||||||
|
|
||||||
|
@ -78,8 +82,15 @@ editable calendar with your account provider.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const start = moment().ceil(30, 'minutes');
|
||||||
|
const end = moment(start).add(1, 'hour');
|
||||||
|
|
||||||
// TODO Have a default calendar config
|
// TODO Have a default calendar config
|
||||||
const event = new Event({calendarId: cals[0].id});
|
const event = new Event({
|
||||||
|
end: end.unix(),
|
||||||
|
start: start.unix(),
|
||||||
|
calendarId: cals[0].id,
|
||||||
|
});
|
||||||
this._session.changes.add({events: [event]});
|
this._session.changes.add({events: [event]});
|
||||||
this._session.changes.commit()
|
this._session.changes.commit()
|
||||||
})
|
})
|
||||||
|
|
|
@ -55,7 +55,16 @@
|
||||||
color: #A9A9A9;
|
color: #A9A9A9;
|
||||||
|
|
||||||
.timezone {
|
.timezone {
|
||||||
font-size: @font-size-smaller;
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.row.time {
|
||||||
|
.time-picker {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.time-picker-wrap {
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.datetime-input-container {
|
.datetime-input-container {
|
||||||
|
@ -79,81 +88,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
input[type=date], input[type=time] {
|
|
||||||
display: inline-block;
|
|
||||||
width: initial;
|
|
||||||
margin: 0 3px;
|
|
||||||
margin-right: -9px;
|
|
||||||
padding: 0;
|
|
||||||
border: 1px solid rgba(0,0,0,0);
|
|
||||||
color: @text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
input[type=date]::-webkit-clear-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
input[type=date]::-webkit-inner-spin-button {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
margin-left: -12px;
|
|
||||||
}
|
|
||||||
input[type=date]::-webkit-calendar-picker-indicator {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
input[type=date]::-webkit-datetime-edit {
|
|
||||||
margin-right: -10px;
|
|
||||||
}
|
|
||||||
input[type=date]::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
border: 1px solid #EEE;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=date]:hover,
|
|
||||||
input[type=date]:active,
|
|
||||||
input[type=date]:focus {
|
|
||||||
background: #FFF;
|
|
||||||
border: 1px solid #EEE;
|
|
||||||
}
|
|
||||||
input[type=date]:hover::-webkit-datetime-edit-fields-wrapper,
|
|
||||||
input[type=date]:active::-webkit-datetime-edit-fields-wrapper,
|
|
||||||
input[type=date]:focus::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
border: 1px solid rgba(0,0,0,0);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=time]::-webkit-datetime-edit {
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
input[type=time]::-webkit-clear-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
input[type=time]::-webkit-inner-spin-button {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
margin-left: -3px;
|
|
||||||
}
|
|
||||||
input[type=time]::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
border: 1px solid #EEE;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=time]:hover,
|
|
||||||
input[type=time]:focus {
|
|
||||||
background: #FFF;
|
|
||||||
border: 1px solid #EEE;
|
|
||||||
}
|
|
||||||
input[type=time]:hover::-webkit-datetime-edit-fields-wrapper,
|
|
||||||
input[type=time]:focus::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
border: 1px solid rgba(0,0,0,0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
input[type=text] {
|
input[type=text] {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
94
src/components/date-picker.jsx
Normal file
94
src/components/date-picker.jsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import React from 'react'
|
||||||
|
import moment from 'moment'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import {DateUtils} from 'nylas-exports'
|
||||||
|
import {MiniMonthView} from 'nylas-component-kit'
|
||||||
|
|
||||||
|
export default class DatePicker extends React.Component {
|
||||||
|
static displayName = "DatePicker";
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.number,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
parentTabGroup: React.PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
value: moment().valueOf(),
|
||||||
|
onChange: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {focused: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
_moveDay(numDays) {
|
||||||
|
const val = moment(this.props.value)
|
||||||
|
const day = val.dayOfYear();
|
||||||
|
this.props.onChange(val.dayOfYear(day + numDays).valueOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyDown = (event) => {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
this._moveDay(-1)
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
this._moveDay(1)
|
||||||
|
} else if (event.key === "ArrowUp") {
|
||||||
|
this._moveDay(-7)
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
this._moveDay(7)
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
this.context.parentTabGroup.shiftFocus(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFocus = () => {
|
||||||
|
this.setState({focused: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
_onBlur = () => {
|
||||||
|
this.setState({focused: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSelectDay = (newTimestamp) => {
|
||||||
|
this.props.onChange(newTimestamp)
|
||||||
|
this.context.parentTabGroup.shiftFocus(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderMiniMonthView() {
|
||||||
|
if (this.state.focused) {
|
||||||
|
return (
|
||||||
|
<div className="mini-month-view-wrap">
|
||||||
|
<MiniMonthView
|
||||||
|
onChange={this._onSelectDay}
|
||||||
|
value={this.props.value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const className = classnames({
|
||||||
|
'day-text': true,
|
||||||
|
focused: this.state.focused,
|
||||||
|
})
|
||||||
|
|
||||||
|
const dayTxt = moment(this.props.value).format(DateUtils.DATE_FORMAT_llll_NO_TIME)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div tabIndex={0} className="date-picker"
|
||||||
|
onKeyDown={this._onKeyDown} onFocus={this._onFocus}
|
||||||
|
onBlur={this._onBlur}
|
||||||
|
>
|
||||||
|
<div className={className}>{dayTxt}</div>
|
||||||
|
{this._renderMiniMonthView()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
117
src/components/nylas-calendar/mini-month-view.jsx
Normal file
117
src/components/nylas-calendar/mini-month-view.jsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import _ from 'underscore'
|
||||||
|
import React from 'react'
|
||||||
|
import moment from 'moment'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
export default class MiniMonthView extends React.Component {
|
||||||
|
static displayName = "MiniMonthView";
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.number,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
value: moment().valueOf(),
|
||||||
|
onChange: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.today = moment()
|
||||||
|
this.state = this._stateFromProps(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(newProps) {
|
||||||
|
this.setState(this._stateFromProps(newProps))
|
||||||
|
}
|
||||||
|
|
||||||
|
_stateFromProps(props) {
|
||||||
|
const m = moment(props.value);
|
||||||
|
return {
|
||||||
|
shownYear: m.year(),
|
||||||
|
shownMonth: m.month(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_shownMonthMoment() {
|
||||||
|
return moment([this.state.shownYear, this.state.shownMonth])
|
||||||
|
}
|
||||||
|
|
||||||
|
_changeMonth = (by) => {
|
||||||
|
const newMonth = this.state.shownMonth + by;
|
||||||
|
const newMoment = this._shownMonthMoment().month(newMonth);
|
||||||
|
this.setState({
|
||||||
|
shownYear: newMoment.year(),
|
||||||
|
shownMonth: newMoment.month(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderLegend() {
|
||||||
|
const weekdayGen = moment([2016]);
|
||||||
|
const legendEls = [];
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const dayStr = weekdayGen.weekday(i).format("dd"); // Locale aware!
|
||||||
|
legendEls.push(<span key={i} className="weekday">{dayStr}</span>)
|
||||||
|
}
|
||||||
|
return <div className="legend">{legendEls}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClickDay = (event) => {
|
||||||
|
const newVal = moment(parseInt(event.target.dataset.timestamp, 10)).valueOf()
|
||||||
|
this.props.onChange(newVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
_isSameDay(m1, m2) {
|
||||||
|
return m1.dayOfYear() === m2.dayOfYear() && m1.year() === m2.year()
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderDays() {
|
||||||
|
const dayIter = this._shownMonthMoment().date(1);
|
||||||
|
const startWeek = dayIter.week();
|
||||||
|
const curMonth = this.state.shownMonth;
|
||||||
|
const weekEls = []
|
||||||
|
const valDay = moment(this.props.value)
|
||||||
|
for (let week = startWeek; week < 5 + startWeek; week++) {
|
||||||
|
dayIter.week(week); // Locale aware!
|
||||||
|
const dayEls = []
|
||||||
|
for (let weekday = 0; weekday < 7; weekday++) {
|
||||||
|
dayIter.weekday(weekday); // Locale aware!
|
||||||
|
const dayStr = dayIter.format("D");
|
||||||
|
const className = classnames({
|
||||||
|
day: true,
|
||||||
|
today: this._isSameDay(dayIter, this.today),
|
||||||
|
"cur-day": this._isSameDay(dayIter, valDay),
|
||||||
|
"cur-month": dayIter.month() === curMonth,
|
||||||
|
})
|
||||||
|
dayEls.push(
|
||||||
|
<div className={className} key={`${week}-${weekday}`}
|
||||||
|
data-timestamp={dayIter.valueOf()}
|
||||||
|
>{dayStr}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
weekEls.push(<div className="week" key={week}>{dayEls}</div>)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="day-grid" onClick={this._onClickDay}>{weekEls}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="mini-month-view">
|
||||||
|
<div className="header">
|
||||||
|
<div className="btn btn-icon"
|
||||||
|
onClick={_.partial(this._changeMonth, -1)}
|
||||||
|
>‹</div>
|
||||||
|
<span className="title">{this._shownMonthMoment().format("MMMM YYYY")}</span>
|
||||||
|
<div className="btn btn-icon"
|
||||||
|
onClick={_.partial(this._changeMonth, 1)}
|
||||||
|
>›</div>
|
||||||
|
</div>
|
||||||
|
{this._renderLegend()}
|
||||||
|
{this._renderDays()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ class TabGroupRegion extends React.Component
|
||||||
return
|
return
|
||||||
|
|
||||||
shiftFocus: (dir) =>
|
shiftFocus: (dir) =>
|
||||||
nodes = ReactDOM.findDOMNode(@).querySelectorAll('input, [contenteditable], [tabIndex]')
|
nodes = ReactDOM.findDOMNode(@).querySelectorAll('input, textarea, [contenteditable], [tabIndex]')
|
||||||
current = document.activeElement
|
current = document.activeElement
|
||||||
idx = Array.from(nodes).indexOf(current)
|
idx = Array.from(nodes).indexOf(current)
|
||||||
|
|
||||||
|
@ -27,10 +27,15 @@ class TabGroupRegion extends React.Component
|
||||||
|
|
||||||
continue if nodes[idx].tabIndex is -1
|
continue if nodes[idx].tabIndex is -1
|
||||||
nodes[idx].focus()
|
nodes[idx].focus()
|
||||||
if nodes[idx].nodeName is 'INPUT' and nodes[idx].type is "text"
|
if @_shouldSelectEnd(nodes[idx])
|
||||||
nodes[idx].setSelectionRange(nodes[idx].value.length, nodes[idx].value.length)
|
nodes[idx].setSelectionRange(nodes[idx].value.length, nodes[idx].value.length)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_shouldSelectEnd: (node) ->
|
||||||
|
node.nodeName is "INPUT" and
|
||||||
|
node.type is "text" and
|
||||||
|
"no-select-end" not in node.classList
|
||||||
|
|
||||||
getChildContext: =>
|
getChildContext: =>
|
||||||
parentTabGroup: @
|
parentTabGroup: @
|
||||||
|
|
||||||
|
|
128
src/components/time-picker.jsx
Normal file
128
src/components/time-picker.jsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import moment from 'moment'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
export default class TimePicker extends React.Component {
|
||||||
|
static displayName = "TimePicker";
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.number,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
relativeTo: React.PropTypes.number, // TODO For `renderTimeOptions`
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
parentTabGroup: React.PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
value: moment().valueOf(),
|
||||||
|
onChange: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
focused: false,
|
||||||
|
rawText: this._valToTimeString(props.value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(newProps) {
|
||||||
|
this.setState({rawText: this._valToTimeString(newProps.value)})
|
||||||
|
}
|
||||||
|
|
||||||
|
_valToTimeString(value) {
|
||||||
|
return moment(value).format("LT")
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyDown = (event) => {
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
// TODO: When `renderTimeOptions` is implemented
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
// TODO: When `renderTimeOptions` is implemented
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
this.context.parentTabGroup.shiftFocus(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFocus = () => {
|
||||||
|
this.setState({focused: true});
|
||||||
|
const el = ReactDOM.findDOMNode(this.refs.input);
|
||||||
|
el.setSelectionRange(0, el.value.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
_onBlur = () => {
|
||||||
|
this.setState({focused: false})
|
||||||
|
this._saveIfValid(this.state.rawText)
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRawTextChange = (event) => {
|
||||||
|
this.setState({rawText: event.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveIfValid(rawText = "") {
|
||||||
|
// Locale-aware am/pm parsing!!
|
||||||
|
const parsedMoment = moment(rawText, "h:ma");
|
||||||
|
if (parsedMoment.isValid()) {
|
||||||
|
if (this._shouldAddTwelve(rawText) && parsedMoment.hour() < 12) {
|
||||||
|
parsedMoment.add(12, 'hours');
|
||||||
|
}
|
||||||
|
this.props.onChange(parsedMoment.valueOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you're going to punch only "2" into the time field, you probably
|
||||||
|
* mean 2pm instead of 2am. The regex explicitly checks for only digits
|
||||||
|
* (no meridiem indicators) and very basic use cases.
|
||||||
|
*/
|
||||||
|
_shouldAddTwelve(rawText) {
|
||||||
|
const simpleDigitMatch = rawText.match(/^(\d{1,2})(:\d{1,2})?$/);
|
||||||
|
if (simpleDigitMatch && simpleDigitMatch.length > 0) {
|
||||||
|
const hr = parseInt(simpleDigitMatch[1], 10);
|
||||||
|
if (hr <= 7) {
|
||||||
|
// If you're going to punch in "2" into the time field, you
|
||||||
|
// probably mean 2pm, not 2am.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
_renderTimeOptions() {
|
||||||
|
// TODO: When you select a time a dropdown will additionally show
|
||||||
|
// letting you pick from preset times. The `relativeTo` prop will give
|
||||||
|
// you relative times
|
||||||
|
const opts = []
|
||||||
|
if (this.state.focused) {
|
||||||
|
return (
|
||||||
|
<div className="time-options">{opts}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const className = classnames({
|
||||||
|
"time-picker": true,
|
||||||
|
"no-select-end": true,
|
||||||
|
invalid: !moment(this.state.rawText, "h:ma").isValid(),
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div className="time-picker-wrap">
|
||||||
|
<input className={className}
|
||||||
|
type="text"
|
||||||
|
ref="input"
|
||||||
|
value={this.state.rawText}
|
||||||
|
onChange={this._onRawTextChange}
|
||||||
|
onKeyDown={this._onKeyDown} onFocus={this._onFocus}
|
||||||
|
onBlur={this._onBlur}
|
||||||
|
/>
|
||||||
|
{this._renderTimeOptions()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,6 +82,8 @@ const DateUtils = {
|
||||||
// Localized format: MMM D, h:mmA
|
// Localized format: MMM D, h:mmA
|
||||||
DATE_FORMAT_SHORT: moment.localeData().longDateFormat('lll').replace(yearRegex, ''),
|
DATE_FORMAT_SHORT: moment.localeData().longDateFormat('lll').replace(yearRegex, ''),
|
||||||
|
|
||||||
|
DATE_FORMAT_llll_NO_TIME: moment.localeData().longDateFormat("llll").replace(/h:mm/, "").replace(" A", ""),
|
||||||
|
|
||||||
format(momentDate, formatString) {
|
format(momentDate, formatString) {
|
||||||
if (!momentDate) return null;
|
if (!momentDate) return null;
|
||||||
return momentDate.format(formatString);
|
return momentDate.format(formatString);
|
||||||
|
|
|
@ -34,6 +34,7 @@ class NylasComponentKit
|
||||||
@load "ListTabular", 'list-tabular'
|
@load "ListTabular", 'list-tabular'
|
||||||
@load "DraggableImg", 'draggable-img'
|
@load "DraggableImg", 'draggable-img'
|
||||||
@load "NylasCalendar", 'nylas-calendar/nylas-calendar'
|
@load "NylasCalendar", 'nylas-calendar/nylas-calendar'
|
||||||
|
@load "MiniMonthView", 'nylas-calendar/mini-month-view'
|
||||||
@load "EventedIFrame", 'evented-iframe'
|
@load "EventedIFrame", 'evented-iframe'
|
||||||
@load "ButtonDropdown", 'button-dropdown'
|
@load "ButtonDropdown", 'button-dropdown'
|
||||||
@load "Contenteditable", 'contenteditable/contenteditable'
|
@load "Contenteditable", 'contenteditable/contenteditable'
|
||||||
|
@ -52,6 +53,8 @@ class NylasComponentKit
|
||||||
@load "OutlineViewItem", "outline-view-item"
|
@load "OutlineViewItem", "outline-view-item"
|
||||||
@load "OutlineView", "outline-view"
|
@load "OutlineView", "outline-view"
|
||||||
@load "DateInput", "date-input"
|
@load "DateInput", "date-input"
|
||||||
|
@load "DatePicker", "date-picker"
|
||||||
|
@load "TimePicker", "time-picker"
|
||||||
|
|
||||||
@load "ScrollRegion", 'scroll-region'
|
@load "ScrollRegion", 'scroll-region'
|
||||||
@load "ResizableRegion", 'resizable-region'
|
@load "ResizableRegion", 'resizable-region'
|
||||||
|
|
23
static/components/date-picker.less
Normal file
23
static/components/date-picker.less
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@import 'ui-variables';
|
||||||
|
|
||||||
|
.date-picker {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
z-index: 999;
|
||||||
|
.day-text {
|
||||||
|
&:hover {
|
||||||
|
color: @text-color-link-hover;
|
||||||
|
}
|
||||||
|
&.focused {
|
||||||
|
color: @text-color-link-hover;
|
||||||
|
}
|
||||||
|
color: @text-color-link;
|
||||||
|
}
|
||||||
|
.mini-month-view-wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 225px;
|
||||||
|
height: 225px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -255,3 +255,81 @@
|
||||||
order: 0;
|
order: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mini-month-view {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 200px;
|
||||||
|
min-height: 200px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: @background-primary;
|
||||||
|
border: 1px solid @border-color-divider;
|
||||||
|
border-radius: @border-radius-base;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
background: @background-secondary;
|
||||||
|
padding: 4px 0 3px 0;
|
||||||
|
.title {
|
||||||
|
padding-top: 3px;
|
||||||
|
color: @text-color;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
border-bottom: 1px solid @border-color-divider;
|
||||||
|
.btn.btn-icon {
|
||||||
|
line-height: 27px;
|
||||||
|
height: 27px;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-right: 0;
|
||||||
|
&:active {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
.weekday {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
padding: 3px 0;
|
||||||
|
border-bottom: 1px solid @border-color-divider;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
.week {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
.day {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 28px;
|
||||||
|
color: @text-color-very-subtle;
|
||||||
|
&.cur-month {
|
||||||
|
color: @text-color;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
&.today {
|
||||||
|
border: 1px solid @accent-primary;
|
||||||
|
}
|
||||||
|
&.cur-day {
|
||||||
|
background: @accent-primary;
|
||||||
|
color: @text-color-inverse;
|
||||||
|
&:hover {
|
||||||
|
background: darken(@accent-primary, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
12
static/components/time-picker.less
Normal file
12
static/components/time-picker.less
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@import "ui-variables";
|
||||||
|
|
||||||
|
.time-picker-wrap {
|
||||||
|
display: inline-block;
|
||||||
|
width: 5.5em;
|
||||||
|
input {
|
||||||
|
width: 5.5em;
|
||||||
|
&.invalid {
|
||||||
|
background: fade(@color-error, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,3 +35,5 @@
|
||||||
@import "components/date-input";
|
@import "components/date-input";
|
||||||
@import "components/nylas-calendar";
|
@import "components/nylas-calendar";
|
||||||
@import "components/empty-list-state";
|
@import "components/empty-list-state";
|
||||||
|
@import "components/date-picker";
|
||||||
|
@import "components/time-picker";
|
||||||
|
|
Loading…
Reference in a new issue