Introduce refactored components using Vue.js.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/editor.html Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Iconolab</title>
+ <link rel="stylesheet" href="/static/iconolab/css/iconolab.css">
+ <style>
+ body {
+ margin-top: 40px;
+ }
+ </style>
+ </head>
+ <body>
+
+ <div class="container-fluid">
+ <div class="row">
+ <div class="col-md-9">
+ <div id="wrapper">
+ <image-annotator ref="annotator" image="img/main-image.jpg"></image-annotator>
+ </div>
+ </div>
+ <div class="col-md-3">
+ <div class="alert alert-success" style="display: none;">Annotation saved in LocalStorage!</div>
+ <pre id="annotation"></pre>
+ </div>
+ </div>
+ </div>
+
+ <script src="/static/iconolab/js/vendor.js"></script>
+ <script src="/static/iconolab/js/iconolab.js"></script>
+ <script>
+
+ function showAlert() {
+ $('.alert-success').show();
+ setTimeout(function() {
+ $('.alert-success').hide();
+ }, 3000);
+ }
+
+ var vm = new Vue({
+ el: '#wrapper'
+ });
+
+ var data = localStorage.getItem('annotation');
+ if (data) {
+ var annotation = JSON.parse(data);
+ vm.$refs.annotator.setAnnotation(annotation);
+ $('#annotation').text(JSON.stringify(annotation, null, 2));
+ }
+
+ vm.$refs.annotator.$on('save', function(data) {
+ console.log('Saving in localStorage', JSON.stringify(data));
+ showAlert();
+ localStorage.setItem('annotation', JSON.stringify(data));
+ });
+
+ </script>
+ </body>
+</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/Annotation.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,27 @@
+<script>
+
+ export default {
+ props: [
+ 'title',
+ 'description',
+ 'fragment',
+ 'tags'
+ ],
+ mounted() {
+
+ var tags = [];
+ if (this.tags) {
+ tags = JSON.parse(this.tags);
+ }
+
+ this.$parent.setAnnotation({
+ title: this.title,
+ description: this.description,
+ fragment: this.fragment,
+ tags: tags
+ });
+ },
+ render: function(createElement) {}
+ }
+
+</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/Canvas.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,452 @@
+<template>
+ <div class="zoom">
+ <div>
+ <svg ref="svg"
+ v-bind:class="{ 'cut-canvas': true, 'canvas--rect': mode === 'rect', 'canvas--free': mode === 'free' }"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <image ref="image" v-if="loaded" xmlns:xlink="http://www.w3.org/1999/xlink" v-bind:xlink:href="image" x="0" y="0" />
+ <!-- This slot may contain annotation data -->
+ <slot></slot>
+ <shape-rect ref="rect" v-show="loaded && mode == 'rect'"
+ v-bind:paper="paper" v-bind:original-annotation="annotation"></shape-rect>
+ <shape-free ref="free" v-show="loaded && mode == 'free'"
+ v-bind:paper="paper" v-bind:original-annotation="annotation"></shape-free>
+ </svg>
+ </div>
+ <div class="zoomer">
+ <div class="btn-group-vertical" role="group" aria-label="...">
+ <button @click="zoomIn" type="button" class="btn btn-default"><i class="fa fa-plus" aria-hidden="true"></i></button>
+ <button @click="zoomOut" type="button" class="btn btn-default"><i class="fa fa-minus" aria-hidden="true"></i></button>
+ </div>
+ </div>
+ <div class="mode-controls">
+ <div class="btn-group" role="group" aria-label="...">
+ <button @click="mode = 'rect'" type="button" v-bind:class="{ btn: true, 'btn-default': true, 'btn-primary': mode === 'rect'}">
+ <svg width="24" 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>
+ </button>
+ <button @click="mode = 'free'" type="button" v-bind:class="{ btn: true, 'btn-default': true, 'btn-primary': mode === 'free'}">
+ <svg width="24" 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 30 30" xml:space="preserve"><g transform="translate(-450 -380)"><g xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M453,395c0,2.209,1.79,4,4,4c1.307,0,2.455-0.635,3.186-1.604l7.121,4.069C467.11,401.938,467,402.456,467,403 c0,2.209,1.79,4,4,4c2.209,0,4-1.791,4-4s-1.791-4-4-4c-1.307,0-2.455,0.635-3.186,1.604l-7.121-4.069 c0.196-0.473,0.307-0.99,0.307-1.534s-0.11-1.062-0.307-1.534l7.121-4.069c0.73,0.969,1.879,1.604,3.186,1.604 c2.209,0,4-1.791,4-4s-1.791-4-4-4c-2.21,0-4,1.791-4,4c0,0.544,0.11,1.062,0.307,1.534l-7.121,4.069 c-0.73-0.969-1.879-1.604-3.186-1.604C454.79,391,453,392.791,453,395z M471,400c1.654,0,3,1.346,3,3s-1.346,3-3,3s-3-1.346-3-3 S469.346,400,471,400z M471,384c1.654,0,3,1.346,3,3s-1.346,3-3,3s-3-1.346-3-3S469.346,384,471,384z M460,395 c0,1.654-1.346,3-3,3s-3-1.346-3-3s1.346-3,3-3S460,393.346,460,395z"/></g></g><text x="0" y="45" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Hea Poh Lin</text><text x="0" y="50" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>
+ </button>
+ </path>
+ </div>
+ </div>
+</template>
+
+<script>
+
+ import Snap from 'snapsvg'
+ import ShapeRect from './ShapeRect.vue'
+ import ShapeFree from './ShapeFree.vue'
+
+ export default {
+ props: [
+ 'image',
+ ],
+ components: {
+ shapeRect: ShapeRect,
+ shapeFree: ShapeFree
+ },
+ data() {
+ return {
+ paper: null,
+ loaded: false,
+ mode: 'rect',
+ viewport: { width: 0, height: 0 },
+ viewBox: [0 , 0, 0, 0],
+ zoomFactor: 0.1,
+ scale: 1,
+ imgMinSize: 0,
+ imageWidth: 0,
+ imageHeight: 0,
+ annotation: null
+ }
+ },
+ watch: {
+ mode: function(mode) {
+ this.reset();
+ if (mode === 'free') {
+ this.handleDrawFree();
+ }
+ if (mode === 'rect') {
+ this.handleDrawRect();
+ }
+ },
+ loaded: function(loaded) {
+ if (!loaded) { return; }
+
+ if (this.annotation) {
+
+ 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);
+ }
+ if (mode === 'rect') {
+ this.$refs.rect.fromSVGPath(path);
+ }
+ });
+
+ } else {
+
+ if (this.mode === 'free') {
+ this.handleDrawFree();
+ }
+ if (this.mode === 'rect') {
+ this.handleDrawRect();
+ }
+ }
+ }
+ },
+ mounted() {
+
+ var img = new Image();
+ img.onload = (e) => {
+
+ this.paper = new Snap(this.$refs.svg);
+
+ this.imgMinSize = Math.min(img.width, img.height);
+
+ // FIXME
+ // Image is actually NOT loaded at this step
+ setTimeout(() => {
+
+ console.log('Viewport: %s x %s',
+ this.paper.node.clientWidth, this.paper.node.clientHeight);
+
+ var viewBox = [0 , 0, img.width, img.height];
+ var viewport = {
+ width: this.paper.node.clientWidth,
+ height: this.paper.node.clientHeight
+ };
+
+ Object.assign(this, {
+ imageWidth: img.width,
+ imageHeight: img.height,
+ viewBox: viewBox,
+ viewport: viewport,
+ });
+
+ var handlerSize = 15 * Math.min(viewBox[2], viewBox[3]) / viewport.width;
+
+ this.$refs.rect.handlerSize = handlerSize;
+ this.$refs.free.handlerRadius = handlerSize / 2;
+
+ this.paper.attr({"viewBox": this.viewBox});
+
+ this.loaded = true;
+
+ }, 100);
+
+ }
+ img.src = this.image;
+
+ },
+ methods: {
+
+ reset: function() {
+ // Clear shapes
+ this.$refs.rect.clear();
+ this.$refs.free.clear();
+
+ // Remove event handlers
+ this.paper.unmousedown();
+ this.paper.unmousemove();
+ this.paper.unmouseup();
+ this.paper.unclick();
+ },
+
+ setAnnotation: function(annotation) {
+ this.annotation = annotation;
+ },
+
+ getCenter: function() {
+ return {
+ x: this.viewBox[0] + (this.viewBox[2] / 2),
+ y: this.viewBox[1] + (this.viewBox[3] / 2)
+ }
+ },
+
+ resetZoom: function() {
+ this.scale = 1;
+ this.viewBox = [0, 0, this.imageWidth, this.imageHeight];
+ this.paper.attr({ "viewBox": this.viewBox });
+ },
+
+ zoomIn: function() {
+
+ if ( this.scale === 9) { this.scale--; return; }
+
+ var center = this.getCenter();
+ var scaleFactor = this.zoomFactor * this.scale;
+
+ var viewBoxW = this.imgMinSize - (this.imgMinSize * scaleFactor);
+ var viewBoxH = viewBoxW;
+
+ const viewBoxPrev = this.viewBox.slice(0);
+
+ this.viewBox[0] = center.x - viewBoxW / 2;
+ this.viewBox[1] = center.y - viewBoxH / 2;
+ this.viewBox[2] = viewBoxW;
+ this.viewBox[3] = viewBoxH;
+
+ this.scale++;
+
+ // this.paper.attr({ "viewBox": this.viewBox });
+
+ this.$refs.rect.hideTooltip();
+ Snap.animate(
+ viewBoxPrev, this.viewBox,
+ (viewBox) => this.paper.attr({ "viewBox": viewBox }),
+ 300, mina.easeinout,
+ () => this.$refs.rect.showTooltip()
+ );
+ },
+
+ zoomOut: function() {
+
+ if (this.scale === 1) { this.resetZoom(); return; }
+
+ var center = this.getCenter();
+ var scaleFactor = this.zoomFactor * (this.scale - 1);
+
+ var viewBoxW = this.imgMinSize - (this.imgMinSize * scaleFactor);
+ var viewBoxH = viewBoxW;
+
+ var topX = center.x - viewBoxW / 2;
+ var topY = center.y - viewBoxH / 2;
+
+ const viewBoxPrev = this.viewBox.slice(0);
+
+ this.viewBox[0] = topX; //deal with X and Y
+ this.viewBox[1] = topY;
+ this.viewBox[2] = viewBoxW;
+ this.viewBox[3] = viewBoxH;
+
+ this.scale--;
+
+ // this.paper.attr({ "viewBox": this.viewBox });
+
+ this.$refs.rect.hideTooltip();
+ Snap.animate(
+ viewBoxPrev, this.viewBox,
+ (viewBox) => this.paper.attr({ "viewBox": viewBox }),
+ 300, mina.easeinout,
+ () => this.$refs.rect.showTooltip()
+ );
+ },
+
+ zoomOffset: function() {
+ return {
+ x: this.viewport.width / this.viewBox[2],
+ y: this.viewport.height / this.viewBox[3]
+ };
+ },
+
+ computeOffset: function(e) {
+ var rect = this.$refs.image.getBoundingClientRect();
+ var zoomOffset = this.zoomOffset();
+ var offsetX = (e.clientX - rect.left) / Math.min(zoomOffset.x, zoomOffset.y);
+ var offsetY = (e.clientY - rect.top) / Math.min(zoomOffset.x, zoomOffset.y);
+
+ return { x: offsetX, y: offsetY };
+ },
+
+ computeHandlerSize: function(e) {
+ return 60 * Math.min(this.viewBox[2], this.viewBox[3]) / this.imageWidth;
+ },
+
+ normalizePath: function(path) {
+
+ var xRatio = 100 / this.imageWidth;
+ var yRatio = 100 / this.imageHeight;
+
+ if (isNaN(xRatio) || isNaN(yRatio)) {
+ throw new Error('Ratio should be a number.');
+ }
+
+ var normalizeMatrix = Snap.matrix(xRatio, 0, 0, yRatio, 0, 0);
+
+ path = Snap.path.map(path, normalizeMatrix).toString();
+
+ if (path.search(/[z|Z]/gi) === -1) {
+ path += " Z";
+ }
+
+ return path;
+ },
+
+ denormalizePath: function(path) {
+
+ var xRatio = this.imageWidth / 100;
+ var yRatio = this.imageHeight / 100;
+
+ if (isNaN(xRatio) || isNaN(yRatio)) {
+ throw new Error('Ratio should be a number.');
+ }
+
+ var transformMatrix = Snap.matrix(xRatio, 0, 0, yRatio, 0, 0);
+
+ path = Snap.path.map(path, transformMatrix).toString();
+
+ if (path.search(/[z|Z]/gi) === -1) {
+ path += " Z";
+ }
+
+ return path;
+ },
+
+ handleDrawFree: function() {
+
+ var clickTimeout;
+
+ var clickHandler = function (offsetX, offsetY) {
+ clickTimeout = null;
+ this.$refs.free.addPoint(offsetX, offsetY);
+ }
+
+ this.paper.click((e) => {
+ if (clickTimeout) { return; }
+ if (!$(e.target).is('image')) { return; }
+ if (this.$refs.free.closed) { return; }
+
+ var offset = this.computeOffset(e);
+ var offsetX = offset.x;
+ var offsetY = offset.y;
+ clickTimeout = setTimeout(clickHandler.bind(this, offsetX, offsetY), 190);
+ });
+
+ },
+
+ handleDrawRect: function() {
+
+ var startPosition = { x: 0, y: 0 };
+ var currentPosition = { x: 0, y: 0 };
+ var canDraw = false;
+
+ var computeOffset = (e) => {
+ var rect = this.$refs.image.getBoundingClientRect();
+ var zoomOffset = this.zoomOffset();
+ var offsetX = (e.clientX - rect.left) / Math.min(zoomOffset.x, zoomOffset.y);
+ var offsetY = (e.clientY - rect.top) / Math.min(zoomOffset.x, zoomOffset.y);
+
+ return { x: offsetX, y: offsetY };
+ }
+
+ this.paper.mousedown((e) => {
+
+ if (this.$refs.rect.width > 0 && this.$refs.rect.height > 0) { return; }
+
+ startPosition = computeOffset(e);
+ canDraw = true;
+ });
+
+ this.paper.mousemove((e) => {
+
+ if (!canDraw) { return; }
+
+ var x, y;
+ currentPosition = computeOffset(e);
+
+ /* bas -> droite */
+ var width = Math.abs(currentPosition.x - startPosition.x);
+ var height = Math.abs(startPosition.y - currentPosition.y);
+
+ if (currentPosition.y > startPosition.y && currentPosition.x > startPosition.x) {
+ x = startPosition.x;
+ y = startPosition.y;
+ }
+
+ /* haut -> droite */
+ if (currentPosition.y < startPosition.y && currentPosition.x > startPosition.x) {
+ x = currentPosition.x - width;
+ y = currentPosition.y;
+ }
+
+ /* haut -> gauche */
+ if (currentPosition.y < startPosition.y && currentPosition.x < startPosition.x) {
+ x = currentPosition.x;
+ y = currentPosition.y;
+ }
+
+ /* bas -> gauche */
+ if (currentPosition.y > startPosition.y && currentPosition.x < startPosition.x) {
+ x = currentPosition.x
+ y = currentPosition.y - height;
+ }
+
+ if(!x || !y) { return; }
+
+ Object.assign(this.$refs.rect, {
+ x: x,
+ y: y,
+ width: width,
+ height: height,
+ });
+ });
+
+ this.paper.mouseup((e) => {
+
+ if (!canDraw) { return; }
+
+ canDraw = false;
+
+ if (this.$refs.rect.width === 0 && this.$refs.rect.height === 0) {
+ var currentPosition = computeOffset(e);
+ Object.assign(this.$refs.rect, {
+ x: currentPosition.x,
+ y: currentPosition.y,
+ width: this.imgMinSize / 4,
+ height: this.imgMinSize / 4,
+ });
+ }
+
+ this.$nextTick(() => {
+ this.$refs.rect.addResizeHandlers();
+ this.$refs.rect.addTooltip();
+ });
+
+ });
+ }
+ }
+ }
+
+</script>
+
+<style scoped>
+.zoom {
+ position: relative;
+}
+.zoomer {
+ position: absolute;
+ bottom: 30px;
+ right: 30px;
+}
+.cut-canvas {
+ width: 100%;
+ height: 800px;
+}
+.canvas--rect:hover {
+ cursor: crosshair;
+}
+.canvas--free:hover {
+ cursor: pointer;
+}
+.mode-controls {
+ position: absolute;
+ top: 15px;
+ left: 15px;
+}
+.mode-controls .btn > svg {
+ margin-top: 4px;
+}
+.mode-controls .btn-primary > svg {
+ fill: #fff;
+}
+
+</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/ShapeFree.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,192 @@
+<template>
+ <g>
+ <path ref="path" v-bind:d="path" stroke="#000000" fill="#bdc3c7" style="stroke-width: 10; stroke-dasharray: 20, 20; opacity: 0.6;"></path>
+ <circle
+ v-for="(point, key) in points"
+ :key="key"
+ v-bind:data-key="key"
+ ref="handlers"
+ v-bind:cx="point.x"
+ v-bind:cy="point.y"
+ v-bind:r="handlerRadius"
+ stroke="#000000" stroke-width="10"
+ style="opacity: 0.9;"
+ v-bind:class="{ handler: true, 'handler--first': key === 0 && !closed }"></circle>
+ </g>
+</template>
+
+<script>
+
+ import Snap from 'snapsvg'
+ import tooltip from './mixins/tooltip'
+ import save from './mixins/save'
+
+ export default {
+ mixins: [ tooltip, save ],
+ props: [
+ 'paper',
+ 'original-annotation',
+ ],
+ data() {
+ return {
+ path: '',
+ closed: false,
+ points: [],
+ handlerRadius: 30
+ }
+ },
+ mounted() {
+
+ },
+ watch: {
+ closed: function(closed) {
+ if (closed) {
+ this.path += ' Z';
+ setTimeout(() => this.addTooltip(), 50);
+ }
+ },
+ // Redraw the path when the points have changed
+ points: function(points) {
+
+ var path = "M";
+
+ if (points.length <= 1) {
+ return;
+ }
+
+ path += points[0].x + ',' + points[0].y;
+
+ for (var i = 0; i < points.length; i++) {
+ if (i == 0) continue;
+
+ var pointInfos = points[i];
+ var lPath = "L" + pointInfos.x + "," + pointInfos.y;
+ path += " " + lPath;
+ }
+
+ if (this.closed) { path += ' Z'; }
+
+ this.path = path;
+ }
+ },
+ methods: {
+
+ addPoint: function(x, y) {
+
+ this.points.push({ x: x, y: y });
+
+ // Attach events to last point once DOM has been refreshed
+ // @link https://vuejs.org/v2/guide/reactivity.html
+ this.$nextTick(() => {
+ var handler = this.$refs.handlers[this.$refs.handlers.length - 1];
+ this.addResizeHandler(handler);
+ });
+ },
+
+ clear: function() {
+ this.destroyTooltip();
+ Object.assign(this, {
+ points: [],
+ closed: false,
+ path: ''
+ });
+ },
+
+ getTooltipTarget: function() {
+ return this.$refs.path;
+ },
+
+ fromSVGPath: function(pathString) {
+
+ var segments = Snap.parsePathString(pathString);
+ var points = [];
+
+ // Don't use this.addPoint to avoid
+ // race condition when registering events
+ segments.map((segment) => {
+ if (segment[0] !== 'Z') {
+ points.push({ x: segment[1], y: segment[2] });
+ }
+ });
+
+ this.points = points;
+ this.closed = true;
+
+ this.$nextTick(() => {
+ this.addResizeHandlers();
+ });
+ },
+
+ toSVGPath: function() {
+ return this.$parent.normalizePath(this.path) + ';FREE'
+ },
+
+ addResizeHandler: function(handler) {
+
+ var circle = new Snap(handler);
+
+ var isDragged = false;
+
+ circle.click(function(e) {
+ var key = parseInt(this.attr('data-key'), 10);
+ if (key === 0 && self.points.length > 2) {
+ self.closed = true;
+ }
+ });
+
+ // Remove point on double click
+ circle.dblclick((e) => {
+ var circle = new Snap(e.target);
+ var key = parseInt(circle.attr('data-key'), 10);
+ this.points.splice(key, 1);
+ });
+
+ var self = this;
+
+ var dragEvents = {
+ onMove: function(dx, dy, x, y, e) {
+
+ isDragged = true;
+
+ var offset = self.$parent.computeOffset(e);
+ this.attr({ cx: offset.x, cy: offset.y });
+
+ var key = parseInt(this.attr('data-key'), 10);
+
+ // Must use splice for reactivity to work
+ // @see https://vuejs.org/v2/guide/list.html#Mutation-Methods
+ self.points.splice(key, 1, { x: offset.x, y: offset.y });
+ },
+ onStart: () => this.hideTooltip(),
+ onEnd: function(e) {
+ if (!isDragged) { return; }
+
+ isDragged = false;
+ self.showTooltip();
+ }
+ }
+
+ circle.drag(dragEvents.onMove, dragEvents.onStart, dragEvents.onEnd);
+ },
+
+ addResizeHandlers: function() {
+ this.$refs.handlers.forEach((handler) => this.addResizeHandler(handler));
+ }
+ }
+ }
+
+</script>
+
+<style scoped>
+.handler {
+ fill: #fff;
+}
+/*.handler:hover {
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+ cursor: grab;
+}*/
+.handler--first {
+ fill: yellow;
+}
+</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/ShapeRect.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,208 @@
+<template>
+<g ref="g" v-bind:transform="transform">
+ <rect
+ ref="shape"
+ x="0" y="0"
+ v-bind:width="width" v-bind:height="height"
+ fill="#bdc3c7"
+ stroke="#000"
+ v-bind:stroke-width="handlerSize / 5"
+ v-bind:stroke-dasharray="(handlerSize / 5) + ',' + (handlerSize / 5)"
+ style="opacity: 0.6;"
+ class="shape"></rect>
+ <rect
+ ref="topLeft"
+ v-show="width > 0 && height > 0"
+ v-bind:x="(handlerSize / 2) * -1" v-bind:y="(handlerSize / 2) * -1"
+ v-bind:width="handlerSize" v-bind:height="handlerSize"
+ fill="#ffffff"
+ stroke="#000000" v-bind:stroke-width="handlerSize / 5" class="handler-rect handler-top-left"></rect>
+ <rect
+ ref="bottomRight"
+ v-show="width > 0 && height > 0"
+ v-bind:x="width - (handlerSize / 2)" v-bind:y="height - (handlerSize / 2)"
+ v-bind:width="handlerSize" v-bind:height="handlerSize"
+ fill="#ffffff"
+ stroke="#000000" v-bind:stroke-width="handlerSize / 5" class="handler-rect handler-bottom-right"></rect>
+</g>
+</template>
+
+<script>
+
+ import Snap from 'snapsvg'
+ import tooltip from './mixins/tooltip'
+ import save from './mixins/save'
+
+ export default {
+ mixins: [ tooltip, save ],
+ props: [
+ 'paper',
+ 'original-annotation',
+ ],
+ data() {
+ return {
+ transform: 'translate(0, 0)',
+ isResizing: false,
+ x: 0, y: 0,
+ width: 0, height: 0,
+ handlerSize: 60
+ }
+ },
+ mounted() {
+
+ var self = this;
+
+ var groupEvents = {
+ onMove: function(dx, dy) {
+ if (self.isResizing) { return; }
+
+ var snapInvMatrix = this.transform().diffMatrix.invert();
+ var tdx = snapInvMatrix.x(dx, dy);
+ var tdy = snapInvMatrix.y(dx, dy);
+
+ var transformValue = this.data('origTransform') + (this.data('origTransform') ? "T" : "t") + [tdx, tdy];
+ this.transform(transformValue);
+ },
+ onStart: function() {
+ this.data('origTransform', this.transform().local);
+ self.$emit('drag:start');
+ },
+ onEnd: () => self.$emit('drag:end')
+ }
+
+ var g = new Snap(this.$refs.g);
+ g.drag(groupEvents.onMove, groupEvents.onStart, groupEvents.onEnd);
+ },
+ watch: {
+ x: function (val, oldVal) {
+ this.transform = 'translate(' + this.x + ', ' + this.y + ')';
+ },
+ y: function (val, oldVal) {
+ this.transform = 'translate(' + this.x + ', ' + this.y + ')';
+ },
+ },
+ methods: {
+
+ clear: function() {
+
+ var shape = new Snap(this.$refs.shape);
+ var topLeftHandler = new Snap(this.$refs.topLeft);
+ var bottomRightHandler = new Snap(this.$refs.bottomRight);
+
+ shape.node.removeAttribute('transform');
+ topLeftHandler.node.removeAttribute('transform');
+ bottomRightHandler.node.removeAttribute('transform');
+
+ this.destroyTooltip();
+
+ Object.assign(this, {
+ transform: 'translate(0, 0)',
+ x: 0, y: 0,
+ width: 0, height: 0,
+ });
+ },
+
+ getTooltipTarget: function() {
+ return this.$refs.shape;
+ },
+
+ addResizeHandlers: function() {
+
+ var self = this;
+
+ var shape = new Snap(this.$refs.shape);
+ var topLeftHandler = new Snap(this.$refs.topLeft);
+ var bottomRightHandler = new Snap(this.$refs.bottomRight);
+
+ var handlerEvents = {
+ onMove: function(dx, dy) {
+
+ var snapInvMatrix = this.transform().diffMatrix.invert();
+ snapInvMatrix.e = snapInvMatrix.f = 0;
+ var tdx = snapInvMatrix.x(dx, dy);
+ var tdy = snapInvMatrix.y(dx, dy);
+
+ this.transform( "t" + [ tdx, tdy ] + this.data("origTransform") );
+
+ // Update shape
+
+ var newWidth = bottomRightHandler.getBBox().x - topLeftHandler.getBBox().x;
+ var newHeight = bottomRightHandler.getBBox().y - topLeftHandler.getBBox().y;
+
+ var attr = {
+ width: newWidth,
+ height: newHeight
+ };
+ if (this === topLeftHandler) {
+ attr.transform = shape.data('origTransform') + (shape.data('origTransform') ? "T" : "t") + [tdx, tdy];
+ }
+
+ shape.attr(attr);
+ },
+ onStart: function() {
+ self.isResizing = true;
+ shape.data("origTransform", shape.transform().local);
+ this.data('origTransform', this.transform().local);
+ },
+ onEnd: function() {
+ self.isResizing = false;
+ }
+ }
+
+ topLeftHandler.drag(handlerEvents.onMove, handlerEvents.onStart, handlerEvents.onEnd);
+ bottomRightHandler.drag(handlerEvents.onMove, handlerEvents.onStart, handlerEvents.onEnd);
+ },
+
+ fromSVGPath: function(pathString, imageWidth, imageHeight) {
+ var bBox = Snap.path.getBBox(pathString);
+
+ Object.assign(this, {
+ x: bBox.x, y: bBox.y,
+ width: bBox.width, height: bBox.height
+ });
+
+ setTimeout(() => {
+ this.addResizeHandlers();
+ this.addTooltip();
+ }, 50);
+ },
+
+ toSVGPath: function() {
+
+ var shape = new Snap(this.$refs.shape);
+
+ var shapePath;
+ var bBox = shape.getBBox();
+ var transform = shape.transform();
+
+ if (!transform.global.length) {
+ shapePath = shape.getBBox().path;
+ } else {
+ var shapeX = shape.node.getAttribute('x');
+ var shapeY = shape.node.getAttribute('y');
+ var transformMatrix = transform.totalMatrix;
+ var fakeShape = this.paper.rect(transformMatrix.x(shapeX, shapeY),transformMatrix.y(shapeX, shapeY), bBox.width, bBox.height);
+ shapePath = fakeShape.getBBox().path;
+ fakeShape.remove();
+ }
+
+ var path = Snap.path.toAbsolute(shapePath).toString();
+
+ return this.$parent.normalizePath(path) + ';RECT';
+ }
+ }
+ }
+
+</script>
+
+<style scoped>
+.shape:hover {
+ cursor: move;
+}
+.handler-top-left:hover {
+ cursor: nw-resize;
+}
+.handler-bottom-right:hover {
+ cursor: se-resize;
+}
+</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/Tooltip.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,105 @@
+<template>
+
+ <form>
+ <button @click="close" type="button" class="close" data-dismiss="alert" aria-label="Close">
+ <span aria-hidden="true">×</span>
+ </button>
+ <div class="form-group" v-bind:class="titleFormGroup" style="clear: both;">
+ <label class="control-label">Titre</label>
+ <input name="title" v-model="title" type="text" class="form-control input-sm" placeholder="Donnez un titre court">
+ </div>
+ <div class="form-group">
+ <label class="control-label">Description</label>
+ <textarea name="description" v-model="description" class="form-control input-sm" placeholder="Décrivez ce que vous voyez"></textarea>
+ </div>
+ <div class="form-group">
+ <label class="control-label">Mots-clé</label>
+ <tag-list ref="taglist" v-bind:original-tags="originalTags"></tag-list>
+ </div>
+ <button @click="save" class="btn btn-block btn-sm btn-primary">Valider</button>
+ </form>
+
+</template>
+
+<script>
+
+ import TagList from '../tagform/TagList.vue'
+
+ export default {
+ props: [
+ 'original-title',
+ 'original-description',
+ 'original-tags'
+ ],
+ components: {
+ 'tag-list': TagList
+ },
+ data() {
+ return {
+ title: '',
+ description: '',
+ tags: [],
+ error: false
+ }
+ },
+ computed: {
+ titleFormGroup: function() {
+ return {
+ 'has-error': this.error
+ }
+ }
+ },
+ mounted() {
+ if (this.originalTitle) {
+ this.title = this.originalTitle;
+ }
+ if (this.originalDescription) {
+ this.description = this.originalDescription;
+ }
+ this.$on('error', (err) => {
+ if (err.title) {
+ this.error = true;
+ }
+ });
+ },
+ methods: {
+ close: function(e) {
+ e.preventDefault();
+ this.$emit('close');
+ },
+ save: function(e) {
+ e.preventDefault();
+
+ this.error = false;
+ if (this.title.trim().length === 0) {
+ this.$emit('error', {
+ title: true
+ });
+ return;
+ }
+
+ this.$emit('save', {
+ title: this.title,
+ description: this.description,
+ tags: this.$refs.taglist.tags
+ });
+ }
+ }
+ }
+
+</script>
+
+<style>
+.popover {
+ min-width: 300px;
+}
+.popover-content .form-group {
+ margin-bottom: 10px;
+}
+.popover-content .form-group label {
+ font-size: 12px;
+}
+.popover-content .taglist {
+ margin: 10px 0 15px;
+}
+</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/index.js Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,7 @@
+import Canvas from './Canvas.vue'
+import Annotation from './Annotation.vue'
+
+export default {
+ Canvas: Canvas,
+ Annotation: Annotation,
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/mixins/save.js Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,12 @@
+export default {
+ methods: {
+ save: function(data) {
+
+ Object.assign(data, {
+ fragment: this.toSVGPath()
+ });
+
+ this.$parent.$emit('save', data);
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/mixins/tooltip.js Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,80 @@
+import Snap from 'snapsvg'
+import Tooltip from '../Tooltip.vue'
+
+var popoverOptions = {
+ placement: 'auto',
+ container: 'body',
+ trigger: 'manual',
+ html: true,
+ title: '',
+ content: '',
+}
+
+export default {
+ methods: {
+ addTooltip: function() {
+
+ var vm = new Vue(Tooltip);
+
+ if (this.originalAnnotation) {
+ vm.originalTitle = this.originalAnnotation.title;
+ vm.originalDescription = this.originalAnnotation.description;
+ vm.originalTags = this.originalAnnotation.tags;
+ }
+
+ vm.$mount(jQuery('<div>').get(0));
+
+ vm.$on('close', () => {
+ this.clear();
+ });
+ vm.$on('save', (data) => {
+ this.save(data);
+ });
+
+ popoverOptions.content = vm.$el;
+
+ var target = new Snap(this.getTooltipTarget());
+
+ var $el = $(target.node);
+ $el
+ .popover(popoverOptions)
+ .popover('show');
+
+ this.$on('drag:start', function() {
+ $el.popover('hide');
+ });
+ this.$on('drag:end', function() {
+ $el.popover('show');
+ });
+
+ $el.on('shown.bs.popover', (e) => {
+ var $tip = $el.data('bs.popover').$tip;
+ $tip.find('input[name="title"]').focus();
+ });
+ },
+ destroyTooltip: function() {
+ var target = new Snap(this.getTooltipTarget());
+
+ var $el = $(target.node);
+ if ($el.data('bs.popover')) {
+ $el.popover('destroy');
+ }
+ },
+ hideTooltip: function() {
+ var target = new Snap(this.getTooltipTarget());
+
+ var $el = $(target.node);
+ if ($el.data('bs.popover')) {
+ $el.popover('hide');
+ }
+ },
+ showTooltip: function() {
+ var target = new Snap(this.getTooltipTarget());
+
+ var $el = $(target.node);
+ if ($el.data('bs.popover')) {
+ $el.popover('show');
+ }
+ },
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/tagform/ColorButtons.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,139 @@
+<template>
+ <div>
+ <div class="btn-group" data-toggle="buttons">
+ <label
+ class="btn btn-default"
+ v-bind:class="{ 'btn--highlight': value && i <= value }"
+ ref="buttons"
+ v-for="i in 5"
+ v-bind:data-value="i"
+ v-on:mouseenter="onMouseEnter"
+ v-on:mouseleave="onMouseLeave"
+ v-on:click="onClick">
+ <input type="radio" name="options" autocomplete="off">
+ <span class="sr-only">{{ i }}</span>
+ </label>
+ </div>
+ </div>
+</template>
+
+<script>
+
+ export default {
+
+ props: [ 'original-value' ],
+
+ data() {
+ return {
+ value: null,
+ }
+ },
+
+ mounted() {
+ if (this.originalValue) {
+ this.value = this.originalValue;
+ }
+ },
+
+ methods: {
+
+ animate: function() {
+
+ var timeout;
+ var times = 0;
+ var direction = 1;
+
+ var increment = function() {
+ if (this.value === 5) {
+ direction = -1;
+ }
+ if (this.value === 1) {
+ direction = 1;
+ }
+
+ this.value += direction;
+
+ if (++times === 10) {
+ clearTimeout(timeout);
+ if (this.originalValue === null) {
+ this.value = null;
+ }
+ return;
+ }
+
+ timeout = setTimeout(increment.bind(this), 100)
+ }
+
+ increment.apply(this);
+ },
+
+ onMouseEnter: function(e) {
+ var value = $(e.target).data('value');
+ this.$refs.buttons.forEach((button) => {
+ if ($(button).data('value') <= value) {
+ $(button).addClass('btn--highlight');
+ } else {
+ $(button).removeClass('btn--highlight');
+ }
+ });
+ },
+
+ onMouseLeave: function(e) {
+ if (!this.value) {
+ this.$refs.buttons.forEach((button) => {
+ $(button).removeClass('btn--highlight');
+ });
+ } else {
+ this.$refs.buttons.forEach((button) => {
+ if ($(button).data('value') <= this.value) {
+ $(button).addClass('btn--highlight');
+ } else {
+ $(button).removeClass('btn--highlight');
+ }
+ });
+ }
+ },
+
+ onClick: function(e) {
+ var value = parseInt($(e.target).data('value'));
+ this.value = value;
+ this.$emit('change', { value: value });
+ }
+
+ }
+
+ }
+
+</script>
+
+<style scoped>
+
+.btn-group {
+ margin-bottom: 10px;
+}
+
+.btn--highlight {
+ box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+.btn--highlight[data-value="1"] {
+ background-color: #fc5f62;
+}
+
+.btn--highlight[data-value="2"] {
+ background-color: #f7c136;
+}
+
+.btn--highlight[data-value="3"] {
+ background-color: #f7e53b;
+}
+
+.btn--highlight[data-value="4"] {
+ background-color: #ebf63d;
+}
+
+.btn--highlight[data-value="5"] {
+ background-color: #b9e78b;
+}
+
+</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/tagform/TagList.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,69 @@
+<template>
+ <div>
+ <div class="tag-list">
+ <tag-list-item ref="items"
+ v-for="(tag, index) in tags" v-if="tags.length > 0"
+ v-bind:label="tag.tag_label"
+ v-bind:index="index"
+ v-bind:original-accuracy="tag.accuracy"
+ v-bind:original-relevancy="tag.relevancy"></tag-list-item>
+ </div>
+ <typeahead ref="typeahead" placeholder="Rechercher"></typeahead>
+ </div>
+</template>
+
+<script>
+
+ import TagListItem from './TagListItem.vue'
+ import Typeahead from './Typeahead.vue'
+
+ export default {
+ props: ['original-tags'],
+ components: {
+ "typeahead": Typeahead,
+ "tag-list-item": TagListItem
+ },
+ data() {
+ return {
+ tags: []
+ }
+ },
+ watch: {
+ tags: function(tags) {
+ this.$emit('change', { tags: tags });
+ }
+ },
+ mounted() {
+ if (this.originalTags) {
+ this.tags = this.originalTags;
+ }
+ this.$refs.typeahead.$on('selected', (tag) => {
+ this.tags.push(tag);
+ });
+ },
+
+ methods: {
+ hideAll: function() {
+ this.$refs.items.forEach((item) => item.hide());
+ },
+ replaceItemAt: function(index, data) {
+ const tag = this.tags[index];
+ Object.assign(tag, data);
+ this.tags.splice(index, 1, tag);
+ },
+ removeItemAt: function(index) {
+ this.tags.$remove(this.tags[index]);
+ },
+ }
+
+ }
+
+</script>
+
+<style scoped>
+
+.tag-list {
+ margin-bottom: 15px;
+}
+
+</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/tagform/TagListItem.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,202 @@
+<template>
+ <div class="tag-container">
+ <div @click="toggle" class="tag-item">
+ <span class="tag-title">{{ label }}</span>
+ <div class="tag-item-buttons">
+ <div class="tag-item-btn tag-item-accuracy" v-bind:data-value="accuracy">
+ <button class="btn btn-default">{{ accuracy || '?' }}</button>
+ </div>
+ <div class="tag-item-btn tag-item-relevancy" v-bind:data-value="relevancy">
+ <button class="btn btn-default">{{ relevancy || '?' }}</button>
+ </div>
+ <div class="tag-item-btn tag-item-delete">
+ <button class="btn btn-default"
+ @click="remove"><i class="fa fa-times" aria-hidden="true"></i></button>
+ </div>
+ </div>
+ </div>
+ <div class="collapse">
+ <div class="tag-item-form">
+ <div>
+ <label>Fiabilité</label>
+ <small>Êtes-vous sûr de votre tag ?</small>
+ <color-buttons ref="accuracy"
+ @change="accuracy = $event.value"
+ v-bind:original-value="accuracy"></color-buttons>
+ </div>
+ <div>
+ <label>Pertinence</label>
+ <small>Votre tag est-il indispensable ?</small>
+ <color-buttons ref="relevancy"
+ @change="relevancy = $event.value"
+ v-bind:original-value="relevancy"></color-buttons>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+
+ import ColorButtons from './ColorButtons.vue'
+ import Typeahead from './Typeahead.vue'
+
+ export default {
+ props: [
+ 'index',
+ 'label',
+ 'original-accuracy',
+ 'original-relevancy'
+ ],
+ components: {
+ "typeahead": Typeahead,
+ "color-buttons": ColorButtons
+ },
+ data() {
+ return {
+ accuracy: null,
+ relevancy: null,
+ isNew: true,
+ }
+ },
+ watch: {
+ accuracy: function(accuracy) {
+ this.onChange();
+ },
+ relevancy: function(relevancy) {
+ this.onChange();
+ }
+ },
+ mounted() {
+
+ this.accuracy = this.originalAccuracy;
+ this.relevancy = this.originalRelevancy;
+ this.isNew = (!this.originalAccuracy && !this.originalRelevancy);
+
+ this.$refs.accuracy.value = this.originalAccuracy;
+ this.$refs.relevancy.value = this.originalRelevancy;
+
+ $(this.$el).find('.collapse').collapse({ toggle: false });
+
+ if (this.isNew) {
+ this.$parent.hideAll();
+ this.show();
+ this.$refs.accuracy.animate();
+ }
+ },
+
+ methods: {
+ isComplete: function() {
+ return this.accuracy && this.relevancy;
+ },
+ onChange: function() {
+ this.$parent.replaceItemAt(this.index, {
+ accuracy: this.accuracy,
+ relevancy: this.relevancy
+ });
+ if (this.isNew && this.isComplete()) {
+ this.isNew = false;
+ this.hide();
+ }
+ },
+ show: function() {
+ $(this.$el).find('.collapse').collapse('show');
+ },
+ hide: function() {
+ $(this.$el).find('.collapse').collapse('hide');
+ },
+ toggle: function(e) {
+ e.preventDefault();
+ this.$parent.hideAll();
+ $(this.$el).find('.collapse').collapse('toggle');
+ },
+ remove: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.$parent.removeItemAt(this.index);
+ },
+ }
+
+ }
+
+</script>
+
+<style scoped>
+
+.tag-item-btn {
+ float: left;
+}
+
+.tag-item-btn button {
+ border: none;
+ background-color: transparent;
+}
+
+.tag-item-btn[data-value="1"] {
+ background-color: #fc5f62;
+}
+
+.tag-item-btn[data-value="2"] {
+ background-color: #f7c136;
+}
+
+.tag-item-btn[data-value="3"] {
+ background-color: #f7e53b;
+}
+
+.tag-item-btn[data-value="4"] {
+ background-color: #ebf63d;
+}
+
+.tag-item-btn[data-value="5"] {
+ background-color: #b9e78b;
+}
+
+.tag-title {
+ padding: 5px;
+ max-width: 140px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.tag-item-delete {
+ /* padding-left: 15px; */
+}
+
+.tag-item-form {
+ padding: 15px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.tag-item-form label,
+.tag-item-form small {
+ display: block;
+ margin-bottom: 5px;
+}
+
+.tag-container {
+ border: 1px solid #ccc;
+ border-bottom: none;
+}
+
+.tag-list .tag-container:last-of-type {
+ border-bottom: 1px solid #ccc;
+}
+
+.tag-item {
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #ccc;
+}
+.tag-item:hover {
+ background-color: #f5f5f5;
+}
+
+</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/tagform/Typeahead.vue Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,160 @@
+<template>
+ <div>
+ <input type="text"
+ class="form-control"
+ v-bind:placeholder="placeholder"
+ autocomplete="off"
+ v-model="query"
+ v-on:keyup.8="checkQuery"
+ @keydown.down="down"
+ @keydown.up="up"
+ @keydown.enter="hit($event)"
+ @keydown.esc="reset"
+ @keyup="update" />
+ <ul v-show="hasItems">
+ <li v-for="(item, index) in items" :class="activeClass(index)" @mousedown="hit" @mousemove="setActive(index)">
+ <span v-text="item.tag_label"></span>
+ </li>
+ </ul>
+ </div>
+</template>
+
+<style scoped src="./typeahead.css"></style>
+
+<script>
+
+ import typeahead from 'vue-typeahead'
+
+ var autoCompletePath = "https://lookup.dbpedia.org/api/search/PrefixSearch?MaxHits=5";
+ var wikipediaPath = "https://fr.wikipedia.org/w/api.php"
+ var parentsMethods = {
+ reset: typeahead.methods.reset
+ };
+
+
+ var get = function (url, data) {
+ var dfd = jQuery.Deferred();
+ var promise = jQuery.getJSON(url, data).done( function (response) {
+ var envelope = {};
+ envelope.data = response;
+ dfd.resolve(envelope);
+ }).fail(dfd.reject);
+ return dfd.promise();
+ }
+
+ export default {
+ mixins: [typeahead],
+
+ props: ['placeholder'],
+
+ mounted() {
+ },
+
+ data() {
+ return {
+ src: autoCompletePath,
+ limit: 7,
+ minChars: 2,
+ showAddButton: false,
+ datasource: "wikipedia",
+ selectedTags: "[]",
+ items: [],
+ queryParamName: "QueryString",
+ }
+ },
+
+ methods: {
+
+ checkQuery () {
+ if (this.query.length === 0) {
+ this.reset();
+ }
+ },
+
+ fetch() {
+ if (this.datasource === "wikipedia") {
+ return this.fetchWikiPedia();
+ }
+
+ else {
+ var request = {};
+ request[this.queryParamName] = this.query;
+ return get(this.src, query);
+ }
+ },
+
+ fetchWikiPedia () {
+ this.src = wikipediaPath;
+ var self = this;
+ var request = {
+ 'action': 'opensearch',
+ 'format': 'json',
+ 'search': this.query
+ };
+
+ /* make request */
+ var dfd = jQuery.Deferred();
+ jQuery.ajax({
+ url: this.src,
+ data: request,
+ dataType: "jsonp",
+ success: function (response) {
+ var envelope = {};
+ envelope.data = response;
+ dfd.resolve(envelope);
+ }
+ });
+ return dfd.promise();
+ },
+
+ reset () {
+ this.showAddButton = false;
+ parentsMethods.reset.call(this);
+ },
+
+ prepareWikipediaResponse (data) {
+ var results = [];
+ if (data.length !== 4) { return results; }
+ var labelsList = data[1];
+ var urlsList = data[3];
+
+ if (labelsList.length !== urlsList.length) {
+ return;
+ }
+
+ labelsList.map(function(item, index) {
+ var tagItem = {};
+ tagItem.tag_label = item;
+ var link = urlsList[index];
+ link = link.replace("https://fr.wikipedia.org/wiki/", "http://fr.dbpedia.org/resource/");
+ tagItem.tag_link = decodeURI(link);
+ tagItem.accuracy = null;
+ tagItem.relevancy = null;
+ results.push(tagItem);
+ });
+
+ return results;
+ },
+
+ prepareResponseData (data) {
+ var responseData = (typeof data === 'string') ? JSON.parse(data): data;
+
+ if(this.datasource === "wikipedia") {
+ responseData = this.prepareWikipediaResponse(responseData);
+ }
+
+ if (Array.isArray(responseData) && !responseData.length) {
+ this.showAddButton = true;
+ }
+ return responseData;
+ },
+
+ onHit (selected) {
+ this.$emit('selected', selected);
+ this.reset();
+ }
+ }
+
+ }
+
+</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/tagform/typeahead.css Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,70 @@
+Typeahead {
+ position: relative;
+}
+.selected-tags { border: 1px solid red; width: 200px !important; }
+.selected-tags select {display: inline-block;}
+
+.Typeahead__input {
+ width: 100%;
+ font-size: 14px;
+ color: #2c3e50;
+ line-height: 1.42857143;
+ box-shadow: inset 0 1px 4px rgba(0,0,0,.4);
+ -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
+ transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+ font-weight: 300;
+ padding: 12px 26px;
+ border: none;
+ border-radius: 22px;
+ letter-spacing: 1px;
+ box-sizing: border-box;
+}
+.Typeahead__input:focus {
+ border-color: #4fc08d;
+ outline: 0;
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px #4fc08d;
+}
+.tag-item {border: 1px solid red;}
+.fa-times {
+ cursor: pointer;
+}
+
+ul {
+ padding: 0;
+ margin-top: 8px;
+ min-width: 100%;
+ background-color: #fff;
+ list-style: none;
+ border-radius: 4px;
+ box-shadow: 0 0 10px rgba(0,0,0, 0.25);
+ z-index: 1000;
+}
+li {
+ padding: 10px 16px;
+ border-bottom: 1px solid #ccc;
+ cursor: pointer;
+}
+li:first-child {
+ border-radius: 4px 4px 0 0;
+}
+li:last-child {
+ border-radius: 0 0 4px 4px;
+ border-bottom: 0;
+}
+span {
+ display: block;
+ color: #2c3e50;
+}
+.active {
+ background-color: #3aa373;
+}
+.active span {
+ color: white;
+}
+.name {
+ font-weight: 700;
+ font-size: 18px;
+}
+.screen-name {
+ font-style: italic;
+}
--- a/src_js/iconolab-bundle/src/main.js Thu Feb 09 16:57:05 2017 +0100
+++ b/src_js/iconolab-bundle/src/main.js Wed Feb 15 16:42:06 2017 +0100
@@ -12,6 +12,9 @@
import DescriptionViewer from './components/collectionhome/descriptionviewer/DescriptionViewer.vue'
import DiffViewer from './components/diffviewer/diffviewer.vue'
import jsondiffpatch from 'jsondiffpatch'
+import Editor from './components/editor'
+import ColorButtons from './components/tagform/ColorButtons.vue'
+import TagList from './components/tagform/TagList.vue'
const Diff = require('diff')
Vue.config.ignoredElements = ["mask"];
@@ -26,10 +29,17 @@
Typeahead: Typeahead,
MergeTool: MergeTool,
Zoomview: Zoomview,
- DiffViewer: DiffViewer
+ DiffViewer: DiffViewer,
+ Editor: Editor,
+ ColorButtons: ColorButtons,
+ TagList: TagList
}
};
+Vue.component('color-buttons', ColorButtons);
+Vue.component('image-annotator', Editor.Canvas);
+Vue.component('annotation', Editor.Annotation);
+
if (!window.iconolab) {
window.iconolab = iconolab;
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/tagform.html Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Iconolab</title>
+ <link rel="stylesheet" href="/static/iconolab/css/iconolab.css">
+ <style>
+ body {
+ margin-top: 40px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <div class="row">
+ <div class="col-md-6">
+ <div id="tagform">
+ <color-buttons ref="buttons" v-for="i in [null, 1, 2, 3, 4, 5]" v-bind:original-value="i"></color-buttons>
+ </div>
+ <hr>
+ <button id="animate" class="btn btn-default">Animate</button>
+ </div>
+ <div class="col-md-6">
+ <div id="taglist"></div>
+ <hr>
+ <pre id="tags-json"></pre>
+ </div>
+ </div>
+ </div>
+
+ <script src="/static/iconolab/js/vendor.js"></script>
+ <script src="/static/iconolab/js/iconolab.js"></script>
+ <script>
+
+ var vm = new Vue({
+ el: '#tagform'
+ });
+
+ $('#animate').on('click', function(e) {
+ $.each(vm.$refs.buttons, function() {
+ this.animate();
+ });
+ })
+ //
+
+
+ function refreshTags() {
+ $('#tags-json').html(JSON.stringify(taglist.tags, null, 2));
+ }
+
+ var TagList = iconolab.VueComponents.TagList;
+ var taglist = new Vue(TagList);
+ taglist.tags = [
+ {
+ "tag_label": "Football",
+ "tag_input": "http://fr.dbpedia.org/resource/Football",
+ "accuracy": 5,
+ "relevancy": 2
+ }, {
+ "tag_label": "Sexion d'Assaut",
+ "tag_input": "http://fr.dbpedia.org/resource/Sexion_d'Assaut",
+ "accuracy": 3,
+ "relevancy": 1
+ },
+ {
+ "tag_label": "Fillon",
+ "tag_input": "http://fr.dbpedia.org/resource/Fillon",
+ "accuracy": 4,
+ "relevancy": 3
+ }
+ ];
+ taglist.$mount('#taglist');
+
+ taglist.$on('change', function(e) {
+ refreshTags()
+ });
+
+ refreshTags();
+
+ </script>
+ </body>
+</html>