ui-pick (first draft)

This commit is contained in:
2020-06-09 16:57:57 +02:00
parent 0d3a0ee9c2
commit 6f7e0889d4
6 changed files with 397 additions and 5 deletions
+4
View File
@@ -5,5 +5,9 @@
public string Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public bool IsShared { get; set; }
}
}
@@ -37,6 +37,7 @@
open: false
}),
methods: {
toggle()
@@ -84,6 +85,7 @@
z-index: 8;
top: calc(100% + 5px);
padding: 5px;
color: var(--color-fg);
&.align-right
{
@@ -0,0 +1,376 @@
<template>
<div class="ui-pick" :class="{'is-disabled': disabled }">
<!-- previews -->
<div class="ui-pick-previews" v-if="previews.length > 0">
<div v-if="configuration.preview.enabled && !configuration.preview.combined" v-for="preview in previews" class="ui-pick-preview">
<ui-select-button :icon="getPreviewIcon(preview)" :label="preview.name" :description="getPreviewDescription(preview)" :disabled="disabled" @click="pick(preview.id)" :tokens="preview" />
<ui-icon-button v-if="!disabled && configuration.preview.delete" @click="remove(preview.id)" icon="fth-x" title="@ui.close" />
</div>
</div>
<!-- add button -->
<ui-select-button v-if="canAdd && configuration.addButton.enabled" icon="fth-plus" :label="configuration.addButton.label" @click="pick()" :disabled="disabled" />
<!-- overlay -->
<ui-dropdown ref="overlay" class="ui-pick-overlay">
<div class="ui-pick-overlay-head">
<div class="ui-pick-overlay-head-title">
<span class="-name" v-localize="title"></span>
<span class="-max" v-if="configuration.limit > 1" v-localize="{ key: '@ui.pick.max', tokens: { count: configuration.limit }}"></span>
</div>
<ui-icon-button @click="hide" icon="fth-x" title="@ui.close" />
</div>
<ui-search class="ui-pick-overlay-search" />
<!-- empty results -->
<div class="ui-pick-overlay-items">
<button v-for="item in items" :key="item.id" type="button" class="ui-pick-overlay-item">
<i v-if="item.icon" class="-icon" :class="item.icon" />
<span class="-name" v-localize="item.name"></span>
<span v-if="item.text && configuration.list.description" class="-text" v-localize="item.text"></span>
</button>
</div>
<!-- loading + empty states -->
<div class="ui-pick-overlay-center">
<div v-if="!isLoading && !configuration.autocomplete && !items.length" class="ui-pick-overlay-message">
<i class="ui-pick-overlay-message-icon fth-list"></i>
No items found
</div>
<ui-loading v-if="isLoading" />
</div>
</ui-dropdown>
</div>
</template>
<script>
import Overlay from 'zero/services/overlay';
import { extend as _extend, filter as _filter } from 'underscore';
const defaultConfig = {
// picker items, can either be a static list or a promise
items: [],
// exclude Ids from the picker selection items
excludedIds: [],
// autocomplete allows entering custom texts
autocomplete: false,
// maximum selection count
limit: 10,
// whether the previews/results are sortable or not
sortable: true,
// close picker when an item is clicked
closeOnClick: true,
// title in dropdown
title: null,
addButton: {
// hide the add button
enabled: true,
// text key for the add button
label: '@ui.select'
},
list: {
// output description text if available (second line)
description: false
},
preview: {
// output previews
enabled: true,
// output all selected items in one line (comma-separated)
combined: false,
// prefixed title when combine=true
combinedTitle: null,
// default icon used for previews
defaultIcon: 'fth-square',
// hides the icon in the preview
icon: true,
// hides the delete icon in preview
delete: true,
// displays an image instead of an icon in the preview
iconAsImage: false,
// output description text if available (second line)
description: false
},
search: {
// hides the search input
enabled: true,
// can force a local search for remote items (via promise)
local: true,
// sets the current model as the search input when opened
setDefaultValue: false,
// focus search input on open
focus: false,
// placeholder key in search input
placeholder: null //vm.options.autocomplete ? 'ui_search_autocomplete' : 'ui_search',
}
};
export default {
name: 'uiPick',
props: {
value: {
type: [String, Array],
default: null
},
disabled: {
type: Boolean,
default: false
},
config: {
type: Object,
default: () =>
{
return defaultConfig;
}
}
},
watch: {
config()
{
this.buildConfig();
},
value(val)
{
if (val)
{
this.load();
}
}
},
computed: {
canAdd()
{
return (!this.value && this.limit < 2) || (!this.value || this.value.length < this.configuration.limit);
},
isRemote()
{
return typeof this.configuration.items === 'function';
},
title()
{
if (this.configuration.title) return this.configuration.title;
if (this.configuration.autocomplete) return '@ui.pick.title_autocomplete';
if (this.configuration.limit > 1) return '@ui.pick.title_multiple';
return '@ui.pick.title';
}
},
data: () => ({
configuration: {},
previews: [],
allItems: [],
items: [],
loaded: false,
isLoading: false
}),
created()
{
this.buildConfig();
},
methods: {
buildConfig()
{
var config = JSON.parse(JSON.stringify(defaultConfig));
this.configuration = _extend(defaultConfig, this.config);
this.configuration.search = _extend(config.search, this.config.search || {});
this.configuration.addButton = _extend(config.addButton, this.config.addButton || {});
this.configuration.preview = _extend(config.preview, this.config.preview || {});
this.configuration.list = _extend(config.preview, this.config.list || {});
},
pick()
{
this.$refs.overlay.toggle();
if (!this.loaded)
{
this.load();
}
},
hide()
{
this.$refs.overlay.hide();
},
remove()
{
},
// loads remote items or items from the given configuration
// this will store both all items and the reduced search results
load()
{
if (this.isLoading)
{
return;
}
this.isLoading = true;
this.allItems = [];
let onLoaded = (items) =>
{
this.allItems = items;
this.loadSuggestions();
this.loaded = true;
this.isLoading = false;
};
if (this.isRemote)
{
this.configuration.items().then(onLoaded);
}
else
{
onLoaded(this.configuration.items);
}
},
loadSuggestions()
{
this.isLoading = true;
let items = [];
let search = null; // TODO use search value vm.searchTerm.toLowerCase();
if (!search)
{
items = this.allItems;
}
else
{
if (!this.isRemote || this.configuration.search.local)
{
items = _.filter(this.allItems, (item) => item.name.toLowerCase().indexOf(search) > -1);
}
else if (this.isRemote)
{
configuration.items(search).then(res => this.items = res);
}
}
if (this.configuration.excludedIds && this.configuration.excludedIds.length)
{
items = _.filter(items, (item) => this.configuration.excludedIds.indexOf(item.id) < 0);
}
this.items = items;
this.isLoading = false;
},
getPreviewIcon(preview)
{
return this.configuration.preview.icon ? (preview.icon || this.configuration.preview.defaultIcon) : null;
},
getPreviewDescription(preview)
{
return this.configuration.preview.description ? preview.text : null;
}
}
}
</script>
<style lang="scss">
.ui-pick-overlay-search
{
margin: 15px 15px 5px;
.ui-input
{
background: var(--color-highlight);
min-width: 0;
}
}
.ui-pick-overlay
{
.ui-dropdown
{
min-width: 0;
width: 100%;
max-width: 380px;
}
}
.ui-pick-overlay-center
{
display: flex;
justify-content: center;
margin: 30px 0;
}
.ui-pick-overlay-head
{
padding: 15px 15px 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.ui-pick-overlay-head-title
{
font-size: var(--font-size-l);
font-weight: 600;
.-max
{
font-size: var(--font-size-s);
font-weight: 400;
color: var(--color-fg-light);
margin-left: .6em;
}
}
.ui-pick-overlay-message
{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 0 15px;
}
.ui-pick-overlay-message-icon
{
font-size: 18px;
margin-bottom: 10px;
}
.ui-pick-overlay-items
{
}
.ui-pick-overlay-item
{
display: flex;
padding: 5px 15px;
}
</style>
+5 -1
View File
@@ -29,7 +29,11 @@ Vue.directive('click-outside', {
handler: handler,
callback: binding.value
};
!Helpers.isServer(vNode) && document.addEventListener('click', handler);
setTimeout(() =>
{
!Helpers.isServer(vNode) && document.addEventListener('click', handler);
}, 200);
},
update(el, binding)
@@ -287,9 +287,9 @@ button::-moz-focus-inner
.ui-select-button-icon
{
width: 50px;
height: 50px;
line-height: 50px !important;
width: 42px;
height: 42px;
line-height: 42px !important;
border-radius: var(--radius);
background: var(--color-bg-mid);
color: var(--color-fg);
@@ -32,7 +32,13 @@
"select": "Select",
"icon": "Icon",
"icon_select": "Select an icon",
"tab_general": "General"
"tab_general": "General",
"pick": {
"title": "Choose one",
"title_multiple": "Choose",
"title_autocomplete": "Enter a value",
"max": "max. {count}"
}
},
"errors": {