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

  div.cell-grid-container
4
    q-window-resize-observable(@resize="updateGridDimensions")
Anton's avatar
Intial  
Anton committed
5
6

    div.cell-grid(
7
8
9
10
11
12
    @dragenter="handleGridDragOver",
    @dragover="handleGridDragOver",
    @dragleave="handleGridDragEnd",
    @drop="handleGridDrop",
    @contextmenu="handleGridContextMenu",
    :style="gridStyle")
Anton's avatar
Intial  
Anton committed
13
14
15
16

      q-context-menu(ref="gridmenu")
        q-list(link, separator, no-border, style="min-width: 150px; max-height: 300px;")
          q-item(
17
18
19
20
21
          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
22
23
24

      template(v-if="!resizingGrid")

25
        template(v-for="(annotation, index) in annotations")
Anton's avatar
Intial  
Anton committed
26
          .cell-item(
27
28
29
30
31
32
33
34
35
36
37
38
39
          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
40
              q-btn.edit-button.absolute-top-right(
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
              @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)

            // selecting cells disabled because it has no use currently
            // 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"
            )

            //----- resize-handler (cell)
            div.cell-item-resize-handle(
            draggable="true",
            @dragstart="event => {handleCellResizerDragStart(event, annotation)}",
            @dragend="event => {handleCellResizerDragEnd(event, annotation)}",
            @dragexit="event => {handleCellResizerDragEnd(event, annotation)}")
              q-icon(name="network cell")

            q-context-menu
              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
75

76
        template(v-for="(tmpCell, index) in tmpObjects")
77
          .cell-item.cell-item-tmp(:style="getAnnotationStyle(tmpCell)", :key="`cell-tmp-${index}`")
Anton's avatar
Intial  
Anton committed
78
79
80
            cell(:cell="tmpCell")

      template(v-else)
81
        .cell-item(:style="getAnnotationStyle({x:0,y:0,width:1,height:1})", key="cell-grid-resizer")
Anton's avatar
Intial  
Anton committed
82
          div.cell-item-resize-handle(
83
84
85
86
87
          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
88

Mathias Bär's avatar
Mathias Bär committed
89
      //template(v-if="!isMobile")
90
91
92
93
94
        .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
95
      //template(v-if="isMobile")
Mathias Bär's avatar
Mathias Bär committed
96
97
98
99
100
        .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
101

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
    // --------------------------------------------------------------------------------------------------------- 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
          q-btn.full-width(@click="setEditMode('edit')", :class="{'bg-primary text-white' : editMode === 'edit'}", flat, size="lg")
            q-icon(name="edit")
        .col-3
          q-btn.full-width(@click="setEditMode('resize')", :class="{'bg-primary text-white' : editMode === 'resize'}", flat, size="lg")
            q-icon.flip-vertical(name="photo_size_select_small")
        .col-3
          q-btn.full-width(@click="setEditMode('move')", :class="{'bg-primary text-white' : editMode === 'move'}", flat, size="lg")
            q-icon(name="open_with")
        .col-3
          q-btn.full-width(@click="setEditMode('delete')", :class="{'bg-primary text-white' : editMode === 'delete'}", flat, size="lg")
            q-icon(name="delete")

      div.full-width

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

        //----- move
        div(v-if="editMode === 'move'", :class="{'q-pa-md' : editMode}")
          .text-center
            q-btn.bg-dark(@click="mobileCellMove(mobileSelectedCell, 0, -1)", round, flat)
              q-icon.rotate-90(name="keyboard_backspace")
          .text-center
            q-btn.bg-dark(@click="mobileCellMove(mobileSelectedCell, -1, 0)", round, flat)
              q-icon(name="keyboard_backspace")
            q-btn.invisible.text-dark(round, flat)
              q-icon.flip-vertical(name="photo_size_select_small")
            q-btn.bg-dark(@click="mobileCellMove(mobileSelectedCell, 1, 0)", round, flat)
              q-icon.rotate-180(name="keyboard_backspace")
          .text-center
            q-btn.bg-dark(@click="mobileCellMove(mobileSelectedCell, 0, 1)", round, flat)
              q-icon.rotate-270(name="keyboard_backspace")

        //----- resize
        div(v-if="editMode === 'resize'", :class="{'q-pa-md' : editMode}")
          .text-center
            q-btn.bg-dark(@click="mobileCellResize(mobileSelectedCell, 0, -1)", round, flat)
              q-icon(name="remove")
          .text-center
            q-btn.bg-dark(@click="mobileCellResize(mobileSelectedCell, -1, 0)", round, flat)
              q-icon(name="remove")
            q-btn.invisible.text-dark(round, flat)
              q-icon.rotate-45(name="zoom_out_map")
            q-btn.bg-dark(@click="mobileCellResize(mobileSelectedCell, 1, 0)", round, flat)
              q-icon(name="add")
          .text-center
            q-btn.bg-dark(@click="mobileCellResize(mobileSelectedCell, 0, 1)", round, flat)
              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
              q-btn.bg-dark.text-white.q-mx-sm(@click="event => {handleCellContextMenuDelete(event, mobileSelectedCell)}", flat, round)
                q-icon(name="check")

Anton's avatar
Intial  
Anton committed
166
167
168
169
</template>

<script>
  import Cell from './Cell'
170
171
  import { userHasFeature } from 'mbjs-quasar/src/lib'
  import { mapGetters } from 'vuex'
172
  import GridEditorEditingCellsMobile from './/GridEditorEditingCellsMobile'
Anton's avatar
Intial  
Anton committed
173
174
175
176
177
178

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

  export default {
    components: {
179
180
      Cell,
      GridEditorEditingCellsMobile
Anton's avatar
Intial  
Anton committed
181
    },
182
    props: ['gridUuid', 'tabsAreOpen'],
Anton's avatar
Intial  
Anton committed
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
    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,
208
        annotations: undefined,
209
        tmpObjects: [],
210
        annotationUIStates: {},
Anton's avatar
Anton committed
211
        gridDimensions: { gridWidth: 0, gridHeight: 0, cellWidth: 0, cellHeight: 0 },
Anton's avatar
Intial  
Anton committed
212
        contextMenuClickPosition: {},
213
214
        resizingGrid: false,
        mobileSelectedCell: undefined
Anton's avatar
Intial  
Anton committed
215
216
      }
    },
217
218
    computed: {
      ...mapGetters({
219
        user: 'auth/getUserState',
Mathias Bär's avatar
Mathias Bär committed
220
221
        isMobile: 'globalSettings/getIsMobile',
        // editingCells: 'mosys/getEditingCells'
222
        showEditingCells: 'mosys/getShowEditingCells',
223
224
        scrollPositionCache: 'mosys/getScrollPositionCache',
        editMode: 'mosys/getEditMode'
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
      }),
      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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
      },
      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>")`
        }
264
265
      }
    },
Anton's avatar
Intial  
Anton committed
266
267
    async mounted () {
      await this.fetchData()
268
      this.resetScrollPosition()
Anton's avatar
Intial  
Anton committed
269
270
    },
    watch: {
Anton's avatar
Anton committed
271
      annotations () {
272
        this.updateAnnotationUIStates()
Anton's avatar
Intial  
Anton committed
273
274
275
276
277
278
      },
      gridMetadata () {
        this.updateGridDimensions()
      },
      async gridUuid () {
        await this.fetchData()
Mathias Bär's avatar
Mathias Bär committed
279
280
      },
      showEditingCells (val) {
281
        // console.log('show editing cells', val)
Mathias Bär's avatar
Mathias Bär committed
282
283
284
        if (val === false) {
          this.updateAnnotationUIStates()
        }
285
286
287
      },
      tabsAreOpen () {
        this.resetScrollPosition()
Anton's avatar
Intial  
Anton committed
288
289
290
      }
    },
    methods: {
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
      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)
      },
335
336
337
338
      //
      // DATA
      //

Anton's avatar
Intial  
Anton committed
339
340
341
      async fetchData () {
        if (this.gridUuid) {
          this.grid = await this.$store.dispatch('maps/get', this.gridUuid)
342
343
344
345
346
347
348
349
          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
350
          this.updateGridDimensions()
351
352
353
354
355
356
          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
357
358
        }
      },
359
360
361
      async updateGridMetadataStore () {
        await this.$store.dispatch('maps/patch', [this.grid.id, { config: this.grid.config }])
        this.updateGridDimensions()
Anton's avatar
Intial  
Anton committed
362
      },
363
364
365
366
367
      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
368
369
          // return _this.cells.find(c => c._uuid === k)
          return _this.annotations.find(c => c._uuid === k)
370
371
        })
        this.$store.commit('mosys/setSelectedCells', selectedCells)
Anton's avatar
Intial  
Anton committed
372
      },
Mathias Bär's avatar
Mathias Bär committed
373
374
375
376
377
378
379
380
      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)
        })
381
382
383
384
385
386
387
        /*
        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
388
389
        this.$store.commit('mosys/setEditingCells', editingCells)
      },
390
391
392
393
394
      updateAnnotationUIStates () {
        let newAnnotationUIStates = {}
        this.annotations.forEach(a => {
          newAnnotationUIStates[a._uuid] = {
            selected: false,
Mathias Bär's avatar
Mathias Bär committed
395
            editing: false,
396
            beingResized: false,
Mathias Bär's avatar
Mathias Bär committed
397
            beginDragged: false,
398
            annotation: a
Anton's avatar
Intial  
Anton committed
399
          }
400
401
        })
        this.annotationUIStates = newAnnotationUIStates
Anton's avatar
Intial  
Anton committed
402
        this.updateSelectedCells()
Mathias Bär's avatar
Mathias Bär committed
403
        this.updateEditingCells()
404
      },
405
406
407
408
409

      //
      // GRID DRAG & DROP HANDLERS
      //

410
      async handleGridDragOver (event) {
Anton's avatar
Intial  
Anton committed
411
412
413
        let _this = this
        if (this.resizingGrid) {
          const position = this.getGridPositionForEvent(event)
414
415
          this.grid.config.ratio = position.ox / (position.oy * 1.0)
          await this.updateGridMetadataStore()
Anton's avatar
Intial  
Anton committed
416
417
        }
        else {
418
419
          let annotation = this.annotations.filter(annotation => {
            if (!_this.annotationUIStates[annotation._uuid]) return false
Mathias Bär's avatar
Mathias Bär committed
420
            return _this.annotationUIStates[annotation._uuid].beingDragged ||
421
              _this.annotationUIStates[annotation._uuid].beingResized
Anton's avatar
Intial  
Anton committed
422
423
          }).shift()
          let offset, position
424
425
426
427
          if (!annotation) {
            annotation = {
              target: this.grid.get2DArea([1, 1], [1, 1])
            }
Anton's avatar
Intial  
Anton committed
428
429
430
            position = this.getGridPositionForEvent(event)
          }
          else {
431
            offset = this.annotationUIStates[annotation._uuid].draggingOffset
Anton's avatar
Intial  
Anton committed
432
433
            position = this.getGridPositionForEvent(event, offset)
          }
434
435
          if (!this.tmpObjects.length) this.tmpObjects.push(annotation)
          const parsed = annotation.target.selector.parse()
Anton's avatar
Intial  
Anton committed
436
          if (event.dataTransfer.types.includes('text/plain')) {
437
438
            parsed.xywh[0] = position.x
            parsed.xywh[1] = position.y
Anton's avatar
Intial  
Anton committed
439
440
441
            event.preventDefault()
          }
          else {
442
443
            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
444
          }
445
          annotation.target.selector.value = parsed
Anton's avatar
Intial  
Anton committed
446
447
448
        }
      },
      handleGridDragEnd () {
449
        this.tmpObjects = []
Anton's avatar
Intial  
Anton committed
450
      },
451
452
453
454
      async handleGridDrop (event) {
        let dropData = event.dataTransfer.getData('text/plain')
        if (dropData) {
          dropData = JSON.parse(dropData)
455
          let annotation = this.annotations.find(a => a.id === dropData.id)
456
457
458
459
460
          const { x, y } = this.getGridPositionForEvent(
            event,
            annotation ? this.annotationUIStates[annotation._uuid].draggingOffset : undefined
          )
          if (annotation) {
461
462
463
            const
              parsed = annotation.target.selector.parse(),
              target = this.grid.get2DArea([x, y], parsed.xywh.slice(2))
464
465
466
467
468
469
            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
470
471
          }
          else {
472
473
474
475
476
477
478
479
480
481
482
            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
                }
              },
483
              target: this.grid.get2DArea([x, y], [1, 1])
484
            })
485
            this.annotations.push(annotation)
Anton's avatar
Anton committed
486
            this.updateAnnotationUIStates()
Anton's avatar
Intial  
Anton committed
487
          }
488

489
          this.tmpObjects = []
Anton's avatar
Intial  
Anton committed
490
491
492
          event.preventDefault()
        }
      },
493
494
495
496

      //
      // CELL DRAG & DROP HANDLERS
      //
Mathias Bär's avatar
Mathias Bär committed
497
498
499
      handleCellEditClick (event, cell) {
        this.annotationUIStates[cell._uuid].editing = !this.annotationUIStates[cell._uuid].editing
        this.updateEditingCells()
500
        this.$root.$emit('mosys_saveScrollPosition')
Mathias Bär's avatar
Mathias Bär committed
501
      },
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
      handleCellClick (event, cell) {
        this.annotationUIStates[cell._uuid].selected = !this.annotationUIStates[cell._uuid].selected
        this.updateSelectedCells()
      },
      handleCellDragStart (event, annotation) {
        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
517
          this.annotationUIStates[annotation._uuid].beingDragged = true
518
519
520
521
522
523
524
525
526
        }
        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
527
        this.updateEditingCells()
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
        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
571
572
        this.contextMenuClickPosition = this.getGridPositionForEvent(event)
      },
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
      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
590
        }
591
592
593
594
595
596
597
598
599
        catch (e) { /* dialog canceled */ }
      },

      //
      // GRID CONTEXT MENU
      //

      handleGridContextMenu (event) {
        this.contextMenuClickPosition = this.getGridPositionForEvent(event)
Anton's avatar
Intial  
Anton committed
600
601
602
      },
      async handleGridContextMenuInsertColumnLeft () {
        let position = this.contextMenuClickPosition
603
604
605
606
607
608
609
610
        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
611
612
          }
        }
613
        this.grid.config.columns += 1
Anton's avatar
Intial  
Anton committed
614
615
616
617
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuDeleteColumn () {
        let position = this.contextMenuClickPosition
618
619
620
621
622
623
624
625
        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
626
627
          }
        }
628
        this.grid.config.columns -= 1
Anton's avatar
Intial  
Anton committed
629
630
631
632
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuInsertRowAbove () {
        let position = this.contextMenuClickPosition
633
634
635
636
637
638
639
640
        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
641
642
          }
        }
643
        this.grid.config.rows += 1
Anton's avatar
Intial  
Anton committed
644
645
646
647
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuDeleteRow () {
        let position = this.contextMenuClickPosition
648
649
650
651
652
653
654
655
        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
656
657
          }
        }
658
        this.grid.config.rows -= 1
Anton's avatar
Intial  
Anton committed
659
660
        await this.updateGridMetadataStore()
      },
661
662
663
664
665

      //
      // NAVIGATION
      //

Anton's avatar
Intial  
Anton committed
666
      handleGridButtonClickEdit () {
667
        this.$store.commit('mosys/toggleSources')
Anton's avatar
Intial  
Anton committed
668
      },
669
670
671
672
673

      //
      // GRID HELPERS
      //

Anton's avatar
Anton committed
674
675
      getGridPositionForEvent (event, offset = { x: 0, y: 0 }) {
        offset = { x: 0, y: 0 } // TODO: remove quick fix
Anton's avatar
Intial  
Anton committed
676
677
678
679
680
        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
681
        return { x: x, y: y, ox: ox, oy: oy }
Anton's avatar
Intial  
Anton committed
682
      },
683
684
685
686
      updateGridDimensions (size) {
        if (!this.grid || !this.grid.config) return

        let elWidth = size ? size.width : this.$el.offsetWidth
687
        // let elHeight = size ? size.height : this.$el.offsetHeight
688
        let cellSizeRatio = this.grid.config.ratio
689
690
        // let gridHeight = elHeight
        let gridHeight = this.$el.offsetHeight
691
        let cellHeight = gridHeight / this.grid.config.rows
Anton's avatar
Intial  
Anton committed
692
        let cellWidth = elWidth / Math.round(elWidth / (cellHeight * cellSizeRatio))
693
        let gridWidth = cellWidth * this.grid.config.columns
Anton's avatar
Intial  
Anton committed
694
        let cellsPerWidth = elWidth / cellWidth
695
        let cellWidthMini = elWidth / this.grid.config.columns
Anton's avatar
Intial  
Anton committed
696
697
698
699
700
701
702
703
704
705
706
707
708
        let gridHeightMini = cellWidthMini / cellSizeRatio
        this.gridDimensions = {
          full: {
            width: gridWidth,
            height: gridHeight,
            cell: {
              width: cellWidth,
              height: cellHeight
            },
            cells_per_width: cellsPerWidth
          },
          mini: {
            width: elWidth,
709
            height: gridHeightMini * this.grid.config.rows,
Anton's avatar
Intial  
Anton committed
710
711
712
713
714
715
716
            cell: {
              width: cellWidthMini,
              height: gridHeightMini
            }
          }
        }
      },
717
718
      getAnnotationStyle (annotation) {
        const parsed = annotation.target.selector.parse()
Anton's avatar
Intial  
Anton committed
719
        return {
720
721
722
723
          '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
724
        }
Mathias Bär's avatar
Mathias Bär committed
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
      },
      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 {}
742
743
744
745
      },
      resetScrollPosition () {
        console.log('reset', this.scrollPositionCache, this.$el.scrollLeft)
        this.$el.scrollLeft = this.scrollPositionCache
Anton's avatar
Intial  
Anton committed
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
      }
    }
  }
</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
773
774
775
776
      .edit-button
        display: block

    &.editing
777
      background-color lightpink
Mathias Bär's avatar
Mathias Bär committed
778
779
780
781

      .edit-button
        display: block

Anton's avatar
Intial  
Anton committed
782
783
784
    &.selected
      background-color lightpink

Mathias Bär's avatar
Mathias Bär committed
785
786
787
    .edit-button
      display: none

Anton's avatar
Intial  
Anton committed
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
    .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

803
804
805
806
807
808
809
810
811
  .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
812
</style>