jellies-draw 0.2.4 → 0.2.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jellies-draw",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "A drawer for jellies",
5
5
  "private": false,
6
6
  "main": "./src/index.js",
@@ -117,10 +117,11 @@ export default {
117
117
  if (action === 'clear') {
118
118
  Tools.clear.clear()
119
119
  }
120
- if (this.currentTool !== action &&
120
+ if (this.currentTool !== action &&
121
121
  (
122
122
  (this.toolsCanBeLocked.includes(this.currentTool) && this.isCurrentToolLocked) ||
123
- this.toolsCanBeUsedContinuously.includes(this.currentTool)
123
+ this.toolsCanBeUsedContinuously.includes(this.currentTool) ||
124
+ this.currentTool === 'selector'
124
125
  )
125
126
  ) {
126
127
  this.latestTool = {
@@ -4,6 +4,7 @@ import Clipboard from './clipboard';
4
4
  import Properties from './properties';
5
5
  import Tools from './tools';
6
6
  import Histories from './histories';
7
+ import AttachedText from './tools/helper/attachedText';
7
8
  export default {
8
9
  layer: null,
9
10
  stage: null,
@@ -29,6 +30,7 @@ export default {
29
30
  });
30
31
  this.layer = new Konva.Layer();
31
32
  this.stage.add(this.layer);
33
+ AttachedText.attachLayerListeners(this.layer);
32
34
  document.addEventListener('keydown', this.keydownHandler);
33
35
  document.addEventListener('keyup', this.keyupHandler);
34
36
  Transformer.initialize();
@@ -25,10 +25,19 @@ export default {
25
25
  }
26
26
  },
27
27
  _addEventsToCopiedNode(node) {
28
- if (node.getClassName() === 'Text') {
28
+ const className = node.getClassName();
29
+ if (className === 'Text') {
29
30
  Tools.text.bindEvents(node);
30
- } else if (node.getClassName() === 'Arrow') {
31
+ } else if (className === 'Arrow') {
31
32
  Tools.arrow.bindEvents(node);
33
+ } else if (className === 'Rect') {
34
+ Tools.rectangle.bindEvents(node);
35
+ } else if (className === 'Ellipse') {
36
+ Tools.ellipse.bindEvents(node);
37
+ } else if (className === 'Line') {
38
+ Tools.line.bindEvents(node);
39
+ } else if (className === 'Shape' && node.attrs.nodeType === 'pen') {
40
+ Tools.pen.bindEvents(node);
32
41
  }
33
42
  }
34
43
  }
@@ -3,6 +3,7 @@ import Properties from '../properties'
3
3
  import Canvas from '../canvas'
4
4
  import Histories from '../histories'
5
5
  import AngleSnap from './helper/angleSnap'
6
+ import AttachedText from './helper/attachedText'
6
7
 
7
8
  export default {
8
9
  temporalShape: null,
@@ -58,8 +59,9 @@ export default {
58
59
  })
59
60
  newArrow.on('transformend', Histories.record)
60
61
  newArrow.on('dragend', Histories.record)
62
+ AttachedText.bindShapeEvents(newArrow)
61
63
  },
62
- generate({ x, y, scaleX, scaleY, skewX, rotation, points, stroke, strokeWidth, pointerWidth, pointerLength }) {
64
+ generate({ x, y, scaleX, scaleY, skewX, rotation, points, stroke, strokeWidth, pointerWidth, pointerLength, attachedText, attachedFontSize, attachedFill }) {
63
65
  const newArrow = new Konva.Arrow({
64
66
  x,
65
67
  y,
@@ -72,6 +74,9 @@ export default {
72
74
  strokeWidth,
73
75
  pointerWidth,
74
76
  pointerLength,
77
+ attachedText,
78
+ attachedFontSize,
79
+ attachedFill,
75
80
  name: 'node',
76
81
  nodeType: 'arrow',
77
82
  lineCap: 'round',
@@ -3,6 +3,7 @@ import Properties from '../properties'
3
3
  import Canvas from '../canvas'
4
4
  import Histories from '../histories'
5
5
  import ShapeConstraint from './helper/shapeConstraint'
6
+ import AttachedText from './helper/attachedText'
6
7
 
7
8
  export default {
8
9
  temporalShape: null,
@@ -59,7 +60,7 @@ export default {
59
60
  }
60
61
  }, 200)
61
62
  },
62
- generate({ x, y, radiusX, radiusY, scaleX, scaleY, skewX, rotation, fill, stroke, strokeWidth }) {
63
+ generate({ x, y, radiusX, radiusY, scaleX, scaleY, skewX, rotation, fill, stroke, strokeWidth, attachedText, attachedFontSize, attachedFill }) {
63
64
  const newEllipse = new Konva.Ellipse({
64
65
  x,
65
66
  y,
@@ -72,6 +73,9 @@ export default {
72
73
  fill,
73
74
  stroke,
74
75
  strokeWidth,
76
+ attachedText,
77
+ attachedFontSize,
78
+ attachedFill,
75
79
  name: 'node',
76
80
  nodeType: 'ellipse',
77
81
  strokeScaleEnabled: false
@@ -84,5 +88,6 @@ export default {
84
88
  newEllipse.off('dragend')
85
89
  newEllipse.on('transformend', Histories.record)
86
90
  newEllipse.on('dragend', Histories.record)
91
+ AttachedText.bindShapeEvents(newEllipse)
87
92
  }
88
93
  }
@@ -1,6 +1,7 @@
1
1
  import Konva from 'konva'
2
2
  import Properties from '../properties'
3
3
  import Canvas from '../canvas'
4
+ import AttachedText from './helper/attachedText'
4
5
 
5
6
  export default {
6
7
  temporalShape: null,
@@ -87,7 +88,10 @@ export default {
87
88
  },
88
89
 
89
90
  _destroyErasedNodes() {
90
- this.erasedNodes.forEach(node => node.destroy())
91
+ this.erasedNodes.forEach(node => {
92
+ AttachedText.deleteVisibleNode(node)
93
+ node.destroy()
94
+ })
91
95
  this.erasedNodes.clear()
92
96
  },
93
97
 
@@ -0,0 +1,386 @@
1
+ import Konva from 'konva'
2
+ import Canvas from '../../canvas'
3
+ import Properties from '../../properties'
4
+ import Histories from '../../histories'
5
+
6
+ const SHAPE_CLASS_NAMES = ['Rect', 'Ellipse', 'Line', 'Arrow']
7
+ const CHANGE_EVENTS_BY_CLASS = {
8
+ Rect: ['xChange', 'yChange', 'scaleXChange', 'scaleYChange', 'skewXChange', 'rotationChange', 'widthChange', 'heightChange', 'strokeChange'],
9
+ Ellipse: ['xChange', 'yChange', 'scaleXChange', 'scaleYChange', 'skewXChange', 'rotationChange', 'radiusXChange', 'radiusYChange', 'strokeChange'],
10
+ Line: ['xChange', 'yChange', 'scaleXChange', 'scaleYChange', 'skewXChange', 'rotationChange', 'pointsChange', 'strokeChange'],
11
+ Arrow: ['xChange', 'yChange', 'scaleXChange', 'scaleYChange', 'skewXChange', 'rotationChange', 'pointsChange', 'strokeChange']
12
+ }
13
+
14
+ let activeEditor = null
15
+
16
+ function isPathShape(shape) {
17
+ return shape.className === 'Line' || shape.className === 'Arrow'
18
+ }
19
+
20
+ function supportsAttachedText(node) {
21
+ return SHAPE_CLASS_NAMES.includes(node.className)
22
+ }
23
+
24
+ function getCenterMetrics(shape) {
25
+ if (shape.className === 'Rect') {
26
+ return {
27
+ cx: shape.width() / 2,
28
+ cy: shape.height() / 2,
29
+ width: shape.width(),
30
+ height: shape.height()
31
+ }
32
+ }
33
+ if (shape.className === 'Ellipse') {
34
+ return {
35
+ cx: 0,
36
+ cy: 0,
37
+ width: shape.radiusX() * 2,
38
+ height: shape.radiusY() * 2
39
+ }
40
+ }
41
+ return null
42
+ }
43
+
44
+ function freshShapeTransform(shape) {
45
+ const m = new Konva.Transform()
46
+ const a = shape.attrs
47
+ const x = a.x || 0
48
+ const y = a.y || 0
49
+ if (x !== 0 || y !== 0) m.translate(x, y)
50
+ const rotation = Konva.getAngle(a.rotation || 0)
51
+ if (rotation !== 0) m.rotate(rotation)
52
+ const skewX = a.skewX || 0
53
+ const skewY = a.skewY || 0
54
+ if (skewX !== 0 || skewY !== 0) m.skew(skewX, skewY)
55
+ const sx = a.scaleX === undefined ? 1 : a.scaleX
56
+ const sy = a.scaleY === undefined ? 1 : a.scaleY
57
+ if (sx !== 1 || sy !== 1) m.scale(sx, sy)
58
+ const ox = a.offsetX || 0
59
+ const oy = a.offsetY || 0
60
+ if (ox !== 0 || oy !== 0) m.translate(-ox, -oy)
61
+ return m
62
+ }
63
+
64
+ function getPathMidpoint(shape) {
65
+ const points = shape.getPoints()
66
+ if (!points || points.length < 4) return null
67
+ const midX = (points[0] + points[points.length - 2]) / 2
68
+ const midY = (points[1] + points[points.length - 1]) / 2
69
+ return freshShapeTransform(shape).point({ x: midX, y: midY })
70
+ }
71
+
72
+ function findVisibleNode(shape) {
73
+ if (shape._attachedVisibleNode && shape._attachedVisibleNode.getStage()) {
74
+ return shape._attachedVisibleNode
75
+ }
76
+ shape._attachedVisibleNode = null
77
+ return null
78
+ }
79
+
80
+ function syncCenterText(shape, textNode) {
81
+ const metrics = getCenterMetrics(shape)
82
+ if (!metrics) return
83
+ const padding = 4
84
+ const innerWidth = Math.max(20, metrics.width - padding * 2)
85
+ textNode.fill(shape.stroke() || Properties.strokeColor)
86
+ textNode.width(innerWidth)
87
+ textNode.align('center')
88
+ textNode.verticalAlign('middle')
89
+ textNode.scaleX(shape.scaleX() || 1)
90
+ textNode.scaleY(shape.scaleY() || 1)
91
+ textNode.skewX(shape.skewX() || 0)
92
+ textNode.rotation(shape.rotation() || 0)
93
+ textNode.offsetX(innerWidth / 2)
94
+ textNode.offsetY(textNode.height() / 2)
95
+ const worldCenter = freshShapeTransform(shape).point({ x: metrics.cx, y: metrics.cy })
96
+ textNode.absolutePosition(worldCenter)
97
+ }
98
+
99
+ function syncPathText(shape, textNode) {
100
+ const midpoint = getPathMidpoint(shape)
101
+ if (!midpoint) return
102
+ textNode.fill(shape.stroke() || Properties.strokeColor)
103
+ textNode.setAttr('width', undefined)
104
+ textNode.align('left')
105
+ textNode.verticalAlign('top')
106
+ textNode.scaleX(1)
107
+ textNode.scaleY(1)
108
+ textNode.skewX(0)
109
+ textNode.rotation(0)
110
+ textNode.offsetX(textNode.width() / 2)
111
+ textNode.offsetY(textNode.height() / 2)
112
+ textNode.absolutePosition(midpoint)
113
+ applyLineGap(shape, textNode)
114
+ }
115
+
116
+ function applyLineGap(shape, textNode) {
117
+ const text = textNode.text()
118
+ const points = shape.getPoints()
119
+ if (!text || !points || points.length < 4) {
120
+ shape.dashEnabled(false)
121
+ return
122
+ }
123
+ const transform = freshShapeTransform(shape)
124
+ const start = transform.point({ x: points[0], y: points[1] })
125
+ const end = transform.point({ x: points[points.length - 2], y: points[points.length - 1] })
126
+ const screenLength = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2))
127
+ const padding = 6
128
+ const screenGap = textNode.width() + padding * 2
129
+ if (screenLength > screenGap + 4) {
130
+ const halfRemaining = (screenLength - screenGap) / 2
131
+ shape.dash([halfRemaining, screenGap, halfRemaining])
132
+ shape.dashEnabled(true)
133
+ } else {
134
+ shape.dashEnabled(false)
135
+ }
136
+ }
137
+
138
+ function syncVisibleNode(shape) {
139
+ const visible = findVisibleNode(shape)
140
+ if (!visible) return
141
+ if (isPathShape(shape)) {
142
+ syncPathText(shape, visible)
143
+ } else {
144
+ syncCenterText(shape, visible)
145
+ }
146
+ }
147
+
148
+ function createVisibleText(shape, options = {}) {
149
+ const text = shape.attrs.attachedText
150
+ if (!options.allowEmpty && (text === undefined || text === null || text === '')) {
151
+ return null
152
+ }
153
+ if (!shape.getStage()) return null
154
+ const existing = findVisibleNode(shape)
155
+ if (existing) {
156
+ existing.text(text || '')
157
+ syncVisibleNode(shape)
158
+ return existing
159
+ }
160
+ const fontSize = shape.attrs.attachedFontSize || Properties.fontSize
161
+ const fill = shape.stroke() || Properties.strokeColor
162
+ const visible = new Konva.Text({
163
+ text: text || '',
164
+ fontSize,
165
+ fill,
166
+ align: isPathShape(shape) ? 'left' : 'center',
167
+ verticalAlign: isPathShape(shape) ? 'top' : 'middle',
168
+ name: 'attached',
169
+ listening: false,
170
+ strokeEnabled: false
171
+ })
172
+ shape._attachedVisibleNode = visible
173
+ Canvas.layer.add(visible)
174
+ syncVisibleNode(shape)
175
+ raiseTransformerControls()
176
+ return visible
177
+ }
178
+
179
+ function raiseTransformerControls() {
180
+ if (!Canvas.layer) return
181
+ const dragArea = Canvas.layer.findOne('.dragArea')
182
+ if (dragArea) dragArea.moveToTop()
183
+ const transformer = Canvas.layer.findOne('Transformer')
184
+ if (transformer) transformer.moveToTop()
185
+ }
186
+
187
+ function deleteVisibleNode(shape) {
188
+ const visible = findVisibleNode(shape)
189
+ if (visible) {
190
+ visible.destroy()
191
+ }
192
+ shape._attachedVisibleNode = null
193
+ if (isPathShape(shape)) {
194
+ shape.dashEnabled(false)
195
+ }
196
+ }
197
+
198
+ function bindShapeEvents(shape) {
199
+ if (!supportsAttachedText(shape)) return
200
+ const events = CHANGE_EVENTS_BY_CLASS[shape.className] || []
201
+ const eventStr = events.map(e => `${e}.attached`).join(' ')
202
+ shape.off('.attached')
203
+ if (eventStr) {
204
+ shape.on(eventStr, () => syncVisibleNode(shape))
205
+ }
206
+ shape.on('dblclick.attached dbltap.attached', () => startEdit(shape))
207
+ }
208
+
209
+ function startEdit(shape) {
210
+ if (Canvas.isEditingText) return
211
+ if (!supportsAttachedText(shape)) return
212
+ Canvas.isEditingText = true
213
+ Properties.tool = 'text'
214
+ let visible = findVisibleNode(shape)
215
+ if (!visible) {
216
+ visible = createVisibleText(shape, { allowEmpty: true })
217
+ }
218
+ if (!visible) {
219
+ Canvas.isEditingText = false
220
+ Properties.tool = 'selector'
221
+ return
222
+ }
223
+ Canvas.layer.batchDraw()
224
+ openEditor(shape, visible)
225
+ }
226
+
227
+ function openEditor(shape, visible) {
228
+ if (!visible) {
229
+ Canvas.isEditingText = false
230
+ Properties.tool = 'selector'
231
+ return
232
+ }
233
+ if (activeEditor) {
234
+ activeEditor.cleanup(false)
235
+ activeEditor = null
236
+ }
237
+ const stage = Canvas.stage
238
+ const originalText = shape.attrs.attachedText || ''
239
+ const fontSize = shape.attrs.attachedFontSize || Properties.fontSize
240
+ const fill = shape.stroke() || Properties.strokeColor
241
+ const containerOffset = {
242
+ x: stage.container().offsetLeft,
243
+ y: stage.container().offsetTop
244
+ }
245
+ const textarea = document.createElement('textarea')
246
+ document.body.appendChild(textarea)
247
+ textarea.value = originalText
248
+ textarea.style.position = 'absolute'
249
+ textarea.style.fontSize = `${fontSize}px`
250
+ textarea.style.lineHeight = '1'
251
+ textarea.style.color = 'transparent'
252
+ textarea.style.caretColor = fill
253
+ textarea.style.fontFamily = visible.fontFamily()
254
+ textarea.style.border = 'none'
255
+ textarea.style.padding = '0'
256
+ textarea.style.margin = '0'
257
+ textarea.style.background = 'transparent'
258
+ textarea.style.outline = 'none'
259
+ textarea.style.resize = 'none'
260
+ textarea.style.textAlign = 'center'
261
+ textarea.style.overflow = 'hidden'
262
+ textarea.style.zIndex = '10000'
263
+ textarea.style.transformOrigin = 'center center'
264
+ textarea.setAttribute('wrap', isPathShape(shape) ? 'off' : 'soft')
265
+
266
+ const state = { closed: false }
267
+
268
+ function applyLiveValue() {
269
+ if (!visible) return
270
+ visible.text(textarea.value)
271
+ syncVisibleNode(shape)
272
+ const w = visible.width() || fontSize
273
+ const h = visible.height() || fontSize
274
+ const cx = visible.x()
275
+ const cy = visible.y()
276
+ textarea.style.width = `${w}px`
277
+ textarea.style.height = `${h}px`
278
+ textarea.style.left = `${containerOffset.x + cx - w / 2}px`
279
+ textarea.style.top = `${containerOffset.y + cy - h / 2}px`
280
+ const rotation = visible.rotation()
281
+ textarea.style.transform = rotation ? `rotate(${rotation}deg)` : ''
282
+ Canvas.layer.batchDraw()
283
+ }
284
+
285
+ applyLiveValue()
286
+ textarea.focus()
287
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length)
288
+
289
+ function keyHandler(event) {
290
+ if (event.key === 'Enter' && !event.shiftKey) {
291
+ event.preventDefault()
292
+ cleanup(true)
293
+ } else if (event.key === 'Escape') {
294
+ event.preventDefault()
295
+ cleanup(false)
296
+ }
297
+ }
298
+
299
+ function inputHandler() {
300
+ applyLiveValue()
301
+ }
302
+
303
+ function mouseHandler(event) {
304
+ if (event.target !== textarea) {
305
+ cleanup(true)
306
+ }
307
+ }
308
+
309
+ function cleanup(commit) {
310
+ if (state.closed) return
311
+ state.closed = true
312
+ textarea.removeEventListener('keydown', keyHandler)
313
+ textarea.removeEventListener('input', inputHandler)
314
+ window.removeEventListener('mousedown', mouseHandler, true)
315
+ if (textarea.parentNode) textarea.parentNode.removeChild(textarea)
316
+ activeEditor = null
317
+ if (commit) {
318
+ const value = textarea.value
319
+ if (value) {
320
+ shape.setAttr('attachedText', value)
321
+ shape.setAttr('attachedFontSize', fontSize)
322
+ const node = findVisibleNode(shape)
323
+ if (node) {
324
+ node.text(value)
325
+ syncVisibleNode(shape)
326
+ } else {
327
+ createVisibleText(shape)
328
+ }
329
+ } else {
330
+ shape.setAttr('attachedText', '')
331
+ deleteVisibleNode(shape)
332
+ }
333
+ } else {
334
+ shape.setAttr('attachedText', originalText)
335
+ const node = findVisibleNode(shape)
336
+ if (originalText) {
337
+ if (node) {
338
+ node.text(originalText)
339
+ syncVisibleNode(shape)
340
+ } else {
341
+ createVisibleText(shape)
342
+ }
343
+ } else if (node) {
344
+ deleteVisibleNode(shape)
345
+ }
346
+ }
347
+ Canvas.isEditingText = false
348
+ Properties.tool = 'selector'
349
+ Canvas.layer.batchDraw()
350
+ if (commit) Histories.record()
351
+ }
352
+
353
+ textarea.addEventListener('keydown', keyHandler)
354
+ textarea.addEventListener('input', inputHandler)
355
+ setTimeout(() => {
356
+ if (!state.closed) {
357
+ window.addEventListener('mousedown', mouseHandler, true)
358
+ }
359
+ })
360
+
361
+ activeEditor = { cleanup }
362
+ }
363
+
364
+ function handleNodeAdded(event) {
365
+ const node = event.child
366
+ if (!node || !node.attrs) return
367
+ if (!supportsAttachedText(node)) return
368
+ if (node.attrs.attachedText && node.getStage()) {
369
+ createVisibleText(node)
370
+ }
371
+ }
372
+
373
+ function attachLayerListeners(layer) {
374
+ if (!layer || layer._attachedLayerHooked) return
375
+ layer._attachedLayerHooked = true
376
+ layer.on('add', handleNodeAdded)
377
+ }
378
+
379
+ export default {
380
+ bindShapeEvents,
381
+ syncVisibleNode,
382
+ createVisibleText,
383
+ deleteVisibleNode,
384
+ startEdit,
385
+ attachLayerListeners
386
+ }
@@ -3,6 +3,7 @@ import Properties from '../properties'
3
3
  import Canvas from '../canvas'
4
4
  import Histories from '../histories'
5
5
  import AngleSnap from './helper/angleSnap'
6
+ import AttachedText from './helper/attachedText'
6
7
 
7
8
  export default {
8
9
  temporalShape: null,
@@ -41,7 +42,7 @@ export default {
41
42
  }
42
43
  this.temporalShape = null
43
44
  },
44
- generate({ x, y, scaleX, scaleY, skewX, rotation, points, stroke, strokeWidth }) {
45
+ generate({ x, y, scaleX, scaleY, skewX, rotation, points, stroke, strokeWidth, attachedText, attachedFontSize, attachedFill }) {
45
46
  const newLine = new Konva.Line({
46
47
  x,
47
48
  y,
@@ -52,6 +53,9 @@ export default {
52
53
  points,
53
54
  stroke,
54
55
  strokeWidth,
56
+ attachedText,
57
+ attachedFontSize,
58
+ attachedFill,
55
59
  name: 'node',
56
60
  nodeType: 'line',
57
61
  lineCap: 'round',
@@ -66,5 +70,6 @@ export default {
66
70
  newLine.off('dragend')
67
71
  newLine.on('transformend', Histories.record)
68
72
  newLine.on('dragend', Histories.record)
73
+ AttachedText.bindShapeEvents(newLine)
69
74
  }
70
75
  }
@@ -3,6 +3,7 @@ import Properties from '../properties'
3
3
  import Canvas from '../canvas'
4
4
  import Histories from '../histories'
5
5
  import ShapeConstraint from './helper/shapeConstraint'
6
+ import AttachedText from './helper/attachedText'
6
7
 
7
8
  export default {
8
9
  temporalShape: null,
@@ -57,11 +58,11 @@ export default {
57
58
  }
58
59
  }, 200)
59
60
  },
60
- generate({ x, y, scaleX, scaleY, width, height, skewX, rotation, fill, stroke, strokeWidth }) {
61
+ generate({ x, y, scaleX, scaleY, width, height, skewX, rotation, fill, stroke, strokeWidth, attachedText, attachedFontSize, attachedFill }) {
61
62
  const newRectangle = new Konva.Rect({
62
63
  x,
63
64
  y,
64
- scaleX,
65
+ scaleX,
65
66
  scaleY,
66
67
  width,
67
68
  height,
@@ -70,6 +71,9 @@ export default {
70
71
  fill,
71
72
  stroke,
72
73
  strokeWidth,
74
+ attachedText,
75
+ attachedFontSize,
76
+ attachedFill,
73
77
  name: 'node',
74
78
  nodeType: 'rectangle',
75
79
  strokeScaleEnabled: false
@@ -82,5 +86,6 @@ export default {
82
86
  newRectangle.off('dragend')
83
87
  newRectangle.on('transformend', Histories.record)
84
88
  newRectangle.on('dragend', Histories.record)
89
+ AttachedText.bindShapeEvents(newRectangle)
85
90
  }
86
91
  }
@@ -60,111 +60,79 @@ export default {
60
60
  Canvas.isEditingText = true;
61
61
  Properties.tool = 'text'
62
62
  this.temporalText = newText;
63
- newText.hide();
64
63
  Transformer.deselectAllNodes();
65
64
 
66
- const textPosition = newText.absolutePosition();
67
- const areaPosition = {
68
- x: Canvas.stage.container().offsetLeft + textPosition.x,
69
- y: Canvas.stage.container().offsetTop + textPosition.y
70
- };
71
-
72
65
  if (this.textarea) {
73
66
  this.textarea.removeEventListener('keydown', this.textareaEditingHandler);
67
+ if (this.textarea.parentNode) {
68
+ this.textarea.parentNode.removeChild(this.textarea);
69
+ }
74
70
  }
75
71
 
76
72
  this.textarea = document.createElement('textarea');
77
73
  document.body.appendChild(this.textarea);
78
-
74
+
79
75
  this.textarea.value = newText.text();
80
76
  this.textarea.style.position = 'absolute';
81
- this.textarea.style.top = `${areaPosition.y}px`;
82
- this.textarea.style.left = `${areaPosition.x}px`;
83
- this.textarea.style.width = `${newText.width() - newText.padding() * 2}px`;
84
- this.textarea.style.height =
85
- `${ newText.height() - newText.padding() * 2 + 5 }px`;
86
77
  this.textarea.style.fontSize = `${ newText.fontSize() }px`;
87
78
  this.textarea.style.border = 'none';
88
79
  this.textarea.style.padding = '0';
89
80
  this.textarea.style.margin = '0';
90
81
  this.textarea.style.overflow = 'hidden';
91
- this.textarea.style.background = 'none';
82
+ this.textarea.style.background = 'transparent';
92
83
  this.textarea.style.outline = 'none';
93
84
  this.textarea.style.resize = 'none';
94
85
  this.textarea.style.lineHeight = newText.lineHeight();
95
86
  this.textarea.style.fontFamily = newText.fontFamily();
96
- this.textarea.style.transformOrigin = 'left top';
97
87
  this.textarea.style.textAlign = newText.align();
98
- this.textarea.style.color = newText.fill();
99
- const rotation = newText.rotation();
100
- let transform = '';
101
- if (rotation) {
102
- transform += `rotateZ(${ rotation }deg)`;
103
- }
104
-
105
- let px = 0;
106
-
107
- const isFirefox =
108
- navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
109
- if (isFirefox) {
110
- px += 2 + Math.round(newText.fontSize() / 20);
111
- }
112
- transform += `translateY(-${ px }px)`;
113
-
114
- this.textarea.style.transform = transform;
115
-
116
- this.textarea.style.height = 'auto';
117
- this.textarea.style.height = `${this.textarea.scrollHeight + 3}px`;
88
+ this.textarea.style.color = 'transparent';
89
+ this.textarea.style.caretColor = newText.fill();
90
+ this.textarea.style.transformOrigin = 'left top';
91
+ this.textarea.style.zIndex = '10000';
118
92
 
93
+ this._syncTextareaToText(newText);
119
94
  this.textarea.focus();
95
+ this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
120
96
 
121
97
  this.textareaEditingHandler = this._handleTextareaEditing.bind(this);
122
98
  this.disableTextareaHandler = this._handleDisableTextarea.bind(this);
123
99
  this.textarea.addEventListener('keydown', this.textareaEditingHandler);
100
+ this.textarea.addEventListener('input', this.textareaEditingHandler);
124
101
  setTimeout(() => {
125
102
  window.addEventListener('mousedown', this.disableTextareaHandler);
126
103
  });
127
104
  },
105
+ _syncTextareaToText(newText) {
106
+ const textPosition = newText.absolutePosition();
107
+ const areaPosition = {
108
+ x: Canvas.stage.container().offsetLeft + textPosition.x,
109
+ y: Canvas.stage.container().offsetTop + textPosition.y
110
+ };
111
+ this.textarea.style.top = `${areaPosition.y}px`;
112
+ this.textarea.style.left = `${areaPosition.x}px`;
113
+ this.textarea.style.width = `${newText.width() - newText.padding() * 2}px`;
114
+ this.textarea.style.height = `${newText.height() - newText.padding() * 2}px`;
115
+ const rotation = newText.rotation();
116
+ this.textarea.style.transform = rotation ? `rotateZ(${rotation}deg)` : '';
117
+ },
128
118
  _handleDisableTextarea(event) {
129
119
  if (event.target !== this.textarea) {
130
120
  if (this.textarea.value) {
131
121
  this.temporalText.text(this.textarea.value)
132
- this.temporalText.show()
133
122
  this.textarea.remove()
134
123
  } else {
135
124
  this.textarea.remove()
136
125
  this.temporalText.remove()
137
126
  }
138
127
  Properties.tool = 'selector'
128
+ Canvas.isEditingText = false
139
129
  window.removeEventListener('mousedown', this.disableTextareaHandler)
140
130
  }
141
131
  },
142
132
  _handleTextareaEditing() {
143
- const scale = this.temporalText.getAbsoluteScale().x;
144
- this._setTextareaWidth(this.temporalText.width() * scale);
145
- this.textarea.style.height = 'auto';
146
- this.textarea.style.height =
147
- `${this.textarea.scrollHeight + this.temporalText.fontSize() }px`;
148
- },
149
- _setTextareaWidth(newWidth) {
150
- if (!newWidth) {
151
- newWidth = this.textNode.placeholder.length * this.temporalText.fontSize();
152
- }
153
- const isSafari = /^((?!chrome|android).)*safari/i.test(
154
- navigator.userAgent
155
- );
156
- const isFirefox =
157
- navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
158
- if (isSafari || isFirefox) {
159
- newWidth = Math.ceil(newWidth);
160
- }
161
-
162
- const isEdge =
163
- document.documentMode || /Edge/.test(navigator.userAgent);
164
- if (isEdge) {
165
- newWidth += 1;
166
- }
167
- this.textarea.style.width = `${ newWidth }px`;
133
+ this.temporalText.text(this.textarea.value);
134
+ this._syncTextareaToText(this.temporalText);
135
+ Canvas.layer.batchDraw();
168
136
  },
169
137
  generate({ text, x, y, fill, fontSize, width, height, scaleX, scaleY, skewX, rotation }) {
170
138
  const newText = new Konva.Text({
@@ -2,6 +2,7 @@ import Konva from 'konva'
2
2
  import Canvas from './canvas'
3
3
  import Properties from './properties'
4
4
  import Histories from './histories'
5
+ import AttachedText from './tools/helper/attachedText'
5
6
  export default {
6
7
  transformer: null,
7
8
  linearTransformer: {
@@ -11,14 +12,21 @@ export default {
11
12
  },
12
13
  initialize() {
13
14
  this.transformer = new Konva.Transformer();
14
-
15
+
15
16
  // 创建一个透明的拖动区域
16
17
  this.dragArea = new Konva.Rect({
18
+ name: 'dragArea',
17
19
  fill: 'transparent',
18
20
  draggable: true,
19
21
  listening: true
20
22
  });
21
-
23
+ this.dragArea.on('dblclick dbltap', (event) => {
24
+ const nodes = this._nodes();
25
+ if (nodes.length === 1) {
26
+ nodes[0]._fire('dblclick', { evt: event.evt, target: nodes[0] });
27
+ }
28
+ });
29
+
22
30
  Canvas.layer.add(this.dragArea);
23
31
  Canvas.layer.add(this.transformer);
24
32
  },
@@ -31,7 +39,10 @@ export default {
31
39
  if (nodes.length === 0) {
32
40
  return;
33
41
  }
34
- nodes.forEach((node) => node.destroy());
42
+ nodes.forEach((node) => {
43
+ AttachedText.deleteVisibleNode(node);
44
+ node.destroy();
45
+ });
35
46
  this.deselectAllNodes();
36
47
  Histories.record();
37
48
  },