@atlaskit/editor-plugin-synced-block 9.0.1 → 9.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 9.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 9.0.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [`ed89ab85318b9`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ed89ab85318b9) -
14
+ [ux] Allow text selection and copying within reference synced blocks in the editor
15
+
3
16
  ## 9.0.1
4
17
 
5
18
  ### Patch Changes
@@ -17,6 +17,8 @@ var _analytics = require("@atlaskit/editor-common/analytics");
17
17
  var _errorBoundary = require("@atlaskit/editor-common/error-boundary");
18
18
  var _reactNodeView = _interopRequireDefault(require("@atlaskit/editor-common/react-node-view"));
19
19
  var _syncBlock = require("@atlaskit/editor-common/sync-block");
20
+ var _state = require("@atlaskit/editor-prosemirror/state");
21
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
20
22
  var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
21
23
  var _editorCommands = require("../editor-commands");
22
24
  var _SyncBlockRendererWrapper = require("../ui/SyncBlockRendererWrapper");
@@ -24,6 +26,11 @@ var _SyncBlockSSRReactContextsProvider = require("../ui/SyncBlockSSRReactContext
24
26
  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)); }
25
27
  function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
26
28
  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; }
29
+ // Event types that should be intercepted (returned as handled) when they
30
+ // originate inside the sync block content area, so ProseMirror does not
31
+ // convert them into node-level selections or drag operations and the browser
32
+ // can perform native text selection/cut instead.
33
+ var STOPPED_EVENT_TYPES = ['mousedown', 'mousemove', 'mouseup', 'click', 'dblclick', 'selectstart'];
27
34
  var SyncBlock = exports.SyncBlock = /*#__PURE__*/function (_ReactNodeView) {
28
35
  function SyncBlock(props) {
29
36
  var _this;
@@ -54,12 +61,124 @@ var SyncBlock = exports.SyncBlock = /*#__PURE__*/function (_ReactNodeView) {
54
61
  (0, _inherits2.default)(SyncBlock, _ReactNodeView);
55
62
  return (0, _createClass2.default)(SyncBlock, [{
56
63
  key: "createDomRef",
57
- value: function createDomRef() {
64
+ value:
65
+ // Stored reference so the listener can be removed in destroy() to
66
+ // avoid a memory leak on every nodeview destruction.
67
+
68
+ function createDomRef() {
58
69
  // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- NodeView DOM must be created against active runtime document
59
70
  var domRef = document.createElement('div');
60
71
  domRef.classList.add(_syncBlock.SyncBlockSharedCssClassName.prefix);
72
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
73
+ // Prevent native browser drag on the contentEditable="false" wrapper.
74
+ // Without this, clicking in empty space (outside the contentEditable
75
+ // renderer but inside the domRef) initiates a native element drag.
76
+ domRef.draggable = false;
77
+ this.dragStartHandler = function (e) {
78
+ e.preventDefault();
79
+ };
80
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop, @repo/internal/dom-events/no-unsafe-event-listeners
81
+ domRef.addEventListener('dragstart', this.dragStartHandler);
82
+ }
61
83
  return domRef;
62
84
  }
85
+
86
+ /**
87
+ * Allow mouse and selection events inside the renderer content to pass
88
+ * through to the browser so that users can select and copy text within a
89
+ * reference sync block.
90
+ *
91
+ * Events that originate inside the sync block content area (but not the label)
92
+ * are stopped so ProseMirror does not intercept them for node-level selection.
93
+ * This includes the full click-drag cycle (mousedown, mousemove, mouseup),
94
+ * click, dblclick, selectstart and cut. The `cut` event is stopped because
95
+ * mousedown explicitly sets a NodeSelection on the sync block — without
96
+ * stopping `cut`, a subsequent Ctrl+X would cause ProseMirror to delete the
97
+ * entire sync block node instead of cutting the user's text selection.
98
+ *
99
+ * Copy events are conditionally stopped: when the user has an active native
100
+ * text selection inside the renderer, `copy` is stopped so the browser handles
101
+ * it natively (copying the selected text). When there is no text selection
102
+ * (just a PM NodeSelection), `copy` is NOT stopped so ProseMirror's copy
103
+ * handler runs and sets the "sync-block-copied" flag for the reference paste flow.
104
+ *
105
+ * Events on the SyncBlockLabel are left for ProseMirror to handle, preserving
106
+ * label click interactions and the floating toolbar.
107
+ *
108
+ * The renderer wrapper sets contentEditable="true" to create a re-editable
109
+ * island inside ProseMirror's contentEditable="false" nodeview, enabling
110
+ * native text selection and preventing browser drag behaviour.
111
+ */
112
+ }, {
113
+ key: "stopEvent",
114
+ value: function stopEvent(event) {
115
+ if (!(0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
116
+ return false;
117
+ }
118
+ var target = event.target;
119
+ if (!(target instanceof Element)) {
120
+ return false;
121
+ }
122
+
123
+ // Stop events inside the sync block content area, but not on the
124
+ // SyncBlockLabel (to preserve label click interactions).
125
+ if (target.closest(".".concat(_syncBlock.SyncBlockSharedCssClassName.prefix)) && !target.closest(".".concat(_syncBlock.SyncBlockLabelSharedCssClassName.labelClassName))) {
126
+ var eventType = event.type;
127
+
128
+ // Stop `copy` only when there is an active native text selection
129
+ // inside the renderer. This lets the browser handle text copy
130
+ // natively. When there is no text selection (just a PM
131
+ // NodeSelection), we let PM handle the copy event so the
132
+ // "sync-block-copied" flag is set for the reference paste flow.
133
+ if (eventType === 'copy') {
134
+ var selection = window.getSelection();
135
+ return !!(selection && selection.toString().length > 0);
136
+ }
137
+
138
+ // For `cut`: when text is selected inside the renderer, stop the
139
+ // event and prevent default to avoid the browser removing text from
140
+ // the read-only content. When no text is selected (NodeSelection),
141
+ // let PM handle it so cut deletes the sync block as expected.
142
+ if (eventType === 'cut') {
143
+ var _selection = window.getSelection();
144
+ if (_selection && _selection.toString().length > 0) {
145
+ event.preventDefault();
146
+ return true;
147
+ }
148
+ return false;
149
+ }
150
+
151
+ // Stop keyboard events that would cause PM to replace the
152
+ // NodeSelection with typed text (deleting the sync block).
153
+ // Allow modifier-key combos (Cmd+C, Cmd+A, etc.) through.
154
+ // Allow Delete/Backspace through so PM's delete handler can
155
+ // process them (e.g. show offline error flag).
156
+ if ((eventType === 'keydown' || eventType === 'keypress') && event instanceof KeyboardEvent && !event.metaKey && !event.ctrlKey && event.key !== 'Delete' && event.key !== 'Backspace') {
157
+ return true;
158
+ }
159
+ if (STOPPED_EVENT_TYPES.includes(eventType)) {
160
+ // Ensure the syncBlock has a NodeSelection so the floating
161
+ // toolbar is visible while the user interacts with the renderer.
162
+ // stopEvent prevents PM from processing the mousedown, so we
163
+ // need to explicitly set the selection ourselves.
164
+ if (eventType === 'mousedown' && !(this.view.state.selection instanceof _state.NodeSelection && this.view.state.selection.node === this.node)) {
165
+ if (typeof this.getPos === 'function') {
166
+ var pos = this.getPos();
167
+ if (typeof pos === 'number') {
168
+ try {
169
+ var tr = this.view.state.tr;
170
+ this.view.dispatch(tr.setSelection(_state.NodeSelection.create(tr.doc, pos)));
171
+ } catch (_unused) {
172
+ // pos no longer valid — leave selection unchanged
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return true;
178
+ }
179
+ }
180
+ return false;
181
+ }
63
182
  }, {
64
183
  key: "validUpdate",
65
184
  value: function validUpdate(currentNode, newNode) {
@@ -135,6 +254,12 @@ var SyncBlock = exports.SyncBlock = /*#__PURE__*/function (_ReactNodeView) {
135
254
  value: function destroy() {
136
255
  var _this$unsubscribe;
137
256
  (_this$unsubscribe = this.unsubscribe) === null || _this$unsubscribe === void 0 || _this$unsubscribe.call(this);
257
+ if (this.dragStartHandler) {
258
+ var _this$dom;
259
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop, @repo/internal/dom-events/no-unsafe-event-listeners
260
+ (_this$dom = this.dom) === null || _this$dom === void 0 || _this$dom.removeEventListener('dragstart', this.dragStartHandler);
261
+ this.dragStartHandler = undefined;
262
+ }
138
263
  _superPropGet(SyncBlock, "destroy", this, 3)([]);
139
264
  }
140
265
  }]);
@@ -1,14 +1,16 @@
1
1
  "use strict";
2
2
 
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
+ var _typeof = require("@babel/runtime/helpers/typeof");
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.SyncBlockRendererWrapper = void 0;
8
- var _react = _interopRequireDefault(require("react"));
8
+ var _react = _interopRequireWildcard(require("react"));
9
9
  var _syncBlock = require("@atlaskit/editor-common/sync-block");
10
10
  var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
11
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
11
12
  var _SyncBlockLabel = require("./SyncBlockLabel");
13
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
12
14
  var SyncBlockRendererWrapperDataId = 'sync-block-plugin-renderer-wrapper';
13
15
  var SyncBlockRendererWrapperComponent = function SyncBlockRendererWrapperComponent(_ref) {
14
16
  var _api$analytics, _syncBlockFetchResult, _syncBlockFetchResult2, _syncBlockFetchResult3;
@@ -23,11 +25,32 @@ var SyncBlockRendererWrapperComponent = function SyncBlockRendererWrapperCompone
23
25
  var contentUpdatedAt = syncBlockFetchResult === null || syncBlockFetchResult === void 0 || (_syncBlockFetchResult = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult === void 0 || (_syncBlockFetchResult = _syncBlockFetchResult.data) === null || _syncBlockFetchResult === void 0 ? void 0 : _syncBlockFetchResult.contentUpdatedAt;
24
26
  var isUnpublishedBlock = ((_syncBlockFetchResult2 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult2 === void 0 || (_syncBlockFetchResult2 = _syncBlockFetchResult2.data) === null || _syncBlockFetchResult2 === void 0 ? void 0 : _syncBlockFetchResult2.status) === 'unpublished';
25
27
  var isUnsyncedBlock = isUnpublishedBlock || (syncBlockFetchResult === null || syncBlockFetchResult === void 0 || (_syncBlockFetchResult3 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult3 === void 0 || (_syncBlockFetchResult3 = _syncBlockFetchResult3.error) === null || _syncBlockFetchResult3 === void 0 ? void 0 : _syncBlockFetchResult3.type) === _editorSyncedBlockProvider.SyncBlockError.NotFound;
28
+ var isTextSelectionEnabled = (0, _platformFeatureFlags.fg)('platform_synced_block_patch_14');
29
+
30
+ // Prevent editing in the contentEditable renderer wrapper. We set
31
+ // contentEditable="true" to enable text selection (creating an editable
32
+ // island inside ProseMirror's contentEditable="false" nodeview wrapper),
33
+ // but users must not be able to type into or modify the renderer content.
34
+ var preventInput = (0, _react.useCallback)(function (e) {
35
+ e.preventDefault();
36
+ }, []);
26
37
  return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("div", {
27
38
  "data-testid": SyncBlockRendererWrapperDataId
28
39
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
29
40
  ,
30
- className: _syncBlock.SyncBlockSharedCssClassName.renderer
41
+ className: _syncBlock.SyncBlockSharedCssClassName.renderer,
42
+ contentEditable: isTextSelectionEnabled || undefined,
43
+ suppressContentEditableWarning: true
44
+ // Prevent the contentEditable div from being keyboard-focusable.
45
+ // It is only used to enable text selection, not as an input target.
46
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
47
+ ,
48
+ tabIndex: isTextSelectionEnabled ? -1 : undefined,
49
+ onBeforeInput: isTextSelectionEnabled ? preventInput : undefined,
50
+ onPaste: isTextSelectionEnabled ? preventInput : undefined
51
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop
52
+ ,
53
+ onDrop: isTextSelectionEnabled ? preventInput : undefined
31
54
  }, syncedBlockRenderer({
32
55
  syncBlockFetchResult: syncBlockFetchResult,
33
56
  api: api
@@ -3,11 +3,19 @@ import React from 'react';
3
3
  import { ACTION_SUBJECT } from '@atlaskit/editor-common/analytics';
4
4
  import { ErrorBoundary } from '@atlaskit/editor-common/error-boundary';
5
5
  import ReactNodeView from '@atlaskit/editor-common/react-node-view';
6
- import { SyncBlockSharedCssClassName, SyncBlockActionsProvider } from '@atlaskit/editor-common/sync-block';
6
+ import { SyncBlockSharedCssClassName, SyncBlockLabelSharedCssClassName, SyncBlockActionsProvider } from '@atlaskit/editor-common/sync-block';
7
+ import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
8
+ import { fg } from '@atlaskit/platform-feature-flags';
7
9
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
8
10
  import { removeSyncedBlockAtPos } from '../editor-commands';
9
11
  import { SyncBlockRendererWrapper } from '../ui/SyncBlockRendererWrapper';
10
12
  import { SyncBlockSSRReactContextsProvider } from '../ui/SyncBlockSSRReactContextsProvider';
13
+
14
+ // Event types that should be intercepted (returned as handled) when they
15
+ // originate inside the sync block content area, so ProseMirror does not
16
+ // convert them into node-level selections or drag operations and the browser
17
+ // can perform native text selection/cut instead.
18
+ const STOPPED_EVENT_TYPES = ['mousedown', 'mousemove', 'mouseup', 'click', 'dblclick', 'selectstart'];
11
19
  export class SyncBlock extends ReactNodeView {
12
20
  constructor(props) {
13
21
  super(props.node, props.view, props.getPos, props.portalProviderAPI, props.eventDispatcher, props);
@@ -32,12 +40,123 @@ export class SyncBlock extends ReactNodeView {
32
40
  this.syncBlockStore = props.syncBlockStore;
33
41
  this.intl = props.intl;
34
42
  }
43
+ // Stored reference so the listener can be removed in destroy() to
44
+ // avoid a memory leak on every nodeview destruction.
45
+
35
46
  createDomRef() {
36
47
  // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- NodeView DOM must be created against active runtime document
37
48
  const domRef = document.createElement('div');
38
49
  domRef.classList.add(SyncBlockSharedCssClassName.prefix);
50
+ if (fg('platform_synced_block_patch_14')) {
51
+ // Prevent native browser drag on the contentEditable="false" wrapper.
52
+ // Without this, clicking in empty space (outside the contentEditable
53
+ // renderer but inside the domRef) initiates a native element drag.
54
+ domRef.draggable = false;
55
+ this.dragStartHandler = e => {
56
+ e.preventDefault();
57
+ };
58
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop, @repo/internal/dom-events/no-unsafe-event-listeners
59
+ domRef.addEventListener('dragstart', this.dragStartHandler);
60
+ }
39
61
  return domRef;
40
62
  }
63
+
64
+ /**
65
+ * Allow mouse and selection events inside the renderer content to pass
66
+ * through to the browser so that users can select and copy text within a
67
+ * reference sync block.
68
+ *
69
+ * Events that originate inside the sync block content area (but not the label)
70
+ * are stopped so ProseMirror does not intercept them for node-level selection.
71
+ * This includes the full click-drag cycle (mousedown, mousemove, mouseup),
72
+ * click, dblclick, selectstart and cut. The `cut` event is stopped because
73
+ * mousedown explicitly sets a NodeSelection on the sync block — without
74
+ * stopping `cut`, a subsequent Ctrl+X would cause ProseMirror to delete the
75
+ * entire sync block node instead of cutting the user's text selection.
76
+ *
77
+ * Copy events are conditionally stopped: when the user has an active native
78
+ * text selection inside the renderer, `copy` is stopped so the browser handles
79
+ * it natively (copying the selected text). When there is no text selection
80
+ * (just a PM NodeSelection), `copy` is NOT stopped so ProseMirror's copy
81
+ * handler runs and sets the "sync-block-copied" flag for the reference paste flow.
82
+ *
83
+ * Events on the SyncBlockLabel are left for ProseMirror to handle, preserving
84
+ * label click interactions and the floating toolbar.
85
+ *
86
+ * The renderer wrapper sets contentEditable="true" to create a re-editable
87
+ * island inside ProseMirror's contentEditable="false" nodeview, enabling
88
+ * native text selection and preventing browser drag behaviour.
89
+ */
90
+ stopEvent(event) {
91
+ if (!fg('platform_synced_block_patch_14')) {
92
+ return false;
93
+ }
94
+ const target = event.target;
95
+ if (!(target instanceof Element)) {
96
+ return false;
97
+ }
98
+
99
+ // Stop events inside the sync block content area, but not on the
100
+ // SyncBlockLabel (to preserve label click interactions).
101
+ if (target.closest(`.${SyncBlockSharedCssClassName.prefix}`) && !target.closest(`.${SyncBlockLabelSharedCssClassName.labelClassName}`)) {
102
+ const eventType = event.type;
103
+
104
+ // Stop `copy` only when there is an active native text selection
105
+ // inside the renderer. This lets the browser handle text copy
106
+ // natively. When there is no text selection (just a PM
107
+ // NodeSelection), we let PM handle the copy event so the
108
+ // "sync-block-copied" flag is set for the reference paste flow.
109
+ if (eventType === 'copy') {
110
+ const selection = window.getSelection();
111
+ return !!(selection && selection.toString().length > 0);
112
+ }
113
+
114
+ // For `cut`: when text is selected inside the renderer, stop the
115
+ // event and prevent default to avoid the browser removing text from
116
+ // the read-only content. When no text is selected (NodeSelection),
117
+ // let PM handle it so cut deletes the sync block as expected.
118
+ if (eventType === 'cut') {
119
+ const selection = window.getSelection();
120
+ if (selection && selection.toString().length > 0) {
121
+ event.preventDefault();
122
+ return true;
123
+ }
124
+ return false;
125
+ }
126
+
127
+ // Stop keyboard events that would cause PM to replace the
128
+ // NodeSelection with typed text (deleting the sync block).
129
+ // Allow modifier-key combos (Cmd+C, Cmd+A, etc.) through.
130
+ // Allow Delete/Backspace through so PM's delete handler can
131
+ // process them (e.g. show offline error flag).
132
+ if ((eventType === 'keydown' || eventType === 'keypress') && event instanceof KeyboardEvent && !event.metaKey && !event.ctrlKey && event.key !== 'Delete' && event.key !== 'Backspace') {
133
+ return true;
134
+ }
135
+ if (STOPPED_EVENT_TYPES.includes(eventType)) {
136
+ // Ensure the syncBlock has a NodeSelection so the floating
137
+ // toolbar is visible while the user interacts with the renderer.
138
+ // stopEvent prevents PM from processing the mousedown, so we
139
+ // need to explicitly set the selection ourselves.
140
+ if (eventType === 'mousedown' && !(this.view.state.selection instanceof NodeSelection && this.view.state.selection.node === this.node)) {
141
+ if (typeof this.getPos === 'function') {
142
+ const pos = this.getPos();
143
+ if (typeof pos === 'number') {
144
+ try {
145
+ const {
146
+ tr
147
+ } = this.view.state;
148
+ this.view.dispatch(tr.setSelection(NodeSelection.create(tr.doc, pos)));
149
+ } catch {
150
+ // pos no longer valid — leave selection unchanged
151
+ }
152
+ }
153
+ }
154
+ }
155
+ return true;
156
+ }
157
+ }
158
+ return false;
159
+ }
41
160
  validUpdate(currentNode, newNode) {
42
161
  // Only consider as the valid update if the localId and resourceId are the same
43
162
  // This prevents PM reusing the same node view for different sync block node in live page transition
@@ -100,6 +219,12 @@ export class SyncBlock extends ReactNodeView {
100
219
  destroy() {
101
220
  var _this$unsubscribe;
102
221
  (_this$unsubscribe = this.unsubscribe) === null || _this$unsubscribe === void 0 ? void 0 : _this$unsubscribe.call(this);
222
+ if (this.dragStartHandler) {
223
+ var _this$dom;
224
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop, @repo/internal/dom-events/no-unsafe-event-listeners
225
+ (_this$dom = this.dom) === null || _this$dom === void 0 ? void 0 : _this$dom.removeEventListener('dragstart', this.dragStartHandler);
226
+ this.dragStartHandler = undefined;
227
+ }
103
228
  super.destroy();
104
229
  }
105
230
  }
@@ -1,6 +1,7 @@
1
- import React from 'react';
1
+ import React, { useCallback } from 'react';
2
2
  import { SyncBlockSharedCssClassName } from '@atlaskit/editor-common/sync-block';
3
3
  import { SyncBlockError, useFetchSyncBlockData, useFetchSyncBlockTitle } from '@atlaskit/editor-synced-block-provider';
4
+ import { fg } from '@atlaskit/platform-feature-flags';
4
5
  import { SyncBlockLabel } from './SyncBlockLabel';
5
6
  const SyncBlockRendererWrapperDataId = 'sync-block-plugin-renderer-wrapper';
6
7
  const SyncBlockRendererWrapperComponent = ({
@@ -17,11 +18,32 @@ const SyncBlockRendererWrapperComponent = ({
17
18
  const contentUpdatedAt = syncBlockFetchResult === null || syncBlockFetchResult === void 0 ? void 0 : (_syncBlockFetchResult = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult === void 0 ? void 0 : (_syncBlockFetchResult2 = _syncBlockFetchResult.data) === null || _syncBlockFetchResult2 === void 0 ? void 0 : _syncBlockFetchResult2.contentUpdatedAt;
18
19
  const isUnpublishedBlock = ((_syncBlockFetchResult3 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult3 === void 0 ? void 0 : (_syncBlockFetchResult4 = _syncBlockFetchResult3.data) === null || _syncBlockFetchResult4 === void 0 ? void 0 : _syncBlockFetchResult4.status) === 'unpublished';
19
20
  const isUnsyncedBlock = isUnpublishedBlock || (syncBlockFetchResult === null || syncBlockFetchResult === void 0 ? void 0 : (_syncBlockFetchResult5 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult5 === void 0 ? void 0 : (_syncBlockFetchResult6 = _syncBlockFetchResult5.error) === null || _syncBlockFetchResult6 === void 0 ? void 0 : _syncBlockFetchResult6.type) === SyncBlockError.NotFound;
21
+ const isTextSelectionEnabled = fg('platform_synced_block_patch_14');
22
+
23
+ // Prevent editing in the contentEditable renderer wrapper. We set
24
+ // contentEditable="true" to enable text selection (creating an editable
25
+ // island inside ProseMirror's contentEditable="false" nodeview wrapper),
26
+ // but users must not be able to type into or modify the renderer content.
27
+ const preventInput = useCallback(e => {
28
+ e.preventDefault();
29
+ }, []);
20
30
  return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
21
31
  "data-testid": SyncBlockRendererWrapperDataId
22
32
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
23
33
  ,
24
- className: SyncBlockSharedCssClassName.renderer
34
+ className: SyncBlockSharedCssClassName.renderer,
35
+ contentEditable: isTextSelectionEnabled || undefined,
36
+ suppressContentEditableWarning: true
37
+ // Prevent the contentEditable div from being keyboard-focusable.
38
+ // It is only used to enable text selection, not as an input target.
39
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
40
+ ,
41
+ tabIndex: isTextSelectionEnabled ? -1 : undefined,
42
+ onBeforeInput: isTextSelectionEnabled ? preventInput : undefined,
43
+ onPaste: isTextSelectionEnabled ? preventInput : undefined
44
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop
45
+ ,
46
+ onDrop: isTextSelectionEnabled ? preventInput : undefined
25
47
  }, syncedBlockRenderer({
26
48
  syncBlockFetchResult,
27
49
  api
@@ -12,11 +12,19 @@ import React from 'react';
12
12
  import { ACTION_SUBJECT } from '@atlaskit/editor-common/analytics';
13
13
  import { ErrorBoundary } from '@atlaskit/editor-common/error-boundary';
14
14
  import ReactNodeView from '@atlaskit/editor-common/react-node-view';
15
- import { SyncBlockSharedCssClassName, SyncBlockActionsProvider } from '@atlaskit/editor-common/sync-block';
15
+ import { SyncBlockSharedCssClassName, SyncBlockLabelSharedCssClassName, SyncBlockActionsProvider } from '@atlaskit/editor-common/sync-block';
16
+ import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
17
+ import { fg } from '@atlaskit/platform-feature-flags';
16
18
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
17
19
  import { removeSyncedBlockAtPos } from '../editor-commands';
18
20
  import { SyncBlockRendererWrapper } from '../ui/SyncBlockRendererWrapper';
19
21
  import { SyncBlockSSRReactContextsProvider } from '../ui/SyncBlockSSRReactContextsProvider';
22
+
23
+ // Event types that should be intercepted (returned as handled) when they
24
+ // originate inside the sync block content area, so ProseMirror does not
25
+ // convert them into node-level selections or drag operations and the browser
26
+ // can perform native text selection/cut instead.
27
+ var STOPPED_EVENT_TYPES = ['mousedown', 'mousemove', 'mouseup', 'click', 'dblclick', 'selectstart'];
20
28
  export var SyncBlock = /*#__PURE__*/function (_ReactNodeView) {
21
29
  function SyncBlock(props) {
22
30
  var _this;
@@ -47,12 +55,124 @@ export var SyncBlock = /*#__PURE__*/function (_ReactNodeView) {
47
55
  _inherits(SyncBlock, _ReactNodeView);
48
56
  return _createClass(SyncBlock, [{
49
57
  key: "createDomRef",
50
- value: function createDomRef() {
58
+ value:
59
+ // Stored reference so the listener can be removed in destroy() to
60
+ // avoid a memory leak on every nodeview destruction.
61
+
62
+ function createDomRef() {
51
63
  // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- NodeView DOM must be created against active runtime document
52
64
  var domRef = document.createElement('div');
53
65
  domRef.classList.add(SyncBlockSharedCssClassName.prefix);
66
+ if (fg('platform_synced_block_patch_14')) {
67
+ // Prevent native browser drag on the contentEditable="false" wrapper.
68
+ // Without this, clicking in empty space (outside the contentEditable
69
+ // renderer but inside the domRef) initiates a native element drag.
70
+ domRef.draggable = false;
71
+ this.dragStartHandler = function (e) {
72
+ e.preventDefault();
73
+ };
74
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop, @repo/internal/dom-events/no-unsafe-event-listeners
75
+ domRef.addEventListener('dragstart', this.dragStartHandler);
76
+ }
54
77
  return domRef;
55
78
  }
79
+
80
+ /**
81
+ * Allow mouse and selection events inside the renderer content to pass
82
+ * through to the browser so that users can select and copy text within a
83
+ * reference sync block.
84
+ *
85
+ * Events that originate inside the sync block content area (but not the label)
86
+ * are stopped so ProseMirror does not intercept them for node-level selection.
87
+ * This includes the full click-drag cycle (mousedown, mousemove, mouseup),
88
+ * click, dblclick, selectstart and cut. The `cut` event is stopped because
89
+ * mousedown explicitly sets a NodeSelection on the sync block — without
90
+ * stopping `cut`, a subsequent Ctrl+X would cause ProseMirror to delete the
91
+ * entire sync block node instead of cutting the user's text selection.
92
+ *
93
+ * Copy events are conditionally stopped: when the user has an active native
94
+ * text selection inside the renderer, `copy` is stopped so the browser handles
95
+ * it natively (copying the selected text). When there is no text selection
96
+ * (just a PM NodeSelection), `copy` is NOT stopped so ProseMirror's copy
97
+ * handler runs and sets the "sync-block-copied" flag for the reference paste flow.
98
+ *
99
+ * Events on the SyncBlockLabel are left for ProseMirror to handle, preserving
100
+ * label click interactions and the floating toolbar.
101
+ *
102
+ * The renderer wrapper sets contentEditable="true" to create a re-editable
103
+ * island inside ProseMirror's contentEditable="false" nodeview, enabling
104
+ * native text selection and preventing browser drag behaviour.
105
+ */
106
+ }, {
107
+ key: "stopEvent",
108
+ value: function stopEvent(event) {
109
+ if (!fg('platform_synced_block_patch_14')) {
110
+ return false;
111
+ }
112
+ var target = event.target;
113
+ if (!(target instanceof Element)) {
114
+ return false;
115
+ }
116
+
117
+ // Stop events inside the sync block content area, but not on the
118
+ // SyncBlockLabel (to preserve label click interactions).
119
+ if (target.closest(".".concat(SyncBlockSharedCssClassName.prefix)) && !target.closest(".".concat(SyncBlockLabelSharedCssClassName.labelClassName))) {
120
+ var eventType = event.type;
121
+
122
+ // Stop `copy` only when there is an active native text selection
123
+ // inside the renderer. This lets the browser handle text copy
124
+ // natively. When there is no text selection (just a PM
125
+ // NodeSelection), we let PM handle the copy event so the
126
+ // "sync-block-copied" flag is set for the reference paste flow.
127
+ if (eventType === 'copy') {
128
+ var selection = window.getSelection();
129
+ return !!(selection && selection.toString().length > 0);
130
+ }
131
+
132
+ // For `cut`: when text is selected inside the renderer, stop the
133
+ // event and prevent default to avoid the browser removing text from
134
+ // the read-only content. When no text is selected (NodeSelection),
135
+ // let PM handle it so cut deletes the sync block as expected.
136
+ if (eventType === 'cut') {
137
+ var _selection = window.getSelection();
138
+ if (_selection && _selection.toString().length > 0) {
139
+ event.preventDefault();
140
+ return true;
141
+ }
142
+ return false;
143
+ }
144
+
145
+ // Stop keyboard events that would cause PM to replace the
146
+ // NodeSelection with typed text (deleting the sync block).
147
+ // Allow modifier-key combos (Cmd+C, Cmd+A, etc.) through.
148
+ // Allow Delete/Backspace through so PM's delete handler can
149
+ // process them (e.g. show offline error flag).
150
+ if ((eventType === 'keydown' || eventType === 'keypress') && event instanceof KeyboardEvent && !event.metaKey && !event.ctrlKey && event.key !== 'Delete' && event.key !== 'Backspace') {
151
+ return true;
152
+ }
153
+ if (STOPPED_EVENT_TYPES.includes(eventType)) {
154
+ // Ensure the syncBlock has a NodeSelection so the floating
155
+ // toolbar is visible while the user interacts with the renderer.
156
+ // stopEvent prevents PM from processing the mousedown, so we
157
+ // need to explicitly set the selection ourselves.
158
+ if (eventType === 'mousedown' && !(this.view.state.selection instanceof NodeSelection && this.view.state.selection.node === this.node)) {
159
+ if (typeof this.getPos === 'function') {
160
+ var pos = this.getPos();
161
+ if (typeof pos === 'number') {
162
+ try {
163
+ var tr = this.view.state.tr;
164
+ this.view.dispatch(tr.setSelection(NodeSelection.create(tr.doc, pos)));
165
+ } catch (_unused) {
166
+ // pos no longer valid — leave selection unchanged
167
+ }
168
+ }
169
+ }
170
+ }
171
+ return true;
172
+ }
173
+ }
174
+ return false;
175
+ }
56
176
  }, {
57
177
  key: "validUpdate",
58
178
  value: function validUpdate(currentNode, newNode) {
@@ -128,6 +248,12 @@ export var SyncBlock = /*#__PURE__*/function (_ReactNodeView) {
128
248
  value: function destroy() {
129
249
  var _this$unsubscribe;
130
250
  (_this$unsubscribe = this.unsubscribe) === null || _this$unsubscribe === void 0 || _this$unsubscribe.call(this);
251
+ if (this.dragStartHandler) {
252
+ var _this$dom;
253
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop, @repo/internal/dom-events/no-unsafe-event-listeners
254
+ (_this$dom = this.dom) === null || _this$dom === void 0 || _this$dom.removeEventListener('dragstart', this.dragStartHandler);
255
+ this.dragStartHandler = undefined;
256
+ }
131
257
  _superPropGet(SyncBlock, "destroy", this, 3)([]);
132
258
  }
133
259
  }]);
@@ -1,6 +1,7 @@
1
- import React from 'react';
1
+ import React, { useCallback } from 'react';
2
2
  import { SyncBlockSharedCssClassName } from '@atlaskit/editor-common/sync-block';
3
3
  import { SyncBlockError, useFetchSyncBlockData, useFetchSyncBlockTitle } from '@atlaskit/editor-synced-block-provider';
4
+ import { fg } from '@atlaskit/platform-feature-flags';
4
5
  import { SyncBlockLabel } from './SyncBlockLabel';
5
6
  var SyncBlockRendererWrapperDataId = 'sync-block-plugin-renderer-wrapper';
6
7
  var SyncBlockRendererWrapperComponent = function SyncBlockRendererWrapperComponent(_ref) {
@@ -16,11 +17,32 @@ var SyncBlockRendererWrapperComponent = function SyncBlockRendererWrapperCompone
16
17
  var contentUpdatedAt = syncBlockFetchResult === null || syncBlockFetchResult === void 0 || (_syncBlockFetchResult = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult === void 0 || (_syncBlockFetchResult = _syncBlockFetchResult.data) === null || _syncBlockFetchResult === void 0 ? void 0 : _syncBlockFetchResult.contentUpdatedAt;
17
18
  var isUnpublishedBlock = ((_syncBlockFetchResult2 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult2 === void 0 || (_syncBlockFetchResult2 = _syncBlockFetchResult2.data) === null || _syncBlockFetchResult2 === void 0 ? void 0 : _syncBlockFetchResult2.status) === 'unpublished';
18
19
  var isUnsyncedBlock = isUnpublishedBlock || (syncBlockFetchResult === null || syncBlockFetchResult === void 0 || (_syncBlockFetchResult3 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult3 === void 0 || (_syncBlockFetchResult3 = _syncBlockFetchResult3.error) === null || _syncBlockFetchResult3 === void 0 ? void 0 : _syncBlockFetchResult3.type) === SyncBlockError.NotFound;
20
+ var isTextSelectionEnabled = fg('platform_synced_block_patch_14');
21
+
22
+ // Prevent editing in the contentEditable renderer wrapper. We set
23
+ // contentEditable="true" to enable text selection (creating an editable
24
+ // island inside ProseMirror's contentEditable="false" nodeview wrapper),
25
+ // but users must not be able to type into or modify the renderer content.
26
+ var preventInput = useCallback(function (e) {
27
+ e.preventDefault();
28
+ }, []);
19
29
  return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
20
30
  "data-testid": SyncBlockRendererWrapperDataId
21
31
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
22
32
  ,
23
- className: SyncBlockSharedCssClassName.renderer
33
+ className: SyncBlockSharedCssClassName.renderer,
34
+ contentEditable: isTextSelectionEnabled || undefined,
35
+ suppressContentEditableWarning: true
36
+ // Prevent the contentEditable div from being keyboard-focusable.
37
+ // It is only used to enable text selection, not as an input target.
38
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
39
+ ,
40
+ tabIndex: isTextSelectionEnabled ? -1 : undefined,
41
+ onBeforeInput: isTextSelectionEnabled ? preventInput : undefined,
42
+ onPaste: isTextSelectionEnabled ? preventInput : undefined
43
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop
44
+ ,
45
+ onDrop: isTextSelectionEnabled ? preventInput : undefined
24
46
  }, syncedBlockRenderer({
25
47
  syncBlockFetchResult: syncBlockFetchResult,
26
48
  api: api
@@ -31,7 +31,35 @@ export declare class SyncBlock extends ReactNodeView<SyncBlockNodeViewProps> {
31
31
  private removeSyncBlockStable;
32
32
  private fetchSyncBlockSourceInfoStable;
33
33
  unsubscribe: (() => void) | undefined;
34
+ private dragStartHandler;
34
35
  createDomRef(): HTMLElement;
36
+ /**
37
+ * Allow mouse and selection events inside the renderer content to pass
38
+ * through to the browser so that users can select and copy text within a
39
+ * reference sync block.
40
+ *
41
+ * Events that originate inside the sync block content area (but not the label)
42
+ * are stopped so ProseMirror does not intercept them for node-level selection.
43
+ * This includes the full click-drag cycle (mousedown, mousemove, mouseup),
44
+ * click, dblclick, selectstart and cut. The `cut` event is stopped because
45
+ * mousedown explicitly sets a NodeSelection on the sync block — without
46
+ * stopping `cut`, a subsequent Ctrl+X would cause ProseMirror to delete the
47
+ * entire sync block node instead of cutting the user's text selection.
48
+ *
49
+ * Copy events are conditionally stopped: when the user has an active native
50
+ * text selection inside the renderer, `copy` is stopped so the browser handles
51
+ * it natively (copying the selected text). When there is no text selection
52
+ * (just a PM NodeSelection), `copy` is NOT stopped so ProseMirror's copy
53
+ * handler runs and sets the "sync-block-copied" flag for the reference paste flow.
54
+ *
55
+ * Events on the SyncBlockLabel are left for ProseMirror to handle, preserving
56
+ * label click interactions and the floating toolbar.
57
+ *
58
+ * The renderer wrapper sets contentEditable="true" to create a re-editable
59
+ * island inside ProseMirror's contentEditable="false" nodeview, enabling
60
+ * native text selection and preventing browser drag behaviour.
61
+ */
62
+ stopEvent(event: Event): boolean;
35
63
  validUpdate(currentNode: PMNode, newNode: PMNode): boolean;
36
64
  update(node: PMNode, decorations: ReadonlyArray<Decoration>, innerDecorations?: DecorationSource): boolean;
37
65
  render({ getPos }: SyncBlockNodeViewProps): React.JSX.Element | null;
@@ -31,7 +31,35 @@ export declare class SyncBlock extends ReactNodeView<SyncBlockNodeViewProps> {
31
31
  private removeSyncBlockStable;
32
32
  private fetchSyncBlockSourceInfoStable;
33
33
  unsubscribe: (() => void) | undefined;
34
+ private dragStartHandler;
34
35
  createDomRef(): HTMLElement;
36
+ /**
37
+ * Allow mouse and selection events inside the renderer content to pass
38
+ * through to the browser so that users can select and copy text within a
39
+ * reference sync block.
40
+ *
41
+ * Events that originate inside the sync block content area (but not the label)
42
+ * are stopped so ProseMirror does not intercept them for node-level selection.
43
+ * This includes the full click-drag cycle (mousedown, mousemove, mouseup),
44
+ * click, dblclick, selectstart and cut. The `cut` event is stopped because
45
+ * mousedown explicitly sets a NodeSelection on the sync block — without
46
+ * stopping `cut`, a subsequent Ctrl+X would cause ProseMirror to delete the
47
+ * entire sync block node instead of cutting the user's text selection.
48
+ *
49
+ * Copy events are conditionally stopped: when the user has an active native
50
+ * text selection inside the renderer, `copy` is stopped so the browser handles
51
+ * it natively (copying the selected text). When there is no text selection
52
+ * (just a PM NodeSelection), `copy` is NOT stopped so ProseMirror's copy
53
+ * handler runs and sets the "sync-block-copied" flag for the reference paste flow.
54
+ *
55
+ * Events on the SyncBlockLabel are left for ProseMirror to handle, preserving
56
+ * label click interactions and the floating toolbar.
57
+ *
58
+ * The renderer wrapper sets contentEditable="true" to create a re-editable
59
+ * island inside ProseMirror's contentEditable="false" nodeview, enabling
60
+ * native text selection and preventing browser drag behaviour.
61
+ */
62
+ stopEvent(event: Event): boolean;
35
63
  validUpdate(currentNode: PMNode, newNode: PMNode): boolean;
36
64
  update(node: PMNode, decorations: ReadonlyArray<Decoration>, innerDecorations?: DecorationSource): boolean;
37
65
  render({ getPos }: SyncBlockNodeViewProps): React.JSX.Element | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "9.0.1",
3
+ "version": "9.0.3",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -28,12 +28,12 @@
28
28
  "sideEffects": false,
29
29
  "atlaskit:src": "src/index.ts",
30
30
  "dependencies": {
31
- "@atlaskit/adf-schema": "^52.15.0",
31
+ "@atlaskit/adf-schema": "^52.16.0",
32
32
  "@atlaskit/button": "23.11.8",
33
33
  "@atlaskit/dropdown-menu": "16.10.1",
34
34
  "@atlaskit/editor-json-transformer": "^8.33.0",
35
35
  "@atlaskit/editor-plugin-analytics": "^11.0.0",
36
- "@atlaskit/editor-plugin-block-menu": "^10.0.0",
36
+ "@atlaskit/editor-plugin-block-menu": "^10.1.0",
37
37
  "@atlaskit/editor-plugin-connectivity": "11.0.0",
38
38
  "@atlaskit/editor-plugin-content-format": "^5.0.0",
39
39
  "@atlaskit/editor-plugin-decorations": "^11.0.0",
@@ -44,7 +44,7 @@
44
44
  "@atlaskit/editor-prosemirror": "^7.3.0",
45
45
  "@atlaskit/editor-shared-styles": "^3.11.0",
46
46
  "@atlaskit/editor-synced-block-provider": "^7.0.0",
47
- "@atlaskit/editor-toolbar": "^1.9.0",
47
+ "@atlaskit/editor-toolbar": "^1.10.0",
48
48
  "@atlaskit/flag": "^17.12.0",
49
49
  "@atlaskit/icon": "35.4.0",
50
50
  "@atlaskit/icon-lab": "^6.13.0",
@@ -54,7 +54,7 @@
54
54
  "@atlaskit/platform-feature-flags": "^1.1.0",
55
55
  "@atlaskit/primitives": "^19.0.0",
56
56
  "@atlaskit/spinner": "19.1.2",
57
- "@atlaskit/tmp-editor-statsig": "^88.1.0",
57
+ "@atlaskit/tmp-editor-statsig": "^89.0.0",
58
58
  "@atlaskit/tokens": "13.1.1",
59
59
  "@atlaskit/tooltip": "^22.6.0",
60
60
  "@atlaskit/visually-hidden": "^3.1.0",
@@ -64,7 +64,7 @@
64
64
  "date-fns": "^2.17.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@atlaskit/editor-common": "^115.0.0",
67
+ "@atlaskit/editor-common": "^115.2.0",
68
68
  "react": "^18.2.0",
69
69
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
70
70
  },