UNPKG

@atlaskit/editor-plugin-mentions

Version:

Mentions plugin for @atlaskit/editor-core

487 lines (482 loc) 26.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.MentionNodeView = void 0; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _browser = require("@atlaskit/editor-common/browser"); var _whitespace = require("@atlaskit/editor-common/whitespace"); var _model = require("@atlaskit/editor-prosemirror/model"); var _constants = require("@atlaskit/mention/constants"); var _resource = require("@atlaskit/mention/resource"); var _types = require("@atlaskit/mention/types"); var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled"); var _fg = require("@atlaskit/platform-feature-flags/fg"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); var _expVal = require("@atlaskit/tmp-editor-statsig/expVal"); var _disabledTooltipRenderer = require("./disabledTooltipRenderer"); var _mentionAvatarRenderer = require("./mentionAvatarRenderer"); var _profileCardRenderer2 = require("./profileCardRenderer"); 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) { (0, _defineProperty2.default)(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; } // oxlint-disable-next-line import/no-duplicates var primitiveClassName = 'editor-mention-primitive'; var primitiveWithAvatarClassName = 'editor-mention-primitive-with-avatar'; var avatarContainerClassName = 'editor-mention-avatar'; var mentionTextClassName = 'editor-mention-text'; var genericMentionIds = ['HipChat', 'all', 'here']; var unknownMentionText = "@".concat(_constants.UNKNOWN_USER_ID); // eslint-disable-next-line require-unicode-regexp var AT_PREFIX_REGEX = /^@/; var getAccessibilityLabelFromName = function getAccessibilityLabelFromName(name) { return name.replace(AT_PREFIX_REGEX, ''); }; // Workaround: AI-rewritten pages may store the raw AAID (e.g. '@712020:uuid') in attrs.text for // APP/AGENT mentions instead of the display name. Detect this so we can fall through to provider // resolution. Pattern: '@digits:uuid' — distinct from plain-UUID human AAIDs (no numeric prefix). var AAID_MENTION_TEXT_PATTERN = // eslint-disable-next-line require-unicode-regexp /^@\d+:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** Returns true if `text` looks like a raw AAID rather than a display name. */ var isMentionTextAnAaid = function isMentionTextAnAaid(text) { return AAID_MENTION_TEXT_PATTERN.test(text); }; var isAgentMentionsExperimentEnabled = function isAgentMentionsExperimentEnabled() { return (0, _expVal.expVal)('platform_editor_agent_mentions', 'isEnabled', false); }; /** * Returns true if the node is an APP/AGENT mention whose stored text is a raw * AAID (e.g. '@712020:uuid') rather than a resolved display name. * All AAID-workaround guards should go through this helper so the checks stay * consistent and can't drift apart. */ var isAgentAaidText = function isAgentAaidText(node, isAgentMentionsEnabled) { return isAgentMentionsEnabled && (node.attrs.userType === 'APP' || node.attrs.userType === 'AGENT') && !!node.attrs.text && isMentionTextAnAaid(node.attrs.text); }; var toDOM = function toDOM(node, hasAvatarSlot) { // packages/elements/mention/src/components/Mention/index.tsx var mentionAttrs = { contenteditable: 'false', 'data-access-level': node.attrs.accessLevel, 'data-mention-id': node.attrs.id, 'data-prosemirror-content-type': 'node', 'data-prosemirror-node-inline': 'true', 'data-prosemirror-node-name': 'mention', 'data-prosemirror-node-view-type': 'vanilla', class: 'mentionView-content-wrap inlineNodeView' }; if ((0, _fg.fg)('platform_editor_adf_with_localid')) { mentionAttrs = _objectSpread(_objectSpread({}, mentionAttrs), {}, { 'data-local-id': node.attrs.localId }); } var isAgentMentionsEnabled = isAgentMentionsExperimentEnabled(); if (isAgentMentionsEnabled && node.attrs.userType) { mentionAttrs = _objectSpread(_objectSpread({}, mentionAttrs), {}, { 'data-user-type': node.attrs.userType }); } var browser = (0, _browser.getBrowserInfo)(); var hasAgentAaidText = isAgentAaidText(node, isAgentMentionsEnabled); var mentionText = node.attrs.text && !hasAgentAaidText ? node.attrs.text : '@…'; var visibleMentionText = hasAvatarSlot && mentionText.startsWith('@') ? mentionText.slice(1) : mentionText; var mentionContentSpec = hasAvatarSlot ? ['span', { spellcheck: 'false', class: "".concat(primitiveClassName, " ").concat(primitiveWithAvatarClassName) }, ['span', { class: avatarContainerClassName, 'aria-hidden': 'true' }], ['span', { class: mentionTextClassName }, visibleMentionText]] : ['span', { spellcheck: 'false', class: primitiveClassName }, mentionText]; return ['span', mentionAttrs, ['span', { class: 'zeroWidthSpaceContainer' }, ['span', { class: 'inlineNodeViewAddZeroWidthSpace' }, _whitespace.ZERO_WIDTH_SPACE]], mentionContentSpec, browser.android ? ['span', { class: 'zeroWidthSpaceContainer', contenteditable: 'false' }, ['span', { class: 'inlineNodeViewAddZeroWidthSpace' }, _whitespace.ZERO_WIDTH_SPACE]] : ['span', { class: 'inlineNodeViewAddZeroWidthSpace' }, _whitespace.ZERO_WIDTH_SPACE]]; }; var processName = function processName(name) { return name.status === _resource.MentionNameStatus.OK ? "@".concat(name.name || '') : unknownMentionText; }; var handleProviderName = /*#__PURE__*/function () { var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(mentionProvider, node, isAgentMentionsEnabled) { var textIsAaid, nameDetail, resolvedNameDetail; return _regenerator.default.wrap(function (_context) { while (1) switch (_context.prev = _context.next) { case 0: // Also resolve when text is a raw AAID — provider will return the real display name. textIsAaid = isAgentAaidText(node, isAgentMentionsEnabled); if (!((0, _resource.isResolvingMentionProvider)(mentionProvider) && node.attrs.id && (!node.attrs.text || textIsAaid))) { _context.next = 2; break; } nameDetail = mentionProvider === null || mentionProvider === void 0 ? void 0 : mentionProvider.resolveMentionName(node.attrs.id); _context.next = 1; return nameDetail; case 1: resolvedNameDetail = _context.sent; return _context.abrupt("return", processName(resolvedNameDetail)); case 2: case "end": return _context.stop(); } }, _callee); })); return function handleProviderName(_x, _x2, _x3) { return _ref.apply(this, arguments); }; }(); var getNewState = function getNewState(isHighlighted, isRestricted, isDisabled) { if (isDisabled) { return 'disabled'; } if (isHighlighted) { return 'self'; } if (isRestricted) { return 'restricted'; } return 'default'; }; var MentionNodeView = exports.MentionNodeView = /*#__PURE__*/function () { function MentionNodeView(node, config) { var _this$domElement$quer, _api$mention$sharedSt, _this = this; (0, _classCallCheck2.default)(this, MentionNodeView); (0, _defineProperty2.default)(this, "hasAvatarSlot", false); (0, _defineProperty2.default)(this, "isDestroyed", false); var options = config.options, api = config.api, portalProviderAPI = config.portalProviderAPI, editorView = config.editorView; this.hasAvatarSlot = Boolean(options === null || options === void 0 ? void 0 : options.mentionNodeDataProvider) && node.attrs.userType !== 'SPECIAL' && !genericMentionIds.includes(node.attrs.id) && (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_mention_node_avatar'); var _DOMSerializer$render = _model.DOMSerializer.renderSpec(document, toDOM(node, this.hasAvatarSlot)), dom = _DOMSerializer$render.dom, contentDOM = _DOMSerializer$render.contentDOM; this.dom = dom; this.contentDOM = contentDOM; this.config = config; this.node = node; this.domElement = dom instanceof HTMLElement ? dom : undefined; this.mentionPrimitiveElement = this.domElement ? (_this$domElement$quer = this.domElement.querySelector(".".concat(primitiveClassName))) !== null && _this$domElement$quer !== void 0 ? _this$domElement$quer : undefined : undefined; this.mentionTextElement = this.mentionPrimitiveElement; if (this.hasAvatarSlot) { var _this$domElement$quer2, _this$domElement, _this$domElement2; this.mentionTextElement = (_this$domElement$quer2 = (_this$domElement = this.domElement) === null || _this$domElement === void 0 ? void 0 : _this$domElement.querySelector(".".concat(mentionTextClassName))) !== null && _this$domElement$quer2 !== void 0 ? _this$domElement$quer2 : this.mentionPrimitiveElement; var avatarContainer = (_this$domElement2 = this.domElement) === null || _this$domElement2 === void 0 ? void 0 : _this$domElement2.querySelector(".".concat(avatarContainerClassName)); if (avatarContainer) { this.mentionAvatar = (0, _mentionAvatarRenderer.mentionAvatarRenderer)({ container: avatarContainer }); this.resolveMentionAvatar(options === null || options === void 0 ? void 0 : options.mentionNodeDataProvider); } } var _ref2 = (_api$mention$sharedSt = api === null || api === void 0 ? void 0 : api.mention.sharedState.currentState()) !== null && _api$mention$sharedSt !== void 0 ? _api$mention$sharedSt : {}, mentionProvider = _ref2.mentionProvider; this.updateState(mentionProvider); this.subscribeToProviderDisabledStateChanges(mentionProvider); this.cleanup = api === null || api === void 0 ? void 0 : api.mention.sharedState.onChange(function (_ref3) { var nextSharedState = _ref3.nextSharedState; _this.updateState(nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.mentionProvider); _this.subscribeToProviderDisabledStateChanges(nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.mentionProvider); }); var _profileCardRenderer = (0, _profileCardRenderer2.profileCardRenderer)({ dom: dom, options: options, portalProviderAPI: portalProviderAPI, node: node, api: api, editorView: editorView }), destroyProfileCard = _profileCardRenderer.destroyProfileCard, removeProfileCard = _profileCardRenderer.removeProfileCard, updateNode = _profileCardRenderer.updateNode; // Accessibility attributes - based on `packages/people-and-teams/profilecard/src/components/User/ProfileCardTrigger.tsx` if (this.domElement && options !== null && options !== void 0 && options.profilecardProvider) { var isAgentMentionsEnabled = isAgentMentionsExperimentEnabled(); if (node.attrs.text && !isAgentAaidText(node, isAgentMentionsEnabled)) { this.domElement.setAttribute('aria-label', getAccessibilityLabelFromName(node.attrs.text)); } this.domElement.setAttribute('aria-expanded', 'false'); this.domElement.setAttribute('role', 'button'); this.domElement.setAttribute('tabindex', '0'); this.domElement.setAttribute('aria-haspopup', 'dialog'); } this.destroyProfileCard = destroyProfileCard; this.removeProfileCard = removeProfileCard; this.updateProfileCardNode = updateNode; } return (0, _createClass2.default)(MentionNodeView, [{ key: "setClassList", value: function setClassList(state, disabledTooltip) { var _this$mentionPrimitiv, _this$mentionPrimitiv2, _this$mentionPrimitiv3; (_this$mentionPrimitiv = this.mentionPrimitiveElement) === null || _this$mentionPrimitiv === void 0 || _this$mentionPrimitiv.classList.toggle('mention-self', state === 'self'); (_this$mentionPrimitiv2 = this.mentionPrimitiveElement) === null || _this$mentionPrimitiv2 === void 0 || _this$mentionPrimitiv2.classList.toggle('mention-restricted', state === 'restricted'); (_this$mentionPrimitiv3 = this.mentionPrimitiveElement) === null || _this$mentionPrimitiv3 === void 0 || _this$mentionPrimitiv3.classList.toggle('mention-disabled', state === 'disabled'); // Mirror the React `<Mention>` a11y behaviour: when the chip is // disabled, expose `aria-disabled` so assistive tech announces it as // such. Also surface the tooltip text via `aria-label` so screen-reader // users hear *why* the chip is disabled, matching the React `<Mention>` // behaviour at `Mention/index.tsx` line 152. if (this.domElement) { if (state === 'disabled') { this.domElement.setAttribute('aria-disabled', 'true'); if (disabledTooltip) { var text = this.node.attrs.text || '@...'; this.domElement.setAttribute('aria-label', "".concat(text, " \u2014 ").concat(disabledTooltip)); } } else { this.domElement.removeAttribute('aria-disabled'); this.domElement.removeAttribute('aria-label'); } } } }, { key: "getDisabledState", value: function getDisabledState(mentionProvider) { var _mentionProvider$getM; var input = { id: this.node.attrs.id, userType: this.node.attrs.userType }; return mentionProvider === null || mentionProvider === void 0 || (_mentionProvider$getM = mentionProvider.getMentionDisabledState) === null || _mentionProvider$getM === void 0 ? void 0 : _mentionProvider$getM.call(mentionProvider, input); } /** * Subscribes this NodeView to disabled-state-change notifications on the * supplied provider so already-rendered chips can re-evaluate themselves * when the consumer's predicate inputs change (e.g. the active agent * selection toggling in Rovo Chat). No-op for providers that don't * implement `subscribeToDisabledStateChanges`. * * Idempotent: re-calling with the same provider keeps the existing * subscription; passing a different provider tears the old subscription * down before attaching the new one. Safe to call from the sharedState * `onChange` handler when the editor swaps providers. */ }, { key: "subscribeToProviderDisabledStateChanges", value: function subscribeToProviderDisabledStateChanges(mentionProvider) { var _this$unsubscribeFrom, _this2 = this; if (this.subscribedProvider === mentionProvider) { return; } (_this$unsubscribeFrom = this.unsubscribeFromDisabledStateChanges) === null || _this$unsubscribeFrom === void 0 || _this$unsubscribeFrom.call(this); this.unsubscribeFromDisabledStateChanges = undefined; this.subscribedProvider = mentionProvider; if (!(mentionProvider !== null && mentionProvider !== void 0 && mentionProvider.subscribeToDisabledStateChanges)) { return; } this.unsubscribeFromDisabledStateChanges = mentionProvider.subscribeToDisabledStateChanges(function () { _this2.updateState(_this2.subscribedProvider); }); } }, { key: "syncDisabledTooltip", value: function syncDisabledTooltip(disabledState) { // Capture the tooltip text into a local so the rest of the method can // branch on a truthy string instead of re-asserting non-null fields // off of `disabledState`. var tooltipText = disabledState !== null && disabledState !== void 0 && disabledState.disabled ? disabledState.tooltip : undefined; var chip = this.mentionPrimitiveElement; var portalProviderAPI = this.config.portalProviderAPI; if (!chip || !portalProviderAPI) { return; } if (tooltipText) { if (!this.disabledTooltip) { this.disabledTooltip = (0, _disabledTooltipRenderer.disabledTooltipRenderer)({ chipElement: chip, portalProviderAPI: portalProviderAPI }); } this.disabledTooltip.setTooltip(tooltipText); } else if (this.disabledTooltip) { this.disabledTooltip.destroy(); this.disabledTooltip = undefined; } } }, { key: "setTextContent", value: function setTextContent(name, isAgentMentionsEnabled) { // Also overwrite when text is a raw AAID so the resolved name takes precedence. var textIsAaid = isAgentAaidText(this.node, isAgentMentionsEnabled); if (name && (!this.node.attrs.text || textIsAaid) && this.mentionTextElement) { this.setVisibleText(name); } } }, { key: "setVisibleText", value: function setVisibleText(text) { if (!this.mentionTextElement) { return; } this.mentionTextElement.textContent = this.hasAvatarSlot && text.startsWith('@') && text !== unknownMentionText ? text.slice(1) : text; } }, { key: "resolveMentionAvatar", value: function resolveMentionAvatar(mentionNodeDataProvider) { var _this3 = this; if (!this.hasAvatarSlot || !mentionNodeDataProvider || this.node.attrs.userType === 'SPECIAL') { return; } var mention = { id: this.node.attrs.id, userType: this.node.attrs.userType }; var applyData = function applyData(data) { var _this3$mentionAvatar; if (_this3.isDestroyed || !(data !== null && data !== void 0 && data.avatarUrl)) { return; } (_this3$mentionAvatar = _this3.mentionAvatar) === null || _this3$mentionAvatar === void 0 || _this3$mentionAvatar.render(data); }; mentionNodeDataProvider.getMentionData(mention, function (payload) { if (payload.data) { applyData(payload.data); } }); } }, { key: "shouldHighlightMention", value: function shouldHighlightMention(mentionProvider) { var _this$config$options; var _ref4 = (_this$config$options = this.config.options) !== null && _this$config$options !== void 0 ? _this$config$options : {}, currentUserId = _ref4.currentUserId; // Check options first (immediate), then provider (async), then default to false if (currentUserId && this.node.attrs.id === currentUserId) { return true; } else { var _mentionProvider$shou; return (_mentionProvider$shou = mentionProvider === null || mentionProvider === void 0 ? void 0 : mentionProvider.shouldHighlightMention({ id: this.node.attrs.id })) !== null && _mentionProvider$shou !== void 0 ? _mentionProvider$shou : false; } } }, { key: "updateState", value: function () { var _updateState = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(mentionProvider) { var _mentionProvider$shou2, _this$config$options2; var isAgentMentionsEnabled, isHighlighted, disabledState, isDisabled, newState, disabledTooltip, name, text; return _regenerator.default.wrap(function (_context2) { while (1) switch (_context2.prev = _context2.next) { case 0: isAgentMentionsEnabled = isAgentMentionsExperimentEnabled(); isHighlighted = (0, _expValEquals.expValEquals)('platform_editor_vc90_transition_mentions', 'isEnabled', true) ? this.shouldHighlightMention(mentionProvider) : (_mentionProvider$shou2 = mentionProvider === null || mentionProvider === void 0 ? void 0 : mentionProvider.shouldHighlightMention({ id: this.node.attrs.id })) !== null && _mentionProvider$shou2 !== void 0 ? _mentionProvider$shou2 : false; disabledState = this.getDisabledState(mentionProvider); isDisabled = !!(disabledState !== null && disabledState !== void 0 && disabledState.disabled); newState = getNewState(isHighlighted, (0, _types.isRestricted)(this.node.attrs.accessLevel), isDisabled); disabledTooltip = disabledState !== null && disabledState !== void 0 && disabledState.disabled ? disabledState.tooltip : undefined; // `setClassList` always runs so the aria-label (which depends on the // tooltip text) stays in sync when the tooltip reason changes while // the chip remains disabled. State-change-only writes would leave a // stale aria-label after a tooltip-text-only update. this.setClassList(newState, disabledTooltip); // Tooltip wiring runs every update (not just on state change) so that // the tooltip text stays in sync if the disabled reason changes while // the chip is already disabled. this.syncDisabledTooltip(disabledState); _context2.next = 1; return handleProviderName(mentionProvider, this.node, isAgentMentionsEnabled); case 1: name = _context2.sent; this.setTextContent(name, isAgentMentionsEnabled); // Only overwrite the disabled-state aria-label with the name-based one // when the chip is NOT disabled; otherwise the disabled reason set in // `setClassList` would be silently clobbered, regressing a11y. if (this.domElement && (_this$config$options2 = this.config.options) !== null && _this$config$options2 !== void 0 && _this$config$options2.profilecardProvider && newState !== 'disabled') { text = name !== null && name !== void 0 ? name : this.node.attrs.text; if (text && !isAgentAaidText(this.node, isAgentMentionsEnabled)) { this.domElement.setAttribute('aria-label', getAccessibilityLabelFromName(text)); } } case 2: case "end": return _context2.stop(); } }, _callee2, this); })); function updateState(_x4) { return _updateState.apply(this, arguments); } return updateState; }() }, { key: "nodeIsEqual", value: function nodeIsEqual(nextNode) { var _this$config$options3; if ((_this$config$options3 = this.config.options) !== null && _this$config$options3 !== void 0 && _this$config$options3.sanitizePrivateContent) { // Compare nodes but ignore the text parameter as it may be sanitized var nextNodeAttrs = _objectSpread(_objectSpread({}, nextNode.attrs), {}, { text: this.node.attrs.text }); return this.node.hasMarkup(nextNode.type, nextNodeAttrs, nextNode.marks); } return this.node.sameMarkup(nextNode); } }, { key: "update", value: function update(node) { var _this$updateProfileCa; if (!this.nodeIsEqual(node)) { return false; } this.node = node; // Keep the profile card renderer's node reference in sync so that the // click handler always reads up-to-date attrs. (_this$updateProfileCa = this.updateProfileCardNode) === null || _this$updateProfileCa === void 0 || _this$updateProfileCa.call(this, node); return true; } }, { key: "destroy", value: function destroy() { var _this$cleanup, _this$destroyProfileC, _this$mentionAvatar, _this$disabledTooltip, _this$unsubscribeFrom2; this.isDestroyed = true; // Surface the destruction to the provider before tearing down so the // chat layer can react (e.g. drop the agent id from `selectedAgentIds`). // This is the lowest-level deletion signal — fires for backspace, // select-and-delete, programmatic doc replaces, and editor unmount. try { var _this$subscribedProvi, _this$subscribedProvi2; (_this$subscribedProvi = this.subscribedProvider) === null || _this$subscribedProvi === void 0 || (_this$subscribedProvi2 = _this$subscribedProvi.notifyMentionDestroyed) === null || _this$subscribedProvi2 === void 0 || _this$subscribedProvi2.call(_this$subscribedProvi, { id: this.node.attrs.id }); } catch (_error) { // Defensive: never let consumer-side notification errors prevent // the NodeView from cleaning up its own resources below. } (_this$cleanup = this.cleanup) === null || _this$cleanup === void 0 || _this$cleanup.call(this); (_this$destroyProfileC = this.destroyProfileCard) === null || _this$destroyProfileC === void 0 || _this$destroyProfileC.call(this); (_this$mentionAvatar = this.mentionAvatar) === null || _this$mentionAvatar === void 0 || _this$mentionAvatar.destroy(); this.mentionAvatar = undefined; (_this$disabledTooltip = this.disabledTooltip) === null || _this$disabledTooltip === void 0 || _this$disabledTooltip.destroy(); this.disabledTooltip = undefined; (_this$unsubscribeFrom2 = this.unsubscribeFromDisabledStateChanges) === null || _this$unsubscribeFrom2 === void 0 || _this$unsubscribeFrom2.call(this); this.unsubscribeFromDisabledStateChanges = undefined; this.subscribedProvider = undefined; } }, { key: "deselectNode", value: function deselectNode() { var _this$removeProfileCa; (_this$removeProfileCa = this.removeProfileCard) === null || _this$removeProfileCa === void 0 || _this$removeProfileCa.call(this); } }]); }();