@visns-studio/visns-components 5.0.19 → 5.0.20
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/package.json +6 -6
- package/src/components/crm/MultiSelect.jsx +46 -67
- package/src/components/crm/auth/styles/Profile.module.scss +6 -7
- package/src/components/crm/generic/GenericDashboard.jsx +467 -29
- package/src/components/crm/generic/GenericFormBuilder.jsx +96 -32
- package/src/components/crm/generic/styles/AuditLog.module.scss +3 -3
- package/src/components/crm/generic/styles/AuditLogs.module.scss +1 -1
- package/src/components/crm/generic/styles/GenericDashboard.module.scss +364 -0
- package/src/components/crm/generic/styles/GenericDynamic.module.scss +11 -11
- package/src/components/crm/generic/styles/GenericFormBuilder.module.scss +327 -317
- package/src/components/crm/generic/styles/GenericIndex.module.scss +8 -8
- package/src/components/crm/generic/styles/GenericSort.module.scss +3 -3
- package/src/components/crm/generic/styles/NotificationList.module.scss +2 -2
- package/src/components/crm/sketch/SketchField.jsx +395 -0
- package/src/components/crm/sketch/arrow.jsx +108 -0
- package/src/components/crm/sketch/circle.jsx +75 -0
- package/src/components/crm/sketch/default-tool.jsx +16 -0
- package/src/components/crm/sketch/fabrictool.jsx +22 -0
- package/src/components/crm/sketch/history.jsx +144 -0
- package/src/components/crm/sketch/json/config.json +14 -0
- package/src/components/crm/sketch/line.jsx +64 -0
- package/src/components/crm/sketch/pan.jsx +48 -0
- package/src/components/crm/sketch/pencil.jsx +36 -0
- package/src/components/crm/sketch/rectangle-label-object.jsx +69 -0
- package/src/components/crm/sketch/rectangle-label.jsx +93 -0
- package/src/components/crm/sketch/rectangle.jsx +76 -0
- package/src/components/crm/sketch/select.jsx +16 -0
- package/src/components/crm/sketch/tools.jsx +11 -0
- package/src/components/crm/sketch/utils.jsx +67 -0
- package/src/components/crm/sorting/Item.jsx +2 -2
- package/src/components/crm/sorting/styles/Item.module.scss +12 -65
- package/src/components/crm/sorting/styles/List.module.scss +6 -20
- package/src/components/crm/styles/Form.module.scss +4 -6
- package/src/components/crm/styles/MultiSelect.module.scss +4 -0
- package/src/components/crm/styles/Navigation.module.scss +13 -6
- package/src/components/crm/styles/QrCode.module.scss +2 -2
- package/src/components/styles/global.css +8 -0
- package/src/index.js +2 -0
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
position: relative;
|
|
18
18
|
padding: 20px;
|
|
19
19
|
margin: 8px 0;
|
|
20
|
-
box-shadow: 0 4px 0 rgba(var(--primary-
|
|
20
|
+
box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
.grid__half {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
position: relative;
|
|
44
44
|
padding: 2px;
|
|
45
45
|
margin: 8px 0;
|
|
46
|
-
box-shadow: 0 4px 0 rgba(var(--primary-
|
|
46
|
+
box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
.grid__dashfull {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
position: relative;
|
|
57
57
|
padding: 20px;
|
|
58
58
|
margin: 8px 0;
|
|
59
|
-
box-shadow: 0 4px 0 rgba(var(--primary-
|
|
59
|
+
box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
.grid__subrow {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
padding: 5px;
|
|
76
76
|
margin: 8px 0;
|
|
77
77
|
height: auto;
|
|
78
|
-
box-shadow: 0 4px 0 rgba(var(--primary-
|
|
78
|
+
box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
.grid__subnav > ul {
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
padding: 5px;
|
|
127
127
|
margin: 8px 0;
|
|
128
128
|
height: auto;
|
|
129
|
-
box-shadow: 0 4px 0 rgba(var(--primary-
|
|
129
|
+
box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
.grid__subnav__alternate > ul {
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
margin: 8px 0;
|
|
187
187
|
height: auto;
|
|
188
188
|
position: relative;
|
|
189
|
-
box-shadow: 0 4px 0 rgba(var(--primary-
|
|
189
|
+
box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
.grid__subcontent h2 {
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
svg {
|
|
263
|
-
color: rgba(var(--primary-
|
|
263
|
+
color: rgba(var(--primary-rgb), 0.5);
|
|
264
264
|
position: relative;
|
|
265
265
|
top: 2px;
|
|
266
266
|
margin: 0 0.25rem;
|
|
@@ -270,7 +270,7 @@
|
|
|
270
270
|
.titleInfo {
|
|
271
271
|
width: max-content;
|
|
272
272
|
}
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
.titleInfo span {
|
|
275
275
|
font-size: 0.875em;
|
|
276
276
|
color: rgba(var(--paragraph-color-rgb), 0.65);
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
position: relative;
|
|
27
27
|
padding: 2px;
|
|
28
28
|
margin: 8px 0;
|
|
29
|
-
box-shadow: 0 4px 0 rgba(var(--primary-
|
|
29
|
+
box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
.dragitem {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
align-items: center;
|
|
37
37
|
flex-wrap: wrap;
|
|
38
38
|
border-radius: 5px;
|
|
39
|
-
border: 1px solid rgba(var(--primary-
|
|
40
|
-
box-shadow: 0px 0px 20px rgba(var(--primary-
|
|
39
|
+
border: 1px solid rgba(var(--primary-rgb), 0.2);
|
|
40
|
+
box-shadow: 0px 0px 20px rgba(var(--primary-rgb), 0.25);
|
|
41
41
|
padding: 1.25rem;
|
|
42
42
|
box-sizing: border-box;
|
|
43
43
|
cursor: grab;
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import React, { PureComponent } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import History from './history';
|
|
4
|
+
import Select from './select';
|
|
5
|
+
import Pencil from './pencil';
|
|
6
|
+
import Line from './line';
|
|
7
|
+
import Arrow from './arrow';
|
|
8
|
+
import Rectangle from './rectangle';
|
|
9
|
+
import Circle from './circle';
|
|
10
|
+
import Pan from './pan';
|
|
11
|
+
import Tool from './tools';
|
|
12
|
+
import RectangleLabel from './rectangle-label';
|
|
13
|
+
import DefaultTool from './default-tool';
|
|
14
|
+
import * as fabric from 'fabric'; // v6
|
|
15
|
+
import { toast } from 'react-toastify'; // Import toast
|
|
16
|
+
|
|
17
|
+
// Debounce function
|
|
18
|
+
function debounce(func, delay) {
|
|
19
|
+
let timeout;
|
|
20
|
+
return (...args) => {
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
timeout = setTimeout(() => func.apply(this, args), delay);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class SketchField extends PureComponent {
|
|
27
|
+
static propTypes = {
|
|
28
|
+
lineColor: PropTypes.string,
|
|
29
|
+
lineWidth: PropTypes.number,
|
|
30
|
+
lineRadius: PropTypes.number,
|
|
31
|
+
fillColor: PropTypes.string,
|
|
32
|
+
backgroundColor: PropTypes.string,
|
|
33
|
+
backgroundImage: PropTypes.string,
|
|
34
|
+
opacity: PropTypes.number,
|
|
35
|
+
undoSteps: PropTypes.number,
|
|
36
|
+
tool: PropTypes.string,
|
|
37
|
+
imageFormat: PropTypes.string,
|
|
38
|
+
value: PropTypes.object,
|
|
39
|
+
forceValue: PropTypes.bool,
|
|
40
|
+
widthCorrection: PropTypes.number,
|
|
41
|
+
heightCorrection: PropTypes.number,
|
|
42
|
+
onChange: PropTypes.func,
|
|
43
|
+
defaultValue: PropTypes.object,
|
|
44
|
+
width: PropTypes.number,
|
|
45
|
+
height: PropTypes.number,
|
|
46
|
+
onObjectAdded: PropTypes.func,
|
|
47
|
+
onObjectModified: PropTypes.func,
|
|
48
|
+
onObjectRemoved: PropTypes.func,
|
|
49
|
+
className: PropTypes.string,
|
|
50
|
+
style: PropTypes.object,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
static defaultProps = {
|
|
54
|
+
lineColor: 'black',
|
|
55
|
+
lineWidth: 10,
|
|
56
|
+
lineRadius: 10,
|
|
57
|
+
fillColor: 'transparent',
|
|
58
|
+
backgroundColor: 'transparent',
|
|
59
|
+
opacity: 1.0,
|
|
60
|
+
undoSteps: 25,
|
|
61
|
+
tool: null,
|
|
62
|
+
widthCorrection: 0,
|
|
63
|
+
heightCorrection: 0,
|
|
64
|
+
forceValue: false,
|
|
65
|
+
onObjectAdded: () => null,
|
|
66
|
+
onObjectModified: () => null,
|
|
67
|
+
onObjectRemoved: () => null,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
_initTools = (fabricCanvas) => {
|
|
71
|
+
this._tools = {
|
|
72
|
+
[Tool.Select]: new Select(fabricCanvas),
|
|
73
|
+
[Tool.Pencil]: new Pencil(fabricCanvas),
|
|
74
|
+
[Tool.Line]: new Line(fabricCanvas),
|
|
75
|
+
[Tool.Arrow]: new Arrow(fabricCanvas),
|
|
76
|
+
[Tool.Rectangle]: new Rectangle(fabricCanvas),
|
|
77
|
+
[Tool.RectangleLabel]: new RectangleLabel(fabricCanvas),
|
|
78
|
+
[Tool.Circle]: new Circle(fabricCanvas),
|
|
79
|
+
[Tool.Pan]: new Pan(fabricCanvas),
|
|
80
|
+
[Tool.DefaultTool]: new DefaultTool(fabricCanvas),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
setBackgroundImage = async (imageUrl) => {
|
|
85
|
+
try {
|
|
86
|
+
const img = await fabric.Image.fromURL(imageUrl, {
|
|
87
|
+
crossOrigin: 'anonymous', // Enable CORS
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const imgWidth = img.width;
|
|
91
|
+
const imgHeight = img.height;
|
|
92
|
+
|
|
93
|
+
// Get the aspect ratio of the image
|
|
94
|
+
const aspectRatio = imgWidth / imgHeight;
|
|
95
|
+
|
|
96
|
+
// Calculate the new canvas dimensions based on aspect ratio
|
|
97
|
+
const { width, height } = this.props; // Default width and height
|
|
98
|
+
|
|
99
|
+
let newWidth, newHeight;
|
|
100
|
+
if (aspectRatio > 1) {
|
|
101
|
+
// Landscape orientation: Set width to max and adjust height
|
|
102
|
+
newWidth = width;
|
|
103
|
+
newHeight = width / aspectRatio;
|
|
104
|
+
} else {
|
|
105
|
+
// Portrait orientation: Set height to max and adjust width
|
|
106
|
+
newHeight = height;
|
|
107
|
+
newWidth = height * aspectRatio;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Set the new canvas dimensions
|
|
111
|
+
this._fc.setWidth(newWidth);
|
|
112
|
+
this._fc.setHeight(newHeight);
|
|
113
|
+
|
|
114
|
+
// Scale the image to fit the canvas dimensions
|
|
115
|
+
img.scaleToWidth(newWidth);
|
|
116
|
+
img.scaleToHeight(newHeight);
|
|
117
|
+
|
|
118
|
+
// Center the image both horizontally and vertically
|
|
119
|
+
img.set({
|
|
120
|
+
left: (newWidth - img.getScaledWidth()) / 2,
|
|
121
|
+
top: (newHeight - img.getScaledHeight()) / 2,
|
|
122
|
+
selectable: false,
|
|
123
|
+
evented: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Set the image as the background directly
|
|
127
|
+
this._fc.backgroundImage = img;
|
|
128
|
+
|
|
129
|
+
// Render the canvas to apply changes
|
|
130
|
+
this._fc.requestRenderAll();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Failed to load background image:', error);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
saveCanvasState = () => {
|
|
137
|
+
// Return the current state of the canvas as JSON
|
|
138
|
+
const state = this._fc.toJSON();
|
|
139
|
+
const backgroundImage = this._fc.backgroundImage
|
|
140
|
+
? {
|
|
141
|
+
src: this._fc.backgroundImage._element.src,
|
|
142
|
+
left: this._fc.backgroundImage.left,
|
|
143
|
+
top: this._fc.backgroundImage.top,
|
|
144
|
+
scaleX: this._fc.backgroundImage.scaleX,
|
|
145
|
+
scaleY: this._fc.backgroundImage.scaleY,
|
|
146
|
+
}
|
|
147
|
+
: null;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
state,
|
|
151
|
+
backgroundImage,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
exportCanvasImage = async (format = 'png') => {
|
|
156
|
+
// Ensure all images are loaded with CORS
|
|
157
|
+
const imageObjects = this._fc.getObjects('image');
|
|
158
|
+
for (const imgObj of imageObjects) {
|
|
159
|
+
if (imgObj.crossOrigin !== 'anonymous') {
|
|
160
|
+
// Set crossOrigin for images that are not set
|
|
161
|
+
imgObj.set({ crossOrigin: 'anonymous' });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Get the Base64 representation of the canvas image
|
|
166
|
+
const dataURL = this._fc.toDataURL({
|
|
167
|
+
format: format,
|
|
168
|
+
quality: 0.8,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Return the Base64 string
|
|
172
|
+
return dataURL; // This returns a promise
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
clearCanvas = () => {
|
|
176
|
+
// Clear all objects from the canvas while keeping the background image
|
|
177
|
+
const backgroundImage = this._fc.backgroundImage;
|
|
178
|
+
this._fc.clear();
|
|
179
|
+
|
|
180
|
+
// Restore the background image after clearing
|
|
181
|
+
if (backgroundImage) {
|
|
182
|
+
this._fc.backgroundImage = backgroundImage;
|
|
183
|
+
}
|
|
184
|
+
this._fc.renderAll(); // Re-render to show the background image
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
_resize = (e) => {
|
|
188
|
+
const { widthCorrection, heightCorrection } = this.props;
|
|
189
|
+
const canvas = this._fc;
|
|
190
|
+
|
|
191
|
+
const { offsetWidth, clientHeight } = this._container;
|
|
192
|
+
const wfactor = (offsetWidth - widthCorrection) / canvas.width;
|
|
193
|
+
const hfactor = (clientHeight - heightCorrection) / canvas.height;
|
|
194
|
+
|
|
195
|
+
canvas.setWidth(offsetWidth - widthCorrection);
|
|
196
|
+
canvas.setHeight(clientHeight - heightCorrection);
|
|
197
|
+
|
|
198
|
+
canvas.getObjects().forEach((obj) => {
|
|
199
|
+
obj.scaleX *= wfactor;
|
|
200
|
+
obj.scaleY *= hfactor;
|
|
201
|
+
obj.left *= wfactor;
|
|
202
|
+
obj.top *= hfactor;
|
|
203
|
+
obj.setCoords();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this.setState({ parentWidth: offsetWidth });
|
|
207
|
+
canvas.requestRenderAll();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
_backgroundColor = (color) => {
|
|
211
|
+
if (this._fc && color) {
|
|
212
|
+
this._fc.backgroundColor = color;
|
|
213
|
+
this._fc.requestRenderAll();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
undo = () => {
|
|
218
|
+
if (this._history && this._history.canUndo()) {
|
|
219
|
+
const previousState = this._history.undo();
|
|
220
|
+
if (previousState) {
|
|
221
|
+
// Restore the previous state
|
|
222
|
+
this.fromJSON(previousState.state);
|
|
223
|
+
// Reapply the background image after undoing
|
|
224
|
+
if (previousState.backgroundImage) {
|
|
225
|
+
this.setBackgroundImage(previousState.backgroundImage.src);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
this._fc.clear(); // If no previous state, clear the canvas
|
|
229
|
+
|
|
230
|
+
this.setBackgroundImage(this.props.backgroundImage);
|
|
231
|
+
}
|
|
232
|
+
this._fc.renderAll(); // Re-render canvas
|
|
233
|
+
this.props.onChange && this.props.onChange();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
redo = () => {
|
|
238
|
+
if (this._history && this._history.canRedo()) {
|
|
239
|
+
const nextState = this._history.redo();
|
|
240
|
+
if (nextState) {
|
|
241
|
+
this.fromJSON(nextState.state);
|
|
242
|
+
// Reapply the background image after redoing
|
|
243
|
+
if (nextState.backgroundImage) {
|
|
244
|
+
this.setBackgroundImage(nextState.backgroundImage.src);
|
|
245
|
+
}
|
|
246
|
+
this._fc.renderAll(); // Re-render canvas
|
|
247
|
+
this.props.onChange && this.props.onChange();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
fromJSON = (json) => {
|
|
253
|
+
if (!json) return;
|
|
254
|
+
this._fc.loadFromJSON(json, () => {
|
|
255
|
+
this._fc.requestRenderAll();
|
|
256
|
+
this.props.onChange && this.props.onChange();
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
componentDidMount = () => {
|
|
261
|
+
const {
|
|
262
|
+
tool,
|
|
263
|
+
undoSteps,
|
|
264
|
+
backgroundColor,
|
|
265
|
+
width = 800,
|
|
266
|
+
height = 600,
|
|
267
|
+
backgroundImage,
|
|
268
|
+
defaultValue,
|
|
269
|
+
} = this.props;
|
|
270
|
+
|
|
271
|
+
this._fc = new fabric.Canvas(this._canvas);
|
|
272
|
+
this._fc.setWidth(width);
|
|
273
|
+
this._fc.setHeight(height);
|
|
274
|
+
|
|
275
|
+
this._initTools(this._fc);
|
|
276
|
+
this._backgroundColor(backgroundColor);
|
|
277
|
+
|
|
278
|
+
const selectedTool = this._tools[tool];
|
|
279
|
+
if (selectedTool) {
|
|
280
|
+
selectedTool.configureCanvas(this.props);
|
|
281
|
+
selectedTool.attachListeners();
|
|
282
|
+
}
|
|
283
|
+
this._selectedTool = selectedTool;
|
|
284
|
+
|
|
285
|
+
window.addEventListener('resize', this._resize, false);
|
|
286
|
+
|
|
287
|
+
this._history = new History(undoSteps);
|
|
288
|
+
|
|
289
|
+
this._fc.on('object:added', this._captureState);
|
|
290
|
+
this._fc.on('object:modified', this._captureState);
|
|
291
|
+
this._fc.on('object:removed', this._captureState);
|
|
292
|
+
|
|
293
|
+
this.setState({ canvasReady: true }, () => {
|
|
294
|
+
// Load the defaultValue to set the initial state of the canvas
|
|
295
|
+
if (defaultValue) {
|
|
296
|
+
// Show loading toast
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
this.fromJSON(defaultValue);
|
|
299
|
+
}, 1000);
|
|
300
|
+
} else {
|
|
301
|
+
if (backgroundImage) {
|
|
302
|
+
this.setBackgroundImage(backgroundImage);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Load the defaultValue to set the initial state of the canvas
|
|
308
|
+
if (defaultValue) {
|
|
309
|
+
// Show loading toast
|
|
310
|
+
const toastId = toast.loading('Loading Canvas...');
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
this.fromJSON(defaultValue);
|
|
313
|
+
// Update toast after loading is done
|
|
314
|
+
toast.update(toastId, {
|
|
315
|
+
render: 'Saved Canvas loaded!',
|
|
316
|
+
type: 'success',
|
|
317
|
+
isLoading: false,
|
|
318
|
+
autoClose: 2000,
|
|
319
|
+
});
|
|
320
|
+
}, 1000);
|
|
321
|
+
} else {
|
|
322
|
+
if (backgroundImage) {
|
|
323
|
+
this.setBackgroundImage(backgroundImage);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
_captureState = debounce(() => {
|
|
329
|
+
if (this._history && this._fc) {
|
|
330
|
+
const state = this._fc.toJSON();
|
|
331
|
+
const backgroundImage = this._fc.backgroundImage
|
|
332
|
+
? {
|
|
333
|
+
src: this._fc.backgroundImage._element.src,
|
|
334
|
+
left: this._fc.backgroundImage.left,
|
|
335
|
+
top: this._fc.backgroundImage.top,
|
|
336
|
+
scaleX: this._fc.backgroundImage.scaleX,
|
|
337
|
+
scaleY: this._fc.backgroundImage.scaleY,
|
|
338
|
+
}
|
|
339
|
+
: null;
|
|
340
|
+
|
|
341
|
+
this._history.keep({
|
|
342
|
+
state,
|
|
343
|
+
backgroundImage,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}, 300);
|
|
347
|
+
|
|
348
|
+
componentWillUnmount = () => {
|
|
349
|
+
window.removeEventListener('resize', this._resize);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
componentDidUpdate = (prevProps) => {
|
|
353
|
+
if (this.props.tool !== prevProps.tool) {
|
|
354
|
+
if (this._selectedTool) {
|
|
355
|
+
this._selectedTool.detachListeners();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this._selectedTool = this._tools[this.props.tool];
|
|
359
|
+
this._selectedTool.configureCanvas(this.props);
|
|
360
|
+
this._selectedTool.attachListeners();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (this.props.backgroundColor !== prevProps.backgroundColor) {
|
|
364
|
+
this._backgroundColor(this.props.backgroundColor);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (this.props.defaultValue !== prevProps.defaultValue) {
|
|
368
|
+
this.fromJSON(this.props.defaultValue);
|
|
369
|
+
} else {
|
|
370
|
+
if (this.props.backgroundImage !== prevProps.backgroundImage) {
|
|
371
|
+
this.setBackgroundImage(this.props.backgroundImage);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
render() {
|
|
377
|
+
const { className, style, width = 800, height = 600 } = this.props;
|
|
378
|
+
const canvasDivStyle = { ...style, width, height };
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div
|
|
382
|
+
className={className}
|
|
383
|
+
ref={(c) => (this._container = c)}
|
|
384
|
+
style={canvasDivStyle}
|
|
385
|
+
>
|
|
386
|
+
<canvas ref={(c) => (this._canvas = c)}>
|
|
387
|
+
Sorry, Canvas HTML5 element is not supported by your
|
|
388
|
+
browser.
|
|
389
|
+
</canvas>
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export default SketchField;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import FabricCanvasTool from './fabrictool';
|
|
2
|
+
import {
|
|
3
|
+
Line as FabricLine,
|
|
4
|
+
Triangle as FabricTriangle,
|
|
5
|
+
Group as FabricGroup,
|
|
6
|
+
} from 'fabric'; // Import specific classes
|
|
7
|
+
|
|
8
|
+
class Arrow extends FabricCanvasTool {
|
|
9
|
+
configureCanvas(props) {
|
|
10
|
+
const canvas = this._canvas;
|
|
11
|
+
|
|
12
|
+
// Disable selection and set up drawing mode
|
|
13
|
+
canvas.isDrawingMode = false;
|
|
14
|
+
canvas.selection = false;
|
|
15
|
+
canvas.forEachObject((o) => {
|
|
16
|
+
o.selectable = false;
|
|
17
|
+
o.evented = false;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Set arrow line properties
|
|
21
|
+
this._width = props.lineWidth || 1;
|
|
22
|
+
this._color = props.lineColor || 'black';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
doMouseDown(o) {
|
|
26
|
+
this.isDown = true;
|
|
27
|
+
const canvas = this._canvas;
|
|
28
|
+
|
|
29
|
+
// Get initial mouse pointer position
|
|
30
|
+
const pointer = canvas.getPointer(o.e);
|
|
31
|
+
const points = [pointer.x, pointer.y, pointer.x, pointer.y];
|
|
32
|
+
|
|
33
|
+
// Create the main line of the arrow
|
|
34
|
+
this.line = new FabricLine(points, {
|
|
35
|
+
strokeWidth: this._width,
|
|
36
|
+
fill: this._color,
|
|
37
|
+
stroke: this._color,
|
|
38
|
+
originX: 'center',
|
|
39
|
+
originY: 'center',
|
|
40
|
+
selectable: false,
|
|
41
|
+
evented: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Create the arrowhead as a triangle
|
|
45
|
+
this.head = new FabricTriangle({
|
|
46
|
+
fill: this._color,
|
|
47
|
+
left: pointer.x,
|
|
48
|
+
top: pointer.y,
|
|
49
|
+
originX: 'center',
|
|
50
|
+
originY: 'center',
|
|
51
|
+
height: 3 * this._width,
|
|
52
|
+
width: 3 * this._width,
|
|
53
|
+
selectable: false,
|
|
54
|
+
evented: false,
|
|
55
|
+
angle: 90,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Add both line and head to the canvas
|
|
59
|
+
canvas.add(this.line);
|
|
60
|
+
canvas.add(this.head);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
doMouseMove(o) {
|
|
64
|
+
if (!this.isDown) return;
|
|
65
|
+
const canvas = this._canvas;
|
|
66
|
+
const pointer = canvas.getPointer(o.e);
|
|
67
|
+
|
|
68
|
+
// Update the line endpoint as mouse moves
|
|
69
|
+
this.line.set({ x2: pointer.x, y2: pointer.y });
|
|
70
|
+
this.line.setCoords();
|
|
71
|
+
|
|
72
|
+
// Calculate angle for the arrowhead to point along the line
|
|
73
|
+
const xDelta = pointer.x - this.line.x1;
|
|
74
|
+
const yDelta = pointer.y - this.line.y1;
|
|
75
|
+
this.head.set({
|
|
76
|
+
left: pointer.x,
|
|
77
|
+
top: pointer.y,
|
|
78
|
+
angle: 90 + (Math.atan2(yDelta, xDelta) * 180) / Math.PI,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Render changes
|
|
82
|
+
canvas.requestRenderAll();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
doMouseUp() {
|
|
86
|
+
this.isDown = false;
|
|
87
|
+
const canvas = this._canvas;
|
|
88
|
+
|
|
89
|
+
// Remove separate line and head and group them into an arrow
|
|
90
|
+
canvas.remove(this.line);
|
|
91
|
+
canvas.remove(this.head);
|
|
92
|
+
|
|
93
|
+
const arrow = new FabricGroup([this.line, this.head], {
|
|
94
|
+
selectable: true,
|
|
95
|
+
evented: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Add the grouped arrow back to the canvas
|
|
99
|
+
canvas.add(arrow);
|
|
100
|
+
canvas.requestRenderAll();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
doMouseOut() {
|
|
104
|
+
this.isDown = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default Arrow;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import FabricCanvasTool from './fabrictool';
|
|
2
|
+
import { Circle as FabricCircle } from 'fabric';
|
|
3
|
+
|
|
4
|
+
class Circle extends FabricCanvasTool {
|
|
5
|
+
constructor(canvas) {
|
|
6
|
+
super(canvas);
|
|
7
|
+
// Define event handlers as arrow functions for consistent references
|
|
8
|
+
this.doMouseDown = (o) => this._doMouseDown(o);
|
|
9
|
+
this.doMouseMove = (o) => this._doMouseMove(o);
|
|
10
|
+
this.doMouseUp = () => this._doMouseUp();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
configureCanvas(props) {
|
|
14
|
+
const canvas = this._canvas;
|
|
15
|
+
canvas.isDrawingMode = false; // Disable drawing mode for custom drawing
|
|
16
|
+
canvas.selection = false;
|
|
17
|
+
this._width = props.lineWidth || 2;
|
|
18
|
+
this._color = props.lineColor || 'black';
|
|
19
|
+
this._fill = props.fillColor || 'transparent';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
attachListeners() {
|
|
23
|
+
this._canvas.on('mouse:down', this.doMouseDown);
|
|
24
|
+
this._canvas.on('mouse:move', this.doMouseMove);
|
|
25
|
+
this._canvas.on('mouse:up', this.doMouseUp);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
detachListeners() {
|
|
29
|
+
this._canvas.off('mouse:down', this.doMouseDown);
|
|
30
|
+
this._canvas.off('mouse:move', this.doMouseMove);
|
|
31
|
+
this._canvas.off('mouse:up', this.doMouseUp);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_doMouseDown(o) {
|
|
35
|
+
const canvas = this._canvas;
|
|
36
|
+
this.isDown = true;
|
|
37
|
+
const pointer = canvas.getPointer(o.e);
|
|
38
|
+
this.startX = pointer.x;
|
|
39
|
+
this.startY = pointer.y;
|
|
40
|
+
|
|
41
|
+
this.circle = new FabricCircle({
|
|
42
|
+
left: this.startX,
|
|
43
|
+
top: this.startY,
|
|
44
|
+
originX: 'center',
|
|
45
|
+
originY: 'center',
|
|
46
|
+
strokeWidth: this._width,
|
|
47
|
+
stroke: this._color,
|
|
48
|
+
fill: this._fill,
|
|
49
|
+
radius: 1, // Start with a minimal radius, updated on `doMouseMove`
|
|
50
|
+
selectable: false,
|
|
51
|
+
evented: false,
|
|
52
|
+
});
|
|
53
|
+
canvas.add(this.circle);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_doMouseMove(o) {
|
|
57
|
+
if (!this.isDown) return;
|
|
58
|
+
const canvas = this._canvas;
|
|
59
|
+
const pointer = canvas.getPointer(o.e);
|
|
60
|
+
const radius = Math.sqrt(
|
|
61
|
+
Math.pow(pointer.x - this.startX, 2) +
|
|
62
|
+
Math.pow(pointer.y - this.startY, 2)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
this.circle.set({ radius });
|
|
66
|
+
this.circle.setCoords();
|
|
67
|
+
canvas.renderAll();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_doMouseUp() {
|
|
71
|
+
this.isDown = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default Circle;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*eslint no-unused-vars: 0*/
|
|
2
|
+
|
|
3
|
+
import FabricCanvasTool from './fabrictool';
|
|
4
|
+
|
|
5
|
+
class DefaultTool extends FabricCanvasTool {
|
|
6
|
+
configureCanvas(props) {
|
|
7
|
+
let canvas = this._canvas;
|
|
8
|
+
canvas.isDrawingMode = canvas.selection = false;
|
|
9
|
+
canvas.forEachObject((o) => (o.selectable = o.evented = false));
|
|
10
|
+
canvas.discardActiveObject();
|
|
11
|
+
canvas.defaultCursor = 'pointer';
|
|
12
|
+
canvas.renderAll();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default DefaultTool;
|