@atlaskit/editor-plugin-placeholder-text
Version:
placeholder text plugin for @atlaskit/editor-core
264 lines (263 loc) • 10.2 kB
JavaScript
import React from 'react';
import { placeholder } from '@atlaskit/adf-schema';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { toolbarInsertBlockMessages as messages } from '@atlaskit/editor-common/messages';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { isNodeEmpty } from '@atlaskit/editor-common/utils';
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import TextIcon from '@atlaskit/icon/core/text';
import { hidePlaceholderFloatingToolbar, insertPlaceholderTextAtSelection, showPlaceholderFloatingToolbar } from './editor-actions/actions';
import { drawFakeTextCursor, FakeTextCursorSelection } from './pm-plugins/fake-text-cursor/cursor';
import { PlaceholderTextNodeView } from './pm-plugins/placeholder-text-nodeview';
import { pluginKey } from './pm-plugins/plugin-key';
import { isSelectionAtPlaceholder } from './pm-plugins/utils/selection-utils';
import PlaceholderFloatingToolbar from './ui/PlaceholderFloatingToolbar';
const getOpenTypeAhead = (trigger, api) => {
var _api$typeAhead, _api$typeAhead$action, _api$typeAhead2, _api$typeAhead2$actio;
const typeAheadHandler = api === null || api === void 0 ? void 0 : (_api$typeAhead = api.typeAhead) === null || _api$typeAhead === void 0 ? void 0 : (_api$typeAhead$action = _api$typeAhead.actions) === null || _api$typeAhead$action === void 0 ? void 0 : _api$typeAhead$action.findHandlerByTrigger(trigger);
if (!typeAheadHandler || !typeAheadHandler.id) {
return null;
}
return api === null || api === void 0 ? void 0 : (_api$typeAhead2 = api.typeAhead) === null || _api$typeAhead2 === void 0 ? void 0 : (_api$typeAhead2$actio = _api$typeAhead2.actions) === null || _api$typeAhead2$actio === void 0 ? void 0 : _api$typeAhead2$actio.openAtTransaction({
triggerHandler: typeAheadHandler,
inputMethod: INPUT_METHOD.KEYBOARD
});
};
export function createPlugin(dispatch, options, api) {
const allowInserting = !!options.allowInserting;
return new SafePlugin({
key: pluginKey,
state: {
init: () => ({
showInsertPanelAt: null,
allowInserting
}),
apply: (tr, state) => {
const meta = tr.getMeta(pluginKey);
if (meta && meta.showInsertPanelAt !== undefined) {
const newState = {
showInsertPanelAt: meta.showInsertPanelAt,
allowInserting
};
dispatch(pluginKey, newState);
return newState;
} else if (state.showInsertPanelAt) {
const newState = {
showInsertPanelAt: tr.mapping.map(state.showInsertPanelAt),
allowInserting
};
dispatch(pluginKey, newState);
return newState;
}
return state;
}
},
appendTransaction(transactions, oldState, newState) {
if (transactions.some(txn => txn.docChanged)) {
const didPlaceholderExistBeforeTxn = oldState.selection.$head.nodeAfter === newState.selection.$head.nodeAfter;
const adjacentNode = newState.selection.$head.nodeAfter;
const adjacentNodePos = newState.selection.$head.pos;
const placeholderNodeType = newState.schema.nodes.placeholder;
if (adjacentNode && adjacentNode.type === placeholderNodeType && didPlaceholderExistBeforeTxn) {
var _$newHead$nodeBefore;
const {
$head: $newHead
} = newState.selection;
const {
$head: $oldHead
} = oldState.selection;
// Check that cursor has moved forward in the document **and** that there is content before the cursor
const cursorMoved = $oldHead.pos < $newHead.pos;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const nodeBeforeHasContent = !isNodeEmpty($newHead.nodeBefore);
const nodeBeforeIsInline = (_$newHead$nodeBefore = $newHead.nodeBefore) === null || _$newHead$nodeBefore === void 0 ? void 0 : _$newHead$nodeBefore.type.isInline;
if (cursorMoved && (nodeBeforeHasContent || nodeBeforeIsInline)) {
const {
$from,
$to
} = NodeSelection.create(newState.doc, adjacentNodePos);
return newState.tr.deleteRange($from.pos, $to.pos);
}
}
}
// Handle Fake Text Cursor for Floating Toolbar
if (!pluginKey.getState(oldState).showInsertPanelAt && pluginKey.getState(newState).showInsertPanelAt) {
return newState.tr.setSelection(new FakeTextCursorSelection(newState.selection.$from));
}
if (pluginKey.getState(oldState).showInsertPanelAt && !pluginKey.getState(newState).showInsertPanelAt) {
if (newState.selection instanceof FakeTextCursorSelection) {
return newState.tr.setSelection(new TextSelection(newState.selection.$from));
}
}
return;
},
props: {
decorations: drawFakeTextCursor,
handleDOMEvents: {
beforeinput: (view, event) => {
const {
state
} = view;
if (event instanceof InputEvent && !event.isComposing && event.inputType === 'insertText' && isSelectionAtPlaceholder(view.state.selection)) {
event.stopPropagation();
event.preventDefault();
const startNodePosition = state.selection.from;
const content = event.data || '';
const tr = view.state.tr;
tr.delete(startNodePosition, startNodePosition + 1);
const openTypeAhead = getOpenTypeAhead(content, api);
if (openTypeAhead) {
openTypeAhead(tr);
} else {
tr.insertText(content);
}
view.dispatch(tr);
return true;
}
return false;
}
},
nodeViews: {
placeholder: (node, view, getPos) => new PlaceholderTextNodeView(node, view, getPos)
}
}
});
}
function ContentComponent({
editorView,
dependencyApi,
popupsMountPoint,
popupsBoundariesElement
}) {
const {
showInsertPanelAt
} = useSharedPluginStateWithSelector(dependencyApi, ['placeholderText'], states => {
var _states$placeholderTe;
return {
showInsertPanelAt: (_states$placeholderTe = states.placeholderTextState) === null || _states$placeholderTe === void 0 ? void 0 : _states$placeholderTe.showInsertPanelAt
};
});
const insertPlaceholderText = value => insertPlaceholderTextAtSelection(value)(editorView.state, editorView.dispatch);
const hidePlaceholderToolbar = () => hidePlaceholderFloatingToolbar(editorView.state, editorView.dispatch);
const getNodeFromPos = pos => editorView.domAtPos(pos).node;
const getFixedCoordinatesFromPos = pos => editorView.coordsAtPos(pos);
const setFocusInEditor = () => editorView.focus();
if (showInsertPanelAt) {
return /*#__PURE__*/React.createElement(PlaceholderFloatingToolbar
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
, {
editorViewDOM: editorView.dom,
popupsMountPoint: popupsMountPoint,
popupsBoundariesElement: popupsBoundariesElement,
getFixedCoordinatesFromPos: getFixedCoordinatesFromPos,
getNodeFromPos: getNodeFromPos,
hidePlaceholderFloatingToolbar: hidePlaceholderToolbar,
showInsertPanelAt: showInsertPanelAt,
insertPlaceholder: insertPlaceholderText,
setFocusInEditor: setFocusInEditor
});
}
return null;
}
const basePlaceholderTextPlugin = ({
api,
config: options
}) => ({
name: 'placeholderText',
nodes() {
return [{
name: 'placeholder',
node: placeholder
}];
},
pmPlugins() {
return [{
name: 'placeholderText',
plugin: ({
dispatch
}) => createPlugin(dispatch, options, api)
}];
},
actions: {
showPlaceholderFloatingToolbar
},
getSharedState(editorState) {
if (!editorState) {
return undefined;
}
const {
showInsertPanelAt,
allowInserting
} = pluginKey.getState(editorState) || {
showInsertPanelAt: null
};
return {
showInsertPanelAt,
allowInserting: !!allowInserting
};
},
contentComponent({
editorView,
popupsMountPoint,
popupsBoundariesElement
}) {
if (!editorView) {
return null;
}
return /*#__PURE__*/React.createElement(ContentComponent, {
editorView: editorView,
popupsMountPoint: popupsMountPoint,
popupsBoundariesElement: popupsBoundariesElement,
dependencyApi: api
});
}
});
const decorateWithPluginOptions = (plugin, options, api) => {
if (!options.allowInserting) {
return plugin;
}
plugin.pluginsOptions = {
quickInsert: ({
formatMessage
}) => [{
id: 'placeholderText',
title: formatMessage(messages.placeholderText),
description: formatMessage(messages.placeholderTextDescription),
priority: 1400,
keywords: ['placeholder'],
icon: () => /*#__PURE__*/React.createElement(TextIcon, {
label: ""
}),
action(insert, state) {
var _api$analytics;
const tr = state.tr;
tr.setMeta(pluginKey, {
showInsertPanelAt: tr.selection.anchor
});
const resolvedInputMethod = INPUT_METHOD.QUICK_INSERT;
api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
action: ACTION.INSERTED,
actionSubject: ACTION_SUBJECT.DOCUMENT,
actionSubjectId: ACTION_SUBJECT_ID.PLACEHOLDER_TEXT,
attributes: {
inputMethod: resolvedInputMethod
},
eventType: EVENT_TYPE.TRACK
})(tr);
return tr;
}
}]
};
return plugin;
};
const placeholderTextPlugin = ({
config: options = {},
api
}) => decorateWithPluginOptions(basePlaceholderTextPlugin({
config: options,
api
}), options, api);
export default placeholderTextPlugin;