UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

251 lines (248 loc) • 9.18 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import React from 'react'; import PropTypes from 'prop-types'; import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '../analytics'; import { createDispatch } from '../event-dispatcher'; import { analyticsEventKey, startMeasure, stopMeasure } from '../utils'; const DEFAULT_SAMPLING_RATE = 100; const DEFAULT_SLOW_THRESHOLD = 4; // That context was exctract from the old WithPluginState from editor-core // It was using some private types from // - EditorAction: packages/editor/editor-core/src/actions/index.ts // - EditorSharedConfig: packages/editor/editor-core/src/labs/next/internal/context/shared-config.tsx /** * @private * @deprecated * * Using this component is deprecated. It should be replaced with `useSharedPluginState`. * This requires having access to the injection API from the plugin itself. * * An example of the refactor with the new hook (using hyperlink as an example) is: * * Before: * ```ts * <WithPluginState * editorView={editorView} * plugins={{ * hyperlinkState: hyperlinkPluginKey * }} * render={({ hyperlinkState }) => * renderComponent({ hyperlinkState }) * } * /> * ``` * * After: * ```ts * import { useSharedPluginState } from '@atlaskit/editor-common/hooks'; * import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types'; * * function ComponentWithState( * api: ExtractInjectionAPI<typeof hyperlinkPlugin> | undefined * ) { * const { hyperlinkState } = useSharedPluginState(api, ['hyperlink']); * return renderComponent({ hyperlinkState }) * } * ``` * */ class WithPluginState extends React.Component { constructor(props, context) { super(props, context); _defineProperty(this, "listeners", {}); _defineProperty(this, "debounce", null); _defineProperty(this, "notAppliedState", {}); _defineProperty(this, "isSubscribed", false); _defineProperty(this, "callsCount", 0); _defineProperty(this, "handlePluginStateChange", (propName, pluginName, performanceOptions, skipEqualityCheck) => pluginState => { // skipEqualityCheck is being used for old plugins since they are mutating plugin state instead of creating a new one if (this.state[propName] !== pluginState || skipEqualityCheck) { this.updateState({ stateSubset: { [propName]: pluginState }, pluginName, performanceOptions }); } }); /** * Debounces setState calls in order to reduce number of re-renders caused by several plugin state changes. */ _defineProperty(this, "updateState", ({ stateSubset, pluginName, performanceOptions }) => { this.notAppliedState = { ...this.notAppliedState, ...stateSubset }; if (this.debounce) { window.clearTimeout(this.debounce); } const debounce = this.props.debounce !== false ? fn => window.setTimeout(fn, 0) : fn => fn(); this.debounce = debounce(() => { const measure = `🦉${pluginName}::WithPluginState`; performanceOptions.trackingEnabled && startMeasure(measure); this.setState(this.notAppliedState, () => { performanceOptions.trackingEnabled && stopMeasure(measure, duration => { // Each WithPluginState component will fire analytics event no more than once every `samplingLimit` times if (++this.callsCount % performanceOptions.samplingRate === 0 && duration > performanceOptions.slowThreshold) { this.dispatchAnalyticsEvent({ action: ACTION.WITH_PLUGIN_STATE_CALLED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL, attributes: { plugin: pluginName, duration } }); } }); }); this.debounce = null; this.notAppliedState = {}; }); }); _defineProperty(this, "dispatchAnalyticsEvent", payload => { const eventDispatcher = this.getEventDispatcher(); if (eventDispatcher) { const dispatch = createDispatch(eventDispatcher); dispatch(analyticsEventKey, { payload }); } }); _defineProperty(this, "onContextUpdate", () => { this.subscribe(this.props); }); this.state = this.getPluginsStates(this.props.plugins, this.getEditorView(props, context)); } getEditorView(maybeProps, maybeContext) { const props = maybeProps || this.props; const context = maybeContext || this.context; return props.editorView || context && context.editorActions && context.editorActions._privateGetEditorView() || context && context.editorSharedConfig && context.editorSharedConfig.editorView; } getEventDispatcher(maybeProps) { const props = maybeProps || this.props; return props.eventDispatcher || this.context && this.context.editorActions && this.context.editorActions._privateGetEventDispatcher() || this.context && this.context.editorSharedConfig && this.context.editorSharedConfig.eventDispatcher; } getPluginsStates(plugins, editorView) { if (!editorView || !plugins) { return {}; } const keys = Object.keys(plugins); return keys.reduce((acc, propName) => { const pluginKey = plugins[propName]; if (!pluginKey) { return acc; } acc[propName] = pluginKey.getState(editorView.state); return acc; }, {}); } subscribe(props) { var _uiTracking$samplingR, _uiTracking$slowThres; const plugins = props.plugins; const eventDispatcher = this.getEventDispatcher(props); const editorView = this.getEditorView(props); if (!eventDispatcher || !editorView || this.isSubscribed) { return; } // TODO: ED-15663 // Please, do not copy or use this kind of code below // @ts-ignore const fakePluginKey = { key: 'analyticsPlugin$', getState: state => { return state['analyticsPlugin$']; } }; const analyticsPlugin = fakePluginKey.getState(editorView.state); const uiTracking = analyticsPlugin && analyticsPlugin.performanceTracking ? analyticsPlugin.performanceTracking.uiTracking || {} : {}; const trackingEnabled = uiTracking.enabled === true; const samplingRate = (_uiTracking$samplingR = uiTracking.samplingRate) !== null && _uiTracking$samplingR !== void 0 ? _uiTracking$samplingR : DEFAULT_SAMPLING_RATE; const slowThreshold = (_uiTracking$slowThres = uiTracking.slowThreshold) !== null && _uiTracking$slowThres !== void 0 ? _uiTracking$slowThres : DEFAULT_SLOW_THRESHOLD; this.isSubscribed = true; const pluginsStates = this.getPluginsStates(plugins, editorView); this.setState(pluginsStates); Object.keys(plugins).forEach(propName => { const pluginKey = plugins[propName]; if (!pluginKey) { return; } const pluginName = pluginKey.key; const pluginState = pluginsStates[propName]; const isPluginWithSubscribe = pluginState && pluginState.subscribe; const handler = this.handlePluginStateChange(propName, pluginName, { samplingRate, slowThreshold, trackingEnabled }, isPluginWithSubscribe); if (isPluginWithSubscribe) { pluginState.subscribe(handler); } else { eventDispatcher.on(pluginKey.key, handler); } this.listeners[pluginKey.key] = { handler, pluginKey }; }); } unsubscribe() { const eventDispatcher = this.getEventDispatcher(); const editorView = this.getEditorView(); if (!eventDispatcher || !editorView || !this.isSubscribed) { return; } Object.keys(this.listeners).forEach(key => { const pluginState = this.listeners[key].pluginKey.getState(editorView.state); if (pluginState && pluginState.unsubscribe) { pluginState.unsubscribe(this.listeners[key].handler); } else { eventDispatcher.off(key, this.listeners[key].handler); } }); this.listeners = []; } subscribeToContextUpdates(context) { if (context && context.editorActions) { context.editorActions._privateSubscribe(this.onContextUpdate); } } unsubscribeFromContextUpdates(context) { if (context && context.editorActions) { context.editorActions._privateUnsubscribe(this.onContextUpdate); } } componentDidMount() { this.subscribe(this.props); this.subscribeToContextUpdates(this.context); } UNSAFE_componentWillReceiveProps(nextProps) { if (!this.isSubscribed) { this.subscribe(nextProps); } } componentWillUnmount() { if (this.debounce) { window.clearTimeout(this.debounce); } this.unsubscribeFromContextUpdates(this.context); this.unsubscribe(); } render() { const { render } = this.props; return render(this.state); } } _defineProperty(WithPluginState, "displayName", 'WithPluginState'); _defineProperty(WithPluginState, "contextTypes", { editorActions: PropTypes.object, editorSharedConfig: PropTypes.object }); export { WithPluginState };