parameters.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import * as React from 'react'
  8. import { ParamDefinition as PD } from 'mol-util/param-definition';
  9. import { camelCaseToWords } from 'mol-util/string';
  10. import { ColorNames } from 'mol-util/color/tables';
  11. import { Color } from 'mol-util/color';
  12. export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
  13. params: P,
  14. values: any,
  15. onChange: ParamOnChange,
  16. isDisabled?: boolean,
  17. onEnter?: () => void
  18. }
  19. export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
  20. render() {
  21. const params = this.props.params;
  22. const values = this.props.values;
  23. return <div style={{ width: '100%' }}>
  24. {Object.keys(params).map(key => {
  25. const param = params[key];
  26. const Control = controlFor(param);
  27. if (!Control) return null;
  28. return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
  29. })}
  30. </div>;
  31. }
  32. }
  33. function controlFor(param: PD.Any): ParamControl | undefined {
  34. switch (param.type) {
  35. case 'value': return void 0;
  36. case 'boolean': return BoolControl;
  37. case 'number': return NumberControl;
  38. case 'converted': return ConvertedControl;
  39. case 'multi-select': return MultiSelectControl;
  40. case 'color': return ColorControl;
  41. case 'select': return SelectControl;
  42. case 'text': return TextControl;
  43. case 'interval': return IntervalControl;
  44. case 'group': return GroupControl;
  45. case 'mapped': return MappedControl;
  46. case 'line-graph': return void 0;
  47. }
  48. throw new Error('not supported');
  49. }
  50. // type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, onChange: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
  51. export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
  52. export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> { name: string, value: P['defaultValue'], param: P, isDisabled?: boolean, onChange: ParamOnChange, onEnter?: () => void }
  53. export type ParamControl = React.ComponentClass<ParamProps<any>>
  54. export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>> {
  55. protected update(value: any) {
  56. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  57. }
  58. abstract renderControl(): JSX.Element;
  59. render() {
  60. const label = this.props.param.label || camelCaseToWords(this.props.name);
  61. return <div style={{ padding: '0 3px', borderBottom: '1px solid #ccc' }}>
  62. <div style={{ lineHeight: '20px', float: 'left' }} title={this.props.param.description}>{label}</div>
  63. <div style={{ float: 'left', marginLeft: '5px' }}>
  64. {this.renderControl()}
  65. </div>
  66. <div style={{ clear: 'both' }} />
  67. </div>;
  68. }
  69. }
  70. export class BoolControl extends SimpleParam<PD.Boolean> {
  71. onClick = () => { this.update(!this.props.value); }
  72. renderControl() {
  73. return <button onClick={this.onClick} disabled={this.props.isDisabled}>{this.props.value ? '✓ On' : '✗ Off'}</button>;
  74. }
  75. }
  76. export class NumberControl extends SimpleParam<PD.Numeric> {
  77. onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.update(+e.target.value); }
  78. renderControl() {
  79. return <span>
  80. <input type='range' 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} />
  81. <br />{this.props.value}
  82. </span>
  83. }
  84. }
  85. export class TextControl extends SimpleParam<PD.Text> {
  86. onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  87. const value = e.target.value;
  88. if (value !== this.props.value) {
  89. this.update(value);
  90. }
  91. }
  92. onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
  93. if (!this.props.onEnter) return;
  94. if ((e.keyCode === 13 || e.charCode === 13)) {
  95. this.props.onEnter();
  96. }
  97. }
  98. renderControl() {
  99. return <input type='text'
  100. value={this.props.value || ''}
  101. onChange={this.onChange}
  102. onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
  103. disabled={this.props.isDisabled}
  104. />;
  105. }
  106. }
  107. export class SelectControl extends SimpleParam<PD.Select<any>> {
  108. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { this.update(e.target.value); }
  109. renderControl() {
  110. return <select value={this.props.value || ''} onChange={this.onChange} disabled={this.props.isDisabled}>
  111. {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
  112. </select>;
  113. }
  114. }
  115. export class IntervalControl extends SimpleParam<PD.Interval> {
  116. // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  117. // this.setState({ value: e.target.value });
  118. // this.props.onChange(e.target.value);
  119. // }
  120. renderControl() {
  121. return <span>interval TODO</span>;
  122. }
  123. }
  124. export class ColorControl extends SimpleParam<PD.Color> {
  125. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  126. this.update(Color(parseInt(e.target.value)));
  127. }
  128. renderControl() {
  129. return <select value={this.props.value} onChange={this.onChange}>
  130. {Object.keys(ColorNames).map(name => {
  131. return <option key={name} value={(ColorNames as { [k: string]: Color})[name]}>{name}</option>
  132. })}
  133. </select>;
  134. }
  135. }
  136. export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>> {
  137. change(value: PD.MultiSelect<any>['defaultValue'] ) {
  138. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  139. }
  140. toggle(key: string) {
  141. return () => {
  142. if (this.props.value.indexOf(key) < 0) this.change(this.props.value.concat(key));
  143. else this.change(this.props.value.filter(v => v !== key))
  144. }
  145. }
  146. render() {
  147. const current = this.props.value;
  148. const label = this.props.param.label || camelCaseToWords(this.props.name);
  149. return <div>
  150. <div>{label} <small>{`${current.length} of ${this.props.param.options.length}`}</small></div>
  151. <div style={{ paddingLeft: '7px' }}>
  152. {this.props.param.options.map(([value, label]) =>
  153. <button key={value} onClick={this.toggle(value)} disabled={this.props.isDisabled}>
  154. {current.indexOf(value) >= 0 ? `✓ ${label}` : `✗ ${label}`}
  155. </button>)}
  156. </div>
  157. </div>;
  158. }
  159. }
  160. export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>> {
  161. change(value: PD.Mapped<any>['defaultValue'] ) {
  162. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  163. }
  164. onChangeParam: ParamOnChange = e => {
  165. const value: PD.Mapped<any>['defaultValue'] = this.props.value;
  166. this.change({ ...value.params, [e.name]: e.value });
  167. }
  168. render() {
  169. const value: PD.Mapped<any>['defaultValue'] = this.props.value;
  170. const params = this.props.param.params;
  171. const label = this.props.param.label || camelCaseToWords(this.props.name);
  172. // TODO toggle panel
  173. return <div>
  174. <div>{label}</div>
  175. <ParameterControls params={params} onChange={this.onChangeParam} values={value.params} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  176. </div>
  177. }
  178. }
  179. export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> {
  180. change(value: PD.Mapped<any>['defaultValue'] ) {
  181. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  182. }
  183. onChangeName: ParamOnChange = e => {
  184. // TODO: Cache values when changing types?
  185. this.change({ name: e.value, params: this.props.param.map(e.value).defaultValue });
  186. }
  187. onChangeParam: ParamOnChange = e => {
  188. const value: PD.Mapped<any>['defaultValue'] = this.props.value;
  189. this.change({ name: value.name, params: e.value });
  190. }
  191. render() {
  192. const value: PD.Mapped<any>['defaultValue'] = this.props.value;
  193. const param = this.props.param.map(value.name);
  194. const label = this.props.param.label || camelCaseToWords(this.props.name);
  195. const Mapped = controlFor(param);
  196. const select = <SelectControl param={this.props.param.select}
  197. isDisabled={this.props.isDisabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
  198. name={label} value={value.name} />
  199. if (!Mapped) {
  200. return select;
  201. }
  202. return <div>
  203. {select}
  204. <div style={{ borderLeft: '5px solid #777', paddingLeft: '5px' }}>
  205. <Mapped param={param} value={value} name='' onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  206. </div>
  207. </div>
  208. }
  209. }
  210. export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converted<any, any>>> {
  211. onChange: ParamOnChange = e => {
  212. this.props.onChange({
  213. name: this.props.name,
  214. param: this.props.param,
  215. value: this.props.param.toValue(e.value)
  216. });
  217. }
  218. render() {
  219. const value = this.props.param.fromValue(this.props.value);
  220. const Converted = controlFor(this.props.param.converted);
  221. if (!Converted) return null;
  222. return <Converted param={this.props.param.converted} value={value} name={this.props.name} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  223. }
  224. }