Move the InteractiveImage logic away from the component
This is a suggested refactoring from !654 (merged). Some very strange issues occurred where a missing return
statement caused unexpected updates if the user selected another element while a PATCH
was in progress. In "classic" JavaScript code, we could have written tests; but this is a Vue component, so this would require tools like Puppeteer or Selenium.
But nothing really stops us from writing pure JavaScript code and just linking it in the Vue component. We could use a store module as we do for a large amount of things, but this would cause a dramatic increase in CPU and memory usage and slow the whole frontend down as moving the mouse by a single pixel would trigger a large amount of actions and mutations. Writing simple functions would not allow mutating some local state (the component's data
), making event handling much harder, so we can just use an ES6 class:
// js/annotator.js
export default class ImageAnnotator {
...
}
<template>
<ImageLayer
ref="svgImage"
v-if="image"
:scale.sync="annotator.scale"
:position.sync="annotator.viewPosition"
:image="zone.image"
:image-coords="annotator.imageCoords"
:element-coords="annotator.elementCoords"
:class="{ 'dragCursor': annotator.isDragged }"
v-on:wheel="annotator.mouseAction"
v-on:click="annotator.mouseAction"
v-on:dblclick="annotator.mouseAction"
v-on:mousedown="annotator.mouseAction"
v-on:mouseup="annotator.mouseAction"
v-on:mousemove="annotator.mouseAction"
>
...
</ImageLayer>
</template>
<script>
import ImageAnnotator from '~/js/annotator'
export default {
props: {
elementId: {...},
zone: {...}
},
data: () => ({
annotator: null
}),
watch: {
elementId: {
handler: 'initClass',
immediate: true
},
zone: {
handler: 'initClass',
immediate: true
}
},
methods: {
initClass () {
if (this.annotator && this.annotator.elementId === this.elementId && this.annotator.zone === this.zone) return
this.annotator = new ImageAnnotator(this)
}
}
}
</script>
The component just turns into boilerplate code that connects the ImageAnnotator
class to HTML. With proper declarations in the constructor
of ImageAnnotator
, Vue should be able to set up proper reactivity and everything should work as usual.
The point of moving everything to this class is to make it possible to write a test like this:
import store from '~/test/store'
describe('ImageAnnotator', async () => {
it('updates the selected element', () => {
const instance = { $store: store }
const annotator = new ImageAnnotator(instance)
const element = {
id: 'elementid',
zone: { polygon: [[0, 0], [0, 100], [100, 100], [100, 0], [0, 0]] }
}
store.selectElement(element)
store.setHovered(element)
annotator.editionMode = 'select'
annotator.mouseAction(new MouseEvent('mousedown', { clientX: 42, clientY: 42 }))
annotator.mouseAction(new MouseEvent('mousemove', { clientX: 43, clientY: 43 }))
await store.actionsCompleted()
assert.deepStrictEqual(store.history, [
{ mutation: 'elementsv2/selectElement', payload: element },
{ mutation: 'elementsv2/setHovered', payload: element },
{ mutation: 'elementsv2/updateElement', payload: { id: element.id, polygon: ... } },
...
])
})
})