forked from platypush/platypush
[UI] Added general-purpose entity icon editor component.
This commit is contained in:
parent
1316af9553
commit
1133c6019a
2 changed files with 276 additions and 106 deletions
|
@ -0,0 +1,260 @@
|
||||||
|
<template>
|
||||||
|
<form class="icon-editor"
|
||||||
|
@submit="onIconEdit(newIcon, $event)"
|
||||||
|
@click.stop>
|
||||||
|
<div class="row item">
|
||||||
|
<div class="title">
|
||||||
|
Icon
|
||||||
|
<EditButton title="Edit Icon"
|
||||||
|
@click.stop="editIcon = true"
|
||||||
|
v-if="!editIcon" />
|
||||||
|
</div>
|
||||||
|
<div class="value icon-canvas">
|
||||||
|
<span class="icon-editor" v-if="editIcon">
|
||||||
|
<span class="icon-edit-form" v-if="newIcon != null">
|
||||||
|
<span class="icon-container">
|
||||||
|
<img :src="currentIcon.url" v-if="currentIcon?.url" />
|
||||||
|
<i :class="currentIcon.class" :style="{color: currentIcon.color}" v-else />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<NameEditor :value="currentIcon.url || currentIcon.class"
|
||||||
|
:disabled="loading"
|
||||||
|
@keyup="newIcon = $event.target.value?.trim()"
|
||||||
|
@input="onIconEdit(newIcon)"
|
||||||
|
@cancel="editIcon = false">
|
||||||
|
<button type="button"
|
||||||
|
title="Reset"
|
||||||
|
@click.stop="onIconEdit(null, $event)">
|
||||||
|
<i class="fas fa-rotate-left" />
|
||||||
|
</button>
|
||||||
|
</NameEditor>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="help">
|
||||||
|
Supported: image URLs or
|
||||||
|
<a href="https://fontawesome.com/icons" target="_blank">FontAwesome icon classes</a>.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Icon v-bind="currentIcon" v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row item">
|
||||||
|
<div class="title">
|
||||||
|
Icon color
|
||||||
|
</div>
|
||||||
|
<div class="value icon-color-picker">
|
||||||
|
<input type="color"
|
||||||
|
:value="currentIcon.color"
|
||||||
|
@input.stop
|
||||||
|
@change.stop="onIconColorEdit">
|
||||||
|
<button type="button"
|
||||||
|
title="Reset"
|
||||||
|
@click.stop="onIconColorEdit(null)">
|
||||||
|
<i class="fas fa-rotate-left" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EditButton from "@/components/elements/EditButton";
|
||||||
|
import Icon from "@/components/elements/Icon";
|
||||||
|
import NameEditor from "@/components/elements/NameEditor";
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import meta from './meta.json';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
EditButton,
|
||||||
|
Icon,
|
||||||
|
NameEditor,
|
||||||
|
},
|
||||||
|
mixins: [Utils],
|
||||||
|
emits: ['change', 'input'],
|
||||||
|
props: {
|
||||||
|
entity: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editIcon: false,
|
||||||
|
loading: false,
|
||||||
|
newIcon: null,
|
||||||
|
newColor: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
currentIcon() {
|
||||||
|
return {
|
||||||
|
...((meta[this.entity.type] || {})?.icon || {}),
|
||||||
|
...(this.entity.meta?.icon || {}),
|
||||||
|
...(this.newIcon ? this.iconObj(this.newIcon) : {}),
|
||||||
|
...(this.newColor?.length ? {color: this.newColor} : {}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async edit(icon, event) {
|
||||||
|
const req = {
|
||||||
|
[this.entity.id]: {icon},
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
if (event)
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entity = (await this.request('entities.set_meta', req))[0]
|
||||||
|
icon = entity?.meta?.icon
|
||||||
|
if (icon) {
|
||||||
|
this.$emit('input', icon)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
this.editIcon = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onIconEdit(newIcon, event) {
|
||||||
|
const icon = {
|
||||||
|
...this.currentIcon,
|
||||||
|
...(this.iconObj(newIcon) || {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.edit(icon, event)
|
||||||
|
},
|
||||||
|
|
||||||
|
async onIconColorEdit(event) {
|
||||||
|
const color = event?.target?.value
|
||||||
|
const icon = {
|
||||||
|
...this.currentIcon,
|
||||||
|
...(color?.length ? {color} : {color: null}),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newColor = color
|
||||||
|
await this.edit(icon, event)
|
||||||
|
},
|
||||||
|
|
||||||
|
iconObj(iconStr) {
|
||||||
|
if (!iconStr?.length)
|
||||||
|
return {
|
||||||
|
...this.currentIcon,
|
||||||
|
url: this.entity.meta?.icon?.url,
|
||||||
|
class: this.entity.meta?.icon?.class,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iconStr.startsWith('http'))
|
||||||
|
return {url: iconStr}
|
||||||
|
|
||||||
|
return {class: iconStr}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
editIcon() {
|
||||||
|
this.newIcon = (this.entity.meta?.icon?.['class'] || this.entity.meta?.icon?.url)?.trim()
|
||||||
|
},
|
||||||
|
|
||||||
|
newIcon() {
|
||||||
|
this.$emit('change', this.currentIcon)
|
||||||
|
},
|
||||||
|
|
||||||
|
newColor() {
|
||||||
|
this.$emit('change', this.currentIcon)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.icon-editor {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
|
||||||
|
@include until($tablet) {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 10em;
|
||||||
|
|
||||||
|
@include until($tablet) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
@include until($tablet) {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-editor {
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
@include until($tablet) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 2em;
|
||||||
|
max-height: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-canvas {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@include until($tablet) {
|
||||||
|
.icon-container {
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include from($tablet) {
|
||||||
|
.icon-container {
|
||||||
|
justify-content: right;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -20,40 +20,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-row">
|
<div class="table-row">
|
||||||
<div class="title">
|
<IconEditor :entity="entity" @input="onIconEdit" />
|
||||||
Icon
|
|
||||||
<EditButton @click="editIcon = true" v-if="!editIcon" />
|
|
||||||
</div>
|
|
||||||
<div class="value icon-canvas">
|
|
||||||
<span class="icon-editor" v-if="editIcon">
|
|
||||||
<NameEditor :value="entity.meta?.icon?.class || entity.meta?.icon?.url" @input="onIconEdit"
|
|
||||||
@cancel="editIcon = false" :disabled="loading">
|
|
||||||
<button type="button" title="Reset" @click="onIconEdit(null)"
|
|
||||||
@touch="onIconEdit(null)">
|
|
||||||
<i class="fas fa-rotate-left" />
|
|
||||||
</button>
|
|
||||||
</NameEditor>
|
|
||||||
<span class="help">
|
|
||||||
Supported: image URLs or
|
|
||||||
<a href="https://fontawesome.com/icons" target="_blank">FontAwesome icon classes</a>.
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Icon v-bind="entity?.meta?.icon || {}" v-else />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-row">
|
|
||||||
<div class="title">
|
|
||||||
Icon color
|
|
||||||
</div>
|
|
||||||
<div class="value icon-color-picker">
|
|
||||||
<input type="color" :value="entity.meta?.icon?.color" @change="onIconColorEdit">
|
|
||||||
<button type="button" title="Reset" @click="onIconColorEdit(null)"
|
|
||||||
@touch="onIconColorEdit(null)">
|
|
||||||
<i class="fas fa-rotate-left" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-row">
|
<div class="table-row">
|
||||||
|
@ -206,14 +173,13 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import Icon from "@/components/elements/Icon";
|
import IconEditor from "./IconEditor";
|
||||||
import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
||||||
import EditButton from "@/components/elements/EditButton";
|
import EditButton from "@/components/elements/EditButton";
|
||||||
import EntityIcon from "./EntityIcon"
|
import EntityIcon from "./EntityIcon"
|
||||||
import NameEditor from "@/components/elements/NameEditor";
|
import NameEditor from "@/components/elements/NameEditor";
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
import Entity from "./Entity";
|
import Entity from "./Entity";
|
||||||
import meta from './meta.json';
|
|
||||||
|
|
||||||
// These fields have a different rendering logic than the general-purpose one
|
// These fields have a different rendering logic than the general-purpose one
|
||||||
const specialFields = [
|
const specialFields = [
|
||||||
|
@ -233,9 +199,14 @@ const specialFields = [
|
||||||
]
|
]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "EntityModal",
|
|
||||||
components: {
|
components: {
|
||||||
Entity, EntityIcon, Modal, EditButton, NameEditor, Icon, ConfirmDialog
|
ConfirmDialog,
|
||||||
|
EditButton,
|
||||||
|
Entity,
|
||||||
|
EntityIcon,
|
||||||
|
IconEditor,
|
||||||
|
Modal,
|
||||||
|
NameEditor,
|
||||||
},
|
},
|
||||||
mixins: [Utils],
|
mixins: [Utils],
|
||||||
emits: ['input', 'loading', 'entity-update'],
|
emits: ['input', 'loading', 'entity-update'],
|
||||||
|
@ -276,7 +247,6 @@ export default {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
editName: false,
|
editName: false,
|
||||||
editIcon: false,
|
|
||||||
configCollapsed: true,
|
configCollapsed: true,
|
||||||
childrenCollapsed: true,
|
childrenCollapsed: true,
|
||||||
extraInfoCollapsed: true,
|
extraInfoCollapsed: true,
|
||||||
|
@ -308,47 +278,14 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async onIconEdit(newIcon) {
|
onIconEdit(icon) {
|
||||||
this.loading = true
|
this.$emit(
|
||||||
|
'input',
|
||||||
try {
|
{
|
||||||
const icon = {url: null, class: null}
|
...this.entity,
|
||||||
if (newIcon?.length) {
|
meta: {...this.entity.meta, icon},
|
||||||
if (newIcon.startsWith('http'))
|
|
||||||
icon.url = newIcon
|
|
||||||
else
|
|
||||||
icon.class = newIcon
|
|
||||||
} else {
|
|
||||||
icon.url = (meta[this.entity.type] || {})?.icon?.url
|
|
||||||
icon.class = (meta[this.entity.type] || {})?.icon?.['class']
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = {}
|
|
||||||
req[this.entity.id] = {icon: icon}
|
|
||||||
await this.request('entities.set_meta', req)
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
this.editIcon = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async onIconColorEdit(event) {
|
|
||||||
this.loading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const icon = this.entity.meta?.icon || {}
|
|
||||||
if (event)
|
|
||||||
icon.color = event.target.value
|
|
||||||
else
|
|
||||||
icon.color = null
|
|
||||||
|
|
||||||
const req = {}
|
|
||||||
req[this.entity.id] = {icon: icon}
|
|
||||||
await this.request('entities.set_meta', req)
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
this.editIcon = false
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
stringify(value) {
|
stringify(value) {
|
||||||
|
@ -419,39 +356,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-canvas {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@include until($tablet) {
|
|
||||||
.icon-container {
|
|
||||||
justify-content: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include from($tablet) {
|
|
||||||
.icon-container {
|
|
||||||
justify-content: right;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-editor {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help {
|
|
||||||
font-size: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-entity-container {
|
.delete-entity-container {
|
||||||
color: $error-fg;
|
color: $error-fg;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
Loading…
Reference in a new issue