@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
251 lines (248 loc) • 9.18 kB
JavaScript
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 };