box-annotations 5.2.1-beta.1 → 5.2.1-beta.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.
Files changed (38) hide show
  1. package/dist/annotations.css +1 -1
  2. package/dist/annotations.js +1 -1
  3. package/i18n/en-US.js +2 -0
  4. package/i18n/en-US.properties +4 -0
  5. package/i18n/json/src/components/BoundingBoxHighlight/messages.json +1 -0
  6. package/package.json +1 -1
  7. package/src/@types/events.ts +4 -0
  8. package/src/boundingBoxHighlight/BoundingBoxHighlightContainer.tsx +33 -0
  9. package/src/boundingBoxHighlight/BoundingBoxHighlightManager.tsx +19 -0
  10. package/src/boundingBoxHighlight/index.ts +1 -0
  11. package/src/common/BaseAnnotator.ts +78 -0
  12. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.scss +8 -0
  13. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.tsx +67 -0
  14. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.scss +73 -0
  15. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.tsx +49 -0
  16. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.scss +22 -0
  17. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.tsx +77 -0
  18. package/src/components/BoundingBoxHighlight/index.ts +4 -0
  19. package/src/components/BoundingBoxHighlight/messages.ts +14 -0
  20. package/src/document/DocumentAnnotator.ts +69 -4
  21. package/src/icons/ChevronLeft.tsx +24 -0
  22. package/src/icons/ChevronRight.tsx +24 -0
  23. package/src/store/annotations/reducer.ts +6 -1
  24. package/src/store/boundingBoxHighlights/actions.ts +6 -0
  25. package/src/store/boundingBoxHighlights/index.ts +4 -0
  26. package/src/store/boundingBoxHighlights/reducer.ts +33 -0
  27. package/src/store/boundingBoxHighlights/selectors.ts +12 -0
  28. package/src/store/boundingBoxHighlights/types.ts +13 -0
  29. package/src/store/createRootReducer.ts +2 -0
  30. package/src/store/eventing/boundingBoxHighlightNav.ts +13 -0
  31. package/src/store/eventing/middleware.ts +3 -0
  32. package/src/store/index.ts +1 -0
  33. package/src/store/options/actions.ts +2 -0
  34. package/src/store/options/reducer.ts +6 -1
  35. package/src/store/options/selectors.ts +2 -0
  36. package/src/store/options/types.ts +4 -0
  37. package/src/store/types.ts +2 -0
  38. package/src/utils/scroll.ts +8 -3
@@ -8,15 +8,19 @@ import {
8
8
  HighlightManager,
9
9
  isHighlight,
10
10
  } from '../highlight';
11
+ import { BoundingBoxHighlightManager } from '../boundingBoxHighlight';
11
12
  import { centerRegion, isRegion, RegionCreationManager, RegionManager } from '../region';
12
13
  import { Event } from '../@types';
13
14
  import { getAnnotation } from '../store/annotations';
15
+ import { getBoundingBoxHighlights, BoundingBox } from '../store/boundingBoxHighlights';
14
16
  import { getSelection } from './docUtil';
15
17
  import { Manager } from '../common/BaseManager';
16
- import { getFileId, getIsCurrentFileVersion, Mode } from '../store';
18
+ import { getFileId, getIsCurrentFileVersion, getViewMode, Mode } from '../store';
17
19
  import { scrollToLocation } from '../utils/scroll';
18
20
  import './DocumentAnnotator.scss';
19
21
 
22
+ import type { ViewMode } from '../store/options/types';
23
+
20
24
  // Pdf.js textLayer enhancement requires waiting for 300ms (they hardcode this magic number)
21
25
  const TEXT_LAYER_ENHANCEMENT = 300;
22
26
 
@@ -27,6 +31,9 @@ export default class DocumentAnnotator extends BaseAnnotator {
27
31
 
28
32
  managers: Map<number, Set<Manager>> = new Map();
29
33
 
34
+ /** Tracks which view mode the current managers were created for. Used to destroy/recreate when switching. */
35
+ private managersViewMode: ViewMode | null = null;
36
+
30
37
  constructor(options: Options) {
31
38
  super(options);
32
39
 
@@ -53,13 +60,22 @@ export default class DocumentAnnotator extends BaseAnnotator {
53
60
  return this.containerEl?.querySelector('.bp-doc');
54
61
  }
55
62
 
63
+ /** Destroys all managers and clears the cache. Call when switching view modes. */
64
+ private clearManagers(): void {
65
+ this.managers.forEach(managers => managers.forEach(manager => manager.destroy()));
66
+ this.managers.clear();
67
+ this.managersViewMode = null;
68
+ }
69
+
56
70
  getPageManagers(pageEl: HTMLElement): Set<Manager> {
57
71
  const fileId = getFileId(this.store.getState());
58
72
  const isCurrentFileVersion = getIsCurrentFileVersion(this.store.getState());
59
73
  const pageNumber = this.getPageNumber(pageEl);
60
74
  const pageReferenceEl = this.getPageReference(pageEl);
61
- const managers = this.managers.get(pageNumber) || new Set();
62
75
  const resinTags = { fileid: fileId, iscurrent: isCurrentFileVersion };
76
+ const viewMode = getViewMode(this.store.getState());
77
+
78
+ const managers = this.managers.get(pageNumber) || new Set();
63
79
  let destroyManagers = false;
64
80
 
65
81
  // If a referenced page element doesn't exist, destroy all managers to keep them in sync
@@ -77,6 +93,12 @@ export default class DocumentAnnotator extends BaseAnnotator {
77
93
 
78
94
  // Lazily instantiate managers as pages are added or re-rendered
79
95
  if (managers.size === 0) {
96
+ if (viewMode === 'boundingBoxes') {
97
+ managers.add(new BoundingBoxHighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
98
+ return managers;
99
+ }
100
+
101
+ // Annotations mode
80
102
  managers.add(new PopupManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
81
103
  managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
82
104
 
@@ -94,7 +116,6 @@ export default class DocumentAnnotator extends BaseAnnotator {
94
116
  }
95
117
 
96
118
  managers.add(new HighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
97
-
98
119
  managers.add(new RegionManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
99
120
 
100
121
  const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
@@ -132,7 +153,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
132
153
  }
133
154
 
134
155
  handleChangeMode = ({ mode }: { mode: Mode }): void => {
135
- if (!this.annotatedEl) {
156
+ if (!this.annotatedEl || getViewMode(this.store.getState()) === 'boundingBoxes') {
136
157
  return;
137
158
  }
138
159
 
@@ -146,6 +167,13 @@ export default class DocumentAnnotator extends BaseAnnotator {
146
167
  };
147
168
 
148
169
  render(): void {
170
+ const viewMode = getViewMode(this.store.getState());
171
+
172
+ if (this.managersViewMode !== null && this.managersViewMode !== viewMode) {
173
+ this.clearManagers();
174
+ }
175
+ this.managersViewMode = viewMode;
176
+
149
177
  this.getPages()
150
178
  .filter(({ dataset }) => dataset.loaded && dataset.pageNumber)
151
179
  .forEach(pageEl => this.renderPage(pageEl));
@@ -153,6 +181,18 @@ export default class DocumentAnnotator extends BaseAnnotator {
153
181
  this.postRender();
154
182
  }
155
183
 
184
+ public postRender(): void {
185
+ // DeselectManager is only needed for annotation creation; skip in bounding box mode
186
+ if (getViewMode(this.store.getState()) === 'boundingBoxes') {
187
+ if (this.deselectManager) {
188
+ this.deselectManager.destroy();
189
+ this.deselectManager = null;
190
+ }
191
+ return;
192
+ }
193
+ super.postRender();
194
+ }
195
+
156
196
  renderPage(pageEl: HTMLElement): void {
157
197
  const pageManagers = this.getPageManagers(pageEl);
158
198
  const pageNumber = this.getPageNumber(pageEl);
@@ -194,4 +234,29 @@ export default class DocumentAnnotator extends BaseAnnotator {
194
234
  scrollToLocation(this.annotatedEl, annotationPageEl, { offsets });
195
235
  }
196
236
  }
237
+
238
+ scrollToBoundingBoxHighlight(highlightId: string | null): void {
239
+ if (!highlightId || !this.annotatedEl) {
240
+ return;
241
+ }
242
+
243
+ const highlights = getBoundingBoxHighlights(this.store.getState());
244
+ const highlight = highlights.find((h: BoundingBox) => h.id === highlightId);
245
+
246
+ if (!highlight) {
247
+ return;
248
+ }
249
+
250
+ const pageEl = this.getPage(highlight.pageNumber);
251
+ if (!pageEl) {
252
+ return;
253
+ }
254
+
255
+ const offsets = {
256
+ x: highlight.x + highlight.width / 2,
257
+ y: highlight.y + highlight.height / 2,
258
+ };
259
+
260
+ scrollToLocation(this.annotatedEl, pageEl, { offsets, smooth: true });
261
+ }
197
262
  }
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ function ChevronLeft(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+ return (
5
+ <svg
6
+ data-testid="ChevronLeft"
7
+ fill="none"
8
+ focusable="false"
9
+ height="1em"
10
+ role="img"
11
+ viewBox="0 0 24 24"
12
+ width="1em"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ {...props}
15
+ >
16
+ <path
17
+ d="M13.293 4.293a1 1 0 1 1 1.414 1.414L8.414 12l6.293 6.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068L7 13.414a2 2 0 0 1 0-2.828l6.293-6.293Z"
18
+ fill="currentColor"
19
+ />
20
+ </svg>
21
+ );
22
+ }
23
+
24
+ export default ChevronLeft;
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ function ChevronRight(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+ return (
5
+ <svg
6
+ data-testid="ChevronRight"
7
+ fill="none"
8
+ focusable="false"
9
+ height="1em"
10
+ role="img"
11
+ viewBox="0 0 24 24"
12
+ width="1em"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ {...props}
15
+ >
16
+ <path
17
+ d="M9.293 4.293a1 1 0 0 1 1.414 0L17 10.586a2 2 0 0 1 0 2.828l-6.293 6.293a1 1 0 0 1-1.414-1.414L15.586 12 9.293 5.707a1 1 0 0 1 0-1.414Z"
18
+ fill="currentColor"
19
+ />
20
+ </svg>
21
+ );
22
+ }
23
+
24
+ export default ChevronRight;
@@ -8,13 +8,18 @@ import {
8
8
  setActiveAnnotationIdAction,
9
9
  setIsInitialized,
10
10
  } from './actions';
11
+ import { setViewModeAction } from '../options/actions';
11
12
 
12
13
  const activeAnnotationId = createReducer<AnnotationsState['activeId']>(null, builder =>
13
14
  builder
14
15
  /* Preview will set the active ID with a an event which will trigger the url change */
15
16
  .addCase(createAnnotationAction.fulfilled, () => null)
16
17
  .addCase(removeAnnotationAction, (state, { payload: id }) => (state === id ? null : state))
17
- .addCase(setActiveAnnotationIdAction, (state, { payload: annotationId }) => annotationId),
18
+ .addCase(setActiveAnnotationIdAction, (state, { payload: annotationId }) => annotationId)
19
+ .addCase(setViewModeAction, (state, { payload }) => {
20
+ // Clear active annotation when switching to bounding boxes mode
21
+ return payload === 'boundingBoxes' ? null : state;
22
+ }),
18
23
  );
19
24
 
20
25
  const annotationsAllIds = createReducer<AnnotationsState['allIds']>([], builder =>
@@ -0,0 +1,6 @@
1
+ import { createAction } from '@reduxjs/toolkit';
2
+ import { BoundingBox } from './types';
3
+
4
+ export const setBoundingBoxHighlightsAction = createAction<BoundingBox[]>('SET_BOUNDING_BOX_HIGHLIGHTS');
5
+ export const setSelectedBoundingBoxHighlightAction = createAction<string | null>('SET_SELECTED_BOUNDING_BOX_HIGHLIGHT');
6
+ export const navigateBoundingBoxHighlightAction = createAction<string>('NAVIGATE_BOUNDING_BOX_HIGHLIGHT');
@@ -0,0 +1,4 @@
1
+ export { default as boundingBoxHighlightsReducer } from './reducer';
2
+ export * from './actions';
3
+ export * from './selectors';
4
+ export * from './types';
@@ -0,0 +1,33 @@
1
+ import { createReducer } from '@reduxjs/toolkit';
2
+ import { BoundingBoxHighlightsState } from './types';
3
+ import {
4
+ navigateBoundingBoxHighlightAction,
5
+ setBoundingBoxHighlightsAction,
6
+ setSelectedBoundingBoxHighlightAction,
7
+ } from './actions';
8
+ import { setViewModeAction } from '../options/actions';
9
+
10
+ const initialState: BoundingBoxHighlightsState = {
11
+ boundingBoxes: [],
12
+ selectedId: null,
13
+ };
14
+
15
+ export default createReducer(initialState, builder => {
16
+ builder
17
+ .addCase(setBoundingBoxHighlightsAction, (state, { payload }) => {
18
+ state.boundingBoxes = payload;
19
+ state.selectedId = null;
20
+ })
21
+ .addCase(setSelectedBoundingBoxHighlightAction, (state, { payload }) => {
22
+ state.selectedId = payload;
23
+ })
24
+ .addCase(navigateBoundingBoxHighlightAction, (state, { payload }) => {
25
+ state.selectedId = payload;
26
+ })
27
+ .addCase(setViewModeAction, (state, { payload }) => {
28
+ // Clear selected bounding box when switching to annotations mode
29
+ if (payload === 'annotations') {
30
+ state.selectedId = null;
31
+ }
32
+ });
33
+ });
@@ -0,0 +1,12 @@
1
+ import { BoundingBox, BoundingBoxHighlightsState } from './types';
2
+
3
+ type State = { boundingBoxHighlights: BoundingBoxHighlightsState };
4
+
5
+ export const getBoundingBoxHighlights = ({ boundingBoxHighlights }: State): BoundingBox[] =>
6
+ boundingBoxHighlights.boundingBoxes;
7
+
8
+ export const getBoundingBoxHighlightsForPage = ({ boundingBoxHighlights }: State, pageNumber: number): BoundingBox[] =>
9
+ boundingBoxHighlights.boundingBoxes.filter(box => box.pageNumber === pageNumber);
10
+
11
+ export const getSelectedBoundingBoxHighlightId = ({ boundingBoxHighlights }: State): string | null =>
12
+ boundingBoxHighlights.selectedId;
@@ -0,0 +1,13 @@
1
+ export interface BoundingBox {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ pageNumber: number;
7
+ id: string;
8
+ }
9
+
10
+ export interface BoundingBoxHighlightsState {
11
+ boundingBoxes: BoundingBox[];
12
+ selectedId: string | null;
13
+ }
@@ -4,6 +4,7 @@ import { commonReducer } from './common';
4
4
  import { creatorReducer } from './creator';
5
5
  import { drawingReducer } from './drawing';
6
6
  import { highlightReducer } from './highlight';
7
+ import { boundingBoxHighlightsReducer } from './boundingBoxHighlights';
7
8
  import { optionsReducer } from './options';
8
9
  import { usersReducer } from './users';
9
10
 
@@ -14,6 +15,7 @@ const createRootReducer = (): Reducer =>
14
15
  creator: creatorReducer,
15
16
  drawing: drawingReducer,
16
17
  highlight: highlightReducer,
18
+ boundingBoxHighlights: boundingBoxHighlightsReducer,
17
19
  options: optionsReducer,
18
20
  users: usersReducer,
19
21
  });
@@ -0,0 +1,13 @@
1
+ import eventManager from '../../common/EventManager';
2
+ import { AppState } from '../types';
3
+ import { Event } from '../../@types';
4
+ import { getSelectedBoundingBoxHighlightId } from '../boundingBoxHighlights';
5
+
6
+ const handleNavigateBoundingBoxHighlight = (_prevState: AppState, nextState: AppState): void => {
7
+ const highlightId = getSelectedBoundingBoxHighlightId(nextState);
8
+ if (highlightId) {
9
+ eventManager.emit(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, highlightId);
10
+ }
11
+ };
12
+
13
+ export { handleNavigateBoundingBoxHighlight };
@@ -6,11 +6,13 @@ import {
6
6
  setActiveAnnotationIdAction,
7
7
  setIsInitialized,
8
8
  } from '../annotations/actions';
9
+ import { navigateBoundingBoxHighlightAction } from '../boundingBoxHighlights/actions';
9
10
  import { EventHandlerMap } from './types';
10
11
  import { handleActiveAnnotationEvents } from './active';
11
12
  import { handleAnnotationsInitialized } from './init';
12
13
  import { handleCreateErrorEvents, handleCreatePendingEvents, handleCreateSuccessEvents } from './create';
13
14
  import { handleFetchErrorEvents } from './fetch';
15
+ import { handleNavigateBoundingBoxHighlight } from './boundingBoxHighlightNav';
14
16
  import { handleResetCreatorAction, handleSetStagedAction } from './staged';
15
17
  import { handleSetStatusAction } from './status';
16
18
  import { handleToggleAnnotationModeAction } from './mode';
@@ -23,6 +25,7 @@ const eventHandlers: EventHandlerMap = {
23
25
  [createAnnotationAction.pending.toString()]: handleCreatePendingEvents,
24
26
  [createAnnotationAction.rejected.toString()]: handleCreateErrorEvents,
25
27
  [fetchAnnotationsAction.rejected.toString()]: handleFetchErrorEvents,
28
+ [navigateBoundingBoxHighlightAction.toString()]: handleNavigateBoundingBoxHighlight,
26
29
  [resetCreatorAction.toString()]: handleResetCreatorAction,
27
30
  [setActiveAnnotationIdAction.toString()]: handleActiveAnnotationEvents,
28
31
  [setIsInitialized.toString()]: handleAnnotationsInitialized,
@@ -5,6 +5,7 @@ export * from './annotations';
5
5
  export * from './common';
6
6
  export * from './creator';
7
7
  export * from './highlight';
8
+ export * from './boundingBoxHighlights';
8
9
  export * from './options';
9
10
  export * from './types';
10
11
  export * from './users';
@@ -1,8 +1,10 @@
1
1
  import { createAction } from '@reduxjs/toolkit';
2
2
  import { Permissions } from '../../@types';
3
+ import { ViewMode } from './types';
3
4
 
4
5
  export const setFileIdAction = createAction<string | null>('SET_FIlE_ID');
5
6
  export const setFileVersionIdAction = createAction<string | null>('SET_FILE_VERSION_ID');
6
7
  export const setPermissionsAction = createAction<Permissions>('SET_PERMISSIONS');
7
8
  export const setRotationAction = createAction<number>('SET_ROTATION');
8
9
  export const setScaleAction = createAction<number>('SET_SCALE');
10
+ export const setViewModeAction = createAction<ViewMode>('SET_VIEW_MODE');
@@ -1,11 +1,12 @@
1
1
  import { createReducer } from '@reduxjs/toolkit';
2
- import { OptionsState } from './types';
2
+ import { OptionsState, ViewMode } from './types';
3
3
  import {
4
4
  setFileIdAction,
5
5
  setFileVersionIdAction,
6
6
  setPermissionsAction,
7
7
  setRotationAction,
8
8
  setScaleAction,
9
+ setViewModeAction,
9
10
  } from './actions';
10
11
 
11
12
  export const initialState = {
@@ -16,6 +17,7 @@ export const initialState = {
16
17
  permissions: {},
17
18
  rotation: 0,
18
19
  scale: 1,
20
+ viewMode: 'annotations' as ViewMode,
19
21
  };
20
22
 
21
23
  export default createReducer<OptionsState>(initialState, builder =>
@@ -34,5 +36,8 @@ export default createReducer<OptionsState>(initialState, builder =>
34
36
  })
35
37
  .addCase(setScaleAction, (state, { payload }) => {
36
38
  state.scale = payload;
39
+ })
40
+ .addCase(setViewModeAction, (state, { payload }) => {
41
+ state.viewMode = payload;
37
42
  }),
38
43
  );
@@ -2,10 +2,12 @@ import getProp from 'lodash/get';
2
2
  import { AppState } from '../types';
3
3
  import { Permissions } from '../../@types';
4
4
  import { Features } from '../../BoxAnnotations';
5
+ import { ViewMode } from './types';
5
6
 
6
7
  type State = Pick<AppState, 'options'>;
7
8
 
8
9
  export const getFeatures = (state: State): Features => state.options.features;
10
+ export const getViewMode = (state: State): ViewMode => state.options.viewMode;
9
11
  export const getFileId = (state: State): string | null => state.options.fileId;
10
12
  export const getFileVersionId = (state: State): string | null => state.options.fileVersionId;
11
13
  export const getIsCurrentFileVersion = (state: State): boolean => state.options.isCurrentFileVersion;
@@ -1,6 +1,9 @@
1
1
  import { Features } from '../../BoxAnnotations';
2
2
  import { Permissions } from '../../@types';
3
3
 
4
+ /** View mode: annotations and bounding boxes are mutually exclusive. */
5
+ export type ViewMode = 'annotations' | 'boundingBoxes';
6
+
4
7
  export type OptionsState = {
5
8
  features: Features;
6
9
  fileId: string | null;
@@ -9,4 +12,5 @@ export type OptionsState = {
9
12
  permissions: Permissions;
10
13
  rotation: number;
11
14
  scale: number;
15
+ viewMode: ViewMode;
12
16
  };
@@ -6,6 +6,7 @@ import { CommonState } from './common';
6
6
  import { CreatorState } from './creator';
7
7
  import { DrawingState } from './drawing';
8
8
  import { HighlightState } from './highlight';
9
+ import { BoundingBoxHighlightsState } from './boundingBoxHighlights';
9
10
  import { OptionsState } from './options';
10
11
  import { UsersState } from './users';
11
12
 
@@ -15,6 +16,7 @@ export type AppState = {
15
16
  creator: CreatorState;
16
17
  drawing: DrawingState;
17
18
  highlight: HighlightState;
19
+ boundingBoxHighlights: BoundingBoxHighlightsState;
18
20
  options: OptionsState;
19
21
  users: UsersState;
20
22
  };
@@ -2,13 +2,14 @@ import { Position } from '../@types';
2
2
 
3
3
  export type ScrollOptions = {
4
4
  offsets?: Position;
5
+ smooth?: boolean;
5
6
  threshold?: number;
6
7
  };
7
8
 
8
9
  const DEFAULT_OFFSETS = { x: 0, y: 0 };
9
10
 
10
11
  export function scrollToLocation(parentEl: HTMLElement, referenceEl: HTMLElement, options: ScrollOptions = {}): void {
11
- const { offsets = DEFAULT_OFFSETS } = options;
12
+ const { offsets = DEFAULT_OFFSETS, smooth = false } = options;
12
13
  const { x: offsetXPercentage, y: offsetYPercentage } = offsets;
13
14
  // Get the bounding client rects so that the offsetLeft of the reference element can be calculated --
14
15
  // even if it has been transformed via rotation
@@ -40,6 +41,10 @@ export function scrollToLocation(parentEl: HTMLElement, referenceEl: HTMLElement
40
41
  const scrollLeft = Math.max(0, Math.min(parentEl.scrollLeft + offsetScrollLeft, parentEl.scrollWidth));
41
42
  const scrollTop = Math.max(0, Math.min(parentEl.scrollTop + offsetScrollTop, parentEl.scrollHeight));
42
43
 
43
- parentEl.scrollLeft = scrollLeft;
44
- parentEl.scrollTop = scrollTop;
44
+ if (smooth) {
45
+ parentEl.scrollTo({ left: scrollLeft, top: scrollTop, behavior: 'smooth' });
46
+ } else {
47
+ parentEl.scrollLeft = scrollLeft;
48
+ parentEl.scrollTop = scrollTop;
49
+ }
45
50
  }