UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

202 lines (201 loc) • 7.32 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import { GapCursorSelection } from '@atlaskit/editor-common/selection/gap-cursor/selection'; import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model'; import { EditorState, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { dropPoint } from '@atlaskit/editor-prosemirror/transform'; import { getMediaNodeInsertionType } from './utils/media-inline'; /** Keeps a media insertion point collapsed so a drop never replaces the current selection. */ export const mediaDropSelection = (doc, pos) => { const $pos = doc.resolve(pos); return $pos.parent.inlineContent ? TextSelection.create(doc, pos) : new GapCursorSelection($pos); }; export const findMediaDropPosition = (view, coords, fileTypes, mediaOptions) => { if (!view.editable || view.dragging || fileTypes.length === 0) { return; } const rect = view.dom.getBoundingClientRect(); const hit = view.posAtCoords({ left: Math.max(rect.left + 1, Math.min(coords.left, rect.right - 1)), top: Math.max(rect.top + 1, Math.min(coords.top, rect.bottom - 1)) }); if (!hit) { return; } const { doc, schema } = view.state; const targetState = EditorState.create({ doc, schema, selection: mediaDropSelection(doc, hit.pos) }); const supportedOptions = schema.nodes.mediaInline ? mediaOptions : { ...mediaOptions, featureFlags: { ...(mediaOptions === null || mediaOptions === void 0 ? void 0 : mediaOptions.featureFlags), mediaInline: false } }; const insertionTypes = new Set(Array.from(new Set(fileTypes), fileType => getMediaNodeInsertionType(targetState, supportedOptions, fileType))); const slices = Array.from(insertionTypes).flatMap(insertionType => { let node; if (insertionType === 'inline') { node = schema.nodes.mediaInline.create(); } else { const container = insertionType === 'block' ? schema.nodes.mediaSingle : schema.nodes.mediaGroup; if (!container || !schema.nodes.media) { return []; } node = container.create(null, schema.nodes.media.create({ type: 'file' })); } return new Slice(Fragment.from(node), 0, 0); }); if (slices.length !== insertionTypes.size) { return; } for (const slice of slices) { const pos = dropPoint(doc, hit.pos, slice); if (pos !== null && slices.every(content => { const alreadyValidated = content === slice && pos === hit.pos; return alreadyValidated || dropPoint(doc, pos, content) === pos; })) { return pos; } } return; }; /** Only a single image over a media card can replace it; captions and surrounding space insert. */ export const findMediaDropReplacement = (view, event, fileTypes) => { var _mediaDom$querySelect; if (!view.editable || view.dragging || fileTypes.length !== 1 || !fileTypes[0].startsWith('image/')) { return; } if (!(event.target instanceof Element)) { return; } const mediaDom = event.target.closest('[data-prosemirror-node-name="media"], [data-node-type="media"]'); if (!mediaDom || !view.dom.contains(mediaDom)) { return; } const card = (_mediaDom$querySelect = mediaDom.querySelector('.media-card-wrapper')) !== null && _mediaDom$querySelect !== void 0 ? _mediaDom$querySelect : mediaDom; const rect = card.getBoundingClientRect(); if (event.clientX < rect.left || event.clientX >= rect.right || event.clientY < rect.top || event.clientY >= rect.bottom) { return; } const $pos = view.state.doc.resolve(view.posAtDOM(mediaDom, 0)); if ($pos.parent.type !== view.state.schema.nodes.mediaSingle) { return; } const media = $pos.parent.firstChild; if (!media || media.type !== view.state.schema.nodes.media) { return; } const mimeType = media.attrs.__fileMimeType; const hasImagePreview = !!card.querySelector('img[data-fileid], img[data-testid="media-image"], img[data-testid="media-card-svg"]') && !card.querySelector('video, [data-testid="media-card-play-button-wrapper"]'); const isImage = mimeType ? mimeType.startsWith('image/') : media.attrs.type === 'external' || hasImagePreview; return isImage ? { pos: $pos.before(), media } : undefined; }; const mapReplacement = (replacement, mapping) => { if (!replacement) { return; } const mapped = mapping.mapResult(replacement.pos, 1); return mapped.deleted ? undefined : { ...replacement, pos: mapped.pos }; }; const currentReplacement = (replacement, doc) => { if (!replacement) { return; } const node = doc.nodeAt(replacement.pos); const media = node === null || node === void 0 ? void 0 : node.firstChild; const original = replacement.media; if ((node === null || node === void 0 ? void 0 : node.type.name) === 'mediaSingle' && (media === null || media === void 0 ? void 0 : media.type) === original.type && media.attrs.id === original.attrs.id && media.attrs.collection === original.attrs.collection && media.attrs.type === original.attrs.type && media.attrs.url === original.attrs.url) { return replacement; } return; }; /** Retains drop locations while uploads prepare and other edits change document positions. */ export class MediaDropTargets { constructor() { _defineProperty(this, "batches", new Map()); _defineProperty(this, "files", new Map()); } start(traceId, pos, replacement) { const batch = { pos, replacement, files: new Set(), prepared: false }; this.batches.set(traceId, batch); return () => { batch.prepared = true; this.prune(); }; } register(traceId, fileIds) { const batch = this.batches.get(traceId); if (!batch) { return; } for (const id of fileIds) { batch.files.add(id); this.files.set(id, batch); } } positionFor(fileId) { var _this$files$get; return (_this$files$get = this.files.get(fileId)) === null || _this$files$get === void 0 ? void 0 : _this$files$get.pos; } replacementFor(fileId, doc) { var _this$files$get2; return currentReplacement((_this$files$get2 = this.files.get(fileId)) === null || _this$files$get2 === void 0 ? void 0 : _this$files$get2.replacement, doc); } hoverReplacementFor(doc) { return currentReplacement(this.hoverReplacement, doc); } clearHover() { this.hover = undefined; this.hoverReplacement = undefined; } inserted(fileId) { var _this$files$get3; (_this$files$get3 = this.files.get(fileId)) === null || _this$files$get3 === void 0 ? void 0 : _this$files$get3.files.delete(fileId); this.files.delete(fileId); this.prune(); } map(mapping) { if (mapping.maps.length === 0) { return; } if (this.hover !== undefined) { this.hover = mapping.map(this.hover, 1); } this.hoverReplacement = mapReplacement(this.hoverReplacement, mapping); for (const batch of this.batches.values()) { batch.pos = mapping.map(batch.pos, 1); batch.replacement = mapReplacement(batch.replacement, mapping); } } clear() { this.clearHover(); this.batches.clear(); this.files.clear(); } prune() { for (const [id, batch] of this.batches) { if (batch.prepared && batch.files.size === 0) { this.batches.delete(id); } } } }