@atlaskit/editor-plugin-mentions
Version:
Mentions plugin for @atlaskit/editor-core
373 lines (371 loc) • 19.6 kB
JavaScript
"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 _resource = require("@atlaskit/mention/resource");
var _types = require("@atlaskit/mention/types");
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
var _expVal = require("@atlaskit/tmp-editor-statsig/expVal");
var _disabledTooltipRenderer = require("./disabledTooltipRenderer");
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; }
var primitiveClassName = 'editor-mention-primitive';
// @ts-ignore - TS1501 TypeScript 5.9.2 upgrade
var getAccessibilityLabelFromName = function getAccessibilityLabelFromName(name) {
return name.replace(/^@/, '');
};
var toDOM = function toDOM(node) {
// 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, _platformFeatureFlags.fg)('platform_editor_adf_with_localid')) {
mentionAttrs = _objectSpread(_objectSpread({}, mentionAttrs), {}, {
'data-local-id': node.attrs.localId
});
}
if ((0, _expVal.expVal)('platform_editor_agent_mentions', 'isEnabled', false) && node.attrs.userType) {
mentionAttrs = _objectSpread(_objectSpread({}, mentionAttrs), {}, {
'data-user-type': node.attrs.userType
});
}
var browser = (0, _browser.getBrowserInfo)();
return ['span', mentionAttrs, ['span', {
class: 'zeroWidthSpaceContainer'
}, ['span', {
class: 'inlineNodeViewAddZeroWidthSpace'
}, _whitespace.ZERO_WIDTH_SPACE]], ['span', {
spellcheck: 'false',
class: primitiveClassName
}, node.attrs.text || '@…'], 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 || '') : "@_|unknown|_";
};
var handleProviderName = /*#__PURE__*/function () {
var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(mentionProvider, node) {
var nameDetail, resolvedNameDetail;
return _regenerator.default.wrap(function (_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
if (!((0, _resource.isResolvingMentionProvider)(mentionProvider) && node.attrs.id && !node.attrs.text)) {
_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) {
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);
var options = config.options,
api = config.api,
portalProviderAPI = config.portalProviderAPI;
var _DOMSerializer$render = _model.DOMSerializer.renderSpec(document, toDOM(node)),
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;
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
}),
destroyProfileCard = _profileCardRenderer.destroyProfileCard,
removeProfileCard = _profileCardRenderer.removeProfileCard;
// Accessibility attributes - based on `packages/people-and-teams/profilecard/src/components/User/ProfileCardTrigger.tsx`
if (this.domElement && options !== null && options !== void 0 && options.profilecardProvider) {
if (node.attrs.text) {
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;
}
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) {
if (name && !this.node.attrs.text && this.mentionPrimitiveElement) {
this.mentionPrimitiveElement.textContent = name;
}
}
}, {
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 isHighlighted, disabledState, isDisabled, newState, disabledTooltip, name;
return _regenerator.default.wrap(function (_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
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);
case 1:
name = _context2.sent;
this.setTextContent(name);
// 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 (name && this.domElement && (_this$config$options2 = this.config.options) !== null && _this$config$options2 !== void 0 && _this$config$options2.profilecardProvider && newState !== 'disabled') {
this.domElement.setAttribute('aria-label', getAccessibilityLabelFromName(name));
}
case 2:
case "end":
return _context2.stop();
}
}, _callee2, this);
}));
function updateState(_x3) {
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) {
if (!this.nodeIsEqual(node)) {
return false;
}
this.node = node;
return true;
}
}, {
key: "destroy",
value: function destroy() {
var _this$cleanup, _this$destroyProfileC, _this$disabledTooltip, _this$unsubscribeFrom2;
// 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$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);
}
}]);
}();