@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
538 lines (527 loc) • 20.5 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _extends from "@babel/runtime/helpers/extends";
import React from 'react';
import PropTypes from 'prop-types';
import { fg } from '@atlaskit/platform-feature-flags';
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '../analytics';
import { createDispatch } from '../event-dispatcher';
import { startMeasure, stopMeasure } from '../performance-measures';
import { EditorContext } from '../ui/EditorContext';
import { analyticsEventKey } from '../utils';
const DEFAULT_SAMPLING_RATE = 100;
const DEFAULT_SLOW_THRESHOLD = 4;
// That context was extract 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 })
* }
* ```
*
*/
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/react/no-class-components, react/prefer-stateless-function
class WithPluginState extends React.Component {
constructor(props) {
super(props);
}
render() {
if (fg('platform_editor_react18_phase2_v2')) {
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
return /*#__PURE__*/React.createElement(WithPluginStateNew, this.props);
}
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
return /*#__PURE__*/React.createElement(WithPluginStateOld, this.props);
}
}
function WithPluginStateNew(props) {
const context = React.useContext(EditorContext);
return /*#__PURE__*/React.createElement(WithPluginStateInner
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
, _extends({}, props, {
editorActions: context === null || context === void 0 ? void 0 : context.editorActions
}));
}
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/react/no-class-components
export class WithPluginStateInner extends React.Component {
constructor(props) {
super(props);
_defineProperty(this, "listeners", {});
_defineProperty(this, "debounce", null);
_defineProperty(this, "notAppliedState", {});
_defineProperty(this, "isSubscribed", false);
_defineProperty(this, "callsCount", 0);
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/max-params
_defineProperty(this, "handlePluginStateChange", (propName, pluginName, performanceOptions, skipEqualityCheck) =>
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pluginState => {
// skipEqualityCheck is being used for old plugins since they are mutating plugin state instead of creating a new one
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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
});
}
});
this.state = this.getPluginsStates(this.props.plugins, this.getEditorView(props));
}
getEditorView(maybeProps) {
const props = maybeProps || this.props;
const editorActions = props.editorActions;
return props.editorView || (editorActions === null || editorActions === void 0 ? void 0 : editorActions._privateGetEditorView());
}
getEventDispatcher(maybeProps) {
var _props$editorActions;
const props = maybeProps || this.props;
return props.eventDispatcher || ((_props$editorActions = props.editorActions) === null || _props$editorActions === void 0 ? void 0 : _props$editorActions._privateGetEventDispatcher());
}
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 => {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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;
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pluginName = pluginKey.key;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
eventDispatcher.on(pluginKey.key, handler);
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 => {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pluginState = this.listeners[key].pluginKey.getState(editorView.state);
if (pluginState && pluginState.unsubscribe) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pluginState.unsubscribe(this.listeners[key].handler);
} else {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
eventDispatcher.off(key, this.listeners[key].handler);
}
});
this.listeners = [];
}
subscribeToContextUpdates() {
var _this$props$editorAct;
(_this$props$editorAct = this.props.editorActions) === null || _this$props$editorAct === void 0 ? void 0 : _this$props$editorAct._privateSubscribe(() => this.subscribe(this.props));
}
unsubscribeFromContextUpdates() {
var _this$props$editorAct2;
(_this$props$editorAct2 = this.props.editorActions) === null || _this$props$editorAct2 === void 0 ? void 0 : _this$props$editorAct2._privateUnsubscribe(() => this.subscribe(this.props));
}
componentDidMount() {
this.subscribe(this.props);
this.subscribeToContextUpdates();
}
// Ignored via go/ees005
// eslint-disable-next-line react/no-unsafe
UNSAFE_componentWillReceiveProps(nextProps) {
if (!this.isSubscribed) {
this.subscribe(nextProps);
}
}
componentWillUnmount() {
if (this.debounce) {
window.clearTimeout(this.debounce);
}
this.unsubscribeFromContextUpdates();
this.unsubscribe();
}
render() {
const {
render
} = this.props;
return render(this.state);
}
}
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/react/no-class-components
_defineProperty(WithPluginStateInner, "displayName", 'WithPluginState');
export class WithPluginStateOld 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);
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/max-params
_defineProperty(this, "handlePluginStateChange", (propName, pluginName, performanceOptions, skipEqualityCheck) =>
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pluginState => {
// skipEqualityCheck is being used for old plugins since they are mutating plugin state instead of creating a new one
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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$samplingR2, _uiTracking$slowThres2;
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 => {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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$samplingR2 = uiTracking.samplingRate) !== null && _uiTracking$samplingR2 !== void 0 ? _uiTracking$samplingR2 : DEFAULT_SAMPLING_RATE;
const slowThreshold = (_uiTracking$slowThres2 = uiTracking.slowThreshold) !== null && _uiTracking$slowThres2 !== void 0 ? _uiTracking$slowThres2 : 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;
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pluginName = pluginKey.key;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
eventDispatcher.on(pluginKey.key, handler);
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 => {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pluginState = this.listeners[key].pluginKey.getState(editorView.state);
if (pluginState && pluginState.unsubscribe) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pluginState.unsubscribe(this.listeners[key].handler);
} else {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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);
}
// Ignored via go/ees005
// eslint-disable-next-line react/no-unsafe
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(WithPluginStateOld, "displayName", 'WithPluginState');
_defineProperty(WithPluginStateOld, "contextTypes", {
editorActions: PropTypes.object,
editorSharedConfig: PropTypes.object
});
export { WithPluginState };