GridEditor.vue 20.6 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
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

    div.cell-grid(
      @dragenter="handleGridDragOver",
      @dragover="handleGridDragOver",
      @dragleave="handleGridDragEnd",
      @drop="handleGridDrop",
      @contextmenu="handleGridContextMenu",
      :style="gridStyle")

      q-context-menu(ref="gridmenu")
        q-list(link, separator, no-border, style="min-width: 150px; max-height: 300px;")
          q-item(
            v-for="action in gridContextMenuActions",
            :key="action.label",
            v-close-overlay,
            @click.native="event => {action.handler(event)}")
              q-item-main(:label="action.label")

      template(v-if="!resizingGrid")

25
        template(v-for="(annotation, index) in annotations")
Anton's avatar
Intial  
Anton committed
26
          .cell-item(
27
            v-if="!annotationUIStates[annotation._uuid] || !annotationUIStates[annotation._uuid].beingDragged",
Anton's avatar
Intial  
Anton committed
28
            draggable="true",
29
30
            @dragstart="event => {handleCellDragStart(event, annotation)}",
            @dragend="event => {handleCellDragEnd(event, annotation)}",
Anton's avatar
Intial  
Anton committed
31
            @contextmenu="handleCellContextMenu",
32
33
34
            :style="getAnnotationStyle(annotation)",
            @click.prevent="event => {handleCellClick(event, annotation)}",
            :class="{selected: annotationUIStates[annotation._uuid] ? annotationUIStates[annotation._uuid].selected : false}",
Anton's avatar
Intial  
Anton committed
35
            :key="`cell-${index}`")
36
              cell(:annotation="annotation", preview)
Anton's avatar
Intial  
Anton committed
37
38
              div.cell-item-resize-handle(
                draggable="true",
39
40
41
                @dragstart="event => {handleCellResizerDragStart(event, annotation)}",
                @dragend="event => {handleCellResizerDragEnd(event, annotation)}",
                @dragexit="event => {handleCellResizerDragEnd(event, annotation)}")
Anton's avatar
Intial  
Anton committed
42
43
44
45
46
47
48
49
                  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,
50
                    @click.native="event => {action.handler(event, annotation)}")
Anton's avatar
Intial  
Anton committed
51
52
                      q-item-main(:label="action.label")

53
        template(v-for="(tmpCell, index) in tmpObjects")
54
          .cell-item.cell-item-tmp(:style="getAnnotationStyle(tmpCell)", :key="`cell-tmp-${index}`")
Anton's avatar
Intial  
Anton committed
55
56
57
            cell(:cell="tmpCell")

      template(v-else)
58
        .cell-item(:style="getAnnotationStyle({x:0,y:0,width:1,height:1})", key="cell-grid-resizer")
Anton's avatar
Intial  
Anton committed
59
60
61
62
63
64
65
          div.cell-item-resize-handle(
            draggable="true",
            @dragstart="event => {handleGridResizerDragStart(event)}",
            @dragend="event => {handleGridResizerDragEnd(event)}",
            @dragexit="event => {handleGridResizerDragEnd(event)}")
              q-icon(name="network cell")

66
      div.fixed-top-right(style="right:18px; top:68px", v-if="!$store.state.mosys.showSources")
Anton's avatar
Intial  
Anton committed
67
68
        q-btn(round, color="primary", small, @click="handleGridButtonClickEdit", style="margin-right: 0.5em")
          q-icon(name="add")
69
        q-btn(round, color="primary", small, @click="$router.push(`/mosys/grids/${$route.params.uuid}`)")
Anton's avatar
Intial  
Anton committed
70
71
72
73
74
75
          q-icon(name="remove red eye")

</template>

<script>
  import Cell from './Cell'
76
77
  import { userHasFeature } from 'mbjs-quasar/src/lib'
  import { mapGetters } from 'vuex'
Anton's avatar
Intial  
Anton committed
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

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

  export default {
    components: {
      Cell
    },
    props: ['gridUuid'],
    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,
112
        annotations: undefined,
113
        tmpObjects: [],
114
        annotationUIStates: {},
Anton's avatar
Anton committed
115
        gridDimensions: { gridWidth: 0, gridHeight: 0, cellWidth: 0, cellHeight: 0 },
Anton's avatar
Intial  
Anton committed
116
117
118
119
        contextMenuClickPosition: {},
        resizingGrid: false
      }
    },
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
    computed: {
      ...mapGetters({
        user: 'auth/getUserState'
      }),
      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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
      },
      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>")`
        }
162
163
      }
    },
Anton's avatar
Intial  
Anton committed
164
165
166
167
    async mounted () {
      await this.fetchData()
    },
    watch: {
Anton's avatar
Anton committed
168
      annotations () {
169
        this.updateAnnotationUIStates()
Anton's avatar
Intial  
Anton committed
170
171
172
173
174
175
176
177
178
      },
      gridMetadata () {
        this.updateGridDimensions()
      },
      async gridUuid () {
        await this.fetchData()
      }
    },
    methods: {
179
180
181
182
      //
      // DATA
      //

Anton's avatar
Intial  
Anton committed
183
184
185
      async fetchData () {
        if (this.gridUuid) {
          this.grid = await this.$store.dispatch('maps/get', this.gridUuid)
186
187
188
189
190
191
192
193
          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
194
          this.updateGridDimensions()
195
196
197
198
199
200
          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
201
202
        }
      },
203
204
205
      async updateGridMetadataStore () {
        await this.$store.dispatch('maps/patch', [this.grid.id, { config: this.grid.config }])
        this.updateGridDimensions()
Anton's avatar
Intial  
Anton committed
206
      },
207
208
209
210
211
212
213
214
      updateSelectedCells () {
        const _this = this
        let selectedCells = Object.keys(this.annotationUIStates).filter(k => {
          return _this.annotationUIStates[k].selected
        }).map(k => {
          return _this.cells.find(c => c._uuid === k)
        })
        this.$store.commit('mosys/setSelectedCells', selectedCells)
Anton's avatar
Intial  
Anton committed
215
      },
216
217
218
219
220
221
222
      updateAnnotationUIStates () {
        let newAnnotationUIStates = {}
        this.annotations.forEach(a => {
          newAnnotationUIStates[a._uuid] = {
            selected: false,
            beingResized: false,
            annotation: a
Anton's avatar
Intial  
Anton committed
223
          }
224
225
        })
        this.annotationUIStates = newAnnotationUIStates
Anton's avatar
Intial  
Anton committed
226
        this.updateSelectedCells()
227
      },
228
229
230
231
232

      //
      // GRID DRAG & DROP HANDLERS
      //

233
      async handleGridDragOver (event) {
Anton's avatar
Intial  
Anton committed
234
235
236
        let _this = this
        if (this.resizingGrid) {
          const position = this.getGridPositionForEvent(event)
237
238
          this.grid.config.ratio = position.ox / (position.oy * 1.0)
          await this.updateGridMetadataStore()
Anton's avatar
Intial  
Anton committed
239
240
        }
        else {
241
242
243
244
          let annotation = this.annotations.filter(annotation => {
            if (!_this.annotationUIStates[annotation._uuid]) return false
            return _this.annotationUIStates[annotation._uuid].beginDragged ||
              _this.annotationUIStates[annotation._uuid].beingResized
Anton's avatar
Intial  
Anton committed
245
246
          }).shift()
          let offset, position
247
248
249
250
          if (!annotation) {
            annotation = {
              target: this.grid.get2DArea([1, 1], [1, 1])
            }
Anton's avatar
Intial  
Anton committed
251
252
253
            position = this.getGridPositionForEvent(event)
          }
          else {
254
            offset = this.annotationUIStates[annotation._uuid].draggingOffset
Anton's avatar
Intial  
Anton committed
255
256
            position = this.getGridPositionForEvent(event, offset)
          }
257
258
          if (!this.tmpObjects.length) this.tmpObjects.push(annotation)
          const parsed = annotation.target.selector.parse()
Anton's avatar
Intial  
Anton committed
259
          if (event.dataTransfer.types.includes('text/plain')) {
260
261
            parsed.xywh[0] = position.x
            parsed.xywh[1] = position.y
Anton's avatar
Intial  
Anton committed
262
263
264
            event.preventDefault()
          }
          else {
265
266
            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
267
          }
268
          annotation.target.selector.value = parsed
Anton's avatar
Intial  
Anton committed
269
270
271
        }
      },
      handleGridDragEnd () {
272
        this.tmpObjects = []
Anton's avatar
Intial  
Anton committed
273
      },
274
275
276
277
      async handleGridDrop (event) {
        let dropData = event.dataTransfer.getData('text/plain')
        if (dropData) {
          dropData = JSON.parse(dropData)
278
          let annotation = this.annotations.find(a => a.id === dropData.id)
279
280
281
282
283
          const { x, y } = this.getGridPositionForEvent(
            event,
            annotation ? this.annotationUIStates[annotation._uuid].draggingOffset : undefined
          )
          if (annotation) {
284
285
286
            const
              parsed = annotation.target.selector.parse(),
              target = this.grid.get2DArea([x, y], parsed.xywh.slice(2))
287
288
289
290
291
292
            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
293
294
          }
          else {
295
296
297
298
299
300
301
302
303
304
305
            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
                }
              },
306
              target: this.grid.get2DArea([x, y], [1, 1])
307
            })
308
            this.annotations.push(annotation)
Anton's avatar
Anton committed
309
            this.updateAnnotationUIStates()
Anton's avatar
Intial  
Anton committed
310
          }
311

312
          this.tmpObjects = []
Anton's avatar
Intial  
Anton committed
313
314
315
          event.preventDefault()
        }
      },
316
317
318
319
320
321
322
323
324
325
326
327
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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388

      //
      // CELL DRAG & DROP HANDLERS
      //

      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
          this.annotationUIStates[annotation._uuid].beginDragged = true
        }
        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()
        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
389
390
        this.contextMenuClickPosition = this.getGridPositionForEvent(event)
      },
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
      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
408
        }
409
410
411
412
413
414
415
416
417
        catch (e) { /* dialog canceled */ }
      },

      //
      // GRID CONTEXT MENU
      //

      handleGridContextMenu (event) {
        this.contextMenuClickPosition = this.getGridPositionForEvent(event)
Anton's avatar
Intial  
Anton committed
418
419
420
      },
      async handleGridContextMenuInsertColumnLeft () {
        let position = this.contextMenuClickPosition
421
422
423
424
425
426
427
428
        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
429
430
          }
        }
431
        this.grid.config.columns += 1
Anton's avatar
Intial  
Anton committed
432
433
434
435
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuDeleteColumn () {
        let position = this.contextMenuClickPosition
436
437
438
439
440
441
442
443
        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
444
445
          }
        }
446
        this.grid.config.columns -= 1
Anton's avatar
Intial  
Anton committed
447
448
449
450
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuInsertRowAbove () {
        let position = this.contextMenuClickPosition
451
452
453
454
455
456
457
458
        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
459
460
          }
        }
461
        this.grid.config.rows += 1
Anton's avatar
Intial  
Anton committed
462
463
464
465
        await this.updateGridMetadataStore()
      },
      async handleGridContextMenuDeleteRow () {
        let position = this.contextMenuClickPosition
466
467
468
469
470
471
472
473
        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
474
475
          }
        }
476
        this.grid.config.rows -= 1
Anton's avatar
Intial  
Anton committed
477
478
        await this.updateGridMetadataStore()
      },
479
480
481
482
483

      //
      // NAVIGATION
      //

Anton's avatar
Intial  
Anton committed
484
      handleGridButtonClickEdit () {
485
        this.$store.commit('mosys/toggleSources')
Anton's avatar
Intial  
Anton committed
486
      },
487
488
489
490
491

      //
      // GRID HELPERS
      //

Anton's avatar
Anton committed
492
493
      getGridPositionForEvent (event, offset = { x: 0, y: 0 }) {
        offset = { x: 0, y: 0 } // TODO: remove quick fix
Anton's avatar
Intial  
Anton committed
494
495
496
497
498
        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
499
        return { x: x, y: y, ox: ox, oy: oy }
Anton's avatar
Intial  
Anton committed
500
      },
501
502
503
504
      updateGridDimensions (size) {
        if (!this.grid || !this.grid.config) return

        let elWidth = size ? size.width : this.$el.offsetWidth
505
        // let elHeight = size ? size.height : this.$el.offsetHeight
506
        let cellSizeRatio = this.grid.config.ratio
507
508
        // let gridHeight = elHeight
        let gridHeight = this.$el.offsetHeight
509
        let cellHeight = gridHeight / this.grid.config.rows
Anton's avatar
Intial  
Anton committed
510
        let cellWidth = elWidth / Math.round(elWidth / (cellHeight * cellSizeRatio))
511
        let gridWidth = cellWidth * this.grid.config.columns
Anton's avatar
Intial  
Anton committed
512
        let cellsPerWidth = elWidth / cellWidth
513
        let cellWidthMini = elWidth / this.grid.config.columns
Anton's avatar
Intial  
Anton committed
514
515
516
517
518
519
520
521
522
523
524
525
526
        let gridHeightMini = cellWidthMini / cellSizeRatio
        this.gridDimensions = {
          full: {
            width: gridWidth,
            height: gridHeight,
            cell: {
              width: cellWidth,
              height: cellHeight
            },
            cells_per_width: cellsPerWidth
          },
          mini: {
            width: elWidth,
527
            height: gridHeightMini * this.grid.config.rows,
Anton's avatar
Intial  
Anton committed
528
529
530
531
532
533
534
            cell: {
              width: cellWidthMini,
              height: gridHeightMini
            }
          }
        }
      },
535
536
      getAnnotationStyle (annotation) {
        const parsed = annotation.target.selector.parse()
Anton's avatar
Intial  
Anton committed
537
        return {
538
539
540
541
          '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
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
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
        }
      }
    }
  }
</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

    &.selected
      background-color lightpink

    .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

</style>