Browse Source

mol-plugin: ability to edit slider in text input

David Sehnal 6 years ago
parent
commit
3cc793888c

+ 18 - 31
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%;

+ 1 - 0
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 {

+ 53 - 0
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,

+ 8 - 38
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} />;
     }
 }
 

+ 57 - 24
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(); }