diff --git a/src/mol-plugin/skin/base/components/controls.scss b/src/mol-plugin/skin/base/components/controls.scss index 3605b3cd9d918adac14de6a5b84ee5c093bfd20f..bd527f69b3c6fa427c0ebe08921aa5539b5374b2 100644 --- a/src/mol-plugin/skin/base/components/controls.scss +++ b/src/mol-plugin/skin/base/components/controls.scss @@ -76,19 +76,10 @@ > div:first-child { position: absolute; top: 0; - left: 0; + left: 18px; bottom: 0; - right: 50px; - width: 100%; - padding-right: 50px; - display: table; - - > div { - height: $row-height; - display: table-cell; - vertical-align: middle; - padding: 0 ($control-spacing + 4px); - } + right: 62px; + display: grid; } > div:last-child { position: absolute; @@ -101,9 +92,12 @@ bottom: 0; } - // input[type=text] { - // text-align: right; - // } + input[type=text] { + padding-right: 6px; + padding-left: 4px; + font-size: 80%; + text-align: right; + } // input[type=range] { // width: 100%; @@ -125,20 +119,10 @@ > div:nth-child(2) { position: absolute; top: 0; - left: 0; + left: 35px; bottom: 0; - right: 25px; - width: 100%; - padding-left: 20px; - padding-right: 25px; - display: table; - - > div { - height: $row-height; - display: table-cell; - vertical-align: middle; - padding: 0 ($control-spacing + 4px); - } + right: 37px; + display: grid; } > div:last-child { position: absolute; @@ -152,9 +136,12 @@ font-size: 80%; } - // input[type=text] { - // text-align: right; - // } + input[type=text] { + padding-right: 4px; + padding-left: 4px; + font-size: 80%; + text-align: center; + } // input[type=range] { // width: 100%; diff --git a/src/mol-plugin/skin/base/components/slider.scss b/src/mol-plugin/skin/base/components/slider.scss index 3d879558045cb147effefb5861d7eb6e457c57c2..cc5c2c689c48808b44927d8b015600e9d29c90e4 100644 --- a/src/mol-plugin/skin/base/components/slider.scss +++ b/src/mol-plugin/skin/base/components/slider.scss @@ -14,6 +14,7 @@ padding: 5px 0; width: 100%; border-radius: $slider-border-radius-base; + align-self: center; @include borderBox; &-rail { diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx index 5c33082538ebac60e3a6d2f2bdb37f0b7bc61a4b..4a9588fbeab6903662439f8576775f9068f00c68 100644 --- a/src/mol-plugin/ui/controls/common.tsx +++ b/src/mol-plugin/ui/controls/common.tsx @@ -27,6 +27,59 @@ export class ControlGroup extends React.Component<{ header: string, initialExpan } } +export class NumericInput extends React.PureComponent<{ + value: number, + onChange: (v: number) => void, + onEnter?: () => void, + blurOnEnter?: boolean, + isDisabled?: boolean, + placeholder?: string +}, { value: string }> { + state = { value: '0' }; + input = React.createRef<HTMLInputElement>(); + + onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const value = +e.target.value; + this.setState({ value: e.target.value }, () => { + if (!Number.isNaN(value) && value !== this.props.value) { + this.props.onChange(value); + } + }); + } + + onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { + if ((e.keyCode === 13 || e.charCode === 13)) { + if (this.props.blurOnEnter && this.input.current) { + this.input.current.blur(); + } + if (this.props.onEnter) this.props.onEnter(); + } + } + + onBlur = () => { + this.setState({ value: '' + this.props.value }); + } + + static getDerivedStateFromProps(props: { value: number }, state: { value: string }) { + const value = +state.value; + if (Number.isNaN(value) || value === props.value) return null; + return { value: '' + props.value }; + } + + render() { + return <input type='text' + ref={this.input} + onBlur={this.onBlur} + value={this.state.value} + placeholder={this.props.placeholder} + onChange={this.onChange} + onKeyPress={this.props.onEnter || this.props.blurOnEnter ? this.onKeyPress : void 0} + disabled={!!this.props.isDisabled} + /> + } +} + + // export const ToggleButton = (props: { // onChange: (v: boolean) => void, // value: boolean, diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index caa63c87a96e8ed267212070ff1285fd947c3161..ad7ef082d48cf230cbe745d3e4bed7a610d6b545 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/src/mol-plugin/ui/controls/parameters.tsx @@ -15,6 +15,7 @@ import { camelCaseToWords } from 'mol-util/string'; import * as React from 'react'; import LineGraphComponent from './line-graph/line-graph-component'; import { Slider, Slider2 } from './slider'; +import { NumericInput } from './common'; export interface ParameterControlsProps<P extends PD.Params = PD.Params> { params: P, @@ -152,53 +153,22 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap } } -export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>, { value: string }> { +export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>> { state = { value: '0' }; - protected update(value: any) { + update = (value: number) => { this.props.onChange({ param: this.props.param, name: this.props.name, value }); } - onChange = (e: React.ChangeEvent<HTMLInputElement>) => { - const value = +e.target.value; - this.setState({ value: e.target.value }, () => { - if (!Number.isNaN(value) && value !== this.props.value) { - this.update(value); - } - }); - } - - onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (!this.props.onEnter) return; - if ((e.keyCode === 13 || e.charCode === 13)) { - this.props.onEnter(); - } - } - - onBlur = () => { - this.setState({ value: '' + this.props.value }); - } - - static getDerivedStateFromProps(props: { value: number }, state: { value: string }) { - const value = +state.value; - if (Number.isNaN(value) || value === props.value) return null; - return { value: '' + props.value }; - } - render() { const placeholder = this.props.param.label || camelCaseToWords(this.props.name); const label = this.props.param.label || camelCaseToWords(this.props.name); return <div className='msp-control-row'> <span title={this.props.param.description}>{label}</span> <div> - <input type='text' - onBlur={this.onBlur} - value={this.state.value} - placeholder={placeholder} - onChange={this.onChange} - onKeyPress={this.props.onEnter ? this.onKeyPress : void 0} - disabled={this.props.isDisabled} - /> + <NumericInput + value={this.props.value} onEnter={this.props.onEnter} placeholder={placeholder} + isDisabled={this.props.isDisabled} onChange={this.update} /> </div> </div>; } @@ -208,7 +178,7 @@ export class NumberRangeControl extends SimpleParam<PD.Numeric> { onChange = (v: number) => { this.update(v); } renderControl() { return <Slider value={this.props.value} min={this.props.param.min!} max={this.props.param.max!} - step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} /> + step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} /> } } @@ -267,7 +237,7 @@ export class BoundedIntervalControl extends SimpleParam<PD.Interval> { onChange = (v: [number, number]) => { this.update(v); } renderControl() { return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!} - step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />; + step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />; } } diff --git a/src/mol-plugin/ui/controls/slider.tsx b/src/mol-plugin/ui/controls/slider.tsx index f930a20fe558d0496ba4eeec0689a97ad4bb506d..c807995fbad0abecd3e93939dbb245ff3b3ac952 100644 --- a/src/mol-plugin/ui/controls/slider.tsx +++ b/src/mol-plugin/ui/controls/slider.tsx @@ -5,6 +5,7 @@ */ import * as React from 'react' +import { NumericInput } from './common'; export class Slider extends React.Component<{ min: number, @@ -12,7 +13,8 @@ export class Slider extends React.Component<{ value: number, step?: number, onChange: (v: number) => void, - disabled?: boolean + disabled?: boolean, + onEnter?: () => void }, { isChanging: boolean, current: number }> { state = { isChanging: false, current: 0 } @@ -35,18 +37,27 @@ export class Slider extends React.Component<{ this.setState({ current }); } + updateManually = (v: number) => { + let n = v; + if (this.props.step === 1) n = Math.round(n); + if (n < this.props.min) n = this.props.min; + if (n > this.props.max) n = this.props.max; + this.props.onChange(n); + } + render() { let step = this.props.step; if (step === void 0) step = 1; return <div className='msp-slider'> <div> - <div> - <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} - onBeforeChange={this.begin} - onChange={this.updateCurrent as any} onAfterChange={this.end as any} /> - </div></div> + <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} + onBeforeChange={this.begin} + onChange={this.updateCurrent as any} onAfterChange={this.end as any} /> + </div> <div> - {`${Math.round(100 * this.state.current) / 100}`} + <NumericInput + value={this.state.current} onEnter={this.props.onEnter} blurOnEnter={true} + isDisabled={this.props.disabled} onChange={this.updateManually} /> </div> </div>; } @@ -58,7 +69,8 @@ export class Slider2 extends React.Component<{ value: [number, number], step?: number, onChange: (v: [number, number]) => void, - disabled?: boolean + disabled?: boolean, + onEnter?: () => void }, { isChanging: boolean, current: [number, number] }> { state = { isChanging: false, current: [0, 1] as [number, number] } @@ -81,20 +93,41 @@ export class Slider2 extends React.Component<{ this.setState({ current }); } + updateMax = (v: number) => { + let n = v; + if (this.props.step === 1) n = Math.round(n); + if (n < this.state.current[0]) n = this.state.current[0] + else if (n < this.props.min) n = this.props.min; + if (n > this.props.max) n = this.props.max; + this.props.onChange([this.state.current[0], n]); + } + + updateMin = (v: number) => { + let n = v; + if (this.props.step === 1) n = Math.round(n); + if (n < this.props.min) n = this.props.min; + if (n > this.state.current[1]) n = this.state.current[1]; + else if (n > this.props.max) n = this.props.max; + this.props.onChange([n, this.state.current[1]]); + } + render() { let step = this.props.step; if (step === void 0) step = 1; return <div className='msp-slider2'> <div> - {`${Math.round(100 * this.state.current[0]) / 100}`} + <NumericInput + value={this.state.current[0]} onEnter={this.props.onEnter} blurOnEnter={true} + isDisabled={this.props.disabled} onChange={this.updateMin} /> </div> <div> - <div> - <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} - onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} /> - </div></div> + <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} + onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} /> + </div> <div> - {`${Math.round(100 * this.state.current[1]) / 100}`} + <NumericInput + value={this.state.current[1]} onEnter={this.props.onEnter} blurOnEnter={true} + isDisabled={this.props.disabled} onChange={this.updateMax} /> </div> </div>; } @@ -102,10 +135,10 @@ export class Slider2 extends React.Component<{ /** * The following code was adapted from react-components/slider library. - * + * * The MIT License (MIT) * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -116,12 +149,12 @@ export class Slider2 extends React.Component<{ * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ @@ -540,7 +573,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState } return false; - // return this.state.bounds.some((x, i) => e.target + // return this.state.bounds.some((x, i) => e.target // ( // //this.handleElements[i] && e.target === ReactDOM.findDOMNode(this.handleElements[i]) @@ -702,7 +735,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState dragging: handle === i, index: i, key: i, - ref: (h: any) => this.handleElements.push(h) //`handle-${i}`, + ref: (h: any) => this.handleElements.push(h) // `handle-${i}`, })); if (!range) { handles.shift(); }