parameters.tsx 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. /**
  2. * Copyright (c) 2018-2019 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 { Vec2, Vec3 } from '../../../mol-math/linear-algebra';
  8. import { Color } from '../../../mol-util/color';
  9. import { ColorListName, getColorListFromName } from '../../../mol-util/color/lists';
  10. import { memoize1 } from '../../../mol-util/memoize';
  11. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  12. import { camelCaseToWords } from '../../../mol-util/string';
  13. import * as React from 'react';
  14. import LineGraphComponent from './line-graph/line-graph-component';
  15. import { Slider, Slider2 } from './slider';
  16. import { NumericInput, IconButton, ControlGroup } from './common';
  17. import { _Props, _State } from '../base';
  18. import { legendFor } from './legend';
  19. import { Legend as LegendData } from '../../../mol-util/legend';
  20. import { CombinedColorControl, ColorValueOption, ColorOptions } from './color';
  21. export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
  22. params: P,
  23. values: any,
  24. onChange: ParamOnChange,
  25. isDisabled?: boolean,
  26. onEnter?: () => void
  27. }
  28. export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
  29. render() {
  30. const params = this.props.params;
  31. const values = this.props.values;
  32. const keys = Object.keys(params);
  33. if (keys.length === 0 || values === undefined) return null;
  34. return <>
  35. {keys.map(key => {
  36. const param = params[key];
  37. if (param.isHidden) return null;
  38. const Control = controlFor(param);
  39. if (!Control) return null;
  40. return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
  41. })}
  42. </>;
  43. }
  44. }
  45. function controlFor(param: PD.Any): ParamControl | undefined {
  46. switch (param.type) {
  47. case 'value': return void 0;
  48. case 'boolean': return BoolControl;
  49. case 'number': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
  50. ? NumberRangeControl : NumberInputControl;
  51. case 'converted': return ConvertedControl;
  52. case 'conditioned': return ConditionedControl;
  53. case 'multi-select': return MultiSelectControl;
  54. case 'color': return CombinedColorControl;
  55. case 'color-list': return ColorListControl;
  56. case 'vec3': return Vec3Control;
  57. case 'file': return FileControl;
  58. case 'select': return SelectControl;
  59. case 'text': return TextControl;
  60. case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
  61. ? BoundedIntervalControl : IntervalControl;
  62. case 'group': return GroupControl;
  63. case 'mapped': return MappedControl;
  64. case 'line-graph': return LineGraphControl;
  65. case 'script': return ScriptControl;
  66. case 'object-list': return ObjectListControl;
  67. default:
  68. const _: never = param;
  69. console.warn(`${_} has no associated UI component`);
  70. return void 0;
  71. }
  72. }
  73. export class ParamHelp<L extends LegendData> extends React.PureComponent<{ legend?: L, description?: string }> {
  74. render() {
  75. const { legend, description } = this.props
  76. const Legend = legend && legendFor(legend)
  77. return <div className='msp-control-row msp-help-text'>
  78. <div>
  79. <div className='msp-help-description'><span className={`msp-icon msp-icon-help-circle`} />{description}</div>
  80. {Legend && <div className='msp-help-legend'><Legend legend={legend} /></div>}
  81. </div>
  82. </div>
  83. }
  84. }
  85. export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
  86. export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> {
  87. name: string,
  88. value: P['defaultValue'],
  89. param: P,
  90. isDisabled?: boolean,
  91. onChange: ParamOnChange,
  92. onEnter?: () => void
  93. }
  94. export type ParamControl = React.ComponentClass<ParamProps<any>>
  95. export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { isExpanded: boolean }> {
  96. state = { isExpanded: false };
  97. protected update(value: P['defaultValue']) {
  98. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  99. }
  100. abstract renderControl(): JSX.Element;
  101. private get className() {
  102. const className = ['msp-control-row'];
  103. if (this.props.param.shortLabel) className.push('msp-control-label-short')
  104. if (this.props.param.twoColumns) className.push('msp-control-col-2')
  105. return className.join(' ')
  106. }
  107. toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
  108. render() {
  109. const label = this.props.param.label || camelCaseToWords(this.props.name);
  110. const help = this.props.param.help
  111. ? this.props.param.help(this.props.value)
  112. : { description: this.props.param.description, legend: this.props.param.legend }
  113. const desc = this.props.param.description;
  114. const hasHelp = help.description || help.legend
  115. return <>
  116. <div className={this.className}>
  117. <span title={desc}>
  118. {label}
  119. {hasHelp &&
  120. <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={this.toggleExpanded}
  121. title={desc || `${this.state.isExpanded ? 'Hide' : 'Show'} help`}
  122. style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
  123. <span className={`msp-icon msp-icon-help-circle-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
  124. </button>
  125. }
  126. </span>
  127. <div>
  128. {this.renderControl()}
  129. </div>
  130. </div>
  131. {hasHelp && this.state.isExpanded && <div className='msp-control-offset'>
  132. <ParamHelp legend={help.legend} description={help.description} />
  133. </div>}
  134. </>;
  135. }
  136. }
  137. export class BoolControl extends SimpleParam<PD.BooleanParam> {
  138. onClick = (e: React.MouseEvent<HTMLButtonElement>) => { this.update(!this.props.value); e.currentTarget.blur(); }
  139. renderControl() {
  140. return <button onClick={this.onClick} disabled={this.props.isDisabled}>
  141. <span className={`msp-icon msp-icon-${this.props.value ? 'ok' : 'off'}`} />
  142. {this.props.value ? 'On' : 'Off'}
  143. </button>;
  144. }
  145. }
  146. export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGraph>, { isExpanded: boolean, isOverPoint: boolean, message: string }> {
  147. state = {
  148. isExpanded: false,
  149. isOverPoint: false,
  150. message: `${this.props.param.defaultValue.length} points`,
  151. }
  152. onHover = (point?: Vec2) => {
  153. this.setState({ isOverPoint: !this.state.isOverPoint });
  154. if (point) {
  155. this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
  156. return;
  157. }
  158. this.setState({ message: `${this.props.value.length} points` });
  159. }
  160. onDrag = (point: Vec2) => {
  161. this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
  162. }
  163. onChange = (value: PD.LineGraph['defaultValue']) => {
  164. this.props.onChange({ name: this.props.name, param: this.props.param, value: value });
  165. }
  166. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  167. this.setState({ isExpanded: !this.state.isExpanded });
  168. e.currentTarget.blur();
  169. }
  170. render() {
  171. const label = this.props.param.label || camelCaseToWords(this.props.name);
  172. return <>
  173. <div className='msp-control-row'>
  174. <span>{label}</span>
  175. <div>
  176. <button onClick={this.toggleExpanded}>
  177. {`${this.state.message}`}
  178. </button>
  179. </div>
  180. </div>
  181. <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
  182. <LineGraphComponent
  183. data={this.props.param.defaultValue}
  184. onChange={this.onChange}
  185. onHover={this.onHover}
  186. onDrag={this.onDrag} />
  187. </div>
  188. </>;
  189. }
  190. }
  191. export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>> {
  192. state = { value: '0' };
  193. update = (value: number) => {
  194. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  195. }
  196. render() {
  197. const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
  198. const label = this.props.param.label || camelCaseToWords(this.props.name);
  199. return <div className='msp-control-row'>
  200. <span title={this.props.param.description}>{label}</span>
  201. <div>
  202. <NumericInput
  203. value={this.props.value} onEnter={this.props.onEnter} placeholder={placeholder}
  204. isDisabled={this.props.isDisabled} onChange={this.update} />
  205. </div>
  206. </div>;
  207. }
  208. }
  209. export class NumberRangeControl extends SimpleParam<PD.Numeric> {
  210. onChange = (v: number) => { this.update(v); }
  211. renderControl() {
  212. const value = typeof this.props.value === 'undefined' ? this.props.param.defaultValue : this.props.value;
  213. return <Slider value={value} min={this.props.param.min!} max={this.props.param.max!}
  214. step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />
  215. }
  216. }
  217. export class TextControl extends SimpleParam<PD.Text> {
  218. onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  219. const value = e.target.value;
  220. if (value !== this.props.value) {
  221. this.update(value);
  222. }
  223. }
  224. onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
  225. if ((e.keyCode === 13 || e.charCode === 13)) {
  226. if (this.props.onEnter) this.props.onEnter();
  227. }
  228. e.stopPropagation();
  229. }
  230. renderControl() {
  231. const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
  232. return <input type='text'
  233. value={this.props.value || ''}
  234. placeholder={placeholder}
  235. onChange={this.onChange}
  236. onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
  237. disabled={this.props.isDisabled}
  238. />;
  239. }
  240. }
  241. export class PureSelectControl extends React.PureComponent<ParamProps<PD.Select<string | number>> & { title?: string }> {
  242. protected update(value: string | number) {
  243. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  244. }
  245. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  246. if (typeof this.props.param.defaultValue === 'number') {
  247. this.update(parseInt(e.target.value, 10));
  248. } else {
  249. this.update(e.target.value);
  250. }
  251. }
  252. render() {
  253. const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
  254. return <select className='msp-form-control' title={this.props.title} value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
  255. {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
  256. {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
  257. </select>;
  258. }
  259. }
  260. export class SelectControl extends SimpleParam<PD.Select<string | number>> {
  261. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  262. if (typeof this.props.param.defaultValue === 'number') {
  263. this.update(parseInt(e.target.value, 10));
  264. } else {
  265. this.update(e.target.value);
  266. }
  267. }
  268. renderControl() {
  269. const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
  270. return <select value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
  271. {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
  272. {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
  273. </select>;
  274. }
  275. }
  276. export class IntervalControl extends SimpleParam<PD.Interval> {
  277. onChange = (v: [number, number]) => { this.update(v); }
  278. renderControl() {
  279. return <span>interval TODO</span>;
  280. }
  281. }
  282. export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
  283. onChange = (v: [number, number]) => { this.update(v); }
  284. renderControl() {
  285. return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
  286. step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />;
  287. }
  288. }
  289. export class ColorControl extends SimpleParam<PD.Color> {
  290. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  291. this.update(Color(parseInt(e.target.value)));
  292. }
  293. stripStyle(): React.CSSProperties {
  294. return {
  295. background: Color.toStyle(this.props.value),
  296. position: 'absolute',
  297. bottom: '0',
  298. height: '4px',
  299. right: '0',
  300. left: '0'
  301. };
  302. }
  303. renderControl() {
  304. return <div style={{ position: 'relative' }}>
  305. <select value={this.props.value} onChange={this.onChange}>
  306. {ColorValueOption(this.props.value)}
  307. {ColorOptions()}
  308. </select>
  309. <div style={this.stripStyle()} />
  310. </div>;
  311. }
  312. }
  313. const colorGradientInterpolated = memoize1((colors: Color[]) => {
  314. const styles = colors.map(c => Color.toStyle(c))
  315. return `linear-gradient(to right, ${styles.join(', ')})`
  316. });
  317. const colorGradientBanded = memoize1((colors: Color[]) => {
  318. const n = colors.length
  319. const styles: string[] = [`${Color.toStyle(colors[0])} ${100 * (1 / n)}%`]
  320. for (let i = 1, il = n - 1; i < il; ++i) {
  321. styles.push(
  322. `${Color.toStyle(colors[i])} ${100 * (i / n)}%`,
  323. `${Color.toStyle(colors[i])} ${100 * ((i + 1) / n)}%`
  324. )
  325. }
  326. styles.push(`${Color.toStyle(colors[n - 1])} ${100 * ((n - 1) / n)}%`)
  327. return `linear-gradient(to right, ${styles.join(', ')})`
  328. });
  329. function colorGradient(name: ColorListName, banded: boolean) {
  330. const { list, type } = getColorListFromName(name)
  331. return type === 'qualitative' ? colorGradientBanded(list) : colorGradientInterpolated(list)
  332. }
  333. export class ColorListControl extends SimpleParam<PD.ColorList<any>> {
  334. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { this.update(e.target.value); }
  335. stripStyle(): React.CSSProperties {
  336. return {
  337. background: colorGradient(this.props.value, true),
  338. position: 'absolute',
  339. bottom: '0',
  340. height: '4px',
  341. right: '0',
  342. left: '0'
  343. };
  344. }
  345. renderControl() {
  346. return <div style={{ position: 'relative' }}>
  347. <select value={this.props.value || ''} onChange={this.onChange} disabled={this.props.isDisabled}>
  348. {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
  349. </select>
  350. <div style={this.stripStyle()} />
  351. </div>;
  352. }
  353. }
  354. export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isExpanded: boolean }> {
  355. state = { isExpanded: false }
  356. components = {
  357. 0: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
  358. 1: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
  359. 2: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
  360. }
  361. change(value: PD.MultiSelect<any>['defaultValue']) {
  362. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  363. }
  364. componentChange: ParamOnChange = ({ name, value }) => {
  365. const v = Vec3.copy(Vec3.zero(), this.props.value);
  366. v[+name] = value;
  367. this.change(v);
  368. }
  369. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  370. this.setState({ isExpanded: !this.state.isExpanded });
  371. e.currentTarget.blur();
  372. }
  373. render() {
  374. const v = this.props.value;
  375. const label = this.props.param.label || camelCaseToWords(this.props.name);
  376. const value = `[${v[0].toFixed(2)}, ${v[1].toFixed(2)}, ${v[2].toFixed(2)}]`;
  377. return <>
  378. <div className='msp-control-row'>
  379. <span>{label}</span>
  380. <div>
  381. <button onClick={this.toggleExpanded}>{value}</button>
  382. </div>
  383. </div>
  384. <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
  385. <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
  386. </div>
  387. </>;
  388. }
  389. }
  390. export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
  391. change(value: File) {
  392. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  393. }
  394. onChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
  395. this.change(e.target.files![0]);
  396. }
  397. render() {
  398. const value = this.props.value;
  399. // return <input disabled={this.props.isDisabled} value={void 0} type='file' multiple={false} />
  400. return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
  401. {value ? value.name : 'Select a file...'} <input disabled={this.props.isDisabled} onChange={this.onChangeFile} type='file' multiple={false} accept={this.props.param.accept} />
  402. </div>
  403. }
  404. }
  405. export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
  406. state = { isExpanded: false }
  407. change(value: PD.MultiSelect<any>['defaultValue']) {
  408. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  409. }
  410. toggle(key: string) {
  411. return (e: React.MouseEvent<HTMLButtonElement>) => {
  412. if (this.props.value.indexOf(key) < 0) this.change(this.props.value.concat(key));
  413. else this.change(this.props.value.filter(v => v !== key));
  414. e.currentTarget.blur();
  415. }
  416. }
  417. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  418. this.setState({ isExpanded: !this.state.isExpanded });
  419. e.currentTarget.blur();
  420. }
  421. render() {
  422. const current = this.props.value;
  423. const label = this.props.param.label || camelCaseToWords(this.props.name);
  424. return <>
  425. <div className='msp-control-row'>
  426. <span>{label}</span>
  427. <div>
  428. <button onClick={this.toggleExpanded}>
  429. {`${current.length} of ${this.props.param.options.length}`}
  430. </button>
  431. </div>
  432. </div>
  433. <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
  434. {this.props.param.options.map(([value, label]) => {
  435. const sel = current.indexOf(value) >= 0;
  436. return <div key={value} className='msp-row'>
  437. <button onClick={this.toggle(value)} disabled={this.props.isDisabled}>
  438. <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
  439. </button>
  440. </div>
  441. })}
  442. </div>
  443. </>;
  444. }
  445. }
  446. export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> {
  447. state = { isExpanded: !!this.props.param.isExpanded }
  448. change(value: any) {
  449. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  450. }
  451. onChangeParam: ParamOnChange = e => {
  452. this.change({ ...this.props.value, [e.name]: e.value });
  453. }
  454. toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
  455. render() {
  456. const params = this.props.param.params;
  457. // Do not show if there are no params.
  458. if (Object.keys(params).length === 0) return null;
  459. const label = this.props.param.label || camelCaseToWords(this.props.name);
  460. const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
  461. if (this.props.param.isFlat) {
  462. return controls;
  463. }
  464. return <div className='msp-control-group-wrapper'>
  465. <div className='msp-control-group-header'>
  466. <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
  467. <span className={`msp-icon msp-icon-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
  468. {label}
  469. </button>
  470. </div>
  471. {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
  472. {controls}
  473. </div>}
  474. </div>
  475. }
  476. }
  477. export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> {
  478. private valuesCache: { [name: string]: PD.Values<any> } = {}
  479. private setValues(name: string, values: PD.Values<any>) {
  480. this.valuesCache[name] = values
  481. }
  482. private getValues(name: string) {
  483. if (name in this.valuesCache) {
  484. return this.valuesCache[name]
  485. } else {
  486. return this.props.param.map(name).defaultValue
  487. }
  488. }
  489. change(value: PD.Mapped<any>['defaultValue']) {
  490. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  491. }
  492. onChangeName: ParamOnChange = e => {
  493. this.change({ name: e.value, params: this.getValues(e.value) });
  494. }
  495. onChangeParam: ParamOnChange = e => {
  496. this.setValues(this.props.value.name, e.value)
  497. this.change({ name: this.props.value.name, params: e.value });
  498. }
  499. render() {
  500. const value: PD.Mapped<any>['defaultValue'] = this.props.value;
  501. const param = this.props.param.map(value.name);
  502. const label = this.props.param.label || camelCaseToWords(this.props.name);
  503. const Mapped = controlFor(param);
  504. const help = this.props.param.help
  505. const select = help
  506. ? {
  507. ...this.props.param.select,
  508. help: (name: any) => help({ name, params: this.getValues(name) })
  509. }
  510. : this.props.param.select
  511. const Select = <SelectControl param={select}
  512. isDisabled={this.props.isDisabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
  513. name={label} value={value.name} />
  514. if (!Mapped) {
  515. return Select;
  516. }
  517. return <>
  518. {Select}
  519. <Mapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  520. </>
  521. }
  522. }
  523. class ObjectListEditor extends React.PureComponent<{ params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }, { params: PD.Params, value: object, current: object }> {
  524. state = { params: {}, value: void 0 as any, current: void 0 as any };
  525. onChangeParam: ParamOnChange = e => {
  526. this.setState({ current: { ...this.state.current, [e.name]: e.value } });
  527. }
  528. apply = () => {
  529. this.props.apply(this.state.current);
  530. }
  531. static getDerivedStateFromProps(props: _Props<ObjectListEditor>, state: _State<ObjectListEditor>): _State<ObjectListEditor> | null {
  532. if (props.params === state.params && props.value === state.value) return null;
  533. return {
  534. params: props.params,
  535. value: props.value,
  536. current: props.value
  537. };
  538. }
  539. render() {
  540. return <>
  541. <ParameterControls params={this.props.params} onChange={this.onChangeParam} values={this.state.current} onEnter={this.apply} isDisabled={this.props.isDisabled} />
  542. <button className={`msp-btn msp-btn-block msp-form-control msp-control-top-offset`} onClick={this.apply} disabled={this.props.isDisabled}>
  543. {this.props.isUpdate ? 'Update' : 'Add'}
  544. </button>
  545. </>;
  546. }
  547. }
  548. class ObjectListItem extends React.PureComponent<{ param: PD.ObjectList, value: object, index: number, actions: ObjectListControl['actions'], isDisabled?: boolean }, { isExpanded: boolean }> {
  549. state = { isExpanded: false };
  550. update = (v: object) => {
  551. this.setState({ isExpanded: false });
  552. this.props.actions.update(v, this.props.index);
  553. }
  554. moveUp = () => {
  555. this.props.actions.move(this.props.index, -1);
  556. };
  557. moveDown = () => {
  558. this.props.actions.move(this.props.index, 1);
  559. };
  560. remove = () => {
  561. this.setState({ isExpanded: false });
  562. this.props.actions.remove(this.props.index);
  563. };
  564. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  565. this.setState({ isExpanded: !this.state.isExpanded });
  566. e.currentTarget.blur();
  567. };
  568. static getDerivedStateFromProps(props: _Props<ObjectListEditor>, state: _State<ObjectListEditor>): _State<ObjectListEditor> | null {
  569. if (props.params === state.params && props.value === state.value) return null;
  570. return {
  571. params: props.params,
  572. value: props.value,
  573. current: props.value
  574. };
  575. }
  576. render() {
  577. return <>
  578. <div className='msp-param-object-list-item'>
  579. <button className='msp-btn msp-btn-block msp-form-control' onClick={this.toggleExpanded}>
  580. <span>{`${this.props.index + 1}: `}</span>
  581. {this.props.param.getLabel(this.props.value)}
  582. </button>
  583. <div>
  584. <IconButton icon='up-thin' title='Move Up' onClick={this.moveUp} isSmall={true} />
  585. <IconButton icon='down-thin' title='Move Down' onClick={this.moveDown} isSmall={true} />
  586. <IconButton icon='remove' title='Remove' onClick={this.remove} isSmall={true} />
  587. </div>
  588. </div>
  589. {this.state.isExpanded && <div className='msp-control-offset'>
  590. <ObjectListEditor params={this.props.param.element} apply={this.update} value={this.props.value} isUpdate isDisabled={this.props.isDisabled} />
  591. </div>}
  592. </>;
  593. }
  594. }
  595. export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean }> {
  596. state = { isExpanded: false }
  597. change(value: any) {
  598. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  599. }
  600. add = (v: object) => {
  601. this.change([...this.props.value, v]);
  602. };
  603. actions = {
  604. update: (v: object, i: number) => {
  605. const value = this.props.value.slice(0);
  606. value[i] = v;
  607. this.change(value);
  608. },
  609. move: (i: number, dir: -1 | 1) => {
  610. let xs = this.props.value;
  611. if (xs.length === 1) return;
  612. let j = (i + dir) % xs.length;
  613. if (j < 0) j += xs.length;
  614. xs = xs.slice(0);
  615. const t = xs[i];
  616. xs[i] = xs[j];
  617. xs[j] = t;
  618. this.change(xs);
  619. },
  620. remove: (i: number) => {
  621. const xs = this.props.value;
  622. const update: object[] = [];
  623. for (let j = 0; j < xs.length; j++) {
  624. if (i !== j) update.push(xs[j]);
  625. }
  626. this.change(update);
  627. }
  628. }
  629. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  630. this.setState({ isExpanded: !this.state.isExpanded });
  631. e.currentTarget.blur();
  632. };
  633. render() {
  634. const v = this.props.value;
  635. const label = this.props.param.label || camelCaseToWords(this.props.name);
  636. const value = `${v.length} item${v.length !== 1 ? 's' : ''}`;
  637. return <>
  638. <div className='msp-control-row'>
  639. <span>{label}</span>
  640. <div>
  641. <button onClick={this.toggleExpanded}>{value}</button>
  642. </div>
  643. </div>
  644. {this.state.isExpanded && <div className='msp-control-offset'>
  645. {this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} />)}
  646. <ControlGroup header='New Item'>
  647. <ObjectListEditor params={this.props.param.element} apply={this.add} value={this.props.param.ctor()} isDisabled={this.props.isDisabled} />
  648. </ControlGroup>
  649. </div>}
  650. </>;
  651. }
  652. }
  653. export class ConditionedControl extends React.PureComponent<ParamProps<PD.Conditioned<any, any, any>>> {
  654. change(value: PD.Conditioned<any, any, any>['defaultValue']) {
  655. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  656. }
  657. onChangeCondition: ParamOnChange = e => {
  658. this.change(this.props.param.conditionedValue(this.props.value, e.value));
  659. }
  660. onChangeParam: ParamOnChange = e => {
  661. this.change(e.value);
  662. }
  663. render() {
  664. const value = this.props.value;
  665. const condition = this.props.param.conditionForValue(value) as string
  666. const param = this.props.param.conditionParams[condition];
  667. const label = this.props.param.label || camelCaseToWords(this.props.name);
  668. const Conditioned = controlFor(param);
  669. const select = <SelectControl param={this.props.param.select}
  670. isDisabled={this.props.isDisabled} onChange={this.onChangeCondition} onEnter={this.props.onEnter}
  671. name={`${label} Kind`} value={condition} />
  672. if (!Conditioned) {
  673. return select;
  674. }
  675. return <>
  676. {select}
  677. <Conditioned param={param} value={value} name={label} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  678. </>
  679. }
  680. }
  681. export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converted<any, any>>> {
  682. onChange: ParamOnChange = e => {
  683. this.props.onChange({
  684. name: this.props.name,
  685. param: this.props.param,
  686. value: this.props.param.toValue(e.value)
  687. });
  688. }
  689. render() {
  690. const value = this.props.param.fromValue(this.props.value);
  691. const Converted = controlFor(this.props.param.converted);
  692. if (!Converted) return null;
  693. return <Converted param={this.props.param.converted} value={value} name={this.props.name} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  694. }
  695. }
  696. export class ScriptControl extends SimpleParam<PD.Script> {
  697. onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  698. const value = e.target.value;
  699. if (value !== this.props.value.expression) {
  700. this.update({ language: this.props.value.language, expression: value });
  701. }
  702. }
  703. onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
  704. if ((e.keyCode === 13 || e.charCode === 13)) {
  705. if (this.props.onEnter) this.props.onEnter();
  706. }
  707. e.stopPropagation();
  708. }
  709. renderControl() {
  710. // TODO: improve!
  711. const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
  712. return <input type='text'
  713. value={this.props.value.expression || ''}
  714. placeholder={placeholder}
  715. onChange={this.onChange}
  716. onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
  717. disabled={this.props.isDisabled}
  718. />;
  719. }
  720. }