GridEditor.vue 31.8 KB
Newer Older
Anton's avatar
Intial  
Anton committed
1
2
3
<template lang="pug">

  div.cell-grid-container
4

5
    q-window-resize-observable(@resize="updateGridDimensions")
Anton's avatar
Intial  
Anton committed
6

christianrhansen's avatar
christianrhansen committed
7
8
9
10
    // ------------------------------------------------------------------------------------------ cached new cell helper
    // q-page-sticky.z-top(v-touch-pan="moveCachedCell", v-if="cachedNewCell", position="top-right")
    .fixed.z-top(v-touch-pan="moveCachedCell", v-if="cachedNewCell",
    :style="{top: touch.position.top - 16 - 16 + 'px', left: touch.position.left - 16 - 16 + 'px'}")
11
      .bg-frosted-glass.text-dark.q-px-md.q-py-sm.q-ma-md.shadow-4(:style="{width: gridDimensions.full.cell.width + 'px', height: gridDimensions.full.cell.height + 'px', borderRadius: 0}")
christianrhansen's avatar
christianrhansen committed
12
        | {{ cachedNewCell.type }} Cell:
13
        br
christianrhansen's avatar
christianrhansen committed
14
        | {{ cachedNewCell.value }}
15
16
17
18
        q-btn.bg-dark.text-white.on-right(@click="", round, flat, size="sm")
          q-icon(name="clear")

    // ------------------------------------------------------------------------------------------------------------ grid
Anton's avatar
Intial  
Anton committed
19
    div.cell-grid(
christianrhansen's avatar
christianrhansen committed
20
    @click="event => {test(event)}",
21
22
23
24
25
26
    @dragenter="handleGridDragOver",
    @dragover="handleGridDragOver",
    @dragleave="handleGridDragEnd",
    @drop="handleGridDrop",
    @contextmenu="handleGridContextMenu",
    :style="gridStyle")
Anton's avatar
Intial  
Anton committed
27
28
29
30

      q-context-menu(ref="gridmenu")
        q-list(link, separator, no-border, style="min-width: 150px; max-height: 300px;")
          q-item(
31
32
33
34
35
          v-for="action in gridContextMenuActions",
          :key="action.label",
          v-close-overlay,
          @click.native="event => {action.handler(event)}")
            q-item-main(:label="action.label")
Anton's avatar
Intial  
Anton committed
36
37
38

      template(v-if="!resizingGrid")

39
        template(v-for="(annotation, index) in annotations")
christianrhansen's avatar
christianrhansen committed
40
41

          //----- cell
Anton's avatar
Intial  
Anton committed
42
          .cell-item(
43
44
45
46
47
48
49
50
51
52
53
54
55
          v-if="!annotationUIStates[annotation._uuid] || !annotationUIStates[annotation._uuid].beingDragged",
          draggable="true",
          @click.prevent="event => {handleCellTouch(event, annotation)}",
          @dragstart="event => {handleCellDragStart(event, annotation)}",
          @dragend="event => {handleCellDragEnd(event, annotation)}",
          @contextmenu="handleCellContextMenu",
          :style="getAnnotationStyle(annotation)",
          :class="getAnnotationClasses(annotation._uuid, 'cell-item')",
          :key="`cell-${index}`")

            //----- edit-/close-button
            // TODO: find a more elegant solution
            .desktop-only
Mathias Bär's avatar
Mathias Bär committed
56
              q-btn.edit-button.absolute-top-right(
57
58
59
60
61
62
63
64
65
              @click.prevent="event => {handleCellEditClick(event, annotation)}",
              :class="getAnnotationClasses(annotation._uuid, 'editing')",
              style="top: 8px; right: 8px;",
              :icon="annotationUIStates[annotation._uuid].editing ? 'close' : 'edit'", flat, round, size="md"
              )
            .mobile-only
              q-btn.edit-button.absolute.fit.bg-transparent(
              @click.prevent="event => {touchMobileCell(event, annotation)}", flat)

66
            //----- selecting cells disabled because it has no use currently
67
68
69
70
71
72
73
74
            // switch with cell component below to re-enable it
            //cell(
              @click.native.prevent="event => {handleCellClick(event, annotation)}", :annotation="annotation", :preview="true")
            cell(
            :annotation="annotation",
            :preview="true"
            )

75
            //----- resize-handler
76
            .desktop-only.cell-item-resize-handle(
77
78
79
80
81
82
            draggable="true",
            @dragstart="event => {handleCellResizerDragStart(event, annotation)}",
            @dragend="event => {handleCellResizerDragEnd(event, annotation)}",
            @dragexit="event => {handleCellResizerDragEnd(event, annotation)}")
              q-icon(name="network cell")

83
84
85
            //----- context menu for cells (desktop only)
            // TODO: needs revision
            q-context-menu.desktop-only
86
87
88
89
90
91
92
              q-list(link, separator, no-border, style="min-width: 150px; max-height: 300px;")
                q-item(
                v-for="action in cellContextMenuActions",
                :key="action.label",
                v-close-overlay,
                @click.native="event => {action.handler(event, annotation)}")
                  q-item-main(:label="action.label")
Anton's avatar
Intial  
Anton committed
93

94
        //----- temporary cell when pulling a new cell into grid (desktop)
95
        template(v-for="(tmpCell, index) in tmpObjects")
96
          .cell-item.cell-item-tmp(:style="getAnnotationStyle(tmpCell)", :key="`cell-tmp-${index}`")
Anton's avatar
Intial  
Anton committed
97
98
            cell(:cell="tmpCell")

christianrhansen's avatar
christianrhansen committed
99
      // ---------------------------------------------------------------------------------------------------------------
Anton's avatar
Intial  
Anton committed
100
      template(v-else)
101
        .cell-item(:style="getAnnotationStyle({x:0,y:0,width:1,height:1})", key="cell-grid-resizer")
Anton's avatar
Intial  
Anton committed
102
          div.cell-item-resize-handle(
103
104
105
106
107
          draggable="true",
          @dragstart="event => {handleGridResizerDragStart(event)}",
          @dragend="event => {handleGridResizerDragEnd(event)}",
          @dragexit="event => {handleGridResizerDragEnd(event)}")
            q-icon(name="network cell")
Anton's avatar
Intial  
Anton committed
108

Mathias Bär's avatar
Mathias Bär committed
109
      //template(v-if="!isMobile")
110
111
112
113
114
        .fixed-top-right(style="right:18px; top:68px", v-if="!$store.state.mosys.showSources")
          q-btn(round, color="primary", small, @click="handleGridButtonClickEdit", style="margin-right: 0.5em")
            q-icon(name="add")
          q-btn(round, color="primary", small, @click="$router.push(`/mosys/grids/${$route.params.uuid}`)")
            q-icon(name="remove red eye")
Mathias Bär's avatar
Mathias Bär committed
115
      //template(v-if="isMobile")
Mathias Bär's avatar
Mathias Bär committed
116
117
118
119
120
        .fixed-top-right.q-mt-sm(v-if="!$store.state.mosys.showSources", style="z-index: 10000; padding-top: 3px;")
          q-btn.q-mr-sm(round, color="primary", size="sm", @click="handleGridButtonClickEdit")
            q-icon(name="add")
          q-btn.q-mr-md(round, color="primary", size="sm", @click="$router.push(`/mosys/grids/${$route.params.uuid}`)")
            q-icon(name="remove red eye")
Anton's avatar
Intial  
Anton committed
121

122
123
124
125
126
    // --------------------------------------------------------------------------------------------------------- buttons

    q-page-sticky.z-top.q-mb-md.backdrop-filter.shadow-1.q-mx-md.overflow-hidden(v-if="showEditingCells", position="bottom-right")
      div.row.text-center.text-dark.round-borders.full-width(style="border-bottom: 1px solid #bbb;")
        .col-3
127
          q-btn.full-width(@click="setEditMode('edit')", :class="{'bg-primary text-white' : editMode === 'edit'}", flat)
128
129
            q-icon(name="edit")
        .col-3
130
          q-btn.full-width(@click="setEditMode('resize')", :class="{'bg-primary text-white' : editMode === 'resize'}", flat)
131
132
            q-icon.flip-vertical(name="photo_size_select_small")
        .col-3
133
          q-btn.full-width(@click="setEditMode('move')", :class="{'bg-primary text-white' : editMode === 'move'}", flat)
134
135
            q-icon(name="open_with")
        .col-3
136
          q-btn.full-width(@click="setEditMode('delete')", :class="{'bg-primary text-white' : editMode === 'delete'}", flat)
137
138
139
140
141
142
143
144
145
            q-icon(name="delete")

      div.full-width

        //----- edit
        div#edit-content-mobile(v-if="editMode === 'edit'")
          grid-editor-editing-cells-mobile

        //----- move
christianrhansen's avatar
christianrhansen committed
146
        div(v-if="editMode === 'move'", :class="{'q-pa-sm' : editMode}")
147
          .text-center
148
            q-btn.bg-dark(@click="mobileCellMove(mobileSelectedCell, 0, -1)", round, flat, size="md")
149
150
              q-icon.rotate-90(name="keyboard_backspace")
          .text-center
151
            q-btn.bg-dark(@click="mobileCellMove(mobileSelectedCell, -1, 0)", round, flat, size="md")
152
              q-icon(name="keyboard_backspace")
153
            q-btn.invisible.text-dark(round, flat, size="md")
154
              q-icon.flip-vertical(name="photo_size_select_small")
155
            q-btn.bg-dark(@click="mobileCellMove(mobileSelectedCell, 1, 0)", round, flat, size="md")
156
157
              q-icon.rotate-180(name="keyboard_backspace")
          .text-center
158
            q-btn.bg-dark(@click="mobileCellMove(mobileSelectedCell, 0, 1)", round, flat, size="md")
159
160
161
              q-icon.rotate-270(name="keyboard_backspace")

        //----- resize
christianrhansen's avatar
christianrhansen committed
162
        div(v-if="editMode === 'resize'", :class="{'q-pa-sm' : editMode}")
163
          .text-center
164
            q-btn.bg-dark(@click="mobileCellResize(mobileSelectedCell, 0, -1)", round, flat, size="md")
165
166
              q-icon(name="remove")
          .text-center
167
            q-btn.bg-dark(@click="mobileCellResize(mobileSelectedCell, -1, 0)", round, flat, size="md")
168
              q-icon(name="remove")
169
            q-btn.invisible.text-dark(round, flat, size="md")
170
              q-icon.rotate-45(name="zoom_out_map")
171
            q-btn.bg-dark(@click="mobileCellResize(mobileSelectedCell, 1, 0)", round, flat, size="md")
172
173
              q-icon(name="add")
          .text-center
174
            q-btn.bg-dark(@click="mobileCellResize(mobileSelectedCell, 0, 1)", round, flat, size="md")
175
176
177
178
179
180
181
182
              q-icon(name="add")

        //----- delete
        div(v-if="editMode === 'delete'", :class="{'q-pa-md' : editMode}")
          .text-center
            .text-dark
              | {{ $t('labels.delete_cell') }}
            .q-mt-md
183
184
              q-btn.bg-dark.text-white.q-mx-sm(@click="event => {handleCellContextMenuDelete(event, mobileSelectedCell)}",
              flat, round, size="md")
185
186
                q-icon(name="check")

Anton's avatar
Intial  
Anton committed
187
188
189
190
</template>

<script>
  import Cell from './Cell'
191
192
  import { userHasFeature } from 'mbjs-quasar/src/lib'
  import { mapGetters } from 'vuex'
193
  import GridEditorEditingCellsMobile from './/GridEditorEditingCellsMobile'
Anton's avatar
Intial  
Anton committed
194
195
196
197
198
199

  const nullImage = new Image()
  nullImage.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='

  export default {
    components: {
200
201
      Cell,
      GridEditorEditingCellsMobile
Anton's avatar
Intial  
Anton committed
202
    },
203
    props: ['gridUuid', 'tabsAreOpen'],
Anton's avatar
Intial  
Anton committed
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
    data () {
      return {
        gridContextMenuActions: {
          insert_column_left: {
            label: 'Insert Column Left',
            handler: this.handleGridContextMenuInsertColumnLeft
          },
          delete_column: {
            label: 'Delete Column',
            handler: this.handleGridContextMenuDeleteColumn
          },
          insert_row_above: {
            label: 'Insert Row Above',
            handler: this.handleGridContextMenuInsertRowAbove
          },
          delete_row: {
            label: 'Delete Row',
            handler: this.handleGridContextMenuDeleteRow
          },
          edit_grid_dimensions: {
            label: 'Change Grid',
            handler: () => { this.resizingGrid = !this.resizingGrid }
          }
        },
        grid: undefined,
229
        annotations: undefined,
230
        tmpObjects: [],
231
        annotationUIStates: {},
Anton's avatar
Anton committed
232
        gridDimensions: { gridWidth: 0, gridHeight: 0, cellWidth: 0, cellHeight: 0 },
Anton's avatar
Intial  
Anton committed
233
        contextMenuClickPosition: {},
234
        resizingGrid: false,
christianrhansen's avatar
christianrhansen committed
235
236
        mobileSelectedCell: undefined,
        touch: {position: {top: undefined, left: undefined}}
Anton's avatar
Intial  
Anton committed
237
238
      }
    },
239
240
    computed: {
      ...mapGetters({
christianrhansen's avatar
christianrhansen committed
241
        cachedNewCell: 'mosys/getNewCell',
242
        user: 'auth/getUserState',
Mathias Bär's avatar
Mathias Bär committed
243
244
        isMobile: 'globalSettings/getIsMobile',
        // editingCells: 'mosys/getEditingCells'
245
        showEditingCells: 'mosys/getShowEditingCells',
246
247
        scrollPositionCache: 'mosys/getScrollPositionCache',
        editMode: 'mosys/getEditMode'
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
      }),
      cellContextMenuActions () {
        const actions = {
          delete: {
            label: 'Delete',
            handler: this.handleCellContextMenuDelete
          },
          insert_column_left: {
            label: 'Insert Column Left',
            handler: this.handleGridContextMenuInsertColumnLeft
          },
          insert_row_above: {
            label: 'Insert Row Above',
            handler: this.handleGridContextMenuInsertRowAbove
          }
        }
        if (userHasFeature(this.user, 'cssediting')) {
          actions.edit_css_classname = {
            label: 'Edit CSS class name',
            handler: this.handleCellContextMenuEditCSS
          }
        }
        return actions
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
      },
      gridStyle () {
        if (!this.gridDimensions || !this.gridDimensions.full) return {}
        // TODO: fix mobile grid editor view
        const cell = this.gridDimensions.full.cell
        return {
          width: `${this.gridDimensions.full.width}px`,
          height: '100%',
          'grid-auto-columns': `${cell.width}px`,
          'grid-auto-rows': `${cell.height}px`,
          'background-image': `url("data:image/svg+xml;utf8,` +
            `<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><defs>` +
            `<pattern id='smallGrid' width='${cell.width}' height='${cell.height}' patternUnits='userSpaceOnUse'>` +
            `<path d='M ${cell.width} 0 L 0 0 0 ${cell.height}' fill='none' stroke='gray' stroke-width='0.5'/>` +
            `</pattern></defs><rect width='100%' height='100%' fill='url(%23smallGrid)' /></svg>")`
        }
287
288
      }
    },
Anton's avatar
Intial  
Anton committed
289
290
    async mounted () {
      await this.fetchData()
291
      this.resetScrollPosition()
Anton's avatar
Intial  
Anton committed
292
293
    },
    watch: {
Anton's avatar
Anton committed
294
      annotations () {
295
        this.updateAnnotationUIStates()
Anton's avatar
Intial  
Anton committed
296
297
298
299
300
301
      },
      gridMetadata () {
        this.updateGridDimensions()
      },
      async gridUuid () {
        await this.fetchData()
Mathias Bär's avatar
Mathias Bär committed
302
303
      },
      showEditingCells (val) {
304
        // console.log('show editing cells', val)
Mathias Bär's avatar
Mathias Bär committed
305
306
307
        if (val === false) {
          this.updateAnnotationUIStates()
        }
308
309
310
      },
      tabsAreOpen () {
        this.resetScrollPosition()
Anton's avatar
Intial  
Anton committed
311
312
313
      }
    },
    methods: {
christianrhansen's avatar
christianrhansen committed
314
315
316
317
318
319
320
321
322
323
324
325
326
327
      moveCachedCell (obj) {
        this.touch.position = {top: obj.position.top, left: obj.position.left}
      },
      test (event) {
        console.log('--->>>', this.getGridPositionForEvent(event))
        this.addMobileCell(event)
      },
      addMobileCell (event) {
        if (this.cachedNewCell) {
          console.log('###--- ', this.getGridPositionForEvent(event))
          this.$store.commit('mosys/cacheNewCell', undefined)
          this.handleGridDrop(event)
        }
      },
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
      touchMobileCell (event, cell) {
        Object.keys(this.annotationUIStates).filter((k) => {
          if (k === cell._uuid && this.annotationUIStates[k].editing) console.log(k, cell._uuid)
          else this.annotationUIStates[k].editing = false
        })
        this.annotationUIStates[cell._uuid].editing = !this.annotationUIStates[cell._uuid].editing
        this.updateEditingCells()
        this.$root.$emit('mosys_saveScrollPosition')
        // this.handleCellEditClick(event, annotation)
      },
      async mobileCellMove (annotation, _x, _y) {
        let
          parsed = annotation.target.selector.parse(),
          sliced = parsed.xywh.slice(0, 2),
          x = sliced[0],
          y = sliced[1]
        x += _x
        y += _y
        let target = this.grid.get2DArea([x, y], parsed.xywh.slice(2))
        annotation.target.selector.value = target.selector.value
        await this.$store.dispatch('annotations/patch', [annotation.id, {
          target: {
            selector: { value: target.selector.value }
          }
        }])
      },
      handleCellTouch (event, annotation) {
        console.log(event, annotation)
        this.mobileSelectedCell = annotation
      },
      async mobileCellResize (annotation, width, height) {
        let
          parsed = annotation.target.selector.parse(),
          [x, y, w, h] = parsed.xywh
        w += width
        h += height
        const value = { xywh: [x, y, w, h] }
        annotation.target.selector.value = value

        await this.$store.dispatch('annotations/patch', [annotation.id, { target: { selector: { value } } }])
      },
      setEditMode (mode) {
        this.$store.commit('mosys/setEditMode', mode)
      },
372
373
374
375
      //
      // DATA
      //

Anton's avatar
Intial  
Anton committed
376
377
378
      async fetchData () {
        if (this.gridUuid) {
          this.grid = await this.$store.dispatch('maps/get', this.gridUuid)
379
380
381
382
383
384
385
386
          if (!Object.keys(this.grid.config).length) {
            this.grid.config = {
              columns: 10,
              rows: 6,
              ratio: 16 / 9.0
            }
            await this.updateGridMetadataStore()
          }
Anton's avatar
Intial  
Anton committed
387
          this.updateGridDimensions()
388
389
390
391
392
393
          const { items } = await this.$store.dispatch('annotations/find', {
            'target.id': this.grid.id,
            'body.purpose': 'linking',
            'body.type': 'Cell'
          })
          this.annotations = items
Anton's avatar
Intial  
Anton committed
394
395
        }
      },
396
397
398
      async updateGridMetadataStore () {
        await this.$store.dispatch('maps/patch', [this.grid.id, { config: this.grid.config }])
        this.updateGridDimensions()
Anton's avatar
Intial  
Anton committed
399
      },
400
401
402
403
404
      updateSelectedCells () {
        const _this = this
        let selectedCells = Object.keys(this.annotationUIStates).filter(k => {
          return _this.annotationUIStates[k].selected
        }).map(k => {
Christian Hansen's avatar
Christian Hansen committed
405
406
          // return _this.cells.find(c => c._uuid === k)
          return _this.annotations.find(c => c._uuid === k)
407
408
        })
        this.$store.commit('mosys/setSelectedCells', selectedCells)
Anton's avatar
Intial  
Anton committed
409
      },
Mathias Bär's avatar
Mathias Bär committed
410
411
412
413
414
415
416
417
      updateEditingCells () {
        const _this = this
        let editingCells = Object.keys(this.annotationUIStates).filter(k => {
          return _this.annotationUIStates[k].editing
        }).map(k => {
          // return _this.cells.find(c => c._uuid === k)
          return _this.annotations.find(c => c._uuid === k)
        })
418
419
420
421
422
423
424
        /*
        console.log('this.annotationUIStates: ', this.annotationUIStates)
        console.log('editingCells: ', editingCells)
        if (this.isMobile) {
          console.log('MOBILE')
        }
        */
Mathias Bär's avatar
Mathias Bär committed
425
426
        this.$store.commit('mosys/setEditingCells', editingCells)
      },
427
428
429
430
431
      updateAnnotationUIStates () {
        let newAnnotationUIStates = {}
        this.annotations.forEach(a => {
          newAnnotationUIStates[a._uuid] = {
            selected: false,
Mathias Bär's avatar
Mathias Bär committed
432
            editing: false,
433
            beingResized: false,
Mathias Bär's avatar
Mathias Bär committed
434
            beginDragged: false,
435
            annotation: a
Anton's avatar
Intial  
Anton committed
436
          }
437
438
        })
        this.annotationUIStates = newAnnotationUIStates
Anton's avatar
Intial  
Anton committed
439
        this.updateSelectedCells()
Mathias Bär's avatar
Mathias Bär committed
440
        this.updateEditingCells()
441
      },
442
443
444
445
446

      //
      // GRID DRAG & DROP HANDLERS
      //

447
      async handleGridDragOver (event) {
Anton's avatar
Intial  
Anton committed
448
449
450
        let _this = this
        if (this.resizingGrid) {
          const position = this.getGridPositionForEvent(event)
451
452
          this.grid.config.ratio = position.ox / (position.oy * 1.0)
          await this.updateGridMetadataStore()
Anton's avatar
Intial  
Anton committed
453
454
        }
        else {
455
456
          let annotation = this.annotations.filter(annotation => {
            if (!_this.annotationUIStates[annotation._uuid]) return false
Mathias Bär's avatar
Mathias Bär committed
457
            return _this.annotationUIStates[annotation._uuid].beingDragged ||
458
              _this.annotationUIStates[annotation._uuid].beingResized
Anton's avatar
Intial  
Anton committed
459
460
          }).shift()
          let offset, position
461
462
463
464
          if (!annotation) {
            annotation = {
              target: this.grid.get2DArea([1, 1], [1, 1])
            }
Anton's avatar
Intial  
Anton committed
465
466
467
            position = this.getGridPositionForEvent(event)
          }
          else {
468
            offset = this.annotationUIStates[annotation._uuid].draggingOffset
Anton's avatar
Intial  
Anton committed
469
470
            position = this.getGridPositionForEvent(event, offset)
          }
471
472
          if (!this.tmpObjects.length) this.tmpObjects.push(annotation)
          const parsed = annotation.target.selector.parse()
Anton's avatar
Intial  
Anton committed
473
          if (event.dataTransfer.types.includes('text/plain')) {
474
475
            parsed.xywh[0] = position.x
            parsed.xywh[1] = position.y
Anton's avatar
Intial  
Anton committed
476
477
478
            event.preventDefault()
          }
          else {
479
480
            parsed.xywh[2] = Math.max(1, 1 + position.x - parsed.xywh[0])
            parsed.xywh[3] = Math.max(1, 1 + position.y - parsed.xywh[1])
Anton's avatar
Intial  
Anton committed
481
          }
482
          annotation.target.selector.value = parsed
Anton's avatar
Intial  
Anton committed
483
484
485
        }
      },
      handleGridDragEnd () {
486
        this.tmpObjects = []
Anton's avatar
Intial  
Anton committed
487
      },
488
489
      async handleGridDrop (event) {
        let dropData = event.dataTransfer.getData('text/plain')
christianrhansen's avatar
christianrhansen committed
490
491
        console.log('dropData', dropData)
        // {"data":{"content":"vvvvvvsdscscdsc"},"config":{},"component":"CellText"}
492
493
        if (dropData) {
          dropData = JSON.parse(dropData)
494
          let annotation = this.annotations.find(a => a.id === dropData.id)
495
496
497
498
499
          const { x, y } = this.getGridPositionForEvent(
            event,
            annotation ? this.annotationUIStates[annotation._uuid].draggingOffset : undefined
          )
          if (annotation) {
500
501
502
            const
              parsed = annotation.target.selector.parse(),
              target = this.grid.get2DArea([x, y], parsed.xywh.slice(2))
503
504
505
506
507
508
            annotation.target.selector.value = target.selector.value
            await this.$store.dispatch('annotations/patch', [annotation.id, {
              target: {
                selector: { value: target.selector.value }
              }
            }])
Anton's avatar
Intial  
Anton committed
509
510
          }
          else {
511
512
513
514
515
516
517
518
519
520
521
            const
              { data, config, component } = dropData,
              cell = await this.$store.dispatch('cells/post', { data, config, component })
            annotation = await this.$store.dispatch('annotations/post', {
              body: {
                type: 'Cell',
                purpose: 'linking',
                source: {
                  id: cell.id
                }
              },
522
              target: this.grid.get2DArea([x, y], [1, 1])
523
            })
524
            this.annotations.push(annotation)
Anton's avatar
Anton committed
525
            this.updateAnnotationUIStates()
Anton's avatar
Intial  
Anton committed
526
          }
527

528
          this.tmpObjects = []
Anton's avatar
Intial  
Anton committed
529
530
531
          event.preventDefault()
        }
      },
532
533
534
535

      //
      // CELL DRAG & DROP HANDLERS
      //
Mathias Bär's avatar
Mathias Bär committed
536
537
538
      handleCellEditClick (event, cell) {
        this.annotationUIStates[cell._uuid].editing = !this.annotationUIStates[cell._uuid].editing
        this.updateEditingCells()
539
        this.$root.$emit('mosys_saveScrollPosition')
Mathias Bär's avatar
Mathias Bär committed
540
      },
541
542
543
544
545
      handleCellClick (event, cell) {
        this.annotationUIStates[cell._uuid].selected = !this.annotationUIStates[cell._uuid].selected
        this.updateSelectedCells()
      },
      handleCellDragStart (event, annotation) {
christianrhansen's avatar
christianrhansen committed
546
        console.log('§§§§§§§§', annotation)
547
548
549
550
551
552
553
554
555
556
        if (!this.annotationUIStates[annotation._uuid].beingResized) {
          event.dataTransfer.setData('text/plain', JSON.stringify(annotation))
          event.dataTransfer.setDragImage(nullImage, 0, 0)
          let elContainerBoundingBox = this.$el.getBoundingClientRect()
          let elBoundingBox = event.target.getBoundingClientRect()
          let offset = {
            x: (event.clientX - elContainerBoundingBox.x) - (elBoundingBox.x - elContainerBoundingBox.x),
            y: (event.clientY - elContainerBoundingBox.y) - (elBoundingBox.y - elContainerBoundingBox.y)
          }
          this.annotationUIStates[annotation._uuid].draggingOffset = offset
Mathias Bär's avatar
Mathias Bär committed
557
          this.annotationUIStates[annotation._uuid].beingDragged = true
558
559
560
561
562
563
564
565
566
        }
        this.tmpObjects.push(annotation)
      },
      handleCellDragEnd (event, annotation) {
        this.annotationUIStates[annotation._uuid].beingDragged = false
      },
      async handleCellContextMenuDelete (event, annotation) {
        this.annotationUIStates[annotation._uuid].selected = false
        this.updateSelectedCells()
Mathias Bär's avatar
Mathias Bär committed
567
        this.updateEditingCells()
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
        await this.$store.dispatch('cells/delete', annotation.body.source.id)
        await this.$store.dispatch('annotations/delete', annotation.id)
        this.annotations = this.annotations.filter(a => a.id !== annotation.id)
      },

      //
      // GRID RESIZE HANDLERS

      handleGridResizerDragStart (event) {
        event.dataTransfer.setDragImage(nullImage, 0, 0)
      },
      async handleGridResizerDragEnd () {
        await this.updateGridMetadataStore()
      },

      //
      // CELL RESIZE HANDLERS
      //

      handleCellResizerDragStart (event, annotation) {
        event.dataTransfer.setDragImage(nullImage, 0, 0)
        this.annotationUIStates[annotation._uuid].beingResized = true
        this.tmpObjects.push(annotation)
      },
      async handleCellResizerDragEnd (event, annotation) {
        let position = this.getGridPositionForEvent(event)
        let
          parsed = annotation.target.selector.parse(),
          [x, y, w, h] = parsed.xywh
        w = Math.max(1, 1 + position.x - x)
        h = Math.max(1, 1 + position.y - y)
        const value = { xywh: [x, y, w, h] }
        annotation.target.selector.value = value
        this.annotationUIStates[annotation._uuid].beingResized = false
        this.tmpObjects = []
        await this.$store.dispatch('annotations/patch', [annotation.id, { target: { selector: { value } } }])
      },

      //
      // CELL CONTEXT MENU
      //

      handleCellContextMenu (event) {
Anton's avatar
Intial  
Anton committed
611
612
        this.contextMenuClickPosition = this.getGridPositionForEvent(event)
      },
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
      async handleCellContextMenuEditCSS (event, annotation) {
        try {
          let styleClass = await this.$q.dialog({
            title: this.$t('forms.edit_css_class.title'),
            ok: this.$t('buttons.set_css_class'),
            cancel: this.$t('buttons.cancel'),
            prompt: {
              model: annotation.styleClass,
              type: 'text'
            }
          })
          if (styleClass) {
            if (!styleClass.length) styleClass = undefined
            else if (styleClass.indexOf('.') === 0) styleClass = styleClass.substr(1)
          }
          annotation.styleClass = styleClass
          this.$store.dispatch('annotations/patch', [annotation._uuid, { target: { styleClass: styleClass || null } }])
Anton's avatar
Intial  
Anton committed
630
        }
631
632
633
634
635
636
637
638
639
        catch (e) { /* dialog canceled */ }
      },

      //
      // GRID CONTEXT MENU
      //

      handleGridContextMenu (event) {
        this.contextMenuClickPosition = this.getGridPositionForEvent(event)
Anton's avatar
Intial  
Anton committed
640
641
642
      },
      async handleGridContextMenuInsertColumnLeft () {
        let position = this.contextMenuClickPosition
643
644
645
646
647
648
649
650
        for (let annotation of this.annotations) {
          const parsed = annotation.target.selector.parse()
          if (parsed.xywh[0] >= position.x) {
            parsed.xywh[0] += 1
            annotation.target.selector.value = parsed
            await this.$store.dispatch('annotations/patch', [annotation.id, {
              target: { selector: { value: parsed } }
            }])
Anton's avatar
Intial  
Anton committed
651
652
          }
        }
653
        this.grid.config.columns += 1
Anton's avatar
Intial  
Anton committed
654
655
656
657
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuDeleteColumn () {
        let position = this.contextMenuClickPosition
658
659
660
661
662
663
664
665
        for (let annotation of this.annotations) {
          const parsed = annotation.target.selector.parse()
          if (parsed.xywh[0] > position.x) {
            parsed.xywh[0] -= 1
            annotation.target.selector.value = parsed
            await this.$store.dispatch('annotations/patch', [annotation.id, {
              target: { selector: { value: parsed } }
            }])
Anton's avatar
Intial  
Anton committed
666
667
          }
        }
668
        this.grid.config.columns -= 1
Anton's avatar
Intial  
Anton committed
669
670
671
672
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuInsertRowAbove () {
        let position = this.contextMenuClickPosition
673
674
675
676
677
678
679
680
        for (let annotation of this.annotations) {
          const parsed = annotation.target.selector.parse()
          if (parsed.xywh[1] >= position.y) {
            parsed.xywh[1] += 1
            annotation.target.selector.value = parsed
            await this.$store.dispatch('annotations/patch', [annotation.id, {
              target: { selector: { value: parsed } }
            }])
Anton's avatar
Intial  
Anton committed
681
682
          }
        }
683
        this.grid.config.rows += 1
Anton's avatar
Intial  
Anton committed
684
685
686
687
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuDeleteRow () {
        let position = this.contextMenuClickPosition
688
689
690
691
692
693
694
695
        for (let annotation of this.annotations) {
          const parsed = annotation.target.selector.parse()
          if (parsed.xywh[1] > position.y) {
            parsed.xywh[1] -= 1
            annotation.target.selector.value = parsed
            await this.$store.dispatch('annotations/patch', [annotation.id, {
              target: { selector: { value: parsed } }
            }])
Anton's avatar
Intial  
Anton committed
696
697
          }
        }
698
        this.grid.config.rows -= 1
Anton's avatar
Intial  
Anton committed
699
700
        await this.updateGridMetadataStore()
      },
701
702
703
704
705

      //
      // NAVIGATION
      //

Anton's avatar
Intial  
Anton committed
706
      handleGridButtonClickEdit () {
707
        this.$store.commit('mosys/toggleSources')
Anton's avatar
Intial  
Anton committed
708
      },
709
710
711
712
713

      //
      // GRID HELPERS
      //

Anton's avatar
Anton committed
714
715
      getGridPositionForEvent (event, offset = { x: 0, y: 0 }) {
        offset = { x: 0, y: 0 } // TODO: remove quick fix
Anton's avatar
Intial  
Anton committed
716
717
718
719
720
        const elContainerBoundingBox = this.$el.getBoundingClientRect()
        const ox = event.clientX + this.$el.scrollLeft - elContainerBoundingBox.x - offset.x
        const oy = event.clientY + this.$el.scrollTop - elContainerBoundingBox.y - offset.y
        const x = Math.ceil(ox / this.gridDimensions.full.cell.width)
        const y = Math.ceil(oy / this.gridDimensions.full.cell.height)
Anton's avatar
Anton committed
721
        return { x: x, y: y, ox: ox, oy: oy }
Anton's avatar
Intial  
Anton committed
722
      },
723
724
725
726
      updateGridDimensions (size) {
        if (!this.grid || !this.grid.config) return

        let elWidth = size ? size.width : this.$el.offsetWidth
727
        // let elHeight = size ? size.height : this.$el.offsetHeight
728
        let cellSizeRatio = this.grid.config.ratio
729
730
        // let gridHeight = elHeight
        let gridHeight = this.$el.offsetHeight
731
        let cellHeight = gridHeight / this.grid.config.rows
Anton's avatar
Intial  
Anton committed
732
        let cellWidth = elWidth / Math.round(elWidth / (cellHeight * cellSizeRatio))
733
        let gridWidth = cellWidth * this.grid.config.columns
Anton's avatar
Intial  
Anton committed
734
        let cellsPerWidth = elWidth / cellWidth
735
        let cellWidthMini = elWidth / this.grid.config.columns
Anton's avatar
Intial  
Anton committed
736
737
738
739
740
741
742
743
744
745
746
747
748
        let gridHeightMini = cellWidthMini / cellSizeRatio
        this.gridDimensions = {
          full: {
            width: gridWidth,
            height: gridHeight,
            cell: {
              width: cellWidth,
              height: cellHeight
            },
            cells_per_width: cellsPerWidth
          },
          mini: {
            width: elWidth,
749
            height: gridHeightMini * this.grid.config.rows,
Anton's avatar
Intial  
Anton committed
750
751
752
753
754
755
756
            cell: {
              width: cellWidthMini,
              height: gridHeightMini
            }
          }
        }
      },
757
758
      getAnnotationStyle (annotation) {
        const parsed = annotation.target.selector.parse()
Anton's avatar
Intial  
Anton committed
759
        return {
760
761
762
763
          'grid-column-start': parsed.xywh[0],
          'grid-column-end': `span ${parsed.xywh[2]}`,
          'grid-row-start': parsed.xywh[1],
          'grid-row-end': `span ${parsed.xywh[3]}`
Anton's avatar
Intial  
Anton committed
764
        }
Mathias Bär's avatar
Mathias Bär committed
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
      },
      getAnnotationClasses (uuid, which) {
        if (which === 'editing') {
          if (this.annotationUIStates[uuid].editing) {
            return 'bg-primary text-white'
          }
          else {
            return 'bg-grey'
          }
        }
        if (which === 'cell-item') {
          return {
            selected: this.annotationUIStates[uuid] ? this.annotationUIStates[uuid].selected : false,
            editing: this.annotationUIStates[uuid] ? this.annotationUIStates[uuid].editing : false
          }
        }
        else return {}
782
783
784
785
      },
      resetScrollPosition () {
        console.log('reset', this.scrollPositionCache, this.$el.scrollLeft)
        this.$el.scrollLeft = this.scrollPositionCache
Anton's avatar
Intial  
Anton committed
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
      }
    }
  }
</script>

<style scoped lang="stylus">
  .cell-grid
    display grid
    background-color #eee

  .cell-item

    &
      background-color rgba(0,0,0,0.2)
      border 1px solid #777
      margin 1px
      box-sizing border-box
      position relative
      overflow: hidden
      grid-column-start: 1
      grid-column-end: span 1
      grid-row-start: 1
      grid-row-end: span 1

    &:hover
      background-color lightblue

Mathias Bär's avatar
Mathias Bär committed
813
814
815
816
      .edit-button
        display: block

    &.editing
817
      background-color lightpink
Mathias Bär's avatar
Mathias Bär committed
818
819
820
821

      .edit-button
        display: block

Anton's avatar
Intial  
Anton committed
822
823
824
    &.selected
      background-color lightpink

Mathias Bär's avatar
Mathias Bär committed
825
826
827
    .edit-button
      display: none

Anton's avatar
Intial  
Anton committed
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
    .cell-item-inner
      width 100%
      height 100%

    .cell-item-resize-handle
      color rgba(0,0,0,0.2)
      position absolute
      right 0
      bottom 0
      width 18px
      height 20px

      &:hover
        color black

843
844
845
846
847
848
849
850
851
  .backdrop-filter
    backdrop-filter blur(6px)
    background-color rgba(255, 255, 255, .3)
    border-radius .5rem

  #edit-content-mobile
    width calc(100vw - 32px)
    max-height calc(calc(100vh - 60px - 16px - 16px) / 2)
    overflow-y scroll
Anton's avatar
Intial  
Anton committed
852
</style>