@atlaskit/editor-plugin-list 19.0.1 → 19.0.4
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 +25 -0
- package/dist/cjs/pm-plugins/main.js +214 -9
- package/dist/es2019/pm-plugins/main.js +196 -8
- package/dist/esm/pm-plugins/main.js +213 -8
- package/dist/types/pm-plugins/main.d.ts +17 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-list
|
|
2
2
|
|
|
3
|
+
## 19.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`352bac5c0a9ed`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/352bac5c0a9ed) -
|
|
8
|
+
Reduce the per-transaction cost of recomputing list indentation decorations, behind the
|
|
9
|
+
`platform_editor_list_performance_improv` experiment. The decoration set is now maintained
|
|
10
|
+
incrementally: it is mapped through each transaction and only the top-level blocks that the
|
|
11
|
+
transaction touched are recomputed, instead of rebuilding the whole set from a full document scan.
|
|
12
|
+
The remaining scan also skips textblock subtrees, which can never contain a list, and each list
|
|
13
|
+
emits a single decoration carrying both the indentation level and the ordered-list item counter
|
|
14
|
+
padding instead of two decorations over the same range. Rendered output is unchanged.
|
|
15
|
+
|
|
16
|
+
## 19.0.3
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
|
|
22
|
+
## 19.0.2
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies
|
|
27
|
+
|
|
3
28
|
## 19.0.1
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
-
exports.pluginKey = exports.getDecorations = exports.createPlugin = void 0;
|
|
7
|
+
exports.updateDecorations = exports.pluginKey = exports.getDecorationsForDocument = exports.getDecorations = exports.createPlugin = void 0;
|
|
8
8
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
9
|
var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
|
|
10
10
|
var _selection = require("@atlaskit/editor-common/selection");
|
|
@@ -13,9 +13,13 @@ var _utils = require("@atlaskit/editor-common/utils");
|
|
|
13
13
|
var _state2 = require("@atlaskit/editor-prosemirror/state");
|
|
14
14
|
var _utils2 = require("@atlaskit/editor-prosemirror/utils");
|
|
15
15
|
var _view = require("@atlaskit/editor-prosemirror/view");
|
|
16
|
+
var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
|
|
16
17
|
var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
|
|
17
18
|
var _transforms = require("./transforms");
|
|
18
19
|
var _selection2 = require("./utils/selection");
|
|
20
|
+
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
|
|
21
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
22
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
19
23
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
20
24
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
21
25
|
var listPluginKey = new _state2.PluginKey('listPlugin');
|
|
@@ -27,6 +31,75 @@ var initialState = {
|
|
|
27
31
|
orderedListDisabled: false,
|
|
28
32
|
decorationSet: _view.DecorationSet.empty
|
|
29
33
|
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Numbered lists whose item counters reach 2+ digits need extra gutter spacing so the counter
|
|
37
|
+
* does not collide with the item content.
|
|
38
|
+
*/
|
|
39
|
+
var getItemCounterPaddingStyle = function getItemCounterPaddingStyle(node) {
|
|
40
|
+
var _node$attrs;
|
|
41
|
+
if (node.type.name !== 'orderedList') {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
var digitsSize = (0, _utils.getItemCounterDigitsSize)({
|
|
45
|
+
itemsCount: node === null || node === void 0 ? void 0 : node.childCount,
|
|
46
|
+
order: node === null || node === void 0 || (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.order
|
|
47
|
+
});
|
|
48
|
+
return digitsSize && digitsSize > 1 ? (0, _styles.getOrderedListInlineStyles)(digitsSize, 'string') : undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds the decorations for a range aligned to top-level block boundaries — or for the whole
|
|
53
|
+
* document, which is the same thing. Because a top-level block has no list ancestors, the
|
|
54
|
+
* indentation depth always restarts at zero here.
|
|
55
|
+
*
|
|
56
|
+
* Textblocks only hold inline content, so they can never contain a list. Skipping their subtrees
|
|
57
|
+
* avoids visiting every text node in the range. Each list gets a single decoration carrying both
|
|
58
|
+
* attributes, which renders identically to two decorations over the same range at half the
|
|
59
|
+
* allocation and comparison cost.
|
|
60
|
+
*/
|
|
61
|
+
var getDecorationsForRange = function getDecorationsForRange(doc, from, to) {
|
|
62
|
+
var decorations = [];
|
|
63
|
+
// this stack keeps track of each (nested) list to calculate the indentation level
|
|
64
|
+
var processedListsStack = [];
|
|
65
|
+
doc.nodesBetween(from, to, function (node, currentNodeStartPos) {
|
|
66
|
+
if (node.isTextblock) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
while (processedListsStack.length > 0) {
|
|
70
|
+
var lastList = processedListsStack[processedListsStack.length - 1];
|
|
71
|
+
// once we finish iterating over each innermost list, pop the stack to
|
|
72
|
+
// decrease the indent level attribute accordingly
|
|
73
|
+
if (currentNodeStartPos >= lastList.startPos + lastList.node.nodeSize) {
|
|
74
|
+
processedListsStack.pop();
|
|
75
|
+
} else {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if ((0, _utils.isListNode)(node)) {
|
|
80
|
+
processedListsStack.push({
|
|
81
|
+
node: node,
|
|
82
|
+
startPos: currentNodeStartPos
|
|
83
|
+
});
|
|
84
|
+
var style = getItemCounterPaddingStyle(node);
|
|
85
|
+
decorations.push(_view.Decoration.node(currentNodeStartPos, currentNodeStartPos + node.nodeSize, _objectSpread({
|
|
86
|
+
'data-indent-level': "".concat(processedListsStack.length)
|
|
87
|
+
}, style ? {
|
|
88
|
+
style: style
|
|
89
|
+
} : {})));
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
return decorations;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Full-document rebuild on the improved path, used when the decoration set has no previous value
|
|
98
|
+
* to update. The whole document is just one aligned range.
|
|
99
|
+
*/
|
|
100
|
+
var getDecorationsForDocument = exports.getDecorationsForDocument = function getDecorationsForDocument(doc) {
|
|
101
|
+
return _view.DecorationSet.empty.add(doc, getDecorationsForRange(doc, 0, doc.content.size));
|
|
102
|
+
};
|
|
30
103
|
var getDecorations = exports.getDecorations = function getDecorations(doc, _state, _featureFlags) {
|
|
31
104
|
var decorations = [];
|
|
32
105
|
|
|
@@ -58,12 +131,12 @@ var getDecorations = exports.getDecorations = function getDecorations(doc, _stat
|
|
|
58
131
|
'data-indent-level': "".concat(depth)
|
|
59
132
|
}));
|
|
60
133
|
if (node.type.name === 'orderedList') {
|
|
61
|
-
var _node$
|
|
134
|
+
var _node$attrs2;
|
|
62
135
|
// If a numbered list has item counters numbering >= 100, we'll need to add special
|
|
63
136
|
// spacing to account for the extra digit chars
|
|
64
137
|
var digitsSize = (0, _utils.getItemCounterDigitsSize)({
|
|
65
138
|
itemsCount: node === null || node === void 0 ? void 0 : node.childCount,
|
|
66
|
-
order: node === null || node === void 0 || (_node$
|
|
139
|
+
order: node === null || node === void 0 || (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.order
|
|
67
140
|
});
|
|
68
141
|
if (digitsSize && digitsSize > 1) {
|
|
69
142
|
decorations.push(_view.Decoration.node(from, to, {
|
|
@@ -75,6 +148,121 @@ var getDecorations = exports.getDecorations = function getDecorations(doc, _stat
|
|
|
75
148
|
});
|
|
76
149
|
return _view.DecorationSet.empty.add(doc, decorations);
|
|
77
150
|
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The parts of a transaction the decoration update needs, so that both `Transaction` and
|
|
154
|
+
* `ReadonlyTransaction` can be passed in.
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Expands the ranges touched by a transaction out to whole top-level blocks. A list decoration
|
|
159
|
+
* depends only on the subtree of the top-level block containing it, so recomputing whole blocks is
|
|
160
|
+
* enough — and it keeps the depth calculation free of any ancestor accounting.
|
|
161
|
+
*/
|
|
162
|
+
var getDirtyTopLevelRanges = function getDirtyTopLevelRanges(tr, doc) {
|
|
163
|
+
var docSize = doc.content.size;
|
|
164
|
+
var wholeDoc = [{
|
|
165
|
+
from: 0,
|
|
166
|
+
to: docSize
|
|
167
|
+
}];
|
|
168
|
+
var boundsAt = function boundsAt(pos) {
|
|
169
|
+
// resolving a position strictly inside a block gives us its boundaries in O(depth)
|
|
170
|
+
var inside = Math.max(1, Math.min(pos, Math.max(1, docSize - 1)));
|
|
171
|
+
var $inside = doc.resolve(inside);
|
|
172
|
+
if ($inside.depth > 0) {
|
|
173
|
+
return {
|
|
174
|
+
from: $inside.before(1),
|
|
175
|
+
to: $inside.after(1)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// exactly between two top-level blocks — cover both neighbours
|
|
179
|
+
var $before = doc.resolve(Math.max(1, inside - 1));
|
|
180
|
+
var $after = doc.resolve(Math.min(Math.max(1, docSize - 1), inside + 1));
|
|
181
|
+
return {
|
|
182
|
+
from: $before.depth > 0 ? $before.before(1) : 0,
|
|
183
|
+
to: $after.depth > 0 ? $after.after(1) : docSize
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
var ranges = [];
|
|
187
|
+
for (var index = 0; index < tr.steps.length; index++) {
|
|
188
|
+
var step = tr.steps[index];
|
|
189
|
+
// Duck-typed rather than matched on step class so that unknown step types fall back to a
|
|
190
|
+
// full recompute instead of being silently skipped.
|
|
191
|
+
// Ignored via go/ees005
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
193
|
+
var _ref = step,
|
|
194
|
+
from = _ref.from,
|
|
195
|
+
to = _ref.to,
|
|
196
|
+
pos = _ref.pos;
|
|
197
|
+
var positions = typeof from === 'number' && typeof to === 'number' ? [from, to] : typeof pos === 'number' ? [pos] : [];
|
|
198
|
+
if (positions.length === 0) {
|
|
199
|
+
return wholeDoc;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Step positions are in the document before this step, so map through the later steps. The
|
|
203
|
+
// range start is biased left and the end biased right, so that neither slides across a
|
|
204
|
+
// block boundary and leaves a dirty block out of the recompute.
|
|
205
|
+
var remainder = tr.mapping.slice(index);
|
|
206
|
+
var start = boundsAt(remainder.map(Math.min.apply(Math, positions), -1));
|
|
207
|
+
var end = boundsAt(remainder.map(Math.max.apply(Math, positions), 1));
|
|
208
|
+
ranges.push({
|
|
209
|
+
from: Math.min(start.from, end.from),
|
|
210
|
+
to: Math.max(start.to, end.to)
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
ranges.sort(function (a, b) {
|
|
214
|
+
return a.from - b.from;
|
|
215
|
+
});
|
|
216
|
+
var merged = [];
|
|
217
|
+
for (var _i = 0, _ranges = ranges; _i < _ranges.length; _i++) {
|
|
218
|
+
var range = _ranges[_i];
|
|
219
|
+
var last = merged[merged.length - 1];
|
|
220
|
+
if (last && range.from <= last.to) {
|
|
221
|
+
last.to = Math.max(last.to, range.to);
|
|
222
|
+
} else {
|
|
223
|
+
merged.push(_objectSpread({}, range));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return merged;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Maps the previous decoration set through the transaction and recomputes only the top-level blocks
|
|
231
|
+
* it touched, instead of rebuilding the whole set from a full document scan.
|
|
232
|
+
*/
|
|
233
|
+
var updateDecorations = exports.updateDecorations = function updateDecorations(previousDecorationSet, tr, doc) {
|
|
234
|
+
var decorationSet = previousDecorationSet.map(tr.mapping, doc);
|
|
235
|
+
var _iterator = _createForOfIteratorHelper(getDirtyTopLevelRanges(tr, doc)),
|
|
236
|
+
_step;
|
|
237
|
+
try {
|
|
238
|
+
var _loop = function _loop() {
|
|
239
|
+
var _step$value = _step.value,
|
|
240
|
+
from = _step$value.from,
|
|
241
|
+
to = _step$value.to;
|
|
242
|
+
// find() also returns decorations that merely touch the range, so removal is restricted to
|
|
243
|
+
// the ones fully inside it — those are exactly the decorations recomputed below. Removing a
|
|
244
|
+
// decoration that only touches the boundary would drop it permanently.
|
|
245
|
+
var stale = decorationSet.find(from, to).filter(function (decoration) {
|
|
246
|
+
return decoration.from >= from && decoration.to <= to;
|
|
247
|
+
});
|
|
248
|
+
if (stale.length > 0) {
|
|
249
|
+
decorationSet = decorationSet.remove(stale);
|
|
250
|
+
}
|
|
251
|
+
var fresh = getDecorationsForRange(doc, from, to);
|
|
252
|
+
if (fresh.length > 0) {
|
|
253
|
+
decorationSet = decorationSet.add(doc, fresh);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
257
|
+
_loop();
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
_iterator.e(err);
|
|
261
|
+
} finally {
|
|
262
|
+
_iterator.f();
|
|
263
|
+
}
|
|
264
|
+
return decorationSet;
|
|
265
|
+
};
|
|
78
266
|
var getListState = function getListState(doc, selection) {
|
|
79
267
|
var _doc$type$schema$node = doc.type.schema.nodes,
|
|
80
268
|
bulletList = _doc$type$schema$node.bulletList,
|
|
@@ -92,12 +280,19 @@ var getListState = function getListState(doc, selection) {
|
|
|
92
280
|
orderedListDisabled: orderedListDisabled
|
|
93
281
|
};
|
|
94
282
|
};
|
|
95
|
-
var
|
|
283
|
+
var handleDocChangedOld = function handleDocChangedOld(featureFlags) {
|
|
284
|
+
return function (tr, pluginState, editorState) {
|
|
285
|
+
var nextPluginState = handleSelectionChanged(tr, pluginState);
|
|
286
|
+
return _objectSpread(_objectSpread({}, nextPluginState), {}, {
|
|
287
|
+
decorationSet: getDecorations(tr.doc, editorState, featureFlags)
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
var handleDocChangedNew = function handleDocChangedNew() {
|
|
96
292
|
return function (tr, pluginState, editorState) {
|
|
97
293
|
var nextPluginState = handleSelectionChanged(tr, pluginState);
|
|
98
|
-
var decorationSet = getDecorations(tr.doc, editorState, featureFlags);
|
|
99
294
|
return _objectSpread(_objectSpread({}, nextPluginState), {}, {
|
|
100
|
-
decorationSet: decorationSet
|
|
295
|
+
decorationSet: updateDecorations(pluginState.decorationSet, tr, tr.doc)
|
|
101
296
|
});
|
|
102
297
|
};
|
|
103
298
|
};
|
|
@@ -123,7 +318,7 @@ var reducer = function reducer() {
|
|
|
123
318
|
return state;
|
|
124
319
|
};
|
|
125
320
|
};
|
|
126
|
-
var
|
|
321
|
+
var createInitialStateOld = function createInitialStateOld(featureFlags, api) {
|
|
127
322
|
return function (state) {
|
|
128
323
|
var isToolbarAIFCEnabled = Boolean(api === null || api === void 0 ? void 0 : api.toolbar);
|
|
129
324
|
return _objectSpread(_objectSpread({}, isToolbarAIFCEnabled ? getListState(state.doc, state.selection) : initialState), {}, {
|
|
@@ -131,15 +326,25 @@ var createInitialState = function createInitialState(featureFlags, api) {
|
|
|
131
326
|
});
|
|
132
327
|
};
|
|
133
328
|
};
|
|
329
|
+
var createInitialStateNew = function createInitialStateNew(featureFlags, api) {
|
|
330
|
+
return function (state) {
|
|
331
|
+
var isToolbarAIFCEnabled = Boolean(api === null || api === void 0 ? void 0 : api.toolbar);
|
|
332
|
+
return _objectSpread(_objectSpread({}, isToolbarAIFCEnabled ? getListState(state.doc, state.selection) : initialState), {}, {
|
|
333
|
+
decorationSet: getDecorationsForDocument(state.doc)
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
};
|
|
134
337
|
var createPlugin = exports.createPlugin = function createPlugin(eventDispatch, featureFlags, api) {
|
|
135
338
|
var _pluginFactory = (0, _utils.pluginFactory)(listPluginKey, reducer(), {
|
|
136
|
-
|
|
339
|
+
// Resolved once per editor instance rather than per transaction, so the exposure event fires
|
|
340
|
+
// once and the decoration hot path stays free of experiment lookups.
|
|
341
|
+
onDocChanged: (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_list_performance_improv') ? handleDocChangedNew() : handleDocChangedOld(featureFlags),
|
|
137
342
|
onSelectionChanged: handleSelectionChanged
|
|
138
343
|
}),
|
|
139
344
|
getPluginState = _pluginFactory.getPluginState,
|
|
140
345
|
createPluginState = _pluginFactory.createPluginState;
|
|
141
346
|
return new _safePlugin.SafePlugin({
|
|
142
|
-
state: createPluginState(eventDispatch,
|
|
347
|
+
state: createPluginState(eventDispatch, (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_list_performance_improv') ? createInitialStateNew(featureFlags, api) : createInitialStateOld(featureFlags, api)),
|
|
143
348
|
key: listPluginKey,
|
|
144
349
|
appendTransaction: function appendTransaction(transactions, _oldState, newState) {
|
|
145
350
|
if (!(0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_flexible_list_schema', 'isEnabled', true)) {
|
|
@@ -5,6 +5,7 @@ import { getItemCounterDigitsSize, isListNode, pluginFactory } from '@atlaskit/e
|
|
|
5
5
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
6
6
|
import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
7
7
|
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
8
|
+
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
|
|
8
9
|
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
9
10
|
import { applyListNormalisationFixes } from './transforms';
|
|
10
11
|
import { isWrappingPossible } from './utils/selection';
|
|
@@ -17,6 +18,74 @@ const initialState = {
|
|
|
17
18
|
orderedListDisabled: false,
|
|
18
19
|
decorationSet: DecorationSet.empty
|
|
19
20
|
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Numbered lists whose item counters reach 2+ digits need extra gutter spacing so the counter
|
|
24
|
+
* does not collide with the item content.
|
|
25
|
+
*/
|
|
26
|
+
const getItemCounterPaddingStyle = node => {
|
|
27
|
+
var _node$attrs;
|
|
28
|
+
if (node.type.name !== 'orderedList') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const digitsSize = getItemCounterDigitsSize({
|
|
32
|
+
itemsCount: node === null || node === void 0 ? void 0 : node.childCount,
|
|
33
|
+
order: node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.order
|
|
34
|
+
});
|
|
35
|
+
return digitsSize && digitsSize > 1 ? getOrderedListInlineStyles(digitsSize, 'string') : undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds the decorations for a range aligned to top-level block boundaries — or for the whole
|
|
40
|
+
* document, which is the same thing. Because a top-level block has no list ancestors, the
|
|
41
|
+
* indentation depth always restarts at zero here.
|
|
42
|
+
*
|
|
43
|
+
* Textblocks only hold inline content, so they can never contain a list. Skipping their subtrees
|
|
44
|
+
* avoids visiting every text node in the range. Each list gets a single decoration carrying both
|
|
45
|
+
* attributes, which renders identically to two decorations over the same range at half the
|
|
46
|
+
* allocation and comparison cost.
|
|
47
|
+
*/
|
|
48
|
+
const getDecorationsForRange = (doc, from, to) => {
|
|
49
|
+
const decorations = [];
|
|
50
|
+
// this stack keeps track of each (nested) list to calculate the indentation level
|
|
51
|
+
const processedListsStack = [];
|
|
52
|
+
doc.nodesBetween(from, to, (node, currentNodeStartPos) => {
|
|
53
|
+
if (node.isTextblock) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
while (processedListsStack.length > 0) {
|
|
57
|
+
const lastList = processedListsStack[processedListsStack.length - 1];
|
|
58
|
+
// once we finish iterating over each innermost list, pop the stack to
|
|
59
|
+
// decrease the indent level attribute accordingly
|
|
60
|
+
if (currentNodeStartPos >= lastList.startPos + lastList.node.nodeSize) {
|
|
61
|
+
processedListsStack.pop();
|
|
62
|
+
} else {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (isListNode(node)) {
|
|
67
|
+
processedListsStack.push({
|
|
68
|
+
node,
|
|
69
|
+
startPos: currentNodeStartPos
|
|
70
|
+
});
|
|
71
|
+
const style = getItemCounterPaddingStyle(node);
|
|
72
|
+
decorations.push(Decoration.node(currentNodeStartPos, currentNodeStartPos + node.nodeSize, {
|
|
73
|
+
'data-indent-level': `${processedListsStack.length}`,
|
|
74
|
+
...(style ? {
|
|
75
|
+
style
|
|
76
|
+
} : {})
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
return decorations;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Full-document rebuild on the improved path, used when the decoration set has no previous value
|
|
86
|
+
* to update. The whole document is just one aligned range.
|
|
87
|
+
*/
|
|
88
|
+
export const getDecorationsForDocument = doc => DecorationSet.empty.add(doc, getDecorationsForRange(doc, 0, doc.content.size));
|
|
20
89
|
export const getDecorations = (doc, _state, _featureFlags) => {
|
|
21
90
|
const decorations = [];
|
|
22
91
|
|
|
@@ -48,12 +117,12 @@ export const getDecorations = (doc, _state, _featureFlags) => {
|
|
|
48
117
|
'data-indent-level': `${depth}`
|
|
49
118
|
}));
|
|
50
119
|
if (node.type.name === 'orderedList') {
|
|
51
|
-
var _node$
|
|
120
|
+
var _node$attrs2;
|
|
52
121
|
// If a numbered list has item counters numbering >= 100, we'll need to add special
|
|
53
122
|
// spacing to account for the extra digit chars
|
|
54
123
|
const digitsSize = getItemCounterDigitsSize({
|
|
55
124
|
itemsCount: node === null || node === void 0 ? void 0 : node.childCount,
|
|
56
|
-
order: node === null || node === void 0 ? void 0 : (_node$
|
|
125
|
+
order: node === null || node === void 0 ? void 0 : (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.order
|
|
57
126
|
});
|
|
58
127
|
if (digitsSize && digitsSize > 1) {
|
|
59
128
|
decorations.push(Decoration.node(from, to, {
|
|
@@ -65,6 +134,108 @@ export const getDecorations = (doc, _state, _featureFlags) => {
|
|
|
65
134
|
});
|
|
66
135
|
return DecorationSet.empty.add(doc, decorations);
|
|
67
136
|
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The parts of a transaction the decoration update needs, so that both `Transaction` and
|
|
140
|
+
* `ReadonlyTransaction` can be passed in.
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Expands the ranges touched by a transaction out to whole top-level blocks. A list decoration
|
|
145
|
+
* depends only on the subtree of the top-level block containing it, so recomputing whole blocks is
|
|
146
|
+
* enough — and it keeps the depth calculation free of any ancestor accounting.
|
|
147
|
+
*/
|
|
148
|
+
const getDirtyTopLevelRanges = (tr, doc) => {
|
|
149
|
+
const docSize = doc.content.size;
|
|
150
|
+
const wholeDoc = [{
|
|
151
|
+
from: 0,
|
|
152
|
+
to: docSize
|
|
153
|
+
}];
|
|
154
|
+
const boundsAt = pos => {
|
|
155
|
+
// resolving a position strictly inside a block gives us its boundaries in O(depth)
|
|
156
|
+
const inside = Math.max(1, Math.min(pos, Math.max(1, docSize - 1)));
|
|
157
|
+
const $inside = doc.resolve(inside);
|
|
158
|
+
if ($inside.depth > 0) {
|
|
159
|
+
return {
|
|
160
|
+
from: $inside.before(1),
|
|
161
|
+
to: $inside.after(1)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// exactly between two top-level blocks — cover both neighbours
|
|
165
|
+
const $before = doc.resolve(Math.max(1, inside - 1));
|
|
166
|
+
const $after = doc.resolve(Math.min(Math.max(1, docSize - 1), inside + 1));
|
|
167
|
+
return {
|
|
168
|
+
from: $before.depth > 0 ? $before.before(1) : 0,
|
|
169
|
+
to: $after.depth > 0 ? $after.after(1) : docSize
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
const ranges = [];
|
|
173
|
+
for (let index = 0; index < tr.steps.length; index++) {
|
|
174
|
+
const step = tr.steps[index];
|
|
175
|
+
// Duck-typed rather than matched on step class so that unknown step types fall back to a
|
|
176
|
+
// full recompute instead of being silently skipped.
|
|
177
|
+
// Ignored via go/ees005
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
+
const {
|
|
180
|
+
from,
|
|
181
|
+
to,
|
|
182
|
+
pos
|
|
183
|
+
} = step;
|
|
184
|
+
const positions = typeof from === 'number' && typeof to === 'number' ? [from, to] : typeof pos === 'number' ? [pos] : [];
|
|
185
|
+
if (positions.length === 0) {
|
|
186
|
+
return wholeDoc;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Step positions are in the document before this step, so map through the later steps. The
|
|
190
|
+
// range start is biased left and the end biased right, so that neither slides across a
|
|
191
|
+
// block boundary and leaves a dirty block out of the recompute.
|
|
192
|
+
const remainder = tr.mapping.slice(index);
|
|
193
|
+
const start = boundsAt(remainder.map(Math.min(...positions), -1));
|
|
194
|
+
const end = boundsAt(remainder.map(Math.max(...positions), 1));
|
|
195
|
+
ranges.push({
|
|
196
|
+
from: Math.min(start.from, end.from),
|
|
197
|
+
to: Math.max(start.to, end.to)
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
ranges.sort((a, b) => a.from - b.from);
|
|
201
|
+
const merged = [];
|
|
202
|
+
for (const range of ranges) {
|
|
203
|
+
const last = merged[merged.length - 1];
|
|
204
|
+
if (last && range.from <= last.to) {
|
|
205
|
+
last.to = Math.max(last.to, range.to);
|
|
206
|
+
} else {
|
|
207
|
+
merged.push({
|
|
208
|
+
...range
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return merged;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Maps the previous decoration set through the transaction and recomputes only the top-level blocks
|
|
217
|
+
* it touched, instead of rebuilding the whole set from a full document scan.
|
|
218
|
+
*/
|
|
219
|
+
export const updateDecorations = (previousDecorationSet, tr, doc) => {
|
|
220
|
+
let decorationSet = previousDecorationSet.map(tr.mapping, doc);
|
|
221
|
+
for (const {
|
|
222
|
+
from,
|
|
223
|
+
to
|
|
224
|
+
} of getDirtyTopLevelRanges(tr, doc)) {
|
|
225
|
+
// find() also returns decorations that merely touch the range, so removal is restricted to
|
|
226
|
+
// the ones fully inside it — those are exactly the decorations recomputed below. Removing a
|
|
227
|
+
// decoration that only touches the boundary would drop it permanently.
|
|
228
|
+
const stale = decorationSet.find(from, to).filter(decoration => decoration.from >= from && decoration.to <= to);
|
|
229
|
+
if (stale.length > 0) {
|
|
230
|
+
decorationSet = decorationSet.remove(stale);
|
|
231
|
+
}
|
|
232
|
+
const fresh = getDecorationsForRange(doc, from, to);
|
|
233
|
+
if (fresh.length > 0) {
|
|
234
|
+
decorationSet = decorationSet.add(doc, fresh);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return decorationSet;
|
|
238
|
+
};
|
|
68
239
|
const getListState = (doc, selection) => {
|
|
69
240
|
const {
|
|
70
241
|
bulletList,
|
|
@@ -83,12 +254,18 @@ const getListState = (doc, selection) => {
|
|
|
83
254
|
orderedListDisabled
|
|
84
255
|
};
|
|
85
256
|
};
|
|
86
|
-
const
|
|
257
|
+
const handleDocChangedOld = featureFlags => (tr, pluginState, editorState) => {
|
|
87
258
|
const nextPluginState = handleSelectionChanged(tr, pluginState);
|
|
88
|
-
const decorationSet = getDecorations(tr.doc, editorState, featureFlags);
|
|
89
259
|
return {
|
|
90
260
|
...nextPluginState,
|
|
91
|
-
decorationSet
|
|
261
|
+
decorationSet: getDecorations(tr.doc, editorState, featureFlags)
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
const handleDocChangedNew = () => (tr, pluginState, editorState) => {
|
|
265
|
+
const nextPluginState = handleSelectionChanged(tr, pluginState);
|
|
266
|
+
return {
|
|
267
|
+
...nextPluginState,
|
|
268
|
+
decorationSet: updateDecorations(pluginState.decorationSet, tr, tr.doc)
|
|
92
269
|
};
|
|
93
270
|
};
|
|
94
271
|
const handleSelectionChanged = (tr, pluginState) => {
|
|
@@ -113,7 +290,7 @@ const handleSelectionChanged = (tr, pluginState) => {
|
|
|
113
290
|
const reducer = () => state => {
|
|
114
291
|
return state;
|
|
115
292
|
};
|
|
116
|
-
const
|
|
293
|
+
const createInitialStateOld = (featureFlags, api) => state => {
|
|
117
294
|
const isToolbarAIFCEnabled = Boolean(api === null || api === void 0 ? void 0 : api.toolbar);
|
|
118
295
|
return {
|
|
119
296
|
// When plugin is initialised, editor state is defined with selection
|
|
@@ -122,16 +299,27 @@ const createInitialState = (featureFlags, api) => state => {
|
|
|
122
299
|
decorationSet: getDecorations(state.doc, state, featureFlags)
|
|
123
300
|
};
|
|
124
301
|
};
|
|
302
|
+
const createInitialStateNew = (featureFlags, api) => state => {
|
|
303
|
+
const isToolbarAIFCEnabled = Boolean(api === null || api === void 0 ? void 0 : api.toolbar);
|
|
304
|
+
return {
|
|
305
|
+
// When plugin is initialised, editor state is defined with selection
|
|
306
|
+
// hence returning the list state based on the selection to avoid list button in primary toolbar flickering during initial load
|
|
307
|
+
...(isToolbarAIFCEnabled ? getListState(state.doc, state.selection) : initialState),
|
|
308
|
+
decorationSet: getDecorationsForDocument(state.doc)
|
|
309
|
+
};
|
|
310
|
+
};
|
|
125
311
|
export const createPlugin = (eventDispatch, featureFlags, api) => {
|
|
126
312
|
const {
|
|
127
313
|
getPluginState,
|
|
128
314
|
createPluginState
|
|
129
315
|
} = pluginFactory(listPluginKey, reducer(), {
|
|
130
|
-
|
|
316
|
+
// Resolved once per editor instance rather than per transaction, so the exposure event fires
|
|
317
|
+
// once and the decoration hot path stays free of experiment lookups.
|
|
318
|
+
onDocChanged: isExperimentEnabled('platform_editor_list_performance_improv') ? handleDocChangedNew() : handleDocChangedOld(featureFlags),
|
|
131
319
|
onSelectionChanged: handleSelectionChanged
|
|
132
320
|
});
|
|
133
321
|
return new SafePlugin({
|
|
134
|
-
state: createPluginState(eventDispatch,
|
|
322
|
+
state: createPluginState(eventDispatch, isExperimentEnabled('platform_editor_list_performance_improv') ? createInitialStateNew(featureFlags, api) : createInitialStateOld(featureFlags, api)),
|
|
135
323
|
key: listPluginKey,
|
|
136
324
|
appendTransaction(transactions, _oldState, newState) {
|
|
137
325
|
if (!expValEqualsNoExposure('platform_editor_flexible_list_schema', 'isEnabled', true)) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
|
|
3
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
4
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
2
5
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
6
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
7
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
@@ -8,6 +11,7 @@ import { getItemCounterDigitsSize, isListNode, pluginFactory } from '@atlaskit/e
|
|
|
8
11
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
9
12
|
import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
10
13
|
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
14
|
+
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
|
|
11
15
|
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
12
16
|
import { applyListNormalisationFixes } from './transforms';
|
|
13
17
|
import { isWrappingPossible } from './utils/selection';
|
|
@@ -20,6 +24,75 @@ var initialState = {
|
|
|
20
24
|
orderedListDisabled: false,
|
|
21
25
|
decorationSet: DecorationSet.empty
|
|
22
26
|
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Numbered lists whose item counters reach 2+ digits need extra gutter spacing so the counter
|
|
30
|
+
* does not collide with the item content.
|
|
31
|
+
*/
|
|
32
|
+
var getItemCounterPaddingStyle = function getItemCounterPaddingStyle(node) {
|
|
33
|
+
var _node$attrs;
|
|
34
|
+
if (node.type.name !== 'orderedList') {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
var digitsSize = getItemCounterDigitsSize({
|
|
38
|
+
itemsCount: node === null || node === void 0 ? void 0 : node.childCount,
|
|
39
|
+
order: node === null || node === void 0 || (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.order
|
|
40
|
+
});
|
|
41
|
+
return digitsSize && digitsSize > 1 ? getOrderedListInlineStyles(digitsSize, 'string') : undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds the decorations for a range aligned to top-level block boundaries — or for the whole
|
|
46
|
+
* document, which is the same thing. Because a top-level block has no list ancestors, the
|
|
47
|
+
* indentation depth always restarts at zero here.
|
|
48
|
+
*
|
|
49
|
+
* Textblocks only hold inline content, so they can never contain a list. Skipping their subtrees
|
|
50
|
+
* avoids visiting every text node in the range. Each list gets a single decoration carrying both
|
|
51
|
+
* attributes, which renders identically to two decorations over the same range at half the
|
|
52
|
+
* allocation and comparison cost.
|
|
53
|
+
*/
|
|
54
|
+
var getDecorationsForRange = function getDecorationsForRange(doc, from, to) {
|
|
55
|
+
var decorations = [];
|
|
56
|
+
// this stack keeps track of each (nested) list to calculate the indentation level
|
|
57
|
+
var processedListsStack = [];
|
|
58
|
+
doc.nodesBetween(from, to, function (node, currentNodeStartPos) {
|
|
59
|
+
if (node.isTextblock) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
while (processedListsStack.length > 0) {
|
|
63
|
+
var lastList = processedListsStack[processedListsStack.length - 1];
|
|
64
|
+
// once we finish iterating over each innermost list, pop the stack to
|
|
65
|
+
// decrease the indent level attribute accordingly
|
|
66
|
+
if (currentNodeStartPos >= lastList.startPos + lastList.node.nodeSize) {
|
|
67
|
+
processedListsStack.pop();
|
|
68
|
+
} else {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (isListNode(node)) {
|
|
73
|
+
processedListsStack.push({
|
|
74
|
+
node: node,
|
|
75
|
+
startPos: currentNodeStartPos
|
|
76
|
+
});
|
|
77
|
+
var style = getItemCounterPaddingStyle(node);
|
|
78
|
+
decorations.push(Decoration.node(currentNodeStartPos, currentNodeStartPos + node.nodeSize, _objectSpread({
|
|
79
|
+
'data-indent-level': "".concat(processedListsStack.length)
|
|
80
|
+
}, style ? {
|
|
81
|
+
style: style
|
|
82
|
+
} : {})));
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
return decorations;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Full-document rebuild on the improved path, used when the decoration set has no previous value
|
|
91
|
+
* to update. The whole document is just one aligned range.
|
|
92
|
+
*/
|
|
93
|
+
export var getDecorationsForDocument = function getDecorationsForDocument(doc) {
|
|
94
|
+
return DecorationSet.empty.add(doc, getDecorationsForRange(doc, 0, doc.content.size));
|
|
95
|
+
};
|
|
23
96
|
export var getDecorations = function getDecorations(doc, _state, _featureFlags) {
|
|
24
97
|
var decorations = [];
|
|
25
98
|
|
|
@@ -51,12 +124,12 @@ export var getDecorations = function getDecorations(doc, _state, _featureFlags)
|
|
|
51
124
|
'data-indent-level': "".concat(depth)
|
|
52
125
|
}));
|
|
53
126
|
if (node.type.name === 'orderedList') {
|
|
54
|
-
var _node$
|
|
127
|
+
var _node$attrs2;
|
|
55
128
|
// If a numbered list has item counters numbering >= 100, we'll need to add special
|
|
56
129
|
// spacing to account for the extra digit chars
|
|
57
130
|
var digitsSize = getItemCounterDigitsSize({
|
|
58
131
|
itemsCount: node === null || node === void 0 ? void 0 : node.childCount,
|
|
59
|
-
order: node === null || node === void 0 || (_node$
|
|
132
|
+
order: node === null || node === void 0 || (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.order
|
|
60
133
|
});
|
|
61
134
|
if (digitsSize && digitsSize > 1) {
|
|
62
135
|
decorations.push(Decoration.node(from, to, {
|
|
@@ -68,6 +141,121 @@ export var getDecorations = function getDecorations(doc, _state, _featureFlags)
|
|
|
68
141
|
});
|
|
69
142
|
return DecorationSet.empty.add(doc, decorations);
|
|
70
143
|
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The parts of a transaction the decoration update needs, so that both `Transaction` and
|
|
147
|
+
* `ReadonlyTransaction` can be passed in.
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Expands the ranges touched by a transaction out to whole top-level blocks. A list decoration
|
|
152
|
+
* depends only on the subtree of the top-level block containing it, so recomputing whole blocks is
|
|
153
|
+
* enough — and it keeps the depth calculation free of any ancestor accounting.
|
|
154
|
+
*/
|
|
155
|
+
var getDirtyTopLevelRanges = function getDirtyTopLevelRanges(tr, doc) {
|
|
156
|
+
var docSize = doc.content.size;
|
|
157
|
+
var wholeDoc = [{
|
|
158
|
+
from: 0,
|
|
159
|
+
to: docSize
|
|
160
|
+
}];
|
|
161
|
+
var boundsAt = function boundsAt(pos) {
|
|
162
|
+
// resolving a position strictly inside a block gives us its boundaries in O(depth)
|
|
163
|
+
var inside = Math.max(1, Math.min(pos, Math.max(1, docSize - 1)));
|
|
164
|
+
var $inside = doc.resolve(inside);
|
|
165
|
+
if ($inside.depth > 0) {
|
|
166
|
+
return {
|
|
167
|
+
from: $inside.before(1),
|
|
168
|
+
to: $inside.after(1)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// exactly between two top-level blocks — cover both neighbours
|
|
172
|
+
var $before = doc.resolve(Math.max(1, inside - 1));
|
|
173
|
+
var $after = doc.resolve(Math.min(Math.max(1, docSize - 1), inside + 1));
|
|
174
|
+
return {
|
|
175
|
+
from: $before.depth > 0 ? $before.before(1) : 0,
|
|
176
|
+
to: $after.depth > 0 ? $after.after(1) : docSize
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
var ranges = [];
|
|
180
|
+
for (var index = 0; index < tr.steps.length; index++) {
|
|
181
|
+
var step = tr.steps[index];
|
|
182
|
+
// Duck-typed rather than matched on step class so that unknown step types fall back to a
|
|
183
|
+
// full recompute instead of being silently skipped.
|
|
184
|
+
// Ignored via go/ees005
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
|
+
var _ref = step,
|
|
187
|
+
from = _ref.from,
|
|
188
|
+
to = _ref.to,
|
|
189
|
+
pos = _ref.pos;
|
|
190
|
+
var positions = typeof from === 'number' && typeof to === 'number' ? [from, to] : typeof pos === 'number' ? [pos] : [];
|
|
191
|
+
if (positions.length === 0) {
|
|
192
|
+
return wholeDoc;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Step positions are in the document before this step, so map through the later steps. The
|
|
196
|
+
// range start is biased left and the end biased right, so that neither slides across a
|
|
197
|
+
// block boundary and leaves a dirty block out of the recompute.
|
|
198
|
+
var remainder = tr.mapping.slice(index);
|
|
199
|
+
var start = boundsAt(remainder.map(Math.min.apply(Math, positions), -1));
|
|
200
|
+
var end = boundsAt(remainder.map(Math.max.apply(Math, positions), 1));
|
|
201
|
+
ranges.push({
|
|
202
|
+
from: Math.min(start.from, end.from),
|
|
203
|
+
to: Math.max(start.to, end.to)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
ranges.sort(function (a, b) {
|
|
207
|
+
return a.from - b.from;
|
|
208
|
+
});
|
|
209
|
+
var merged = [];
|
|
210
|
+
for (var _i = 0, _ranges = ranges; _i < _ranges.length; _i++) {
|
|
211
|
+
var range = _ranges[_i];
|
|
212
|
+
var last = merged[merged.length - 1];
|
|
213
|
+
if (last && range.from <= last.to) {
|
|
214
|
+
last.to = Math.max(last.to, range.to);
|
|
215
|
+
} else {
|
|
216
|
+
merged.push(_objectSpread({}, range));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return merged;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Maps the previous decoration set through the transaction and recomputes only the top-level blocks
|
|
224
|
+
* it touched, instead of rebuilding the whole set from a full document scan.
|
|
225
|
+
*/
|
|
226
|
+
export var updateDecorations = function updateDecorations(previousDecorationSet, tr, doc) {
|
|
227
|
+
var decorationSet = previousDecorationSet.map(tr.mapping, doc);
|
|
228
|
+
var _iterator = _createForOfIteratorHelper(getDirtyTopLevelRanges(tr, doc)),
|
|
229
|
+
_step;
|
|
230
|
+
try {
|
|
231
|
+
var _loop = function _loop() {
|
|
232
|
+
var _step$value = _step.value,
|
|
233
|
+
from = _step$value.from,
|
|
234
|
+
to = _step$value.to;
|
|
235
|
+
// find() also returns decorations that merely touch the range, so removal is restricted to
|
|
236
|
+
// the ones fully inside it — those are exactly the decorations recomputed below. Removing a
|
|
237
|
+
// decoration that only touches the boundary would drop it permanently.
|
|
238
|
+
var stale = decorationSet.find(from, to).filter(function (decoration) {
|
|
239
|
+
return decoration.from >= from && decoration.to <= to;
|
|
240
|
+
});
|
|
241
|
+
if (stale.length > 0) {
|
|
242
|
+
decorationSet = decorationSet.remove(stale);
|
|
243
|
+
}
|
|
244
|
+
var fresh = getDecorationsForRange(doc, from, to);
|
|
245
|
+
if (fresh.length > 0) {
|
|
246
|
+
decorationSet = decorationSet.add(doc, fresh);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
250
|
+
_loop();
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
_iterator.e(err);
|
|
254
|
+
} finally {
|
|
255
|
+
_iterator.f();
|
|
256
|
+
}
|
|
257
|
+
return decorationSet;
|
|
258
|
+
};
|
|
71
259
|
var getListState = function getListState(doc, selection) {
|
|
72
260
|
var _doc$type$schema$node = doc.type.schema.nodes,
|
|
73
261
|
bulletList = _doc$type$schema$node.bulletList,
|
|
@@ -85,12 +273,19 @@ var getListState = function getListState(doc, selection) {
|
|
|
85
273
|
orderedListDisabled: orderedListDisabled
|
|
86
274
|
};
|
|
87
275
|
};
|
|
88
|
-
var
|
|
276
|
+
var handleDocChangedOld = function handleDocChangedOld(featureFlags) {
|
|
277
|
+
return function (tr, pluginState, editorState) {
|
|
278
|
+
var nextPluginState = handleSelectionChanged(tr, pluginState);
|
|
279
|
+
return _objectSpread(_objectSpread({}, nextPluginState), {}, {
|
|
280
|
+
decorationSet: getDecorations(tr.doc, editorState, featureFlags)
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
var handleDocChangedNew = function handleDocChangedNew() {
|
|
89
285
|
return function (tr, pluginState, editorState) {
|
|
90
286
|
var nextPluginState = handleSelectionChanged(tr, pluginState);
|
|
91
|
-
var decorationSet = getDecorations(tr.doc, editorState, featureFlags);
|
|
92
287
|
return _objectSpread(_objectSpread({}, nextPluginState), {}, {
|
|
93
|
-
decorationSet: decorationSet
|
|
288
|
+
decorationSet: updateDecorations(pluginState.decorationSet, tr, tr.doc)
|
|
94
289
|
});
|
|
95
290
|
};
|
|
96
291
|
};
|
|
@@ -116,7 +311,7 @@ var reducer = function reducer() {
|
|
|
116
311
|
return state;
|
|
117
312
|
};
|
|
118
313
|
};
|
|
119
|
-
var
|
|
314
|
+
var createInitialStateOld = function createInitialStateOld(featureFlags, api) {
|
|
120
315
|
return function (state) {
|
|
121
316
|
var isToolbarAIFCEnabled = Boolean(api === null || api === void 0 ? void 0 : api.toolbar);
|
|
122
317
|
return _objectSpread(_objectSpread({}, isToolbarAIFCEnabled ? getListState(state.doc, state.selection) : initialState), {}, {
|
|
@@ -124,15 +319,25 @@ var createInitialState = function createInitialState(featureFlags, api) {
|
|
|
124
319
|
});
|
|
125
320
|
};
|
|
126
321
|
};
|
|
322
|
+
var createInitialStateNew = function createInitialStateNew(featureFlags, api) {
|
|
323
|
+
return function (state) {
|
|
324
|
+
var isToolbarAIFCEnabled = Boolean(api === null || api === void 0 ? void 0 : api.toolbar);
|
|
325
|
+
return _objectSpread(_objectSpread({}, isToolbarAIFCEnabled ? getListState(state.doc, state.selection) : initialState), {}, {
|
|
326
|
+
decorationSet: getDecorationsForDocument(state.doc)
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
};
|
|
127
330
|
export var createPlugin = function createPlugin(eventDispatch, featureFlags, api) {
|
|
128
331
|
var _pluginFactory = pluginFactory(listPluginKey, reducer(), {
|
|
129
|
-
|
|
332
|
+
// Resolved once per editor instance rather than per transaction, so the exposure event fires
|
|
333
|
+
// once and the decoration hot path stays free of experiment lookups.
|
|
334
|
+
onDocChanged: isExperimentEnabled('platform_editor_list_performance_improv') ? handleDocChangedNew() : handleDocChangedOld(featureFlags),
|
|
130
335
|
onSelectionChanged: handleSelectionChanged
|
|
131
336
|
}),
|
|
132
337
|
getPluginState = _pluginFactory.getPluginState,
|
|
133
338
|
createPluginState = _pluginFactory.createPluginState;
|
|
134
339
|
return new SafePlugin({
|
|
135
|
-
state: createPluginState(eventDispatch,
|
|
340
|
+
state: createPluginState(eventDispatch, isExperimentEnabled('platform_editor_list_performance_improv') ? createInitialStateNew(featureFlags, api) : createInitialStateOld(featureFlags, api)),
|
|
136
341
|
key: listPluginKey,
|
|
137
342
|
appendTransaction: function appendTransaction(transactions, _oldState, newState) {
|
|
138
343
|
if (!expValEqualsNoExposure('platform_editor_flexible_list_schema', 'isEnabled', true)) {
|
|
@@ -2,11 +2,27 @@ import type { Dispatch } from '@atlaskit/editor-common/event-dispatcher';
|
|
|
2
2
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
3
3
|
import type { ExtractInjectionAPI, FeatureFlags } from '@atlaskit/editor-common/types';
|
|
4
4
|
import type { Node } from '@atlaskit/editor-prosemirror/model';
|
|
5
|
-
import type { EditorState } from '@atlaskit/editor-prosemirror/state';
|
|
5
|
+
import type { EditorState, ReadonlyTransaction } from '@atlaskit/editor-prosemirror/state';
|
|
6
6
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
7
7
|
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
8
8
|
import type { ListPlugin } from '../listPluginType';
|
|
9
9
|
import type { ListState } from '../types';
|
|
10
10
|
export declare const pluginKey: PluginKey<ListState>;
|
|
11
|
+
/**
|
|
12
|
+
* Full-document rebuild on the improved path, used when the decoration set has no previous value
|
|
13
|
+
* to update. The whole document is just one aligned range.
|
|
14
|
+
*/
|
|
15
|
+
export declare const getDecorationsForDocument: (doc: Node) => DecorationSet;
|
|
11
16
|
export declare const getDecorations: (doc: Node, _state: EditorState, _featureFlags: FeatureFlags) => DecorationSet;
|
|
17
|
+
/**
|
|
18
|
+
* The parts of a transaction the decoration update needs, so that both `Transaction` and
|
|
19
|
+
* `ReadonlyTransaction` can be passed in.
|
|
20
|
+
*/
|
|
21
|
+
type TransactionSteps = Pick<ReadonlyTransaction, 'mapping' | 'steps'>;
|
|
22
|
+
/**
|
|
23
|
+
* Maps the previous decoration set through the transaction and recomputes only the top-level blocks
|
|
24
|
+
* it touched, instead of rebuilding the whole set from a full document scan.
|
|
25
|
+
*/
|
|
26
|
+
export declare const updateDecorations: (previousDecorationSet: DecorationSet, tr: TransactionSteps, doc: Node) => DecorationSet;
|
|
12
27
|
export declare const createPlugin: (eventDispatch: Dispatch, featureFlags: FeatureFlags, api?: ExtractInjectionAPI<ListPlugin>) => SafePlugin;
|
|
28
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-list",
|
|
3
|
-
"version": "19.0.
|
|
3
|
+
"version": "19.0.4",
|
|
4
4
|
"description": "List plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@atlaskit/platform-feature-flags": "^2.2.0",
|
|
34
34
|
"@atlaskit/prosemirror-history": "^1.2.0",
|
|
35
35
|
"@atlaskit/prosemirror-input-rules": "^4.1.0",
|
|
36
|
-
"@atlaskit/tmp-editor-statsig": "^
|
|
36
|
+
"@atlaskit/tmp-editor-statsig": "^182.0.0",
|
|
37
37
|
"@babel/runtime": "^7.0.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|