@atlaskit/editor-plugin-tasks-and-decisions
Version:
Tasks and decisions plugin for @atlaskit/editor-core
342 lines (332 loc) • 16.6 kB
JavaScript
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();
}
}
}
};
}
});
}