UNPKG

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

Version:

Tasks and decisions plugin for @atlaskit/editor-core

342 lines (332 loc) 16.6 kB
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; var _excluded = ["localId"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } 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) { var 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) { var useLongPressSelection = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false; var hasEditPermission = arguments.length > 6 ? arguments[6] : undefined; var hasRequestedEditPermission = arguments.length > 7 ? arguments[7] : undefined; var requestToEditContent = arguments.length > 8 ? arguments[8] : undefined; var taskPlaceholder = arguments.length > 9 ? arguments[9] : undefined; return new SafePlugin({ props: { nodeViews: _objectSpread({ taskItem: taskView(api, getIntl(), taskPlaceholder), decisionItem: function (node) { return new DecisionItemNodeView(node, getIntl()); } }, expValEquals('platform_editor_blocktaskitem_node_tenantid', 'isEnabled', true) ? { blockTaskItem: taskView(api, getIntl(), taskPlaceholder) } : {}), decorations: function decorations(state) { var pluginState = stateKey.getState(state); if (pluginState !== null && pluginState !== void 0 && pluginState.decorations) { return pluginState.decorations; } return DecorationSet.empty; }, handleTextInput: function 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. var state = view.state, dispatch = view.dispatch; var tr = state.tr; 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'], function (target) { return target.hasAttribute('data-decision-wrapper') || target.getAttribute('aria-label') === 'Decision'; }, { useLongPressSelection: 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: function handleKeyDown(view, event) { var state = view.state; var selection = state.selection, schema = state.schema; var $from = selection.$from, $to = selection.$to; var parentOffset = $from.parentOffset; var isInTaskItem = expValEquals('platform_editor_blocktaskitem_patch_1', 'isEnabled', true) ? isInsideTask(state) : $from.node().type === schema.nodes.taskItem; var focusedTaskItemLocalId = stateKey.getState(state).focusedTaskItemLocalId; var currentTaskItemData = getTaskItemDataAtPos(view); var 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') { var 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) { var taskList = $from.nodeAfter; var firstTaskItemNode = taskList.child(0); var 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) { var nextTaskItemData = getTaskItemDataToFocus(view, 'next'); if (nextTaskItemData) { focusCheckboxAndUpdateSelection(view, nextTaskItemData); return true; } } else { return false; } } }, transformPasted: function transformPasted(slice, view) { var _view$state; if (Boolean(view === null || view === void 0 || (_view$state = view.state) === null || _view$state === void 0 || (_view$state = _view$state.schema) === null || _view$state === void 0 || (_view$state = _view$state.nodes) === null || _view$state === void 0 ? void 0 : _view$state.blockTaskItem)) { slice = tempTransformSliceToRemoveBlockTaskItem(slice, view); } return slice; } }, state: { init: function init() { return { insideTaskDecisionItem: false, hasEditPermission: hasEditPermission, hasRequestedEditPermission: hasRequestedEditPermission, requestToEditContent: requestToEditContent, focusedTaskItemLocalId: null, taskDecisionProvider: undefined, decorations: DecorationSet.empty }; }, apply: function apply(tr, pluginState) { var metaData = tr.getMeta(stateKey); var _ref = metaData !== null && metaData !== void 0 ? metaData : { action: null, data: null }, action = _ref.action, data = _ref.data; var newPluginState = pluginState; // Actions switch (action) { case ACTIONS.FOCUS_BY_LOCALID: newPluginState = focusTaskDecision(newPluginState, { action: ACTIONS.FOCUS_BY_LOCALID, data: data }); break; case ACTIONS.SET_PROVIDER: newPluginState = setProvider(newPluginState, { action: ACTIONS.SET_PROVIDER, data: data }); break; case ACTIONS.OPEN_REQUEST_TO_EDIT_POPUP: newPluginState = openRequestEditPopup(newPluginState, { action: ACTIONS.OPEN_REQUEST_TO_EDIT_POPUP, data: data }); break; } // Commands if (metaData && 'hasEditPermission' in metaData) { newPluginState = _objectSpread(_objectSpread({}, newPluginState), {}, { hasEditPermission: metaData.hasEditPermission }); } if (metaData && 'hasRequestedEditPermission' in metaData) { newPluginState = _objectSpread(_objectSpread({}, newPluginState), {}, { hasRequestedEditPermission: metaData.hasRequestedEditPermission }); } var taskItemInfo = tr.getMeta('taskItemInfo'); var 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); } var newState = _objectSpread(_objectSpread({}, newPluginState), {}, { decorations: 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: function 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(function (t) { return t.docChanged; })) { var normTr = applyTaskListNormalisationFixes({ tr: newState.tr, transactions: 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. var tr = newState.tr; var modified = false; transactions.forEach(function (transaction) { if (!transaction.docChanged) { return; } // Adds a unique id to a node nodesBetweenChanged(transaction, function (node, pos) { var _newState$schema$node = newState.schema.nodes, decisionList = _newState$schema$node.decisionList, decisionItem = _newState$schema$node.decisionItem, taskList = _newState$schema$node.taskList, taskItem = _newState$schema$node.taskItem, blockTaskItem = _newState$schema$node.blockTaskItem; if (!!node.type && (node.type === decisionList || node.type === decisionItem || node.type === taskList || node.type === taskItem || blockTaskItem && node.type === blockTaskItem)) { var _node$attrs = node.attrs, localId = _node$attrs.localId, rest = _objectWithoutProperties(_node$attrs, _excluded); if (localId === undefined || localId === null || localId === '') { tr.step(new SetAttrsStep(pos, _objectSpread({ localId: uuid.generate() }, rest))); modified = true; } } }); }); if (modified) { return tr.setMeta('addToHistory', false); } return; }, view: function view() { return { update: function update(view, prevState) { var pluginState = stateKey.getState(view.state); var prevPluginState = stateKey.getState(prevState); if (pluginState.focusedTaskItemLocalId === prevPluginState.focusedTaskItemLocalId) { return; } var taskItem = getTaskItemDataAtPos(view); if (!taskItem) { return; } if (pluginState.focusedTaskItemLocalId === taskItem.localId) { var taskElement = view.nodeDOM(taskItem.pos); if (taskElement instanceof HTMLElement) { var _taskElement$querySel; taskElement === null || taskElement === void 0 || (_taskElement$querySel = taskElement.querySelector('input')) === null || _taskElement$querySel === void 0 || _taskElement$querySel.focus(); } } } }; } }); }