@atlaskit/renderer 137.1.5 → 137.1.7

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,46 @@
1
1
  # @atlaskit/renderer
2
2
 
3
+ ## 137.1.7
4
+
5
+ ### Patch Changes
6
+
7
+ - [`2a540b54c3b3d`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2a540b54c3b3d) -
8
+ Two fixes to the lazy loading of collapsed expand bodies, now behind the
9
+ `platform_editor_defer_collapsed_expand_body` experiment rather than the
10
+ `hot-121622_lazy_load_expand_content` feature gate.
11
+
12
+ The body is now only held back where `hidden="until-found"` can reveal it again. Without it
13
+ nothing can reveal the text standing in for the body, so browser find cannot reach the content and
14
+ the body was missing for no gain — those browsers render the body as normal.
15
+
16
+ `display: block` is now set on the collapsed wrapper alongside the attribute. Products ship a CSS
17
+ reset with `[hidden] { display: none }`, and as an author rule it beats the UA stylesheet's
18
+ `content-visibility` for `[hidden="until-found"]`, taking the content out of the page where
19
+ browser find cannot reach it at all.
20
+
21
+ `user-select: none` is no longer set on the collapsed wrapper while browser find is enabled.
22
+ WebKit scrolls to a find-in-page match by selecting it, so unselectable text was matched but never
23
+ revealed: Safari reported a hit, the attribute stayed in place, and the expand never opened.
24
+ Chrome does not need a selection, which hid the problem. Nothing is lost by leaving it off,
25
+ because `content-visibility: hidden` already makes a skipped subtree unselectable.
26
+
27
+ A collapsed expand no longer replaces its whole body with text. Instead each block of the body
28
+ shows its own text, and blocks holding something that can be opened on its own — a nested expand,
29
+ or an extension whose stashed ADF a nested renderer mounts expands from — are rendered as they
30
+ are. So a nested expand keeps a real element while its parent is collapsed, which is what lets
31
+ browser find reveal it directly: one match now opens every expand on the path to it and none
32
+ beside it, where before it opened only the outermost one and left the reader searching again at
33
+ each level. Because blocks decide for themselves rather than being swapped out, opening an expand
34
+ no longer rebuilds everything inside it.
35
+
36
+ - Updated dependencies
37
+
38
+ ## 137.1.6
39
+
40
+ ### Patch Changes
41
+
42
+ - Updated dependencies
43
+
3
44
  ## 137.1.5
4
45
 
5
46
  ### Patch Changes
@@ -15,6 +15,7 @@ var _react = _interopRequireDefault(require("react"));
15
15
  var _model = require("@atlaskit/editor-prosemirror/model");
16
16
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
17
17
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
18
+ var _expandBody = require("../ui/expand-body");
18
19
  var _nodes = require("./nodes");
19
20
  var _textWrapper = _interopRequireDefault(require("./nodes/text-wrapper"));
20
21
  var _links = require("./utils/links");
@@ -99,12 +100,13 @@ var ReactSerializer = exports.default = /*#__PURE__*/function () {
99
100
  var shouldSkipLinkMark = function shouldSkipLinkMark(mark) {
100
101
  return _this.allowMediaLinking !== true && isMedia && mark.type.name === 'link';
101
102
  };
102
- return marks.reduceRight(function (content, mark) {
103
+ var serialized = marks.reduceRight(function (content, mark) {
103
104
  if (shouldSkipLinkMark(mark) || shouldSkipBorderMark(mark)) {
104
105
  return content;
105
106
  }
106
107
  return _this.renderMark((0, _marks.toReact)(mark), _this.withMediaMarkProps(node, mark, _this.getMarkProps(mark, [], node)), "".concat(mark.type.name, "-").concat(index), content);
107
108
  }, serializedContent);
109
+ return (0, _expandBody.withExpandBodyBlock)(node, currentPath, index, serialized);
108
110
  });
109
111
  // Ignored via go/ees005
110
112
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -283,7 +285,7 @@ var ReactSerializer = exports.default = /*#__PURE__*/function () {
283
285
  case 'expand':
284
286
  return this.getExpandProps(node, path);
285
287
  case 'nestedExpand':
286
- if ((0, _fg.fg)('hot-121622_lazy_load_expand_content')) {
288
+ if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_defer_collapsed_expand_body')) {
287
289
  return this.getExpandProps(node, path);
288
290
  }
289
291
  return this.getProps(node, path);
@@ -315,7 +317,7 @@ var ReactSerializer = exports.default = /*#__PURE__*/function () {
315
317
  if (key === 'root-0') {
316
318
  this.resetState();
317
319
  }
318
- return this.renderNode(target, props, key, this.getChildNodes(fragment).map(function (node, index) {
320
+ return this.renderNode(target, props, key, (0, _expandBody.mergeExpandBodyText)(this.getChildNodes(fragment).map(function (node, index) {
319
321
  if ((0, _nodes.isTextWrapper)(node)) {
320
322
  return _this2.serializeTextWrapper(node.content, {
321
323
  index: index,
@@ -326,7 +328,7 @@ var ReactSerializer = exports.default = /*#__PURE__*/function () {
326
328
  index: index,
327
329
  parentInfo: parentInfo
328
330
  });
329
- }));
331
+ })));
330
332
  }
331
333
  }, {
332
334
  key: "serializeTextWrapper",
@@ -736,11 +738,8 @@ var ReactSerializer = exports.default = /*#__PURE__*/function () {
736
738
  value: function getExpandProps(node) {
737
739
  var _this7 = this;
738
740
  var _path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
739
- // Expand receives the node rather than a precomputed string so it can derive the collapsed
740
- // text mirror only when it actually renders one a nested expand is not mounted until its
741
- // ancestor opens — and so that derivation can reach content the model walk cannot, such as
742
- // the ADF stashed in a coerced extension's `parameters.nestedContent`. Expand owns the
743
- // feature gate check.
741
+ // Expand is given the node so it can tell whether it opted into lazy body loading at all. The
742
+ // text each block shows is attached to the block itself, by `withExpandBodyBlock`.
744
743
  if (!(0, _links.isNestedHeaderLinksEnabled)(this.allowHeadingAnchorLinks)) {
745
744
  return _objectSpread(_objectSpread({}, this.getProps(node)), {}, {
746
745
  node: node
@@ -17,8 +17,7 @@ var _editorSharedStyles = require("@atlaskit/editor-shared-styles");
17
17
  var _chevronRight = _interopRequireDefault(require("@atlaskit/icon/core/chevron-right"));
18
18
  var _Tooltip = _interopRequireDefault(require("@atlaskit/tooltip/Tooltip"));
19
19
  var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
20
- var _fg = require("@atlaskit/platform-feature-flags/fg");
21
- var _expandSearchText = require("./utils/expand-search-text");
20
+ var _expandBody = require("./expand-body");
22
21
  var _uniqueId2 = _interopRequireDefault(require("lodash/uniqueId"));
23
22
  var _reactIntl = require("react-intl");
24
23
  var _events = require("../analytics/events");
@@ -150,12 +149,18 @@ var contentContainerStylesNotExpanded = (0, _react.css)({
150
149
  // hidden="until-found" attribute on the outer container to hide content.
151
150
  // We remove height:0/overflow:hidden/clip from the inner wrapper so the
152
151
  // browser can actually search through the content.
152
+ //
153
+ // `user-select: none` must NOT be set here, unlike the variant above. WebKit scrolls to a
154
+ // find-in-page match by selecting it, so unselectable text is matched but never revealed: the
155
+ // reader is told there is a hit, the `hidden` attribute stays put, and the expand never opens.
156
+ // Chrome does not need a selection, so it hid the problem. Nothing is lost by leaving it out —
157
+ // content-visibility:hidden already makes a skipped subtree unselectable. It is only needed in the
158
+ // other variant, where the content is merely clipped and would otherwise be selectable.
153
159
  var contentContainerStylesNotExpandedBrowserFind = (0, _react.css)({
154
160
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors
155
161
  '.expand-content-wrapper, .nestedExpand-content-wrapper': {
156
162
  width: '100%',
157
- display: 'block',
158
- userSelect: 'none'
163
+ display: 'block'
159
164
  }
160
165
  });
161
166
  var clearNextSiblingMarginTopStyle = (0, _react.css)({
@@ -223,6 +228,7 @@ function Expand(_ref) {
223
228
  nestedHeaderIds = _ref.nestedHeaderIds,
224
229
  rendererContentMode = _ref.rendererContentMode,
225
230
  node = _ref.node;
231
+ var ancestorBody = (0, _expandBody.useExpandBody)();
226
232
  var _useState = (0, _react2.useState)(false),
227
233
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
228
234
  expanded = _useState2[0],
@@ -232,12 +238,11 @@ function Expand(_ref) {
232
238
  focused = _useState4[0],
233
239
  setFocused = _useState4[1];
234
240
  /**
235
- * PGXT-9021: latches on the first open so the body stays mounted once rendered, including after
236
- * the expand is collapsed again. Remounting it would re-run every macro and Forge data fetch
237
- * inside on each reopen, which is both slower and visibly reloads content the reader has already
238
- * seen. The saving this gate exists for is at initial load, where the body has never been opened,
239
- * so nothing is given up by keeping it after that. Never set on the server, so the server and the
240
- * client's first render agree and hydration cannot desync.
241
+ * PGXT-9021: once opened, the body stays rendered even if the reader closes the expand again.
242
+ * Throwing it away would re-run every macro and Forge fetch inside it on the next open, which is
243
+ * slower and visibly reloads content the reader has already seen. We only wanted to save work on
244
+ * the first page load, and by now that has happened. Starts false on both the server and the
245
+ * client, so hydration cannot disagree.
241
246
  */
242
247
  var _useState5 = (0, _react2.useState)(false),
243
248
  _useState6 = (0, _slicedToArray2.default)(_useState5, 2),
@@ -261,15 +266,21 @@ function Expand(_ref) {
261
266
  setExpanded(true);
262
267
  setHasBeenExpanded(true);
263
268
  }, []);
269
+ // The browser found the text inside this expand. Opening this one is not enough: if it sits
270
+ // inside other expands, those have to open too or the reader still cannot see it. Each expand
271
+ // asks the one above it, so a single match opens the whole chain and nothing else.
272
+ var openWithAncestors = (0, _react2.useCallback)(function () {
273
+ openBody();
274
+ ancestorBody === null || ancestorBody === void 0 || ancestorBody.openWithAncestors();
275
+ }, [ancestorBody, openBody]);
264
276
 
265
- // Feature-detect hidden="until-found" support via the beforematch event.
266
- // Chrome 102+ and Firefox 130+ support it; Safari does not yet.
267
- // In unsupported browsers, setting hidden="until-found" is treated as boolean hidden
268
- // (display:none), which would break the expand entirely.
269
- // Initialised as false and set in useEffect to avoid SSR/client hydration mismatch
270
- // useMemo would return true on the client's first render in supported browsers,
271
- // differing from the server snapshot which always produces false.
272
- var _useState7 = (0, _react2.useState)(false),
277
+ // Feature-detect hidden="until-found" support via the beforematch event. Chrome 102+,
278
+ // Firefox 139+ and Safari 26.2+ all have it; in a browser without it, hidden="until-found" is
279
+ // treated as plain boolean hidden (display:none), which would break the expand entirely.
280
+ // Starts as `undefined`, meaning "we do not know yet", and is only answered in an effect. We
281
+ // cannot check during render: the server has no browser to check, so it would say no there and
282
+ // yes on the client, and hydration would disagree.
283
+ var _useState7 = (0, _react2.useState)(undefined),
273
284
  _useState8 = (0, _slicedToArray2.default)(_useState7, 2),
274
285
  supportsHiddenUntilFound = _useState8[0],
275
286
  setSupportsHiddenUntilFound = _useState8[1];
@@ -286,9 +297,9 @@ function Expand(_ref) {
286
297
  // because visibility:hidden blocks browser find. On expanded, we restore visibility to visible.
287
298
  //
288
299
  // Only applied when the browser supports hidden="until-found" (detected via onbeforematch).
289
- // In unsupported browsers (Safari), we skip this entirely and fall back to the normal
290
- // CSS hiding (visibility:hidden + height:0), which doesn't support find-in-page but
291
- // still works correctly for expand/collapse.
300
+ // Without it we skip this entirely and fall back to the normal CSS hiding
301
+ // (visibility:hidden + height:0), which doesn't support find-in-page but still works
302
+ // correctly for expand/collapse.
292
303
  (0, _react2.useEffect)(function () {
293
304
  var contentContainer = contentContainerRef.current;
294
305
  var contentWrapper = contentWrapperRef.current;
@@ -297,6 +308,13 @@ function Expand(_ref) {
297
308
  }
298
309
  if (supportsHiddenUntilFound && (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_close_expand_find') && !expanded) {
299
310
  contentWrapper.setAttribute('hidden', 'until-found');
311
+ // Products ship a CSS reset with `[hidden] { display: none }` — Confluence does. That is an
312
+ // author rule, so it beats the UA stylesheet's
313
+ // `[hidden="until-found"] { content-visibility: hidden }` and takes the content out of the
314
+ // page altogether, where find cannot reach it. Setting display next to the attribute keeps
315
+ // the two together, rather than relying on a class selector elsewhere out-weighing a reset
316
+ // we do not own.
317
+ contentWrapper.style.display = 'block';
300
318
  // Override the CSS visibility:hidden from contentContainerStyles — hidden="until-found"
301
319
  // now handles hiding via content-visibility:hidden, which allows browser find to index
302
320
  // the content. We use 'visible' (not '') because '' only clears the inline style but
@@ -305,6 +323,7 @@ function Expand(_ref) {
305
323
  contentWrapper.style.visibility = 'visible';
306
324
  } else {
307
325
  contentWrapper.removeAttribute('hidden');
326
+ contentWrapper.style.display = '';
308
327
  contentContainer === null || contentContainer === void 0 || contentContainer.style.removeProperty('visibility');
309
328
  contentWrapper.style.visibility = '';
310
329
  }
@@ -316,22 +335,28 @@ function Expand(_ref) {
316
335
  var contentWrapper = contentWrapperRef.current;
317
336
  var unbindWrapperBeforeMatch = contentWrapper && supportsHiddenUntilFound ? (0, _bindEventListener.bind)(contentWrapper, {
318
337
  type: 'beforematch',
319
- listener: openBody
338
+ listener: openWithAncestors
320
339
  }) : undefined;
321
340
  return function () {
322
341
  unbindWrapperBeforeMatch === null || unbindWrapperBeforeMatch === void 0 || unbindWrapperBeforeMatch();
323
342
  };
324
- }, [expanded, openBody, supportsHiddenUntilFound]);
343
+ }, [expanded, openWithAncestors, supportsHiddenUntilFound]);
325
344
 
326
- // Until the expand is first opened, stand in a plain-text mirror of the body so
327
- // hidden="until-found" keeps the content findable by browser find without mounting the real
328
- // subtree. The beforematch listener above expands before the browser scrolls to its match.
329
- // Computed lazily: a nested expand is not mounted until its ancestor opens, so its mirror is
330
- // never built on initial load.
331
- var searchText = (0, _react2.useMemo)(function () {
332
- return node && (0, _fg.fg)('hot-121622_lazy_load_expand_content') ? (0, _expandSearchText.getExpandSearchText)(node) : undefined;
333
- }, [node]);
334
- var expandContent = searchText && !expanded && !hasBeenExpanded ? (0, _react.jsx)("span", null, searchText) : children;
345
+ // While this is false, the blocks of the body show their text instead of rendering. It is true
346
+ // when:
347
+ // - the expand is open, or has been opened before, so the reader wants to see the content;
348
+ // - no node was given, so this expand never opted into lazy loading;
349
+ // - the experiment is off;
350
+ // - the browser has no hidden="until-found", so the text would not be findable and standing it in
351
+ // would gain nothing. While we still do not know, we assume the browser has it — assuming the
352
+ // opposite would render the whole body on the first paint and lose the saving entirely.
353
+ var revealed = expanded || hasBeenExpanded || !node || supportsHiddenUntilFound === false || !(0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_defer_collapsed_expand_body');
354
+ var expandBody = (0, _react2.useMemo)(function () {
355
+ return {
356
+ revealed: revealed,
357
+ openWithAncestors: openWithAncestors
358
+ };
359
+ }, [revealed, openWithAncestors]);
335
360
  return (0, _react.jsx)(Container, {
336
361
  "data-testid": "expand-container-".concat(nodeType, "-").concat(id),
337
362
  "data-node-type": nodeType,
@@ -396,7 +421,9 @@ function Expand(_ref) {
396
421
  ref: contentWrapperRef
397
422
  }, (0, _react.jsx)(_ui.WidthProvider, null, (0, _react.jsx)("div", {
398
423
  css: clearNextSiblingMarginTopStyle
399
- }), expandContent))));
424
+ }), (0, _react.jsx)(_expandBody.ExpandBodyProvider, {
425
+ value: expandBody
426
+ }, children)))));
400
427
  }
401
428
 
402
429
  // eslint-disable-next-line @typescript-eslint/ban-types
@@ -74,7 +74,7 @@ var DEGRADED_SEVERITY_THRESHOLD = exports.DEGRADED_SEVERITY_THRESHOLD = 3000;
74
74
  var TABLE_INFO_TIMEOUT = 10000;
75
75
  var RENDER_EVENT_SAMPLE_RATE = 0.1;
76
76
  var packageName = "@atlaskit/renderer";
77
- var packageVersion = "137.1.4";
77
+ var packageVersion = "137.1.6";
78
78
  var setAsQueryContainerStyles = (0, _react2.css)({
79
79
  containerName: 'ak-renderer-wrapper',
80
80
  containerType: 'inline-size'
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+
3
+ var _typeof = require("@babel/runtime/helpers/typeof");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.withExpandBodyBlock = exports.useExpandBody = exports.mergeExpandBodyText = exports.ExpandBodyProvider = exports.ExpandBodyBlock = void 0;
8
+ var _react = _interopRequireWildcard(require("react"));
9
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
10
+ var _expandSearchText = require("./utils/expand-search-text");
11
+ 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
+ var ExpandBodyContext = /*#__PURE__*/(0, _react.createContext)(null);
13
+ var ExpandBodyProvider = exports.ExpandBodyProvider = ExpandBodyContext.Provider;
14
+ var useExpandBody = exports.useExpandBody = function useExpandBody() {
15
+ return (0, _react.useContext)(ExpandBodyContext);
16
+ };
17
+ /**
18
+ * One or more neighbouring blocks of an expand's body. Shows their text until the expand is
19
+ * opened, then renders them. Falls back to rendering if there is no text, or if there is no
20
+ * expand above it.
21
+ *
22
+ * The text is rendered as-is, with no element around it. Nothing reads it but browser find,
23
+ * which only needs the characters to be in the DOM.
24
+ */
25
+ var ExpandBodyBlock = exports.ExpandBodyBlock = function ExpandBodyBlock(_ref) {
26
+ var children = _ref.children,
27
+ searchText = _ref.searchText;
28
+ var body = useExpandBody();
29
+ if (searchText === undefined || body === null || body.revealed) {
30
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, children);
31
+ }
32
+ return searchText;
33
+ };
34
+
35
+ /**
36
+ * Called for every node the serializer renders. If the node is a block of an expand's body,
37
+ * wraps it so it can show its text instead of rendering while that expand is collapsed.
38
+ * Everything else is returned untouched.
39
+ *
40
+ * A block holding an expand of its own is also returned untouched, so a table with a nested
41
+ * expand in it, or an extension holding stashed ADF, renders even while collapsed. That
42
+ * expand needs an element of its own, because the element browser find reveals is the only
43
+ * way we learn the match was inside it rather than higher up. We give up the saving on those
44
+ * blocks so the reader searches once instead of once per level.
45
+ *
46
+ * `ancestors` is the node's ancestor chain, nearest last.
47
+ */
48
+ var withExpandBodyBlock = exports.withExpandBodyBlock = function withExpandBodyBlock(node, ancestors, index, serialized) {
49
+ var parent = ancestors[ancestors.length - 1];
50
+ if (!parent || !(0, _expandSearchText.isExpandNode)(parent)) {
51
+ return serialized;
52
+ }
53
+ if (!(0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_defer_collapsed_expand_body') || (0, _expandSearchText.holdsRevealableContent)(node)) {
54
+ return serialized;
55
+ }
56
+ var searchText = (0, _expandSearchText.getBlockSearchText)(node);
57
+ if (searchText === undefined) {
58
+ return serialized;
59
+ }
60
+ return /*#__PURE__*/_react.default.createElement(ExpandBodyBlock, {
61
+ key: "expand-body-block-".concat(index),
62
+ searchText: searchText
63
+ }, serialized);
64
+ };
65
+
66
+ /** Whatever the serializer produced for one child: an element, a string, an array of either. */
67
+
68
+ var searchTextOf = function searchTextOf(child) {
69
+ return /*#__PURE__*/(0, _react.isValidElement)(child) && child.type === ExpandBodyBlock ? child.props.searchText : undefined;
70
+ };
71
+
72
+ /**
73
+ * Joins neighbouring blocks that are showing text into one, so a run of them is a single string in
74
+ * the DOM rather than one per block. A body of twenty paragraphs becomes one text node instead of
75
+ * twenty.
76
+ *
77
+ * A block that is being rendered — a nested expand, say — ends the run, because the text either
78
+ * side of it has to stay either side of it.
79
+ */
80
+ var mergeExpandBodyText = exports.mergeExpandBodyText = function mergeExpandBodyText(children) {
81
+ // Called for every fragment in the document, and only an expand's body has anything to merge, so
82
+ // leave the array alone unless two neighbours are actually showing text.
83
+ var hasRun = children.some(function (child, index) {
84
+ return index > 0 && searchTextOf(child) !== undefined && searchTextOf(children[index - 1]) !== undefined;
85
+ });
86
+ if (!hasRun) {
87
+ return children;
88
+ }
89
+ var merged = [];
90
+ var run = [];
91
+ var endRun = function endRun() {
92
+ if (run.length === 0) {
93
+ return;
94
+ }
95
+ if (run.length === 1) {
96
+ merged.push(run[0]);
97
+ } else {
98
+ var _key;
99
+ var text = run.map(searchTextOf).filter(Boolean).join(_expandSearchText.BLOCK_SEPARATOR);
100
+ // The blocks themselves, not the wrappers around them: nesting the wrappers would show
101
+ // each block's text again inside the joined run.
102
+ var blocks = run.map(function (child) {
103
+ return child.props.children;
104
+ });
105
+ merged.push( /*#__PURE__*/_react.default.createElement(ExpandBodyBlock, {
106
+ key: (_key = run[0].key) !== null && _key !== void 0 ? _key : undefined,
107
+ searchText: text
108
+ }, blocks));
109
+ }
110
+ run = [];
111
+ };
112
+ children.forEach(function (child) {
113
+ if (searchTextOf(child) === undefined) {
114
+ endRun();
115
+ merged.push(child);
116
+ return;
117
+ }
118
+ run.push(child);
119
+ });
120
+ endRun();
121
+ return merged;
122
+ };
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.getExpandSearchText = void 0;
6
+ exports.isExpandNode = exports.holdsRevealableContent = exports.getBlockSearchText = exports.BLOCK_SEPARATOR = void 0;
7
7
  var _utils = require("@atlaskit/editor-prosemirror/utils");
8
8
  /**
9
9
  * Minimal structural view of an ADF entity. `parameters.nestedContent` holds raw ADF JSON rather
@@ -11,22 +11,29 @@ var _utils = require("@atlaskit/editor-prosemirror/utils");
11
11
  */
12
12
 
13
13
  /** Separates text from adjacent blocks so phrases cannot fuse across block boundaries. */
14
- var BLOCK_SEPARATOR = ' ';
14
+ var BLOCK_SEPARATOR = exports.BLOCK_SEPARATOR = ' ';
15
15
  var asText = function asText(value) {
16
16
  return typeof value === 'string' ? value : '';
17
17
  };
18
+ var nestedContentOf = function nestedContentOf(attrs) {
19
+ var _attrs$parameters;
20
+ return attrs === null || attrs === void 0 || (_attrs$parameters = attrs.parameters) === null || _attrs$parameters === void 0 ? void 0 : _attrs$parameters.nestedContent;
21
+ };
22
+ var isExpandNode = exports.isExpandNode = function isExpandNode(node) {
23
+ return node.type.name === 'expand' || node.type.name === 'nestedExpand';
24
+ };
18
25
 
19
26
  /**
20
- * Extracts text from raw ADF JSON.
27
+ * Reads the text out of raw ADF JSON.
21
28
  *
22
- * Needed because some extensions stash a whole ADF subtree in `attrs.parameters.nestedContent`
23
- * instead of in their node content Confluence does this when it coerces nesting the schema
24
- * cannot represent (an expand more than two collapsible layers deep, say). Such a node is a
25
- * ProseMirror leaf, so the model walk cannot see the subtree even though it renders on the page
26
- * via a nested renderer. Recurses, because nestedContent routinely holds further nested content.
29
+ * Some extensions keep a whole ADF subtree in `attrs.parameters.nestedContent` rather than in their
30
+ * node content. Confluence does this when the authored nesting is deeper than the schema allows —
31
+ * an expand more than two collapsible layers deep, for example. To ProseMirror the extension is a
32
+ * leaf, so walking the node tree never sees that subtree, even though a nested renderer puts it on
33
+ * the page. Recurses, because stashed content often stashes more content of its own.
27
34
  */
28
35
  var _adfEntityText = function adfEntityText(entity) {
29
- var _entity$attrs, _entity$content;
36
+ var _entity$content;
30
37
  if (!entity) {
31
38
  return '';
32
39
  }
@@ -34,13 +41,13 @@ var _adfEntityText = function adfEntityText(entity) {
34
41
  if (entity.type === 'text') {
35
42
  parts.push(asText(entity.text));
36
43
  }
37
- var nestedContent = (_entity$attrs = entity.attrs) === null || _entity$attrs === void 0 || (_entity$attrs = _entity$attrs.parameters) === null || _entity$attrs === void 0 ? void 0 : _entity$attrs.nestedContent;
44
+ var nestedContent = nestedContentOf(entity.attrs);
38
45
  if (nestedContent) {
39
46
  // Prefer the real subtree over `attrs.text`, which is only a placeholder label for it.
40
47
  parts.push(_adfEntityText(nestedContent));
41
48
  } else {
42
- var _entity$attrs2;
43
- parts.push(asText((_entity$attrs2 = entity.attrs) === null || _entity$attrs2 === void 0 ? void 0 : _entity$attrs2.text));
49
+ var _entity$attrs;
50
+ parts.push(asText((_entity$attrs = entity.attrs) === null || _entity$attrs === void 0 ? void 0 : _entity$attrs.text));
44
51
  }
45
52
  (_entity$content = entity.content) === null || _entity$content === void 0 || _entity$content.forEach(function (child) {
46
53
  return parts.push(_adfEntityText(child));
@@ -49,16 +56,48 @@ var _adfEntityText = function adfEntityText(entity) {
49
56
  };
50
57
 
51
58
  /**
52
- * `textBetween` drops every non-text leaf unless the node spec declares `leafText`, and no ADF
53
- * node does. Leaves carry their visible text in attrs, so recover it here.
59
+ * `textBetween` skips every leaf node unless its spec declares `leafText`, and no ADF node
60
+ * does so mentions, emoji, dates and statuses would all be lost. They keep their visible
61
+ * text in attrs, so read it from there.
54
62
  */
55
63
  var leafText = function leafText(leaf) {
56
- var _leaf$attrs, _leaf$attrs2;
57
- var nestedContent = (_leaf$attrs = leaf.attrs) === null || _leaf$attrs === void 0 || (_leaf$attrs = _leaf$attrs.parameters) === null || _leaf$attrs === void 0 ? void 0 : _leaf$attrs.nestedContent;
64
+ var _leaf$attrs;
65
+ var nestedContent = nestedContentOf(leaf.attrs);
58
66
  if (nestedContent) {
59
67
  return _adfEntityText(nestedContent);
60
68
  }
61
- return asText((_leaf$attrs2 = leaf.attrs) === null || _leaf$attrs2 === void 0 ? void 0 : _leaf$attrs2.text);
69
+ return asText((_leaf$attrs = leaf.attrs) === null || _leaf$attrs === void 0 ? void 0 : _leaf$attrs.text);
70
+ };
71
+ var isRevealable = function isRevealable(node) {
72
+ return isExpandNode(node) || Boolean(nestedContentOf(node.attrs));
73
+ };
74
+ var revealableCache = new WeakMap();
75
+
76
+ /**
77
+ * Whether this block holds something that can be opened on its own: an expand, or an extension
78
+ * holding stashed ADF that a nested renderer will mount expands from.
79
+ *
80
+ * Those need a real element even while the expand around them is collapsed. The element is how we
81
+ * know find matched inside them rather than higher up: the beforematch event says nothing about
82
+ * where the match was, so the element the browser chose to reveal is the only clue.
83
+ */
84
+ var holdsRevealableContent = exports.holdsRevealableContent = function holdsRevealableContent(node) {
85
+ var cached = revealableCache.get(node);
86
+ if (cached !== undefined) {
87
+ return cached;
88
+ }
89
+ var found = isRevealable(node);
90
+ if (!found) {
91
+ node.descendants(function (descendant) {
92
+ if (found) {
93
+ return false;
94
+ }
95
+ found = isRevealable(descendant);
96
+ return !found;
97
+ });
98
+ }
99
+ revealableCache.set(node, found);
100
+ return found;
62
101
  };
63
102
  var hasInlineComment = function hasInlineComment(node) {
64
103
  return (0, _utils.findChildrenByMark)(node, node.type.schema.marks.annotation, true).some(function (annotation) {
@@ -68,30 +107,31 @@ var hasInlineComment = function hasInlineComment(node) {
68
107
  });
69
108
  };
70
109
 
71
- /** `null` is a cached "no mirror for this node", as distinct from `undefined` for a cache miss. */
110
+ /** `null` is a cached "no search text for this node", as distinct from `undefined` for a miss. */
72
111
  var searchTextCache = new WeakMap();
73
112
 
74
113
  /**
75
- * Plain-text mirror of an expand's body, rendered in its place while collapsed so browser
76
- * find-in-page can still reach the content.
114
+ * The text shown in place of one block of a collapsed expand's body, so browser find can still
115
+ * reach the content without the block being rendered.
77
116
  *
78
- * Cached on the node. ProseMirror nodes are immutable and `render-document` memoises
79
- * `nodeFromJSON`, so the same node instance is reused across renders including across the
80
- * serializer rebuilds that invalidate the module-level `memoizeOne` caches.
117
+ * Cached on the node. ProseMirror nodes never change, and the serializer reuses the same instances
118
+ * across renders, so the text only has to be worked out once per block.
81
119
  *
82
- * @returns the mirror text, or `undefined` if this expand must keep its body rendered.
120
+ * @returns the text, or `undefined` if this block has to be rendered instead.
83
121
  */
84
- var getExpandSearchText = exports.getExpandSearchText = function getExpandSearchText(node) {
122
+ var getBlockSearchText = exports.getBlockSearchText = function getBlockSearchText(node) {
85
123
  var cached = searchTextCache.get(node);
86
124
  if (cached === undefined) {
87
125
  cached = hasInlineComment(node) ?
88
- // Withhold the mirror, which makes Expand keep rendering `children`. Comment
89
- // navigation walks the DOM to find the annotated node and scrolls it into view, so
90
- // the annotation has to exist as a real element a flat text mirror carries no
91
- // annotation marks and cannot stand in for it. Without this, stepping through
92
- // comments with the arrow keys silently fails to reach any comment inside a
93
- // collapsed expand. Costs the lazy-load win for these expands, deliberately.
94
- null : node.textBetween(0, node.content.size, BLOCK_SEPARATOR, leafText);
126
+ // No text, which makes the block render instead. Comment navigation looks through the DOM
127
+ // for the commented text and scrolls to it, so that text has to be a real element —
128
+ // plain text carries no comment marks and cannot stand in for it. Without this, the
129
+ // arrow keys silently skip every comment inside a collapsed expand. We give up the
130
+ // saving for this block on purpose.
131
+ null :
132
+ // A leaf block is not inside its own range, so `textBetween` would return nothing and its
133
+ // text would be lost — a macro sitting straight in an expand body, for instance.
134
+ node.isLeaf ? leafText(node) : node.textBetween(0, node.content.size, BLOCK_SEPARATOR, leafText);
95
135
  searchTextCache.set(node, cached);
96
136
  }
97
137
  return cached !== null && cached !== void 0 ? cached : undefined;