aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/ObservableReactComponent.tsx
blob: 2290516dcb8b8ec98e5c53e32744183c789edaf8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { action, makeObservable, observable } from 'mobx';
import * as React from 'react';
import './AntimodeMenu.scss';
import { observer } from 'mobx-react';
import JsxParser from 'react-jsx-parser';

/**
 * This is an abstract class that serves as the base for a PDF-style or Marquee-style
 * menu. To use this class, look at PDFMenu.tsx or MarqueeOptionsMenu.tsx for an example.
 */
export abstract class ObservableReactComponent<T> extends React.Component<T, object> {
    @observable _props: React.PropsWithChildren<T>;
    constructor(props: React.PropsWithChildren<T>) {
        super(props);
        this._props = props;
        makeObservable(this);
    }
    __passiveWheel: HTMLElement | null = null;
    _isContentActive: () => boolean | undefined = () => false;

    /**
     * default method to stop wheel events from bubbling up to parent components.
     * @param e
     */
    onPassiveWheel = (e: WheelEvent) => {
        if (this._isContentActive?.()) e.stopPropagation();
    };

    /**
     * This fixes the problem where a component uses wheel events to scroll, but is nested inside another component that
     * can also scroll. In that case, the wheel event will bubble up to the parent component and cause it to scroll in addition.
     * This is based on the native HTML5 behavior where wheel events are passive by default, meaning that they do not prevent the default action of scrolling.
     * This method should be called from a ref={} property on or above the component that uses wheel events to scroll.
     * @param ele HTMLELement containing the component that will scroll
     * @param isContentActive function determining if the component is active and should handle the wheel event.
     * @param onPassiveWheel an optional function to call to handle the wheel event (and block its propagation. If omitted, the event won't propagate.
     */
    fixWheelEvents = (ele: HTMLElement | null, isContentActive: () => boolean | undefined, onPassiveWheel?: (e: WheelEvent) => void) => {
        this._isContentActive = isContentActive;
        this.__passiveWheel?.removeEventListener('wheel', onPassiveWheel ?? this.onPassiveWheel);
        this.__passiveWheel = ele;
        ele?.addEventListener('wheel', onPassiveWheel ?? this.onPassiveWheel, { passive: false });
    };

    componentDidUpdate(prevProps: Readonly<T>): void {
        Object.keys(prevProps)
            .filter(pkey => (prevProps as {[key:string]: unknown})[pkey] !== (this.props as  {[key:string]: unknown})[pkey])
            .forEach(action(pkey => {
                (this._props as  {[key:string]: unknown})[pkey] = (this.props as  {[key:string]: unknown})[pkey];
            })); // prettier-ignore
    }
}

class ObserverJsxParser1 extends JsxParser {
    constructor(props: object) {
        super(props);
        observer(this as typeof JsxParser);
    }
}

export const ObserverJsxParser = ObserverJsxParser1 as typeof JsxParser;