Skip to content
This repository was archived by the owner on Feb 14, 2023. It is now read-only.

Commit 4551bc7

Browse files
committed
Refactoring and adding some basic events
- Emit move related event, close #15 - Emit resize related event - Breaking change draw related event by renaming event name - Refactor by registering interact logic using MutationObserver
1 parent 802ca0f commit 4551bc7

File tree

10 files changed

+138
-82
lines changed

10 files changed

+138
-82
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class MyCanvas extends Vue {
6262

6363
</details>
6464

65+
For complete example see [`src/stories/*/*.vue`](./src/stories)
6566

6667
### Props
6768
> \* : must be set if no background
@@ -70,7 +71,7 @@ export class MyCanvas extends Vue {
7071
|---------- |-------- |---------- |---------- |---------- |
7172
| `width` | width of drawing canvas | `Number` | *optional | width of background |
7273
| `height` | width of drawing canvas | `Number` | *optional | height of background |
73-
| `grid` | set grid for sanpping. `:grid="[w,h]"` for setting width and height. `:grid="w"` for setting grid in square | `Array[2]` or `Number` | optional | `null` |
74+
| `grid` | set grid for snapping. `:grid="[w,h]"` for setting width and height. `:grid="w"` for setting grid in square | `Array[2]` or `Number` | optional | `null` |
7475
| `minSize` | set minimum size of annotation. `:minSize="[w,h]"` for set minimum width and height of annotation size. `:grid="w"` for set minimum width and height of annotation size equal to `w` | `Array[2]` or `Number` | optional | `false` |
7576
| `drawing` | switch to drawing mode | `Boolean` | optional | `false` |
7677
| `inertia` | enable inertia moment animation when interacting | `Boolean` | optional | `false` |
@@ -92,14 +93,19 @@ export class MyCanvas extends Vue {
9293
|---------- |-------- |---------- |
9394
| `select` | emit when element is click/select | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
9495
| `unselect` | emit when element is unselected (by clicking the background) | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
95-
| `drawfinish` | emit when drawing element is finish | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
96-
| `drawcancel` | emit when drawing element is canceled (via right click) |
96+
| `move` | emit when element is moved | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
97+
| `move-end` | emit __after__ the element is moved | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
98+
| `resize` | emit when element is resized | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
99+
| `resize-end` | emit __after__ the element is resized | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
100+
| `draw` | emit when drawing an element | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
101+
| `draw-end` | emit when drawing element is finish | element: [`SVG.Element`](http://svgjs.com/elements/#elements) |
102+
| `draw-cancel` | emit when drawing element is canceled (via right click) |
97103
| `update:delete` | emit when shape was successfully deleted |
98104

99105
> Tips: use `element.node.isSameNode(this.$refs.myAnnotation)` for identifying the element.
100106
101107
### Style CSS
102-
> Vue-Annotator use `svg.select.js` with this predefined style that can be override
108+
> Vue-Annotator use `svg.select.js`
103109
104110
| Class name | Description | Notes
105111
|---------- |-------- |--------- |

index.d.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,6 @@ export default class VAnnotator extends Vue {
3232
background: SVGForeignObjectElement
3333
annotations: SVGGElement
3434

35-
/** @see select.js */
36-
selectables: Interactable
37-
38-
/** @see manipulate.js */
39-
interactables: InteractStatic[]
40-
4135
/** @see drawing.js */
4236
drawingable?: Interactable
4337
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vue-annotator",
3-
"version": "0.12.1",
3+
"version": "0.13.0",
44
"description": "Vue Component for drawing annotation (box, etc)",
55
"homepage": "http://vue-annotator.surge.sh",
66
"repository": "github:DrSensor/vue-annotator",

src/components/Annotator.vue

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,30 @@ export default {
8989
} else this.$forceUpdate()
9090
},
9191
92+
created () {
93+
this.observer = new MutationObserver(mutations => {
94+
for (const mutation of mutations) {
95+
if (mutation.type === 'childList') {
96+
for (const node of mutation.addedNodes) {
97+
this.makeInteractable(node, this.drawing)
98+
this.makeSelectable(node)
99+
}
100+
for (const node of mutation.removedNodes)
101+
interact(node).unset()
102+
}
103+
}
104+
})
105+
},
106+
92107
mounted () {
93-
this.background = SVG.adopt(this.$refs.bgSvg)
94-
this.annotations = SVG.adopt(this.$refs.annotations)
95-
this.enableInteraction(!this.noInteract)
96-
this.enableSelection(!this.noSelect)
97-
this.enableDrawing(this.drawing)
108+
this.$nextTick(() => {
109+
this.background = SVG.adopt(this.$refs.bgSvg)
110+
this.annotations = SVG.adopt(this.$refs.annotations)
111+
this.enableSelection(!this.noSelect)
112+
this.enableInteraction(!this.noInteract)
113+
this.enableDrawing(this.drawing)
114+
this.observer.observe(this.annotations.node, { childList: true })
115+
})
98116
}
99117
}
100118
</script>

src/mixins/delete.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import interact from 'interactjs'
12
import SVG from 'svg.js'
23
import 'svg.select.js'
34

@@ -13,26 +14,13 @@ export default {
1314
},
1415

1516
methods: {
16-
$_gracefulUnset (elm) {
17-
const unset = (interacts, callback) => interacts.forEach((interack, index) => {
18-
if (interack.target.isSameNode(elm)) {
19-
callback(interack)
20-
delete interacts[index]
21-
interacts.splice(index, 1)
22-
}
23-
})
24-
25-
unset(this.interactables, inter => inter.unset())
26-
unset(this.selectables, inter => this.background.off('click', inter.unselectListener))
27-
},
28-
2917
triggerDelete () {
3018
this.$refs.annotations.childNodes.forEach(elm => {
3119
const shape = SVG.adopt(elm)
3220
if (shape.data('selected')) {
33-
this.$_gracefulUnset(elm)
21+
interact(elm).unset()
3422
shape.selectize(false, { deepSelect: ['g', 'foreignObject', 'polygon'].includes(shape.type) })
35-
setTimeout(() => shape.remove(), 100) // Dirty hack for multiple select delete
23+
this.$nextTick(() => shape.remove())
3624
}
3725
})
3826
this.$emit('update:delete', false)

src/mixins/drawing.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,17 @@ export default {
3737
.attr('oncontextmenu', 'return false;')
3838
.on('contextmenu', event => {
3939
annotator.draw('cancel').data('canceled', true)
40-
this.$emit('drawcancel')
40+
this.$emit('draw-cancel')
4141
})
4242
},
4343

44-
onmove: event => annotator.draw('update', event),
44+
onmove: event => this.$emit('draw', annotator.draw('update', event)),
4545

4646
onend: event => {
4747
if (!annotator.data('canceled')) {
4848
annotator.draw('stop', event).style('cursor', null).removeClass('foreground').toParent(this.annotations).off('contextmenu')
4949
this.$forceUpdate()
50-
this.$emit('drawfinish', annotator)
50+
this.$emit('draw-end', annotator)
5151
} else annotator.off('contextmenu').data('canceled', null)
5252

5353
for (const key in attr) {

src/mixins/manipulate.js

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,6 @@ export default {
1616
multipleSelect: Boolean
1717
},
1818

19-
data () {
20-
return {
21-
interactables: [] // waste of memory?
22-
}
23-
},
24-
2519
methods: {
2620
/** @func fixDrawingMode is hard fix, need to think another solution */
2721
makeInteractable (node, fixDrawingMode = false) {
@@ -39,8 +33,10 @@ export default {
3933
},
4034
autoScroll: true,
4135

36+
onend: event => this.$emit('move-end', SVG.adopt(event.target)),
37+
4238
// call this function on every dragmove event
43-
onmove: event => SVG.adopt(event.target).dmove(event.dx, event.dy)
39+
onmove: event => this.$emit('move', SVG.adopt(event.target).dmove(event.dx, event.dy))
4440
})
4541

4642
.resizable({
@@ -61,7 +57,9 @@ export default {
6157
min: { width: this.minWidth, height: this.minHeight }
6258
},
6359

64-
// call this function on every resizemove event
60+
onend: event => this.$emit('resize-end', SVG.adopt(event.target)),
61+
62+
// called on every resizemove event
6563
onmove: (event) => {
6664
const target = SVG.adopt(event.target)
6765

@@ -104,6 +102,8 @@ export default {
104102
target.size(event.rect.width, event.rect.height)
105103
break
106104
}
105+
106+
this.$emit('resize', target)
107107
}
108108
})
109109

@@ -113,28 +113,20 @@ export default {
113113
enableInteraction (enabled = true) {
114114
if (this.$refs.annotations.hasChildNodes()) {
115115
this.$refs.annotations.childNodes.forEach((node, id) => {
116-
if (!enabled && this.interactables.length) {
117-
this.interactables.forEach(interaction => interaction.draggable(false).resizable(false))
118-
} else this.interactables[id] = this.makeInteractable(node)
116+
if (!enabled) {
117+
interact(node).draggable(false).resizable(false)
118+
} else this.makeInteractable(node)
119119
})
120120
}
121-
},
122-
123-
$_onUpdate (isDrawing) {
124-
if ((this.$refs.annotations.hasChildNodes() ? this.$refs.annotations.childNodes.length : 0) > this.interactables.length) {
125-
const element = this.$refs.annotations.childNodes[this.$refs.annotations.childNodes.length - 1]
126-
const interaction = this.makeInteractable(element, isDrawing)
127-
this.interactables.push(interaction)
128-
}
129121
}
130122
},
131123

132124
mounted () {
133-
this.$on('drawfinish', () => this.$_onUpdate(false))
125+
this.$on('draw-end', annotator => this.makeInteractable(annotator.node, false))
134126
},
135127

136-
updated () {
137-
this.$_onUpdate(this.drawing)
128+
beforeDestroy () {
129+
this.$off('draw-end')
138130
},
139131

140132
computed: {

src/mixins/select.js

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ export default {
77
multipleSelect: Boolean
88
},
99

10-
data () {
11-
return {
12-
selectables: [] // waste of memory?
13-
}
14-
},
15-
1610
methods: {
1711
makeSelectable (node) {
1812
const annotator = SVG.adopt(node)
@@ -57,36 +51,35 @@ export default {
5751
}
5852
const selection = interact(node).on('tap', selectListener)
5953

60-
selection.unselectListener = () => {
61-
if (annotator.data('selected')) {
62-
annotator.selectize(false, { deepSelect: ['g', 'foreignObject', 'polygon'].includes(annotator.type) })
63-
annotator.data('selected', null)
64-
this.$emit('unselect', annotator)
65-
}
66-
}
67-
68-
this.background.on('click', selection.unselectListener)
6954
return selection
7055
},
7156

7257
enableSelection (enabled = true) {
7358
if (this.$refs.annotations.hasChildNodes()) {
7459
this.$refs.annotations.childNodes.forEach((node, id) => {
75-
if (!enabled && this.selectables.length) {
76-
this.selectables.forEach(selection => selection.off('tap'))
60+
if (!enabled) {
61+
interact(node).selection.off('tap')
7762
this.background.fire('click') // unselect all
7863
this.background.off('click')
79-
} else this.selectables[id] = this.makeSelectable(node)
64+
} else {
65+
this.makeSelectable(node)
66+
this.background.on('click', this.unselectAll)
67+
}
8068
})
8169
}
82-
}
83-
},
70+
},
8471

85-
updated () {
86-
if ((this.$refs.annotations.hasChildNodes() ? this.$refs.annotations.childNodes.length : 0) > this.selectables.length) {
87-
const element = this.$refs.annotations.childNodes[this.$refs.annotations.childNodes.length - 1]
88-
const selection = this.makeSelectable(element)
89-
this.selectables.push(selection)
72+
unselectAll () {
73+
if (this.$refs.annotations.hasChildNodes()) {
74+
this.$refs.annotations.childNodes.forEach((node, id) => {
75+
const annotator = SVG.adopt(node)
76+
if (annotator.data('selected')) {
77+
annotator.selectize(false, { deepSelect: ['g', 'foreignObject', 'polygon'].includes(annotator.type) })
78+
annotator.data('selected', null)
79+
this.$emit('unselect', annotator)
80+
}
81+
})
82+
}
9083
}
9184
}
9285
}

src/stories/Interact and Manipulate/Create.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div id="annotation">
33
<button @click="drawing = !drawing">{{drawing ? "stop" : "drawing" }}</button>
44
<annotations :drawing="drawing" :inertia="inertia" :minSize="minSize" :grid="grid" :multipleSelect="multipleSelect"
5-
@drawfinish="drawfinish" @drawcancel="drawcancel">
5+
@draw-end="drawfinish" @draw-cancel="drawcancel" @draw="onDraw">
66
<img draggable="false" src="https://cdn.css-tricks.com/wp-content/uploads/2017/01/vue-2-1.jpg" />
77
<!-- <polygon class="stroke" slot="drawing" /> not supported yet -->
88
<rect class="stroke" slot="drawing" />
@@ -40,6 +40,9 @@ export default {
4040
},
4141
drawcancel () {
4242
this.$action('cancel drawing')
43+
},
44+
onDraw (element) {
45+
console.log(element.node)
4346
}
4447
}
4548
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<template>
2+
<div id="annotation">
3+
<annotations @move-end="moved" @resize="resized" :minSize="minSize" width="600" height="600" :grid="grid" :inertia="inertia" :multipleSelect="multipleSelect">
4+
<rect class="stroke" slot="annotation" x="100" y="100" width="100" height="100" />
5+
<rect class="stroke" slot="annotation" x="300" y="150" width="170" height="100" />
6+
</annotations>
7+
</div>
8+
</template>
9+
10+
<script>
11+
import Annotator from 'components/Annotator'
12+
13+
import { number, boolean } from '@storybook/addon-knobs/dist/vue'
14+
15+
export default {
16+
data () {
17+
return {
18+
minSize: number('minimum diameter', 50),
19+
grid: [number('gird width', 0), number('gird height', 0)],
20+
inertia: boolean('enable inertia', true),
21+
multipleSelect: boolean('enable multiple select', false)
22+
}
23+
},
24+
methods: {
25+
moved (element) {
26+
const nextRect = element.next()
27+
if (nextRect)
28+
nextRect.animate().move(element.x(), element.y())
29+
},
30+
31+
resized (element) {
32+
const nextRect = element.next()
33+
if (nextRect)
34+
nextRect.size(element.width(), element.height())
35+
}
36+
},
37+
components: {
38+
annotations: Annotator
39+
}
40+
}
41+
</script>
42+
43+
<style scoped>
44+
svg {
45+
background: whitesmoke;
46+
}
47+
48+
#annotation {
49+
width: 100%;
50+
height: 100%;
51+
}
52+
53+
.stroke {
54+
stroke: limegreen;
55+
stroke-width: 3px;
56+
}
57+
58+
.stroke:hover {
59+
stroke: orange;
60+
stroke-width: 3px;
61+
}
62+
</style>

0 commit comments

Comments
 (0)