@atlaskit/editor-plugin-mentions
Version:
Mentions plugin for @atlaskit/editor-core
183 lines (182 loc) • 9.14 kB
JavaScript
import React from 'react';
import { bind } from 'bind-event-listener';
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
import uuid from 'uuid/v4';
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
import { findChildrenByAttr } from '@atlaskit/editor-prosemirror/utils';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { navigateToTeamsApp } from '@atlaskit/teams-app-config/navigation';
import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
import { getAgentMentionParentContext } from '../pm-plugins/agent-mention-context';
import { isAgentMentionType, ProfileCardComponent } from '../ui/ProfileCardComponent';
export const profileCardRenderer = ({
dom,
options,
portalProviderAPI,
node,
api,
editorView
}) => {
// Keep a mutable reference to the latest node so the click handler always
// reads up-to-date attrs
let currentNode = node;
let renderingProfileCard = false;
let navigatingToProfile = false;
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
const key = uuid();
let cleanupSelection;
const removeProfileCard = () => {
var _cleanupSelection;
if (dom instanceof HTMLElement) {
dom.setAttribute('aria-expanded', 'false');
}
portalProviderAPI.remove(key);
renderingProfileCard = false;
(_cleanupSelection = cleanupSelection) === null || _cleanupSelection === void 0 ? void 0 : _cleanupSelection();
};
const renderProfileCardPopup = renderProfileCard => {
var _api$selection;
if (!(dom instanceof HTMLElement) || renderingProfileCard) {
return;
}
const referenceElement = dom;
referenceElement.setAttribute('aria-expanded', 'true');
renderingProfileCard = true;
portalProviderAPI.render(() => renderProfileCard(referenceElement), referenceElement, key);
cleanupSelection = api === null || api === void 0 ? void 0 : (_api$selection = api.selection) === null || _api$selection === void 0 ? void 0 : _api$selection.sharedState.onChange(({
nextSharedState
}) => {
const selection = nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.selection;
if (selection instanceof NodeSelection ? expVal('platform_editor_reduced_agent_profile_cards', 'isEnabled', false) ? selection.node.sameMarkup(currentNode) : selection.node === node : false) {
return;
}
removeProfileCard === null || removeProfileCard === void 0 ? void 0 : removeProfileCard();
});
};
const renderEditorProfileCard = () => {
const isReducedProfileCards = expVal('platform_editor_reduced_agent_profile_cards', 'isEnabled', false);
const clickedNode = isReducedProfileCards ? currentNode : node;
const activeMention = isReducedProfileCards ? ((_ref, _ref$textContent, _currentNode, _currentNode2, _currentNode2$attrs, _currentNode3, _currentNode3$attrs) => {
// Avatar mentions keep their resolved display name in the nested text span.
// Control mentions preserve the legacy attrs-first fallback.
const mentionTextElement = dom instanceof HTMLElement ? dom.querySelector('.editor-mention-text') : undefined;
const primitiveText = (_ref = mentionTextElement !== null && mentionTextElement !== void 0 ? mentionTextElement : dom instanceof HTMLElement ? dom.querySelector('.editor-mention-primitive') : undefined) === null || _ref === void 0 ? void 0 : (_ref$textContent = _ref.textContent) === null || _ref$textContent === void 0 ? void 0 : _ref$textContent.trim();
const resolvedText = primitiveText && !primitiveText.startsWith('@') ? `@${primitiveText}` : primitiveText;
return {
attrs: {
...((_currentNode = currentNode) === null || _currentNode === void 0 ? void 0 : _currentNode.attrs),
text: mentionTextElement ? resolvedText || ((_currentNode2 = currentNode) === null || _currentNode2 === void 0 ? void 0 : (_currentNode2$attrs = _currentNode2.attrs) === null || _currentNode2$attrs === void 0 ? void 0 : _currentNode2$attrs.text) || undefined : ((_currentNode3 = currentNode) === null || _currentNode3 === void 0 ? void 0 : (_currentNode3$attrs = _currentNode3.attrs) === null || _currentNode3$attrs === void 0 ? void 0 : _currentNode3$attrs.text) || primitiveText || undefined
}
};
})() : {
attrs: node.attrs
};
// Build agent mention context at click time by finding the parent block of this mention
// in the live document.
let agentMentionContext;
if (options !== null && options !== void 0 && options.onAgentMentionChatClick && editorView && fg('platform_editor_agent_mentions_drop_one_fixes')) {
var _clickedNode$attrs;
const localId = (_clickedNode$attrs = clickedNode.attrs) === null || _clickedNode$attrs === void 0 ? void 0 : _clickedNode$attrs.localId;
if (localId) {
const found = findChildrenByAttr(editorView.state.doc, attrs => (attrs === null || attrs === void 0 ? void 0 : attrs.localId) === localId)[0];
if (found) {
const parentBlock = editorView.state.doc.resolve(found.pos).parent;
agentMentionContext = getAgentMentionParentContext(parentBlock, localId);
}
}
}
renderProfileCardPopup(referenceElement => /*#__PURE__*/React.createElement(ProfileCardComponent, {
activeMention: activeMention,
profilecardProvider: options === null || options === void 0 ? void 0 : options.profilecardProvider,
onAgentMentionChatClick: options !== null && options !== void 0 && options.onAgentMentionChatClick && fg('platform_editor_agent_mentions_drop_one_fixes') ? agentId => {
var _options$onAgentMenti;
return (_options$onAgentMenti = options.onAgentMentionChatClick) === null || _options$onAgentMenti === void 0 ? void 0 : _options$onAgentMenti.call(options, agentId, agentMentionContext);
} : undefined,
dom: referenceElement,
closeComponent: removeProfileCard
}));
};
const renderUserProfileCard = ({
userId,
cloudId,
renderUserMentionCard
}) => {
if (!renderUserMentionCard) {
return false;
}
renderProfileCardPopup(referenceElement => /*#__PURE__*/React.createElement(React.Fragment, null, renderUserMentionCard({
userId,
cloudId,
children: null,
referenceElement
})));
return true;
};
const navigateToProfile = (userId, cloudId) => {
if (navigatingToProfile) {
return;
}
navigatingToProfile = true;
const {
href,
target
} = navigateToTeamsApp({
type: 'USER',
payload: {
userId
},
cloudId
});
window.open(href, target, 'noopener,noreferrer');
};
const renderDefaultProfileCard = (userId, provider) => {
if (renderUserProfileCard({
userId,
cloudId: provider.cloudId,
renderUserMentionCard: provider.renderUserMentionCard
})) {
return;
}
navigateToProfile(userId, provider.cloudId);
};
const listenerCleanup = bind(dom, {
type: 'click',
listener: () => {
if (fg('people-teams_migrate-user-profile-card') || isExperimentEnabled('pt_user_profile_card_migration_exp')) {
var _currentNode$attrs, _node$attrs, _options$profilecardP;
const userId = expVal('platform_editor_reduced_agent_profile_cards', 'isEnabled', false) ? (_currentNode$attrs = currentNode.attrs) === null || _currentNode$attrs === void 0 ? void 0 : _currentNode$attrs.id : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.id;
if (!userId) {
return;
}
navigatingToProfile = false;
options === null || options === void 0 ? void 0 : (_options$profilecardP = options.profilecardProvider) === null || _options$profilecardP === void 0 ? void 0 : _options$profilecardP.then(provider => {
var _currentNode$attrs2, _node$attrs2;
if (!expVal('platform_editor_agent_mentions', 'isEnabled', false)) {
renderDefaultProfileCard(userId, provider);
return;
}
if (isAgentMentionType(expVal('platform_editor_reduced_agent_profile_cards', 'isEnabled', false) ? (_currentNode$attrs2 = currentNode.attrs) === null || _currentNode$attrs2 === void 0 ? void 0 : _currentNode$attrs2.userType : (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.userType)) {
renderEditorProfileCard();
} else {
renderDefaultProfileCard(userId, provider);
}
});
return;
}
if (options !== null && options !== void 0 && options.profilecardProvider) {
renderEditorProfileCard();
}
}
});
return {
destroyProfileCard: () => {
listenerCleanup();
removeProfileCard === null || removeProfileCard === void 0 ? void 0 : removeProfileCard();
},
removeProfileCard,
updateNode: nextNode => {
currentNode = nextNode;
}
};
};