@semiont/react-ui 0.2.36 → 0.2.37

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 (33) hide show
  1. package/dist/index.d.mts +8 -0
  2. package/dist/index.mjs +252 -166
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +1 -1
  5. package/src/components/CodeMirrorRenderer.tsx +71 -203
  6. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +142 -0
  7. package/src/components/__tests__/LiveRegion.hooks.test.tsx +79 -0
  8. package/src/components/__tests__/ResizeHandle.test.tsx +165 -0
  9. package/src/components/__tests__/SessionExpiryBanner.test.tsx +123 -0
  10. package/src/components/__tests__/StatusDisplay.test.tsx +160 -0
  11. package/src/components/__tests__/Toolbar.test.tsx +110 -0
  12. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +285 -0
  13. package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +273 -0
  14. package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +90 -0
  15. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +129 -0
  16. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +180 -0
  17. package/src/components/navigation/__tests__/ObservableLink.test.tsx +90 -0
  18. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +169 -0
  19. package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +371 -0
  20. package/src/components/resource/AnnotateView.tsx +27 -153
  21. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +349 -0
  22. package/src/components/resource/__tests__/HistoryEvent.test.tsx +492 -0
  23. package/src/components/resource/__tests__/event-formatting.test.ts +273 -0
  24. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +226 -0
  25. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +188 -0
  26. package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +69 -0
  27. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +445 -0
  28. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +271 -0
  29. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +210 -0
  30. package/src/components/settings/__tests__/SettingsPanel.test.tsx +190 -0
  31. package/src/components/viewers/__tests__/ImageViewer.test.tsx +63 -0
  32. package/src/integrations/__tests__/css-modules-helper.test.tsx +225 -0
  33. package/src/integrations/__tests__/styled-components-theme.test.ts +179 -0
@@ -0,0 +1,371 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import React from 'react';
3
+ import { screen, fireEvent } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+ import { renderWithProviders } from '../../../test-utils';
6
+ import type { SortableResourceTabProps } from '../../../types/collapsible-navigation';
7
+
8
+ // Mock @dnd-kit/sortable
9
+ const mockSetNodeRef = vi.fn();
10
+ const mockSortableReturn = {
11
+ attributes: { role: 'button', tabIndex: 0 },
12
+ listeners: {},
13
+ setNodeRef: mockSetNodeRef,
14
+ transform: null,
15
+ transition: null,
16
+ isDragging: false,
17
+ };
18
+
19
+ vi.mock('@dnd-kit/sortable', () => ({
20
+ useSortable: vi.fn(() => mockSortableReturn),
21
+ }));
22
+
23
+ vi.mock('@dnd-kit/utilities', () => ({
24
+ CSS: {
25
+ Transform: {
26
+ toString: vi.fn((transform: any) => (transform ? `translate(${transform.x}px, ${transform.y}px)` : undefined)),
27
+ },
28
+ },
29
+ }));
30
+
31
+ vi.mock('../../../lib/resource-utils', () => ({
32
+ getResourceIcon: vi.fn((mediaType: string | undefined) => {
33
+ if (!mediaType) return '\u{1F4C4}';
34
+ if (mediaType.startsWith('image/')) return '\u{1F5BC}\uFE0F';
35
+ if (mediaType === 'text/markdown') return '\u{1F4DD}';
36
+ return '\u{1F4C4}';
37
+ }),
38
+ }));
39
+
40
+ import { useSortable } from '@dnd-kit/sortable';
41
+ import { SortableResourceTab } from '../SortableResourceTab';
42
+
43
+ describe('SortableResourceTab', () => {
44
+ const MockLink = ({ href, children, ...props }: any) => (
45
+ <a href={href} {...props}>
46
+ {children}
47
+ </a>
48
+ );
49
+
50
+ const defaultProps: SortableResourceTabProps = {
51
+ resource: {
52
+ id: 'resource-1',
53
+ name: 'Test Document',
54
+ openedAt: Date.now(),
55
+ mediaType: 'text/plain',
56
+ },
57
+ isCollapsed: false,
58
+ isActive: false,
59
+ href: '/resources/resource-1',
60
+ onClose: vi.fn(),
61
+ LinkComponent: MockLink,
62
+ translations: {},
63
+ index: 0,
64
+ totalCount: 3,
65
+ };
66
+
67
+ beforeEach(() => {
68
+ vi.clearAllMocks();
69
+ vi.mocked(useSortable).mockReturnValue(mockSortableReturn as any);
70
+ });
71
+
72
+ describe('Rendering', () => {
73
+ it('should render the resource name', () => {
74
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
75
+
76
+ expect(screen.getByText('Test Document')).toBeInTheDocument();
77
+ });
78
+
79
+ it('should render the resource icon', () => {
80
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
81
+
82
+ const icon = screen.getByText('\u{1F4C4}');
83
+ expect(icon).toBeInTheDocument();
84
+ });
85
+
86
+ it('should render link with correct href', () => {
87
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
88
+
89
+ const link = screen.getByTitle('Test Document');
90
+ expect(link).toHaveAttribute('href', '/resources/resource-1');
91
+ });
92
+
93
+ it('should render close button when not collapsed', () => {
94
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
95
+
96
+ const closeButton = screen.getByLabelText('Close Test Document');
97
+ expect(closeButton).toBeInTheDocument();
98
+ });
99
+
100
+ it('should not render close button when collapsed', () => {
101
+ renderWithProviders(
102
+ <SortableResourceTab {...defaultProps} isCollapsed={true} />
103
+ );
104
+
105
+ expect(screen.queryByLabelText('Close Test Document')).not.toBeInTheDocument();
106
+ });
107
+
108
+ it('should not render resource name text when collapsed', () => {
109
+ renderWithProviders(
110
+ <SortableResourceTab {...defaultProps} isCollapsed={true} />
111
+ );
112
+
113
+ expect(screen.queryByText('Test Document')).not.toBeInTheDocument();
114
+ });
115
+
116
+ it('should render icon when collapsed', () => {
117
+ renderWithProviders(
118
+ <SortableResourceTab {...defaultProps} isCollapsed={true} />
119
+ );
120
+
121
+ expect(screen.getByText('\u{1F4C4}')).toBeInTheDocument();
122
+ });
123
+ });
124
+
125
+ describe('Active state', () => {
126
+ it('should have active class when isActive is true', () => {
127
+ const { container } = renderWithProviders(
128
+ <SortableResourceTab {...defaultProps} isActive={true} />
129
+ );
130
+
131
+ const tab = container.querySelector('.semiont-resource-tab');
132
+ expect(tab).toHaveClass('semiont-resource-tab--active');
133
+ });
134
+
135
+ it('should not have active class when isActive is false', () => {
136
+ const { container } = renderWithProviders(
137
+ <SortableResourceTab {...defaultProps} isActive={false} />
138
+ );
139
+
140
+ const tab = container.querySelector('.semiont-resource-tab');
141
+ expect(tab).not.toHaveClass('semiont-resource-tab--active');
142
+ });
143
+
144
+ it('should set aria-selected based on isActive', () => {
145
+ renderWithProviders(
146
+ <SortableResourceTab {...defaultProps} isActive={true} />
147
+ );
148
+
149
+ const tab = screen.getByRole('tab');
150
+ expect(tab).toHaveAttribute('aria-selected', 'true');
151
+ });
152
+ });
153
+
154
+ describe('Dragging state', () => {
155
+ it('should have dragging class when isDragging prop is true', () => {
156
+ const { container } = renderWithProviders(
157
+ <SortableResourceTab {...defaultProps} isDragging={true} />
158
+ );
159
+
160
+ const tab = container.querySelector('.semiont-resource-tab');
161
+ expect(tab).toHaveClass('semiont-resource-tab--dragging');
162
+ });
163
+
164
+ it('should have dragging class when useSortable reports dragging', () => {
165
+ vi.mocked(useSortable).mockReturnValue({
166
+ ...mockSortableReturn,
167
+ isDragging: true,
168
+ } as any);
169
+
170
+ const { container } = renderWithProviders(
171
+ <SortableResourceTab {...defaultProps} />
172
+ );
173
+
174
+ const tab = container.querySelector('.semiont-resource-tab');
175
+ expect(tab).toHaveClass('semiont-resource-tab--dragging');
176
+ });
177
+
178
+ it('should not have dragging class when not dragging', () => {
179
+ const { container } = renderWithProviders(
180
+ <SortableResourceTab {...defaultProps} isDragging={false} />
181
+ );
182
+
183
+ const tab = container.querySelector('.semiont-resource-tab');
184
+ expect(tab).not.toHaveClass('semiont-resource-tab--dragging');
185
+ });
186
+ });
187
+
188
+ describe('Close button', () => {
189
+ it('should call onClose with resource id and event when clicked', () => {
190
+ const onClose = vi.fn();
191
+ renderWithProviders(
192
+ <SortableResourceTab {...defaultProps} onClose={onClose} />
193
+ );
194
+
195
+ const closeButton = screen.getByLabelText('Close Test Document');
196
+ fireEvent.click(closeButton);
197
+
198
+ expect(onClose).toHaveBeenCalledOnce();
199
+ expect(onClose).toHaveBeenCalledWith('resource-1', expect.any(Object));
200
+ });
201
+
202
+ it('should use custom translation for close button title', () => {
203
+ renderWithProviders(
204
+ <SortableResourceTab
205
+ {...defaultProps}
206
+ translations={{ closeResource: 'Fermer la ressource' }}
207
+ />
208
+ );
209
+
210
+ const closeButton = screen.getByTitle('Fermer la ressource');
211
+ expect(closeButton).toBeInTheDocument();
212
+ });
213
+
214
+ it('should default to "Close resource" title', () => {
215
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
216
+
217
+ const closeButton = screen.getByTitle('Close resource');
218
+ expect(closeButton).toBeInTheDocument();
219
+ });
220
+ });
221
+
222
+ describe('Keyboard reordering', () => {
223
+ it('should call onReorder with "up" on Alt+ArrowUp', () => {
224
+ const onReorder = vi.fn();
225
+ renderWithProviders(
226
+ <SortableResourceTab {...defaultProps} onReorder={onReorder} />
227
+ );
228
+
229
+ const tab = screen.getByRole('tab');
230
+ fireEvent.keyDown(tab, { key: 'ArrowUp', altKey: true });
231
+
232
+ expect(onReorder).toHaveBeenCalledWith('resource-1', 'up');
233
+ });
234
+
235
+ it('should call onReorder with "down" on Alt+ArrowDown', () => {
236
+ const onReorder = vi.fn();
237
+ renderWithProviders(
238
+ <SortableResourceTab {...defaultProps} onReorder={onReorder} />
239
+ );
240
+
241
+ const tab = screen.getByRole('tab');
242
+ fireEvent.keyDown(tab, { key: 'ArrowDown', altKey: true });
243
+
244
+ expect(onReorder).toHaveBeenCalledWith('resource-1', 'down');
245
+ });
246
+
247
+ it('should not call onReorder without Alt key', () => {
248
+ const onReorder = vi.fn();
249
+ renderWithProviders(
250
+ <SortableResourceTab {...defaultProps} onReorder={onReorder} />
251
+ );
252
+
253
+ const tab = screen.getByRole('tab');
254
+ fireEvent.keyDown(tab, { key: 'ArrowUp' });
255
+
256
+ expect(onReorder).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it('should not call onReorder for non-arrow keys with Alt', () => {
260
+ const onReorder = vi.fn();
261
+ renderWithProviders(
262
+ <SortableResourceTab {...defaultProps} onReorder={onReorder} />
263
+ );
264
+
265
+ const tab = screen.getByRole('tab');
266
+ fireEvent.keyDown(tab, { key: 'Enter', altKey: true });
267
+
268
+ expect(onReorder).not.toHaveBeenCalled();
269
+ });
270
+
271
+ it('should not error when onReorder is not provided', () => {
272
+ renderWithProviders(
273
+ <SortableResourceTab {...defaultProps} onReorder={undefined} />
274
+ );
275
+
276
+ const tab = screen.getByRole('tab');
277
+ expect(() => {
278
+ fireEvent.keyDown(tab, { key: 'ArrowUp', altKey: true });
279
+ }).not.toThrow();
280
+ });
281
+ });
282
+
283
+ describe('Accessibility', () => {
284
+ it('should have tab role', () => {
285
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
286
+
287
+ expect(screen.getByRole('tab')).toBeInTheDocument();
288
+ });
289
+
290
+ it('should have aria-label with position info', () => {
291
+ renderWithProviders(
292
+ <SortableResourceTab {...defaultProps} index={1} totalCount={5} />
293
+ );
294
+
295
+ const tab = screen.getByRole('tab');
296
+ expect(tab).toHaveAttribute('aria-label', 'Test Document, position 2 of 5');
297
+ });
298
+
299
+ it('should have aria-hidden on icon', () => {
300
+ const { container } = renderWithProviders(
301
+ <SortableResourceTab {...defaultProps} />
302
+ );
303
+
304
+ const icon = container.querySelector('.semiont-resource-tab__icon');
305
+ expect(icon).toHaveAttribute('aria-hidden', 'true');
306
+ });
307
+
308
+ it('should have close button with aria-label', () => {
309
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
310
+
311
+ const closeButton = screen.getByLabelText('Close Test Document');
312
+ expect(closeButton).toBeInTheDocument();
313
+ });
314
+ });
315
+
316
+ describe('Styling', () => {
317
+ it('should have base tab class', () => {
318
+ const { container } = renderWithProviders(
319
+ <SortableResourceTab {...defaultProps} />
320
+ );
321
+
322
+ expect(container.querySelector('.semiont-resource-tab')).toBeInTheDocument();
323
+ });
324
+
325
+ it('should have proper link class', () => {
326
+ const { container } = renderWithProviders(
327
+ <SortableResourceTab {...defaultProps} />
328
+ );
329
+
330
+ expect(container.querySelector('.semiont-resource-tab__link')).toBeInTheDocument();
331
+ });
332
+
333
+ it('should have proper icon class', () => {
334
+ const { container } = renderWithProviders(
335
+ <SortableResourceTab {...defaultProps} />
336
+ );
337
+
338
+ expect(container.querySelector('.semiont-resource-tab__icon')).toBeInTheDocument();
339
+ });
340
+
341
+ it('should have proper text class', () => {
342
+ const { container } = renderWithProviders(
343
+ <SortableResourceTab {...defaultProps} />
344
+ );
345
+
346
+ expect(container.querySelector('.semiont-resource-tab__text')).toBeInTheDocument();
347
+ });
348
+
349
+ it('should have proper close button class', () => {
350
+ const { container } = renderWithProviders(
351
+ <SortableResourceTab {...defaultProps} />
352
+ );
353
+
354
+ expect(container.querySelector('.semiont-resource-tab__close')).toBeInTheDocument();
355
+ });
356
+ });
357
+
358
+ describe('useSortable integration', () => {
359
+ it('should call useSortable with resource id', () => {
360
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
361
+
362
+ expect(useSortable).toHaveBeenCalledWith({ id: 'resource-1' });
363
+ });
364
+
365
+ it('should pass setNodeRef to container', () => {
366
+ renderWithProviders(<SortableResourceTab {...defaultProps} />);
367
+
368
+ expect(mockSetNodeRef).toHaveBeenCalled();
369
+ });
370
+ });
371
+ });
@@ -1,20 +1,19 @@
1
1
  'use client';
2
2
 
3
3
  import { useRef, useEffect, useCallback, lazy, Suspense } from 'react';
4
- import type { components } from '@semiont/core';
5
4
  import { resourceUri as toResourceUri } from '@semiont/core';
6
- import { getTextPositionSelector, getTextQuoteSelector, getTargetSelector, getMimeCategory, isPdfMimeType, extractContext, findTextWithContext, buildContentCache } from '@semiont/api-client';
5
+ import { getMimeCategory, isPdfMimeType } from '@semiont/api-client';
7
6
  import { ANNOTATORS } from '../../lib/annotation-registry';
7
+ import { segmentTextWithAnnotations } from '../../lib/text-segmentation';
8
+ import { buildTextSelectors, fallbackTextPosition } from '../../lib/text-selection-handler';
8
9
  import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
10
+
9
11
  import { useResourceAnnotations } from '../../contexts/ResourceAnnotationsContext';
10
12
 
11
13
  // Lazy load PDF component to avoid SSR issues with browser PDF.js loading
12
14
  const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCanvas.client').then(mod => ({ default: mod.PdfAnnotationCanvas })));
13
15
 
14
- type Annotation = components['schemas']['Annotation'];
15
-
16
16
  import { CodeMirrorRenderer } from '../CodeMirrorRenderer';
17
- import type { TextSegment } from '../CodeMirrorRenderer';
18
17
  import type { EditorView } from '@codemirror/view';
19
18
  import { useEventBus } from '../../contexts/EventBusContext';
20
19
  import { useEventSubscriptions } from '../../contexts/useEventSubscription';
@@ -45,90 +44,6 @@ interface Props {
45
44
  annotateMode: boolean;
46
45
  }
47
46
 
48
- // Segment text with annotations - uses fuzzy anchoring when available!
49
- function segmentTextWithAnnotations(content: string, annotations: Annotation[]): TextSegment[] {
50
- if (!content) {
51
- return [{ exact: '', start: 0, end: 0 }];
52
- }
53
-
54
- // Pre-compute normalized/lowered content once for all annotations
55
- const cache = buildContentCache(content);
56
-
57
- const normalizedAnnotations = annotations
58
- .map(ann => {
59
- const targetSelector = getTargetSelector(ann.target);
60
- const posSelector = getTextPositionSelector(targetSelector);
61
- const quoteSelector = targetSelector ? getTextQuoteSelector(targetSelector) : null;
62
-
63
- // Try fuzzy anchoring if TextQuoteSelector is available
64
- // Pass TextPositionSelector as position hint for better fuzzy search
65
- let position;
66
- if (quoteSelector) {
67
- position = findTextWithContext(
68
- content,
69
- quoteSelector.exact,
70
- quoteSelector.prefix,
71
- quoteSelector.suffix,
72
- posSelector?.start,
73
- cache
74
- );
75
- }
76
-
77
- // Fallback to TextPositionSelector or fuzzy position
78
- const start = position?.start ?? posSelector?.start ?? 0;
79
- const end = position?.end ?? posSelector?.end ?? 0;
80
-
81
- return {
82
- annotation: ann,
83
- start,
84
- end
85
- };
86
- })
87
- .filter(a => a.start >= 0 && a.end <= content.length && a.start < a.end)
88
- .sort((a, b) => a.start - b.start);
89
-
90
- if (normalizedAnnotations.length === 0) {
91
- return [{ exact: content, start: 0, end: content.length }];
92
- }
93
-
94
- const segments: TextSegment[] = [];
95
- let position = 0;
96
-
97
- for (const { annotation, start, end } of normalizedAnnotations) {
98
- if (start < position) continue; // Skip overlapping annotations
99
-
100
- // Add text before annotation
101
- if (start > position) {
102
- segments.push({
103
- exact: content.slice(position, start),
104
- start: position,
105
- end: start
106
- });
107
- }
108
-
109
- // Add annotated segment
110
- segments.push({
111
- exact: content.slice(start, end),
112
- annotation,
113
- start,
114
- end
115
- });
116
-
117
- position = end;
118
- }
119
-
120
- // Add remaining text
121
- if (position < content.length) {
122
- segments.push({
123
- exact: content.slice(position),
124
- start: position,
125
- end: content.length
126
- });
127
- }
128
-
129
- return segments;
130
- }
131
-
132
47
  /**
133
48
  * View component for annotating resources with text selection and drawing
134
49
  *
@@ -241,74 +156,33 @@ export function AnnotateView({
241
156
  // Get the CodeMirror EditorView instance stored on the CodeMirror container
242
157
  const cmContainer = container.querySelector('.codemirror-renderer');
243
158
  const view = (cmContainer as EnrichedHTMLElement | null)?.__cmView;
159
+
160
+ let start: number;
161
+ let end: number;
162
+
244
163
  if (!view || !view.posAtDOM) {
245
164
  // Fallback: try to find text in source (won't work for duplicates)
246
- const start = content.indexOf(text);
247
- if (start === -1) {
248
- return;
249
- }
250
- const end = start + text.length;
251
-
252
- // Extract context for TextQuoteSelector
253
- const context = extractContext(content, start, end);
254
-
255
- // Unified flow: all text annotations use BOTH TextPositionSelector and TextQuoteSelector
256
- if (selectedMotivation) {
257
- eventBus.get('mark:requested').next({
258
- selector: [
259
- {
260
- type: 'TextPositionSelector',
261
- start,
262
- end
263
- },
264
- {
265
- type: 'TextQuoteSelector',
266
- exact: text,
267
- ...(context.prefix && { prefix: context.prefix }),
268
- ...(context.suffix && { suffix: context.suffix })
269
- }
270
- ],
271
- motivation: selectedMotivation
272
- });
273
-
274
- // Clear selection after creating annotation
275
- selection.removeAllRanges();
276
- return;
277
- }
278
- return;
165
+ const pos = fallbackTextPosition(content, text);
166
+ if (!pos) return;
167
+ start = pos.start;
168
+ end = pos.end;
169
+ } else {
170
+ // CodeMirror's posAtDOM gives us the position in the document from a DOM node/offset
171
+ start = view.posAtDOM(range.startContainer, range.startOffset);
172
+ end = start + text.length;
279
173
  }
280
174
 
281
- // CodeMirror's posAtDOM gives us the position in the document from a DOM node/offset
282
- const start = view.posAtDOM(range.startContainer, range.startOffset);
283
- const end = start + text.length;
284
-
285
- if (start >= 0) {
286
- // Extract context for TextQuoteSelector
287
- const context = extractContext(content, start, end);
288
-
289
- // Unified flow: all text annotations use BOTH TextPositionSelector and TextQuoteSelector
290
- if (selectedMotivation) {
291
- eventBus.get('mark:requested').next({
292
- selector: [
293
- {
294
- type: 'TextPositionSelector',
295
- start,
296
- end
297
- },
298
- {
299
- type: 'TextQuoteSelector',
300
- exact: text,
301
- ...(context.prefix && { prefix: context.prefix }),
302
- ...(context.suffix && { suffix: context.suffix })
303
- }
304
- ],
305
- motivation: selectedMotivation
306
- });
307
-
308
- // Clear selection after creating annotation
309
- selection.removeAllRanges();
310
- return;
311
- }
175
+ if (start >= 0 && selectedMotivation) {
176
+ const selectors = buildTextSelectors(content, text, start, end);
177
+ if (!selectors) return;
178
+
179
+ eventBus.get('mark:requested').next({
180
+ selector: selectors,
181
+ motivation: selectedMotivation
182
+ });
183
+
184
+ // Clear selection after creating annotation
185
+ selection.removeAllRanges();
312
186
  }
313
187
  };
314
188