parameters.tsx 35 KB

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