UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

257 lines (249 loc) 14.4 kB
"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(); }; };