@atlaskit/editor-plugin-expand
Version:
Expand plugin for @atlaskit/editor-core
270 lines (264 loc) • 10 kB
JavaScript
// 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;
};