@atlaskit/editor-plugin-placeholder
Version:
Placeholder plugin for @atlaskit/editor-core.
174 lines (171 loc) • 10.3 kB
JavaScript
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
import { EMPTY_PARAGRAPH_TIMEOUT_DELAY, pluginKey } from '../placeholderPlugin';
import { TYPEWRITER_TYPED_AND_DELETED_DELAY } from './constants';
import { createPlaceholderDecoration } from './decorations';
import { calculateUserInteractionState, createPlaceHolderStateFrom, getPlaceholderState } from './utils';
export default function createPlugin(intl, defaultPlaceholderText, bracketPlaceholderText, emptyLinePlaceholder, placeholderPrompts, withEmptyParagraph, initialIsPlaceholderHidden, placeholderADF, isRovoLLMEnabled, api) {
if (!defaultPlaceholderText && !placeholderPrompts && !bracketPlaceholderText && !placeholderADF) {
return;
}
let isDestroyed = false;
let activeTypewriterTimeouts = [];
const clearAllTypewriterTimeouts = () => {
activeTypewriterTimeouts.forEach(clearFn => clearFn());
activeTypewriterTimeouts = [];
};
return new SafePlugin({
key: pluginKey,
state: {
init: (_, state) => {
var _api$focus, _api$focus$sharedStat, _api$typeAhead;
return createPlaceHolderStateFrom({
isInitial: true,
isEditorFocused: Boolean(api === null || api === void 0 ? void 0 : (_api$focus = api.focus) === null || _api$focus === void 0 ? void 0 : (_api$focus$sharedStat = _api$focus.sharedState.currentState()) === null || _api$focus$sharedStat === void 0 ? void 0 : _api$focus$sharedStat.hasFocus),
editorState: state,
isTypeAheadOpen: api === null || api === void 0 ? void 0 : (_api$typeAhead = api.typeAhead) === null || _api$typeAhead === void 0 ? void 0 : _api$typeAhead.actions.isOpen,
defaultPlaceholderText,
bracketPlaceholderText,
emptyLinePlaceholder,
placeholderADF,
placeholderPrompts,
typedAndDeleted: false,
userHadTyped: false,
intl,
withEmptyParagraph,
isPlaceholderHidden: initialIsPlaceholderHidden
});
},
apply: (tr, placeholderState, _oldEditorState, newEditorState) => {
var _api$focus2, _api$focus2$sharedSta, _placeholderState$isP, _api$typeAhead2, _ref, _meta$placeholderText, _ref2, _meta$placeholderProm, _meta$showOnEmptyPara;
const meta = tr.getMeta(pluginKey);
const isEditorFocused = Boolean(api === null || api === void 0 ? void 0 : (_api$focus2 = api.focus) === null || _api$focus2 === void 0 ? void 0 : (_api$focus2$sharedSta = _api$focus2.sharedState.currentState()) === null || _api$focus2$sharedSta === void 0 ? void 0 : _api$focus2$sharedSta.hasFocus);
const {
userHadTyped,
typedAndDeleted
} = calculateUserInteractionState({
placeholderState,
oldEditorState: _oldEditorState,
newEditorState
});
let isPlaceholderHidden = (_placeholderState$isP = placeholderState === null || placeholderState === void 0 ? void 0 : placeholderState.isPlaceholderHidden) !== null && _placeholderState$isP !== void 0 ? _placeholderState$isP : false;
const shouldUpdatePlaceholderHidden = (meta === null || meta === void 0 ? void 0 : meta.isPlaceholderHidden) !== undefined;
if (shouldUpdatePlaceholderHidden) {
isPlaceholderHidden = meta.isPlaceholderHidden;
}
if ((meta === null || meta === void 0 ? void 0 : meta.placeholderText) !== undefined && withEmptyParagraph) {
const isCreateWithRovoOverride = !!meta.placeholderText && isRovoLLMEnabled && expVal('cwr_blank_object_experiment', 'isEnabled', false);
// Only update defaultPlaceholderText from meta if we're not using ADF placeholder
// OR when the create-with-rovo experiment is active to allow intentional non-empty placeholder overrides
if (!placeholderADF || isCreateWithRovoOverride) {
defaultPlaceholderText = meta.placeholderText;
}
}
const newPlaceholderState = createPlaceHolderStateFrom({
isEditorFocused,
editorState: newEditorState,
isTypeAheadOpen: api === null || api === void 0 ? void 0 : (_api$typeAhead2 = api.typeAhead) === null || _api$typeAhead2 === void 0 ? void 0 : _api$typeAhead2.actions.isOpen,
defaultPlaceholderText: withEmptyParagraph ? defaultPlaceholderText : (_ref = (_meta$placeholderText = meta === null || meta === void 0 ? void 0 : meta.placeholderText) !== null && _meta$placeholderText !== void 0 ? _meta$placeholderText : placeholderState === null || placeholderState === void 0 ? void 0 : placeholderState.placeholderText) !== null && _ref !== void 0 ? _ref : defaultPlaceholderText,
bracketPlaceholderText,
emptyLinePlaceholder,
placeholderADF,
placeholderPrompts: (_ref2 = (_meta$placeholderProm = meta === null || meta === void 0 ? void 0 : meta.placeholderPrompts) !== null && _meta$placeholderProm !== void 0 ? _meta$placeholderProm : placeholderState === null || placeholderState === void 0 ? void 0 : placeholderState.placeholderPrompts) !== null && _ref2 !== void 0 ? _ref2 : placeholderPrompts,
typedAndDeleted,
userHadTyped,
intl,
isPlaceholderHidden,
withEmptyParagraph,
showOnEmptyParagraph: (_meta$showOnEmptyPara = meta === null || meta === void 0 ? void 0 : meta.showOnEmptyParagraph) !== null && _meta$showOnEmptyPara !== void 0 ? _meta$showOnEmptyPara : placeholderState === null || placeholderState === void 0 ? void 0 : placeholderState.showOnEmptyParagraph
});
// Clear timeouts when hasPlaceholder becomes false
if (!newPlaceholderState.hasPlaceholder) {
clearAllTypewriterTimeouts();
}
return newPlaceholderState;
}
},
props: {
decorations(editorState) {
var _api$composition, _api$showDiff, _api$showDiff$sharedS;
if (expValEquals('confluence_load_editor_title_on_transition', 'contentPlaceholder', true)) {
// @ts-ignore quick fix which needs follow up to use standard apis
if (editorState.collabEditPlugin$ && editorState.collabEditPlugin$.isReady !== true) {
return;
}
}
const {
hasPlaceholder,
placeholderText,
pos,
typedAndDeleted,
contextPlaceholderADF,
showOnEmptyParagraph
} = getPlaceholderState(editorState);
// Decorations is still called after plugin is destroyed
// So we need to make sure decorations is not called if plugin has been destroyed to prevent the placeholder animations' setTimeouts called infinitely
if (isDestroyed) {
return;
}
const compositionPluginState = api === null || api === void 0 ? void 0 : (_api$composition = api.composition) === null || _api$composition === void 0 ? void 0 : _api$composition.sharedState.currentState();
const isShowingDiff = Boolean(api === null || api === void 0 ? void 0 : (_api$showDiff = api.showDiff) === null || _api$showDiff === void 0 ? void 0 : (_api$showDiff$sharedS = _api$showDiff.sharedState.currentState()) === null || _api$showDiff$sharedS === void 0 ? void 0 : _api$showDiff$sharedS.isDisplayingChanges);
if (hasPlaceholder && ((placeholderText !== null && placeholderText !== void 0 ? placeholderText : '') || placeholderPrompts || placeholderADF || contextPlaceholderADF) && pos !== undefined && !(compositionPluginState !== null && compositionPluginState !== void 0 && compositionPluginState.isComposing) && !isShowingDiff) {
const initialDelayWhenUserTypedAndDeleted = typedAndDeleted ? TYPEWRITER_TYPED_AND_DELETED_DELAY : 0;
// contextPlaceholderADF takes precedence over the global placeholderADF
const placeholderAdfToUse = contextPlaceholderADF || placeholderADF;
return createPlaceholderDecoration(editorState, placeholderText !== null && placeholderText !== void 0 ? placeholderText : '', placeholderPrompts, activeTypewriterTimeouts, pos, initialDelayWhenUserTypedAndDeleted, placeholderAdfToUse, showOnEmptyParagraph);
}
return;
}
},
view() {
let timeoutId;
function startEmptyParagraphTimeout(editorView) {
if (timeoutId) {
return;
}
timeoutId = setTimeout(() => {
timeoutId = undefined;
editorView.dispatch(editorView.state.tr.setMeta(pluginKey, {
showOnEmptyParagraph: true
}));
}, EMPTY_PARAGRAPH_TIMEOUT_DELAY);
}
function destroyEmptyParagraphTimeout() {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
}
return {
update(editorView, prevState) {
if (withEmptyParagraph) {
const prevPluginState = getPlaceholderState(prevState);
const newPluginState = getPlaceholderState(editorView.state);
// user start typing after move to an empty paragraph, clear timeout
if (!newPluginState.canShowOnEmptyParagraph && timeoutId) {
destroyEmptyParagraphTimeout();
}
// user move to an empty paragraph again, reset state to hide placeholder, and restart timeout
else if (prevPluginState.canShowOnEmptyParagraph && newPluginState.canShowOnEmptyParagraph && newPluginState.pos !== prevPluginState.pos) {
editorView.dispatch(editorView.state.tr.setMeta(pluginKey, {
showOnEmptyParagraph: false
}));
destroyEmptyParagraphTimeout();
startEmptyParagraphTimeout(editorView);
}
// user move to an empty paragraph (by click enter or move to an empty paragraph), start timeout
else if (!prevPluginState.canShowOnEmptyParagraph && newPluginState.canShowOnEmptyParagraph && !newPluginState.showOnEmptyParagraph && !timeoutId) {
startEmptyParagraphTimeout(editorView);
}
}
},
destroy() {
clearAllTypewriterTimeouts();
destroyEmptyParagraphTimeout();
isDestroyed = true;
}
};
}
});
}