base.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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 * as React from 'react';
  8. import { Observable, Subscription } from 'rxjs';
  9. import { PluginUIContext } from './context';
  10. import { Button, ColorAccent } from './controls/common';
  11. import { Icon, ArrowRightSvg, ArrowDropDownSvg } from './controls/icons';
  12. export const PluginReactContext = React.createContext(void 0 as any as PluginUIContext);
  13. export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.Component<P & { children?: any }, S, SS> {
  14. static contextType = PluginReactContext;
  15. readonly plugin: PluginUIContext;
  16. private subs: Subscription[] | undefined = void 0;
  17. protected subscribe<T>(obs: Observable<T>, action: (v: T) => void) {
  18. if (typeof this.subs === 'undefined') this.subs = [];
  19. this.subs.push(obs.subscribe(action));
  20. }
  21. componentWillUnmount() {
  22. if (!this.subs) return;
  23. for (const s of this.subs) s.unsubscribe();
  24. this.subs = void 0;
  25. }
  26. protected init?(): void;
  27. constructor(props: P, context?: any) {
  28. super(props);
  29. this.plugin = context;
  30. if (this.init) this.init();
  31. }
  32. }
  33. export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> {
  34. static contextType = PluginReactContext;
  35. readonly plugin: PluginUIContext;
  36. private subs: Subscription[] | undefined = void 0;
  37. protected subscribe<T>(obs: Observable<T>, action: (v: T) => void) {
  38. if (typeof this.subs === 'undefined') this.subs = [];
  39. this.subs.push(obs.subscribe(action));
  40. }
  41. componentWillUnmount() {
  42. if (!this.subs) return;
  43. for (const s of this.subs) s.unsubscribe();
  44. this.subs = void 0;
  45. }
  46. protected init?(): void;
  47. constructor(props: P, context?: any) {
  48. super(props, context);
  49. this.plugin = context;
  50. if (this.init) this.init();
  51. }
  52. }
  53. export type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never
  54. export type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never
  55. //
  56. export type CollapsableProps = { initiallyCollapsed?: boolean, header?: string }
  57. export type CollapsableState = {
  58. isCollapsed: boolean,
  59. header: string,
  60. description?: string,
  61. isHidden?: boolean,
  62. brand?: { svg?: React.FC, accent: ColorAccent }
  63. }
  64. export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends PluginUIComponent<P & CollapsableProps, S & CollapsableState, SS> {
  65. toggleCollapsed = () => {
  66. this.setState({ isCollapsed: !this.state.isCollapsed } as (S & CollapsableState));
  67. };
  68. componentDidUpdate(prevProps: P & CollapsableProps) {
  69. if (this.props.initiallyCollapsed !== undefined && prevProps.initiallyCollapsed !== this.props.initiallyCollapsed) {
  70. this.setState({ isCollapsed: this.props.initiallyCollapsed as any });
  71. }
  72. }
  73. protected abstract defaultState(): (S & CollapsableState)
  74. protected abstract renderControls(): JSX.Element | null
  75. render() {
  76. if (this.state.isHidden) return null;
  77. const wrapClass = this.state.isCollapsed
  78. ? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
  79. : 'msp-transform-wrapper';
  80. return <div className={wrapClass}>
  81. <div className='msp-transform-header'>
  82. <Button icon={this.state.brand ? void 0 : this.state.isCollapsed ? ArrowRightSvg : ArrowDropDownSvg} noOverflow onClick={this.toggleCollapsed}
  83. className={this.state.brand ? `msp-transform-header-brand msp-transform-header-brand-${this.state.brand.accent}` : void 0} title={`Click to ${this.state.isCollapsed ? 'expand' : 'collapse'}`}>
  84. {/* {this.state.brand && <div className={`msp-accent-bg-${this.state.brand.accent}`}>{this.state.brand.svg ? <Icon svg={this.state.brand.svg} /> : this.state.brand.name}</div>} */}
  85. <Icon svg={this.state.brand?.svg} inline />
  86. {this.state.header}
  87. <small style={{ margin: '0 6px' }}>{this.state.isCollapsed ? '' : this.state.description}</small>
  88. </Button>
  89. </div>
  90. {!this.state.isCollapsed && this.renderControls()}
  91. </div>;
  92. }
  93. constructor(props: P & CollapsableProps, context?: any) {
  94. super(props, context);
  95. const state = this.defaultState();
  96. if (props.initiallyCollapsed !== undefined) state.isCollapsed = props.initiallyCollapsed;
  97. if (props.header !== undefined) state.header = props.header;
  98. this.state = state;
  99. }
  100. }