@atlaskit/editor-plugin-paste
Version:
Paste plugin for @atlaskit/editor-core
232 lines (218 loc) • 11.5 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isImage = void 0;
exports.transformSliceForMedia = transformSliceForMedia;
exports.unwrapNestedMediaElements = exports.transformSliceToMediaSingleWithNewExperience = exports.transformSliceToCorrectMediaWrapper = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _mediaSingle = require("@atlaskit/editor-common/media-single");
var _utils = require("@atlaskit/editor-common/utils");
var _utils2 = require("@atlaskit/editor-prosemirror/utils");
var _mediaCommon = require("@atlaskit/media-common");
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
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; }
/**
* Ensure correct layout in nested mode
*
* TODO: ED-26959 - this func is only used in handlePaste, so layout update won't work for drop event
*/
function transformSliceForMedia(slice, schema, api) {
var _schema$nodes = schema.nodes,
mediaSingle = _schema$nodes.mediaSingle,
layoutSection = _schema$nodes.layoutSection,
table = _schema$nodes.table,
bulletList = _schema$nodes.bulletList,
orderedList = _schema$nodes.orderedList,
expand = _schema$nodes.expand,
nestedExpand = _schema$nodes.nestedExpand;
return function (selection) {
var newSlice = slice;
if ((0, _utils2.hasParentNodeOfType)([layoutSection, table, bulletList, orderedList, expand, nestedExpand])(selection)) {
newSlice = (0, _utils.mapSlice)(newSlice, function (node) {
var _api$media, _mediaState$mediaOpti;
var mediaState = api === null || api === void 0 || (_api$media = api.media) === null || _api$media === void 0 ? void 0 : _api$media.sharedState.currentState();
var extendedOrLegacyAttrs = mediaState !== null && mediaState !== void 0 && (_mediaState$mediaOpti = mediaState.mediaOptions) !== null && _mediaState$mediaOpti !== void 0 && _mediaState$mediaOpti.allowPixelResizing ? {
layout: node.attrs.layout,
widthType: node.attrs.widthType,
width: node.attrs.width
} : {
layout: node.attrs.layout
};
var attrs = {};
if ((0, _utils2.hasParentNodeOfType)([layoutSection, table])(selection)) {
// Supports layouts
attrs = _objectSpread({}, extendedOrLegacyAttrs);
} else if ((0, _utils2.hasParentNodeOfType)([bulletList, orderedList, expand, nestedExpand])(selection)) {
// does not support other layouts
attrs = _objectSpread(_objectSpread({}, extendedOrLegacyAttrs), {}, {
layout: 'center'
});
}
return node.type.name === 'mediaSingle' ? mediaSingle.createChecked(attrs, node.content, node.marks) : node;
});
}
return newSlice;
};
}
// Ignored via go/ees007
// eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format
// TODO move this to editor-common
var isImage = exports.isImage = function isImage(fileType) {
return !!fileType && (fileType.indexOf('image/') > -1 || fileType.indexOf('video/') > -1);
};
var transformSliceToCorrectMediaWrapper = exports.transformSliceToCorrectMediaWrapper = function transformSliceToCorrectMediaWrapper(slice, schema) {
var _schema$nodes2 = schema.nodes,
mediaGroup = _schema$nodes2.mediaGroup,
mediaSingle = _schema$nodes2.mediaSingle,
media = _schema$nodes2.media;
return (0, _utils.mapSlice)(slice, function (node, parent) {
if (!parent && node.type === media) {
if (mediaSingle && (isImage(node.attrs.__fileMimeType) || node.attrs.type === 'external')) {
return mediaSingle.createChecked({}, node);
} else {
return mediaGroup.createChecked({}, [node]);
}
}
return node;
});
};
/**
* This func will be called when copy & paste, drag & drop external html with media, media files, and slices from editor
* Because width may not be available when transform, DEFAULT_IMAGE_WIDTH is used as a fallback
*
*/
var transformSliceToMediaSingleWithNewExperience = exports.transformSliceToMediaSingleWithNewExperience = function transformSliceToMediaSingleWithNewExperience(slice, schema, api) {
var _schema$nodes3 = schema.nodes,
mediaInline = _schema$nodes3.mediaInline,
mediaSingle = _schema$nodes3.mediaSingle,
media = _schema$nodes3.media;
var newSlice = (0, _utils.mapSlice)(slice, function (node) {
// This logic is duplicated in editor-plugin-ai where external images can be inserted
// from external sources through the use of AI. The editor-plugin-ai package is avoiding
// sharing dependencies with editor-core to support products using it with various versions
// of editor packages.
// The duplication is in the following file:
// packages/editor/editor-plugin-ai/src/prebuilt/content-transformers/markdown-to-pm/markdown-transformer.ts
if (node.type === mediaSingle) {
var _api$media2, _mediaState$mediaOpti2;
var mediaState = api === null || api === void 0 || (_api$media2 = api.media) === null || _api$media2 === void 0 ? void 0 : _api$media2.sharedState.currentState();
return mediaState !== null && mediaState !== void 0 && (_mediaState$mediaOpti2 = mediaState.mediaOptions) !== null && _mediaState$mediaOpti2 !== void 0 && _mediaState$mediaOpti2.allowPixelResizing ? mediaSingle.createChecked({
width: node.attrs.width || _mediaSingle.DEFAULT_IMAGE_WIDTH,
widthType: node.attrs.widthType || 'pixel',
layout: node.attrs.layout
}, node.content, node.marks) : node;
}
return node;
});
return (0, _utils.mapSlice)(newSlice, function (node) {
var __mediaTraceId = (0, _mediaCommon.getRandomHex)(8);
if (node.type === media) {
var _api$media3;
api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 || (_api$media3 = api.media) === null || _api$media3 === void 0 ? void 0 : _api$media3.commands.trackMediaPaste(node.attrs));
return media.createChecked(_objectSpread(_objectSpread({}, node.attrs), {}, {
__external: node.attrs.type === 'external',
__mediaTraceId: node.attrs.type === 'external' ? null : __mediaTraceId
}), node.content, node.marks);
}
if (node.type.name === 'mediaInline') {
var _api$media4;
api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 || (_api$media4 = api.media) === null || _api$media4 === void 0 ? void 0 : _api$media4.commands.trackMediaPaste(node.attrs));
return mediaInline.createChecked(_objectSpread(_objectSpread({}, node.attrs), {}, {
__mediaTraceId: __mediaTraceId
}), node.content, node.marks);
}
return node;
});
};
/**
* Check base styles to see if an element will be invisible when rendered in a document.
* @param element
*/
var isElementInvisible = function isElementInvisible(element) {
return element.style.opacity === '0' || element.style.display === 'none' || element.style.visibility === 'hidden';
};
var VALID_TAGS_CONTAINER = ['DIV', 'TD', 'BLOCKQUOTE'];
function canContainImage(element) {
if (!element) {
return false;
}
return VALID_TAGS_CONTAINER.indexOf(element.tagName) !== -1;
}
/**
* Given a html string, we attempt to hoist any nested `<img>` tags,
* not directly wrapped by a `<div>` as ProseMirror no-op's
* on those scenarios.
* @param html
*/
var unwrapNestedMediaElements = exports.unwrapNestedMediaElements = function unwrapNestedMediaElements(html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var wrapper = doc.body;
// Remove Google Doc's wrapper <b> el
var docsWrapper = wrapper.querySelector('b[id^="docs-internal-guid-"]');
if (docsWrapper) {
(0, _utils.unwrap)(wrapper, docsWrapper);
}
var imageTags = wrapper.querySelectorAll('img');
if (!imageTags.length) {
return html;
}
imageTags.forEach(function (imageTag) {
// Capture the immediate parent, we may remove the media from here later.
var mediaParent = imageTag.parentElement;
if (!mediaParent) {
return;
}
// Bypass emoji
if (imageTag.className.includes('emoji-common-emoji-image')) {
return;
}
// Bypass mediaInline images - don't hoist images that are inside a mediaInline wrapper
// as this would break parseDOM matching for mediaInline nodes
// We remove the img from the DOM since mediaInline is a leaf node with no content
if (imageTag.closest('[data-node-type="mediaInline"]') && (0, _expValEquals.expValEquals)('platform_editor_inline_media_copy_paste_fix', 'isEnabled', true)) {
// Remove the img element so ProseMirror doesn't try to parse it
// mediaInline nodes are leaf nodes and cannot have children
imageTag.remove();
return;
}
// If either the parent or the image itself contains styles that would make
// them invisible on copy, dont paste them.
if (isElementInvisible(mediaParent) || isElementInvisible(imageTag)) {
mediaParent.removeChild(imageTag);
return;
}
// If its wrapped by a valid container we assume its safe to bypass.
// ProseMirror should handle these cases properly.
if (canContainImage(mediaParent) || mediaParent instanceof HTMLSpanElement && mediaParent.closest('[class*="emoji-common"]')) {
return;
}
// Find the top most element that the parent has a valid container for the image.
// Stop just before found the wrapper
var insertBeforeElement = (0, _utils.walkUpTreeUntil)(mediaParent, function (element) {
// If is at the top just use this element as reference
if (element.parentElement === wrapper) {
return true;
}
return canContainImage(element.parentElement);
});
// Here we try to insert the media right after its top most valid parent element
// Unless its the last element in our structure then we will insert above it.
if (insertBeforeElement && insertBeforeElement.parentElement) {
// Insert as close as possible to the most closest valid element index in the tree.
insertBeforeElement.parentElement.insertBefore(imageTag, insertBeforeElement.nextElementSibling || insertBeforeElement);
// Attempt to clean up lines left behind by the image
mediaParent.innerText = mediaParent.innerText.trim();
// Walk up and delete empty elements left over after removing the image tag
(0, _utils.removeNestedEmptyEls)(mediaParent);
}
});
// If last child is a hardbreak we don't want it
if (wrapper.lastElementChild && wrapper.lastElementChild.tagName === 'BR') {
wrapper.removeChild(wrapper.lastElementChild);
}
return wrapper.innerHTML;
};