@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
257 lines (249 loc) • 14.4 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.syncBlockNodeView = exports.SyncBlock = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _get2 = _interopRequireDefault(require("@babel/runtime/helpers/get"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _react = _interopRequireDefault(require("react"));
var _analytics = require("@atlaskit/editor-common/analytics");
var _errorBoundary = require("@atlaskit/editor-common/error-boundary");
var _reactNodeView = _interopRequireDefault(require("@atlaskit/editor-common/react-node-view"));
var _syncBlock = require("@atlaskit/editor-common/sync-block");
var _state = require("@atlaskit/editor-prosemirror/state");
var _editorCommands = require("../editor-commands");
var _SyncBlockRendererWrapper = require("../ui/SyncBlockRendererWrapper");
var _SyncBlockSSRReactContextsProvider = require("../ui/SyncBlockSSRReactContextsProvider");
function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2.default)(o), (0, _possibleConstructorReturn2.default)(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2.default)(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
function _superPropGet(t, o, e, r) { var p = (0, _get2.default)((0, _getPrototypeOf2.default)(1 & r ? t.prototype : t), o, e); return 2 & r && "function" == typeof p ? function (t) { return p.apply(e, t); } : p; } // oxlint-disable-next-line import/no-duplicates
// Event types that should be intercepted (returned as handled) when they
// originate inside the sync block content area, so ProseMirror does not
// convert them into node-level selections or drag operations and the browser
// can perform native text selection/cut instead.
var STOPPED_EVENT_TYPES = ['mousedown', 'mousemove', 'mouseup', 'click', 'dblclick', 'selectstart'];
var SyncBlock = exports.SyncBlock = /*#__PURE__*/function (_ReactNodeView) {
function SyncBlock(props) {
var _this;
(0, _classCallCheck2.default)(this, SyncBlock);
_this = _callSuper(this, SyncBlock, [props.node, props.view, props.getPos, props.portalProviderAPI, props.eventDispatcher, props]);
// Stable callback references — defined as arrow properties so they keep a
// fixed identity across render() calls, avoiding defeats of React.memo.
// The experiment gate lives in render(); these are always available.
(0, _defineProperty2.default)(_this, "removeSyncBlockStable", function () {
var pos = _this.getPos();
if (pos !== undefined) {
(0, _editorCommands.removeSyncedBlockAtPos)(_this.api, pos);
}
});
(0, _defineProperty2.default)(_this, "fetchSyncBlockSourceInfoStable", function (sourceAri) {
var _this$api$syncedBlock, _this$api;
// store is guaranteed non-null: render() guards on syncBlockStore
// before these callbacks can be invoked.
var store = (_this$api$syncedBlock = (_this$api = _this.api) === null || _this$api === void 0 || (_this$api = _this$api.syncedBlock) === null || _this$api === void 0 || (_this$api = _this$api.sharedState.currentState()) === null || _this$api === void 0 ? void 0 : _this$api.syncBlockStore) !== null && _this$api$syncedBlock !== void 0 ? _this$api$syncedBlock : _this.syncBlockStore;
return store ? store.referenceManager.fetchSyncBlockSourceInfoBySourceAri(sourceAri) : Promise.resolve(undefined);
});
_this.options = props.options;
_this.api = props.api;
_this.syncBlockStore = props.syncBlockStore;
_this.intl = props.intl;
return _this;
}
(0, _inherits2.default)(SyncBlock, _ReactNodeView);
return (0, _createClass2.default)(SyncBlock, [{
key: "createDomRef",
value:
// Stored reference so the listener can be removed in destroy() to
// avoid a memory leak on every nodeview destruction.
function createDomRef() {
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- NodeView DOM must be created against active runtime document
var domRef = document.createElement('div');
domRef.classList.add(_syncBlock.SyncBlockSharedCssClassName.prefix);
// Prevent native browser drag on the contentEditable="false" wrapper.
// Without this, clicking in empty space (outside the contentEditable
// renderer but inside the domRef) initiates a native element drag.
domRef.draggable = false;
this.dragStartHandler = function (e) {
e.preventDefault();
};
// eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop, @repo/internal/dom-events/no-unsafe-event-listeners
domRef.addEventListener('dragstart', this.dragStartHandler);
return domRef;
}
/**
* Allow mouse and selection events inside the renderer content to pass
* through to the browser so that users can select and copy text within a
* reference sync block.
*
* Events that originate inside the sync block content area (but not the label)
* are stopped so ProseMirror does not intercept them for node-level selection.
* This includes the full click-drag cycle (mousedown, mousemove, mouseup),
* click, dblclick, selectstart and cut. The `cut` event is stopped because
* mousedown explicitly sets a NodeSelection on the sync block — without
* stopping `cut`, a subsequent Ctrl+X would cause ProseMirror to delete the
* entire sync block node instead of cutting the user's text selection.
*
* Copy events are conditionally stopped: when the user has an active native
* text selection inside the renderer, `copy` is stopped so the browser handles
* it natively (copying the selected text). When there is no text selection
* (just a PM NodeSelection), `copy` is NOT stopped so ProseMirror's copy
* handler runs and sets the "sync-block-copied" flag for the reference paste flow.
*
* Events on the SyncBlockLabel are left for ProseMirror to handle, preserving
* label click interactions and the floating toolbar.
*
* The renderer wrapper sets contentEditable="true" to create a re-editable
* island inside ProseMirror's contentEditable="false" nodeview, enabling
* native text selection and preventing browser drag behaviour.
*/
}, {
key: "stopEvent",
value: function stopEvent(event) {
var target = event.target;
if (!(target instanceof Element)) {
return false;
}
// Stop events inside the sync block content area, but not on the
// SyncBlockLabel (to preserve label click interactions).
if (target.closest(".".concat(_syncBlock.SyncBlockSharedCssClassName.prefix)) && !target.closest(".".concat(_syncBlock.SyncBlockLabelSharedCssClassName.labelClassName))) {
var eventType = event.type;
// Stop `copy` only when there is an active native text selection
// inside the renderer. This lets the browser handle text copy
// natively. When there is no text selection (just a PM
// NodeSelection), we let PM handle the copy event so the
// "sync-block-copied" flag is set for the reference paste flow.
if (eventType === 'copy') {
var selection = window.getSelection();
return !!(selection && selection.toString().length > 0);
}
// For `cut`: when text is selected inside the renderer, stop the
// event and prevent default to avoid the browser removing text from
// the read-only content. When no text is selected (NodeSelection),
// let PM handle it so cut deletes the sync block as expected.
if (eventType === 'cut') {
var _selection = window.getSelection();
if (_selection && _selection.toString().length > 0) {
event.preventDefault();
return true;
}
return false;
}
// Stop keyboard events that would cause PM to replace the
// NodeSelection with typed text (deleting the sync block).
// Allow modifier-key combos (Cmd+C, Cmd+A, etc.) through.
// Allow Delete/Backspace through so PM's delete handler can
// process them (e.g. show offline error flag).
if ((eventType === 'keydown' || eventType === 'keypress') && event instanceof KeyboardEvent && !event.metaKey && !event.ctrlKey && event.key !== 'Delete' && event.key !== 'Backspace') {
return true;
}
if (STOPPED_EVENT_TYPES.includes(eventType)) {
// Ensure the syncBlock has a NodeSelection so the floating
// toolbar is visible while the user interacts with the renderer.
// stopEvent prevents PM from processing the mousedown, so we
// need to explicitly set the selection ourselves.
if (eventType === 'mousedown' && !(this.view.state.selection instanceof _state.NodeSelection && this.view.state.selection.node === this.node)) {
if (typeof this.getPos === 'function') {
var pos = this.getPos();
if (typeof pos === 'number') {
try {
var tr = this.view.state.tr;
this.view.dispatch(tr.setSelection(_state.NodeSelection.create(tr.doc, pos)));
} catch (_unused) {
// pos no longer valid — leave selection unchanged
}
}
}
}
return true;
}
}
return false;
}
}, {
key: "validUpdate",
value: function validUpdate(currentNode, newNode) {
// Only consider as the valid update if the localId and resourceId are the same
// This prevents PM reusing the same node view for different sync block node in live page transition
return currentNode.attrs.localId === newNode.attrs.localId && currentNode.attrs.resourceId === newNode.attrs.resourceId;
}
}, {
key: "update",
value: function update(node, decorations, innerDecorations) {
return _superPropGet(SyncBlock, "update", this, 3)([node, decorations, innerDecorations, this.validUpdate]);
}
}, {
key: "render",
value: function render(_ref) {
var _this$options, _this$api$syncedBlock2, _this$api2, _this$api3, _this$options2;
var getPos = _ref.getPos;
if (!((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.syncedBlockRenderer)) {
return null;
}
var _this$node$attrs = this.node.attrs,
resourceId = _this$node$attrs.resourceId,
localId = _this$node$attrs.localId;
if (!resourceId || !localId) {
return null;
}
var syncBlockStore = (_this$api$syncedBlock2 = (_this$api2 = this.api) === null || _this$api2 === void 0 || (_this$api2 = _this$api2.syncedBlock) === null || _this$api2 === void 0 || (_this$api2 = _this$api2.sharedState.currentState()) === null || _this$api2 === void 0 ? void 0 : _this$api2.syncBlockStore) !== null && _this$api$syncedBlock2 !== void 0 ? _this$api$syncedBlock2 : this.syncBlockStore;
if (!syncBlockStore) {
return null;
}
// get document node from data provider
return /*#__PURE__*/_react.default.createElement(_SyncBlockSSRReactContextsProvider.SyncBlockSSRReactContextsProvider, {
intl: this.intl
}, /*#__PURE__*/_react.default.createElement(_errorBoundary.ErrorBoundary, {
component: _analytics.ACTION_SUBJECT.SYNCED_BLOCK,
dispatchAnalyticsEvent: (_this$api3 = this.api) === null || _this$api3 === void 0 || (_this$api3 = _this$api3.analytics) === null || _this$api3 === void 0 ? void 0 : _this$api3.actions.fireAnalyticsEvent,
fallbackComponent: null
}, /*#__PURE__*/_react.default.createElement(_syncBlock.SyncBlockActionsProvider, {
removeSyncBlock: this.removeSyncBlockStable,
fetchSyncBlockSourceInfo: this.fetchSyncBlockSourceInfoStable
}, /*#__PURE__*/_react.default.createElement(_SyncBlockRendererWrapper.SyncBlockRendererWrapper, {
localId: localId,
resourceId: resourceId,
node: this.node,
syncBlockStore: syncBlockStore,
syncedBlockRenderer: (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.syncedBlockRenderer,
api: this.api
}))));
}
}, {
key: "destroy",
value: function destroy() {
var _this$unsubscribe;
(_this$unsubscribe = this.unsubscribe) === null || _this$unsubscribe === void 0 || _this$unsubscribe.call(this);
if (this.dragStartHandler) {
var _this$dom;
// eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop, @repo/internal/dom-events/no-unsafe-event-listeners
(_this$dom = this.dom) === null || _this$dom === void 0 || _this$dom.removeEventListener('dragstart', this.dragStartHandler);
this.dragStartHandler = undefined;
}
_superPropGet(SyncBlock, "destroy", this, 3)([]);
}
}]);
}(_reactNodeView.default);
var syncBlockNodeView = exports.syncBlockNodeView = function syncBlockNodeView(_ref2) {
var options = _ref2.options,
pmPluginFactoryParams = _ref2.pmPluginFactoryParams,
api = _ref2.api;
return function (node, view, getPos) {
var portalProviderAPI = pmPluginFactoryParams.portalProviderAPI,
eventDispatcher = pmPluginFactoryParams.eventDispatcher;
return new SyncBlock({
api: api,
options: options,
node: node,
view: view,
getPos: getPos,
portalProviderAPI: portalProviderAPI,
eventDispatcher: eventDispatcher
}).init();
};
};