UNPKG

@atlaskit/editor-plugin-tasks-and-decisions

Version:

Tasks and decisions plugin for @atlaskit/editor-core

346 lines (336 loc) 14.9 kB
import { uuid } from '@atlaskit/adf-schema'; import { SetAttrsStep } from '@atlaskit/adf-schema/steps'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { createSelectionClickHandler, GapCursorSelection } from '@atlaskit/editor-common/selection'; import { getStepRange } from '@atlaskit/editor-common/utils'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure'; import { DecisionItemNodeView } from '../nodeviews/DecisionItemNodeView'; import { taskView } from '../nodeviews/task-node-view'; import { focusTaskDecision, openRequestEditPopup, setProvider } from './actions'; import { focusCheckbox, focusCheckboxAndUpdateSelection, getTaskItemDataAtPos, getTaskItemDataToFocus, isInsideTask, removeCheckboxFocus } from './helpers'; import { stateKey } from './plugin-key'; import { taskItemOnChange } from './taskItemOnChange'; import { applyTaskListNormalisationFixes } from './transforms'; import { ACTIONS } from './types'; import { tempTransformSliceToRemoveBlockTaskItem } from './utils/paste'; function nodesBetweenChanged(tr, f, startPos) { const stepRange = getStepRange(tr); if (!stepRange) { return; } tr.doc.nodesBetween(stepRange.from, stepRange.to, f, startPos); } // eslint-disable-next-line jsdoc/require-jsdoc export function createPlugin(portalProviderAPI, eventDispatcher, dispatch, api, getIntl, useLongPressSelection = false, hasEditPermission, hasRequestedEditPermission, requestToEditContent, taskPlaceholder) { return new SafePlugin({ props: { nodeViews: { taskItem: taskView(api, getIntl(), taskPlaceholder), decisionItem: node => { return new DecisionItemNodeView(node, getIntl()); }, ...(expValEquals('platform_editor_blocktaskitem_node_tenantid', 'isEnabled', true) ? { blockTaskItem: taskView(api, getIntl(), taskPlaceholder) } : {}) }, decorations(state) { const pluginState = stateKey.getState(state); if (pluginState !== null && pluginState !== void 0 && pluginState.decorations) { return pluginState.decorations; } return DecorationSet.empty; }, handleTextInput(view, from, to, text) { // When a decision item is selected and the user starts typing, the entire node // should be replaced with what was just typed. This custom text input handler // is needed to implement that behaviour. const { state, dispatch } = view; const { tr } = state; if (state.selection instanceof NodeSelection && state.selection.node.type === view.state.schema.nodes.decisionItem) { state.selection.replace(tr); tr.insertText(text); if (dispatch) { dispatch(tr); } return true; } return false; }, handleClickOn: createSelectionClickHandler(['decisionItem', 'taskItem'], target => target.hasAttribute('data-decision-wrapper') || target.getAttribute('aria-label') === 'Decision', { useLongPressSelection }), handleDOMEvents: { // When the page is lazy loaded and task item is not yet available this allows // our toDOM implementation to toggle the node state change: taskItemOnChange }, handleKeyDown: (view, event) => { const { state } = view; const { selection, schema } = state; const { $from, $to } = selection; const parentOffset = $from.parentOffset; const isInTaskItem = expValEquals('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) ? isInsideTask(state) : $from.node().type === schema.nodes.taskItem; const focusedTaskItemLocalId = stateKey.getState(state).focusedTaskItemLocalId; const currentTaskItemData = getTaskItemDataAtPos(view); const currentTaskItemFocused = focusedTaskItemLocalId === (currentTaskItemData === null || currentTaskItemData === void 0 ? void 0 : currentTaskItemData.localId); // if task item checkbox not focused and arrow key is not pressed // then we don't want to handle event. if (!['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft'].includes(event.key)) { return false; } // We want to handle arrow up, down and left key only // when selection is inside task item and no text is selected. if (['ArrowUp', 'ArrowDown', 'ArrowLeft'].includes(event.key) && (!isInTaskItem || $from.pos !== $to.pos)) { return false; } // Arrow keys are pressed and shift, ctrl or meta is pressed as well. // along with arrow keys and task item checkbox is focused // then first move focus to view and proceed with default event handling. if (event.shiftKey || event.ctrlKey || event.metaKey) { currentTaskItemFocused && removeCheckboxFocus(view); return false; } // task item checkbox is already focused if (focusedTaskItemLocalId) { if (event.key === 'ArrowLeft') { // Move focus to view and proceed with default keyboard handler. // Which will move cursor to previous position. removeCheckboxFocus(view); return false; } if (event.key === 'ArrowRight') { // Move focus to view and DON'T proceed with default handler. // We have assumed that selection is already before first character of task item. removeCheckboxFocus(view); return true; } if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { const taskItemData = getTaskItemDataToFocus(view, event.key === 'ArrowUp' ? 'previous' : 'next'); if (taskItemData) { focusCheckboxAndUpdateSelection(view, taskItemData); return true; } else { // If any how checkbox input not found, then move focus to view // and proceed with default keyboard handler. removeCheckboxFocus(view); return false; } } } // If left arrow key is pressed and cursor is at first position in task-item // then focus checkbox and DON'T proceed with default keyboard handler if (event.key === 'ArrowLeft' && parentOffset === 0) { // here we are not using focusCheckboxAndUpdateSelection() method // because it is working incorretly when we are placing is inside the nested items focusCheckbox(view, currentTaskItemData); return true; } if (event.key === 'ArrowRight') { var _$from$nodeAfter; // If gap cursor is just before task list then focus first task item in list. if (selection instanceof GapCursorSelection && selection.side === 'left' && ((_$from$nodeAfter = $from.nodeAfter) === null || _$from$nodeAfter === void 0 ? void 0 : _$from$nodeAfter.type) === schema.nodes.taskList) { const taskList = $from.nodeAfter; const firstTaskItemNode = taskList.child(0); const taskItemPos = $from.pos + 1; focusCheckboxAndUpdateSelection(view, { pos: taskItemPos, localId: firstTaskItemNode.attrs.localId }); return true; } // if cursor is at then end of task item text then focus next task item checkbox else if (isInTaskItem && $from.node().content.size === parentOffset) { const nextTaskItemData = getTaskItemDataToFocus(view, 'next'); if (nextTaskItemData) { focusCheckboxAndUpdateSelection(view, nextTaskItemData); return true; } } else { return false; } } }, transformPasted: (slice, view) => { var _view$state, _view$state$schema, _view$state$schema$no; if (Boolean(view === null || view === void 0 ? void 0 : (_view$state = view.state) === null || _view$state === void 0 ? void 0 : (_view$state$schema = _view$state.schema) === null || _view$state$schema === void 0 ? void 0 : (_view$state$schema$no = _view$state$schema.nodes) === null || _view$state$schema$no === void 0 ? void 0 : _view$state$schema$no.blockTaskItem)) { slice = tempTransformSliceToRemoveBlockTaskItem(slice, view); } return slice; } }, state: { init() { return { insideTaskDecisionItem: false, hasEditPermission, hasRequestedEditPermission, requestToEditContent, focusedTaskItemLocalId: null, taskDecisionProvider: undefined, decorations: DecorationSet.empty }; }, apply(tr, pluginState) { const metaData = tr.getMeta(stateKey); const { action, data } = metaData !== null && metaData !== void 0 ? metaData : { action: null, data: null }; let newPluginState = pluginState; // Actions switch (action) { case ACTIONS.FOCUS_BY_LOCALID: newPluginState = focusTaskDecision(newPluginState, { action: ACTIONS.FOCUS_BY_LOCALID, data }); break; case ACTIONS.SET_PROVIDER: newPluginState = setProvider(newPluginState, { action: ACTIONS.SET_PROVIDER, data }); break; case ACTIONS.OPEN_REQUEST_TO_EDIT_POPUP: newPluginState = openRequestEditPopup(newPluginState, { action: ACTIONS.OPEN_REQUEST_TO_EDIT_POPUP, data }); break; } // Commands if (metaData && 'hasEditPermission' in metaData) { newPluginState = { ...newPluginState, hasEditPermission: metaData.hasEditPermission }; } if (metaData && 'hasRequestedEditPermission' in metaData) { newPluginState = { ...newPluginState, hasRequestedEditPermission: metaData.hasRequestedEditPermission }; } const taskItemInfo = tr.getMeta('taskItemInfo'); let decorations = DecorationSet.empty; if (taskItemInfo) { decorations = DecorationSet.create(tr.doc, [Decoration.inline(taskItemInfo.from, taskItemInfo.to, {}, { dataTaskNodeCheckState: taskItemInfo.checkState })]); } else { var _newPluginState$decor; decorations = (_newPluginState$decor = newPluginState.decorations) === null || _newPluginState$decor === void 0 ? void 0 : _newPluginState$decor.map(tr.mapping, tr.doc); } const newState = { ...newPluginState, decorations }; dispatch(stateKey, newState); return newState; } }, key: stateKey, /* * After each transaction, we search through the document for any decisionList/Item & taskList/Item nodes * that do not have the localId attribute set and generate a random UUID to use. This is to replace a previous * Prosemirror capability where node attributes could be generated dynamically. * See https://discuss.prosemirror.net/t/release-0-23-0-possibly-to-be-1-0-0/959/17 for a discussion of this approach. * * Note: we currently do not handle the edge case where two nodes may have the same localId */ appendTransaction: (transactions, _oldState, newState) => { // Normalise taskList structure first — runs on its own transaction (without addToHistory: false) // so it is included in history and correctly undone with the triggering operation. if (expValEqualsNoExposure('platform_editor_flexible_list_schema', 'isEnabled', true) && transactions.some(t => t.docChanged)) { const normTr = applyTaskListNormalisationFixes({ tr: newState.tr, transactions, doc: newState.doc, schema: newState.schema }); if (normTr.docChanged) { // Return the normalisation transaction — the next appendTransaction call will // assign any missing localIds to newly inserted nodes via the localId path below. return normTr; } } // Assign unique localIds to any new nodes that don't have one. // Runs with addToHistory: false so localId assignment is not part of the undo history. const tr = newState.tr; let modified = false; transactions.forEach(transaction => { if (!transaction.docChanged) { return; } // Adds a unique id to a node nodesBetweenChanged(transaction, (node, pos) => { const { decisionList, decisionItem, taskList, taskItem, blockTaskItem } = newState.schema.nodes; if (!!node.type && (node.type === decisionList || node.type === decisionItem || node.type === taskList || node.type === taskItem || blockTaskItem && node.type === blockTaskItem)) { const { localId, ...rest } = node.attrs; if (localId === undefined || localId === null || localId === '') { tr.step(new SetAttrsStep(pos, { localId: uuid.generate(), ...rest })); modified = true; } } }); }); if (modified) { return tr.setMeta('addToHistory', false); } return; }, view: () => { return { update: (view, prevState) => { const pluginState = stateKey.getState(view.state); const prevPluginState = stateKey.getState(prevState); if (pluginState.focusedTaskItemLocalId === prevPluginState.focusedTaskItemLocalId) { return; } const taskItem = getTaskItemDataAtPos(view); if (!taskItem) { return; } if (pluginState.focusedTaskItemLocalId === taskItem.localId) { const taskElement = view.nodeDOM(taskItem.pos); if (taskElement instanceof HTMLElement) { var _taskElement$querySel; taskElement === null || taskElement === void 0 ? void 0 : (_taskElement$querySel = taskElement.querySelector('input')) === null || _taskElement$querySel === void 0 ? void 0 : _taskElement$querySel.focus(); } } } }; } }); }