- save & load
- key navigation - ui improvements
This commit is contained in:
		@ -1,30 +1,35 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="playlist-editor">
 | 
			
		||||
        <slot name="top" :set="set" :columns="columns" :items="items"/>
 | 
			
		||||
        <div class="tabs">
 | 
			
		||||
            <ul>
 | 
			
		||||
                <li :class="{'is-active': mode == Modes.Text}"
 | 
			
		||||
                        @click="mode = Modes.Text">
 | 
			
		||||
                    <a>
 | 
			
		||||
                        <span class="icon is-small">
 | 
			
		||||
                            <i class="fa fa-pencil"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                        Texte
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li :class="{'is-active': mode == Modes.List}"
 | 
			
		||||
                        @click="mode = Modes.List">
 | 
			
		||||
                    <a>
 | 
			
		||||
                        <span class="icon is-small">
 | 
			
		||||
                            <i class="fa fa-list"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                        Liste
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        <div class="columns">
 | 
			
		||||
            <div class="column">
 | 
			
		||||
                <slot name="title" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="column has-text-right">
 | 
			
		||||
                <div class="float-right field has-addons">
 | 
			
		||||
                    <p class="control">
 | 
			
		||||
                        <a :class="['button','p-2', mode == Modes.Text ? 'is-primary' : 'is-light']"
 | 
			
		||||
                                @click="mode = Modes.Text">
 | 
			
		||||
                            <span class="icon is-small">
 | 
			
		||||
                                <i class="fa fa-pencil"></i>
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <span>Texte</span>
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p class="control">
 | 
			
		||||
                        <a :class="['button','p-2', mode == Modes.List ? 'is-primary' : 'is-light']"
 | 
			
		||||
                                @click="mode = Modes.List">
 | 
			
		||||
                            <span class="icon is-small">
 | 
			
		||||
                                <i class="fa fa-list"></i>
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <span>Liste</span>
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <slot name="top" :set="set" :columns="columns" :items="items"/>
 | 
			
		||||
        <section class="page" v-show="mode == Modes.Text">
 | 
			
		||||
            <textarea ref="textarea" class="is-fullwidth" style="height: 10em;"
 | 
			
		||||
            <textarea ref="textarea" class="is-fullwidth" rows="20"
 | 
			
		||||
                @change="updateList"
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
@ -36,7 +41,7 @@
 | 
			
		||||
                    <table class="table is-bordered is-inline-block"
 | 
			
		||||
                            style="vertical-align: middle">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <a-row :columns="columns" :item="FormatLabels"
 | 
			
		||||
                            <a-row :cell="{columns}" :item="FormatLabels"
 | 
			
		||||
                                @move="formatMove" :orderable="true">
 | 
			
		||||
                            </a-row>
 | 
			
		||||
                        </tr>
 | 
			
		||||
@ -93,22 +98,25 @@ const FormatLabels = {
 | 
			
		||||
export default {
 | 
			
		||||
    components: { ARow, ARows },
 | 
			
		||||
    props: {
 | 
			
		||||
        dataEl: String,
 | 
			
		||||
        dataPrefix: String,
 | 
			
		||||
        listClass: String,
 | 
			
		||||
        itemClass: String,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            dataEl: String,
 | 
			
		||||
            Modes: Modes,
 | 
			
		||||
            FormatLabels: FormatLabels,
 | 
			
		||||
            mode: Modes.Text,
 | 
			
		||||
            set: new Set(Track),
 | 
			
		||||
            columns: ['artist', 'title', 'tags', 'album', 'year'],
 | 
			
		||||
            extraData: {},
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
       
 | 
			
		||||
        items() {
 | 
			
		||||
            return this.set.items
 | 
			
		||||
        },
 | 
			
		||||
@ -194,23 +202,37 @@ export default {
 | 
			
		||||
            return lines.join('\n')
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        _data_key(key) {
 | 
			
		||||
            key = key.slice(this.dataPrefix.length)
 | 
			
		||||
            try {
 | 
			
		||||
                var [index, attr] = key.split('-', 1)
 | 
			
		||||
                return [Number(index), attr]
 | 
			
		||||
            }
 | 
			
		||||
            catch(err) {
 | 
			
		||||
                return [null, key]
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        /**
 | 
			
		||||
         * Load initial data
 | 
			
		||||
         */
 | 
			
		||||
        loadData({items=[], errors, fieldErrors, ...data}) {
 | 
			
		||||
            for(var item of items)
 | 
			
		||||
                this.set.push(item)
 | 
			
		||||
        },
 | 
			
		||||
        loadData({items=[]}) {
 | 
			
		||||
            for(var index in items)
 | 
			
		||||
                this.set.push(items[index])
 | 
			
		||||
            this.updateInput()
 | 
			
		||||
         },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
        if(this.dataEl) {
 | 
			
		||||
            const el = document.getElementById(this.dataEl)
 | 
			
		||||
            if(el) {
 | 
			
		||||
                const data = JSON.parse(el.textContext)
 | 
			
		||||
                loadData(data)
 | 
			
		||||
                const data = JSON.parse(el.textContent)
 | 
			
		||||
                this.loadData(data)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        this.mode = (this.items) ? Modes.List : Modes.Text
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -1,21 +1,22 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <tr>
 | 
			
		||||
        <slot name="head" :item="item" :row="index"/>
 | 
			
		||||
        <slot name="head" :item="item" :row="row"/>
 | 
			
		||||
        <template v-for="(attr,col) in columns" :key="col">
 | 
			
		||||
            <td :class="['cell', 'cell-' + attr]" :data-index="col"
 | 
			
		||||
            <td :class="['cell', 'cell-' + attr]" :data-col="col"
 | 
			
		||||
                    :draggable="orderable"
 | 
			
		||||
                    @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
 | 
			
		||||
                <slot :name="attr" :item="item" :row="index" :col="col"
 | 
			
		||||
                <slot :name="attr" :item="item" :cell="cells[col]"
 | 
			
		||||
                        :data="itemData" :attr="attr" :emit="cellEmit"
 | 
			
		||||
                        :value="itemData && itemData[attr]">
 | 
			
		||||
                    {{ itemData && itemData[attr] }}
 | 
			
		||||
                </slot>
 | 
			
		||||
            </td>
 | 
			
		||||
        </template>
 | 
			
		||||
        <slot name="tail" :item="item" :row="index"/>
 | 
			
		||||
        <slot name="tail" :item="item" :row="cell.row"/>
 | 
			
		||||
    </tr>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import {isReactive, toRefs} from 'vue'
 | 
			
		||||
import Model from '../model'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
@ -23,35 +24,48 @@ export default {
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
        item: Object,
 | 
			
		||||
        index: Number,
 | 
			
		||||
        columns: Array,
 | 
			
		||||
        cell: Object,
 | 
			
		||||
        orderable: {type: Boolean, default: false},
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        row() { return this.cell.row || 0 },
 | 
			
		||||
        columns() { return this.cell.columns },
 | 
			
		||||
        
 | 
			
		||||
        itemData() {
 | 
			
		||||
            return this.item instanceof Model ? this.item.data : this.item;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        cells() {
 | 
			
		||||
            const cell = isReactive(this.cell) && toRefs(this.cell) || this.cell
 | 
			
		||||
            const cells = []
 | 
			
		||||
            for(var col in this.columns)
 | 
			
		||||
                cells.push({...cell, col: Number(col)})
 | 
			
		||||
            return cells
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        cellEls() {
 | 
			
		||||
            return [...this.$el.querySelectorAll('td')].filter(x => x.dataset.col)
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        /// Emit a 'cell' event.
 | 
			
		||||
        /// Event data: `{index, name, data, item, attr}`
 | 
			
		||||
        /// Event data: `{name, data, item, attr}`
 | 
			
		||||
        ///
 | 
			
		||||
        /// @param {Number} col: cell column's index
 | 
			
		||||
        /// @param {String} name: cell's event name
 | 
			
		||||
        /// @param {} data: cell's event data
 | 
			
		||||
        cellEmit(name, col, data) {
 | 
			
		||||
        cellEmit(name, cell, data) {
 | 
			
		||||
            this.$emit('cell', {
 | 
			
		||||
                name, col, data,
 | 
			
		||||
                name, cell, data,
 | 
			
		||||
                item: this.item,
 | 
			
		||||
                attr: this.columns[col],
 | 
			
		||||
            })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onDragStart(ev) {
 | 
			
		||||
            const dataset = ev.target.dataset;
 | 
			
		||||
            const data = `cell:${dataset.index}`
 | 
			
		||||
            const data = `cell:${dataset.col}`
 | 
			
		||||
            ev.dataTransfer.setData("text/cell", data)
 | 
			
		||||
            ev.dataTransfer.dropEffect = 'move'
 | 
			
		||||
        },
 | 
			
		||||
@ -69,9 +83,27 @@ export default {
 | 
			
		||||
            ev.preventDefault()
 | 
			
		||||
            this.$emit('move', {
 | 
			
		||||
                from: Number(data.slice(5)),
 | 
			
		||||
                to: Number(ev.target.dataset.index),
 | 
			
		||||
                to: Number(ev.target.dataset.col),
 | 
			
		||||
            })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        focus(col, from) {
 | 
			
		||||
            if(from)
 | 
			
		||||
                col += from.col
 | 
			
		||||
 | 
			
		||||
            const target = this.cellEls[col]
 | 
			
		||||
            if(!target)
 | 
			
		||||
                return
 | 
			
		||||
            const control = target.querySelector('input') ||
 | 
			
		||||
                            target.querySelector('button') ||
 | 
			
		||||
                            target.querySelector('select') ||
 | 
			
		||||
                            target.querySelector('a');
 | 
			
		||||
            control && control.focus()
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.$el.__row = this
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,18 +10,26 @@
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
            <slot name="head"/>
 | 
			
		||||
            <template v-for="(item,index) in items" :key="index">
 | 
			
		||||
                <a-row :item="item" :index="index" :columns="columns" :data-index="index"
 | 
			
		||||
            <template v-for="(item,row) in items" :key="row">
 | 
			
		||||
                <!-- data-index comes from AList component drag & drop -->
 | 
			
		||||
                <a-row :item="item" :cell="{row, columns}" :data-index="row"
 | 
			
		||||
                        :draggable="orderable"
 | 
			
		||||
                        @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
 | 
			
		||||
                        @cell="onCellEvent(index, $event)">
 | 
			
		||||
                    <template v-for="[name,slot] of rowSlots" :key="slot" v-slot:[slot]="data">
 | 
			
		||||
                        <slot :name="name" v-bind="data"/>
 | 
			
		||||
                        <template v-if="slot == 'head' || slot == 'tail'">
 | 
			
		||||
                            <slot :name="name" v-bind="data"/>
 | 
			
		||||
                        </template>
 | 
			
		||||
                        <template v-else>
 | 
			
		||||
                            <div @keydown.capture.ctrl="onControlKey($event, data.cell)">
 | 
			
		||||
                                <slot :name="name" v-bind="data"/>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </a-row>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template v-if="allowCreate">
 | 
			
		||||
                <a-row :item="extraItem" :index="items.length" :columns="columns"
 | 
			
		||||
                <a-row :item="extraItem" :cell="{row:items.length, columns}" 
 | 
			
		||||
                        @keypress.enter.stop.prevent="validateExtraCell">
 | 
			
		||||
                    <template v-for="[name,slot] of rowSlots" :key="slot" v-slot:[slot]="data">
 | 
			
		||||
                        <slot :name="name" v-bind="data"/>
 | 
			
		||||
@ -56,6 +64,17 @@ const Component = {
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        rowCells() {
 | 
			
		||||
            const cells = []
 | 
			
		||||
            for(var row in this.items)
 | 
			
		||||
                cells.push({row, columns: this.columns,})
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        rows() {
 | 
			
		||||
            return [...this.$el.querySelectorAll('tr')].filter(x => x.__row)
 | 
			
		||||
                                                       .map(x => x.__row)
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        rowSlots() {
 | 
			
		||||
            return Object.keys(this.$slots).filter(x => x.startsWith('row-'))
 | 
			
		||||
                                           .map(x => [x, x.slice(4)])
 | 
			
		||||
@ -69,19 +88,61 @@ const Component = {
 | 
			
		||||
            this.set.push(this.extraItem)
 | 
			
		||||
            this.extraItem = new this.set.model()
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onControlKey(event, cell) {
 | 
			
		||||
            switch(event.key) {
 | 
			
		||||
                case "ArrowUp": this.focus(-1, 0, cell)
 | 
			
		||||
                                event.stopPropagation()
 | 
			
		||||
                                event.preventDefault()
 | 
			
		||||
                                break;
 | 
			
		||||
                case "ArrowDown": this.focus(1, 0, cell)
 | 
			
		||||
                                event.stopPropagation()
 | 
			
		||||
                                event.preventDefault()
 | 
			
		||||
                                break;
 | 
			
		||||
                case "ArrowLeft": this.focus(0, -1, cell)
 | 
			
		||||
                                event.stopPropagation()
 | 
			
		||||
                                event.preventDefault()
 | 
			
		||||
                                break;
 | 
			
		||||
                case "ArrowRight": this.focus(0, 1, cell)
 | 
			
		||||
                                event.stopPropagation()
 | 
			
		||||
                                event.preventDefault()
 | 
			
		||||
                                break;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    
 | 
			
		||||
        /// React on 'cell' event, re-emitting it with additional values:
 | 
			
		||||
        /// - `set`: data set
 | 
			
		||||
        /// - `row`: row index
 | 
			
		||||
        ///
 | 
			
		||||
        /// @param {Number} row: row index
 | 
			
		||||
        /// @param {} data: cell's event data
 | 
			
		||||
        /**
 | 
			
		||||
         * React on 'cell' event, re-emitting it with additional values:
 | 
			
		||||
         * - `set`: data set
 | 
			
		||||
         * - `row`: row index
 | 
			
		||||
         *
 | 
			
		||||
         * @param {Number} row: row index
 | 
			
		||||
         * @param {} data: cell's event data
 | 
			
		||||
         */
 | 
			
		||||
        onCellEvent(row, event) {
 | 
			
		||||
            if(event.name == 'focus')
 | 
			
		||||
                this.cellFocus(event.data, event.cell)
 | 
			
		||||
            
 | 
			
		||||
            this.$emit('cell', {
 | 
			
		||||
                ...event, row,
 | 
			
		||||
                set: this.set
 | 
			
		||||
            })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        getCellNode(row, col) {
 | 
			
		||||
            const el = this.$refs[row]
 | 
			
		||||
            return el && el.cellEls(col)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Focus on a cell
 | 
			
		||||
         */
 | 
			
		||||
        focus(row, col, from=null) {
 | 
			
		||||
            if(from)
 | 
			
		||||
                row += from.row
 | 
			
		||||
 | 
			
		||||
            row = this.rows[row]
 | 
			
		||||
            row && row.focus(col, from)
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
Component.props.itemTag.default = 'tr'
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user