@searpent/react-image-annotate 2.0.1 → 2.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.
@@ -0,0 +1,515 @@
1
+ /**
2
+ * Build styles
3
+ */
4
+ require('./annotation.css').toString();
5
+
6
+ // Possible classes
7
+ // ================
8
+
9
+ class Annotation {
10
+ /**
11
+ * Render plugin`s main Element and fill it with saved data
12
+ *
13
+ * @param {{data: HeaderData, config: HeaderConfig, api: object}}
14
+ * data — previously saved data
15
+ * config - user config for Tool
16
+ * api - Editor.js API
17
+ * readOnly - read only mode flag
18
+ */
19
+ constructor({ data, config, api, readOnly }) {
20
+ this.api = api;
21
+ this.readOnly = readOnly;
22
+
23
+ /**
24
+ * Styles
25
+ *
26
+ * @type {object}
27
+ */
28
+ this._CSS = {
29
+ block: this.api.styles.block,
30
+ settingsButton: this.api.styles.settingsButton,
31
+ settingsButtonActive: this.api.styles.settingsButtonActive,
32
+ wrapper: 'ce-header'
33
+ };
34
+
35
+ /**
36
+ * Tool's settings passed from Editor
37
+ *
38
+ * @type {HeaderConfig}
39
+ * @private
40
+ */
41
+ this._settings = config;
42
+
43
+ /**
44
+ * Block's data
45
+ *
46
+ * @type {HeaderData}
47
+ * @private
48
+ */
49
+ this._data = this.normalizeData(data);
50
+
51
+ /**
52
+ * List of settings buttons
53
+ *
54
+ * @type {HTMLElement[]}
55
+ */
56
+ this.settingsButtons = [];
57
+
58
+ /**
59
+ * Main Block wrapper
60
+ *
61
+ * @type {HTMLElement}
62
+ * @private
63
+ */
64
+ this._element = this.getTag();
65
+ }
66
+
67
+ /**
68
+ * Normalize input data
69
+ *
70
+ * @param {HeaderData} data - saved data to process
71
+ *
72
+ * @returns {HeaderData}
73
+ * @private
74
+ */
75
+ normalizeData(data) {
76
+ const newData = {};
77
+
78
+ if (typeof data !== 'object') {
79
+ data = {};
80
+ }
81
+
82
+ newData.text = data.text || '';
83
+ newData.labelName = data.labelName || this.defaultLabel.labelName;
84
+
85
+ return newData;
86
+ }
87
+
88
+ /**
89
+ * Return Tool's view
90
+ *
91
+ * @returns {HTMLHeadingElement}
92
+ * @public
93
+ */
94
+ render() {
95
+ return this._element;
96
+ }
97
+
98
+ /**
99
+ * Create Block's settings block
100
+ *
101
+ * @returns {HTMLElement}
102
+ */
103
+ renderSettings() {
104
+ const holder = document.createElement('DIV');
105
+
106
+ // do not add settings button, when only one label is configured
107
+ if (this.labels.length <= 1) {
108
+ return holder;
109
+ }
110
+
111
+ /** Add type selectors */
112
+ this.labels.forEach(label => {
113
+ const selectTypeButton = document.createElement('DIV');
114
+
115
+ selectTypeButton.classList.add(this._CSS.settingsButton);
116
+
117
+ /**
118
+ * Highlight current label button
119
+ */
120
+ if (this.currentLabel.labelName === label.labelName) {
121
+ selectTypeButton.classList.add(this._CSS.settingsButtonActive);
122
+ }
123
+
124
+ /**
125
+ * Add SVG icon
126
+ */
127
+ selectTypeButton.innerHTML = `${label.labelName}`;
128
+
129
+ /**
130
+ * Save label to its button
131
+ */
132
+ selectTypeButton.dataset.labelName = label.labelName;
133
+
134
+ /**
135
+ * Set up click handler
136
+ */
137
+ selectTypeButton.addEventListener('click', () => {
138
+ this.setLabelName(label.labelName);
139
+ });
140
+
141
+ /**
142
+ * Append settings button to holder
143
+ */
144
+ holder.appendChild(selectTypeButton);
145
+
146
+ /**
147
+ * Save settings buttons
148
+ */
149
+ this.settingsButtons.push(selectTypeButton);
150
+ });
151
+
152
+ return holder;
153
+ }
154
+
155
+ /**
156
+ * Callback for Block's settings buttons
157
+ *
158
+ * @param {labelName} labelName - labelName to set
159
+ */
160
+ setLabelName(labelName) {
161
+ this.data = {
162
+ labelName,
163
+ text: this.data.text
164
+ };
165
+
166
+ /**
167
+ * Highlight button by selected labelName
168
+ */
169
+ this.settingsButtons.forEach(button => {
170
+ button.classList.toggle(
171
+ this._CSS.settingsButtonActive,
172
+ button.dataset.labelName === labelName
173
+ );
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Method that specified how to merge two Text blocks.
179
+ * Called by Editor.js by backspace at the beginning of the Block
180
+ *
181
+ * @param {HeaderData} data - saved data to merger with current block
182
+ * @public
183
+ */
184
+ merge(data) {
185
+ const newData = {
186
+ text: this.data.text + data.text,
187
+ labelName: this.data.labelName
188
+ };
189
+
190
+ this.data = newData;
191
+ }
192
+
193
+ /**
194
+ * Validate Text block data:
195
+ * - check for emptiness
196
+ *
197
+ * @param {HeaderData} blockData — data received after saving
198
+ * @returns {boolean} false if saved data is not correct, otherwise true
199
+ * @public
200
+ */
201
+ validate(blockData) {
202
+ return blockData.text.trim() !== '';
203
+ }
204
+
205
+ /**
206
+ * Extract Tool's data from the view
207
+ *
208
+ * @param {HTMLHeadingElement} toolsContent - Text tools rendered view
209
+ * @returns {HeaderData} - saved data
210
+ * @public
211
+ */
212
+ save(toolsContent) {
213
+ return {
214
+ text: toolsContent.innerHTML,
215
+ labelName: this.currentLabel.labelName
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Allow Header to be converted to/from other blocks
221
+ */
222
+ static get conversionConfig() {
223
+ return {
224
+ export: 'text', // use 'text' property for other blocks
225
+ import: 'text' // fill 'text' property from other block's export string
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Sanitizer Rules
231
+ */
232
+ static get sanitize() {
233
+ return {
234
+ labelName: false,
235
+ text: {}
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Returns true to notify core that read-only is supported
241
+ *
242
+ * @returns {boolean}
243
+ */
244
+ static get isReadOnlySupported() {
245
+ return true;
246
+ }
247
+
248
+ /**
249
+ * Get current Tools`s data
250
+ *
251
+ * @returns {HeaderData} Current data
252
+ * @private
253
+ */
254
+ get data() {
255
+ this._data.text = this._element.innerHTML;
256
+ this._data.labelName = this.currentLabel.labelName;
257
+
258
+ return this._data;
259
+ }
260
+
261
+ /**
262
+ * Store data in plugin:
263
+ * - at the this._data property
264
+ * - at the HTML
265
+ *
266
+ * @param {HeaderData} data — data to set
267
+ * @private
268
+ */
269
+ set data(data) {
270
+ this._data = this.normalizeData(data);
271
+
272
+ /**
273
+ * If labelName is set and block in DOM
274
+ * then replace it to a new block
275
+ */
276
+ if (data.labelName !== undefined && this._element.parentNode) {
277
+ /**
278
+ * Create a new tag
279
+ *
280
+ * @type {HTMLHeadingElement}
281
+ */
282
+ const newHeader = this.getTag();
283
+
284
+ /**
285
+ * Save Block's content
286
+ */
287
+ newHeader.innerHTML = this._element.innerHTML;
288
+
289
+ /**
290
+ * Replace blocks
291
+ */
292
+ this._element.parentNode.replaceChild(newHeader, this._element);
293
+
294
+ /**
295
+ * Save new block to private variable
296
+ *
297
+ * @type {HTMLHeadingElement}
298
+ * @private
299
+ */
300
+ this._element = newHeader;
301
+ }
302
+
303
+ /**
304
+ * If data.text was passed then update block's content
305
+ */
306
+ if (data.text !== undefined) {
307
+ this._element.innerHTML = this._data.text || '';
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Get tag for target label
313
+ * By default returns second-labelled header
314
+ *
315
+ * @returns {HTMLElement}
316
+ */
317
+ getTag() {
318
+ /**
319
+ * Create element for current Block's label
320
+ */
321
+ const tag = document.createElement(this.currentLabel.tag);
322
+ if (this.currentLabel.backgroundColor) {
323
+ tag.style.backgroundColor = this.currentLabel.backgroundColor;
324
+ }
325
+
326
+ /**
327
+ * Add text to block
328
+ */
329
+ tag.innerHTML = this._data.text || '';
330
+
331
+ /**
332
+ * Add styles class
333
+ */
334
+ tag.classList.add(this._CSS.wrapper);
335
+
336
+ /**
337
+ * Make tag editable
338
+ */
339
+ tag.contentEditable = this.readOnly ? 'false' : 'true';
340
+
341
+ /**
342
+ * Add Placeholder
343
+ */
344
+ tag.dataset.placeholder = this.api.i18n.t(this._settings.placeholder || '');
345
+
346
+ return tag;
347
+ }
348
+
349
+ /**
350
+ * Get current label
351
+ *
352
+ * @returns {label}
353
+ */
354
+ get currentLabel() {
355
+ let label = this.labels.find(
356
+ labelItem => labelItem.labelName === this._data.labelName
357
+ );
358
+
359
+ if (!label) {
360
+ label = this.defaultLabel;
361
+ }
362
+
363
+ return label;
364
+ }
365
+
366
+ /**
367
+ * Return default label
368
+ *
369
+ * @returns {label}
370
+ */
371
+ get defaultLabel() {
372
+ /**
373
+ * User can specify own default label value
374
+ */
375
+ if (this._settings.defaultLabel) {
376
+ const userSpecified = this.labels.find(labelItem => {
377
+ return labelItem.labelName === this._settings.defaultLabel;
378
+ });
379
+
380
+ if (userSpecified) {
381
+ return userSpecified;
382
+ }
383
+ console.warn(
384
+ "(ง'̀-'́)ง Annotation Tool: the default label specified was not found in available labels"
385
+ );
386
+ }
387
+
388
+ /**
389
+ * With no additional options, there will be H2 by default
390
+ *
391
+ * @type {label}
392
+ */
393
+ return this.labels[1];
394
+ }
395
+
396
+ /**
397
+ * @typedef {object} label
398
+ * @property {labelName} labelName - label labelName
399
+ * @property {string} tag - tag corresponds with label labelName
400
+ * @property {string} svg - icon
401
+ */
402
+
403
+ /**
404
+ * Available header labels
405
+ *
406
+ * @returns {label[]}
407
+ */
408
+ get labels() {
409
+ const availableLabels = [
410
+ {
411
+ labelName: 'title',
412
+ tag: 'h1',
413
+ name: 'title'
414
+ // backgroundColor: '#d0fffe'
415
+ },
416
+ { labelName: 'subtitle', tag: 'h2', name: 'subtitle' },
417
+ { labelName: 'text', tag: 'p', name: 'text' },
418
+ { labelName: 'author', tag: 'i', name: 'author' },
419
+ { labelName: 'appendix', tag: 'p', name: 'appendix' },
420
+ { labelName: 'photo_author', tag: 'p', name: 'photo_author' },
421
+ { labelName: 'photo_caption', tag: 'p', name: 'photo_caption' },
422
+ { labelName: 'advertisement', tag: 'p', name: 'advertisement' },
423
+ { labelName: 'other_graphics', tag: 'p', name: 'other_graphics' },
424
+ { labelName: 'unknown', tag: 's', name: 'unknown' },
425
+ { labelName: 'about_author', tag: 'p', name: 'about_author' },
426
+ { labelName: 'image', tag: 'p', name: 'image' },
427
+ { labelName: 'interview', tag: 'p', name: 'interview' },
428
+ { labelName: 'table', tag: 'p', name: 'table' }
429
+ ];
430
+
431
+ return this._settings.labels
432
+ ? availableLabels.filter(l => this._settings.labels.includes(l.labelName))
433
+ : availableLabels;
434
+ }
435
+
436
+ /**
437
+ * Handle H1-H6 tags on paste to substitute it with header Tool
438
+ *
439
+ * @param {PasteEvent} event - event with pasted content
440
+ */
441
+ onPaste(event) {
442
+ const content = event.detail.data;
443
+
444
+ /**
445
+ * Define default label value
446
+ *
447
+ * @type {labelName}
448
+ */
449
+ let { labelName } = this.defaultLabel;
450
+
451
+ switch (content.tagName) {
452
+ case 'H1':
453
+ labelName = 1;
454
+ break;
455
+ case 'H2':
456
+ labelName = 2;
457
+ break;
458
+ case 'H3':
459
+ labelName = 3;
460
+ break;
461
+ case 'H4':
462
+ labelName = 4;
463
+ break;
464
+ case 'H5':
465
+ labelName = 5;
466
+ break;
467
+ case 'H6':
468
+ labelName = 6;
469
+ break;
470
+ }
471
+
472
+ // if (this._settings.labels) {
473
+ // // Fallback to nearest label when specified not available
474
+ // label = this._settings.labels.reduce((prevLabel, currLabel) => {
475
+ // return Math.abs(currLabel - label) < Math.abs(prevLabel - label)
476
+ // ? currLabel
477
+ // : prevLabel;
478
+ // });
479
+ // }
480
+
481
+ this.data = {
482
+ labelName,
483
+ text: content.innerHTML
484
+ };
485
+ }
486
+
487
+ /**
488
+ * Used by Editor.js paste handling API.
489
+ * Provides configuration to handle H1-H6 tags.
490
+ *
491
+ * @returns {{handler: (function(HTMLElement): {text: string}), tags: string[]}}
492
+ */
493
+ static get pasteConfig() {
494
+ return {
495
+ tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
496
+ };
497
+ }
498
+
499
+ /**
500
+ * Get Tool toolbox settings
501
+ * icon - Tool icon's SVG
502
+ * title - title to show in toolbox
503
+ *
504
+ * @returns {{icon: string, title: string}}
505
+ */
506
+ static get toolbox() {
507
+ return {
508
+ icon:
509
+ '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/></svg>',
510
+ title: 'Annotation'
511
+ };
512
+ }
513
+ }
514
+
515
+ export default Annotation;
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { createReactEditorJS } from 'react-editor-js'
3
+ import { EDITOR_JS_TOOLS } from './tools'
4
+
5
+ const ReactEditorJS = createReactEditorJS()
6
+
7
+ function Editor({ blocks, onChange, imageIndex }) {
8
+ const handleChange = async instance => {
9
+ const data = await instance.saver.save();
10
+ onChange({ imageIndex, data })
11
+ };
12
+
13
+ return (
14
+ <ReactEditorJS defaultValue={{
15
+ blocks
16
+ }}
17
+ tools={EDITOR_JS_TOOLS}
18
+ onChange={handleChange}
19
+ enableReInitialize
20
+ />
21
+ );
22
+ }
23
+
24
+ export default Editor;
@@ -0,0 +1,6 @@
1
+ // tools.js
2
+ import Annotation from './annotation-plugin/annotation';
3
+
4
+ export const EDITOR_JS_TOOLS = {
5
+ annotation: Annotation
6
+ }
@@ -0,0 +1,48 @@
1
+ // @flow
2
+
3
+ import React, { memo } from "react"
4
+ import { makeStyles } from "@mui/styles"
5
+ import { createTheme, ThemeProvider } from "@mui/material/styles"
6
+ import SidebarBoxContainer from "../SidebarBoxContainer"
7
+ import CollectionsIcon from "@mui/icons-material/Collections"
8
+ import { grey } from "@mui/material/colors"
9
+ import List from "@mui/material/List"
10
+ import ListItem from "@mui/material/ListItem"
11
+ import ListItemText from "@mui/material/ListItemText"
12
+ import isEqual from "lodash/isEqual"
13
+
14
+ const theme = createTheme()
15
+ const useStyles = makeStyles((theme) => ({
16
+ img: { width: 40, height: 40, borderRadius: 8 },
17
+ }))
18
+
19
+ export const GroupSelectorSidebarBox = ({ title, subtitle, groups, onSelect, selectedGroupId }) => {
20
+ const classes = useStyles()
21
+ return (
22
+ <ThemeProvider theme={theme}>
23
+ <SidebarBoxContainer
24
+ title={title || ''}
25
+ subTitle={subtitle || ''}
26
+ icon={<CollectionsIcon style={{ color: grey[700] }} />}
27
+ >
28
+ <List>
29
+ {groups.map(({ id, title: groupTitle, subtitle: groupSubtitle, color }, i) => (
30
+ <ListItem button onClick={() => onSelect(id)} dense key={id} style={{
31
+ backgroundColor: id === selectedGroupId ? '#bbdefb' : null
32
+ }}>
33
+ <ListItemText
34
+ primary={<strong style={{
35
+ color
36
+ }}>{groupTitle}</strong>}
37
+ secondary={groupSubtitle}
38
+ />
39
+ </ListItem>
40
+ ))}
41
+ </List>
42
+
43
+ </SidebarBoxContainer>
44
+ </ThemeProvider>
45
+ )
46
+ }
47
+
48
+ export default GroupSelectorSidebarBox
@@ -87,6 +87,8 @@ type Props = {
87
87
  onChangeVideoTime: (number) => any,
88
88
  onRegionClassAdded: () => {},
89
89
  onChangeVideoPlaying?: Function,
90
+ hideNotEditingLabel?: boolean,
91
+ allowedGroups?: Object
90
92
  }
91
93
 
92
94
  const getDefaultMat = (allowedArea = null, { iw, ih } = {}) => {
@@ -142,6 +144,8 @@ export const ImageCanvas = ({
142
144
  modifyingAllowedArea = false,
143
145
  keypointDefinitions,
144
146
  allowComments,
147
+ hideNotEditingLabel = false,
148
+ allowedGroups,
145
149
  }: Props) => {
146
150
  const classes = useStyles()
147
151
 
@@ -288,10 +292,10 @@ export const ImageCanvas = ({
288
292
  !zoomStart || !zoomEnd
289
293
  ? null
290
294
  : {
291
- ...mat.clone().inverse().applyToPoint(zoomStart.x, zoomStart.y),
292
- w: (zoomEnd.x - zoomStart.x) / mat.a,
293
- h: (zoomEnd.y - zoomStart.y) / mat.d,
294
- }
295
+ ...mat.clone().inverse().applyToPoint(zoomStart.x, zoomStart.y),
296
+ w: (zoomEnd.x - zoomStart.x) / mat.a,
297
+ h: (zoomEnd.y - zoomStart.y) / mat.d,
298
+ }
295
299
  if (zoomBox) {
296
300
  if (zoomBox.w < 0) {
297
301
  zoomBox.x += zoomBox.w
@@ -326,14 +330,14 @@ export const ImageCanvas = ({
326
330
  cursor: createWithPrimary
327
331
  ? "crosshair"
328
332
  : dragging
329
- ? "grabbing"
330
- : dragWithPrimary
331
- ? "grab"
332
- : zoomWithPrimary
333
- ? mat.a < 1
334
- ? "zoom-out"
335
- : "zoom-in"
336
- : undefined,
333
+ ? "grabbing"
334
+ : dragWithPrimary
335
+ ? "grab"
336
+ : zoomWithPrimary
337
+ ? mat.a < 1
338
+ ? "zoom-out"
339
+ : "zoom-in"
340
+ : undefined,
337
341
  }}
338
342
  >
339
343
  {showCrosshairs && (
@@ -346,19 +350,19 @@ export const ImageCanvas = ({
346
350
  !modifyingAllowedArea || !allowedArea
347
351
  ? regions
348
352
  : [
349
- {
350
- type: "box",
351
- id: "$$allowed_area",
352
- cls: "allowed_area",
353
- highlighted: true,
354
- x: allowedArea.x,
355
- y: allowedArea.y,
356
- w: allowedArea.w,
357
- h: allowedArea.h,
358
- visible: true,
359
- color: "#ff0",
360
- },
361
- ]
353
+ {
354
+ type: "box",
355
+ id: "$$allowed_area",
356
+ cls: "allowed_area",
357
+ highlighted: true,
358
+ x: allowedArea.x,
359
+ y: allowedArea.y,
360
+ w: allowedArea.w,
361
+ h: allowedArea.h,
362
+ visible: true,
363
+ color: "#ff0",
364
+ },
365
+ ]
362
366
  }
363
367
  mouseEvents={mouseEvents}
364
368
  projectRegionBox={projectRegionBox}
@@ -393,6 +397,8 @@ export const ImageCanvas = ({
393
397
  RegionEditLabel={RegionEditLabel}
394
398
  onRegionClassAdded={onRegionClassAdded}
395
399
  allowComments={allowComments}
400
+ hideNotEditingLabel={hideNotEditingLabel}
401
+ allowedGroups={allowedGroups}
396
402
  />
397
403
  </PreventScrollToParents>
398
404
  )}