UNPKG

@atlaskit/editor-plugin-placeholder-text

Version:

placeholder text plugin for @atlaskit/editor-core

264 lines (263 loc) 10.2 kB
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;