mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-10-03 18:05:32 +08:00
feat(scheduler): better time picker
fix(scheduler): time picker fixed
This commit is contained in:
parent
c78401c566
commit
6a70584ffa
4 changed files with 148 additions and 18 deletions
|
@ -17,9 +17,6 @@
|
||||||
},
|
},
|
||||||
"icon": "./icon.png",
|
"icon": "./icon.png",
|
||||||
"isOptional": true,
|
"isOptional": true,
|
||||||
"dependencies": {
|
|
||||||
"moment-round": "^1.0"
|
|
||||||
},
|
|
||||||
"windowTypes": {
|
"windowTypes": {
|
||||||
"default": true,
|
"default": true,
|
||||||
"composer": true,
|
"composer": true,
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
color: @text-color;
|
color: @text-color;
|
||||||
}
|
}
|
||||||
.row {
|
.row {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.row.time {
|
.row.time {
|
||||||
|
z-index: 10; // So the time pickers show over
|
||||||
.time-picker {
|
.time-picker {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
require('moment-round') // overrides moment
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
const INTERVAL = [30, 'minutes']
|
||||||
|
|
||||||
export default class TimePicker extends React.Component {
|
export default class TimePicker extends React.Component {
|
||||||
static displayName = "TimePicker";
|
static displayName = "TimePicker";
|
||||||
|
|
||||||
|
@ -29,26 +32,54 @@ export default class TimePicker extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._fixTimeOptionScroll()
|
||||||
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
componentWillReceiveProps(newProps) {
|
||||||
this.setState({rawText: this._valToTimeString(newProps.value)})
|
this.setState({rawText: this._valToTimeString(newProps.value)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this._gotoScrollStartOnUpdate) {
|
||||||
|
this._fixTimeOptionScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_valToTimeString(value) {
|
_valToTimeString(value) {
|
||||||
return moment(value).format("LT")
|
return moment(value).format("LT")
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown = (event) => {
|
_onKeyDown = (event) => {
|
||||||
if (event.key === "ArrowUp") {
|
if (event.key === "ArrowUp") {
|
||||||
// TODO: When `renderTimeOptions` is implemented
|
event.preventDefault()
|
||||||
|
this._onArrow(event.key)
|
||||||
} else if (event.key === "ArrowDown") {
|
} else if (event.key === "ArrowDown") {
|
||||||
// TODO: When `renderTimeOptions` is implemented
|
event.preventDefault()
|
||||||
|
this._onArrow(event.key)
|
||||||
} else if (event.key === "Enter") {
|
} else if (event.key === "Enter") {
|
||||||
this.context.parentTabGroup.shiftFocus(1);
|
this.context.parentTabGroup.shiftFocus(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onArrow(key) {
|
||||||
|
let newT = moment(this.props.value);
|
||||||
|
newT = newT.round.apply(newT, INTERVAL);
|
||||||
|
if (key === "ArrowUp") {
|
||||||
|
newT = newT.subtract.apply(newT, INTERVAL);
|
||||||
|
} else if (key === "ArrowDown") {
|
||||||
|
newT = newT.add.apply(newT, INTERVAL);
|
||||||
|
}
|
||||||
|
if (moment(this.props.value).day() !== newT.day()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._gotoScrollStartOnUpdate = true
|
||||||
|
this.props.onChange(newT);
|
||||||
|
}
|
||||||
|
|
||||||
_onFocus = () => {
|
_onFocus = () => {
|
||||||
this.setState({focused: true});
|
this.setState({focused: true});
|
||||||
|
this._gotoScrollStartOnUpdate = true
|
||||||
const el = ReactDOM.findDOMNode(this.refs.input);
|
const el = ReactDOM.findDOMNode(this.refs.input);
|
||||||
el.setSelectionRange(0, el.value.length)
|
el.setSelectionRange(0, el.value.length)
|
||||||
}
|
}
|
||||||
|
@ -83,26 +114,86 @@ export default class TimePicker extends React.Component {
|
||||||
if (simpleDigitMatch && simpleDigitMatch.length > 0) {
|
if (simpleDigitMatch && simpleDigitMatch.length > 0) {
|
||||||
const hr = parseInt(simpleDigitMatch[1], 10);
|
const hr = parseInt(simpleDigitMatch[1], 10);
|
||||||
if (hr <= 7) {
|
if (hr <= 7) {
|
||||||
// If you're going to punch in "2" into the time field, you
|
|
||||||
// probably mean 2pm, not 2am.
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
_fixTimeOptionScroll() {
|
||||||
_renderTimeOptions() {
|
this._gotoScrollStartOnUpdate = false
|
||||||
// TODO: When you select a time a dropdown will additionally show
|
const el = ReactDOM.findDOMNode(this);
|
||||||
// letting you pick from preset times. The `relativeTo` prop will give
|
const scrollTo = el.querySelector(".scroll-start");
|
||||||
// you relative times
|
const scrollWrap = el.querySelector(".time-options");
|
||||||
const opts = []
|
if (scrollTo && scrollWrap) {
|
||||||
if (this.state.focused) {
|
scrollWrap.scrollTop = scrollTo.offsetTop
|
||||||
return (
|
|
||||||
<div className="time-options">{opts}</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
_onSelectOption(val) {
|
||||||
|
this.props.onChange(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderTimeOptions() {
|
||||||
|
if (!this.state.focused) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const enteredMoment = moment(this.props.value);
|
||||||
|
|
||||||
|
const roundedMoment = moment(enteredMoment);
|
||||||
|
roundedMoment.ceil.apply(roundedMoment, INTERVAL);
|
||||||
|
|
||||||
|
const firstVisibleMoment = moment(roundedMoment);
|
||||||
|
firstVisibleMoment.add.apply(firstVisibleMoment, INTERVAL);
|
||||||
|
|
||||||
|
let startVal = moment(this.props.value).startOf('day').valueOf();
|
||||||
|
startVal = Math.max(startVal, (this.props.relativeTo || 0));
|
||||||
|
|
||||||
|
const startMoment = moment(startVal)
|
||||||
|
if (this.props.relativeTo) {
|
||||||
|
startMoment.ceil.apply(startMoment, INTERVAL).add.apply(startMoment, INTERVAL)
|
||||||
|
}
|
||||||
|
const endMoment = moment(startVal).endOf('day');
|
||||||
|
const opts = []
|
||||||
|
|
||||||
|
const relStart = moment(this.props.relativeTo);
|
||||||
|
const timeIter = moment(startMoment)
|
||||||
|
while (timeIter.isSameOrBefore(endMoment)) {
|
||||||
|
const val = timeIter.valueOf();
|
||||||
|
const className = classnames({
|
||||||
|
option: true,
|
||||||
|
selected: timeIter.isSame(enteredMoment),
|
||||||
|
"scroll-start": timeIter.isSame(firstVisibleMoment),
|
||||||
|
})
|
||||||
|
|
||||||
|
let relTxt = false
|
||||||
|
if (this.props.relativeTo) {
|
||||||
|
relTxt = (
|
||||||
|
<span className="rel-text">
|
||||||
|
{`(${timeIter.diff(relStart, 'hours', true)}hr)`}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.push(
|
||||||
|
<div className={className} key={val}
|
||||||
|
onMouseDown={() => this._onSelectOption(val)}
|
||||||
|
>
|
||||||
|
{timeIter.format("LT")}{relTxt}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
timeIter.add.apply(timeIter, INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = classnames({
|
||||||
|
"time-options": true,
|
||||||
|
"relative-to": this.props.relativeTo,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>{opts}</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -3,10 +3,50 @@
|
||||||
.time-picker-wrap {
|
.time-picker-wrap {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 5.5em;
|
width: 5.5em;
|
||||||
|
position: relative;
|
||||||
input {
|
input {
|
||||||
width: 5.5em;
|
width: 5.5em;
|
||||||
&.invalid {
|
&.invalid {
|
||||||
background: fade(@color-error, 10%);
|
background: fade(@color-error, 10%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-options {
|
||||||
|
max-height: 158px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
background: @background-primary;
|
||||||
|
border-top: 0;
|
||||||
|
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||||
|
box-shadow: 0 0 3px rgba(0,0,0,0.1);
|
||||||
|
|
||||||
|
&.relative-to {
|
||||||
|
width: 120px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.option {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
line-height: 1.75;
|
||||||
|
&.selected {
|
||||||
|
color: @text-color;
|
||||||
|
&:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.focused, &:hover {
|
||||||
|
background: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.rel-text {
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue