UNPKG

@atlaskit/editor-plugin-expand

Version:

Expand plugin for @atlaskit/editor-core

270 lines (264 loc) 10 kB
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead import uuid from 'uuid/v4'; import { SetAttrsStep } from '@atlaskit/adf-schema/steps'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD, MODE, PLATFORMS } from '@atlaskit/editor-common/analytics'; import { expandedState } from '@atlaskit/editor-common/expand'; import { GapCursorSelection, Side } from '@atlaskit/editor-common/selection'; import { expandClassNames } from '@atlaskit/editor-common/styles'; import { findExpand } from '@atlaskit/editor-common/transforms'; import { createWrapSelectionTransaction } from '@atlaskit/editor-common/utils'; import { Selection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findParentNodeOfType, safeInsert } from '@atlaskit/editor-prosemirror/utils'; import { findTable } from '@atlaskit/editor-tables/utils'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { isNestedInExpand } from '../utils'; // Creates either an expand or a nestedExpand node based on the current selection export const createExpandNode = (state, setExpandedState = true, addLocalId) => { const { expand, nestedExpand, paragraph } = state.schema.nodes; const isSelectionInTable = !!findTable(state.selection); const isSelectionInExpand = isNestedInExpand(state); const expandType = isSelectionInTable || isSelectionInExpand ? nestedExpand : expand; const expandNode = fg('platform_editor_adf_with_localid') ? expandType.createAndFill(addLocalId ? { // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead localId: uuid() } : {}, // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead paragraph.createAndFill(addLocalId ? { localId: uuid() } : {})) : expandType.createAndFill({}); if (setExpandedState) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expandedState.set(expandNode, true); } return expandNode; }; /** * When cleaning up platform_editor_adf_with_localid we can reuse this function * in insertExpandWithInputMethod. */ export const wrapSelectionAndSetExpandedState = (state, node) => { const tr = createWrapSelectionTransaction({ state, type: node.type, nodeAttributes: node.attrs }); const wrapperNode = findParentNodeOfType(node.type)(tr.selection); if (wrapperNode) { expandedState.set(wrapperNode.node, true); } return tr; }; export const insertExpandWithInputMethod = api => inputMethod => (state, dispatch) => { const expandNode = createExpandNode(state, false, !!(api !== null && api !== void 0 && api.localId)); if (!expandNode) { return false; } let tr; if (state.selection.empty) { tr = safeInsert(expandNode)(state.tr).scrollIntoView(); expandedState.set(expandNode, true); } else { tr = createWrapSelectionTransaction({ state, type: expandNode.type, ...(fg('platform_editor_adf_with_localid') && { nodeAttributes: expandNode.attrs }) }); const wrapperNode = findParentNodeOfType(expandNode.type)(tr.selection); if (wrapperNode) { expandedState.set(wrapperNode.node, true); } } const payload = { action: ACTION.INSERTED, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId: (expandNode === null || expandNode === void 0 ? void 0 : expandNode.type) === state.schema.nodes.expand ? ACTION_SUBJECT_ID.EXPAND : ACTION_SUBJECT_ID.NESTED_EXPAND, attributes: { inputMethod }, eventType: EVENT_TYPE.TRACK }; if (dispatch) { var _api$analytics, _api$analytics$action; api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.attachAnalyticsEvent(payload)(tr); dispatch(tr); } return true; }; export const insertExpand = api => (state, dispatch) => { return insertExpandWithInputMethod(api)(INPUT_METHOD.INSERT_MENU)(state, dispatch); }; export const deleteExpand = editorAnalyticsAPI => (state, dispatch) => { const expandNode = findExpand(state); if (!expandNode) { return false; } return deleteExpandAtPos(editorAnalyticsAPI)(expandNode.pos, expandNode.node)(state, dispatch); }; export const deleteExpandAtPos = editorAnalyticsAPI => (expandNodePos, expandNode) => (state, dispatch) => { if (!expandNode || isNaN(expandNodePos)) { return false; } const payload = { action: ACTION.DELETED, actionSubject: expandNode.type === state.schema.nodes.expand ? ACTION_SUBJECT.EXPAND : ACTION_SUBJECT.NESTED_EXPAND, attributes: { inputMethod: INPUT_METHOD.FLOATING_TB }, eventType: EVENT_TYPE.TRACK }; if (expandNode && dispatch) { const { tr } = state; tr.delete(expandNodePos, expandNodePos + expandNode.nodeSize); editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(payload)(tr); if (expandNode.type === state.schema.nodes.nestedExpand) { const resolvedPos = tr.doc.resolve(expandNodePos + 1); if (resolvedPos) { tr.setSelection(Selection.near(resolvedPos, -1)); } } dispatch(tr); } return true; }; // Used to clear any node or cell selection when expand title is focused export const setSelectionInsideExpand = expandPos => (_state, dispatch, editorView) => { if (editorView) { if (!editorView.hasFocus()) { editorView.focus(); } const sel = Selection.findFrom(editorView.state.doc.resolve(expandPos), 1, true); if (sel && dispatch) { dispatch(editorView.state.tr.setSelection(sel)); } return true; } return false; }; export const toggleExpandExpanded = ({ editorAnalyticsAPI, pos, node }) => (state, dispatch) => { if (node && dispatch) { var _expandedState$get; const { tr } = state; const expanded = (_expandedState$get = expandedState.get(node)) !== null && _expandedState$get !== void 0 ? _expandedState$get : false; const isExpandedNext = !expanded; expandedState.set(node, isExpandedNext); // If we're going to collapse the expand and our cursor is currently inside // Move to a right gap cursor, if the toolbar is interacted (or an API), // it will insert below rather than inside (which will be invisible). if (isExpandedNext === true) { tr.setSelection(new GapCursorSelection(tr.doc.resolve(pos + node.nodeSize), Side.RIGHT)); } // log when people open/close expands // TODO: ED-8523 - make platform/mode global attributes? const payload = { action: ACTION.TOGGLE_EXPAND, actionSubject: node.type === state.schema.nodes.expand ? ACTION_SUBJECT.EXPAND : ACTION_SUBJECT.NESTED_EXPAND, attributes: { platform: PLATFORMS.WEB, mode: MODE.EDITOR, expanded: isExpandedNext }, eventType: EVENT_TYPE.TRACK }; tr.setMeta('scrollIntoView', false); editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(payload)(tr); dispatch(tr); } return true; }; export const updateExpandTitle = ({ title, nodeType, pos }) => (state, dispatch) => { const node = state.doc.nodeAt(pos); if (node && node.type === nodeType && dispatch) { const { tr } = state; tr.step(new SetAttrsStep(pos, { ...node.attrs, title })); dispatch(tr); } return true; }; export const focusTitle = pos => (state, dispatch, editorView) => { if (editorView) { const dom = editorView.domAtPos(pos); const expandWrapper = dom.node.parentElement; if (expandWrapper) { setSelectionInsideExpand(pos)(state, dispatch, editorView); const input = expandWrapper.querySelector('input'); if (input) { input.focus(); return true; } } } return false; }; export const focusIcon = expand => (state, dispatch, editorView) => { if (!(expand instanceof HTMLElement)) { return false; } // TODO: ED-29205 - During platform_editor_vc90_transition_expand_icon cleanup, rename `iconContainer` to `iconButton`. const iconContainer = expValEquals('platform_editor_vc90_transition_expand_icon', 'isEnabled', true) ? expand.querySelector(`.${expandClassNames.iconButton}`) : expand.querySelector(`.${expandClassNames.iconContainer}`); if (iconContainer && iconContainer.focus) { const { tr } = state; const pos = state.selection.from; tr.setSelection(new TextSelection(tr.doc.resolve(pos))); if (dispatch) { dispatch(tr); } editorView === null || editorView === void 0 ? void 0 : editorView.dom.blur(); iconContainer.focus(); return true; } return false; }; export const toggleExpandWithMatch = selection => ({ tr }) => { const { expand, nestedExpand } = tr.doc.type.schema.nodes; // if match is inside a nested expand, open the nested expand const nestedExpandNode = findParentNodeOfType(nestedExpand)(selection); if (nestedExpandNode) { var _expandedState$get2; const expanded = (_expandedState$get2 = expandedState.get(nestedExpandNode.node)) !== null && _expandedState$get2 !== void 0 ? _expandedState$get2 : false; if (!expanded) { expandedState.set(nestedExpandNode.node, true); } } // if match is (also) inside an expand, open the expand const expandNode = findParentNodeOfType(expand)(selection); if (expandNode) { var _expandedState$get3; const expanded = (_expandedState$get3 = expandedState.get(expandNode.node)) !== null && _expandedState$get3 !== void 0 ? _expandedState$get3 : false; if (!expanded) { expandedState.set(expandNode.node, true); } } return tr; };