mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-25 09:32:33 +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 ReactDOM from 'react-dom';
|
||||
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 ProposedTimeList from './proposed-time-list'
|
||||
import EventDatetimeInput from './event-datetime-input'
|
||||
|
||||
import {
|
||||
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() {
|
||||
const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID);
|
||||
if (metadata && 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 (
|
||||
<div className="row time">
|
||||
{this._renderIcon("ic-eventcard-time@2x.png")}
|
||||
<span>
|
||||
<EventDatetimeInput name="start"
|
||||
value={this.props.event.start}
|
||||
onChange={ date => this.props.onChange({start: date}) }
|
||||
/>
|
||||
-
|
||||
<EventDatetimeInput name="end"
|
||||
reversed
|
||||
value={this.props.event.end}
|
||||
onChange={ date => this.props.onChange({end: date}) }
|
||||
<TimePicker value={startVal} onChange={this._onChangeStartTime} />
|
||||
to
|
||||
<TimePicker value={endVal} relativeTo={startVal}
|
||||
onChange={this._onChangeEndTime}
|
||||
/>
|
||||
<span className="timezone">
|
||||
{moment().tz(Utils.timeZone).format("z")}
|
||||
</span>
|
||||
|
||||
on
|
||||
|
||||
<DatePicker value={startVal} onChange={this._onChangeDay} />
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
@ -169,51 +224,53 @@ export default class NewEventCard extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="new-event-card">
|
||||
<div className="remove-button" onClick={this.props.onRemove}>✕</div>
|
||||
<div className="row title">
|
||||
{this._renderIcon("ic-eventcard-description@2x.png")}
|
||||
<input type="text"
|
||||
name="title"
|
||||
placeholder="Add an event title"
|
||||
value={title}
|
||||
onFocus={() => {this._focusedTitle = true}}
|
||||
onBlur={this._onBlurTitle}
|
||||
onChange={e => this.props.onChange({title: e.target.value}) }
|
||||
/>
|
||||
</div>
|
||||
<TabGroupRegion>
|
||||
<div className="remove-button" onClick={this.props.onRemove}>✕</div>
|
||||
<div className="row title">
|
||||
{this._renderIcon("ic-eventcard-description@2x.png")}
|
||||
<input type="text"
|
||||
name="title"
|
||||
placeholder="Add an event title"
|
||||
value={title}
|
||||
onFocus={() => {this._focusedTitle = true}}
|
||||
onBlur={this._onBlurTitle}
|
||||
onChange={e => this.props.onChange({title: e.target.value}) }
|
||||
/>
|
||||
</div>
|
||||
|
||||
{this._renderTimePicker()}
|
||||
{this._renderTimePicker()}
|
||||
|
||||
{this._renderSuggestPrompt()}
|
||||
{this._renderSuggestPrompt()}
|
||||
|
||||
{this._renderCalendarPicker()}
|
||||
{this._renderCalendarPicker()}
|
||||
|
||||
<div className="row recipients">
|
||||
{this._renderIcon("ic-eventcard-people@2x.png")}
|
||||
<div onClick={this.props.onParticipantsClick()}>{this._renderParticipants()}</div>
|
||||
</div>
|
||||
<div className="row recipients">
|
||||
{this._renderIcon("ic-eventcard-people@2x.png")}
|
||||
<div onClick={this.props.onParticipantsClick()}>{this._renderParticipants()}</div>
|
||||
</div>
|
||||
|
||||
<div className="row location">
|
||||
{this._renderIcon("ic-eventcard-location@2x.png")}
|
||||
<input type="text"
|
||||
name="location"
|
||||
placeholder="Add a location"
|
||||
value={this.props.event.location}
|
||||
onChange={e => this.props.onChange({location: e.target.value}) }
|
||||
/>
|
||||
</div>
|
||||
<div className="row location">
|
||||
{this._renderIcon("ic-eventcard-location@2x.png")}
|
||||
<input type="text"
|
||||
name="location"
|
||||
placeholder="Add a location"
|
||||
value={this.props.event.location}
|
||||
onChange={e => this.props.onChange({location: e.target.value}) }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row description">
|
||||
{this._renderIcon("ic-eventcard-notes@2x.png")}
|
||||
<div className="row description">
|
||||
{this._renderIcon("ic-eventcard-notes@2x.png")}
|
||||
|
||||
<textarea
|
||||
ref="description"
|
||||
name="description"
|
||||
placeholder="Add notes"
|
||||
value={this.props.event.description}
|
||||
onChange={ e => this.props.onChange({description: e.target.value}) }
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
ref="description"
|
||||
name="description"
|
||||
placeholder="Add notes"
|
||||
value={this.props.event.description}
|
||||
onChange={ e => this.props.onChange({description: e.target.value}) }
|
||||
/>
|
||||
</div>
|
||||
</TabGroupRegion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ import {
|
|||
import {RetinaImg} from 'nylas-component-kit'
|
||||
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 {
|
||||
static displayName = "SchedulerComposerButton";
|
||||
|
||||
|
@ -78,8 +82,15 @@ editable calendar with your account provider.`);
|
|||
return;
|
||||
}
|
||||
|
||||
const start = moment().ceil(30, 'minutes');
|
||||
const end = moment(start).add(1, 'hour');
|
||||
|
||||
// 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.commit()
|
||||
})
|
||||
|
|
|
@ -55,7 +55,16 @@
|
|||
color: #A9A9A9;
|
||||
|
||||
.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 {
|
||||
|
@ -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] {
|
||||
border-radius: 0;
|
||||
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
|
||||
|
||||
shiftFocus: (dir) =>
|
||||
nodes = ReactDOM.findDOMNode(@).querySelectorAll('input, [contenteditable], [tabIndex]')
|
||||
nodes = ReactDOM.findDOMNode(@).querySelectorAll('input, textarea, [contenteditable], [tabIndex]')
|
||||
current = document.activeElement
|
||||
idx = Array.from(nodes).indexOf(current)
|
||||
|
||||
|
@ -27,10 +27,15 @@ class TabGroupRegion extends React.Component
|
|||
|
||||
continue if nodes[idx].tabIndex is -1
|
||||
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)
|
||||
return
|
||||
|
||||
_shouldSelectEnd: (node) ->
|
||||
node.nodeName is "INPUT" and
|
||||
node.type is "text" and
|
||||
"no-select-end" not in node.classList
|
||||
|
||||
getChildContext: =>
|
||||
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
|
||||
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) {
|
||||
if (!momentDate) return null;
|
||||
return momentDate.format(formatString);
|
||||
|
|
|
@ -34,6 +34,7 @@ class NylasComponentKit
|
|||
@load "ListTabular", 'list-tabular'
|
||||
@load "DraggableImg", 'draggable-img'
|
||||
@load "NylasCalendar", 'nylas-calendar/nylas-calendar'
|
||||
@load "MiniMonthView", 'nylas-calendar/mini-month-view'
|
||||
@load "EventedIFrame", 'evented-iframe'
|
||||
@load "ButtonDropdown", 'button-dropdown'
|
||||
@load "Contenteditable", 'contenteditable/contenteditable'
|
||||
|
@ -52,6 +53,8 @@ class NylasComponentKit
|
|||
@load "OutlineViewItem", "outline-view-item"
|
||||
@load "OutlineView", "outline-view"
|
||||
@load "DateInput", "date-input"
|
||||
@load "DatePicker", "date-picker"
|
||||
@load "TimePicker", "time-picker"
|
||||
|
||||
@load "ScrollRegion", 'scroll-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;
|
||||
}
|
||||
}
|
||||
|
||||
.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/nylas-calendar";
|
||||
@import "components/empty-list-state";
|
||||
@import "components/date-picker";
|
||||
@import "components/time-picker";
|
||||
|
|
Loading…
Reference in a new issue