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>