Introduce display of all annotations at the same time.
authorAlexandre Segura <mex.zktk@gmail.com>
Mon, 13 Mar 2017 18:48:35 +0100
changeset 418 a04c55054afe
parent 417 3a2a2a798bf4
child 419 c3fe3c5d5cab
Introduce display of all annotations at the same time. - Add stats to serialized annotation. - Introduce AnnotationList component. - Pass array of annotations to component. - Handle click on annotation & drawing.
src/iconolab/serializers.py
src/iconolab/templates/iconolab/detail_image.html
src_js/iconolab-bundle/src/components/editor/AnnotationForm.vue
src_js/iconolab-bundle/src/components/editor/AnnotationList.vue
src_js/iconolab-bundle/src/components/editor/Canvas.vue
src_js/iconolab-bundle/src/components/editor/ShapeFree.vue
src_js/iconolab-bundle/src/components/editor/ShapeRect.vue
src_js/iconolab-bundle/src/components/editor/index.js
src_js/iconolab-bundle/src/main.js
src_js/iconolab-bundle/webpack.config.js
--- a/src/iconolab/serializers.py	Thu Mar 09 12:39:36 2017 +0100
+++ b/src/iconolab/serializers.py	Mon Mar 13 18:48:35 2017 +0100
@@ -1,9 +1,15 @@
-from iconolab.models import AnnotationRevision, IconolabComment
+from iconolab.models import AnnotationRevision, IconolabComment, AnnotationStats
 from rest_framework import serializers
 
+class AnnotationStatsSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = AnnotationStats
+        fields = ('submitted_revisions_count', 'awaiting_revisions_count', 'accepted_revisions_count', 'contributors_count', 'views_count', 'comments_count', 'tag_count')
+
 class AnnotationRevisionSerializer(serializers.ModelSerializer):
     tags = serializers.SerializerMethodField('get_normalized_tags')
     annotation_guid = serializers.SerializerMethodField()
+    stats = AnnotationStatsSerializer(source='annotation.stats')
 
     def get_normalized_tags(self, obj):
         tags = []
@@ -21,8 +27,7 @@
 
     class Meta:
         model = AnnotationRevision
-        fields = ('annotation_guid', 'title', 'description', 'fragment', 'tags')
-
+        fields = ('annotation_guid', 'title', 'description', 'fragment', 'tags', 'stats')
 
 class IconolabCommentSerializer(serializers.ModelSerializer):
     allow_thread = serializers.BooleanField()
--- a/src/iconolab/templates/iconolab/detail_image.html	Thu Mar 09 12:39:36 2017 +0100
+++ b/src/iconolab/templates/iconolab/detail_image.html	Mon Mar 13 18:48:35 2017 +0100
@@ -10,25 +10,11 @@
   <div class="annotation-navigator-list">
     <div class="panel panel-default">
       {% if image.annotations.exists %}
-        <div class="list-group">
-        {% for annotation in image.latest_annotations %}
-          <a href="#{{ annotation.annotation_guid }}" class="list-group-item"
-            data-annotation-id="{{ annotation.annotation_guid }}"
-            data-revision-id="{{ annotation.current_revision.revision_guid }}">
-            <h3 class="small list-group-item-heading">
-              {{ annotation.current_revision.title }}
-              <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-            </h3>
-            {% for tagging_info in annotation.current_revision.tagginginfo_set.all %}
-            <p class="list-group-item-text">{{ tagging_info.tag.label }}</p>
-            {% endfor %}
-            <div class="list-group-item-footer">
-              <i class="fa fa-comment"></i>
-              {{ annotation.stats.comments_count }} commentaire{% if annotation.stats.comments_count > 1 %}s{% endif %}
-            </div>
-          </a>
-        {% endfor %}
-        </div>
+        <annotation-list
+          v-bind:annotations="annotations"
+          v-bind:annotation="annotation"
+          v-on:click:annotation="onAnnotationClick($event)"
+          v-on:close:annotation="onAnnotationClose"><annotation-list>
       {% else %}
         <div class="panel-body">
           <div class="alert alert-warning text-center">Pas d'annotation pour cette image</div>
@@ -41,7 +27,10 @@
       ref="annotator"
       image="{% with image.media as img %}{{ img.url }}{% endwith %}"
       thumbnail="{% thumbnail image.media '100x100' crop=False as thumb %}{{ thumb.url }}{% endthumbnail %}"
-      v-bind:is-authenticated="isAuthenticated"></image-annotator>
+      v-bind:is-authenticated="isAuthenticated"
+      v-bind:annotations="annotations"
+      v-on:click:annotation="onAnnotationClick($event)"
+      v-on:close:annotation="onAnnotationClose"></image-annotator>
     <form id="form-annotation" action="{% url 'annotation_create' collection_name image.image_guid %}" method="POST">
       {% csrf_token %}
       <input type="hidden" name="{{ form.title.name }}">
@@ -61,7 +50,7 @@
         <div class="annotation-comment-box" id="form-comment">
           <comment-form v-if="annotation"></comment-form>
         </div>
-        <div style="display: none;" class="annotation-comment-list">
+        <div v-show="annotation" class="annotation-comment-list">
           <label class="small text-muted">Commentaires</label>
           <comment-list v-bind:annotation="annotation"
             fetch="{% url 'get_annotation_comments_json' ':annotation_guid' %}"></comment-list>
@@ -112,82 +101,38 @@
     }
   });
 
+  var annotations = [];
+  {% if image.annotations.exists %}
+    {% for annotation in image.latest_annotations %}
+  annotations.push({{ annotation.current_revision|json|safe }});
+    {% endfor %}
+  {% endif %}
+
   var vm = new Vue({
     el: '.annotation-navigator',
     data: function() {
       return {
-        annotation: null,
-        isAuthenticated: isAuthenticated
+        annotations: annotations,
+        annotation: getAnnotationFromHash(),
+        isAuthenticated: isAuthenticated,
       };
+    },
+    methods: {
+      onAnnotationClick: function(annotation) {
+        var self = this;
+        getCommentForm(annotation).then(function(template) {
+          updateCommentFormComponent(template);
+          self.annotation = annotation;
+          location.hash = '#' + annotation.annotation_guid;
+        });
+      },
+      onAnnotationClose: function() {
+        this.annotation = null;
+        location.hash = '';
+      }
     }
   });
 
-  var annotations = {};
-  {% if image.annotations %}
-    {% for annotation in image.annotations.all %}
-  annotations["{{ annotation.current_revision.revision_guid }}"] = {{ annotation.current_revision|json|safe }}
-    {% endfor %}
-  {% endif %}
-
-  function displayAnnotation($el) {
-
-    var annotation = $el.data('annotation-id');
-    var revision = $el.data('revision-id');
-
-    // Load the form for selected annotation via AJAX
-    $.get(getCommentFormURL.replace(':annotation_guid', annotation), { next: currentPath + '#' + annotation })
-      .then(function(form) {
-
-        Vue.component('comment-form', function (resolve, reject) {
-          resolve({
-            props: ['reply-to'],
-            template: form,
-            data: function() {
-              return {
-                active: null
-              }
-            },
-            watch: {
-              active: function(active) {
-                if (active) {
-                  setTimeout(() => $(this.$el).find('[name="comment"]').focus(), 200);
-                }
-              }
-            },
-            computed: {
-              btnGroupClass: function() {
-                var classes = [
-                  { active: this.active || false }
-                ];
-
-                if (this.active) {
-                  classes.push('btn-group-' + this.active)
-                }
-
-                return classes;
-              }
-            }
-          });
-        });
-
-        $('.list-group a[data-annotation-id]').removeClass('active');
-        $el.addClass('active');
-
-        $('.annotation-comment-list').show();
-
-        vm.annotation = annotations[revision];
-
-        location.hash = '#' + annotation;
-
-      });
-  }
-
-  $('.list-group a[data-annotation-id]').on('click', function(e) {
-    e.preventDefault();
-    var $el = $(e.currentTarget);
-    displayAnnotation($el);
-  });
-
   vm.$refs.annotator.$on('save', function(data) {
     $('#form-annotation').find('input[name="title"]').val(data.title);
     $('#form-annotation').find('input[name="description"]').val(data.description);
@@ -205,39 +150,55 @@
     $('#form-annotation').submit();
   })
 
-  $('.list-group a[data-annotation-id] .close').on('click', function(e) {
-    e.preventDefault();
-    e.stopPropagation();
-
-    var $target = $(e.currentTarget);
-    $target.closest('.list-group-item').removeClass('active');
-
-    vm.annotation = null;
+  function updateCommentFormComponent(template) {
+    Vue.component('comment-form', function (resolve, reject) {
+      resolve({
+        props: ['reply-to'],
+        template: template,
+        data: function() {
+          return {
+            active: null
+          }
+        },
+        watch: {
+          active: function(active) {
+            if (active) {
+              setTimeout(() => $(this.$el).find('[name="comment"]').focus(), 200);
+            }
+          }
+        },
+        computed: {
+          btnGroupClass: function() {
+            var classes = [
+              { active: this.active || false }
+            ];
 
-    $('.annotation-comment-list').hide();
-
-    location.hash = '';
-  });
+            if (this.active) {
+              classes.push('btn-group-' + this.active)
+            }
 
-  function scrollTo($container, $el) {
-    $container.animate({
-      scrollTop:  $container.scrollTop() - $container.offset().top + $el.offset().top
-    }, 600);
+            return classes;
+          }
+        }
+      });
+    });
   }
 
-  function router (scroll) {
+  function getCommentForm(annotation) {
+    return $.get(getCommentFormURL.replace(':annotation_guid', annotation.annotation_guid), {
+      next: currentPath + '#' + annotation.annotation_guid
+    })
+  }
+
+  function getAnnotationFromHash() {
     var url = location.hash.slice(1);
     if (url.length) {
-      var annotation = url;
-      var $el = $('.list-group a[data-annotation-id="'+annotation+'"]');
-      displayAnnotation($el);
-      if (scroll) {
-        scrollTo($el.closest('.panel'), $el); 
-      }
+      var annotation_guid = url;
+      return _.find(annotations, function(annotation) {
+        return annotation.annotation_guid === annotation_guid;
+      });
     }
   }
-  window.addEventListener('hashchange', function() { router(true) });
-  window.addEventListener('load', function() { router(true) });
 
 </script>
 {% endblock %}
--- a/src_js/iconolab-bundle/src/components/editor/AnnotationForm.vue	Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/AnnotationForm.vue	Mon Mar 13 18:48:35 2017 +0100
@@ -76,17 +76,15 @@
         data() {
             return defaults;
         },
+        mounted() {
+            if (this.annotation) {
+                this.loadAnnotation(this.annotation);
+            }
+        },
         watch: {
             annotation: function(annotation) {
                 if (annotation) {
-                    // Make sure we have an actual copy
-                    Object.assign(this, {
-                        title: annotation.title,
-                        description: annotation.description,
-                        fragment: annotation.fragment,
-                        tags: annotation.tags.slice(),
-                        readonly: true,
-                    });
+                    this.loadAnnotation(annotation);
                 } else {
                     this.reset();
                 }
@@ -128,6 +126,16 @@
             reset: function() {
                 Object.assign(this, defaults);
             },
+            loadAnnotation(annotation) {
+                // Make sure we have an actual copy
+                Object.assign(this, {
+                    title: annotation.title,
+                    description: annotation.description,
+                    fragment: annotation.fragment,
+                    tags: annotation.tags.slice(),
+                    readonly: true,
+                });
+            }
         }
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/AnnotationList.vue	Mon Mar 13 18:48:35 2017 +0100
@@ -0,0 +1,79 @@
+<template>
+    <div class="list-group">
+        <a v-for="annotation in annotations"
+            ref="annotations"
+            @click="onAnnotationClick($event, annotation)"
+            :href="'#' + annotation.annotation_guid"
+            class="list-group-item"
+            v-bind:class="{ active: isActive(annotation) }">
+            <h3 class="small list-group-item-heading">
+                {{ annotation.title }}
+                <button type="button" class="close" data-dismiss="alert" aria-label="Close" @click="onAnnotationClose">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </h3>
+            <p class="list-group-item-text" v-for="tag in annotation.tags">{{ tag.tag_label }}</p>
+            <div class="list-group-item-footer">
+              <i class="fa fa-comment"></i> {{ getCommentsCount(annotation) }}
+            </div>
+        </a>
+    </div>
+</template>
+
+<script>
+
+    import _ from 'lodash';
+
+    export default {
+        props: [
+            'annotations',
+            'annotation'
+        ],
+        mounted() {
+            if (this.annotation) {
+                this.slideToAnnotation();
+            }
+        },
+        watch: {
+            annotation: function(annotation) {
+                if (annotation) {
+                    this.slideToAnnotation();
+                }
+            }
+        },
+        methods: {
+            onAnnotationClick: function(e, annotation) {
+                e.preventDefault();
+                this.$emit('click:annotation', annotation);
+            },
+            onAnnotationClose: function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.$emit('close:annotation');
+            },
+            isActive: function(annotation) {
+                return annotation === this.annotation;
+            },
+            getCommentsCount: function(annotation) {
+                var commentsCount = annotation.stats.comments_count;
+                return commentsCount + ' commentaire' + (commentsCount > 1 ? 's' : '');
+            },
+            slideToAnnotation: function() {
+                var el = _.find(this.$refs.annotations, (el) => {
+                    return $(el).attr('href') === ('#' + this.annotation.annotation_guid);
+                });
+                if (el) {
+                    var $container = $(this.$el.closest('.panel'));
+                    $container.animate({
+                        scrollTop: $container.scrollTop() - $container.offset().top + $(el).offset().top
+                    }, 600);
+                }
+            }
+        }
+    }
+
+</script>
+
+<style scoped>
+
+</style>
--- a/src_js/iconolab-bundle/src/components/editor/Canvas.vue	Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/Canvas.vue	Mon Mar 13 18:48:35 2017 +0100
@@ -12,14 +12,40 @@
                     x="0" y="0"
                     v-bind:width="imageWidth"
                     v-bind:height="imageHeight" />
-                <shape-rect ref="rect" v-show="loaded &amp;&amp; mode == 'rect'"
+
+                <!-- These are the existing fragments -->
+
+                <!-- FIXME using <component :is="..."> does not work -->
+                <shape-rect
+                    v-for="annotation in normalizedAnnotations"
+                    :key="annotation.annotation_guid"
+                    v-if="loaded &amp;&amp; readonly &amp;&amp; annotation.mode == 'rect'"
                     v-bind:paper="paper"
                     v-bind:original-annotation="annotation"
-                    v-bind:readonly="readonly"></shape-rect>
-                <shape-free ref="free" v-show="loaded &amp;&amp; mode == 'free'"
+                    v-bind:original-path="annotation.path"
+                    v-bind:readonly="readonly"
+                    v-on:click="onAnnotationClick(annotation)"></shape-rect>
+                <shape-free
+                    v-for="annotation in normalizedAnnotations"
+                    :key="annotation.annotation_guid"
+                    v-if="loaded &amp;&amp; readonly &amp;&amp; annotation.mode == 'free'"
                     v-bind:paper="paper"
                     v-bind:original-annotation="annotation"
-                    v-bind:readonly="readonly"></shape-free>
+                    v-bind:original-path="annotation.path"
+                    v-bind:readonly="readonly"
+                    v-on:click="onAnnotationClick(annotation)"></shape-free>
+
+                <!-- These are the new fragments -->
+
+                <shape-rect ref="rect"
+                    v-show="loaded &amp;&amp; !readonly &amp;&amp; mode == 'rect'"
+                    v-bind:paper="paper"
+                    :readonly="false"></shape-rect>
+                <shape-free ref="free"
+                    v-show="loaded &amp;&amp; !readonly &amp;&amp; mode == 'free'"
+                    v-bind:paper="paper"
+                    :readonly="false"></shape-free>
+
                 <defs>
                     <filter id="shadow" width="200%" height="200%">
                         <feOffset result="offOut" in="SourceAlpha" dx="0" dy="0"/>
@@ -31,6 +57,10 @@
         </div>
         <div class="controls">
             <div class="controls-left">
+                <button @click="readonly = !readonly" type="button" class="btn">
+                    <i v-show="readonly" class="fa fa-pencil"></i>
+                    <i v-show="!readonly" class="fa fa-close"></i>
+                </button>
                 <button @click="setMode('rect')" type="button" class="btn"
                     v-bind:class="{ 'active': mode === 'rect', 'disabled': readonly }">
                     <svg width="14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"><g><rect x="352" y="432" width="64" height="48"/><polygon points="416,352 416,96 176,96 176,160 352,160 352,352 160,352 160,32 96,32 96,96 32,96 32,160 96,160 96,416 480,416 480,352"/></g><text x="0" y="527" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Bluetip Design</text><text x="0" y="532" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>
@@ -72,6 +102,7 @@
     import ShapeRect from './ShapeRect.vue'
     import ShapeFree from './ShapeFree.vue'
     import ZoomThumbnail from './ZoomThumbnail.vue'
+    import _ from 'lodash'
 
     export default {
         props: {
@@ -88,6 +119,10 @@
             isAuthenticated: {
                 type: Boolean,
                 default: false
+            },
+            annotations: {
+                type: Array,
+                default: []
             }
         },
         components: {
@@ -116,17 +151,37 @@
                     'canvas--rect': !this.readonly && this.isAuthenticated && this.mode === 'rect',
                     'canvas--free': !this.readonly && this.isAuthenticated && this.mode === 'free'
                 }
+            },
+            normalizedAnnotations: function() {
+                var normalizedAnnotations = _.map(this.annotations, (annotation) => {
+                    if (annotation.fragment.length > 0) {
+                        var pieces = annotation.fragment.split(';');
+                        var path = this.denormalizePath(pieces[0]);
+                        var mode = pieces[1].toLowerCase();
+
+                        Object.assign(annotation, {
+                            mode: mode,
+                            path: path,
+                        });
+                    }
+
+                    return annotation;
+                });
+
+                if (this.annotation) {
+                    return _.filter(normalizedAnnotations, (annotation) => {
+                        return annotation === this.annotation;
+                    });
+                }
+
+                return normalizedAnnotations;
             }
         },
         watch: {
             mode: function(mode) {
                 this.reset();
-                if (mode === 'free') {
-                    this.handleDrawFree();
-                }
-                if (mode === 'rect') {
-                    this.handleDrawRect();
-                }
+                if (this.readonly) { return; }
+                this.handleDraw();
             },
             loaded: function(loaded) {
                 if (!loaded) { return; }
@@ -153,23 +208,13 @@
 
                 this.paper.attr({"viewBox": this.viewBox});
 
-                if (this.annotation) {
-                    this.loadAnnotation();
-                } else {
-                    if (this.mode === 'free') {
-                        this.handleDrawFree();
-                    }
-                    if (this.mode === 'rect') {
-                        this.handleDrawRect();
-                    }
+                if (_.size(this.annotations) > 0) {
+                    this.readonly = true;
                 }
+
             },
             annotation: function(annotation) {
-                this.reset();
                 this.readonly = !!annotation || !this.isAuthenticated;
-                if (this.annotation) {
-                    this.loadAnnotation();
-                }
             },
             scale: function(scale) {
                 var factor = 0;
@@ -195,6 +240,16 @@
                     this.hideTooltip();
                     this.animateViewBox(viewBox, () => this.showTooltip());
                 }
+            },
+            readonly: function(readonly, previous) {
+                this.reset();
+
+                if (!readonly) {
+                    this.handleDraw();
+                    if (this.annotation) {
+                        this.$emit('close:annotation');
+                    }
+                }
             }
         },
         mounted() {
@@ -219,13 +274,12 @@
 
         },
         methods: {
+            onAnnotationClick: function(annotation) {
+                this.$emit('click:annotation', annotation);
+            },
             hideTooltip: function() {
-                if (this.mode === 'free') {
-                    this.$refs.free.hideTooltip();
-                }
-                if (this.mode === 'rect') {
-                    this.$refs.rect.hideTooltip();
-                }
+                this.$refs.free.hideTooltip();
+                this.$refs.rect.hideTooltip();
             },
             showTooltip: function() {
                 if (this.mode === 'free') {
@@ -236,20 +290,13 @@
                 }
             },
             reset: function() {
-                // Clear shapes
+
                 this.$refs.rect.clear();
                 this.$refs.free.clear();
 
                 this.removeEventHandlers();
                 this.resetZoom();
                 this.resetViewBox();
-
-                if (this.mode === 'free') {
-                    this.handleDrawFree();
-                }
-                if (this.mode === 'rect') {
-                    this.handleDrawRect();
-                }
             },
 
             removeEventHandlers: function() {
@@ -259,26 +306,6 @@
                 this.paper.unclick();
             },
 
-            loadAnnotation: function() {
-                if (this.annotation.fragment.length > 0) {
-                    var pieces = this.annotation.fragment.split(';');
-                    var path = pieces[0];
-                    var mode = pieces[1].toLowerCase();
-
-                    this.mode = mode;
-
-                    this.$nextTick(() => {
-                        path = this.denormalizePath(path);
-                        if (mode === 'free') {
-                            this.$refs.free.fromSVGPath(path, this.tooltip);
-                        }
-                        if (mode === 'rect') {
-                            this.$refs.rect.fromSVGPath(path, this.tooltip);
-                        }
-                    });
-                }
-            },
-
             setMode: function(mode) {
                 if (this.readonly) { return; }
 
@@ -403,6 +430,15 @@
                 }
             },
 
+            handleDraw: function() {
+                if (this.mode === 'free') {
+                    this.handleDrawFree();
+                }
+                if (this.mode === 'rect') {
+                    this.handleDrawRect();
+                }
+            },
+
             handleDrawFree: function() {
 
                 if (!this.isAuthenticated) { return; }
--- a/src_js/iconolab-bundle/src/components/editor/ShapeFree.vue	Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/ShapeFree.vue	Mon Mar 13 18:48:35 2017 +0100
@@ -1,5 +1,5 @@
 <template>
-    <g filter="url(#shadow)">
+    <g ref="g" filter="url(#shadow)">
         <path ref="path" v-bind:d="path"
             class="path"
             v-bind:stroke-width="handlerRadius / 2"
@@ -29,6 +29,7 @@
             'paper',
             'original-annotation',
             'readonly',
+            'original-path'
         ],
         data() {
             return {
@@ -39,7 +40,11 @@
             }
         },
         mounted() {
-
+            if (this.originalPath) {
+                this.fromSVGPath(this.originalPath, false);
+                var g = new Snap(this.$refs.g);
+                g.click(() => this.$emit('click'));
+            }
         },
         watch: {
             closed: function(closed) {
@@ -195,8 +200,12 @@
 }
 .path {
     stroke: #000;
-    fill: #fff;
-    opacity: 0.6;
+    fill: transparent;
+}
+.path:hover {
+    cursor: pointer;
+    fill: #333;
+    opacity: 0.5;
 }
 .handler--first {
     fill: yellow;
--- a/src_js/iconolab-bundle/src/components/editor/ShapeRect.vue	Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/ShapeRect.vue	Mon Mar 13 18:48:35 2017 +0100
@@ -6,7 +6,8 @@
         v-bind:width="width" v-bind:height="height"
         v-bind:stroke-width="handlerSize / 5"
         class="shape"
-        v-bind:class="{ 'shape--draggable': !readonly }"></rect>
+        v-bind:class="{ 'shape--draggable': !readonly }"
+        v-bind:style="{ 'outline-width': (handlerSize / 5) + 'px' }"></rect>
     <rect
         ref="topLeft"
         v-show="showResizeHandlers"
@@ -37,6 +38,7 @@
             'original-annotation',
             'tooltip',
             'readonly',
+            'original-path',
         ],
         data() {
             return {
@@ -79,6 +81,11 @@
 
             var g = new Snap(this.$refs.g);
             g.drag(groupEvents.onMove, groupEvents.onStart, groupEvents.onEnd);
+
+            if (this.originalPath) {
+                this.fromSVGPath(this.originalPath, false);
+                g.click(() => this.$emit('click'))
+            }
         },
         watch: {
             x: function (val, oldVal) {
@@ -208,8 +215,15 @@
 <style scoped>
 .shape {
     fill: transparent;
-    stroke: #000;
-    opacity: 0.6;
+    stroke: #fff;
+    outline-color: #000;
+    outline-style: solid;
+}
+.shape:hover,
+.shape.active {
+    cursor: pointer;
+    fill: #333;
+    opacity: 0.5;
 }
 .shape--draggable:hover {
     cursor: move;
--- a/src_js/iconolab-bundle/src/components/editor/index.js	Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/index.js	Mon Mar 13 18:48:35 2017 +0100
@@ -1,9 +1,11 @@
 import Canvas from './Canvas.vue'
 import AnnotationForm from './AnnotationForm.vue'
+import AnnotationList from './AnnotationList.vue'
 import CommentList from './CommentList.vue'
 
 export default {
     Canvas: Canvas,
     AnnotationForm: AnnotationForm,
+    AnnotationList: AnnotationList,
     CommentList: CommentList,
 }
--- a/src_js/iconolab-bundle/src/main.js	Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/main.js	Mon Mar 13 18:48:35 2017 +0100
@@ -34,8 +34,8 @@
 
 Vue.component('color-buttons', ColorButtons);
 Vue.component('image-annotator', Editor.Canvas);
-Vue.component('annotation', Editor.Annotation);
 Vue.component('annotation-form', Editor.AnnotationForm);
+Vue.component('annotation-list', Editor.AnnotationList);
 Vue.component('comment-list', Editor.CommentList);
 
 if (!window.iconolab) {
--- a/src_js/iconolab-bundle/webpack.config.js	Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/webpack.config.js	Mon Mar 13 18:48:35 2017 +0100
@@ -77,7 +77,8 @@
     new ExtractTextPlugin("iconolab/css/[name].css"),
     new webpack.ProvidePlugin({
       $: "jquery",
-      jQuery: "jquery"
+      jQuery: "jquery",
+      _: "lodash"
     })
   ],
   devServer: {