@atlaskit/editor-plugin-copy-button
Version:
editor-plugin-copy-button for @atlaskit/editor-core
186 lines (181 loc) • 8.75 kB
JavaScript
import { ACTION, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { getBrowserInfo } from '@atlaskit/editor-common/browser';
import { copyHTMLToClipboard, copyHTMLToClipboardPolyfill, getAnalyticsPayload } from '@atlaskit/editor-common/clipboard';
import { copyDomNode, getSelectedNodeOrNodeParentByNodeType, toDOM } from '@atlaskit/editor-common/copy-button';
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { copyButtonPluginKey } from './plugin-key';
export function createToolbarCopyCommandForMark(markType, editorAnalyticsApi) {
function command(state, dispatch) {
const textNode = state.tr.selection.$head.parent.maybeChild(state.tr.selection.$head.index());
if (!textNode) {
return false;
}
if (dispatch) {
// As calling copyHTMLToClipboard causes side effects -- we only run this when
// dispatch is provided -- as otherwise the consumer is only testing to see if
// the action is availble.
const domNode = toDOM(textNode, state.schema);
if (domNode) {
const div = document.createElement('div');
const p = document.createElement('p');
div.appendChild(p);
p.appendChild(domNode);
// The "1 1" refers to the start and end depth of the slice
// since we're copying the text inside a paragraph, it will always be 1 1
// https://github.com/ProseMirror/prosemirror-view/blob/master/src/clipboard.ts#L32
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
div.firstChild.setAttribute('data-pm-slice', '1 1 []');
// If we're copying a hyperlink, we'd copy the url as the fallback plain text
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
const linkUrl = domNode.getAttribute('href');
copyHTMLToClipboard(div, markType.name === 'link' && linkUrl ? linkUrl : undefined);
}
const copyToClipboardTr = state.tr;
copyToClipboardTr.setMeta(copyButtonPluginKey, {
copied: true
});
const analyticsPayload = getAnalyticsPayload(state, ACTION.COPIED);
if (analyticsPayload && editorAnalyticsApi) {
var _editorAnalyticsApi$a;
analyticsPayload.attributes.inputMethod = INPUT_METHOD.FLOATING_TB;
analyticsPayload.attributes.markType = markType.name;
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : (_editorAnalyticsApi$a = editorAnalyticsApi.attachAnalyticsEvent) === null || _editorAnalyticsApi$a === void 0 ? void 0 : _editorAnalyticsApi$a.call(editorAnalyticsApi, analyticsPayload)(copyToClipboardTr);
}
dispatch(copyToClipboardTr);
}
return true;
}
return command;
}
export function getProvideMarkVisualFeedbackForCopyButtonCommand(markType) {
function provideMarkVisualFeedbackForCopyButtonCommand(state, dispatch) {
const tr = state.tr;
tr.setMeta(copyButtonPluginKey, {
showSelection: true,
markType
});
if (dispatch) {
dispatch(tr);
}
return true;
}
return provideMarkVisualFeedbackForCopyButtonCommand;
}
export function removeMarkVisualFeedbackForCopyButtonCommand(state, dispatch) {
const tr = state.tr;
tr.setMeta(copyButtonPluginKey, {
removeSelection: true
});
const copyButtonState = copyButtonPluginKey.getState(state);
if (copyButtonState !== null && copyButtonState !== void 0 && copyButtonState.copied) {
tr.setMeta(copyButtonPluginKey, {
copied: false
});
}
if (dispatch) {
dispatch(tr);
}
return true;
}
export const createToolbarCopyCommandForNode = (nodeType, editorAnalyticsApi, api, onClickMessage) => (state, dispatch) => {
const {
tr,
schema
} = state;
// This command should only be triggered by the Copy button in the floating toolbar
// which is only visible when selection is inside the target node
const contentNodeWithPos = getSelectedNodeOrNodeParentByNodeType({
nodeType,
selection: tr.selection
});
if (!contentNodeWithPos) {
return false;
}
const copyToClipboardTr = tr;
copyToClipboardTr.setMeta(copyButtonPluginKey, {
copied: true
});
const analyticsPayload = getAnalyticsPayload(state, ACTION.COPIED);
if (analyticsPayload && editorAnalyticsApi) {
var _editorAnalyticsApi$a2;
analyticsPayload.attributes.inputMethod = INPUT_METHOD.FLOATING_TB;
analyticsPayload.attributes.nodeType = contentNodeWithPos.node.type.name;
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : (_editorAnalyticsApi$a2 = editorAnalyticsApi.attachAnalyticsEvent) === null || _editorAnalyticsApi$a2 === void 0 ? void 0 : _editorAnalyticsApi$a2.call(editorAnalyticsApi, analyticsPayload)(copyToClipboardTr);
}
if (dispatch) {
// As calling copyHTMLToClipboard causes side effects -- we only run this when
// dispatch is provided -- as otherwise the consumer is only testing to see if
// the action is availble.
const domNode = toDOM(contentNodeWithPos.node, schema);
if (editorExperiment('platform_editor_block_menu', true)) {
copyDomNode(domNode, contentNodeWithPos.node.type, tr.selection);
} else {
if (domNode) {
const div = document.createElement('div');
const browser = getBrowserInfo();
div.appendChild(domNode);
// if copying inline content
if (contentNodeWithPos.node.type.inlineContent) {
// The "1 1" refers to the start and end depth of the slice
// since we're copying the text inside a paragraph, it will always be 1 1
// https://github.com/ProseMirror/prosemirror-view/blob/master/src/clipboard.ts#L32
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
div.firstChild.setAttribute('data-pm-slice', '1 1 []');
} else {
// The "0 0" refers to the start and end depth of the slice
// since we're copying the block node only, it will always be 0 0
// https://github.com/ProseMirror/prosemirror-view/blob/master/src/clipboard.ts#L32
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
div.firstChild.setAttribute('data-pm-slice', '0 0 []');
}
// ED-17083 safari seems have bugs for extension copy because exntension do not have a child text(innerText) and it will not recognized as html in clipboard, this could be merge into one if this extension fixed children issue or safari fix the copy bug
// MEX-2528 safari has a bug related to the mediaSingle node with border or link. The image tag within the clipboard is not recognized as HTML when using the ClipboardItem API. To address this, we have to switch to ClipboardPolyfill
if (browser.safari && state.selection instanceof NodeSelection && (state.selection.node.type === state.schema.nodes.extension || state.selection.node.type === state.schema.nodes.mediaSingle)) {
copyHTMLToClipboardPolyfill(div);
} else {
copyHTMLToClipboard(div);
}
}
}
copyToClipboardTr.setMeta('scrollIntoView', false);
dispatch(copyToClipboardTr);
}
/**
* A11y-10275 This prop api drilled from the plugin all the way down to the usage
* because it's the safest way to ensure accessibilityUtils is available and not
* undefined.
*
* see thread https://atlassian.slack.com/archives/C02CDUS3SUS/p1744256326459569
*/
if (onClickMessage) {
var _api$accessibilityUti;
api === null || api === void 0 ? void 0 : (_api$accessibilityUti = api.accessibilityUtils) === null || _api$accessibilityUti === void 0 ? void 0 : _api$accessibilityUti.actions.ariaNotify(onClickMessage, {
priority: 'important'
});
}
return true;
};
export const resetCopiedState = (nodeType, hoverDecoration, onMouseLeave) => (state, dispatch) => {
let customTr = state.tr;
// Avoid multipe dispatch
// https://product-fabric.atlassian.net/wiki/spaces/E/pages/2241659456/All+about+dispatch+and+why+there+shouldn+t+be+multiple#How-do-I-avoid-them%3F
const customDispatch = tr => {
customTr = tr;
};
onMouseLeave ? onMouseLeave(state, customDispatch) : hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false)(state, customDispatch);
const copyButtonState = copyButtonPluginKey.getState(state);
if (copyButtonState !== null && copyButtonState !== void 0 && copyButtonState.copied) {
customTr.setMeta(copyButtonPluginKey, {
copied: false
});
}
if (dispatch) {
dispatch(customTr);
}
return true;
};