@atlaskit/editor-plugin-extension
Version:
editor-plugin-extension plugin for @atlaskit/editor-core
209 lines (206 loc) • 7.92 kB
JavaScript
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { copyToClipboard } from '@atlaskit/editor-common/clipboard';
import { closestElement, findNodePosByLocalIds } from '@atlaskit/editor-common/utils';
import { JSONTransformer } from '@atlaskit/editor-json-transformer';
import { findDomRefAtPos, findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { isResolvingMentionProvider } from '@atlaskit/mention/resource';
export const getSelectedExtension = (state, searchParent = false) => {
const {
inlineExtension,
extension,
bodiedExtension,
multiBodiedExtension
} = state.schema.nodes;
const nodeTypes = [extension, bodiedExtension, inlineExtension, multiBodiedExtension];
return findSelectedNodeOfType(nodeTypes)(state.selection) || searchParent && findParentNodeOfType(nodeTypes)(state.selection) || undefined;
};
export const findExtensionWithLocalId = (state, localId) => {
const selectedExtension = getSelectedExtension(state, true);
if (!localId) {
return selectedExtension;
}
if (selectedExtension && selectedExtension.node.attrs.localId === localId) {
return selectedExtension;
}
const {
inlineExtension,
extension,
bodiedExtension,
multiBodiedExtension
} = state.schema.nodes;
const nodeTypes = [extension, bodiedExtension, inlineExtension, multiBodiedExtension];
let matched;
state.doc.descendants((node, pos) => {
if (nodeTypes.includes(node.type) && node.attrs.localId === localId) {
matched = {
node,
pos
};
}
});
return matched;
};
export const getSelectedDomElement = (schema, domAtPos, selectedExtensionNode) => {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
const selectedExtensionDomNode = findDomRefAtPos(selectedExtensionNode.pos, domAtPos);
const isContentExtension = selectedExtensionNode.node.type !== schema.nodes.bodiedExtension;
return (
// Content extension can be nested in bodied-extension, the following check is necessary for that case
(isContentExtension // Search down
? selectedExtensionDomNode.querySelector('.extension-container') // Try searching up and then down
: closestElement(selectedExtensionDomNode, '.extension-container') || selectedExtensionDomNode.querySelector('.extension-container')) || selectedExtensionDomNode
);
};
export const getDataConsumerMark = newNode => {
var _newNode$marks;
return (_newNode$marks = newNode.marks) === null || _newNode$marks === void 0 ? void 0 : _newNode$marks.find(mark => mark.type.name === 'dataConsumer');
};
export const getNodeTypesReferenced = (ids, state) => {
return findNodePosByLocalIds(state, ids, {
includeDocNode: true
}).map(({
node
}) => node.type.name);
};
export const findNodePosWithLocalId = (state, localId) => {
const nodes = findNodePosByLocalIds(state, [localId]);
return nodes.length >= 1 ? nodes[0] : undefined;
};
/**
* Converts a ProseMirror node to its text representation.
* Handles text nodes with marks (links) and inline nodes (status, mention, emoji).
* Returns the content for this node, with a trailing separator for text blocks.
*/
const convertNodeToText = (node, mentionSet, parent, locale) => {
if (node.isInline) {
const schema = node.type.schema;
let finalText = '';
if (node.isText) {
finalText = node.text || '';
if (node.marks.length > 0) {
for (const mark of node.marks) {
// if it's link, include the href in the text
if (mark.type === schema.marks.link) {
const href = mark.attrs.href;
const text = node.text || '';
// If the text differs from the href, include both
if (text && text !== href) {
finalText = `${text} ${href}`;
} else {
finalText = href;
}
}
}
}
} else {
switch (node.type) {
case schema.nodes.status:
finalText = node.attrs.text || '';
break;
case schema.nodes.mention:
mentionSet.add(node.attrs.id);
finalText = `@${node.attrs.id}`;
break;
case schema.nodes.emoji:
finalText = node.attrs.shortName || '';
break;
case schema.nodes.date:
const timestamp = new Date(Number(node.attrs.timestamp));
finalText = !isNaN(timestamp.getTime()) ? timestamp.toLocaleDateString(locale !== null && locale !== void 0 ? locale : 'en-US') : String(node.attrs.timestamp);
break;
default:
finalText = node.textContent;
break;
}
}
if (parent && parent.isTextblock && node === parent.lastChild && parent.childCount > 0) {
finalText += '\n\n';
}
return finalText;
}
return '';
};
/**
* Resolves mention IDs to their display names and replaces them in the text.
* Returns the text with resolved mentions, or the original text if the provider is unavailable.
*/
const resolveMentionsInText = async (text, mentionSet, api) => {
var _api$mention, _api$mention$sharedSt, _api$mention$sharedSt2;
const mentionProvider = api === null || api === void 0 ? void 0 : (_api$mention = api.mention) === null || _api$mention === void 0 ? void 0 : (_api$mention$sharedSt = _api$mention.sharedState) === null || _api$mention$sharedSt === void 0 ? void 0 : (_api$mention$sharedSt2 = _api$mention$sharedSt.currentState()) === null || _api$mention$sharedSt2 === void 0 ? void 0 : _api$mention$sharedSt2.mentionProvider;
if (!mentionProvider || !isResolvingMentionProvider(mentionProvider)) {
return text;
}
let resolvedText = text;
for (const id of mentionSet) {
const mention = await mentionProvider.resolveMentionName(id);
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed)
resolvedText = resolvedText.replace(`@${id}`, `@${mention.name}` || '@…');
}
return resolvedText;
};
/**
* copying ADF from the unsupported content extension as text to clipboard
*/
export const copyUnsupportedContentToClipboard = async ({
locale,
schema,
unsupportedContent,
api
}) => {
try {
if (!unsupportedContent) {
throw new Error('No nested content found');
}
if (unsupportedContent.type !== 'doc') {
unsupportedContent = {
version: 1,
type: 'doc',
content: [unsupportedContent]
};
}
const transformer = new JSONTransformer(schema);
const pmNode = transformer.parse(unsupportedContent);
let text = '';
const mentionSet = new Set();
pmNode.nodesBetween(0, pmNode.content.size, (node, _pos, parent) => {
text += convertNodeToText(node, mentionSet, parent, locale);
});
// Trim leading/trailing whitespace from the collected text
text = text.trim();
text = await resolveMentionsInText(text, mentionSet, api);
copyToClipboard(text);
} catch (error) {
throw error instanceof Error ? error : new Error('Failed to copy content');
}
};
export const onCopyFailed = ({
error,
extensionApi,
state
}) => {
var _extensionApi$analyti;
const nodeWithPos = getSelectedExtension(state, true);
if (!nodeWithPos) {
return;
}
const {
node
} = nodeWithPos;
const {
extensionType,
extensionKey
} = node.attrs;
extensionApi === null || extensionApi === void 0 ? void 0 : (_extensionApi$analyti = extensionApi.analytics) === null || _extensionApi$analyti === void 0 ? void 0 : _extensionApi$analyti.actions.fireAnalyticsEvent({
eventType: EVENT_TYPE.OPERATIONAL,
action: ACTION.COPY_FAILED,
actionSubject: ACTION_SUBJECT.EXTENSION,
actionSubjectId: node.type.name,
attributes: {
extensionKey,
extensionType,
errorMessage: error.message,
errorStack: error.stack
}
});
};