@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
446 lines • 21.5 kB
JavaScript
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