UNPKG

@atlaskit/editor-core

Version:

A package contains Atlassian editor core functionality

446 lines • 21.5 kB
import * as tslib_1 from "tslib"; import analyticsService from '../../analytics/service'; import * as assert from 'assert'; import { DefaultMediaStateManager, ContextFactory } from '@atlaskit/media-core'; import { copyOptionalAttrs } from './../../schema/nodes/media'; import { Plugin, PluginKey, NodeSelection, } from '../../prosemirror'; import PickerFacade from './picker-facade'; import { ErrorReporter } from '../../utils'; import { nodeViewFactory } from '../../nodeviews'; import { ReactMediaGroupNode, ReactMediaNode, ReactSingleImageNode } from '../../nodeviews'; import keymapPlugin from './keymap'; import { insertLinks, detectLinkRangesInSteps } from './media-links'; import { insertFile } from './media-files'; import { removeMediaNode, splitMediaGroup } from './media-common'; var MEDIA_RESOLVE_STATES = ['ready', 'error', 'cancelled']; var MediaPluginState = (function () { function MediaPluginState(state, options) { var _this = this; this.allowsMedia = false; this.allowsUploads = false; this.allowsLinks = false; this.pickers = []; this.ignoreLinks = false; this.waitForMediaUpload = true; this.mediaNodes = []; this.pluginStateChangeSubscribers = []; this.useDefaultStateManager = true; this.destroyed = false; this.setMediaProvider = function (mediaProvider) { return tslib_1.__awaiter(_this, void 0, void 0, function () { var resolvedMediaProvider, err_1, wrappedError, stateManager, uploadContext; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: if (!mediaProvider) { this.destroyPickers(); this.allowsLinks = false; this.allowsUploads = false; this.allowsMedia = false; this.notifyPluginStateSubscribers(); return [2 /*return*/]; } _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, mediaProvider]; case 2: resolvedMediaProvider = _a.sent(); assert(resolvedMediaProvider && resolvedMediaProvider.viewContext, "MediaProvider promise did not resolve to a valid instance of MediaProvider - " + resolvedMediaProvider); return [3 /*break*/, 4]; case 3: err_1 = _a.sent(); wrappedError = new Error("Media functionality disabled due to rejected provider: " + err_1.message); this.errorReporter.captureException(wrappedError); this.destroyPickers(); this.allowsLinks = false; this.allowsUploads = false; this.allowsMedia = false; this.notifyPluginStateSubscribers(); return [2 /*return*/]; case 4: this.mediaProvider = resolvedMediaProvider; this.allowsMedia = true; stateManager = resolvedMediaProvider.stateManager; if (stateManager && this.useDefaultStateManager) { stateManager.destroy(); this.useDefaultStateManager = false; } if (stateManager) { this.stateManager = stateManager; } this.allowsLinks = !!resolvedMediaProvider.linkCreateContext; this.allowsUploads = !!resolvedMediaProvider.uploadContext; if (!this.allowsUploads) return [3 /*break*/, 6]; return [4 /*yield*/, resolvedMediaProvider.uploadContext]; case 5: uploadContext = _a.sent(); if (resolvedMediaProvider.uploadParams && uploadContext) { this.initPickers(resolvedMediaProvider.uploadParams, uploadContext); } else { this.destroyPickers(); } return [3 /*break*/, 7]; case 6: this.destroyPickers(); _a.label = 7; case 7: this.notifyPluginStateSubscribers(); return [2 /*return*/]; } }); }); }; this.insertFile = function (mediaState) { var collection = _this.collectionFromProvider(); if (!collection) { return; } _this.stateManager.subscribe(mediaState.id, _this.handleMediaState); insertFile(_this.view, mediaState, collection); var view = _this.view; if (!view.hasFocus()) { view.focus(); } }; this.insertLinks = function () { return tslib_1.__awaiter(_this, void 0, void 0, function () { var mediaProvider, linkCreateContext, linkCreateContextInstance; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: mediaProvider = this.mediaProvider; if (!mediaProvider) { return [2 /*return*/]; } linkCreateContext = this.mediaProvider.linkCreateContext; if (!linkCreateContext) { return [2 /*return*/]; } return [4 /*yield*/, linkCreateContext]; case 1: linkCreateContextInstance = _a.sent(); if (!linkCreateContextInstance) { return [2 /*return*/]; } if (!linkCreateContextInstance.addLinkItem) { linkCreateContextInstance = ContextFactory.create(linkCreateContextInstance); } return [2 /*return*/, insertLinks(this.view, this.stateManager, this.handleMediaState, this.linkRanges, linkCreateContextInstance, this.collectionFromProvider())]; } }); }); }; this.splitMediaGroup = function () { return splitMediaGroup(_this.view); }; this.insertFileFromDataUrl = function (url, fileName) { var binaryPicker = _this.binaryPicker; assert(binaryPicker, 'Unable to insert file because media pickers have not been initialized yet'); binaryPicker.upload(url, fileName); }; this.showMediaPicker = function () { if (!_this.popupPicker) { return; } _this.popupPicker.show(); }; /** * Returns a promise that is resolved after all pending operations have been finished. * An optional timeout will cause the promise to reject if the operation takes too long * * NOTE: The promise will resolve even if some of the media have failed to process. * */ this.waitForPendingTasks = function (timeout) { var _a = _this, mediaNodes = _a.mediaNodes, stateManager = _a.stateManager; return new Promise(function (resolve, reject) { if (timeout) { setTimeout(function () { return reject(new Error("Media operations did not finish in " + timeout + " ms")); }, timeout); } var outstandingNodes = mediaNodes.length; if (!outstandingNodes) { return resolve(); } function onNodeStateChanged(state) { var status = state.status; if (MEDIA_RESOLVE_STATES.indexOf(status || '') !== -1) { onNodeStateReady(state.id); } } function onNodeStateReady(id) { outstandingNodes--; stateManager.unsubscribe(id, onNodeStateChanged); if (outstandingNodes <= 0) { resolve(); } } mediaNodes.forEach(function (_a) { var node = _a.node; var mediaNodeId = node.attrs.id; var nodeCurrentStatus = _this.getMediaNodeStateStatus(mediaNodeId); if (MEDIA_RESOLVE_STATES.indexOf(nodeCurrentStatus) !== -1) { onNodeStateReady(mediaNodeId); } else { stateManager.subscribe(mediaNodeId, onNodeStateChanged); } }); }); }; /** * Called from React UI Component when user clicks on "Delete" icon * inside of it */ this.handleMediaNodeRemoval = function (node, getPos) { removeMediaNode(_this.view, node, getPos); }; /** * Called from React UI Component on componentDidMount */ this.handleMediaNodeMount = function (node, getPos) { _this.mediaNodes.push({ node: node, getPos: getPos }); }; /** * Called from React UI Component on componentWillUnmount and componentWillReceiveProps * when React component's underlying node property is replaced with a new node */ this.handleMediaNodeUnmount = function (oldNode) { _this.mediaNodes = _this.mediaNodes.filter(function (_a) { var node = _a.node; return oldNode !== node; }); }; this.align = function (alignment, display) { if (display === void 0) { display = 'block'; } if (!_this.isMediaNodeSelection()) { return false; } var _a = _this.view.state, from = _a.selection.from, schema = _a.schema, tr = _a.tr; _this.view.dispatch(tr.setNodeType(from - 1, schema.nodes.singleImage, { alignment: alignment, display: display })); return true; }; this.findMediaNode = function (id) { var mediaNodes = _this.mediaNodes; // Array#find... no IE support return mediaNodes.reduce(function (memo, nodeWithPos) { if (memo) { return memo; } var node = nodeWithPos.node; if (node.attrs.id === id) { return nodeWithPos; } return memo; }, null); }; this.detectLinkRangesInSteps = function (tr, oldState) { var link = _this.view.state.schema.marks.link; _this.linkRanges = []; if (_this.ignoreLinks) { _this.ignoreLinks = false; return _this.linkRanges; } if (!link || !_this.allowsLinks) { return _this.linkRanges; } _this.linkRanges = detectLinkRangesInSteps(tr, link, oldState.selection.$anchor.pos); }; this.destroyPickers = function () { var pickers = _this.pickers; pickers.forEach(function (picker) { return picker.destroy(); }); pickers.splice(0, pickers.length); _this.popupPicker = undefined; _this.binaryPicker = undefined; }; this.handleMediaState = function (state) { switch (state.status) { case 'error': _this.removeNodeById(state.id); var uploadErrorHandler = _this.options.uploadErrorHandler; if (uploadErrorHandler) { uploadErrorHandler(state); } break; case 'ready': _this.stateManager.unsubscribe(state.id, _this.handleMediaState); _this.replaceNodeWithPublicId(state.id, state.publicId); break; } }; this.notifyPluginStateSubscribers = function () { _this.pluginStateChangeSubscribers.forEach(function (cb) { return cb.call(cb, _this); }); }; this.removeNodeById = function (id) { // TODO: we would like better error handling and retry support here. var mediaNodeWithPos = _this.findMediaNode(id); if (mediaNodeWithPos) { removeMediaNode(_this.view, mediaNodeWithPos.node, mediaNodeWithPos.getPos); } }; this.replaceNodeWithPublicId = function (temporaryId, publicId) { var view = _this.view; if (!view) { return; } var mediaNodeWithPos = _this.findMediaNode(temporaryId); if (!mediaNodeWithPos) { return; } var getPos = mediaNodeWithPos.getPos, mediaNode = mediaNodeWithPos.node; var newNode = view.state.schema.nodes.media.create(tslib_1.__assign({}, mediaNode.attrs, { id: publicId })); // Copy all optional attributes from old node copyOptionalAttrs(mediaNode.attrs, newNode.attrs); // replace the old node with a new one var nodePos = getPos(); var tr = view.state.tr.replaceWith(nodePos, nodePos + mediaNode.nodeSize, newNode); view.dispatch(tr.setMeta('addToHistory', false)); }; this.removeSelectedMediaNode = function () { var view = _this.view; if (_this.isMediaNodeSelection()) { var _a = view.state.selection, from_1 = _a.from, node = _a.node; removeMediaNode(view, node, function () { return from_1; }); return true; } return false; }; /** * Since we replace nodes with public id when node is finalized * stateManager contains no information for public ids */ this.getMediaNodeStateStatus = function (id) { var stateManager = _this.stateManager; var state = stateManager.getState(id); return (state && state.status) || 'ready'; }; this.options = options; this.waitForMediaUpload = options.waitForMediaUpload === undefined ? true : options.waitForMediaUpload; var nodes = state.schema.nodes; assert(nodes.media && nodes.mediaGroup, 'Editor: unable to init media plugin - media or mediaGroup node absent in schema'); this.stateManager = new DefaultMediaStateManager(); options.providerFactory.subscribe('mediaProvider', function (name, provider) { return _this.setMediaProvider(provider); }); this.errorReporter = options.errorReporter || new ErrorReporter(); } MediaPluginState.prototype.subscribe = function (cb) { this.pluginStateChangeSubscribers.push(cb); cb(this); }; MediaPluginState.prototype.unsubscribe = function (cb) { var pluginStateChangeSubscribers = this.pluginStateChangeSubscribers; var pos = pluginStateChangeSubscribers.indexOf(cb); if (pos > -1) { pluginStateChangeSubscribers.splice(pos, 1); } }; MediaPluginState.prototype.setView = function (view) { this.view = view; }; /** * This is called when media node is removed from media group node view */ MediaPluginState.prototype.cancelInFlightUpload = function (id) { var mediaNodeWithPos = this.findMediaNode(id); if (!mediaNodeWithPos) { return; } var status = this.getMediaNodeStateStatus(id); switch (status) { case 'uploading': case 'processing': this.pickers.forEach(function (picker) { return picker.cancel(id); }); } }; MediaPluginState.prototype.destroy = function () { if (this.destroyed) { return; } this.destroyed = true; var mediaNodes = this.mediaNodes; mediaNodes.splice(0, mediaNodes.length); this.destroyPickers(); }; MediaPluginState.prototype.initPickers = function (uploadParams, context) { var _this = this; if (this.destroyed) { return; } var _a = this, errorReporter = _a.errorReporter, pickers = _a.pickers, stateManager = _a.stateManager; // create pickers if they don't exist, re-use otherwise if (!pickers.length) { pickers.push(this.binaryPicker = new PickerFacade('binary', uploadParams, context, stateManager, errorReporter)); pickers.push(this.popupPicker = new PickerFacade('popup', uploadParams, context, stateManager, errorReporter)); pickers.push(this.clipboardPicker = new PickerFacade('clipboard', uploadParams, context, stateManager, errorReporter)); pickers.push(this.dropzonePicker = new PickerFacade('dropzone', uploadParams, context, stateManager, errorReporter)); pickers.forEach(function (picker) { return picker.onNewMedia(_this.insertFile); }); this.binaryPicker.onNewMedia(function (e) { return analyticsService.trackEvent('atlassian.editor.media.file.binary', e.fileMimeType ? { fileMimeType: e.fileMimeType } : {}); }); this.popupPicker.onNewMedia(function (e) { return analyticsService.trackEvent('atlassian.editor.media.file.popup', e.fileMimeType ? { fileMimeType: e.fileMimeType } : {}); }); this.clipboardPicker.onNewMedia(function (e) { return analyticsService.trackEvent('atlassian.editor.media.file.paste', e.fileMimeType ? { fileMimeType: e.fileMimeType } : {}); }); this.dropzonePicker.onNewMedia(function (e) { return analyticsService.trackEvent('atlassian.editor.media.file.drop', e.fileMimeType ? { fileMimeType: e.fileMimeType } : {}); }); } // set new upload params for the pickers pickers.forEach(function (picker) { return picker.setUploadParams(uploadParams); }); }; MediaPluginState.prototype.collectionFromProvider = function () { return this.mediaProvider && this.mediaProvider.uploadParams && this.mediaProvider.uploadParams.collection; }; MediaPluginState.prototype.isMediaNodeSelection = function () { var _a = this.view.state, selection = _a.selection, schema = _a.schema; return selection instanceof NodeSelection && selection.node.type === schema.nodes.media; }; return MediaPluginState; }()); export { MediaPluginState }; export var stateKey = new PluginKey('mediaPlugin'); export var createPlugin = function (schema, options) { return new Plugin({ state: { init: function (config, state) { return new MediaPluginState(state, options); }, apply: function (tr, pluginState, oldState, newState) { pluginState.detectLinkRangesInSteps(tr, oldState); // Ignore creating link cards during link editing var link = oldState.schema.marks.link; var _a = newState.selection.$from, nodeAfter = _a.nodeAfter, nodeBefore = _a.nodeBefore; if ((nodeAfter && link.isInSet(nodeAfter.marks)) || (nodeBefore && link.isInSet(nodeBefore.marks))) { pluginState.ignoreLinks = true; } // NOTE: We're not calling passing new state to the Editor, because we depend on the view.state reference // throughout the lifetime of view. We injected the view into the plugin state, because we dispatch() // transformations from within the plugin state (i.e. when adding a new file). return pluginState; } }, key: stateKey, view: function (view) { var pluginState = stateKey.getState(view.state); pluginState.setView(view); return { update: function (view, prevState) { pluginState.insertLinks(); } }; }, props: { nodeViews: { mediaGroup: nodeViewFactory(options.providerFactory, { mediaGroup: ReactMediaGroupNode, media: ReactMediaNode, }, true), singleImage: nodeViewFactory(options.providerFactory, { singleImage: ReactSingleImageNode, media: ReactMediaNode, }, true), }, handleTextInput: function (view, from, to, text) { var pluginState = stateKey.getState(view.state); pluginState.splitMediaGroup(); return false; } } }); }; var plugins = function (schema, options) { return [createPlugin(schema, options), keymapPlugin(schema)].filter(function (plugin) { return !!plugin; }); }; export default plugins; //# sourceMappingURL=index.js.map