@atlaskit/editor-plugin-table 2.7.2 → 2.8.0

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.
@@ -8,12 +8,7 @@ import React, {
8
8
 
9
9
  import rafSchd from 'raf-schd';
10
10
 
11
- import {
12
- ACTION_SUBJECT,
13
- EVENT_TYPE,
14
- TABLE_ACTION,
15
- TableEventPayload,
16
- } from '@atlaskit/editor-common/analytics';
11
+ import { TableEventPayload } from '@atlaskit/editor-common/analytics';
17
12
  import { getGuidelinesWithHighlights } from '@atlaskit/editor-common/guideline';
18
13
  import type { GuidelineConfig } from '@atlaskit/editor-common/guideline';
19
14
  import {
@@ -25,21 +20,24 @@ import { resizerHandleShadowClassName } from '@atlaskit/editor-common/styles';
25
20
  import { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
26
21
  import { Transaction } from '@atlaskit/editor-prosemirror/state';
27
22
  import { EditorView } from '@atlaskit/editor-prosemirror/view';
28
- import { TableMap } from '@atlaskit/editor-tables';
29
23
  import { findTable } from '@atlaskit/editor-tables/utils';
30
24
 
31
25
  import {
32
26
  COLUMN_MIN_WIDTH,
33
27
  getColgroupChildrenLength,
34
- hasTableBeenResized,
35
28
  previewScaleTable,
36
29
  scaleTable,
37
30
  } from '../pm-plugins/table-resizing/utils';
38
31
  import { pluginKey as tableWidthPluginKey } from '../pm-plugins/table-width';
39
32
  import { TABLE_HIGHLIGHT_GAP, TABLE_SNAP_GAP } from '../ui/consts';
40
- import { getTableWidth } from '../utils';
33
+ import {
34
+ generateResizedPayload,
35
+ generateResizeFrameRatePayloads,
36
+ useMeasureFramerate,
37
+ } from '../utils/analytics';
41
38
  import { defaultGuidelines, defaultGuidelineWidths } from '../utils/guidelines';
42
39
  import { findClosestSnap } from '../utils/snapping';
40
+
43
41
  interface TableResizerProps {
44
42
  width: number;
45
43
  maxWidth: number;
@@ -57,29 +55,6 @@ interface TableResizerProps {
57
55
  const handles = { right: true };
58
56
  const tableHandleMarginTop = 12;
59
57
 
60
- const generateResizedPayload = (props: {
61
- originalNode: PMNode;
62
- resizedNode: PMNode;
63
- }): TableEventPayload => {
64
- const tableMap = TableMap.get(props.resizedNode);
65
-
66
- return {
67
- action: TABLE_ACTION.RESIZED,
68
- actionSubject: ACTION_SUBJECT.TABLE,
69
- eventType: EVENT_TYPE.TRACK,
70
- attributes: {
71
- newWidth: props.resizedNode.attrs.width,
72
- prevWidth: props.originalNode.attrs.width ?? null,
73
- nodeSize: props.resizedNode.nodeSize,
74
- totalTableWidth: hasTableBeenResized(props.resizedNode)
75
- ? getTableWidth(props.resizedNode)
76
- : null,
77
- totalRowCount: tableMap.height,
78
- totalColumnCount: tableMap.width,
79
- },
80
- };
81
- };
82
-
83
58
  const getResizerHandleHeight = (tableRef: HTMLTableElement) => {
84
59
  const tableHeight = tableRef?.clientHeight;
85
60
  let handleHeightSize: HandleHeightSizeType | undefined = 'small';
@@ -129,6 +104,8 @@ export const TableResizer = ({
129
104
  const resizerMinWidth = getResizerMinWidth(node);
130
105
  const handleHeightSize = getResizerHandleHeight(tableRef);
131
106
 
107
+ const { startMeasure, endMeasure, countFrames } = useMeasureFramerate();
108
+
132
109
  const updateActiveGuidelines = useCallback(
133
110
  ({ gap, keys }: { gap: number; keys: string[] }) => {
134
111
  if (gap !== currentGap.current) {
@@ -157,6 +134,8 @@ export const TableResizer = ({
157
134
  );
158
135
 
159
136
  const handleResizeStart = useCallback(() => {
137
+ startMeasure();
138
+
160
139
  const {
161
140
  dispatch,
162
141
  state: { tr },
@@ -165,7 +144,7 @@ export const TableResizer = ({
165
144
  dispatch(tr.setMeta(tableWidthPluginKey, { resizing: true }));
166
145
 
167
146
  setSnappingEnabled(displayGuideline(defaultGuidelines));
168
- }, [displayGuideline, editorView]);
147
+ }, [displayGuideline, editorView, startMeasure]);
169
148
 
170
149
  const handleResizeStop = useCallback<HandleResize>(
171
150
  (originalState, delta) => {
@@ -174,6 +153,18 @@ export const TableResizer = ({
174
153
  const pos = getPos();
175
154
 
176
155
  let tr = state.tr.setMeta(tableWidthPluginKey, { resizing: false });
156
+ const frameRateSamples = endMeasure();
157
+
158
+ if (frameRateSamples.length > 0) {
159
+ const resizeFrameRatePayloads = generateResizeFrameRatePayloads({
160
+ docSize: state.doc.nodeSize,
161
+ frameRateSamples,
162
+ originalNode: node,
163
+ });
164
+ resizeFrameRatePayloads.forEach((payload) => {
165
+ attachAnalyticsEvent(payload)?.(tr);
166
+ });
167
+ }
177
168
 
178
169
  if (typeof pos === 'number') {
179
170
  tr = tr.setNodeMarkup(pos, undefined, {
@@ -219,11 +210,13 @@ export const TableResizer = ({
219
210
  tableRef,
220
211
  displayGuideline,
221
212
  attachAnalyticsEvent,
213
+ endMeasure,
222
214
  ],
223
215
  );
224
216
 
225
217
  const handleResize = useCallback(
226
218
  (originalState, delta) => {
219
+ countFrames();
227
220
  const newWidth = originalState.width + delta.width;
228
221
  const pos = getPos();
229
222
  if (typeof pos !== 'number') {
@@ -254,7 +247,15 @@ export const TableResizer = ({
254
247
 
255
248
  return newWidth;
256
249
  },
257
- [editorView, getPos, node, tableRef, updateWidth, updateActiveGuidelines],
250
+ [
251
+ editorView,
252
+ getPos,
253
+ node,
254
+ tableRef,
255
+ updateWidth,
256
+ updateActiveGuidelines,
257
+ countFrames,
258
+ ],
258
259
  );
259
260
 
260
261
  const scheduleResize = useMemo(() => rafSchd(handleResize), [handleResize]);
@@ -1,13 +1,25 @@
1
+ import { useEffect, useRef } from 'react';
2
+
1
3
  import type {
2
4
  AnalyticsEventPayload,
3
5
  AnalyticsEventPayloadCallback,
4
6
  EditorAnalyticsAPI,
7
+ TableEventPayload,
8
+ } from '@atlaskit/editor-common/analytics';
9
+ import {
10
+ ACTION_SUBJECT,
11
+ EVENT_TYPE,
12
+ TABLE_ACTION,
5
13
  } from '@atlaskit/editor-common/analytics';
6
14
  import { HigherOrderCommand } from '@atlaskit/editor-common/types';
15
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
7
16
  import { Selection } from '@atlaskit/editor-prosemirror/state';
8
17
  import { TableMap } from '@atlaskit/editor-tables/table-map';
9
18
  import { findTable, getSelectionRect } from '@atlaskit/editor-tables/utils';
10
19
 
20
+ import { hasTableBeenResized } from '../pm-plugins/table-resizing/utils';
21
+ import { getTableWidth } from '../utils';
22
+
11
23
  export function getSelectedTableInfo(selection: Selection) {
12
24
  let map;
13
25
  let totalRowCount = 0;
@@ -78,3 +90,159 @@ export const withEditorAnalyticsAPI =
78
90
  view,
79
91
  );
80
92
  };
93
+
94
+ interface UseMeasureFramerateConfig {
95
+ maxSamples?: number;
96
+ minFrames?: number;
97
+ minTimeMs?: number;
98
+ sampleRateMs?: number;
99
+ timeoutMs?: number;
100
+ }
101
+
102
+ export const generateResizedPayload = (props: {
103
+ originalNode: PMNode;
104
+ resizedNode: PMNode;
105
+ }): TableEventPayload => {
106
+ const tableMap = TableMap.get(props.resizedNode);
107
+
108
+ return {
109
+ action: TABLE_ACTION.RESIZED,
110
+ actionSubject: ACTION_SUBJECT.TABLE,
111
+ eventType: EVENT_TYPE.TRACK,
112
+ attributes: {
113
+ newWidth: props.resizedNode.attrs.width,
114
+ prevWidth: props.originalNode.attrs.width ?? null,
115
+ nodeSize: props.resizedNode.nodeSize,
116
+ totalTableWidth: hasTableBeenResized(props.resizedNode)
117
+ ? getTableWidth(props.resizedNode)
118
+ : null,
119
+ totalRowCount: tableMap.height,
120
+ totalColumnCount: tableMap.width,
121
+ },
122
+ };
123
+ };
124
+
125
+ export const reduceResizeFrameRateSamples = (frameRateSamples: number[]) => {
126
+ if (frameRateSamples.length > 1) {
127
+ const frameRateSum = frameRateSamples.reduce((sum, frameRate, index) => {
128
+ if (index === 0) {
129
+ return sum;
130
+ } else {
131
+ return sum + frameRate;
132
+ }
133
+ }, 0);
134
+ const averageFrameRate = Math.round(
135
+ frameRateSum / (frameRateSamples.length - 1),
136
+ );
137
+ return [frameRateSamples[0], averageFrameRate];
138
+ } else {
139
+ return frameRateSamples;
140
+ }
141
+ };
142
+
143
+ export const generateResizeFrameRatePayloads = (props: {
144
+ docSize: number;
145
+ frameRateSamples: number[];
146
+ originalNode: PMNode;
147
+ }): TableEventPayload[] => {
148
+ const reducedResizeFrameRateSamples = reduceResizeFrameRateSamples(
149
+ props.frameRateSamples,
150
+ );
151
+ return reducedResizeFrameRateSamples.map((frameRateSample, index) => ({
152
+ action: TABLE_ACTION.RESIZE_PERF_SAMPLING,
153
+ actionSubject: ACTION_SUBJECT.TABLE,
154
+ eventType: EVENT_TYPE.OPERATIONAL,
155
+ attributes: {
156
+ frameRate: frameRateSample,
157
+ nodeSize: props.originalNode.nodeSize,
158
+ docSize: props.docSize,
159
+ isInitialSample: index === 0,
160
+ },
161
+ }));
162
+ };
163
+
164
+ /**
165
+ * Measures the framerate of a component over a given time period.
166
+ */
167
+ export const useMeasureFramerate = (config: UseMeasureFramerateConfig = {}) => {
168
+ const {
169
+ maxSamples = 10,
170
+ minFrames = 5,
171
+ minTimeMs = 500,
172
+ sampleRateMs = 1000,
173
+ timeoutMs = 200,
174
+ } = config;
175
+
176
+ let frameCount = useRef(0);
177
+ let lastTime = useRef(0);
178
+ let timeoutId = useRef<NodeJS.Timeout | undefined>();
179
+ let frameRateSamples = useRef<number[]>([]);
180
+
181
+ useEffect(() => {
182
+ return () => {
183
+ if (timeoutId.current) {
184
+ clearTimeout(timeoutId.current);
185
+ }
186
+ };
187
+ }, []);
188
+
189
+ const startMeasure = () => {
190
+ frameCount.current = 0;
191
+ lastTime.current = performance.now();
192
+ };
193
+
194
+ /**
195
+ * Returns an array of frame rate samples as integers.
196
+ */
197
+ const endMeasure = () => {
198
+ const samples = frameRateSamples.current;
199
+ frameRateSamples.current = [];
200
+ return samples;
201
+ };
202
+
203
+ const sampleFrameRate = (delay = 0) => {
204
+ const currentTime = performance.now();
205
+ const deltaTime = currentTime - lastTime.current - delay;
206
+ const isValidSample =
207
+ deltaTime > minTimeMs && frameCount.current >= minFrames;
208
+ if (isValidSample) {
209
+ const frameRate = Math.round(frameCount.current / (deltaTime / 1000));
210
+ frameRateSamples.current.push(frameRate);
211
+ }
212
+ frameCount.current = 0;
213
+ lastTime.current = 0;
214
+ };
215
+
216
+ /**
217
+ * Counts the number of frames that occur within a given time period. Intended to be called
218
+ * inside a `requestAnimationFrame` callback.
219
+ */
220
+ const countFrames = () => {
221
+ if (frameRateSamples.current.length >= maxSamples && timeoutId.current) {
222
+ clearTimeout(timeoutId.current);
223
+ return;
224
+ }
225
+
226
+ /**
227
+ * Allows us to keep counting frames even if `startMeasure` is not called
228
+ */
229
+ if (lastTime.current === 0) {
230
+ lastTime.current = performance.now();
231
+ }
232
+ frameCount.current++;
233
+
234
+ if (timeoutId.current) {
235
+ clearTimeout(timeoutId.current);
236
+ }
237
+ if (performance.now() - lastTime.current > sampleRateMs) {
238
+ sampleFrameRate();
239
+ } else {
240
+ timeoutId.current = setTimeout(
241
+ () => sampleFrameRate(timeoutMs),
242
+ timeoutMs,
243
+ );
244
+ }
245
+ };
246
+
247
+ return { startMeasure, endMeasure, countFrames };
248
+ };