whoop
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyProduct("zero")]
|
||||
[assembly: AssemblyCompany("brothers Klika OG")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2020 brothers Klika OG")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: AssemblyVersion("0.0.1.0")]
|
||||
@@ -1,213 +0,0 @@
|
||||
<template>
|
||||
<div class="page page-error" :class="{'theme-dark': dark }">
|
||||
<i class="page-error-icon fth-cloud-snow"></i>
|
||||
<p class="page-error-text">
|
||||
<strong class="page-error-headline" v-localize="{ key: errorDetails.headline, tokens: tokens }"></strong><br>
|
||||
<span v-localize:html="{ key: errorDetails.text, tokens: tokens }"></span>
|
||||
</p>
|
||||
<ui-button v-if="errorDetails.code === 404" class="page-error-button" type="light onbg" :label="detailsText" @click="details = !details" />
|
||||
<ui-button v-if="errorDetails.code !== 404 && errorDetails.category === 4" class="page-error-button" type="light onbg" label="@ui.back" @click="$router.go(-1)" />
|
||||
<!--<ui-button v-if="errorDetails.code !== 404 && errorDetails.category === 5" class="page-error-button" type="light" label="@ui.back" @click="$router.go(-1)" />-->
|
||||
<template v-if="errorDetails.code === 404 && details">
|
||||
<br><br>
|
||||
<div class="page-error-routes">
|
||||
<span>#</span>
|
||||
<span>Name</span>
|
||||
<span>Path</span>
|
||||
<template v-for="(route, index) in routes">
|
||||
<span>{{index + 1}}.</span>
|
||||
<span>{{route.name}}</span>
|
||||
<span>{{route.path}}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
const KNOWN_ERRORS = [403, 404, 409, 504];
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
dark: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: Error,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
path: null,
|
||||
routes: [],
|
||||
details: false,
|
||||
errorDetails: {
|
||||
category: 0,
|
||||
code: null,
|
||||
headline: null,
|
||||
text: null
|
||||
}
|
||||
}),
|
||||
|
||||
watch: {
|
||||
error: function (val)
|
||||
{
|
||||
this.rebuild();
|
||||
},
|
||||
'$route': function (val)
|
||||
{
|
||||
this.rebuild();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
detailsText()
|
||||
{
|
||||
return this.details ? 'Hide' : 'Defined routes';
|
||||
},
|
||||
tokens()
|
||||
{
|
||||
return {
|
||||
code: this.errorDetails.code,
|
||||
path: this.path
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
console.info(this.error.response);
|
||||
this.rebuild();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
rebuild()
|
||||
{
|
||||
if (this.error && this.error.response)
|
||||
{
|
||||
let errorKey = null;
|
||||
|
||||
const errorCode = this.error.response.status;
|
||||
const errorCategory = Math.round(errorCode / 100);
|
||||
|
||||
if (KNOWN_ERRORS.indexOf(errorCode) > -1)
|
||||
{
|
||||
errorKey = '@errors.http.' + errorCode;
|
||||
}
|
||||
else if (errorCategory === 4)
|
||||
{
|
||||
errorKey = '@errors.http.4xx';
|
||||
}
|
||||
else if (errorCategory === 5)
|
||||
{
|
||||
errorKey = '@errors.http.5xx';
|
||||
}
|
||||
|
||||
this.errorDetails.category = errorCategory;
|
||||
this.errorDetails.code = errorCode;
|
||||
this.errorDetails.headline = errorKey;
|
||||
this.errorDetails.text = errorKey + '_text';
|
||||
}
|
||||
|
||||
this.path = this.$router.options.base + this.$route.path.substring(1);
|
||||
this.routes = [];
|
||||
|
||||
this.$router.options.routes.forEach(route =>
|
||||
{
|
||||
this.routes.push({
|
||||
path: route.path,
|
||||
name: route.name
|
||||
});
|
||||
|
||||
if (route.children)
|
||||
{
|
||||
route.children.forEach(child =>
|
||||
{
|
||||
this.routes.push({
|
||||
path: route.path + '/' + child.path,
|
||||
name: child.name
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-error
|
||||
{
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
padding: var(--padding);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-error-icon
|
||||
{
|
||||
font-size: 82px;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-error-text
|
||||
{
|
||||
font-size: var(--font-size);
|
||||
color: var(--color-text-dim);
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.page-error-headline
|
||||
{
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
font-size: var(--font-size-l);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page-error-button
|
||||
{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.page-error-routes
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
border-top: 1px solid var(--color-line);
|
||||
border-left: 1px solid var(--color-line);
|
||||
margin-top: 30px;
|
||||
|
||||
span
|
||||
{
|
||||
border: 1px solid var(--color-line);
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
padding: 8px 10px 6px;
|
||||
font-family: Consolas;
|
||||
font-size: 12px;
|
||||
|
||||
&:nth-child(3n+1)
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-password-hash">
|
||||
<input value="******************" type="text" class="ui-input" readonly="readonly" disabled="disabled" />
|
||||
<ui-button v-if="!disabled" label="change" type="light" @click="open" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
//import Overlay from 'zero/helpers/overlay.js';
|
||||
//import PasswordChangeOverlay from 'zero/components/overlays/password-change.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
},
|
||||
entity: Object
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
open()
|
||||
{
|
||||
//return Overlay.open({
|
||||
// component: PasswordChangeOverlay
|
||||
//}).then(hash =>
|
||||
//{
|
||||
// this.$emit('input', hash);
|
||||
//});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-password-hash
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-gap: var(--padding-xxs);
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="setup-step setup-step-application">
|
||||
<h2>your application</h2>
|
||||
<p>With zero you can manage multiple applications with one installation. Start by adding your first application.</p>
|
||||
|
||||
<ui-property label="Application name" :vertical="true">
|
||||
<input v-model="value.appName" type="text" class="ui-input" maxlength="40" placeholder="My super awesome app maybe?" />
|
||||
</ui-property>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'setupStepApplication',
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
mounted ()
|
||||
{
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<div class="setup-step setup-step-database">
|
||||
<h2>database configuration</h2>
|
||||
<p>Zero uses <b>RavenDB</b> as it’s database. If you want a more sophisticated setup (i.e. cluster) you can override the IDocumentStore service which is registered as a singleton.</p>
|
||||
|
||||
<ui-property label="Instance URL" :vertical="true">
|
||||
<input v-model="value.database.url" type="text" class="ui-input" placeholder="Enter URL to the Raven instance" />
|
||||
</ui-property>
|
||||
|
||||
<ui-property label="Database name" :vertical="true">
|
||||
<input v-model="value.database.name" type="text" class="ui-input" placeholder="Name of the existing database" />
|
||||
</ui-property>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'setupStepDatabase',
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
mounted ()
|
||||
{
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<div class="setup-step setup-step-finish">
|
||||
<div class="setup-step-finish-inner">
|
||||
<img src="/Icons/bird.svg" class="bird" alt="bird" width="100" />
|
||||
<h2>You are done!</h2>
|
||||
</div>
|
||||
<ui-button label="Continue to zero" type="big" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'setupStepFinish',
|
||||
|
||||
mounted ()
|
||||
{
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.setup-step-finish
|
||||
{
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
.bird
|
||||
{
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,179 +0,0 @@
|
||||
<template>
|
||||
<div class="setup-step setup-step-install">
|
||||
<div v-if="!errorMessage" class="setup-step-install-inner">
|
||||
<div class="setup-step-install-progress"><i :style="{ width: progress + '%' }"></i></div>
|
||||
<h2>Installing</h2>
|
||||
</div>
|
||||
<div v-if="errorMessage" class="setup-step-install-error">
|
||||
<div class="setup-step-install-error-main">
|
||||
<h2>Oh no!</h2>
|
||||
<p>The setup of <b>zero</b> failed with following error(s):</p>
|
||||
<pre class="setup-step-install-error-code"><code v-html="errorMessage"></code></pre>
|
||||
</div>
|
||||
<div class="setup-buttons">
|
||||
<ui-button type="outline" @click="onBack" label="Go back" caret="left" caret-position="left" />
|
||||
<ui-button label="Retry" @click="install" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import Axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'setupStepInstall',
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
onBack: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onNext: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
errorMessage: null,
|
||||
progress: 0
|
||||
}),
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.install();
|
||||
},
|
||||
|
||||
methods: {
|
||||
install()
|
||||
{
|
||||
this.errorMessage = null;
|
||||
|
||||
let interval = setInterval(() =>
|
||||
{
|
||||
if (++this.progress >= 100)
|
||||
{
|
||||
this.onNext();
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
Axios.post(zero.path + 'api/setup/install', this.value)
|
||||
.then(response =>
|
||||
{
|
||||
if (!response.success)
|
||||
{
|
||||
throw response;
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
clearInterval(interval);
|
||||
|
||||
// The request was made and the server responded with a status code that falls out of the range of 2xx
|
||||
if (error.response)
|
||||
{
|
||||
this.error(error.response.data);
|
||||
}
|
||||
// The request was made but no response was received
|
||||
else if (error.request)
|
||||
{
|
||||
this.error(error.request);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.error(error.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
error(error)
|
||||
{
|
||||
this.errorMessage = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.setup-step-install
|
||||
{
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.setup-step-install-inner
|
||||
{
|
||||
text-align: center;
|
||||
|
||||
h2
|
||||
{
|
||||
margin: 30px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setup-step-install-progress
|
||||
{
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-bg-light);
|
||||
overflow: hidden;
|
||||
|
||||
i
|
||||
{
|
||||
background: var(--color-primary);
|
||||
transition: width 0.1s ease;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.setup-step-install-error
|
||||
{
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
h2
|
||||
{
|
||||
color: var(--color-negative);
|
||||
}
|
||||
|
||||
div
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.setup-step-install-error-code
|
||||
{
|
||||
background: var(--color-bg-light);
|
||||
padding: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
width: 440px;
|
||||
max-height: 50vh;
|
||||
overflow: scroll;
|
||||
font-family: 'Lucida Console', Consolas, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.setup-step-install-error-main
|
||||
{
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<div class="setup-step setup-step-user">
|
||||
<h2>welcome to zero</h2>
|
||||
<p>First we need some basic credentials from you to create a login. This login is the solely <b>superadmin</b> which is required to change critical application data.</p>
|
||||
|
||||
<ui-property label="Name" :vertical="true">
|
||||
<input v-model="value.user.name" type="text" class="ui-input" maxlength="40" placeholder="What's your name? It doesn't have to be your real name" />
|
||||
</ui-property>
|
||||
|
||||
<ui-property label="Email" :vertical="true">
|
||||
<input v-model="value.user.email" type="text" class="ui-input" placeholder="Your email will be used as your login" />
|
||||
</ui-property>
|
||||
|
||||
<ui-property label="Password" :vertical="true">
|
||||
<input v-model="value.user.password" type="password" class="ui-input" autocomplete="off" maxlength="1024" placeholder="From 8 characters to a galaxy far, far away" />
|
||||
</ui-property>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'setupStepUser',
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
mounted ()
|
||||
{
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,9 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
import Setup from './setup.vue';
|
||||
//import 'filter/generic.js'
|
||||
//import 'directive/filedrop.js'
|
||||
|
||||
import GlobalComponents from 'zero/components/globals.js';
|
||||
import Directives from 'zero/directives/globals.js';
|
||||
|
||||
new Vue(Setup).$mount('#application');
|
||||
@@ -1,6 +0,0 @@
|
||||
// configuration
|
||||
@import "Core/all";
|
||||
|
||||
// modules
|
||||
@import "Modules/button";
|
||||
@import "Setup/all";
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<h1 class="app-headline">zero</h1>
|
||||
<div class="setup">
|
||||
<component v-bind:is="steps[step]" v-model="model" :on-back="prev" :on-next="next" />
|
||||
<div class="setup-buttons" v-if="step < 3">
|
||||
<ui-button v-if="step > 0" type="outline" label="Go back" @click="prev" caret="left" caret-position="left" />
|
||||
<ui-button :label="nextLabel" @click="next" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import Sass from '../Sass/setup.scss'
|
||||
import UiButton from 'zero/components/buttons/button.vue'
|
||||
import StepUser from './Steps/step-user.vue'
|
||||
import StepApplication from './Steps/step-application.vue'
|
||||
import StepDatabase from './Steps/step-database.vue'
|
||||
import StepInstall from './Steps/step-install.vue'
|
||||
import StepFinish from './Steps/step-finish.vue'
|
||||
|
||||
export default {
|
||||
name: 'setup',
|
||||
|
||||
components: { UiButton, StepUser, StepApplication, StepDatabase, StepInstall, StepFinish },
|
||||
|
||||
data: () => ({
|
||||
step: 0,
|
||||
steps: ['step-user', 'step-database', 'step-application', 'step-install', 'step-finish'],
|
||||
model: { "appName": "Brothers", "user": { "name": "Tobias Klika", "email": "", "password": "" }, "database": { "url": "http://localhost:9800", "name": "zero" } }
|
||||
//model: {
|
||||
// appName: null,
|
||||
// user: {
|
||||
// name: null,
|
||||
// email: null,
|
||||
// password: null
|
||||
// },
|
||||
// database: {
|
||||
// url: 'http://localhost:9800',
|
||||
// name: null
|
||||
// }
|
||||
//}
|
||||
}),
|
||||
|
||||
computed: {
|
||||
nextLabel()
|
||||
{
|
||||
return this.step < 2 ? 'Next' : 'Install application';
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
next()
|
||||
{
|
||||
if (this.step + 1 >= this.steps.length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.step += 1;
|
||||
},
|
||||
|
||||
prev()
|
||||
{
|
||||
this.step -= 1;
|
||||
},
|
||||
|
||||
save()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.setup
|
||||
{
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
align-items: stretch;
|
||||
max-width: 100%;
|
||||
width: 520px;
|
||||
background: var(--color-bg-bright);
|
||||
border-radius: 3px;
|
||||
min-height: 600px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.setup-step
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.setup-step h2
|
||||
{
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.setup-step p
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 2rem;
|
||||
}
|
||||
|
||||
.setup-step p b
|
||||
{
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.setup-step .ui-property + .ui-property
|
||||
{
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,175 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-tableeditor" :class="{'is-disabled': disabled }">
|
||||
<div class="-table">
|
||||
<div v-for="(row, index) in rows" :key="index" class="-row">
|
||||
<div v-for="(cell, cellIndex) in row" :key="cellIndex" class="-cell" :class="{'-head': index == 0 }">
|
||||
<p class="-content" :data-placeholder="(index == 0 ? 'Title' : 'Content')" contenteditable="true" v-html="cell" @input="onCellChange($event, index, cellIndex)"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<br><br>
|
||||
<input type="number" v-model="idx" placeholder="Index" style="display: inline; width: 80px; margin-right: 10px;" />
|
||||
<button type="button" class="ui-button" @click="addRow(idx)">addRow</button>
|
||||
<button type="button" class="ui-button" @click="removeRow(idx)">removeRow</button>
|
||||
<button type="button" class="ui-button" @click="addColumn(idx)">addColumn</button>
|
||||
<button type="button" class="ui-button" @click="removeColumn(idx)">removeColumn</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiTableEditor',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
idx: 0,
|
||||
rows: []
|
||||
}),
|
||||
|
||||
watch: {
|
||||
value(value)
|
||||
{
|
||||
this.rebuild();
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.rebuild();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
rebuild()
|
||||
{
|
||||
this.rows = JSON.parse(JSON.stringify(this.value));
|
||||
},
|
||||
|
||||
update()
|
||||
{
|
||||
this.$emit('input', this.rows);
|
||||
},
|
||||
|
||||
onCellChange(ev, rowIndex, cellIndex)
|
||||
{
|
||||
this.rows[rowIndex][cellIndex] = ev.target.innerText;
|
||||
this.update();
|
||||
},
|
||||
|
||||
addRow(index)
|
||||
{
|
||||
this.rows.splice(index, 0, this.rows[0].map(_ => "<br>"));
|
||||
this.update();
|
||||
},
|
||||
|
||||
removeRow(index)
|
||||
{
|
||||
this.rows.splice(index, 1);
|
||||
this.update();
|
||||
},
|
||||
|
||||
addColumn(index)
|
||||
{
|
||||
this.rows.forEach(row =>
|
||||
{
|
||||
row.splice(index, 0, "<br>");
|
||||
});
|
||||
this.update();
|
||||
},
|
||||
|
||||
removeColumn(index)
|
||||
{
|
||||
this.rows.forEach(row =>
|
||||
{
|
||||
row.splice(index, 1);
|
||||
});
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-tableeditor .-table
|
||||
{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
break-inside: auto;
|
||||
//border: 1px solid var(--color-line-onbg);
|
||||
border-radius: var(--radius);
|
||||
position: relative;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.ui-tableeditor .-row
|
||||
{
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.ui-tableeditor .-cell
|
||||
{
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
width: auto;
|
||||
position: relative;
|
||||
align-self: start;
|
||||
border-top: medium none;
|
||||
align-items: start;
|
||||
border-right: medium none;
|
||||
border-bottom: medium none;
|
||||
-moz-box-align: start;
|
||||
}
|
||||
|
||||
.ui-tableeditor .-cell.-head .-content
|
||||
{
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ui-tableeditor .-content
|
||||
{
|
||||
margin: 0;
|
||||
min-height: 1.2em;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
/*.ui-tableeditor .-content[data-placeholder]:before
|
||||
{
|
||||
content: attr(data-placeholder);
|
||||
color: var(--color-text-dim-one);
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}*/
|
||||
|
||||
.ui-tableeditor .-content:focus-within
|
||||
{
|
||||
background: var(--color-bg-dim);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ui-tableeditor .-row
|
||||
{
|
||||
border-bottom: 1px solid var(--color-line-onbg);
|
||||
padding: 10px;
|
||||
background: rgb(255, 255, 255) none repeat scroll 0% 0%;
|
||||
break-after: auto;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.ui-tableeditor .-row:first-child
|
||||
{
|
||||
//border-bottom-width: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<ui-form v-if="!loading" ref="form" class="app-password-change" v-slot="form" @submit="onSubmit">
|
||||
<h2 class="ui-headline" v-localize="'@changepasswordoverlay.title'"></h2>
|
||||
|
||||
<div class="app-password-change-fields">
|
||||
<ui-error :catch-remaining="true" />
|
||||
<ui-editor :config="editorConfig" v-model="model" />
|
||||
</div>
|
||||
|
||||
<div class="app-confirm-buttons">
|
||||
<ui-button type="accent" :submit="true" :state="form.state" label="@ui.confirm"></ui-button>
|
||||
<ui-button type="light" label="@ui.close" :disabled="loading" @click="config.close"></ui-button>
|
||||
</div>
|
||||
</ui-form>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import UsersApi from 'zero/api/users.js'
|
||||
import passwordRenderer from 'zero/renderers/editors/password.js';
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
config: Object
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loading: false,
|
||||
model: {
|
||||
currentPassword: null,
|
||||
newPassword: null,
|
||||
confirmNewPassword: null
|
||||
},
|
||||
editorConfig: passwordRenderer
|
||||
}),
|
||||
|
||||
methods: {
|
||||
|
||||
onSubmit(form)
|
||||
{
|
||||
form.handle(UsersApi.hashPassword({ ...this.model, userId: this.$route.params.id })).then(res =>
|
||||
{
|
||||
this.config.confirm(res.model);
|
||||
}, errors => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.app-password-change
|
||||
{
|
||||
text-align: left;
|
||||
|
||||
.editor, .ui-tab, .ui-box
|
||||
{
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.app-password-change-fields
|
||||
{
|
||||
margin-top: var(--padding);
|
||||
|
||||
.ui-property + .ui-property, .ui-split + .ui-property
|
||||
{
|
||||
margin-top: var(--padding);
|
||||
}
|
||||
}
|
||||
|
||||
/*.translation-items
|
||||
{
|
||||
margin-top: var(--padding);
|
||||
|
||||
.ui-property + .ui-property,
|
||||
.ui-split + .ui-property
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
}*/
|
||||
</style>
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
|
||||
declare module 'zero/account'
|
||||
{
|
||||
export interface AccountUser
|
||||
{
|
||||
id: string;
|
||||
currentAppId: string;
|
||||
isSuper: boolean;
|
||||
avatarId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
createdDate: string;
|
||||
flavor?: string;
|
||||
}
|
||||
|
||||
export interface AccountLoginResponse
|
||||
{
|
||||
user: AccountUser;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface AccountStoreState
|
||||
{
|
||||
user?: AccountUser;
|
||||
}
|
||||
|
||||
export interface AccountLoginModel
|
||||
{
|
||||
email: string;
|
||||
password: string;
|
||||
isPersistent: boolean;
|
||||
}
|
||||
|
||||
export interface AccountLoggedInResponse
|
||||
{
|
||||
loggedIn: boolean;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { AccountLoggedInResponse, AccountLoginModel, AccountLoginResponse, AccountUser } from 'zero/account';
|
||||
import { get, post, ApiRequestConfig, ApiResponse } from '../services/request';
|
||||
|
||||
export default {
|
||||
|
||||
getUser: (config?: ApiRequestConfig): Promise<ApiResponse<AccountUser>> => get('backoffice/account/user', { ...config }),
|
||||
|
||||
isAuthenticated: (config?: ApiRequestConfig): Promise<ApiResponse<AccountLoggedInResponse>> => get('backoffice/account/loggedin', { ...config }),
|
||||
|
||||
login: (model: AccountLoginModel, config?: ApiRequestConfig): Promise<ApiResponse<AccountLoginResponse>> => post('backoffice/account/login', model, { ...config }),
|
||||
|
||||
logout: (config?: ApiRequestConfig) => post('backoffice/account/logout', null, { ...config })
|
||||
};
|
||||
@@ -1,168 +0,0 @@
|
||||
<template>
|
||||
<div class="app-auth">
|
||||
<span></span>
|
||||
<ui-form class="app-auth-inner" v-slot="form" @submit="onSubmit">
|
||||
<div>
|
||||
<div class="app-auth-logo">
|
||||
<span class="app-nav-logo-circle"></span>
|
||||
<img src="/Assets/zero.svg" class="app-auth-image show-light" v-localize:alt="'@zero.name'" />
|
||||
<img src="/Assets/zero-dark.svg" class="app-auth-image show-dark" v-localize:alt="'@zero.name'" />
|
||||
</div>
|
||||
|
||||
<ui-error :catch-remaining="true" />
|
||||
|
||||
<ui-property field="email" label="@login.fields.email" :vertical="true">
|
||||
<input v-model="model.email" type="text" class="ui-input" maxlength="120" v-localize:placeholder="'@login.fields.email_placeholder'" />
|
||||
</ui-property>
|
||||
|
||||
<ui-property field="password" label="@login.fields.password" :vertical="true">
|
||||
<input v-model="model.password" type="password" class="ui-input" maxlength="1024" v-localize:placeholder="'@login.fields.password_placeholder'" />
|
||||
</ui-property>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="app-auth-bottom">
|
||||
<ui-button class="app-auth-confirm" type="accent big" :submit="true" label="@login.button" :state="form.state" />
|
||||
<!--<ui-button type="blank" label="@login.button_forgot" />-->
|
||||
</div>
|
||||
</ui-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import api from './api';
|
||||
import { useAccountStore } from './store';
|
||||
import { useUiStore } from '../ui/store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'app-login',
|
||||
|
||||
data: () => ({
|
||||
rejectReason: null,
|
||||
model: {
|
||||
email: '',
|
||||
password: '',
|
||||
isPersistent: true
|
||||
}
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
//this.rejectReason = api.rejectReason;
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onSubmit(form)
|
||||
{
|
||||
const accountStore = useAccountStore();
|
||||
|
||||
this.rejectReason = null;
|
||||
form.setState('loading');
|
||||
|
||||
const response = await api.login(this.model);
|
||||
|
||||
if (response.success)
|
||||
{
|
||||
const userResponse = await api.getUser();
|
||||
|
||||
if (userResponse.success)
|
||||
{
|
||||
this.zero.events.emit('zero.authenticate', userResponse.data);
|
||||
form.handle(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
form.handle(response);
|
||||
}
|
||||
|
||||
//this.state = 'error';
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.app-auth
|
||||
{
|
||||
grid-column: span 2 / auto;
|
||||
align-self: stretch;
|
||||
justify-self: stretch;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-page);
|
||||
|
||||
/*&:before
|
||||
{
|
||||
content: 'login';
|
||||
font-family: var(--font-headline);
|
||||
color: rgba(black, 0.008);
|
||||
position: fixed;
|
||||
left: -10vw;
|
||||
bottom: -30vh;
|
||||
font-size: 150vh;
|
||||
line-height: 150vh;
|
||||
font-weight: bold;
|
||||
}*/
|
||||
}
|
||||
|
||||
.app-auth-inner
|
||||
{
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
align-items: stretch;
|
||||
max-width: 100%;
|
||||
width: 420px;
|
||||
background: var(--color-box);
|
||||
box-shadow: var(--shadow-short);
|
||||
border-radius: var(--radius);
|
||||
/*border: 1px solid var(--color-line);*/
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: var(--padding);
|
||||
color: var(--color-text);
|
||||
/*box-shadow: 0 0 60px var(--color-shadow);*/
|
||||
}
|
||||
|
||||
.app-auth-logo
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0 3rem 0;
|
||||
}
|
||||
|
||||
.app-auth-image
|
||||
{
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.app-auth .ui-property + .ui-property
|
||||
{
|
||||
padding-top: 0;
|
||||
margin-top: var(--padding);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.app-auth-bottom
|
||||
{
|
||||
margin: calc(-1 * var(--padding));
|
||||
margin-top: 3rem;
|
||||
padding: var(--padding);
|
||||
background: var(--color-box-nested);
|
||||
border-top: 1px solid var(--color-line-onbg);
|
||||
border-bottom-left-radius: var(--radius);
|
||||
border-bottom-right-radius: var(--radius);
|
||||
}
|
||||
|
||||
.app-auth .ui-message
|
||||
{
|
||||
margin: -16px 0 var(--padding);
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import { AccountStoreState } from 'zero/account';
|
||||
|
||||
export const useAccountStore = defineStore('zero.account', {
|
||||
state: () => ({
|
||||
user: undefined
|
||||
} as AccountStoreState),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: state => state.user != null
|
||||
}
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<div class="app" v-if="!loading" :class="{ 'is-preview': preview}">
|
||||
<app-login v-if="!authenticated" />
|
||||
<template v-if="authenticated && !preview">
|
||||
<app-navigation />
|
||||
<div class="app-main">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
<app-overlays />
|
||||
<app-notifications />
|
||||
</template>
|
||||
<template v-if="authenticated && preview">
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
</div>
|
||||
<div class="app-loading" v-else>
|
||||
<ui-loading />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import './styles/styles';
|
||||
import AppLogin from './account/login.vue';
|
||||
import AppNavigation from './ui/app-navigation.vue';
|
||||
import AppNotifications from './ui/app-notifications.vue';
|
||||
import AppOverlays from './ui/app-overlays.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import startup from './startup';
|
||||
import { AccountUser } from 'zero/account';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'app',
|
||||
|
||||
components: { AppLogin, AppNavigation, AppNotifications, AppOverlays },
|
||||
|
||||
data: () => ({
|
||||
loading: true,
|
||||
authenticated: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
preview()
|
||||
{
|
||||
return this.$route.name === 'preview';
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.startup();
|
||||
|
||||
this.zero.events.on('zero.authenticate', this.onAuthentication);
|
||||
this.zero.events.on('zero.rejectauth', () => this.authenticated = false);
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
async startup()
|
||||
{
|
||||
const result = await startup(this.zero);
|
||||
this.authenticated = result.authenticated;
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
|
||||
async onAuthentication(user?: AccountUser)
|
||||
{
|
||||
await this.startup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.app-loading
|
||||
{
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,93 +0,0 @@
|
||||
import { AxiosError } from 'axios';
|
||||
import Axios from 'axios';
|
||||
import eventHub from './services/eventhub';
|
||||
import { open as openOverlay } from './services/overlay';
|
||||
//import Auth from 'zero/helpers/auth.js';
|
||||
import Qs from 'qs';
|
||||
|
||||
//if (!__zero || !__zero.apiPath)
|
||||
//{
|
||||
// throw Exception('window.zero and zero.apiPath (= base path to the backoffice API) have to be configured');
|
||||
//}
|
||||
|
||||
//Axios.defaults.baseURL = '/zero/api/';
|
||||
Axios.defaults.withCredentials = true;
|
||||
|
||||
Axios.defaults.paramsSerializer = (params) =>
|
||||
{
|
||||
return Qs.stringify(params, { allowDots: true });
|
||||
};
|
||||
|
||||
Axios.interceptors.response.use(
|
||||
response => response,
|
||||
(error: AxiosError) =>
|
||||
{
|
||||
const isApi = error.response && error.response.headers['x-variant'] == 'api-response';
|
||||
|
||||
if (isApi && error.response && error.response.status >= 500)
|
||||
{
|
||||
const errorData = error.response.data && error.response.data.errors ? error.response.data.errors[0] : {
|
||||
category: 'server',
|
||||
code: 'server.error.axios',
|
||||
message: 'Unknown server error',
|
||||
propery: null
|
||||
};
|
||||
|
||||
openOverlay({
|
||||
alias: 'servererror',
|
||||
model: errorData,
|
||||
autoclose: true,
|
||||
softdismiss: true,
|
||||
component: () => import('./ui/overlays/servererror.vue')
|
||||
});
|
||||
}
|
||||
|
||||
if (isApi)
|
||||
{
|
||||
return Promise.resolve(error.response);
|
||||
}
|
||||
|
||||
if (error.response && error.response.status === 401)
|
||||
{
|
||||
console.error('[zero.axios] Auth failed. Please login again.');
|
||||
eventHub.emit('zero.rejectauth');
|
||||
return Promise.reject(error.response);
|
||||
//Auth.rejectUser("@login.rejectReasons.inactive");
|
||||
//Notification.error('Authentication failed. Please login again.', 3);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
Axios.interceptors.request.use(config =>
|
||||
{
|
||||
if (!config.params)
|
||||
{
|
||||
config.params = {};
|
||||
}
|
||||
|
||||
let locationQuery = Qs.parse(location.search.substring(1));
|
||||
let appKey = 'system';
|
||||
|
||||
// set app key to system when required
|
||||
if (config.params['zero.system'] === true || locationQuery['zero.shared'] === "true")
|
||||
{
|
||||
delete config.params['zero.system'];
|
||||
appKey = 'system';
|
||||
}
|
||||
|
||||
// rewrite URLs to replace {app} placeholder
|
||||
if (config.url != null && config.url.indexOf('{app}') > -1)
|
||||
{
|
||||
config.url = config.url.replace('{app}', appKey);
|
||||
}
|
||||
if (config.baseURL != null && config.baseURL.indexOf('{app}') > -1)
|
||||
{
|
||||
config.baseURL = config.baseURL.replace('{app}', appKey);
|
||||
}
|
||||
|
||||
return config;
|
||||
}, error => Promise.reject(error));
|
||||
|
||||
export default Axios;
|
||||
@@ -1,99 +0,0 @@
|
||||
import uiIcon from './ui-icon.vue';
|
||||
import uiLoading from './ui-loading.vue';
|
||||
import uiTab from './ui-tab.vue';
|
||||
import uiTabs from './ui-tabs.vue';
|
||||
import uiInlineTabs from './ui-inline-tabs.vue';
|
||||
import uiButton from './ui-button.vue';
|
||||
import uiDotButton from './ui-dot-button.vue';
|
||||
import uiIconButton from './ui-icon-button.vue';
|
||||
import uiSelectButton from './ui-select-button.vue';
|
||||
import uiStateButton from './ui-state-button.vue';
|
||||
import uiRadioButton from './ui-radio-button.vue';
|
||||
import uiAddButton from './ui-add-button.vue';
|
||||
import uiMessage from './ui-message.vue';
|
||||
import uiPagination from './ui-pagination.vue';
|
||||
import uiError from './ui-error.vue';
|
||||
import uiLocalize from './ui-localize.vue';
|
||||
import uiThumbnail from './ui-thumbnail.vue';
|
||||
import uiDropdownSeparator from './ui-dropdown-separator.vue';
|
||||
import uiDropdownButton from './ui-dropdown-button.vue';
|
||||
import uiDropdown from './ui-dropdown.vue';
|
||||
import uiHeaderBar from './ui-header-bar.vue';
|
||||
import uiCalendar from './ui-calendar.vue';
|
||||
import uiTree from './ui-tree.vue';
|
||||
import uiTreeItem from './ui-tree-item.vue';
|
||||
import uiTable from './ui-table.vue';
|
||||
import uiTableFilter from './ui-table-filter.vue';
|
||||
import uiTrinity from './ui-trinity.vue';
|
||||
import uiPick from './ui-pick.vue';
|
||||
import uiDatagrid from './ui-datagrid.vue';
|
||||
import uiProgress from './ui-progress.vue';
|
||||
import uiDate from './ui-date.vue';
|
||||
import uiRte from './ui-rte/ui-rte.vue';
|
||||
import uiCheckList from './ui-check-list.vue';
|
||||
import uiSearch from './ui-search.vue';
|
||||
import uiToggle from './ui-toggle.vue';
|
||||
import uiProperty from './ui-property.vue';
|
||||
import uiColorpicker from './ui-colorpicker.vue';
|
||||
import uiDatepicker from './ui-datepicker.vue';
|
||||
import uiDaterangepicker from './ui-daterangepicker.vue';
|
||||
import uiTimepicker from './ui-timepicker.vue';
|
||||
import uiSelect from './ui-select.vue';
|
||||
import uiForm from './ui-form.vue';
|
||||
import uiFormHeader from './ui-form-header.vue';
|
||||
import uiFormHeaderLinks from './ui-form-header-links.vue';
|
||||
import uiLink from './ui-link.vue';
|
||||
import uiInputList from './ui-input-list.vue';
|
||||
import uiTags from './ui-tags.vue';
|
||||
import uiIconpicker from './ui-iconpicker.vue';
|
||||
|
||||
export {
|
||||
uiIcon,
|
||||
uiLoading,
|
||||
uiTab,
|
||||
uiTabs,
|
||||
uiInlineTabs,
|
||||
uiButton,
|
||||
uiDotButton,
|
||||
uiIconButton,
|
||||
uiSelectButton,
|
||||
uiStateButton,
|
||||
uiRadioButton,
|
||||
uiAddButton,
|
||||
uiMessage,
|
||||
uiPagination,
|
||||
uiError,
|
||||
uiLocalize,
|
||||
uiThumbnail,
|
||||
uiDropdownSeparator,
|
||||
uiDropdownButton,
|
||||
uiDropdown,
|
||||
uiHeaderBar,
|
||||
uiCalendar,
|
||||
uiTree,
|
||||
uiTreeItem,
|
||||
uiTable,
|
||||
uiTableFilter,
|
||||
uiTrinity,
|
||||
uiPick,
|
||||
uiDatagrid,
|
||||
uiProgress,
|
||||
uiDate,
|
||||
uiRte,
|
||||
uiCheckList,
|
||||
uiSearch,
|
||||
uiToggle,
|
||||
uiProperty,
|
||||
uiColorpicker,
|
||||
uiDatepicker,
|
||||
uiDaterangepicker,
|
||||
uiTimepicker,
|
||||
uiSelect,
|
||||
uiForm,
|
||||
uiFormHeader,
|
||||
uiFormHeaderLinks,
|
||||
uiLink,
|
||||
uiInputList,
|
||||
uiTags,
|
||||
uiIconpicker
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
import { App } from 'vue';
|
||||
import * as components from './index';
|
||||
|
||||
export default function (app: App)
|
||||
{
|
||||
for (var key in components)
|
||||
{
|
||||
let component = components[key];
|
||||
|
||||
app.component(key, component.default || component);
|
||||
}
|
||||
};
|
||||
@@ -1,213 +0,0 @@
|
||||
<template>
|
||||
|
||||
<ui-button v-if="!hasDropdown" :type="type" :label="label" @click="onConfirm(null, false)" :disabled="disabled" />
|
||||
|
||||
<ui-dropdown v-else ref="dropdown" align="right">
|
||||
|
||||
<template v-slot:button>
|
||||
<slot>
|
||||
<ui-button :label="label" :type="type" :disabled="disabled" />
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="flavors.length > 1">
|
||||
<ui-dropdown-button v-for="(flavor, index) in flavors" :key="index" :icon="flavor.icon" :value="flavor" :label="flavor.name" :prevent="true" @click="selectFlavor" :selected="selectedflavor == flavor.alias" />
|
||||
<template v-if="allowBlueprint">
|
||||
<ui-dropdown-separator />
|
||||
<div class="ui-decision">
|
||||
<ui-toggle v-model:on="asBlueprint" :on-content="'Shared'" :off-content="'Not shared'" />
|
||||
</div>
|
||||
</template>
|
||||
<ui-dropdown-separator />
|
||||
<div class="ui-decision-button">
|
||||
<ui-button label="@addoverlay.gotoeditor" type="accent" icon="fth-arrow-right" :disabled="disabled" @click="onConfirm(selectedflavor, asBlueprint)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="ui-add-button-items">
|
||||
<button type="button" class="ui-add-button-item" @click="onConfirm(null, true)" :disabled="disabled">
|
||||
<ui-icon symbol="fth-cloud" :size="20" />
|
||||
<span class="-text">Shared</span>
|
||||
</button>
|
||||
<span class="ui-add-button-items-line"></span>
|
||||
<button type="button" class="ui-add-button-item" @click="onConfirm(null, false)" :disabled="disabled">
|
||||
<ui-icon symbol="fth-arrow-right" :size="20" />
|
||||
<span class="-text">Local</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ui-dropdown>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { useUiStore } from '../ui/store';
|
||||
import { defineComponent } from 'vue';
|
||||
import { UiFlavorProvider } from 'zero/ui';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '@ui.add'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'accent'
|
||||
},
|
||||
route: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
alias: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
flavors: [],
|
||||
loading: false,
|
||||
asBlueprint: false,
|
||||
allowBlueprint: false,
|
||||
selectedflavor: null,
|
||||
store: null
|
||||
}),
|
||||
|
||||
computed: {
|
||||
hasDropdown()
|
||||
{
|
||||
return this.flavors.length > 1 || this.allowBlueprint;
|
||||
}
|
||||
},
|
||||
|
||||
async created()
|
||||
{
|
||||
this.store = useUiStore();
|
||||
this.buildFlavors();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
buildFlavors()
|
||||
{
|
||||
this.flavors = this.store.getFlavors(this.alias);
|
||||
this.allowBlueprint = this.store.blueprints.indexOf(this.alias) > -1;
|
||||
|
||||
if (this.flavors.length > 0)
|
||||
{
|
||||
this.selectedflavor = this.flavors[0].alias;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
selectFlavor(flavor, opts)
|
||||
{
|
||||
this.selectedflavor = flavor.alias;
|
||||
},
|
||||
|
||||
|
||||
onConfirm(flavor, shared)
|
||||
{
|
||||
if (!!this.route)
|
||||
{
|
||||
let routeObj = typeof this.route === 'object' ? this.route : { name: this.route };
|
||||
routeObj.query = routeObj.query || {};
|
||||
if (flavor)
|
||||
{
|
||||
routeObj.query['zero.flavor'] = flavor;
|
||||
}
|
||||
if (shared)
|
||||
{
|
||||
routeObj.query['zero.scope'] = 'system';
|
||||
}
|
||||
this.$router.push(routeObj);
|
||||
}
|
||||
|
||||
this.$emit('click', { flavor, shared });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.ui-decision
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 16px;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
.ui-decision-button
|
||||
{
|
||||
padding: 16px 16px;
|
||||
}
|
||||
|
||||
.ui-dropdown.theme-dark .ui-decision
|
||||
{
|
||||
.ui-toggle-switch:not(.is-active)
|
||||
{
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
input:focus + .ui-toggle-switch
|
||||
{
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-add-button
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ui-add-button-items
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1px 1fr;
|
||||
}
|
||||
|
||||
.ui-add-button-item
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px 10px;
|
||||
font-size: var(--font-size);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.ui-add-button-item .ui-icon
|
||||
{
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ui-add-button-item .is-primary
|
||||
{
|
||||
font-size: 24px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ui-add-button-item:hover
|
||||
{
|
||||
background: var(--color-dropdown-selected);
|
||||
}
|
||||
|
||||
.ui-add-button-items-line
|
||||
{
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--color-line);
|
||||
}
|
||||
</style>
|
||||
@@ -1,166 +0,0 @@
|
||||
<template>
|
||||
<button :type="buttonType" class="ui-button has-state" :class="buttonClass" :disabled="disabled || state == 'loading' || !isDefaultState" @click="tryClick">
|
||||
<span v-if="label" class="ui-button-text" v-localize:html="{ key: label, tokens: tokens }"></span>
|
||||
<ui-icon v-if="caret" :symbol="caretSymbol" class="ui-button-caret" />
|
||||
<ui-icon v-if="icon" :symbol="icon" class="ui-button-icon" :stroke="stroke" />
|
||||
<span v-if="!isDefaultState" class="ui-button-state">
|
||||
<i v-if="stateDisplay == 'loading'" class="ui-button-progress"></i>
|
||||
<ui-icon v-if="stateDisplay == 'success'" symbol="fth-check" />
|
||||
<ui-icon v-if="stateDisplay == 'error'" symbol="fth-x" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
const STATE_DEFAULT = 'default';
|
||||
const STATE_LOADING = 'loading';
|
||||
const STATE_SUCCESS = 'success';
|
||||
const STATE_ERROR = 'error';
|
||||
const STATES = [STATE_DEFAULT, STATE_LOADING, STATE_SUCCESS, STATE_ERROR];
|
||||
|
||||
export default {
|
||||
name: 'uiButton',
|
||||
|
||||
props: {
|
||||
label: {
|
||||
type: [String, Object]
|
||||
},
|
||||
state: {
|
||||
type: String,
|
||||
default: STATE_DEFAULT
|
||||
},
|
||||
submit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'action'
|
||||
},
|
||||
icon: {
|
||||
type: String
|
||||
},
|
||||
stroke: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
caret: String,
|
||||
caretPosition: {
|
||||
type: String,
|
||||
default: 'right'
|
||||
},
|
||||
disabled: Boolean,
|
||||
stateDuration: {
|
||||
type: Number,
|
||||
default: 2000
|
||||
},
|
||||
ellipsis: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
attach: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tokens: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
stateDisplay: null,
|
||||
stateTimeout: null
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
this.stateDisplay = this.state;
|
||||
},
|
||||
|
||||
computed: {
|
||||
buttonType()
|
||||
{
|
||||
return this.submit ? 'submit' : 'button';
|
||||
},
|
||||
buttonClass()
|
||||
{
|
||||
let classes = [];
|
||||
classes.push('type-' + this.type.split(' ').join(' type-'));
|
||||
classes.push('state-' + (this.isDefaultState ? STATE_DEFAULT : this.stateDisplay));
|
||||
|
||||
if (this.caret)
|
||||
{
|
||||
classes.push('caret-' + this.caretPosition);
|
||||
}
|
||||
if (this.icon)
|
||||
{
|
||||
classes.push('has-icon');
|
||||
}
|
||||
if (this.ellipsis)
|
||||
{
|
||||
classes.push('has-ellipsis');
|
||||
}
|
||||
if (!this.label)
|
||||
{
|
||||
classes.push('no-label');
|
||||
}
|
||||
if (this.attach)
|
||||
{
|
||||
classes.push('is-attached');
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
caretSymbol()
|
||||
{
|
||||
return 'fth-chevron-' + this.caret;
|
||||
},
|
||||
isDefaultState()
|
||||
{
|
||||
return !this.stateDisplay || this.stateDisplay === STATE_DEFAULT || STATES.indexOf(this.stateDisplay) < 0;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
state(value)
|
||||
{
|
||||
this.stateDisplay = value;
|
||||
|
||||
clearTimeout(this.stateTimeout);
|
||||
|
||||
if (value && STATES.indexOf(value) < 0)
|
||||
{
|
||||
console.warn(`ui-button: Supported states are "${STATES.join('", "')}"`);
|
||||
}
|
||||
if (value === STATE_SUCCESS || value === STATE_ERROR)
|
||||
{
|
||||
this.stateTimeout = setTimeout(() =>
|
||||
{
|
||||
//const stateUpdate = 'update:state';
|
||||
|
||||
//// check if :state property has .sync modifier
|
||||
//let listeners = this.$options._parentListeners;
|
||||
//const hasSyncModifier = listeners && listeners[stateUpdate];
|
||||
|
||||
//if (!hasSyncModifier)
|
||||
//{
|
||||
// console.warn(`ui-button: Add the .sync modifier to the "state" property, as changing the state to "${STATE_SUCCESS}" or "${STATE_ERROR}" will automatically set it back to "${STATE_DEFAULT}" after the state duration timeout`);
|
||||
//}
|
||||
|
||||
this.stateDisplay = STATE_DEFAULT;
|
||||
}, this.stateDuration);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
tryClick(ev)
|
||||
{
|
||||
this.$emit('click', ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div ref="calendar" class="ui-calendar"></div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import flatpickr from 'flatpickr';
|
||||
import { German } from "flatpickr/dist/l10n/de.js"
|
||||
import { extendObject } from '../utils/objects';
|
||||
import { useAccountStore } from '../account/store';
|
||||
|
||||
export default {
|
||||
name: 'uiCalendar',
|
||||
|
||||
props: {
|
||||
today: String,
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
date: null,
|
||||
initialized: false
|
||||
}),
|
||||
|
||||
mounted()
|
||||
{
|
||||
let vm = this;
|
||||
|
||||
if (this.initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const user = useAccountStore();
|
||||
|
||||
flatpickr(this.$refs.calendar, extendObject({
|
||||
inline: true,
|
||||
enableTime: true,
|
||||
noCalendar: false,
|
||||
time_24hr: true,
|
||||
defaultDate: this.today,
|
||||
minuteIncrement: 1,
|
||||
locale: user.user && user.user.culture == 'de-DE' ? German : null,
|
||||
onChange(dates)
|
||||
{
|
||||
vm.$emit('change', dates[0]);
|
||||
},
|
||||
}, this.options));
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-calendar
|
||||
{
|
||||
}
|
||||
</style>
|
||||
@@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-check-list" :class="{'is-disabled': disabled, 'is-inline': inline }">
|
||||
<label v-for="item in list" class="ui-native-check ui-check-list-item">
|
||||
<input type="checkbox" :checked="isChecked(item)" @input="onChange(item)" :disabled="disabled" />
|
||||
<span class="ui-native-check-toggle"></span>
|
||||
<span v-localize="item[labelKey]"></span>
|
||||
<span class="-desc" v-if="item[descriptionKey]" v-localize="item[descriptionKey]"></span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiCheckList',
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
items: {
|
||||
type: [Array, Function, Promise],
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
reverse: Boolean,
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
descriptionKey: {
|
||||
type: String,
|
||||
default: 'description'
|
||||
},
|
||||
idKey: {
|
||||
type: String,
|
||||
default: 'key'
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
list: []
|
||||
}),
|
||||
|
||||
watch: {
|
||||
items()
|
||||
{
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init()
|
||||
{
|
||||
if (typeof this.items === 'function')
|
||||
{
|
||||
this.items().then(res =>
|
||||
{
|
||||
this.list = res.data;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.list = JSON.parse(JSON.stringify(this.items));
|
||||
}
|
||||
},
|
||||
|
||||
isChecked(item)
|
||||
{
|
||||
let index = this.value.indexOf(item[this.idKey]);
|
||||
return (!this.reverse && index > -1) || (this.reverse && index < 0);
|
||||
},
|
||||
|
||||
onChange(item)
|
||||
{
|
||||
let index = this.value.indexOf(item[this.idKey]);
|
||||
let value = JSON.parse(JSON.stringify(this.value));
|
||||
if (index < 0)
|
||||
{
|
||||
value.push(item[this.idKey]);
|
||||
}
|
||||
else
|
||||
{
|
||||
value.splice(index, 1);
|
||||
}
|
||||
|
||||
this.$emit('input', value);
|
||||
this.$emit('update:value', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-check-list-item
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ui-check-list .ui-check-list-item + .ui-check-list-item
|
||||
{
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.ui-alias + .ui-check-list-item
|
||||
{
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.ui-check-list.is-inline .ui-check-list-item
|
||||
{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ui-check-list.is-inline .ui-check-list-item + .ui-check-list-item
|
||||
{
|
||||
margin-top: 0;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.ui-check-list.is-inline .ui-check-list-item .ui-native-check-toggle
|
||||
{
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.ui-check-list-item .-desc
|
||||
{
|
||||
display: inline-block;
|
||||
color: var(--color-text-dim);
|
||||
font-size: var(--font-size-s);
|
||||
margin-left: 0.5em;
|
||||
|
||||
&:before { content: '('; }
|
||||
&:after { content: ')'; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,123 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-colorpicker" :class="{'is-disabled': disabled }">
|
||||
<label class="ui-colorpicker-color" :for="id">
|
||||
<span class="ui-colorpicker-color-preview" :style="{ 'background-color': value || 'var(--color-box)' }"></span>
|
||||
<input :id="id" type="color" :value="value" :disabled="disabled" @input="onChange" />
|
||||
</label>
|
||||
<input type="text" maxlength="7" class="ui-colorpicker-input" :value="value" @input="onChange" :disabled="disabled" v-localize:placeholder="'@colorpicker.placeholder'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { generateId } from '../utils/numbers';
|
||||
|
||||
export default {
|
||||
name: 'uiColorpicker',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () =>
|
||||
{
|
||||
return {
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
id: null
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
this.id = 'colorpicker-' + generateId();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onChange(ev)
|
||||
{
|
||||
this.$emit('change', ev.target.value);
|
||||
this.$emit('input', ev.target.value);
|
||||
// TODO this does not trigger the forms dirty flag
|
||||
}
|
||||
|
||||
//pick()
|
||||
//{
|
||||
// this.$el.
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-colorpicker
|
||||
{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui-colorpicker-color
|
||||
{
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
left: 0;
|
||||
top: -1px;
|
||||
padding: 0 !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-colorpicker-color-preview
|
||||
{
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
box-shadow: var(--shadow-short);
|
||||
}
|
||||
|
||||
.ui-colorpicker-color input
|
||||
{
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
input[type="text"].ui-colorpicker-input
|
||||
{
|
||||
padding-left: 40px;
|
||||
max-width: 322px;
|
||||
}
|
||||
|
||||
|
||||
.ui-colorpicker-preview
|
||||
{
|
||||
display: inline-block;
|
||||
margin-top: -11px;
|
||||
border: 1px solid #eceaea;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,453 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-datagrid-outer" :class="{'is-selecting': configuration.selectable && selected.length > 0}">
|
||||
<div class="ui-datagrid">
|
||||
<div class="ui-datagrid-items" v-if="!isLoading" :style="'grid-template-columns: repeat(auto-fill, minmax(' + configuration.width + 'px, 1fr))'" :class="{'is-block': configuration.block }">
|
||||
<slot name="before"></slot>
|
||||
<div class="ui-datagrid-item" v-for="(item, index) in items" :key="index" v-on:contextmenu="onRightClicked(item, $event)">
|
||||
<button v-if="configuration.selectable && selected.length > 0" type="button" class="ui-datagrid-cell-select" @click.exact="select(item)" @click.shift="shiftSelect(item)"></button>
|
||||
<slot :item="item" :selected="configuration.selectable && selected.indexOf(item) > -1"></slot>
|
||||
</div>
|
||||
<slot name="below"></slot>
|
||||
</div>
|
||||
|
||||
<div class="ui-datagrid-empty" v-if="!isLoading && items.length < 1">
|
||||
<ui-icon symbol="fth-list" :size="34" class="ui-datagrid-empty-icon" />
|
||||
<span v-localize="'@ui.emptylist'"></span>
|
||||
</div>
|
||||
|
||||
<div class="ui-datagrid-loading" v-if="isLoading">
|
||||
<ui-loading />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="ui-datagrid-pagination" v-if="pages > 1">
|
||||
<ui-pagination :pages="pages" :page="filter.page" @change="setPage" />
|
||||
</footer>
|
||||
|
||||
<ui-dropdown ref="dropdown" align="top" theme="dark" class="ui-datagrid-dropdown">
|
||||
<slot name="actions" v-bind="actionProps"></slot>
|
||||
</ui-dropdown>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import UiPagination from './ui-pagination.vue';
|
||||
import { debounce, extendObject } from '../utils';
|
||||
import Qs from 'qs';
|
||||
|
||||
const defaultConfig = {
|
||||
order: {
|
||||
// allow sorting of columns (asc + desc)
|
||||
enabled: true,
|
||||
// default order by
|
||||
by: 'createdDate',
|
||||
// order is descending
|
||||
isDescending: true
|
||||
},
|
||||
// desired width of an item
|
||||
width: 280,
|
||||
// scroll to top on page change
|
||||
scrollToTop: true,
|
||||
// promise which returns items based on the current filter and sorting
|
||||
items: null,
|
||||
// for block items
|
||||
block: false,
|
||||
// ability to select items
|
||||
selectable: false,
|
||||
// whether the query params should be set for page + search
|
||||
setQuery: true,
|
||||
// custom scroll container
|
||||
scrollContainerSelector: null,
|
||||
// filter
|
||||
page: 1,
|
||||
pageSize: 30
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'uiDatagrid',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () =>
|
||||
{
|
||||
return defaultConfig;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
components: { UiPagination },
|
||||
|
||||
watch: {
|
||||
'$route': function (val)
|
||||
{
|
||||
this.load(true);
|
||||
},
|
||||
'value.search': function (val)
|
||||
{
|
||||
this.filter.search = val;
|
||||
},
|
||||
'filter.search': function (val)
|
||||
{
|
||||
//this.filter.page = 1;
|
||||
this.debouncedUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
configuration: {},
|
||||
items: [],
|
||||
isLoading: true,
|
||||
pages: 1,
|
||||
count: 0,
|
||||
loaded: false,
|
||||
filter: {
|
||||
orderBy: null,
|
||||
orderIsDescending: true,
|
||||
page: 1,
|
||||
pageSize: 30,
|
||||
search: null
|
||||
},
|
||||
debouncedUpdate: null,
|
||||
actionProps: {
|
||||
item: null,
|
||||
selected: false
|
||||
},
|
||||
selected: []
|
||||
}),
|
||||
|
||||
|
||||
computed: {
|
||||
actionsDefined()
|
||||
{
|
||||
return this.$slots.hasOwnProperty('actions');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.setup();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
async setup()
|
||||
{
|
||||
this.debouncedUpdate = debounce(this.update, 300);
|
||||
|
||||
this.configuration = extendObject(JSON.parse(JSON.stringify(defaultConfig)), this.value);
|
||||
|
||||
if (this.configuration.order.enabled)
|
||||
{
|
||||
this.filter.orderBy = this.configuration.order.by;
|
||||
this.filter.orderIsDescending = this.configuration.order.isDescending;
|
||||
}
|
||||
|
||||
await this.load(true);
|
||||
},
|
||||
|
||||
// load items based on the current filter
|
||||
async load(initial)
|
||||
{
|
||||
if (!this.loaded && !initial)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (initial)
|
||||
{
|
||||
this.filter.page = (!this.configuration.setQuery ? null : +this.$route.query.page) || this.configuration.page || 1;
|
||||
this.filter.pageSize = this.configuration.pageSize || 30;
|
||||
this.filter.search = (!this.configuration.setQuery ? null : this.$route.query.search) || this.configuration.search;
|
||||
}
|
||||
|
||||
const result = await this.configuration.items(this.filter);
|
||||
|
||||
this.pages = result.paging.totalPages;
|
||||
this.count = result.paging.totalItems;
|
||||
this.$emit('count', this.count);
|
||||
|
||||
this.isLoading = false;
|
||||
this.items = result.data;
|
||||
|
||||
if (!initial && this.configuration.scrollToTop)
|
||||
{
|
||||
let container = document.querySelector(this.configuration.scrollContainerSelector || '.app-main');
|
||||
|
||||
if (container)
|
||||
{
|
||||
this.$nextTick(() => container.scrollTo({ top: 0, behavior: 'smooth' }));
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
this.loaded = true;
|
||||
}, 500);
|
||||
},
|
||||
|
||||
// updates the list (debounced)
|
||||
async update()
|
||||
{
|
||||
if (!this.isLoading)
|
||||
{
|
||||
await this.load();
|
||||
}
|
||||
},
|
||||
|
||||
// set the active page
|
||||
setPage(index)
|
||||
{
|
||||
this.filter.page = index;
|
||||
this.onChange();
|
||||
},
|
||||
|
||||
// search
|
||||
search(query)
|
||||
{
|
||||
this.filter.search = query;
|
||||
this.onChange();
|
||||
},
|
||||
|
||||
// sort by a column
|
||||
sort(column)
|
||||
{
|
||||
if (this.filter.orderBy === column.field && this.filter.orderIsDescending)
|
||||
{
|
||||
this.filter.orderIsDescending = false;
|
||||
}
|
||||
else if (this.filter.orderBy === column.field)
|
||||
{
|
||||
this.filter.orderBy = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.filter.orderBy = column.field;
|
||||
this.filter.orderIsDescending = true;
|
||||
}
|
||||
|
||||
this.onChange();
|
||||
},
|
||||
|
||||
|
||||
// right clicked on an item
|
||||
onRightClicked(item, ev)
|
||||
{
|
||||
if (this.actionsDefined)
|
||||
{
|
||||
ev.preventDefault();
|
||||
this.onActionsClicked(item, ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// a property has changed and therefore we update the URL and table content
|
||||
onChange()
|
||||
{
|
||||
if (this.configuration.setQuery)
|
||||
{
|
||||
let params = {};
|
||||
|
||||
if (+this.filter.page > 1)
|
||||
{
|
||||
params.page = +this.filter.page;
|
||||
}
|
||||
if (this.filter.search)
|
||||
{
|
||||
params.search = this.filter.search;
|
||||
}
|
||||
|
||||
const query = Qs.stringify(params);
|
||||
const path = this.$route.href.split('?')[0] + (query ? '?' + query : '');
|
||||
|
||||
history.replaceState(null, null, path);
|
||||
}
|
||||
this.debouncedUpdate();
|
||||
},
|
||||
|
||||
|
||||
// actions button clicked on item
|
||||
onActionsClicked(item, ev)
|
||||
{
|
||||
let dropdown = this.$refs.dropdown;
|
||||
|
||||
if (!this.actionsDefined || (typeof this.hasActions === 'function' && !this.hasActions(item)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionProps.item = item;
|
||||
this.actionProps.event = ev;
|
||||
this.actionProps.selected = this.configuration.selectable && this.selected.indexOf(item) > -1;
|
||||
|
||||
dropdown.toggle();
|
||||
|
||||
if (!dropdown.open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
let target = ev.target;
|
||||
do
|
||||
{
|
||||
if (target.classList.contains('ui-datagrid-cell'))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (target = target.parentElement);
|
||||
|
||||
var width = 240;
|
||||
|
||||
var position = {
|
||||
x: ev.pageX,
|
||||
y: ev.pageY
|
||||
};
|
||||
|
||||
let element = dropdown.$el.querySelector('.ui-dropdown');
|
||||
|
||||
element.style.top = position.y + 'px';
|
||||
element.style.left = position.x + 'px';
|
||||
element.style.width = width + 'px';
|
||||
});
|
||||
},
|
||||
|
||||
// toggle selection of an item
|
||||
select(item)
|
||||
{
|
||||
if (!item)
|
||||
{
|
||||
if (this.selected.length >= this.items.length)
|
||||
{
|
||||
this.selected = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected = [];
|
||||
this.items.forEach(item =>
|
||||
{
|
||||
this.selected.push(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const index = this.selected.indexOf(item);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
this.selected.splice(index, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('select', this.selected, this);
|
||||
},
|
||||
|
||||
shiftSelect(item)
|
||||
{
|
||||
return;
|
||||
|
||||
// TODO implement shift selection (this is only a part of it)
|
||||
// for selection area see Viselect https://simonwep.github.io/selection/
|
||||
|
||||
const last = this.selected[this.selected.length - 1];
|
||||
|
||||
if (last)
|
||||
{
|
||||
let startIndex = this.items.indexOf(last);
|
||||
let endIndex = this.items.indexOf(item);
|
||||
|
||||
if (startIndex > endIndex)
|
||||
{
|
||||
const tempIndex = endIndex;
|
||||
endIndex = startIndex;
|
||||
startIndex = tempIndex;
|
||||
}
|
||||
|
||||
for (let index = startIndex; index <= endIndex; index++)
|
||||
{
|
||||
this.selected.push(this.items[index]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
clearSelection()
|
||||
{
|
||||
this.selected = [];
|
||||
this.$emit('select', this.selected, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-datagrid-items
|
||||
{
|
||||
display: grid;
|
||||
gap: var(--padding-s);
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ui-datagrid-items.is-block
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ui-datagrid-item
|
||||
{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui-datagrid-empty, .ui-datagrid-loading
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 250px;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
font-size: var(--font-size);
|
||||
|
||||
.ui-loading
|
||||
{
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-datagrid-empty-icon
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ui-datagrid-dropdown .ui-dropdown
|
||||
{
|
||||
position: fixed;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.ui-datagrid-cell-select
|
||||
{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: transparent;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<span class="ui-date" v-html="output"></span>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { formatDate } from '../utils/dates';
|
||||
|
||||
export default {
|
||||
name: 'uiDate',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
split: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
output: null
|
||||
}),
|
||||
|
||||
watch: {
|
||||
value: function (value)
|
||||
{
|
||||
this.rebuild();
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.rebuild();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
rebuild()
|
||||
{
|
||||
if (!this.value)
|
||||
{
|
||||
this.output = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.split)
|
||||
{
|
||||
this.output = formatDate(this.value, this.format);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.output = formatDate(this.value, 'short') + ' <span class="-minor">' + formatDate(this.value, 'time') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-date .-minor
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
</style>
|
||||
@@ -1,204 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-datepicker" :class="{'is-disabled': disabled }">
|
||||
<input type="text" class="ui-input ui-datepicker-input" v-localize:placeholder="placeholder" :value="output" @input="onChange" @focus="onFocus" @blur="onBlur" :disabled="disabled" />
|
||||
<ui-icon v-if="!clear || !value" symbol="fth-calendar" class="ui-datepicker-icon" :size="17" />
|
||||
<button v-if="clear && value" type="button" class="ui-datepicker-input-button" @click="clearInput"><ui-icon symbol="fth-x" :size="15" /></button>
|
||||
|
||||
<ui-dropdown ref="overlay" class="ui-datepicker-overlay" @opened="overlayOpened">
|
||||
<ui-calendar :today="value" @change="onSelect" :options="pickerOptions" />
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { generateId } from '../utils/numbers';
|
||||
import { formatDate, toIsoDate } from '../utils/dates';
|
||||
|
||||
const DATETIME_FORMAT = 'DD.MM.YY HH:mm';
|
||||
const DATE_FORMAT = 'DD.MM.YY';
|
||||
|
||||
export default {
|
||||
name: 'uiDatepicker',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '@ui.date.select'
|
||||
},
|
||||
clear: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
time: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxDate: {
|
||||
type: [String, Date],
|
||||
default: null
|
||||
},
|
||||
minDate: {
|
||||
type: [String, Date],
|
||||
default: null
|
||||
},
|
||||
amPm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
data: () => ({
|
||||
id: null,
|
||||
output: null,
|
||||
pickerOptions: {}
|
||||
}),
|
||||
|
||||
|
||||
watch: {
|
||||
value()
|
||||
{
|
||||
this.updateOutput();
|
||||
},
|
||||
format()
|
||||
{
|
||||
this.updateOutput();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.id = 'datepicker-' + generateId();
|
||||
this.updateOptions();
|
||||
this.updateOutput();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
updateOutput()
|
||||
{
|
||||
this.output = formatDate(this.value, this.format || (this.time ? DATETIME_FORMAT : DATE_FORMAT));
|
||||
},
|
||||
|
||||
|
||||
updateOptions()
|
||||
{
|
||||
this.pickerOptions = {
|
||||
enableTime: this.time,
|
||||
maxDate: this.maxDate,
|
||||
minDate: this.minDate,
|
||||
time_24hr: !this.amPm
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
onSelect(date)
|
||||
{
|
||||
let dateStr = toIsoDate(date);
|
||||
//console.info(dateStr, date);
|
||||
this.setValue(dateStr);
|
||||
this.$refs.overlay.hide();
|
||||
document.activeElement.blur();
|
||||
},
|
||||
|
||||
|
||||
onChange(ev)
|
||||
{
|
||||
this.setValue(ev.target.value);
|
||||
},
|
||||
|
||||
|
||||
setValue(value)
|
||||
{
|
||||
this.$emit('change', value);
|
||||
this.$emit('input', value);
|
||||
},
|
||||
|
||||
|
||||
onFocus()
|
||||
{
|
||||
this.$refs.overlay.show();
|
||||
},
|
||||
|
||||
|
||||
onBlur()
|
||||
{
|
||||
if (!this.$refs.overlay.open)
|
||||
{
|
||||
this.$refs.overlay.hide();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
clearInput()
|
||||
{
|
||||
this.setValue(null);
|
||||
},
|
||||
|
||||
|
||||
overlayOpened()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
//pick()
|
||||
//{
|
||||
// this.$el.
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-datepicker
|
||||
{
|
||||
position: relative;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
input[type="text"].ui-datepicker-input
|
||||
{
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.ui-datepicker-icon
|
||||
{
|
||||
position: absolute;
|
||||
right: 13px;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.ui-datepicker-overlay .ui-dropdown
|
||||
{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-datepicker-input-button
|
||||
{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
font-size: var(--font-size);
|
||||
padding-top: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<ui-form ref="form" class="ui-daterangepicker" v-slot="form">
|
||||
<div class="ui-daterangepicker-items">
|
||||
<div :class="{ 'ui-split': options.rangeEnd }">
|
||||
<div class="ui-daterangepicker-group">
|
||||
<ui-property :label="options.fromText" :vertical="true">
|
||||
<ui-datepicker v-model="fromDate" v-bind="options" />
|
||||
</ui-property>
|
||||
</div>
|
||||
<div class="ui-daterangepicker-group" v-if="options.rangeEnd">
|
||||
<ui-property :label="options.toText" :vertical="true">
|
||||
<ui-datepicker v-model="toDate" v-bind="options" />
|
||||
</ui-property>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-confirm-buttons">
|
||||
<ui-button type="primary" label="@ui.confirm" @click="confirm"></ui-button>
|
||||
<ui-button type="light" label="@ui.close" @click="config.close"></ui-button>
|
||||
</div>
|
||||
</ui-form>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
model: Object,
|
||||
config: Object,
|
||||
},
|
||||
data: () => ({
|
||||
options: {},
|
||||
disabled: false,
|
||||
fromDate: null,
|
||||
toDate: null
|
||||
}),
|
||||
mounted()
|
||||
{
|
||||
this.options = this.model.options;
|
||||
this.fromDate = this.model.from;
|
||||
this.toDate = this.model.to;
|
||||
},
|
||||
methods: {
|
||||
confirm()
|
||||
{
|
||||
this.config.confirm({
|
||||
from: this.fromDate,
|
||||
to: this.toDate
|
||||
}, this.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-daterangepicker
|
||||
{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3.ui-daterangepicker-group-header
|
||||
{
|
||||
font-weight: 400;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
</style>
|
||||
@@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-daterangepicker" :class="{'is-disabled': disabled }">
|
||||
<button v-if="!inline" type="button" class="ui-link" @click="schedule" v-localize="scheduleLocalize" :disabled="disabled"></button>
|
||||
<div v-if="inline && value" class="ui-daterangepicker-inline">
|
||||
<div class="ui-daterangepicker-group">
|
||||
<ui-property :vertical="true">
|
||||
<ui-datepicker v-model="value.from" :time="time" />
|
||||
</ui-property>
|
||||
</div>
|
||||
<div class="ui-daterangepicker-group" v-if="rangeEnd">
|
||||
<ui-property :vertical="true">
|
||||
<ui-datepicker v-model="value.to" :time="time" />
|
||||
</ui-property>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { generateId } from '../utils/numbers';
|
||||
import { formatDate, toIsoDate } from '../utils/dates';
|
||||
import * as overlays from '../services/overlay';
|
||||
|
||||
const DATETIME_FORMAT = 'DD.MM.YY HH:mm';
|
||||
const DATE_FORMAT = 'DD.MM.YY';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'uiDaterangepicker',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: {
|
||||
from: null,
|
||||
to: null
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
time: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxDate: {
|
||||
type: [String, Date],
|
||||
default: null
|
||||
},
|
||||
minDate: {
|
||||
type: [String, Date],
|
||||
default: null
|
||||
},
|
||||
fromLabel: {
|
||||
type: String,
|
||||
default: '@ui.date.range_from'
|
||||
},
|
||||
toLabel: {
|
||||
type: String,
|
||||
default: '@ui.date.range_to'
|
||||
},
|
||||
amPm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rangeEnd: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
id: null,
|
||||
output: null,
|
||||
pickerOptions: {}
|
||||
}),
|
||||
|
||||
computed: {
|
||||
scheduleLocalize()
|
||||
{
|
||||
return {
|
||||
key: !this.value || (!this.value.from && !this.value.to) ? '@ui.date.set' :
|
||||
(this.value.from && !this.value.to ? '@ui.date.x' :
|
||||
(!this.value.from && this.value.to ? '@ui.date.y' : '@ui.date.xtoy')),
|
||||
tokens: {
|
||||
x: this.value ? formatDate(this.value.from, this.format || (this.time ? DATETIME_FORMAT : DATE_FORMAT)) : null,
|
||||
y: this.value ? formatDate(this.value.to, this.format || (this.time ? DATETIME_FORMAT : DATE_FORMAT)) : null
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
created()
|
||||
{
|
||||
this.id = 'daterangepicker-' + generateId();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async schedule()
|
||||
{
|
||||
const result = await overlays.open({
|
||||
component: DaterangepickerOverlay,
|
||||
model: {
|
||||
from: this.value.from,
|
||||
to: this.value.to,
|
||||
options: {
|
||||
format: this.format,
|
||||
time: this.time,
|
||||
max: this.maxDate,
|
||||
min: this.minDate,
|
||||
fromText: this.fromLabel,
|
||||
toText: this.toLabel,
|
||||
amPm: this.amPm,
|
||||
rangeEnd: this.rangeEnd
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.eventType == 'confirm')
|
||||
{
|
||||
this.$emit('change', result.value);
|
||||
this.$emit('input', result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-daterangepicker.is-primary .ui-link
|
||||
{
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
text-decoration-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.ui-daterangepicker-inline
|
||||
{
|
||||
display: flex;
|
||||
gap: var(--padding-s);
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<button type="button" class="ui-dot-button" :disabled="disabled" @click="tryClick" v-localize:title="title">
|
||||
<span class="sr-only" v-localize="title"></span>
|
||||
<ui-icon class="ui-button-icon" symbol="fth-more-horizontal" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiDotButton',
|
||||
|
||||
props: {
|
||||
state: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '@ui.actions'
|
||||
},
|
||||
disabled: Boolean
|
||||
},
|
||||
|
||||
mounted ()
|
||||
{
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
tryClick(ev)
|
||||
{
|
||||
this.$emit('click', ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,289 +0,0 @@
|
||||
<template>
|
||||
<button v-if="!confirming" :disabled="disabled" type="button" @click.stop="onClick" class="ui-dropdown-button" :class="{ 'has-icon': icon, 'is-active': selected, 'is-multiline': multiline }">
|
||||
<ui-icon v-if="icon" :symbol="icon" class="ui-dropdown-button-icon" />
|
||||
<span class="-name"><ui-localize :value="label" /><span v-if="confirm && !confirming"> …</span></span>
|
||||
<ui-icon v-if="selected" symbol="fth-check" class="ui-dropdown-button-selected" />
|
||||
<i v-if="loading" class="ui-dropdown-button-progress"></i>
|
||||
</button>
|
||||
<div v-if="confirming" class="ui-dropdown-button ui-dropdown-button-confirmation">
|
||||
<ui-icon v-if="icon" :symbol="icon" class="ui-dropdown-button-icon" />
|
||||
<span class="-name"><ui-localize :value="label" /><span>?</span></span>
|
||||
<ui-button type="small light" icon="fth-x" title="@ui.cancel" @click="confirming=false" />
|
||||
<ui-button :type="negative ? 'small danger' : 'small primary'" icon="fth-check" title="@ui.ok" @click="onClick($event, true)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiDropdownButton',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
confirm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
negative: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
prevent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
dropdown: null,
|
||||
loading: false,
|
||||
confirming: false
|
||||
}),
|
||||
|
||||
mounted ()
|
||||
{
|
||||
// find parent dropdown so we can pass it on item click
|
||||
let current = this;
|
||||
do
|
||||
{
|
||||
if (current.$options.name === 'uiDropdown')
|
||||
{
|
||||
this.dropdown = current;
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (current = current.$parent);
|
||||
|
||||
if (!this.dropdown)
|
||||
{
|
||||
console.warn('ui-dropdown-button: Could not find parent <ui-dropdown />');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onClick(e, confirmed)
|
||||
{
|
||||
var instance = this;
|
||||
|
||||
if (!this.loading && !this.disabled)
|
||||
{
|
||||
if (this.confirm && !confirmed)
|
||||
{
|
||||
this.confirming = true;
|
||||
//var confirmed = window.confirm('confirm?');
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.confirming = false;
|
||||
}
|
||||
|
||||
if (!this.prevent && this.dropdown)
|
||||
{
|
||||
this.dropdown.hide();
|
||||
}
|
||||
|
||||
let self = this;
|
||||
|
||||
this.$emit('click', this.value, {
|
||||
dropdown: this.dropdown,
|
||||
hide()
|
||||
{
|
||||
if (self.dropdown)
|
||||
{
|
||||
self.dropdown.hide();
|
||||
}
|
||||
instance.$emit('hide');
|
||||
},
|
||||
loading(isLoading)
|
||||
{
|
||||
instance.loading = isLoading;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
button.ui-dropdown-button, a.ui-dropdown-button
|
||||
{
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font-size: var(--font-size);
|
||||
font-weight: 500;
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
color: var(--color-text);
|
||||
border-radius: var(--radius);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
|
||||
.-minor
|
||||
{
|
||||
display: block;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-dim-one);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.has-icon
|
||||
{
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto;
|
||||
|
||||
&:not([disabled]):hover .ui-dropdown-button-icon
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.has-icon.is-negative:not([disabled]):hover .ui-dropdown-button-icon
|
||||
{
|
||||
color: var(--color-accent-error);
|
||||
}
|
||||
|
||||
&.is-multiline
|
||||
{
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&:not([disabled]):hover, &.is-active, &:focus
|
||||
{
|
||||
background: var(--color-dropdown-selected);
|
||||
color: var(--color-text);
|
||||
//font-weight: 700;
|
||||
|
||||
.ui-dropdown-list-item-icon
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active
|
||||
{
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.is-active .ui-dropdown-button-icon
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&[disabled]
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
|
||||
.ui-dropdown-list-item-icon,
|
||||
.ui-dropdown-button-icon
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
}
|
||||
|
||||
& + .ui-dropdown-button
|
||||
{
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-dropdown-button-icon
|
||||
{
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-dropdown-button-progress
|
||||
{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
z-index: 2;
|
||||
border-radius: 40px;
|
||||
border: 2px solid transparent;
|
||||
border-left-color: var(--color-text);
|
||||
opacity: 1;
|
||||
will-change: transform;
|
||||
animation: rotating .5s linear infinite;
|
||||
transition: opacity .25s ease;
|
||||
}
|
||||
|
||||
.ui-dropdown-button-confirmation
|
||||
{
|
||||
display: grid;
|
||||
flex-grow: 0;
|
||||
width: 100%;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font-size: var(--font-size);
|
||||
padding: 0 6px 0 16px;
|
||||
height: 48px;
|
||||
color: var(--color-text);
|
||||
border-radius: var(--radius);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
|
||||
.ui-button + .ui-button
|
||||
{
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.ui-button
|
||||
{
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotating
|
||||
{
|
||||
from
|
||||
{
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0)
|
||||
}
|
||||
to
|
||||
{
|
||||
-webkit-transform: rotate(1turn);
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<hr class="ui-dropdown-separator">
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiDropdownSeparator'
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.ui-dropdown-separator
|
||||
{
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-dropdown-line);
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.ui-dropdown-separator + .ui-dropdown-separator
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,199 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-dropdown-container">
|
||||
<slot name="before"></slot>
|
||||
<div v-if="hasButton" ref="trigger" class="ui-dropdown-toggle" @click.prevent.stop="toggle">
|
||||
<slot name="button"></slot>
|
||||
</div>
|
||||
<div class="ui-dropdown" ref="overlay" role="dialog" v-if="open" v-click-outside="hide" :class="dropdownClasses">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
//import Overlay from 'zero/helpers/overlay.js';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
const varKey = 'zero.ui-dropdown.current';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'uiDropdown',
|
||||
|
||||
props: {
|
||||
align: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'dark'
|
||||
},
|
||||
locked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
hasButton()
|
||||
{
|
||||
return this.$slots.hasOwnProperty('button');
|
||||
},
|
||||
|
||||
dropdownClasses()
|
||||
{
|
||||
let classes = 'align-' + this.align.split(' ').join(' align-');
|
||||
|
||||
if (!!this.theme)
|
||||
{
|
||||
classes += ' theme-' + this.theme;
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
open: false
|
||||
}),
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
toggle()
|
||||
{
|
||||
if (this.open)
|
||||
{
|
||||
this.hide();
|
||||
}
|
||||
else if (!this.disabled)
|
||||
{
|
||||
this.show();
|
||||
}
|
||||
},
|
||||
|
||||
show()
|
||||
{
|
||||
if (this.disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.zero.runtimeVariables[varKey])
|
||||
{
|
||||
this.zero.runtimeVariables[varKey].hide();
|
||||
}
|
||||
|
||||
this.zero.runtimeVariables[varKey] = this;
|
||||
|
||||
//Overlay.setDropdown(this);
|
||||
this.open = true;
|
||||
//this.position();
|
||||
this.$emit('opened');
|
||||
},
|
||||
|
||||
hide()
|
||||
{
|
||||
if (this.locked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.open = false;
|
||||
this.$emit('closed');
|
||||
this.zero.runtimeVariables[varKey] = null;
|
||||
},
|
||||
|
||||
position()
|
||||
{
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
// the trigger which is the relative to the overlay
|
||||
const reference = this.$refs.trigger;
|
||||
|
||||
// get bounding boxes both for the trigger and the overlay
|
||||
const triggerBoundingBox = reference.getBoundingClientRect();
|
||||
const overlayBoundingBox = this.$refs.overlay.getBoundingClientRect();
|
||||
const windowBox = { width: window.innerWidth, height: window.innerHeight };
|
||||
const windowOffset = 32;
|
||||
|
||||
// calculate available space for the dropdown in all 4 directions areound the trigger element
|
||||
let availableSpace = {
|
||||
left: triggerBoundingBox.left + triggerBoundingBox.width - windowOffset,
|
||||
right: windowBox.width - triggerBoundingBox.left - windowOffset,
|
||||
top: triggerBoundingBox.top + triggerBoundingBox.height - windowOffset,
|
||||
bottom: windowBox.height - triggerBoundingBox.top - windowOffset
|
||||
};
|
||||
|
||||
console.table(availableSpace);
|
||||
|
||||
//console.info(triggerBoundingBox, overlayBoundingBox, windowBox);
|
||||
|
||||
//const resize_ob = new ResizeObserver(function (entries)
|
||||
//{
|
||||
// // since we are observing only a single element, so we access the first element in entries array
|
||||
// let rect = entries[0].contentRect;
|
||||
|
||||
// // current width & height
|
||||
// let width = rect.width;
|
||||
// let height = rect.height;
|
||||
|
||||
// console.log('Current Width : ' + width);
|
||||
// console.log('Current Height : ' + height);
|
||||
//});
|
||||
|
||||
//// start observing for resize
|
||||
//resize_ob.observe(document.querySelector("#demo-textarea"));
|
||||
|
||||
//resize_ob.unobserve(document.querySelector("#demo-textarea"));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.ui-dropdown-container
|
||||
{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui-dropdown
|
||||
{
|
||||
position: absolute;
|
||||
min-width: 300px;
|
||||
min-height: 20px;
|
||||
background: var(--color-dropdown);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--color-dropdown-border);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
z-index: 8;
|
||||
top: calc(100% + 5px);
|
||||
padding: 5px;
|
||||
color: var(--color-text);
|
||||
|
||||
&.align-right
|
||||
{
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.align-top
|
||||
{
|
||||
top: calc(100% + 5px);
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
&.align-bottom
|
||||
{
|
||||
bottom: calc(100% + 5px);
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-error" v-if="visible">
|
||||
<ui-message v-for="error in errors" :key="error.id" type="error" :text="error.message" :title="error.field" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { generateId } from '../utils/numbers';
|
||||
|
||||
export default {
|
||||
name: 'uiError',
|
||||
|
||||
props: {
|
||||
field: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
catchRemaining: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
catchAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
errors: []
|
||||
}),
|
||||
|
||||
mounted ()
|
||||
{
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
visible()
|
||||
{
|
||||
return this.errors && this.errors.length;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
// set and display errors
|
||||
setErrors(errors, append)
|
||||
{
|
||||
if (!errors)
|
||||
{
|
||||
return this.clearErrors();
|
||||
}
|
||||
|
||||
if (!Array.isArray(errors))
|
||||
{
|
||||
errors = [errors];
|
||||
}
|
||||
|
||||
errors.forEach(error =>
|
||||
{
|
||||
error.id = generateId();
|
||||
});
|
||||
|
||||
if (append)
|
||||
{
|
||||
errors.forEach(error =>
|
||||
{
|
||||
this.errors.push(error);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.errors = errors;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// clear errors and hide
|
||||
clearErrors()
|
||||
{
|
||||
this.errors = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-error + .editor
|
||||
{
|
||||
margin-top: var(--padding-m);
|
||||
}
|
||||
</style>
|
||||
@@ -1,94 +0,0 @@
|
||||
<template>
|
||||
<ui-trinity class="ui-form-header-links-list">
|
||||
<template v-slot:header>
|
||||
<ui-header-bar title="@ui.links" :back-button="false" :close-button="true" @close="config.close(true)" />
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<ui-button type="light onbg" label="@ui.close" @click="config.close"></ui-button>
|
||||
</template>
|
||||
|
||||
<div class="ui-form-header-links-list-items">
|
||||
<a :href="urlPrefix + url" target="_blank" v-for="(url, index) in urls" class="ui-form-header-links-list-item">
|
||||
<span class="-text">{{url}}</span>
|
||||
<ui-icon symbol="fth-external-link" class="ui-form-header-links-list-item-icon" :size="16"></ui-icon>
|
||||
</a>
|
||||
</div>
|
||||
</ui-trinity>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
props: {
|
||||
config: Object
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
urls: [],
|
||||
urlPrefix: ''
|
||||
}),
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.urlPrefix = this.config.model.urlPrefix || '';
|
||||
this.urls = this.config.model.urls;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-form-header-links-list .ui-box
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui-form-header-links-list content
|
||||
{
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ui-form-header-links-list-item
|
||||
{
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--padding-xs);
|
||||
align-items: center;
|
||||
font-size: var(--font-size);
|
||||
height: 60px;
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
padding: 0 var(--padding-s);
|
||||
background: var(--color-box);
|
||||
border-radius: var(--radius);
|
||||
border: 2px solid transparent;
|
||||
|
||||
.-text
|
||||
{
|
||||
display: block;
|
||||
padding: 12px 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
& + .ui-form-header-links-list-item
|
||||
{
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-form-header-links-list-text
|
||||
{
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
.ui-form-header-links-list-item-icon
|
||||
{
|
||||
position: relative;
|
||||
top: -1px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
@@ -1,153 +0,0 @@
|
||||
<template>
|
||||
<!--<ui-dropdown-button v-if="hasPreview" label="@page.preview.title" icon="fth-eye" :disabled="disabled || !urlLink" @click="openPreview" />
|
||||
<ui-dropdown-button v-if="hasLink" label="@ui.open.title" icon="fth-external-link" :disabled="disabled || !urlLink" @click="openLink" />-->
|
||||
|
||||
<a v-if="hasPreview" :href="previewLink" target="preview" :disabled="disabled || !urlLink" type="button" class="ui-dropdown-button has-icon" @click.prevent="openPreview">
|
||||
<ui-icon symbol="fth-eye" class="ui-dropdown-button-icon" />
|
||||
<span class="-name"><ui-localize value="@page.preview.title" /></span>
|
||||
</a>
|
||||
<a v-if="hasLink" :href="urlLinkAbsolute" target="_blank" :disabled="disabled || !urlLink" type="button" class="ui-dropdown-button has-icon" @click="dropdown.hide()">
|
||||
<ui-icon symbol="fth-external-link" class="ui-dropdown-button-icon" />
|
||||
<span class="-name"><ui-localize value="@ui.open.title" /></span>
|
||||
<span class="-minor -link" v-if="urlLink && urlList.length > 1" @click.prevent.stop="showLinkList">{{urlList.length}} URLs</span>
|
||||
</a>
|
||||
<ui-dropdown-separator v-if="hasPreview || hasLink" />
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import * as overlays from '../services/overlay';
|
||||
|
||||
export default {
|
||||
name: 'uiFormHeaderLinks',
|
||||
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
url: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
previewEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
dropdown: null,
|
||||
urlList: [],
|
||||
urlDomain: null,
|
||||
urlPreview: null,
|
||||
failed: false
|
||||
}),
|
||||
|
||||
watch: {
|
||||
'value.lastModifiedDate'()
|
||||
{
|
||||
this.loadUrls();
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.loadUrls();
|
||||
|
||||
let current = this;
|
||||
do
|
||||
{
|
||||
if (current.$options.name === 'uiDropdown')
|
||||
{
|
||||
this.dropdown = current;
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (current = current.$parent);
|
||||
|
||||
if (!this.dropdown)
|
||||
{
|
||||
console.warn('ui-dropdown-button: Could not find parent <ui-dropdown />');
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasPreview()
|
||||
{
|
||||
return !this.failed && this.value && typeof this.url === 'function' && this.previewEnabled;
|
||||
},
|
||||
hasLink()
|
||||
{
|
||||
return !this.failed && this.value && typeof this.url === 'function';
|
||||
},
|
||||
urlLink()
|
||||
{
|
||||
return this.urlDomain && this.urlList.length ? this.urlList.reduce((a, b) => a.length <= b.length ? a : b) : null;
|
||||
},
|
||||
urlLinkAbsolute()
|
||||
{
|
||||
return this.urlDomain && this.urlList.length ? this.urlDomain + this.urlList.reduce((a, b) => a.length <= b.length ? a : b) : null;
|
||||
},
|
||||
previewLink()
|
||||
{
|
||||
return this.urlLink ? this.$router.resolve({ name: 'preview', query: { path: this.urlLink } }).href : null;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadUrls()
|
||||
{
|
||||
if (typeof this.url !== 'function')
|
||||
{
|
||||
this.failed = true;
|
||||
return;
|
||||
}
|
||||
const result = await this.url(this.value);
|
||||
this.urlList = result.data.urls ? result.data.urls : (result.data.url ? [result.data.url] : []);
|
||||
this.urlDomain = result.data.domain;
|
||||
this.failed = this.urlList.length < 1 || !result.data.domain;
|
||||
},
|
||||
|
||||
openPreview()
|
||||
{
|
||||
window.open(this.previewLink, 'preview');
|
||||
this.dropdown.hide();
|
||||
},
|
||||
|
||||
async showLinkList()
|
||||
{
|
||||
this.dropdown.hide();
|
||||
await overlays.open({
|
||||
component: () => import('./ui-form-header-links-overlay.vue'),
|
||||
display: 'editor',
|
||||
width: 620,
|
||||
softdismiss: true,
|
||||
model: {
|
||||
urls: this.urlList,
|
||||
urlPrefix: this.urlDomain
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.ui-dropdown-button .-link.-minor
|
||||
{
|
||||
display: inline-block;
|
||||
padding: 10px 0;
|
||||
|
||||
&:hover
|
||||
{
|
||||
color: var(--color-text);
|
||||
text-decoration: underline dotted var(--color-text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,266 +0,0 @@
|
||||
<template>
|
||||
<ui-header-bar class="ui-form-header" :back-button="true" :sticky="sticky">
|
||||
<template v-slot:title>
|
||||
<h2 class="ui-header-bar-title" :class="{'is-empty': title && !value.name && !titleDisabled}">
|
||||
<template v-for="prefix in prefixes">
|
||||
<span class="-minor -prefix" v-localize:html="prefix"></span>
|
||||
<ui-icon class="-chevron" symbol="fth-chevron-right" :size="14" />
|
||||
</template>
|
||||
<ui-error field="name" />
|
||||
<input v-if="!titleDisabled" class="ui-form-header-title-input" type="text" v-model="value.name" v-localize:placeholder="title" :readonly="titleDisabled || disabled" />
|
||||
<!--<ui-alias class="ui-form-header-title-alias" v-if="hasAlias" v-model="value.alias" :name="value.name" :disabled="disabled" />-->
|
||||
<span v-if="titleDisabled" v-localize="forceTitle ? title : (value.name || title)"></span>
|
||||
</h2>
|
||||
</template>
|
||||
<div class="ui-form-header-aside">
|
||||
<slot></slot>
|
||||
<div v-if="!activeDisabled && typeof value.isActive !== 'undefined'" class="ui-form-header-toggle">
|
||||
<ui-toggle v-model:on="value.isActive" class="is-accent" off-content="@ui.inactive" :off-warning="true" on-content="@ui.active" :content-left="true" :disabled="disabled" />
|
||||
</div>
|
||||
<slot name="buttons"></slot>
|
||||
<ui-dropdown v-if="actionsDefined" align="right" theme="default">
|
||||
<template v-slot:button>
|
||||
<ui-button type="light onbg" icon="fth-more-horizontal" v-localize:title="'@ui.actions'" />
|
||||
</template>
|
||||
<slot name="actions"></slot>
|
||||
<ui-dropdown-button v-if="canDelete" label="@ui.delete" icon="fth-trash" @click="onDelete" :disabled="disabled" />
|
||||
<ui-dropdown-separator v-if="!isCreate" />
|
||||
<div class="ui-form-info" v-if="!isCreate">
|
||||
<div class="ui-form-info-item" v-if="value.createdDate"><span class="-key" v-localize="'@ui.createdDate'"></span><span class="-value"><ui-date v-model="value.createdDate" :split="true" format="long" /></span></div>
|
||||
<div class="ui-form-info-item" v-if="value.lastModifiedDate"><span class="-key" v-localize="'@ui.modifiedDate'"></span><span class="-value"><ui-date v-model="value.lastModifiedDate" :split="true" format="long" /></span></div>
|
||||
<!--<div class="ui-form-info-item" v-if="value.alias"><span class="-key" v-localize="'@ui.entityfields.alias'"></span><span class="-value">{{value.alias}}</span></div>-->
|
||||
</div>
|
||||
</ui-dropdown>
|
||||
<ui-button :submit="true" type="accent" :label="isCreate ? '@ui.create' :'@ui.update'" :state="state" v-if="!disabled" class="ui-form-header-primary-button" />
|
||||
</div>
|
||||
</ui-header-bar>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiFormHeader',
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
forceTitle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
prefix: {
|
||||
type: [String, Array]
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: {}
|
||||
},
|
||||
canDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
titleDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
activeDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
state: {
|
||||
type: String
|
||||
},
|
||||
isCreate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hasAlias: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
sticky: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
actionsDefined()
|
||||
{
|
||||
return !this.isCreate && (this.canDelete || this.$slots.hasOwnProperty('actions'));
|
||||
},
|
||||
prefixes()
|
||||
{
|
||||
let items = Array.isArray(this.prefix) ? this.prefix : [this.prefix];
|
||||
return items.filter(x => !!x);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onDelete(item, opts)
|
||||
{
|
||||
this.$emit('delete', item, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.ui-form-header
|
||||
{
|
||||
/* width: 100%;
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;*/
|
||||
}
|
||||
|
||||
.ui-form-header-aside
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
> * + *
|
||||
{
|
||||
margin-left: var(--padding-s);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-form-header-toggle
|
||||
{
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-left: var(--padding-s);
|
||||
//margin-right: var(--padding-s);
|
||||
|
||||
.ui-toggle-off-warning
|
||||
{
|
||||
display: none;
|
||||
color: var(--color-accent-red);
|
||||
}
|
||||
|
||||
.ui-toggle-switch
|
||||
{
|
||||
background: var(--color-button-light-onbg);
|
||||
box-shadow: var(--shadow-short) !important;
|
||||
}
|
||||
|
||||
.ui-toggle-switch.is-active
|
||||
{
|
||||
background: var(--color-toggled);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
input:focus + .ui-toggle-switch
|
||||
{
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
& + .ui-button
|
||||
{
|
||||
margin-left: var(--padding);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-header-bar-title
|
||||
{
|
||||
position: relative;
|
||||
|
||||
.ui-error
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-error + input[type="text"].ui-form-header-title-input:not(:focus)
|
||||
{
|
||||
border-color: var(--color-accent-error);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"].ui-form-header-title-input
|
||||
{
|
||||
font-family: var(--font);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-l);
|
||||
font-weight: 700;
|
||||
background: none;
|
||||
border: 1px dashed var(--color-line-dashed-onbg);
|
||||
/*&:hover, &:focus, .ui-header-bar-title.is-empty &
|
||||
{
|
||||
border: 1px dashed var(--color-text-dim-one);
|
||||
}*/
|
||||
|
||||
.-prefix + &
|
||||
{
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&[readonly]:focus-visible
|
||||
{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[readonly]
|
||||
{
|
||||
cursor: default;
|
||||
border-color: transparent;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-form-header-title-alias
|
||||
{
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 11px;
|
||||
z-index: 2;
|
||||
|
||||
.ui-alias-lock
|
||||
{
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-form-header-info-button
|
||||
{
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-form-header-info-button .ui-button-icon
|
||||
{
|
||||
margin: 0 !important;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ui-form-info
|
||||
{
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--padding-xs);
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
.ui-form-info-item
|
||||
{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.-key
|
||||
{
|
||||
color: var(--color-text-dim-one);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,481 +0,0 @@
|
||||
<template>
|
||||
<form class="ui-form" @keydown="onKeydown" @submit.prevent="onSubmit" @change="onChange">
|
||||
<slot v-if="loadingState === 'default'" v-bind="slotProps" />
|
||||
<div v-if="loadingState == 'loading'" class="ui-form-loading">
|
||||
<i class="ui-form-loading-progress"></i>
|
||||
</div>
|
||||
<div v-if="loadingState === 'error'">
|
||||
error [not implemented]
|
||||
</div>
|
||||
<!--<form-error-view v-if="loadingState === 'error'" :error="loadingError" />-->
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
//import FormErrorView from './form-error-view.vue';
|
||||
import * as overlays from '../services/overlay';
|
||||
import * as notifications from '../services/notification';
|
||||
import { arrayGroupBy, selectorToArray } from '../utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'uiForm',
|
||||
|
||||
//components: { FormErrorView },
|
||||
|
||||
props: {
|
||||
errorComponents: {
|
||||
type: Array,
|
||||
default: () => ['uiError']
|
||||
},
|
||||
inputComponents: {
|
||||
type: Array,
|
||||
default: () => ['uiProperty']
|
||||
},
|
||||
route: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
dirty: false,
|
||||
loadingState: 'default',
|
||||
loadingError: null,
|
||||
state: 'default',
|
||||
errors: [],
|
||||
canEdit: true,
|
||||
slotProps: {
|
||||
state: null
|
||||
},
|
||||
submitBlocked: false
|
||||
}),
|
||||
|
||||
watch: {
|
||||
'$route': {
|
||||
deep: true,
|
||||
handler: function (val)
|
||||
{
|
||||
//console.info('route', val);
|
||||
//this.$nextTick(() =>
|
||||
//{
|
||||
// this.$emit('load', this);
|
||||
//});
|
||||
}
|
||||
},
|
||||
state(val)
|
||||
{
|
||||
this.slotProps.state = val;
|
||||
},
|
||||
canEdit(val)
|
||||
{
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
this.setCanEdit(val);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
created()
|
||||
{
|
||||
this.slotProps.state = this.state;
|
||||
this.$emit('load', this);
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
// shows a confirmation dialog for dirty forms when the route tries to change
|
||||
// it only works when this method is attached to the route component
|
||||
beforeRouteLeave(to, from, next)
|
||||
{
|
||||
if (this.dirty)
|
||||
{
|
||||
Overlay.confirm({
|
||||
title: '@unsavedchanges.title',
|
||||
text: '@unsavedchanges.text',
|
||||
confirmLabel: '@unsavedchanges.confirm',
|
||||
closeLabel: '@unsavedchanges.close'
|
||||
}).then(
|
||||
() => next(false),
|
||||
() =>
|
||||
{
|
||||
this.dirty = false;
|
||||
next();
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
next()
|
||||
}
|
||||
},
|
||||
|
||||
// loads data on creation of the form
|
||||
async load(promise)
|
||||
{
|
||||
this.loadingState = 'loading';
|
||||
const response = await promise();
|
||||
|
||||
if (!response.success)
|
||||
{
|
||||
this.loadingState = 'error';
|
||||
if (response.errors)
|
||||
{
|
||||
this.loadingError = response.errors[0].message;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
this.canEdit = true;
|
||||
this.loadingState = 'default';
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
this.$emit('loaded', this);
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// handles a promise as result of the form submission
|
||||
async handle(response, isCreate)
|
||||
{
|
||||
this.setState('loading');
|
||||
this.clearErrors();
|
||||
|
||||
if (!response.success)
|
||||
{
|
||||
this.setState('error');
|
||||
this.setErrors(response.errors);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setState('success');
|
||||
this.setDirty(false);
|
||||
|
||||
if (response.data && this.route && response.status === 201)
|
||||
{
|
||||
let routeObj = typeof this.route === 'object' ? this.route : { name: this.route };
|
||||
routeObj.params = routeObj.params || {};
|
||||
routeObj.query = this.$route.query || {};
|
||||
routeObj.params.id = response.data.id;
|
||||
|
||||
this.$router.replace(routeObj);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// submits the form
|
||||
onSubmit(e)
|
||||
{
|
||||
if (!this.submitBlocked)
|
||||
{
|
||||
this.setState('loading');
|
||||
this.$emit('submit', this, e);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// set the form to dirty when one of the fields changes
|
||||
onChange(e)
|
||||
{
|
||||
this.dirty = true;
|
||||
},
|
||||
|
||||
|
||||
// handle delete event
|
||||
async onDelete(promise, dontNavigate)
|
||||
{
|
||||
const overlay = await overlays.confirmDelete();
|
||||
|
||||
if (overlay.eventType == 'close')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = overlay.value;
|
||||
|
||||
opts.state('loading');
|
||||
const response = await promise();
|
||||
|
||||
if (response.success)
|
||||
{
|
||||
opts.state('success');
|
||||
opts.close();
|
||||
if (!dontNavigate)
|
||||
{
|
||||
this.$router.go(-1);
|
||||
}
|
||||
notifications.success('@deleteoverlay.success', '@deleteoverlay.success_text');
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
opts.state('error');
|
||||
opts.errors(response.errors);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// prevent submission of form on enter key press
|
||||
onKeydown(e)
|
||||
{
|
||||
if (e.keyCode === 13)
|
||||
{
|
||||
this.submitBlocked = true;
|
||||
clearTimeout(this.submitBlockedTimeout);
|
||||
this.submitBlockedTimeout = setTimeout(() => this.submitBlocked = false, 300);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// set dirty status
|
||||
setDirty(dirty)
|
||||
{
|
||||
this.dirty = dirty;
|
||||
},
|
||||
|
||||
|
||||
// set state for this component and all child components which attached it's state from the slot props
|
||||
setState(state)
|
||||
{
|
||||
this.state = state;
|
||||
},
|
||||
|
||||
|
||||
// clears all errors from the form
|
||||
clearErrors()
|
||||
{
|
||||
this.getErrorComponents().forEach(component =>
|
||||
{
|
||||
component.clearErrors();
|
||||
|
||||
if (component.tab)
|
||||
{
|
||||
component.tab.clearErrors();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// tries to find matching fields for the given errors and displays them
|
||||
setErrors(errors)
|
||||
{
|
||||
if (typeof errors === 'undefined' || !errors)
|
||||
{
|
||||
this.errors = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
this.errors = !Array.isArray(errors) ? [errors] : errors;
|
||||
}
|
||||
|
||||
// get all components + grouped errors
|
||||
let errorComponents = this.getErrorComponents();
|
||||
let errorGroups = arrayGroupBy(this.errors, 'property');
|
||||
let handledGroups = [];
|
||||
|
||||
//console.info({ errorComponents, errorGroups });
|
||||
|
||||
// set errors
|
||||
errorComponents.forEach(component =>
|
||||
{
|
||||
let field = component.field;
|
||||
|
||||
if (field)
|
||||
{
|
||||
this.errors.forEach(error =>
|
||||
{
|
||||
let errorField = error.property;
|
||||
let errorFieldSelector = selectorToArray(errorField);
|
||||
|
||||
// exact error field match
|
||||
if (errorField == field)
|
||||
{
|
||||
handledGroups.push(field);
|
||||
component.setErrors(error, true);
|
||||
|
||||
if (component.tab)
|
||||
{
|
||||
component.tab.setErrors(true);
|
||||
}
|
||||
}
|
||||
// nested error
|
||||
else if (errorFieldSelector[0] == field)
|
||||
{
|
||||
handledGroups.push(field);
|
||||
component.setErrors(error, true);
|
||||
|
||||
if (component.tab)
|
||||
{
|
||||
component.tab.setErrors(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//if (field && errorGroups[field])
|
||||
//{
|
||||
// handledGroups.push(field);
|
||||
// component.setErrors(errorGroups[field]);
|
||||
|
||||
// if (component.tab)
|
||||
// {
|
||||
// component.tab.setErrors(true);
|
||||
// }
|
||||
//}
|
||||
});
|
||||
|
||||
for (var field in errorGroups)
|
||||
{
|
||||
let errorGroup = errorGroups[field];
|
||||
if (handledGroups.indexOf(field) < 0)
|
||||
{
|
||||
errorComponents.forEach(component =>
|
||||
{
|
||||
if (component.catchRemaining || component.catchAll)
|
||||
{
|
||||
component.setErrors(errorGroup, true);
|
||||
|
||||
if (component.tab)
|
||||
{
|
||||
component.tab.setErrors(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// find all error components in form
|
||||
getErrorComponents()
|
||||
{
|
||||
let errorComponents = [];
|
||||
|
||||
// find components which can output errors
|
||||
let traverseChildren = (parent, tab) =>
|
||||
{
|
||||
parent.$children.forEach(component =>
|
||||
{
|
||||
if (this.errorComponents.indexOf(component.$options.name) > -1)
|
||||
{
|
||||
errorComponents.push({
|
||||
name: component.$options.name,
|
||||
field: component.field,
|
||||
catchAll: component.catchAll,
|
||||
catchRemaining: component.catchRemaining,
|
||||
component: component,
|
||||
setErrors: component.setErrors,
|
||||
clearErrors: component.clearErrors,
|
||||
tab: tab
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
traverseChildren(component, tab || (component.$options.name === 'uiTab' ? component : null));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
traverseChildren(this);
|
||||
|
||||
return errorComponents;
|
||||
},
|
||||
|
||||
|
||||
// sets the form editing ability
|
||||
setCanEdit(canEdit)
|
||||
{
|
||||
// find components which can output errors
|
||||
//let traverseChildren = parent =>
|
||||
//{
|
||||
// parent.$children.forEach(component =>
|
||||
// {
|
||||
// const isValidComponent = this.inputComponents.indexOf(component.$options.name) > -1;
|
||||
// //const field = component.field;
|
||||
|
||||
// if (typeof component._props.disabled === 'boolean')
|
||||
// {
|
||||
// component.$emit('update:disabled', canEdit);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// traverseChildren(component);
|
||||
// }
|
||||
// //if (isErrorComponent)
|
||||
// //{
|
||||
// // errorComponents.push(component);
|
||||
// //}
|
||||
|
||||
// //if (isErrorComponent && field)
|
||||
// //{
|
||||
// // let errorGroup = errorGroups[field];
|
||||
|
||||
// // if (errorGroup)
|
||||
// // {
|
||||
// // handledGroups.push(field);
|
||||
// // component.set(errorGroup);
|
||||
// // }
|
||||
// //}
|
||||
// //else
|
||||
// //{
|
||||
// // traverseChildren(component);
|
||||
// //}
|
||||
// });
|
||||
//};
|
||||
|
||||
//traverseChildren(this);
|
||||
|
||||
//console.info('done');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-form
|
||||
{
|
||||
min-height: 100%;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
.ui-form-loading
|
||||
{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-form-loading-progress
|
||||
{
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
z-index: 2;
|
||||
border-radius: 40px;
|
||||
border: 2px solid var(--color-bg-shade-3);
|
||||
border-left-color: var(--color-text);
|
||||
opacity: 1;
|
||||
will-change: transform;
|
||||
animation: rotating .5s linear infinite;
|
||||
transition: opacity .25s ease;
|
||||
}
|
||||
|
||||
@keyframes rotating
|
||||
{
|
||||
from
|
||||
{
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0)
|
||||
}
|
||||
to
|
||||
{
|
||||
-webkit-transform: rotate(1turn);
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,228 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-header-bar" :class="{ 'is-sticky': sticky }">
|
||||
<div class="ui-header-bar-inner">
|
||||
<div class="ui-header-bar-main">
|
||||
<ui-icon-button v-if="backButton" type="light onbg" @click="onBack" :size="15" />
|
||||
<div class="ui-header-bar-main-title">
|
||||
<slot name="title">
|
||||
<h2 class="ui-header-bar-title" :class="{'is-empty': !title && titleEmpty}">
|
||||
<template v-for="prefix in prefixes">
|
||||
<span class="-minor -prefix" v-localize:html="prefix"></span>
|
||||
<ui-icon class="-chevron" symbol="fth-chevron-right" :size="14" />
|
||||
</template>
|
||||
<span v-localize="title || titleEmpty"></span>
|
||||
<span v-if="suffix" class="-minor -suffix" v-localize:html="suffix"></span>
|
||||
<span v-if="count > 0" class="-minor -count">{{count}}</span>
|
||||
</h2>
|
||||
</slot>
|
||||
<p v-if="description" class="ui-header-bar-description" v-localize="description"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-header-bar-aside">
|
||||
<slot></slot>
|
||||
<ui-icon-button class="ui-header-bar-close" v-if="closeButton" @click="$emit('close')" icon="fth-x" title="@ui.close" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiHeaderBar',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
titleEmpty: {
|
||||
type: String
|
||||
},
|
||||
prefix: {
|
||||
type: [String, Array]
|
||||
},
|
||||
suffix: {
|
||||
type: String
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
description: {
|
||||
type: String
|
||||
},
|
||||
backButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
closeButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sticky: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
prefixes()
|
||||
{
|
||||
let items = Array.isArray(this.prefix) ? this.prefix : [this.prefix];
|
||||
return items.filter(x => !!x);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onBack()
|
||||
{
|
||||
this.$router.go(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-header-bar
|
||||
{
|
||||
width: 100%;
|
||||
height: 90px;
|
||||
padding: 0 var(--padding) 0; //10px;
|
||||
|
||||
& + .ui-blank-box, & + .ui-box, & + .ui-view-box
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& + .ui-view-box
|
||||
{
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.app-tree &
|
||||
{
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&.is-sticky
|
||||
{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--color-bg);
|
||||
//width: auto;
|
||||
//border-radius: var(--radius);
|
||||
//box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
//margin-bottom: var(--padding);
|
||||
//background: linear-gradient(0deg, transparent, var(--color-bg) 30%);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-header-bar-inner
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ui-header-bar-main
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex: 1 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
.ui-icon-button
|
||||
{
|
||||
margin-right: var(--padding-s);
|
||||
flex-shrink: 0;
|
||||
//margin-top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-header-bar-main-title
|
||||
{
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.ui-header-bar-aside
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
padding-left: var(--padding-s);
|
||||
|
||||
> * + *
|
||||
{
|
||||
margin-left: var(--padding-s);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-header-bar-title
|
||||
{
|
||||
font-family: var(--font);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.is-empty, .-minor
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.-prefix
|
||||
{
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.-chevron
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin: 0 var(--padding-xxs);
|
||||
}
|
||||
|
||||
.-count
|
||||
{
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-box);
|
||||
//box-shadow: var(--shadow-short);
|
||||
color: var(--color-text);
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding: 0 10px;
|
||||
border-radius: 16px;
|
||||
letter-spacing: .5px;
|
||||
font-style: normal;
|
||||
margin-left: 12px;
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-header-bar-description
|
||||
{
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--color-text-dim);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
|
||||
.ui-header-bar-close
|
||||
{
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<button type="button" class="ui-icon-button" :disabled="disabled" :class="'type-' + type.split(' ').join(' type-')" @click="$emit('click', $event)" v-localize:title="title">
|
||||
<span class="sr-only" v-localize="title"></span>
|
||||
<ui-icon class="ui-button-icon" :symbol="icon" :size="size" :stroke="stroke" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiIconButton',
|
||||
|
||||
emits: ['click'],
|
||||
|
||||
props: {
|
||||
state: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'fth-arrow-left'
|
||||
},
|
||||
stroke: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'action'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14
|
||||
},
|
||||
disabled: Boolean
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<svg class="ui-icon" :width="size" :height="size" :stroke-width="stroke" :data-symbol="symbolName" :class="classes">
|
||||
<use v-show="!isFlag" v-bind="{ 'xlink:href': href }" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
symbol: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: true
|
||||
},
|
||||
file: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 17
|
||||
},
|
||||
stroke: {
|
||||
type: Number,
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
|
||||
symbolName()
|
||||
{
|
||||
return this.symbol && this.symbol.split(' ')[0].trim();
|
||||
},
|
||||
classes()
|
||||
{
|
||||
return this.symbol ? this.symbol.split(' ').slice(1) : [];
|
||||
},
|
||||
isFlag()
|
||||
{
|
||||
return this.symbol && this.symbol.indexOf('flag') === 0;
|
||||
},
|
||||
href()
|
||||
{
|
||||
return (this.file || '') + '#' + this.symbolName;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-icon
|
||||
{
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
//fill: var(--color-bg-shade-3);
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.ui-icon[data-symbol="fth-waffle"]
|
||||
{
|
||||
stroke: none;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
@@ -1,221 +0,0 @@
|
||||
<template>
|
||||
<ui-trinity class="ui-iconpicker-overlay">
|
||||
<template v-slot:header>
|
||||
<ui-header-bar title="@iconpicker.title" :back-button="false" :close-button="true" @close="config.close(true)" />
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<ui-button type="light onbg" label="@ui.close" @click="config.close"></ui-button>
|
||||
<ui-button type="light onbg" label="@ui.remove" @click="remove()"></ui-button>
|
||||
</template>
|
||||
|
||||
<ui-search class="ui-iconpicker-overlay-search onbg" v-model="query" />
|
||||
|
||||
<div class="ui-iconpicker-overlay-colors" v-if="config.model.colors">
|
||||
<i v-for="col in colors" :class="{ 'is-active': ('color-' + col) === color || (col === 'default' && !color), ['bg-color-' + col]: true }" @click="selectColor(col)" :title="col"></i>
|
||||
</div>
|
||||
|
||||
<!--<div class="ui-iconpicker-overlay-size">
|
||||
<input type="range" min="14" max="48" step="2" v-model.number="size" />
|
||||
</div>-->
|
||||
<!--<hr class="ui-iconpicker-overlay-line">-->
|
||||
|
||||
<div class="ui-iconpicker-overlay-items" :style="{ 'grid-template-columns': 'repeat(' + columns + ', 1fr)' }">
|
||||
<button v-for="item in items" type="button" class="ui-iconpicker-overlay-item" :class="{ 'is-active': item === icon, [color]: item === icon }" :title="item" @click="select(item)">
|
||||
<ui-icon :symbol="item" :size="size" />
|
||||
<!--<i :class="item"></i>-->
|
||||
</button>
|
||||
</div>
|
||||
</ui-trinity>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { debounce } from '../utils';
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
config: Object
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
value: null,
|
||||
file: null,
|
||||
colors: ['default', 'gray', 'blue-gray', 'blue', 'teal', 'green', 'lime', 'yellow', 'orange', 'red', 'purple', 'brown'],
|
||||
icon: null,
|
||||
color: null,
|
||||
query: '',
|
||||
set: null,
|
||||
items: [],
|
||||
size: 20
|
||||
}),
|
||||
|
||||
watch: {
|
||||
value()
|
||||
{
|
||||
this.init();
|
||||
},
|
||||
query()
|
||||
{
|
||||
this.debouncedSearch();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
columns()
|
||||
{
|
||||
return ~~(580 / (this.size + 45));
|
||||
}
|
||||
},
|
||||
|
||||
created()
|
||||
{
|
||||
this.debouncedSearch = debounce(this.search, 100);
|
||||
this.set = this.config.model.set;
|
||||
this.value = this.config.model.value;
|
||||
this.items = this.set.icons;
|
||||
this.init();
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
let $selected = this.$el.querySelector('.ui-iconpicker-overlay-item.is-active');
|
||||
|
||||
if ($selected)
|
||||
{
|
||||
let $scrollable = this.$el.querySelector('content');
|
||||
const offset = $selected.offsetTop - $scrollable.clientHeight * 0.5 - 30;
|
||||
$scrollable.scrollTop = offset < 0 ? 0 : offset;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
confirm()
|
||||
{
|
||||
const result = (this.icon || '') + ' ' + (this.color || '').trim();
|
||||
this.config.confirm(result);
|
||||
},
|
||||
|
||||
select(item)
|
||||
{
|
||||
this.icon = item;
|
||||
this.confirm();
|
||||
},
|
||||
|
||||
selectColor(color)
|
||||
{
|
||||
this.color = color === 'default' ? null : 'color-' + color;
|
||||
},
|
||||
|
||||
init()
|
||||
{
|
||||
if (!this.value)
|
||||
{
|
||||
this.icon = null;
|
||||
this.color = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
const parts = this.value.split(' ');
|
||||
this.icon = parts[0];
|
||||
this.color = parts.length > 1 ? parts[1] : null;
|
||||
}
|
||||
},
|
||||
|
||||
search()
|
||||
{
|
||||
const query = this.query;
|
||||
|
||||
if (!query)
|
||||
{
|
||||
this.items = this.set.icons;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.items = this.set.icons.filter(item => item.toLowerCase().indexOf(query) > -1);
|
||||
}
|
||||
},
|
||||
|
||||
remove()
|
||||
{
|
||||
this.config.confirm(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-iconpicker-overlay content
|
||||
{
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ui-iconpicker-overlay-items
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 61px);
|
||||
grid-gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ui-iconpicker-overlay-item
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 22px 0;
|
||||
border-radius: var(--radius);
|
||||
|
||||
&:hover
|
||||
{
|
||||
background: var(--color-box);
|
||||
box-shadow: var(--shadow-short);
|
||||
}
|
||||
|
||||
&.is-active
|
||||
{
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
box-shadow: var(--shadow-short);
|
||||
}
|
||||
|
||||
&.is-active
|
||||
{
|
||||
//color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-iconpicker-overlay-search
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ui-iconpicker-overlay-colors
|
||||
{
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 20px;
|
||||
|
||||
i
|
||||
{
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.is-active
|
||||
{
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-iconpicker-overlay-line
|
||||
{
|
||||
}
|
||||
</style>
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div v-if="render" class="ui-iconpicker" :class="{'is-disabled': disabled }">
|
||||
<input ref="input" type="hidden" :value="value" />
|
||||
<ui-select-button :icon="previewIcon" label="@ui.icon" :description="buttonDescription" @click="pick" :disabled="disabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { open as openOverlay } from '../services/overlay';
|
||||
import { extendObject } from '../utils';
|
||||
import { useUiStore } from '../ui/store';
|
||||
import * as notifications from '../services/notification';
|
||||
import { localize } from '../services/localization';
|
||||
|
||||
export default {
|
||||
name: 'uiIconpicker',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
colors: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
render: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
set: {
|
||||
type: String,
|
||||
default: 'feather'
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () =>
|
||||
{
|
||||
return {
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
iconSet: null,
|
||||
store: null
|
||||
}),
|
||||
|
||||
watch: {
|
||||
set()
|
||||
{
|
||||
this.loadSet();
|
||||
}
|
||||
},
|
||||
|
||||
created()
|
||||
{
|
||||
this.store = useUiStore();
|
||||
this.loadSet();
|
||||
},
|
||||
|
||||
computed: {
|
||||
buttonDescription()
|
||||
{
|
||||
return this.value ? this.value.split(' ')[0] : '@ui.icon_select';
|
||||
},
|
||||
previewIcon()
|
||||
{
|
||||
return this.value || 'fth-plus';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onChange(value)
|
||||
{
|
||||
this.$emit('change', value);
|
||||
this.$emit('input', value);
|
||||
this.$emit('update:value', value);
|
||||
// TODO this does not trigger the forms dirty flag
|
||||
},
|
||||
|
||||
loadSet()
|
||||
{
|
||||
const alias = this.set || 'feather';
|
||||
this.iconSet = this.store.iconSets.find(x => x.alias === alias);
|
||||
},
|
||||
|
||||
async pick()
|
||||
{
|
||||
if (this.disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.iconSet)
|
||||
{
|
||||
notifications.error("@iconpicker.notfound.title", localize("@iconpicker.notfound.text", { tokens: { name: this.set || '' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await openOverlay(extendObject({
|
||||
model: {
|
||||
set: this.iconSet,
|
||||
value: this.value,
|
||||
colors: this.colors
|
||||
},
|
||||
component: () => import('./ui-iconpicker-overlay.vue'),
|
||||
display: 'editor',
|
||||
width: 660
|
||||
}, typeof this.options === 'object' ? this.options : {}));
|
||||
|
||||
if (result.eventType === 'confirm')
|
||||
{
|
||||
this.onChange(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,158 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-inline-tabs">
|
||||
<nav role="tablist" class="ui-inline-tabs-list">
|
||||
<button type="button" v-for="(tab, index) in tabs" class="ui-inline-tabs-list-item" :key="index" :class="{ 'is-active': tab.active, 'has-errors': tab.error }" :disabled="tab.disabled"
|
||||
:aria-selected="tab.active" role="tab" @click="select(index)">
|
||||
<i v-if="tab.error" class="ui-inline-tabs-list-item-error fth-alert-circle"></i>
|
||||
<span v-localize="tab.label"></span>
|
||||
<i v-if="forceCount || tab.count > 0" class="ui-inline-tabs-list-item-count">{{tab.count}}</i>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="ui-inline-tabs-items">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiInlineTabs',
|
||||
|
||||
props: {
|
||||
cache: {
|
||||
type: String
|
||||
},
|
||||
active: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
forceCount: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
storageKey: null,
|
||||
tabs: []
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
this.cacheKey = this.cache ? `zero.ui-inline-tabs.cache.${this.cache}` : null;
|
||||
this.tabs = this.$children;
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
if (this.cache)
|
||||
{
|
||||
const cachedActiveTab = localStorage.getItem(this.cacheKey);
|
||||
if (cachedActiveTab !== null)
|
||||
{
|
||||
this.select(+cachedActiveTab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.select(this.active);
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
select(index, ev)
|
||||
{
|
||||
if (ev)
|
||||
{
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
const currentTab = this.tabs[index];
|
||||
|
||||
if (currentTab.disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabs.forEach((tab, tabIndex) =>
|
||||
{
|
||||
tab.active = index === tabIndex;
|
||||
});
|
||||
|
||||
if (this.cache)
|
||||
{
|
||||
localStorage.setItem(this.cacheKey, index);
|
||||
}
|
||||
|
||||
//this.$emit('changed', { tab: selectedTab });
|
||||
//this.activeTabHash = selectedTab.hash;
|
||||
//this.activeTabIndex = this.getTabIndex(selectedTabHash);
|
||||
//this.lastActiveTabHash = this.activeTabHash = selectedTab.hash;
|
||||
//expiringStorage.set(this.storageKey, selectedTab.hash, this.cacheLifetime);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-inline-tabs-list
|
||||
{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: var(--padding);
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.ui-inline-tabs-list-item
|
||||
{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border-radius: 30px;
|
||||
font-size: var(--font-size);
|
||||
|
||||
.ui-inline-tabs-list-item-count
|
||||
{
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: var(--color-button-light);
|
||||
margin-left: 12px;
|
||||
margin-right: -6px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: var(--color-text-dim);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
&.is-active
|
||||
{
|
||||
background: var(--color-button-light);
|
||||
font-weight: 600;
|
||||
|
||||
.ui-inline-tabs-list-item-count
|
||||
{
|
||||
background: var(--color-box);
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-inline-tabs-list-item-error
|
||||
{
|
||||
display: inline-block;
|
||||
float: left;
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
margin-left: -4px;
|
||||
position: relative;
|
||||
margin-top: -4px;
|
||||
top: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,149 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-input-list" :class="{'is-disabled': disabled }">
|
||||
<div class="ui-input-list-items" v-sortable="{ handle: '.is-handle', onUpdate: onSortingUpdated, enabled: !disabled }">
|
||||
<div v-for="item in items" :key="item.id" class="ui-input-list-item">
|
||||
<button type="button" class="ui-input-list-sort is-handle" tabindex="-1">
|
||||
<ui-icon symbol="fth-grip-vertical" />
|
||||
</button>
|
||||
|
||||
<input v-model="item.value" type="text" class="ui-input" :maxlength="maxLength" :readonly="disabled" @change="onChange(items)" size="5" />
|
||||
<ui-icon-button type="light" icon="fth-x" @click="removeItem(item)" v-if="!disabled" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<ui-button v-if="!disabled && !plusButton" type="light" :label="addLabel" @click="addItem" />
|
||||
<ui-select-button v-if="!disabled && plusButton" icon="fth-plus" :label="items.length > 0 ? null : addLabel" @click="addItem" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { arrayMove, generateId } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'uiInputList',
|
||||
props: {
|
||||
addLabel: {
|
||||
type: String,
|
||||
default: '@ui.add'
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxItems: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
plusButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
items: []
|
||||
}),
|
||||
watch: {
|
||||
value(value)
|
||||
{
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
this.items = (value || []).map(item =>
|
||||
{
|
||||
return { id: generateId(), value: item };
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted()
|
||||
{
|
||||
this.items = (this.value || []).map(item =>
|
||||
{
|
||||
return { id: generateId(), value: item };
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onChange(items)
|
||||
{
|
||||
let value = items.map(item => item.value);
|
||||
this.$emit('input', value);
|
||||
this.$emit('update:value', value);
|
||||
},
|
||||
addItem()
|
||||
{
|
||||
this.items.push({
|
||||
id: generateId(),
|
||||
value: ''
|
||||
});
|
||||
this.onChange(this.items);
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
this.$el.querySelector('.ui-input-list-item:last-of-type input').focus();
|
||||
});
|
||||
},
|
||||
removeItem(item)
|
||||
{
|
||||
const index = this.items.indexOf(item);
|
||||
this.items.splice(index, 1);
|
||||
this.onChange(this.items);
|
||||
},
|
||||
onSortingUpdated(ev)
|
||||
{
|
||||
let items = arrayMove(this.items, ev.oldIndex, ev.newIndex);
|
||||
this.onChange(items);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-input-list
|
||||
{
|
||||
}
|
||||
|
||||
.ui-input-list-item
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
background: var(--color-input);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 6px;
|
||||
|
||||
.ui-input-list.is-disabled &
|
||||
{
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ui-icon-button
|
||||
{
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-left: none;
|
||||
background: transparent !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ui-icon-button + .ui-icon-button
|
||||
{
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button.ui-input-list-sort
|
||||
{
|
||||
height: 100%;
|
||||
width: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
cursor: grab;
|
||||
}
|
||||
</style>
|
||||
@@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<a v-if="isExternalLink"
|
||||
v-bind="attrs"
|
||||
class="ui-link"
|
||||
:class="classes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="to"
|
||||
:tabindex="disabled ? -1 : undefined"
|
||||
:aria-disabled="disabled">
|
||||
<slot />
|
||||
</a>
|
||||
<a v-else
|
||||
v-bind="attrs"
|
||||
class="ui-link"
|
||||
:class="classes"
|
||||
:href="href"
|
||||
:tabindex="disabled ? -1 : undefined"
|
||||
:aria-disabled="disabled"
|
||||
@click="navigate">
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { RouterLinkProps, RouteLocationRaw, START_LOCATION, useLink, useRoute } from 'vue-router';
|
||||
import { computed, defineComponent, toRefs, PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ui-link',
|
||||
|
||||
props: {
|
||||
to: {
|
||||
type: [String, Object] as PropType<RouteLocationRaw>,
|
||||
required: true,
|
||||
},
|
||||
replace: Boolean,
|
||||
custom: Boolean,
|
||||
ariaCurrentValue: {
|
||||
type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
|
||||
default: 'page',
|
||||
},
|
||||
disabled: Boolean
|
||||
},
|
||||
|
||||
setup(props, { attrs })
|
||||
{
|
||||
const { replace, to, disabled } = toRefs(props);
|
||||
|
||||
const isExternalLink = computed(
|
||||
() => typeof to.value === 'string' && to.value.startsWith('http')
|
||||
);
|
||||
|
||||
const currentRoute = useRoute();
|
||||
|
||||
const { route, href, isActive, isExactActive, navigate } = useLink({
|
||||
to: computed(() => (isExternalLink.value ? START_LOCATION : to.value)),
|
||||
replace,
|
||||
});
|
||||
|
||||
const classes = computed(() => ({
|
||||
'is-active': isActive.value || currentRoute.path.startsWith(route.value.path),
|
||||
'is-active-exact': isExactActive.value || currentRoute.path === route.value.path
|
||||
}));
|
||||
|
||||
return { attrs, isExternalLink, href, navigate, classes, disabled };
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-link[aria-disabled]
|
||||
{
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<i class="ui-loading" :class="{'is-big': isBig || big, 'is-small': small }"></i>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
isBig: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
big: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-loading
|
||||
{
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 40px;
|
||||
//border: 2px solid var(--color-box);
|
||||
//border-left-color: var(--color-text);
|
||||
border: 2px solid transparent;
|
||||
border-left-color: var(--color-text);
|
||||
will-change: transform;
|
||||
animation: loadingRotation 0.8s linear infinite;
|
||||
|
||||
&.is-big
|
||||
{
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
&.is-small
|
||||
{
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loadingRotation
|
||||
{
|
||||
0%
|
||||
{
|
||||
transform: rotate(0)
|
||||
}
|
||||
100%
|
||||
{
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<span>{{output}}</span>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { localize } from '../services/localization';
|
||||
|
||||
export default {
|
||||
name: 'uiLocalize',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
force: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tokens: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
hideEmpty: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
output()
|
||||
{
|
||||
return localize(this.value, { tokens: this.tokens, force: this.force, hideEmpty: this.hideEmpty });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<p class="ui-message" :class="messageClasses">
|
||||
<ui-icon v-if="iconClass" class="ui-message-icon" :symbol="iconClass" />
|
||||
<span class="ui-message-text" v-localize:html="text"></span>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
const TYPE_ICONS = {
|
||||
neutral: 'fth-info',
|
||||
info: 'fth-info',
|
||||
primary: 'fth-info',
|
||||
warn: 'fth-alert-circle',
|
||||
error: 'fth-alert-circle',
|
||||
success: 'fth-check-circle'
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'uiMessage',
|
||||
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'info'
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '-1'
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
iconClass()
|
||||
{
|
||||
return this.icon !== '-1' ? this.icon : TYPE_ICONS[this.type];
|
||||
},
|
||||
messageClasses()
|
||||
{
|
||||
return [
|
||||
'type-' + this.type,
|
||||
(this.block ? 'block' : null)
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
mounted ()
|
||||
{
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-message
|
||||
{
|
||||
font-size: var(--font-size-s);
|
||||
background: var(--color-accent-info-bg);
|
||||
color: var(--color-accent-info);
|
||||
display: inline-grid;
|
||||
padding: 12px 12px 11px 12px;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 12px;
|
||||
border-radius: var(--radius-inner);
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
|
||||
&.type-warn
|
||||
{
|
||||
background: var(--color-accent-warn-bg);
|
||||
color: var(--color-accent-warn);
|
||||
}
|
||||
|
||||
&.type-error
|
||||
{
|
||||
background: var(--color-accent-error-bg);
|
||||
color: var(--color-accent-error);
|
||||
}
|
||||
|
||||
&.type-success
|
||||
{
|
||||
background: var(--color-accent-success-bg);
|
||||
color: var(--color-accent-success);
|
||||
}
|
||||
|
||||
&.type-neutral
|
||||
{
|
||||
background: var(--color-box-nested);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.type-primary
|
||||
{
|
||||
background: var(--color-primary-low);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.block
|
||||
{
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-message-icon
|
||||
{
|
||||
font-size: 1.3em;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.ui-message-text
|
||||
{
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-pagination" v-if="pages > 1" :class="{'is-inline': inline }">
|
||||
<ui-icon-button class="ui-pagination-prev ui-pagination-button" :type="buttonType" title="@ui.pagination.previous" icon="fth-chevron-left" :disabled="page < 2" @click="set(page - 1)" />
|
||||
<div class="ui-pagination-select">
|
||||
<select :value="page" @change="selectChanged">
|
||||
<option v-for="value in values" v-bind:value="value">{{value}}</option>
|
||||
</select>
|
||||
<button type="button" class="ui-button type-blank caret-down">
|
||||
<span class="ui-button-text" v-localize="{ key: '@ui.pagination.xofy', tokens: { x: page, y: pages }}"></span>
|
||||
<ui-icon class="ui-button-caret" symbol="fth-chevron-down" />
|
||||
</button>
|
||||
</div>
|
||||
<ui-icon-button class="ui-pagination-next ui-pagination-button" :type="buttonType" title="@ui.pagination.next" icon="fth-chevron-right" :disabled="page >= pages" @click="set(page + 1)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiPagination',
|
||||
|
||||
props: {
|
||||
pages: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
values: function ()
|
||||
{
|
||||
return Array.apply(null, Array(this.pages)).map(function (_, i) { return i + 1; });
|
||||
},
|
||||
buttonType()
|
||||
{
|
||||
return this.inline ? 'light' : 'light onbg';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
set(page)
|
||||
{
|
||||
this.$emit('update:page', page);
|
||||
this.$emit('change', page);
|
||||
},
|
||||
|
||||
selectChanged(ev, a)
|
||||
{
|
||||
this.set(+ev.target.value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-pagination
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: var(--padding);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ui-pagination-select
|
||||
{
|
||||
margin: 0 30px;
|
||||
position: relative;
|
||||
|
||||
.ui-button
|
||||
{
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
select
|
||||
{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pagination-button[disabled] .ui-button-icon
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
</style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="ui-password-hash" :class="{ 'is-reloading': reloading }">
|
||||
<input value="******************" v-if="!changing" type="text" class="ui-input" readonly="readonly" disabled="disabled" />
|
||||
<input v-model="password" v-else type="text" readonly="readonly" class="ui-input" @click="$event.target.select()" />
|
||||
<ui-button v-if="!disabled" :disabled="reloading" icon="fth-rotate-ccw" v-localize:title="'@changepasswordoverlay.regenerate'" type="light" @click="getRandomPassword" />
|
||||
</div>
|
||||
<p v-if="changing" class="ui-property-help" v-localize="'@changepasswordoverlay.regenerate_help'"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import api from '../modules/users/api';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
},
|
||||
entity: Object
|
||||
},
|
||||
|
||||
|
||||
data: () => ({
|
||||
reloading: false,
|
||||
password: null,
|
||||
changing: false
|
||||
}),
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
async getRandomPassword()
|
||||
{
|
||||
this.reloading = true;
|
||||
const result = await api.getRandomPassword(32);
|
||||
this.password = result.data.password;
|
||||
let passedValue = 'raw:' + this.password;
|
||||
this.$emit('input', passedValue);
|
||||
this.$emit('update:value', passedValue);
|
||||
this.changing = true;
|
||||
this.reloading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-password-hash
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-gap: var(--padding-xxs);
|
||||
|
||||
&.is-reloading .ui-icon
|
||||
{
|
||||
animation: rotating 1s linear infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes rotating
|
||||
{
|
||||
from
|
||||
{
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0)
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
-webkit-transform: rotate(1turn);
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
& + .ui-property-help
|
||||
{
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,818 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-pick" :class="{'is-disabled': disabled, 'is-combined': configuration.preview.combined }">
|
||||
|
||||
<!-- previews -->
|
||||
<div class="ui-pick-previews" v-if="configuration.preview.enabled && previews.length > 0 && !configuration.preview.combined" v-sortable="{ onUpdate: onSortingUpdated, enabled: configuration.sortable }">
|
||||
<div v-for="preview in previews" :key="preview[configuration.keys.id]" class="ui-pick-preview">
|
||||
<ui-select-button :icon="getPreviewIcon(preview)" :icon-as-image="configuration.preview.iconAsImage" :label="preview[configuration.keys.name]" :description="getPreviewDescription(preview)" :disabled="disabled" @click="pick(preview[configuration.keys.id])" :tokens="preview" />
|
||||
<ui-icon-button v-if="!disabled && configuration.preview.delete" @click="remove(preview[configuration.keys.id])" icon="fth-x" title="@ui.remove" :size="14" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- combined preview -->
|
||||
<div class="ui-pick-previews" v-if="configuration.preview.enabled && configuration.preview.combined">
|
||||
<div class="ui-pick-preview">
|
||||
<ui-select-button icon="fth-check-circle" :label="combinedTitle" :disabled="disabled" @click="pick()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- add button -->
|
||||
<div class="ui-pick-add" v-if="canAdd && configuration.addButton.enabled && !configuration.preview.combined">
|
||||
<slot name="add">
|
||||
<ui-select-button icon="fth-plus" :label="configuration.addButton.label" @click="pick()" :disabled="disabled" />
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- overlay -->
|
||||
<ui-dropdown ref="overlay" class="ui-pick-overlay" @opened="overlayOpened" :theme="configuration.theme">
|
||||
|
||||
<!-- headline -->
|
||||
<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>
|
||||
|
||||
<!-- search -->
|
||||
<ui-search v-if="configuration.search.enabled" ref="search" class="ui-pick-overlay-search" :value="searchValue" @input="onSearch" @submit="onSearchSubmit">
|
||||
<template v-slot:button="search" v-if="configuration.autocomplete">
|
||||
<button type="button" class="ui-searchinput-button" v-localize:title="'@ui.search.button'" @click="search.onSubmit"><ui-icon symbol="fth-check"></ui-icon></button>
|
||||
</template>
|
||||
</ui-search>
|
||||
|
||||
<!-- items -->
|
||||
<div class="ui-pick-overlay-items">
|
||||
<button v-for="item in items" :key="item[configuration.keys.id]" type="button" class="ui-pick-overlay-item" @click="select(item)" :class="{'is-selected': isSelected(item) }" :disabled="item.disabled">
|
||||
<ui-icon v-if="item[configuration.keys.icon] && !configuration.list.iconAsImage" class="-icon" :symbol="item[configuration.keys.icon]" />
|
||||
<span v-if="item[configuration.keys.icon] && configuration.list.iconAsImage" class="-image">
|
||||
<ui-thumbnail :media="item[configuration.keys.icon]" :alt="item[configuration.keys.name]" />
|
||||
</span>
|
||||
<div class="ui-pick-overlay-item-title">
|
||||
<span class="-name" v-localize="item[configuration.keys.name]"></span>
|
||||
<span v-if="item[configuration.keys.description] && configuration.list.description" class="-text" v-localize="item[configuration.keys.description]"></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- loading + empty states -->
|
||||
<div class="ui-pick-overlay-center" v-if="isLoading || (!configuration.autocomplete && !items.length)">
|
||||
<div v-if="!isLoading && !configuration.autocomplete && !items.length" class="ui-pick-overlay-message">
|
||||
<ui-icon class="ui-pick-overlay-message-icon" symbol="fth-list" :size="18" />
|
||||
<span v-localize="'@ui.emptylist'"></span>
|
||||
</div>
|
||||
<ui-loading v-if="isLoading" />
|
||||
</div>
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { arrayMove, debounce, extendObject } from '../utils';
|
||||
|
||||
const defaultConfig = {
|
||||
// picker items, can either be a static list or a promise
|
||||
items: [],
|
||||
// preview items, can either be a static list or a promise. If null, take the items list
|
||||
previews: [],
|
||||
// exclude Ids from the picker selection items
|
||||
excludedIds: [],
|
||||
// limit results to the given IDs
|
||||
filteredIds: [],
|
||||
// autocomplete allows entering custom texts
|
||||
autocomplete: false,
|
||||
// maximum selection count
|
||||
limit: 10,
|
||||
// multiple selection
|
||||
multiple: true,
|
||||
// whether the previews/results are sortable or not
|
||||
sortable: true,
|
||||
// close picker when an item is clicked
|
||||
closeOnClick: true,
|
||||
// title in dropdown
|
||||
title: null,
|
||||
// automatically open picker
|
||||
autoOpen: false,
|
||||
// picker has pagination
|
||||
paging: false,
|
||||
// theme for the dropdown
|
||||
theme: 'dark',
|
||||
|
||||
keys: {
|
||||
// id key
|
||||
id: 'id',
|
||||
// name key
|
||||
name: 'name',
|
||||
// description key
|
||||
description: 'text',
|
||||
// icon key
|
||||
icon: 'icon'
|
||||
},
|
||||
|
||||
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,
|
||||
// displays an image instead of an icon in the list
|
||||
iconAsImage: 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: true
|
||||
},
|
||||
|
||||
search: {
|
||||
// hides the search input
|
||||
enabled: true,
|
||||
// can force a local search for remote items (via promise)
|
||||
local: false,
|
||||
// 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',
|
||||
// function for local search filtering
|
||||
filterFunc: null
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'uiPick',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default: () =>
|
||||
{
|
||||
return defaultConfig;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
config: {
|
||||
deep: true,
|
||||
handler(newval, oldval)
|
||||
{
|
||||
if (JSON.stringify(newval) !== JSON.stringify(oldval))
|
||||
{
|
||||
this.buildConfig();
|
||||
this.loaded = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
value(val)
|
||||
{
|
||||
this.onValueChanged(val);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
multiple()
|
||||
{
|
||||
return this.configuration.multiple;
|
||||
},
|
||||
canAdd()
|
||||
{
|
||||
return (!this.value && !this.multiple) || (!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.multiple) return '@ui.pick.title_multiple';
|
||||
return '@ui.pick.title';
|
||||
},
|
||||
combinedTitle()
|
||||
{
|
||||
let html = this.configuration.preview.combinedTitle ? this.configuration.preview.combinedTitle : '';
|
||||
|
||||
if (this.previews.length > 0)
|
||||
{
|
||||
html += (html ? ': ' : '') + '<b>' + this.previews.map(p => p[this.configuration.keys.name]).join(', ') + '</b>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
data: () => ({
|
||||
configuration: {},
|
||||
previews: [],
|
||||
allItems: [],
|
||||
items: [],
|
||||
selected: [],
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
debouncedUpdate: null,
|
||||
searchValue: ''
|
||||
}),
|
||||
|
||||
|
||||
created()
|
||||
{
|
||||
this.buildConfig();
|
||||
this.debouncedUpdate = debounce(this.loadSuggestions, 300);
|
||||
this.onValueChanged(this.value);
|
||||
},
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
if (this.configuration.autoOpen)
|
||||
{
|
||||
this.pick();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
refresh()
|
||||
{
|
||||
this.buildConfig();
|
||||
this.loaded = false;
|
||||
},
|
||||
|
||||
onValueChanged(val)
|
||||
{
|
||||
if (this.multiple)
|
||||
{
|
||||
this.selected = Array.isArray(val) && val.length ? JSON.parse(JSON.stringify(val)) : [];
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected = val ? [val] : [];
|
||||
}
|
||||
|
||||
this.loadPreviews();
|
||||
},
|
||||
|
||||
|
||||
buildConfig()
|
||||
{
|
||||
var config = JSON.parse(JSON.stringify(defaultConfig));
|
||||
this.configuration = extendObject(JSON.parse(JSON.stringify(config)), this.config);
|
||||
this.configuration.search = extendObject(config.search, this.config.search || {});
|
||||
this.configuration.addButton = extendObject(config.addButton, this.config.addButton || {});
|
||||
this.configuration.preview = extendObject(config.preview, this.config.preview || {});
|
||||
this.configuration.list = extendObject(config.list, this.config.list || {});
|
||||
this.configuration.keys = extendObject(config.keys, this.config.keys || {});
|
||||
},
|
||||
|
||||
|
||||
overlayOpened()
|
||||
{
|
||||
if (!this.loaded)
|
||||
{
|
||||
this.load();
|
||||
}
|
||||
if (this.configuration.search.enabled && this.configuration.search.focus)
|
||||
{
|
||||
this.$nextTick(() => this.$refs.search.focus());
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
pick()
|
||||
{
|
||||
this.$refs.overlay.toggle();
|
||||
|
||||
if (!this.loaded)
|
||||
{
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
hide()
|
||||
{
|
||||
this.$refs.overlay.hide();
|
||||
},
|
||||
|
||||
|
||||
remove(id)
|
||||
{
|
||||
let index = this.selected.indexOf(id);
|
||||
this.selected.splice(index, 1);
|
||||
this.onChange(this.multiple ? this.selected : null);
|
||||
},
|
||||
|
||||
|
||||
clear()
|
||||
{
|
||||
this.selected = [];
|
||||
this.onChange(this.multiple ? this.selected : null);
|
||||
},
|
||||
|
||||
|
||||
// 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) =>
|
||||
{
|
||||
if (this.configuration.paging && typeof items === 'object' && !Array.isArray(items))
|
||||
{
|
||||
// TODO we need to store metadata to allow pagination
|
||||
items = items.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()
|
||||
{
|
||||
let items = [];
|
||||
let search = this.searchValue;
|
||||
|
||||
let handleResult = (res) =>
|
||||
{
|
||||
if (this.configuration.paging && typeof res === 'object' && !Array.isArray(res))
|
||||
{
|
||||
// TODO we need to store metadata to allow pagination
|
||||
res = res.items;
|
||||
}
|
||||
|
||||
if (this.configuration.excludedIds && this.configuration.excludedIds.length)
|
||||
{
|
||||
res = res.filter(item => this.configuration.excludedIds.indexOf(item[this.configuration.keys.id]) < 0);
|
||||
}
|
||||
if (this.configuration.filteredIds && this.configuration.filteredIds.length)
|
||||
{
|
||||
console.info('filtered-ids:', this.configuration.filteredIds, res);
|
||||
res = res.filter(item => this.configuration.filteredIds.indexOf(item[this.configuration.keys.id]) > -1);
|
||||
}
|
||||
|
||||
this.items = res;
|
||||
};
|
||||
|
||||
if (!search)
|
||||
{
|
||||
items = this.allItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!this.isRemote || this.configuration.search.local)
|
||||
{
|
||||
let query = search.toLowerCase();
|
||||
let filterFunc = this.configuration.search.filterFunc;
|
||||
|
||||
if (!filterFunc)
|
||||
{
|
||||
filterFunc = (item, q, cfg) => item[cfg.keys.name].toLowerCase().indexOf(q) > -1;
|
||||
}
|
||||
|
||||
items = this.allItems.filter(item => filterFunc(item, query, this.configuration));
|
||||
}
|
||||
else if (this.isRemote)
|
||||
{
|
||||
this.configuration.items(search).then(res => handleResult(res));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleResult(items);
|
||||
},
|
||||
|
||||
|
||||
loadPreviews()
|
||||
{
|
||||
let onLoaded = (items, needsFilter) =>
|
||||
{
|
||||
if (this.configuration.paging && typeof items === 'object' && !Array.isArray(items))
|
||||
{
|
||||
// TODO we need to store metadata to allow pagination
|
||||
items = items.data;
|
||||
}
|
||||
|
||||
if (needsFilter)
|
||||
{
|
||||
this.previews = [];
|
||||
this.selected.forEach(id =>
|
||||
{
|
||||
let res = items.find(item => item[this.configuration.keys.id] === id);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
// TODO push error output
|
||||
}
|
||||
else
|
||||
{
|
||||
this.previews.push(JSON.parse(JSON.stringify(res)));
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.previews = items;
|
||||
}
|
||||
|
||||
//console.info(JSON.parse(JSON.stringify(this.previews)));
|
||||
};
|
||||
|
||||
if (this.configuration.autocomplete)
|
||||
{
|
||||
onLoaded(this.selected.map(s =>
|
||||
{
|
||||
let item = { };
|
||||
item[this.configuration.keys.id] = s;
|
||||
item[this.configuration.keys.name] = s;
|
||||
return item;
|
||||
}), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
let promise = (Array.isArray(this.configuration.previews) && this.configuration.previews.length) || typeof this.configuration.previews === 'function' ? this.configuration.previews : this.configuration.items;
|
||||
let isFunc = typeof promise === 'function';
|
||||
|
||||
if (isFunc && this.selected.length > 0)
|
||||
{
|
||||
promise(this.selected).then(onLoaded);
|
||||
}
|
||||
else if (isFunc)
|
||||
{
|
||||
onLoaded([]);
|
||||
}
|
||||
else
|
||||
{
|
||||
onLoaded(promise, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onSearch(value)
|
||||
{
|
||||
this.searchValue = value;
|
||||
if (!this.isRemote || this.configuration.search.local)
|
||||
{
|
||||
this.loadSuggestions();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.debouncedUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onSearchSubmit()
|
||||
{
|
||||
let value = this.searchValue.trim();
|
||||
let item = {};
|
||||
item[this.configuration.keys.id] = value;
|
||||
item[this.configuration.keys.name] = value;
|
||||
this.select(item);
|
||||
},
|
||||
|
||||
|
||||
select(item)
|
||||
{
|
||||
let value = this.configuration.autocomplete ? item[this.configuration.keys.name] : item[this.configuration.keys.id];
|
||||
|
||||
if (this.multiple)
|
||||
{
|
||||
if (!this.canAdd)
|
||||
{
|
||||
if (this.limit > 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.selected = [];
|
||||
}
|
||||
|
||||
var index = this.selected.indexOf(value);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
this.selected.splice(index, 1);
|
||||
this.onDeselected(value, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected.push(value);
|
||||
this.onSelected(value, item);
|
||||
}
|
||||
|
||||
this.onChange(this.selected);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected = [value];
|
||||
this.onChange(value);
|
||||
this.onSelected(value, item);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onSelected(value, item)
|
||||
{
|
||||
this.$emit('select', value, item);
|
||||
},
|
||||
|
||||
|
||||
onDeselected(value, index)
|
||||
{
|
||||
this.$emit('deselect', value, index);
|
||||
},
|
||||
|
||||
|
||||
onChange(value)
|
||||
{
|
||||
this.$emit('input', value);
|
||||
this.$emit('change', value);
|
||||
this.$emit('update:value', value);
|
||||
|
||||
if (this.configuration.closeOnClick)
|
||||
{
|
||||
this.$refs.overlay.hide();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
isSelected(item)
|
||||
{
|
||||
let value = this.configuration.autocomplete ? item[this.configuration.keys.name] : item[this.configuration.keys.id];
|
||||
return this.selected.indexOf(value) > -1;
|
||||
},
|
||||
|
||||
|
||||
getPreviewIcon(preview)
|
||||
{
|
||||
return this.configuration.preview.icon ? (preview[this.configuration.keys.icon] || this.configuration.preview.defaultIcon) : null;
|
||||
},
|
||||
|
||||
|
||||
getPreviewDescription(preview)
|
||||
{
|
||||
return this.configuration.preview.description ? preview[this.configuration.keys.description] : null;
|
||||
},
|
||||
|
||||
|
||||
onSortingUpdated(ev)
|
||||
{
|
||||
this.selected = arrayMove(this.selected, ev.oldIndex, ev.newIndex);
|
||||
this.onChange(this.multiple ? this.selected : this.selected[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-pick-overlay-search
|
||||
{
|
||||
margin: 15px 15px 5px;
|
||||
|
||||
.ui-input
|
||||
{
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-dark .ui-pick-overlay-search .ui-input
|
||||
{
|
||||
background: var(--color-bg-shade-1);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.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-icon-button
|
||||
{
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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-text-dim);
|
||||
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
|
||||
{
|
||||
margin: 15px 0;
|
||||
max-height: 290px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ui-pick-overlay-item
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: 10px 18px;
|
||||
min-height: 48px;
|
||||
width: 100%;
|
||||
border-radius: var(--radius);
|
||||
align-items: center;
|
||||
transition: background .2s, transform .2s, opacity .2s;
|
||||
position: relative;
|
||||
|
||||
&[disabled]
|
||||
{
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
&:hover
|
||||
{
|
||||
background: var(--color-dropdown-selected);
|
||||
}
|
||||
|
||||
&.is-selected
|
||||
{
|
||||
padding-right: 40px;
|
||||
|
||||
.-name
|
||||
{
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-selected:after
|
||||
{
|
||||
font-family: "Feather";
|
||||
content: "\e83e";
|
||||
font-size: 16px;
|
||||
color: var(--color-primary);
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.-icon
|
||||
{
|
||||
margin-right: var(--padding-s);
|
||||
}
|
||||
|
||||
.-image
|
||||
{
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: var(--padding-xs);
|
||||
|
||||
img
|
||||
{
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pick-overlay-item-title
|
||||
{
|
||||
.-name
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.-text
|
||||
{
|
||||
display: block;
|
||||
color: var(--color-text-dim);
|
||||
font-size: var(--font-size-s);
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pick-preview
|
||||
{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ui-pick-preview + .ui-pick-preview,
|
||||
.ui-pick-previews + .ui-select-button,
|
||||
.ui-pick-previews + .ui-pick-add
|
||||
{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ui-pick.is-combined .ui-select-button-label
|
||||
{
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.ui-pick-previews
|
||||
{
|
||||
.ui-icon-button
|
||||
{
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.ui-button-icon
|
||||
{
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-progress" :class="{'is-animated': animated }">
|
||||
<span class="-value" :style="{ transform: 'translateX(' + ((100 - value) * -1) + '%)' }"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiProgress',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
progress()
|
||||
{
|
||||
return this.value > 100 ? 100 : (this.value < 0 ? 0 : this.value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-progress
|
||||
{
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-bg-shade-3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-progress .-value
|
||||
{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform: translateX(-100%);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--color-accent);
|
||||
transition: transform 0.3s linear;
|
||||
}
|
||||
|
||||
.ui-progress.is-animated .-value:before
|
||||
{
|
||||
--color-progress-overlay: #00000022;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
right: -20px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-image: linear-gradient(-60deg, transparent 25%, var(--color-progress-overlay) 25%, var(--color-progress-overlay) 50%, transparent 50%, transparent 75%, var(--color-progress-overlay) 75%, var(--color-progress-overlay));
|
||||
background-size: 20px 30px;
|
||||
animation: uiProgressAnimation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes uiProgressAnimation
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,311 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-property" :class="{'is-vertical': vertical, 'is-text': isText, 'hide-label': hideLabel, 'is-disabled': disabled, 'is-locked': locked, 'can-unlock': canUnlock }">
|
||||
<label v-if="label && !hideLabel" class="ui-property-label" :for="field" v-localize:title="description" :class="{ 'has-description': !!description }">
|
||||
<button type="button" v-if="canUnlock" class="ui-property-label-small is-lock" :class="{'is-unlocked': !locked}" :title="locked ? 'Unlock property...' : 'Lock property'" @click.prevent="$emit(locked ? 'unlock' : 'lock')">
|
||||
<ui-icon :size="13" :symbol="locked ? 'fth-lock' : 'fth-unlock'"></ui-icon>
|
||||
</button>
|
||||
<span v-localize="label"></span>
|
||||
<strong class="ui-property-required" v-if="required">*</strong>
|
||||
<slot name="label-after"></slot>
|
||||
<small v-if="description" v-localize="description"></small>
|
||||
<!--<small class="ui-property-label-small" v-if="description" :size="14" v-localize:title="description">?</small>-->
|
||||
</label>
|
||||
|
||||
<div class="ui-property-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<ui-error v-if="field" :field="field" />
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'uiProperty',
|
||||
|
||||
props: {
|
||||
field: String,
|
||||
label: String,
|
||||
hideLabel: Boolean,
|
||||
description: String,
|
||||
required: Boolean,
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
locked: Boolean,
|
||||
canUnlock: Boolean,
|
||||
isText: Boolean,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-property
|
||||
{
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-gap: 12px 40px;
|
||||
grid-template-columns: minmax(auto, 1fr) auto;
|
||||
|
||||
&.is-disabled .ui-property-content,
|
||||
&.is-locked .ui-property-content
|
||||
{
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.is-disabled, &.is-locked
|
||||
{
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(.is-vertical) > .ui-error
|
||||
{
|
||||
grid-column: span 2 / auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-property + .ui-split,
|
||||
.ui-split + .ui-property,
|
||||
.ui-property + .ui-property
|
||||
{
|
||||
padding-top: 30px;
|
||||
margin-top: 30px;
|
||||
border-top: 1px dashed var(--color-line-dashed);
|
||||
|
||||
.is-narrow &
|
||||
{
|
||||
margin-top: 30px;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-property.is-vertical
|
||||
{
|
||||
grid-template-columns: minmax(auto, 1fr);
|
||||
grid-gap: 12px;
|
||||
flex-direction: column;
|
||||
//border-top: none;
|
||||
|
||||
> .ui-property-label
|
||||
{
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
> .ui-property-label + .ui-property-content
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> .ui-property-content
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-property.is-text
|
||||
{
|
||||
grid-gap: 2px;
|
||||
}
|
||||
|
||||
.ui-property.full-width > .ui-property-content,
|
||||
.ui-property.hide-label > .ui-property-content,
|
||||
.ui-property.is-static > .ui-property-content
|
||||
{
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.ui-property.hide-label > .ui-property-label,
|
||||
.ui-property.is-static > .ui-property-label
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-property.is-static,
|
||||
.ui-property.is-static + .ui-property
|
||||
{
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.ui-property-label
|
||||
{
|
||||
display: block;
|
||||
color: var(--color-text);
|
||||
//flex-basis: 30%;
|
||||
font-size: var(--font-size);
|
||||
line-height: 1.5;
|
||||
font-weight: 700;
|
||||
flex-basis: 100%;
|
||||
|
||||
&.has-description:hover
|
||||
{
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/*.ui-property:focus-within &
|
||||
{
|
||||
color: var(--color-accent-info);
|
||||
}*/
|
||||
}
|
||||
|
||||
.ui-property-label small
|
||||
{
|
||||
display: block;
|
||||
padding-top: 2px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-dim-one);
|
||||
|
||||
&:empty
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ui-property-label-small
|
||||
{
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: 10px;
|
||||
color: var(--color-text-dim);
|
||||
cursor: help;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
border-radius: 16px;
|
||||
background: var(--color-bg-shade-4);
|
||||
|
||||
&:hover
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.is-lock
|
||||
{
|
||||
left: 0;
|
||||
margin-right: 6px;
|
||||
background: none;
|
||||
top: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&[disabled]
|
||||
{
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.is-unlocked
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.is-locked &
|
||||
{
|
||||
color: var(--color-accent-error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-property-required
|
||||
{
|
||||
color: var(--color-required-marker);
|
||||
margin-left: 0.2em;
|
||||
font-weight: 400;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ui-property-content
|
||||
{
|
||||
flex: 1;
|
||||
max-width: 932px;
|
||||
font-size: var(--font-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ui-property-help
|
||||
{
|
||||
max-width: 932px;
|
||||
padding-left: 26px;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-dim);
|
||||
margin: 5px 0 0;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
//letter-spacing: 0.3px;
|
||||
|
||||
&:before
|
||||
{
|
||||
content: "\e87f";
|
||||
font-family: var(--font-icon);
|
||||
color: var(--color-primary);
|
||||
font-size: var(--font-size-l);
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-properties-floating
|
||||
{
|
||||
.ui-property
|
||||
{
|
||||
display: inline-flex;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.ui-property + .ui-property
|
||||
{
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
border-left: 1px solid var(--color-line);
|
||||
padding-left: 50px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-property.ui-property-parent
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
grid-gap: 0; //var(--padding) var(--padding-m);
|
||||
|
||||
> .ui-property
|
||||
{
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 13;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&:empty:first-child + .ui-property,
|
||||
> .ui-property[data-cols] + .ui-property[data-cols]
|
||||
{
|
||||
margin-top: 0;
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-property.ui-property-parent:empty
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,187 +0,0 @@
|
||||
<template>
|
||||
<div v-if="items" class="ui-radio-button" :class="{'is-disabled': disabled, 'is-horizontal': horizontal }" :style="{ 'grid-template-columns': 'repeat(' + items.length + ', minmax(0, 1fr))' }">
|
||||
<label v-for="item in items" class="ui-radio-button-item" :class="{ 'is-active': value === item.value }">
|
||||
<input type="radio" :value="item.value" :checked="item.value === value" @input="onChange(item.value)" :disabled="disabled || item.disabled" />
|
||||
<div class="-output">
|
||||
<span class="-check"><ui-icon symbol="fth-check" :size="12" :stroke="3"></ui-icon></span>
|
||||
<div>
|
||||
<span class="-title">
|
||||
<ui-icon class="-icon" v-if="item.icon" :symbol="item.icon" :size="14"></ui-icon>
|
||||
<span v-localize="item.label"></span>
|
||||
</span>
|
||||
<span class="-description" v-if="item.description" v-localize="item.description"></span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
value: {
|
||||
type: [ String, Number ],
|
||||
default: null
|
||||
},
|
||||
horizontal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onChange(value)
|
||||
{
|
||||
if (!this.disabled)
|
||||
{
|
||||
this.$emit('update:value', value);
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-radio-button
|
||||
{
|
||||
//display: flex;
|
||||
&.is-horizontal
|
||||
{
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-radio-button-item
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: var(--padding-s);
|
||||
border: 1px solid var(--color-line);
|
||||
background: var(--color-box);
|
||||
cursor: pointer;
|
||||
|
||||
&.is-active
|
||||
{
|
||||
//border-color: var(--color-accent);
|
||||
//box-shadow: inset 0 0 0 1px var(--color-accent);
|
||||
//border-radius: var(--radius);
|
||||
background: var(--color-box-nested);
|
||||
z-index: 2;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.-output
|
||||
{
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
grid-gap: var(--padding-s);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.-title
|
||||
{
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.-description
|
||||
{
|
||||
display: block;
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
.-icon
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.-check
|
||||
{
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 20px;
|
||||
background: var(--color-bg-shade-4);
|
||||
|
||||
.ui-icon
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active .-check
|
||||
{
|
||||
background: var(--color-accent);
|
||||
color: var(--color-accent-fg);
|
||||
|
||||
.ui-icon
|
||||
{
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child
|
||||
{
|
||||
border-top-left-radius: var(--radius);
|
||||
border-top-right-radius: var(--radius);
|
||||
}
|
||||
|
||||
&:last-child
|
||||
{
|
||||
border-bottom-left-radius: var(--radius);
|
||||
border-bottom-right-radius: var(--radius);
|
||||
}
|
||||
|
||||
.is-horizontal &
|
||||
{
|
||||
& + .ui-radio-button-item
|
||||
{
|
||||
margin-top: 0;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
&:first-child
|
||||
{
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-left-radius: var(--radius);
|
||||
}
|
||||
|
||||
&:last-child
|
||||
{
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: var(--radius);
|
||||
}
|
||||
}
|
||||
|
||||
& + .ui-radio-button-item
|
||||
{
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
input
|
||||
{
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,253 +0,0 @@
|
||||
|
||||
import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core';
|
||||
import { find } from 'linkifyjs';
|
||||
import { autolink } from '@tiptap/extension-link/src/helpers/autolink';
|
||||
import { clickHandler } from '@tiptap/extension-link/src/helpers/clickHandler';
|
||||
import { pasteHandler } from '@tiptap/extension-link/src/helpers/pasteHandler';
|
||||
import * as overlays from '../../../../services/overlay';
|
||||
|
||||
|
||||
export interface ZeroLinkOptions
|
||||
{
|
||||
/**
|
||||
* If enabled, it adds links as you type.
|
||||
*/
|
||||
autolink: boolean,
|
||||
/**
|
||||
* If enabled, links will be opened on click.
|
||||
*/
|
||||
openOnClick: boolean,
|
||||
/**
|
||||
* Adds a link to the current selection if the pasted content only contains an url.
|
||||
*/
|
||||
linkOnPaste: boolean,
|
||||
/**
|
||||
* A list of HTML attributes to be rendered.
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>,
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType>
|
||||
{
|
||||
zerolink: {
|
||||
/**
|
||||
* Pick a link
|
||||
*/
|
||||
pickLink: () => ReturnType,
|
||||
/**
|
||||
* Set a link mark from data
|
||||
*/
|
||||
setLinkData: (data: object) => ReturnType,
|
||||
/**
|
||||
* Set a link mark
|
||||
*/
|
||||
setLink: (attributes: { href: string, target?: string }) => ReturnType,
|
||||
/**
|
||||
* Toggle a link mark
|
||||
*/
|
||||
toggleLink: (attributes: { href: string, target?: string }) => ReturnType,
|
||||
/**
|
||||
* Unset a link mark
|
||||
*/
|
||||
unsetLink: () => ReturnType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function pickLink(editor)
|
||||
{
|
||||
const result = await overlays.open({
|
||||
component: () => import('../../../../modules/links/picker/ui-linkpicker-overlay.vue'),
|
||||
display: 'editor',
|
||||
model: {
|
||||
value: null,
|
||||
areas: [],
|
||||
allowTitle: false,
|
||||
allowTarget: true,
|
||||
allowSuffix: true
|
||||
}
|
||||
});
|
||||
|
||||
if (result.eventType == 'confirm')
|
||||
{
|
||||
return editor.chain().focus().setLinkData(result.value).run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const ZeroLink = Mark.create<ZeroLinkOptions>({
|
||||
name: 'zerolink',
|
||||
|
||||
priority: 1000,
|
||||
|
||||
keepOnSplit: false,
|
||||
|
||||
inclusive()
|
||||
{
|
||||
return this.options.autolink
|
||||
},
|
||||
|
||||
addOptions()
|
||||
{
|
||||
return {
|
||||
openOnClick: true,
|
||||
linkOnPaste: true,
|
||||
autolink: true,
|
||||
HTMLAttributes: {
|
||||
target: '_self',
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes()
|
||||
{
|
||||
return {
|
||||
href: {
|
||||
default: null,
|
||||
},
|
||||
'zero-link': {
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
default: this.options.HTMLAttributes.target,
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML()
|
||||
{
|
||||
return [
|
||||
{ tag: 'a[href]' },
|
||||
{ tag: 'a[zero-link]' }
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes })
|
||||
{
|
||||
return [
|
||||
'a',
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
]
|
||||
},
|
||||
|
||||
addCommands()
|
||||
{
|
||||
return {
|
||||
|
||||
setLinkData: data => ({ chain }) =>
|
||||
{
|
||||
let attributes = {};
|
||||
|
||||
if (data.target !== 'default')
|
||||
{
|
||||
attributes.target = data.target;
|
||||
}
|
||||
|
||||
if (data.title)
|
||||
{
|
||||
attributes.title = data.title;
|
||||
}
|
||||
|
||||
let link = {
|
||||
area: data.area
|
||||
};
|
||||
|
||||
if (data.values)
|
||||
{
|
||||
link.values = data.values;
|
||||
}
|
||||
|
||||
if (data.urlSuffix)
|
||||
{
|
||||
link.suffix = data.urlSuffix;
|
||||
}
|
||||
|
||||
attributes['zero-link'] = JSON.stringify(link);
|
||||
|
||||
return chain()
|
||||
.setMark(this.name, attributes)
|
||||
.setMeta('preventAutolink', true)
|
||||
.run()
|
||||
},
|
||||
|
||||
setLink: attributes => ({ chain }) =>
|
||||
{
|
||||
return chain()
|
||||
.setMark(this.name, attributes)
|
||||
.setMeta('preventAutolink', true)
|
||||
.run()
|
||||
},
|
||||
|
||||
toggleLink: attributes => ({ chain }) =>
|
||||
{
|
||||
return chain()
|
||||
.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })
|
||||
.setMeta('preventAutolink', true)
|
||||
.run()
|
||||
},
|
||||
|
||||
unsetLink: () => ({ chain }) =>
|
||||
{
|
||||
return chain()
|
||||
.unsetMark(this.name, { extendEmptyMarkRange: true })
|
||||
.setMeta('preventAutolink', true)
|
||||
.run()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addPasteRules()
|
||||
{
|
||||
return [
|
||||
markPasteRule({
|
||||
find: text => find(text)
|
||||
.filter(link => link.isLink)
|
||||
.map(link => ({
|
||||
text: link.value,
|
||||
index: link.start,
|
||||
data: link,
|
||||
})),
|
||||
type: this.type,
|
||||
getAttributes: match => ({
|
||||
href: match.data?.href,
|
||||
}),
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addProseMirrorPlugins()
|
||||
{
|
||||
const plugins = []
|
||||
|
||||
if (this.options.autolink)
|
||||
{
|
||||
plugins.push(autolink({
|
||||
type: this.type,
|
||||
}))
|
||||
}
|
||||
|
||||
if (this.options.openOnClick)
|
||||
{
|
||||
plugins.push(clickHandler({
|
||||
type: this.type,
|
||||
}))
|
||||
}
|
||||
|
||||
if (this.options.linkOnPaste)
|
||||
{
|
||||
plugins.push(pasteHandler({
|
||||
editor: this.editor,
|
||||
type: this.type,
|
||||
}))
|
||||
}
|
||||
|
||||
return plugins
|
||||
},
|
||||
})
|
||||
@@ -1,182 +0,0 @@
|
||||
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
|
||||
import Bold from '@tiptap/extension-bold';
|
||||
import Code from '@tiptap/extension-code';
|
||||
import Italic from '@tiptap/extension-italic';
|
||||
import Strike from '@tiptap/extension-strike';
|
||||
//import Underline from '@tiptap/ex';
|
||||
import History from '@tiptap/extension-history';
|
||||
//import Placeholder from '@tiptap/extension-placeholder';
|
||||
import HorizontalRule from '@tiptap/extension-horizontal-rule';
|
||||
import ListItem from '@tiptap/extension-list-item';
|
||||
import BulletList from '@tiptap/extension-bullet-list';
|
||||
import OrderedList from '@tiptap/extension-ordered-list';
|
||||
import Heading from '@tiptap/extension-heading';
|
||||
import HardBreak from '@tiptap/extension-hard-break';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import { ZeroLink, pickLink } from './extensions/link/link';
|
||||
|
||||
export default function ()
|
||||
{
|
||||
return {
|
||||
|
||||
removeExtension(name)
|
||||
{
|
||||
var ext = this.extensions.find(x => x.name === name);
|
||||
ext && this.extensions.splice(this.extensions.indexOf(ext), 1);
|
||||
},
|
||||
|
||||
removeCommand(alias)
|
||||
{
|
||||
var cmd = this.commands.find(x => x.alias === alias);
|
||||
cmd && this.commands.splice(this.commands.indexOf(cmd), 1);
|
||||
},
|
||||
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
History,
|
||||
HardBreak,
|
||||
ZeroLink,
|
||||
Bold,
|
||||
Code,
|
||||
Italic,
|
||||
Strike,
|
||||
//Underline,
|
||||
ListItem,
|
||||
BulletList,
|
||||
OrderedList,
|
||||
HorizontalRule,
|
||||
Heading.configure({ // TODO
|
||||
levels: [2, 3, 4],
|
||||
})
|
||||
],
|
||||
|
||||
commands: [
|
||||
//{
|
||||
// alias: 'undo',
|
||||
// title: '@rte.undo',
|
||||
// symbol: 'fth-chevron-left',
|
||||
// disabled: cmd => cmd.undoDepth() > 1,
|
||||
// onClick: (ev, cmd) => cmd.undo(ev),
|
||||
//},
|
||||
//{
|
||||
// alias: 'redo',
|
||||
// title: '@rte.redo',
|
||||
// symbol: 'fth-chevron-right',
|
||||
// disabled: cmd => cmd.redoDepth() > 1,
|
||||
// onClick: (ev, cmd) => cmd.redo(ev)
|
||||
//},
|
||||
{
|
||||
alias: 'bold',
|
||||
title: '@rte.bold',
|
||||
symbol: 'fth-bold',
|
||||
symbolSize: 14,
|
||||
isActive: editor => editor.isActive('bold'),
|
||||
onClick: editor => editor.chain().focus().toggleBold().run(),
|
||||
bubble: true
|
||||
},
|
||||
{
|
||||
alias: 'italic',
|
||||
title: '@rte.italic',
|
||||
symbol: 'fth-italic',
|
||||
symbolSize: 14,
|
||||
isActive: editor => editor.isActive('italic'),
|
||||
onClick: editor => editor.chain().focus().toggleItalic().run(),
|
||||
bubble: true
|
||||
},
|
||||
{
|
||||
alias: 'underline',
|
||||
title: '@rte.underline',
|
||||
symbol: 'fth-underline',
|
||||
symbolSize: 14,
|
||||
isActive: editor => editor.isActive('underline'),
|
||||
onClick: editor => editor.chain().focus().toggleUnderline().run(),
|
||||
bubble: true
|
||||
},
|
||||
{
|
||||
alias: 'strikethrough',
|
||||
title: '@rte.strikethrough',
|
||||
symbol: 'fth-strikethrough',
|
||||
symbolSize: 14,
|
||||
isActive: editor => editor.isActive('strike'),
|
||||
onClick: editor => editor.chain().focus().toggleStrike().run(),
|
||||
bubble: true
|
||||
},
|
||||
{
|
||||
alias: 'code',
|
||||
title: '@rte.code',
|
||||
symbol: 'fth-code',
|
||||
symbolSize: 14,
|
||||
isActive: editor => editor.isActive('code'),
|
||||
onClick: editor => editor.chain().focus().toggleCode().run(),
|
||||
bubble: true
|
||||
},
|
||||
{
|
||||
alias: 'link',
|
||||
title: '@rte.link',
|
||||
symbol: 'fth-link-2',
|
||||
symbolSize: 14,
|
||||
isActive: editor => editor.isActive('link'),
|
||||
onClick: editor => pickLink(editor)
|
||||
},
|
||||
{
|
||||
alias: 'line',
|
||||
title: '@rte.line',
|
||||
symbol: 'fth-minus',
|
||||
symbolSize: 15,
|
||||
onClick: editor => editor.chain().focus().setHorizontalRule().run()
|
||||
},
|
||||
{
|
||||
alias: 'list',
|
||||
title: '@rte.list',
|
||||
symbol: 'fth-list',
|
||||
symbolSize: 14,
|
||||
children: [
|
||||
{
|
||||
alias: 'ulist',
|
||||
title: '@rte.ulist',
|
||||
isActive: editor => editor.isActive('bullet_list'),
|
||||
onClick: editor => editor.chain().focus().toggleBulletList().run()
|
||||
},
|
||||
{
|
||||
alias: 'olist',
|
||||
title: '@rte.olist',
|
||||
isActive: editor => editor.isActive('ordered_list'),
|
||||
onClick: editor => editor.chain().focus().toggleOrderedList().run()
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
alias: 'heading',
|
||||
title: '@rte.heading',
|
||||
symbol: 'fth-type',
|
||||
symbolSize: 14,
|
||||
children: [
|
||||
{
|
||||
alias: 'h2',
|
||||
title: '@rte.heading2',
|
||||
isActive: editor => editor.isActive('heading', { level: 2}),
|
||||
onClick: editor => editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
},
|
||||
{
|
||||
alias: 'h3',
|
||||
title: '@rte.heading3',
|
||||
isActive: editor => editor.isActive('heading', { level: 3 }),
|
||||
onClick: editor => editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
},
|
||||
{
|
||||
alias: 'h4',
|
||||
title: '@rte.heading4',
|
||||
isActive: editor => editor.isActive('heading', { level: 4 }),
|
||||
onClick: editor => editor.chain().focus().toggleHeading({ level: 4 }).run()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
|
||||
//import { Node, Extension, Plugin } from '@tiptap/vue-3';
|
||||
//import { chainCommands, exitCode } from 'prosemirror-commands';
|
||||
|
||||
//export class HardBreak extends Node
|
||||
//{
|
||||
// get name()
|
||||
// {
|
||||
// return 'br'
|
||||
// }
|
||||
|
||||
// get schema()
|
||||
// {
|
||||
// return {
|
||||
// inline: true,
|
||||
// group: 'inline',
|
||||
// selectable: false,
|
||||
// parseDOM: [
|
||||
// { tag: 'br' },
|
||||
// ],
|
||||
// toDOM: () => ['br'],
|
||||
// }
|
||||
// }
|
||||
|
||||
// keys({ type })
|
||||
// {
|
||||
// const command = chainCommands(exitCode, (state, dispatch) =>
|
||||
// {
|
||||
// dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
|
||||
// return true
|
||||
// })
|
||||
// return {
|
||||
// 'Enter': command
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
//export class MaxSize extends Extension
|
||||
//{
|
||||
// get name()
|
||||
// {
|
||||
// return 'maxSize'
|
||||
// }
|
||||
|
||||
// get defaultOptions()
|
||||
// {
|
||||
// return {
|
||||
// maxSize: null
|
||||
// }
|
||||
// }
|
||||
|
||||
// get plugins()
|
||||
// {
|
||||
// return [
|
||||
// new Plugin({
|
||||
// appendTransaction: (transactions, oldState, newState) =>
|
||||
// {
|
||||
// const max = this.options.maxSize;
|
||||
// const oldLength = oldState.doc.content.size;
|
||||
// const newLength = newState.doc.content.size;
|
||||
|
||||
// if (max && newLength > max && newLength > oldLength)
|
||||
// {
|
||||
// let newTr = newState.tr;
|
||||
// newTr.insertText('', max + 1, newLength);
|
||||
// return newTr;
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// ]
|
||||
// }
|
||||
//}
|
||||
@@ -1,113 +0,0 @@
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
|
||||
class Menu
|
||||
{
|
||||
|
||||
constructor({ options })
|
||||
{
|
||||
this.options = options
|
||||
this.preventHide = false
|
||||
|
||||
// the mousedown event is fired before blur so we can prevent it
|
||||
this.mousedownHandler = this.handleClick.bind(this)
|
||||
this.options.element.addEventListener('mousedown', this.mousedownHandler, { capture: true })
|
||||
|
||||
this.blurHandler = () =>
|
||||
{
|
||||
if (this.preventHide)
|
||||
{
|
||||
this.preventHide = false
|
||||
return
|
||||
}
|
||||
|
||||
this.options.editor.emit('menubar:focusUpdate', false)
|
||||
}
|
||||
this.options.editor.on('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
handleClick()
|
||||
{
|
||||
this.preventHide = true
|
||||
}
|
||||
|
||||
destroy()
|
||||
{
|
||||
this.options.element.removeEventListener('mousedown', this.mousedownHandler)
|
||||
this.options.editor.off('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const MenuBar = function (options)
|
||||
{
|
||||
return new Plugin({
|
||||
key: new PluginKey('menu_bar'),
|
||||
view(editorView)
|
||||
{
|
||||
return new Menu({ editorView, options })
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
focused: false,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
editor: {
|
||||
immediate: true,
|
||||
handler(editor) {
|
||||
if (editor) {
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
var menubar = MenuBar({
|
||||
editor,
|
||||
element: this.$el,
|
||||
});
|
||||
|
||||
editor.registerPlugin(menubar);
|
||||
|
||||
this.focused = editor.focused
|
||||
|
||||
editor.on('focus', () => {
|
||||
this.focused = true
|
||||
})
|
||||
|
||||
editor.on('menubar:focusUpdate', focused => {
|
||||
this.focused = focused
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
if (!this.editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.$slots.default({
|
||||
focused: this.focused,
|
||||
focus: this.editor.focus,
|
||||
commands: this.editor.commands,
|
||||
isActive: this.editor.isActive,
|
||||
getMarkAttrs: this.editor.getMarkAttrs.bind(this.editor),
|
||||
getNodeAttrs: this.editor.getNodeAttrs.bind(this.editor),
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-rte" :id="id" :disabled="disabled">
|
||||
|
||||
<!--<bubble-menu :editor="editor" :tippy-options="{ duration: 100 }" v-if="editor && hasBubble">
|
||||
<div class="ui-rte-overlay-controls theme-dark is-active">
|
||||
<template v-for="cmd in cmds" :key="cmd.id">
|
||||
<div class="ui-rte-overlay-control-outer" v-if="cmd && cmd.bubble && !cmd.isParent">
|
||||
<button type="button" :data-alias="cmd.alias" v-localize:title="cmd.title" class="ui-rte-overlay-control" :class="{ 'is-active': cmd.isActive(editor) }" @click="cmd.onClick(editor)">
|
||||
<ui-icon :symbol="cmd.symbol" :size="cmd.symbolSize" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</bubble-menu>-->
|
||||
|
||||
<div class="ui-rte-controls" v-if="editor">
|
||||
<div class="ui-rte-control-outer" v-for="cmd in cmds" :key="cmd.id">
|
||||
<button type="button" v-if="!cmd.isParent" :data-alias="cmd.alias" v-localize:title="cmd.title" class="ui-rte-control" :class="{ 'is-active': cmd.isActive(editor) }" @click="cmd.onClick(editor)">
|
||||
<ui-icon :symbol="cmd.symbol" :size="cmd.symbolSize" />
|
||||
</button>
|
||||
<ui-dropdown v-if="cmd.isParent" align="right">
|
||||
<template v-slot:button>
|
||||
<button :data-alias="cmd.alias" type="button" v-localize:title="cmd.title" class="ui-rte-control" :class="{ 'is-active': cmd.isActive(editor) }">
|
||||
<ui-icon :symbol="cmd.symbol" :size="cmd.symbolSize" />
|
||||
</button>
|
||||
</template>
|
||||
<ui-dropdown-button v-for="child in cmd.children" :key="child.id" :label="child.title" @click="child.onClick(editor)" />
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<editor-content class="ui-rte-input" :editor="editor" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { generateId } from '../../utils/numbers';
|
||||
import { debounce } from '../../utils/timing';
|
||||
import { Editor, EditorContent, BubbleMenu } from '@tiptap/vue-3';
|
||||
// import { Placeholder } from '@tiptap/vue-3';
|
||||
import EditorMenuBar from './rte.menubar';
|
||||
//import { MaxSize } from './rte.extensions';
|
||||
import createConfig from './rte.config';
|
||||
|
||||
export default {
|
||||
name: 'uiRte',
|
||||
|
||||
components: { EditorContent, BubbleMenu, EditorMenuBar },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
setup: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
id: 'rte-' + generateId(),
|
||||
blocked: false,
|
||||
onDebouncedChange: null,
|
||||
editor: null,
|
||||
extensions: [],
|
||||
cmds: []
|
||||
}),
|
||||
|
||||
watch: {
|
||||
value()
|
||||
{
|
||||
if (!this.blocked)
|
||||
{
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
disabled(value)
|
||||
{
|
||||
this.editor.setOptions({
|
||||
editable: !value
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasBubble()
|
||||
{
|
||||
return this.cmds.find(x => x.bubble) != null;
|
||||
}
|
||||
},
|
||||
|
||||
created()
|
||||
{
|
||||
this.onDebouncedChange = debounce(this.onChange, 200);
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
var config = createConfig();
|
||||
|
||||
if (typeof this.setup === 'function')
|
||||
{
|
||||
this.setup(config);
|
||||
}
|
||||
|
||||
this.extensions = config.extensions;
|
||||
this.cmds = config.commands.map(cmd =>
|
||||
{
|
||||
let params = this.mapCommand(cmd);
|
||||
|
||||
if (cmd.children && cmd.children.length)
|
||||
{
|
||||
params.isParent = true;
|
||||
params.children = cmd.children.map(child => this.mapCommand(child));
|
||||
}
|
||||
|
||||
return params;
|
||||
});
|
||||
|
||||
//if (this.placeholder)
|
||||
//{
|
||||
// this.extensions.push(new Placeholder({
|
||||
// emptyEditorClass: 'is-editor-empty',
|
||||
// emptyNodeClass: 'is-empty',
|
||||
// emptyNodeText: this.placeholder,
|
||||
// showOnlyWhenEditable: true,
|
||||
// showOnlyCurrent: true
|
||||
// }));
|
||||
//}
|
||||
|
||||
//if (this.maxLength > 0)
|
||||
//{
|
||||
// this.extensions.push(new MaxSize({ maxSize: this.maxLength }));
|
||||
//}
|
||||
|
||||
this.editor = new Editor({
|
||||
editable: !this.disabled,
|
||||
extensions: this.extensions,
|
||||
onUpdate: ({ editor }) =>
|
||||
{
|
||||
this.onDebouncedChange(editor.getHTML());
|
||||
}
|
||||
});
|
||||
|
||||
this.editor.cmds = this.cmds;
|
||||
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init()
|
||||
{
|
||||
this.editor.commands.setContent(this.value, false);
|
||||
},
|
||||
|
||||
onChange(content)
|
||||
{
|
||||
this.blocked = true;
|
||||
this.$emit('input', content);
|
||||
this.$emit('update:value', content);
|
||||
this.$nextTick(() => this.blocked = false);
|
||||
},
|
||||
|
||||
mapCommand(cmd)
|
||||
{
|
||||
return {
|
||||
id: generateId(),
|
||||
alias: cmd.alias,
|
||||
title: cmd.title,
|
||||
symbol: cmd.symbol,
|
||||
symbolSize: cmd.symbolSize || 17,
|
||||
isActive: typeof cmd.isActive === 'function' ? cmd.isActive : () => false,
|
||||
disabled: typeof cmd.disabled === 'function' ? cmd.disabled : () => false,
|
||||
onClick: typeof cmd.onClick === 'function' ? cmd.onClick : () => { },
|
||||
bubble: cmd.bubble || false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.ui-rte
|
||||
{
|
||||
background: var(--color-input);
|
||||
border-radius: var(--radius);
|
||||
border: var(--color-input-border);
|
||||
}
|
||||
|
||||
.ui-rte:focus-within
|
||||
{
|
||||
background-color: var(--color-input-focus-bg);
|
||||
border: var(--color-input-focus-border);
|
||||
box-shadow: var(--color-input-focus-shadow);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ui-rte-input
|
||||
{
|
||||
height: auto;
|
||||
min-height: 48px;
|
||||
padding-top: 9px;
|
||||
padding-bottom: 14px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
border: none !important;
|
||||
|
||||
.ProseMirror:focus
|
||||
{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
p
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p + p
|
||||
{
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
p.is-editor-empty:first-child:before
|
||||
{
|
||||
content: attr(data-empty-text);
|
||||
float: left;
|
||||
color: var(--color-input-placeholder);
|
||||
opacity: .7;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
a
|
||||
{
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-rte-overlay-controls
|
||||
{
|
||||
position: absolute;
|
||||
display: flex;
|
||||
z-index: 20;
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius);
|
||||
padding: 5px;
|
||||
margin-bottom: 0;
|
||||
transform: translateX(-50%) translateY(-100%);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
pointer-events: none;
|
||||
|
||||
&:after
|
||||
{
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 8px solid transparent;
|
||||
margin-left: -7px;
|
||||
border-top-color: var(--color-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-rte-overlay-controls.is-active
|
||||
{
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ui-rte-overlay-control
|
||||
{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--color-text);
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-rte-overlay-control + .ui-rte-overlay-control
|
||||
{
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.ui-rte-overlay-control:hover
|
||||
{
|
||||
background-color: var(--color-bg-shade-1);
|
||||
}
|
||||
|
||||
.ui-rte-overlay-control.is-active
|
||||
{
|
||||
background-color: var(--color-bg-shade-1);
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
|
||||
.ui-rte-controls
|
||||
{
|
||||
background: none;
|
||||
padding: 5px 5px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border-bottom: var(--color-input-border);
|
||||
}
|
||||
|
||||
.ui-rte-controls-label
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
margin: 0 14px 0 8px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ui-rte-control
|
||||
{
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.ui-rte-control[disabled]
|
||||
{
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.ui-rte-control-outer + .ui-rte-control-outer
|
||||
{
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.ui-rte-control:hover, .ui-rte-control.is-active
|
||||
{
|
||||
background: var(--color-box-nested);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-rte-input
|
||||
{
|
||||
background: none !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ui-rte-input .ProseMirror
|
||||
{
|
||||
> *
|
||||
{
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
> :first-child
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> :last-child
|
||||
{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
{
|
||||
margin-bottom: .5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1
|
||||
{
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
h2
|
||||
{
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h3
|
||||
{
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
h4, h5, h6
|
||||
{
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
hr
|
||||
{
|
||||
display: block;
|
||||
border-bottom-style: dashed;
|
||||
border-bottom-color: var(--color-line-dashed);
|
||||
}
|
||||
|
||||
li p
|
||||
{
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-searchinput">
|
||||
<input ref="input" type="search" :value="value" @input="onChange" @keyup.enter="onSubmit" class="ui-input" v-localize:placeholder="placeholder" />
|
||||
<slot name="button" v-bind="{ onSubmit: onSubmit }">
|
||||
<button type="button" class="ui-searchinput-button" v-localize:title="'@ui.search.button'" @click="onSubmit">
|
||||
<ui-icon symbol="fth-search" />
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiSearch',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '@ui.search.placeholder'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onChange(ev)
|
||||
{
|
||||
//this.$emit('change', ev.target.value);
|
||||
this.$emit('input', ev.target.value);
|
||||
//this.$emit('update:value', ev.target.value);
|
||||
//this.$emit('update:modelValue', ev.target.value);
|
||||
},
|
||||
|
||||
onSubmit(ev)
|
||||
{
|
||||
this.$emit('submit', this.$refs.input.value);
|
||||
},
|
||||
|
||||
focus()
|
||||
{
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-searchinput
|
||||
{
|
||||
position: relative;
|
||||
|
||||
.ui-input
|
||||
{
|
||||
display: block;
|
||||
min-width: 320px;
|
||||
padding-right: 40px;
|
||||
border: var(--color-input-border);
|
||||
background: var(--color-input);
|
||||
}
|
||||
|
||||
&.onbg .ui-input:not(:focus)
|
||||
{
|
||||
/*box-shadow: var(--shadow-short);
|
||||
background: var(--color-button-light-onbg);*/
|
||||
background: transparent;
|
||||
border: 1px dashed var(--color-line-dashed-onbg);
|
||||
}
|
||||
|
||||
.ui-searchinput-button
|
||||
{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 45px;
|
||||
text-align: center;
|
||||
font-size: var(--font-size);
|
||||
padding-top: 1px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.ui-input:focus
|
||||
{
|
||||
background-color: var(--color-input-focus-bg);
|
||||
border: var(--color-input-focus-border);
|
||||
box-shadow: var(--color-input-focus-shadow);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<button type="button" class="ui-select-button type-light" :disabled="disabled" @click="tryClick">
|
||||
<span class="ui-select-button-icon" v-if="!isImage">
|
||||
<ui-icon :symbol="icon" />
|
||||
</span>
|
||||
<span class="ui-select-button-icon is-image" v-if="isImage">
|
||||
<ui-thumbnail :media="icon" :alt="icon" />
|
||||
</span>
|
||||
<div class="ui-select-button-content" v-if="label || description">
|
||||
<strong class="ui-select-button-label" v-localize:html="{ key: label, tokens: tokens }"></strong>
|
||||
<span class="ui-select-button-description" v-if="description" v-localize:html="{ key: description, tokens: tokens }"></span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
//import MediaApi from 'zero/api/media.js'; // TODO vue
|
||||
|
||||
export default {
|
||||
name: 'uiSelectButton',
|
||||
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'fth-box'
|
||||
},
|
||||
iconAsImage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
tokens: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
disabled: Boolean
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
isImage()
|
||||
{
|
||||
return this.iconAsImage && (!this.icon || this.icon.indexOf('fth-') !== 0);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
tryClick(ev)
|
||||
{
|
||||
this.$emit('click', ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-native-select" :disabled="disabled">
|
||||
<select :value="value" @input="onChange" :disabled="disabled">
|
||||
<option v-if="emptyOption"></option>
|
||||
<option v-for="option in options" :value="option.value" v-localize="option.label"></option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiSelect',
|
||||
|
||||
props: {
|
||||
value: [String, Number, Object],
|
||||
items: [Array, Function],
|
||||
entity: Object,
|
||||
disabled: Boolean,
|
||||
emptyOption: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
options: []
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
this.rebuild();
|
||||
},
|
||||
|
||||
watch: {
|
||||
items: {
|
||||
deep: true,
|
||||
handler()
|
||||
{
|
||||
this.rebuild();
|
||||
this.onChange({ target: { value: this.value } });
|
||||
}
|
||||
},
|
||||
entity: {
|
||||
deep: true,
|
||||
handler()
|
||||
{
|
||||
this.rebuild();
|
||||
this.onChange({ target: { value: this.value } });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
rebuild()
|
||||
{
|
||||
if (this.entity && typeof this.items === 'function')
|
||||
{
|
||||
this.options = this.items(this.entity);
|
||||
}
|
||||
else if (typeof this.items !== 'function' && this.items)
|
||||
{
|
||||
this.options = [...this.items];
|
||||
}
|
||||
else
|
||||
{
|
||||
this.options = [];
|
||||
}
|
||||
},
|
||||
|
||||
onChange(ev)
|
||||
{
|
||||
this.$emit('update:value', ev.target.value ? ev.target.value : null);
|
||||
this.$emit('input', ev.target.value ? ev.target.value : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-state-button" :class="{'is-disabled': disabled }">
|
||||
<input ref="input" type="hidden" :value="value" />
|
||||
<button v-for="item in items" @click="select(item.value)" v-localize="item.label"
|
||||
type="button" class="ui-state-button-item" :disabled="disabled || item.disabled" :class="{ 'is-active': value === item.value }"></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
value: {
|
||||
type: [ String, Number ],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
select(value)
|
||||
{
|
||||
if (!this.disabled)
|
||||
{
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ui-state-button
|
||||
{
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.ui-state-button.is-disabled button
|
||||
{
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
button.ui-state-button-item
|
||||
{
|
||||
background: var(--color-input);
|
||||
padding: 10px 16px;
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
button.ui-state-button-item + .ui-state-button-item
|
||||
{
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
button.ui-state-button-item:first-of-type
|
||||
{
|
||||
border-top-left-radius: var(--radius);
|
||||
border-bottom-left-radius: var(--radius);
|
||||
}
|
||||
|
||||
button.ui-state-button-item:last-child
|
||||
{
|
||||
border-top-right-radius: var(--radius);
|
||||
border-bottom-right-radius: var(--radius);
|
||||
}
|
||||
|
||||
button.ui-state-button-item.is-active
|
||||
{
|
||||
color: var(--color-text);
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<section v-show="active && !hide" :class="{ 'is-hidden': hide }" class="ui-tab" :aria-hidden="!active ? true : null" role="tabpanel">
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { generateId } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'uiTab',
|
||||
|
||||
props: {
|
||||
label: {
|
||||
type: String
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hide: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
id: null,
|
||||
loaded: false,
|
||||
active: false,
|
||||
hasErrors: false,
|
||||
countOutput: 0
|
||||
}),
|
||||
|
||||
watch: {
|
||||
active(val)
|
||||
{
|
||||
this.loaded = true;
|
||||
},
|
||||
count(val)
|
||||
{
|
||||
this.countOutput = val;
|
||||
}
|
||||
},
|
||||
|
||||
created()
|
||||
{
|
||||
this.id = generateId();
|
||||
this.loaded = this.active;
|
||||
this.countOutput = this.count;
|
||||
this.$parent.register(this);
|
||||
},
|
||||
|
||||
unmounted()
|
||||
{
|
||||
this.$parent.unregister(this);
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
setCount(count)
|
||||
{
|
||||
this.countOutput = count;
|
||||
},
|
||||
|
||||
// set and display errors
|
||||
setErrors(errors, append)
|
||||
{
|
||||
this.hasErrors = !!errors;
|
||||
},
|
||||
|
||||
// clear errors and hide
|
||||
clearErrors()
|
||||
{
|
||||
this.hasErrors = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,202 +0,0 @@
|
||||
<template>
|
||||
<ui-form class="ui-table-filter-overlay" ref="form" v-slot="form" @submit="onSubmit" @load="onLoad">
|
||||
<ui-trinity class="ui-module-overlay">
|
||||
|
||||
<template v-slot:header>
|
||||
<ui-header-bar title="@listfilter.headline" :back-button="false" :close-button="true" @close="config.close(true)" />
|
||||
</template>
|
||||
|
||||
<template v-slot:footer>
|
||||
<ui-button type="light onbg" label="@ui.close" @click="config.close"></ui-button>
|
||||
<ui-button v-if="!model.isCreate" type="light onbg" label="@ui.remove" @click="onRemove"></ui-button>
|
||||
<ui-button type="accent" :submit="true" label="@ui.confirm" :state="form.state" :disabled="loading || disabled || !filterName"></ui-button>
|
||||
</template>
|
||||
|
||||
<ui-loading v-if="loading" :is-big="true" />
|
||||
|
||||
<!--<div v-if="!loading" class="ui-box">
|
||||
<ui-property label="Name" :required="true">
|
||||
<input v-model="model.name" type="text" class="ui-input" maxlength="300" />
|
||||
</ui-property>
|
||||
</div>-->
|
||||
|
||||
<div v-if="!loading" class="ui-box">
|
||||
<div v-for="(field, index) in fields" class="ui-table-filter-overlay-group" :class="{ 'is-open': activeFilter === index, 'has-value': field.preview.selected(value[field.path]) }">
|
||||
<div class="ui-table-filter-overlay-group-head">
|
||||
<ui-select-button :label="field.label" :icon="field.preview.icon" :description="field.preview.value(value[field.path])" @click="toggleFilter(field, index)" />
|
||||
<ui-button class="ui-table-filter-overlay-group-clear" type="blank" label="@listfilter.clearitem" @click="clearFilter(field, index)" />
|
||||
</div>
|
||||
<div v-show="activeFilter === index" class="ui-table-filter-overlay-group-content">
|
||||
<ui-editor-component :field="field" :value="value" @input="onFieldChange" :disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading" class="ui-box ui-table-filter-overlay-filtername">
|
||||
<ui-property label="@listfilter.saveas" description="@listfilter.saveas_text" :vertical="true">
|
||||
<input v-model="filterName" type="text" class="ui-input" maxlength="40" />
|
||||
</ui-property>
|
||||
</div>
|
||||
|
||||
</ui-trinity>
|
||||
</ui-form>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { compileEditor } from '../editor/compile';
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
model: Object,
|
||||
config: Object
|
||||
},
|
||||
|
||||
provide: function ()
|
||||
{
|
||||
return {
|
||||
meta: {},
|
||||
disabled: false
|
||||
};
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
value: {},
|
||||
loading: true,
|
||||
disabled: false,
|
||||
template: null,
|
||||
editor: null,
|
||||
fields: [],
|
||||
filterName: null,
|
||||
activeFilter: null
|
||||
}),
|
||||
|
||||
methods: {
|
||||
|
||||
toggleFilter(filter, index)
|
||||
{
|
||||
this.activeFilter = this.activeFilter === index ? null : index;
|
||||
},
|
||||
|
||||
onFieldChange(value)
|
||||
{
|
||||
//this.$emit('input', this.value);
|
||||
},
|
||||
|
||||
clearFilter(filter, index)
|
||||
{
|
||||
this.value[filter.path] = JSON.parse(JSON.stringify(this.template[filter.path]));
|
||||
},
|
||||
|
||||
onLoad()
|
||||
{
|
||||
this.loading = true;
|
||||
this.template = JSON.parse(JSON.stringify(this.model.template || {}));
|
||||
this.value = JSON.parse(JSON.stringify(this.model.value.filter || this.template));
|
||||
this.editor = compileEditor(this.zero, this.model.editor);
|
||||
this.filterName = this.model.value.name;
|
||||
this.fields = [];
|
||||
|
||||
|
||||
this.editor.tabs.forEach(tab =>
|
||||
{
|
||||
tab.fieldsets.forEach(set =>
|
||||
{
|
||||
set.fields.forEach(field =>
|
||||
{
|
||||
if (!field.preview)
|
||||
{
|
||||
console.warn('[zero] All fields in a filter need filled-out preview options');
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fields.push(field);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
onSubmit()
|
||||
{
|
||||
this.config.confirm({
|
||||
model: this.value,
|
||||
filterName: this.filterName
|
||||
});
|
||||
},
|
||||
|
||||
onRemove()
|
||||
{
|
||||
this.config.confirm({
|
||||
remove: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-table-filter-overlay
|
||||
{
|
||||
.ui-box + .ui-box
|
||||
{
|
||||
margin-top: var(--padding-s);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-filter-overlay-group
|
||||
{
|
||||
&:not(.is-open) + .ui-table-filter-overlay-group
|
||||
{
|
||||
margin-top: var(--padding-s);
|
||||
border-top: 1px solid var(--color-line);
|
||||
padding-top: var(--padding-s);
|
||||
}
|
||||
|
||||
&:not(.has-value) .ui-table-filter-overlay-group-clear
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-filter-overlay-group-head
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-gap: 20px;
|
||||
}
|
||||
|
||||
.ui-table-filter-overlay-group-clear .ui-button-text
|
||||
{
|
||||
font-weight: 400;
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
.ui-table-filter-overlay-group-content
|
||||
{
|
||||
background: var(--color-box);
|
||||
margin: var(--padding-s) 0;
|
||||
padding: var(--padding-m) 0;
|
||||
border-top: 1px solid var(--color-line);
|
||||
border-bottom: 1px solid var(--color-line);
|
||||
|
||||
> .ui-property > .ui-property-label
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-filter-overlay-filtername .ui-property-content
|
||||
{
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
|
||||
.ui-table-filter-overlay .ui-select-button-description
|
||||
{
|
||||
color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,254 +0,0 @@
|
||||
<template>
|
||||
<div v-if="loaded" class="ui-table-filter">
|
||||
<ui-search v-if="!hideSearch" v-model="attach.query.search" class="onbg" />
|
||||
|
||||
<ui-dropdown v-if="hasFilter && storedFilters.length" align="right">
|
||||
<template v-slot:button>
|
||||
<ui-button type="light onbg" :label="filterLabel" :tokens="{ name: currentFilter ? currentFilter.name : null }" caret="down" />
|
||||
</template>
|
||||
<ui-dropdown-button v-if="storedFilters.length" v-for="(filter, index) in storedFilters" :key="index" :value="filter" :label="filter.name" @click="setFilter" />
|
||||
<ui-dropdown-separator v-if="storedFilters.length" />
|
||||
<ui-dropdown-button label="@ui.add" icon="fth-plus" @click="addOrEditFilter()" />
|
||||
<ui-dropdown-button v-if="currentFilter" label="@listfilter.button_edit" icon="fth-edit-2" @click="addOrEditFilter(currentFilter.id)" />
|
||||
<ui-dropdown-button v-if="currentFilter" label="@listfilter.button_clear" icon="fth-x" @click="setFilter(null)" />
|
||||
</ui-dropdown>
|
||||
<ui-button v-if="hasFilter && !storedFilters.length" type="light onbg" label="@listfilter.button" @click="addOrEditFilter()" />
|
||||
|
||||
<ui-dropdown v-if="!hideSelection && selection.length > 0" align="right">
|
||||
<template v-slot:button>
|
||||
<ui-button type="light" :label="selectedText" caret="down" />
|
||||
</template>
|
||||
<slot name="actions"></slot>
|
||||
</ui-dropdown>
|
||||
|
||||
<ui-dropdown v-if="actions && actions.length > 0" align="right">
|
||||
<template v-slot:button>
|
||||
<ui-button type="light onbg" icon="fth-more-horizontal" />
|
||||
</template>
|
||||
<ui-dropdown-button v-for="(action, index) in actions" :key="index" :value="action" :prevent="action.autoclose === false" :label="action.label" :icon="action.icon" @click="onActionClicked" />
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
//import Overlay from 'zero/helpers/overlay.js';
|
||||
import { generateId } from '../utils/numbers';
|
||||
import * as overlays from '../services/overlay';
|
||||
//import FilterOverlay from './table-filter-overlay.vue';
|
||||
|
||||
const KEY_PREFIX = 'zero.ui-table-filter.';
|
||||
|
||||
export default {
|
||||
name: 'uiTableFilter',
|
||||
|
||||
props: {
|
||||
attach: [Object, undefined],
|
||||
hideSearch: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
attach: function (value)
|
||||
{
|
||||
this.setup();
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
hasFilter: false,
|
||||
filterOptions: null,
|
||||
//hideFilter: true,
|
||||
hideSelection: false,
|
||||
selection: [],
|
||||
storedFilters: [],
|
||||
currentFilter: null,
|
||||
actions: []
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
//this.reload();
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.setup();
|
||||
},
|
||||
|
||||
computed: {
|
||||
filterLabel()
|
||||
{
|
||||
return this.currentFilter ? '@listfilter.button_selected' : '@listfilter.button';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
// attach table data
|
||||
setup()
|
||||
{
|
||||
if (!this.attach)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.filterOptions = { ...this.attach.filter };
|
||||
this.hasFilter = this.filterOptions && this.filterOptions.editor;
|
||||
this.storedFilters = this.getStoredFilters();
|
||||
this.actions = this.attach.listConfig.actions;
|
||||
|
||||
let filterId = this.$route.query['filter'];
|
||||
|
||||
if (filterId)
|
||||
{
|
||||
let filter = this.storedFilters.find(x => x.id === filterId);
|
||||
|
||||
if (filter)
|
||||
{
|
||||
this.setFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
},
|
||||
|
||||
|
||||
// called when an action has been clicked
|
||||
onActionClicked(action, opts)
|
||||
{
|
||||
action.call(opts, this);
|
||||
},
|
||||
|
||||
|
||||
// load stored filters from local storage
|
||||
getStoredFilters()
|
||||
{
|
||||
if (!this.hasFilter)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
return JSON.parse(localStorage.getItem(KEY_PREFIX + this.filterOptions.editor.alias) || "[]");
|
||||
},
|
||||
|
||||
|
||||
// set the current filter
|
||||
setFilter(value)
|
||||
{
|
||||
if (!value)
|
||||
{
|
||||
this.currentFilter = null;
|
||||
this.$emit('filter', null);
|
||||
this.attach.setFilter(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.currentFilter = JSON.parse(JSON.stringify(value));
|
||||
this.currentFilter.filter.id = this.currentFilter.id;
|
||||
this.$emit('filter', this.currentFilter.filter);
|
||||
this.attach.setFilter(this.currentFilter.filter);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// removes a filter from the storage
|
||||
removeFilter(id)
|
||||
{
|
||||
let savedFilter = this.storedFilters.find(x => x.id === id);
|
||||
this.storedFilters.splice(this.storedFilters.indexOf(savedFilter), 1);
|
||||
localStorage.setItem(KEY_PREFIX + this.filterOptions.editor.alias, JSON.stringify(this.storedFilters));
|
||||
|
||||
if (this.currentFilter && this.currentFilter.id === id)
|
||||
{
|
||||
this.setFilter(null);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// persist a filter in local storage
|
||||
saveFilter(name, filter, id)
|
||||
{
|
||||
id = id || generateId(4);
|
||||
|
||||
let savedFilter = this.storedFilters.find(x => x.id === id);
|
||||
let index = this.storedFilters.indexOf(savedFilter);
|
||||
let model = { id, name, filter };
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
this.storedFilters.splice(index, 1, model);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.storedFilters.push(model);
|
||||
}
|
||||
|
||||
localStorage.setItem(KEY_PREFIX + this.filterOptions.editor.alias, JSON.stringify(this.storedFilters));
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
|
||||
async addOrEditFilter(id)
|
||||
{
|
||||
let model = { name: null, filter: this.filterOptions.template };
|
||||
|
||||
if (id)
|
||||
{
|
||||
model = this.storedFilters.find(x => x.id === id);
|
||||
}
|
||||
|
||||
const result = await overlays.open({
|
||||
component: () => import('./ui-table-filter-overlay.vue'),
|
||||
display: 'editor',
|
||||
model: {
|
||||
editor: this.filterOptions.editor,
|
||||
template: this.filterOptions.template,
|
||||
value: model,
|
||||
isCreate: !id
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (result.eventType == 'confirm')
|
||||
{
|
||||
const value = result.value;
|
||||
|
||||
if (value.remove && id)
|
||||
{
|
||||
this.removeFilter(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.filterName)
|
||||
{
|
||||
id = this.saveFilter(value.filterName, value.model, id);
|
||||
}
|
||||
|
||||
this.setFilter({
|
||||
id,
|
||||
name: value.filterName,
|
||||
filter: value.model
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-table-filter
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
> * + *
|
||||
{
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,324 +0,0 @@
|
||||
|
||||
|
||||
.ui-table
|
||||
{
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: var(--color-table);
|
||||
flex-wrap: nowrap;
|
||||
-webkit-box-pack: justify;
|
||||
justify-content: space-between;
|
||||
min-width: auto;
|
||||
border-radius: var(--radius);
|
||||
table-layout: fixed;
|
||||
word-wrap: break-word;
|
||||
font-size: var(--font-size);
|
||||
width: 100%;
|
||||
box-shadow: var(--shadow-short);
|
||||
|
||||
&.is-inline
|
||||
{
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-line);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-row
|
||||
{
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
flex-flow: row nowrap;
|
||||
-webkit-box-align: center;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--color-table-line-horizontal);
|
||||
position: relative;
|
||||
transition: outline 0.1s ease, box-shadow 0.1s ease;
|
||||
color: var(--color-text);
|
||||
|
||||
&:last-child
|
||||
{
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ui-table.is-inline &
|
||||
{
|
||||
border-bottom-color: var(--color-line);
|
||||
}
|
||||
}
|
||||
|
||||
a.ui-table-row:hover, button.ui-table-row:hover
|
||||
{
|
||||
//box-shadow: var(--shadow-mid);
|
||||
background: var(--color-table-hover);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
.ui-table-head
|
||||
{
|
||||
font-weight: 700;
|
||||
border-radius: 5px 5px 0 0;
|
||||
color: var(--color-text);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
//border-bottom: 1px solid var(--color-line);
|
||||
z-index: 3;
|
||||
background: var(--color-table-head);
|
||||
/*border-bottom: 2px solid var(--color-bg);*/
|
||||
|
||||
.ui-table-cell
|
||||
{
|
||||
justify-content: space-between;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
.ui-table.is-inline &
|
||||
{
|
||||
background: var(--color-box);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-cell
|
||||
{
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 1 1 5%;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
padding: 15px 20px 14px 20px;
|
||||
border-left: 1px solid var(--color-table-line-vertical);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 58px;
|
||||
min-width: 20px;
|
||||
line-height: 20px;
|
||||
|
||||
&:first-child
|
||||
{
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
&.is-multiline
|
||||
{
|
||||
white-space: normal;
|
||||
overflow: auto;
|
||||
text-overflow: initial;
|
||||
}
|
||||
|
||||
&.is-bold
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.is-selectable, &.is-options
|
||||
{
|
||||
font-size: var(--font-size-l);
|
||||
flex: 0 1 40px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
justify-content: flex-end;
|
||||
|
||||
.ui-native-check-toggle
|
||||
{
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-options
|
||||
{
|
||||
overflow: visible;
|
||||
flex: 0 1 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-row:not(.ui-table-head) .ui-table-cell
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
|
||||
&.is-primary, &:first-child
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.is-vertical
|
||||
{
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.is-name .ui-icon
|
||||
{
|
||||
margin-left: .6em;
|
||||
color: var(--color-synchronized);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-row:not(.is-selected) .ui-table-cell:not(.is-head).is-selectable i
|
||||
{
|
||||
color: var(--color-text);
|
||||
margin-right: 1px;
|
||||
|
||||
&:before
|
||||
{
|
||||
content: "\e8cb";
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-sort
|
||||
{
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: -10px;
|
||||
border-radius: 15px;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&[disabled]
|
||||
{
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.arrow
|
||||
{
|
||||
border-top-color: var(--color-text);
|
||||
transition: opacity 0.2s ease, transform 0.3s ease;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/*&:hover
|
||||
{
|
||||
background: var(--color-bg);
|
||||
}*/
|
||||
|
||||
&:hover .arrow
|
||||
{
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.sort-desc .arrow, &.sort-asc .arrow,
|
||||
{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.sort-asc .arrow
|
||||
{
|
||||
transform: scaleY(-1) translateY(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-empty, .ui-table-loading
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 250px;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.ui-table.is-inline .ui-table-empty,
|
||||
.ui-table.is-inline .ui-table-loading
|
||||
{
|
||||
height: auto;
|
||||
padding: var(--padding) 20px;
|
||||
}
|
||||
|
||||
.ui-table-empty-icon
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
/* special styling for display types */
|
||||
|
||||
.ui-table-field-bool
|
||||
{
|
||||
color: var(--color-text-dim-one);
|
||||
}
|
||||
.ui-table-field-bool[data-symbol="fth-check"]
|
||||
{
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ui-table-cell[field-type="datetime"]
|
||||
{
|
||||
display: inline;
|
||||
|
||||
.-minor
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-cell .-minor
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
font-weight: 400;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.ui-table-field-image
|
||||
{
|
||||
border-radius: 3px;
|
||||
|
||||
&.is-error
|
||||
{
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ui-table-cell .state
|
||||
{
|
||||
display: inline-flex;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
background: var(--color-table-highlight);
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
|
||||
&[data-state="completed"], &[data-state="cancelled"],
|
||||
&[data-state="none"], &[data-state="captured"], &[data-state="authorized"], &[data-state="cancelled"], &[data-state="refunded"]
|
||||
{
|
||||
font-weight: 400;
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
&[data-state="error"]
|
||||
{
|
||||
color: var(--color-accent-error);
|
||||
background: var(--color-accent-error-bg);
|
||||
}
|
||||
&[data-payment-state]
|
||||
{
|
||||
font-weight: 400;
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
&[data-payment-state="error"]
|
||||
{
|
||||
color: var(--color-accent-error);
|
||||
background: var(--color-accent-error-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-table-row:hover .ui-table-cell .state
|
||||
{
|
||||
background: var(--color-bg-shade-4);
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-table-outer">
|
||||
<div v-if="!itemComponentConfig.active" class="ui-table" :class="{'is-inline': inline }">
|
||||
<slot name="top"></slot>
|
||||
|
||||
<header class="ui-table-row ui-table-head">
|
||||
<button type="button" v-if="listConfig.selectable" table-field="table_selectable" class="ui-table-cell is-head is-selectable" @click="select()">
|
||||
<span class="ui-native-check">
|
||||
<input type="checkbox" :checked="selected.length === items.length && selected.length > 0" />
|
||||
<span class="ui-native-check-toggle"></span>
|
||||
</span>
|
||||
</button>
|
||||
<div v-for="column in columns" :key="column.path" class="ui-table-cell" :table-field="column.path" :style="column.flex" :class="column.options.class">
|
||||
<span v-localize:html="column.label"></span>
|
||||
<button :disabled="!column.options.canSort" @click="sort(column)" type="button" class="ui-table-sort" :class="query.orderBy == column.path ? 'sort-' + (query.orderIsDescending ? 'desc' : 'asc') : null">
|
||||
<i class="arrow arrow-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="listConfig.hasOptions" table-field="table_options" class="ui-table-cell is-head is-options"></div>
|
||||
</header>
|
||||
|
||||
<slot name="topRow"></slot>
|
||||
|
||||
<component :is="component" v-for="(item, index) in items" :key="index" :to="getLink(item)" type="button" class="ui-table-row" :class="{ 'is-selected': selected.indexOf(item) > -1 }" @click="onRowClick(item)">
|
||||
<button type="button" v-if="listConfig.selectable" table-field="table_selectable" class="ui-table-cell is-selectable" @click.prevent.stop="select(item)">
|
||||
<span class="ui-native-check">
|
||||
<input type="checkbox" :checked="selected.indexOf(item) > -1" />
|
||||
<span class="ui-native-check-toggle"></span>
|
||||
</span>
|
||||
</button>
|
||||
<div v-for="column in columns" :key="column.path" class="ui-table-cell" :class="column.class" :style="column.flex"
|
||||
@input="onFieldValueChange(column, $event)"
|
||||
:table-field="column.path" :field-type="column.type" v-table-value="{ column, item }"></div>
|
||||
<div v-if="listConfig.hasOptions" table-field="table_options" class="ui-table-cell is-options">
|
||||
<slot name="options" v-bind="{ item, index }"></slot>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
<div class="ui-table-empty" v-if="!isLoading && items.length < 1">
|
||||
<ui-icon class="ui-table-empty-icon" symbol="fth-list" :size="38" />
|
||||
<span v-localize="'@ui.emptylist'"></span>
|
||||
</div>
|
||||
|
||||
<div class="ui-table-loading" v-if="isLoading">
|
||||
<ui-loading />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="itemComponentConfig.active">
|
||||
<div class="ui-datagrid-outer">
|
||||
<div class="ui-datagrid">
|
||||
<div class="ui-datagrid-items" :style="'grid-template-columns: repeat(auto-fill, minmax(' + itemComponentConfig.width + 'px, 1fr))'" :class="{'is-block': itemComponentConfig.block }">
|
||||
<component :is="component" class="ui-datagrid-item" v-for="(item, index) in items" :key="index" :to="getLink(item)" type="button" @click="onRowClick(item)">
|
||||
<component :is="itemComponentConfig.component" :value="item" class="ui-datagrid-cell" :class="{ 'is-selected': selected.indexOf(item) > -1 }"></component>
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<div class="ui-datagrid-empty" v-if="!isLoading && items.length < 1">
|
||||
<i class="ui-datagrid-empty-icon fth-list"></i>
|
||||
<span v-localize="'@ui.emptylist'"></span>
|
||||
</div>
|
||||
|
||||
<div class="ui-datagrid-loading" v-if="isLoading">
|
||||
<ui-loading />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="ui-table-pagination" v-if="pages > 1">
|
||||
<ui-pagination :pages="pages" :page="query.page" @change="setPage" :inline="inline" />
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import './ui-table.scss';
|
||||
import { debounce } from '../utils/timing';
|
||||
import { defineComponent } from 'vue';
|
||||
import Qs from 'qs';
|
||||
import { compileList } from '../editor/compile';
|
||||
|
||||
|
||||
const tableValue = function (el, binding)
|
||||
{
|
||||
const item = binding.value.item;
|
||||
const column = binding.value.column.column;
|
||||
const value = item[column.path];
|
||||
|
||||
if (column.isHtml)
|
||||
{
|
||||
el.innerHTML = column.render(value, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
el.innerText = column.render(value, item);
|
||||
}
|
||||
}
|
||||
|
||||
const FILTER_KEY_PREFIX = 'zero.ui-table-filter.';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'uiTable',
|
||||
|
||||
props: {
|
||||
config: {
|
||||
type: [String, Object],
|
||||
required: true
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
directives: {
|
||||
'table-value': tableValue
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
listConfig: {},
|
||||
columns: [],
|
||||
query: {},
|
||||
alias: null,
|
||||
filter: null,
|
||||
|
||||
items: [],
|
||||
component: 'div',
|
||||
isLoading: true,
|
||||
|
||||
pages: 1,
|
||||
count: 0,
|
||||
|
||||
debouncedUpdate: null,
|
||||
selected: [],
|
||||
|
||||
itemComponentConfig: { active: false }
|
||||
}),
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.setup();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'query.search': function (val)
|
||||
{
|
||||
if (this.loaded)
|
||||
{
|
||||
this.query.page = 1;
|
||||
this.onChange();
|
||||
}
|
||||
},
|
||||
$route(to, from)
|
||||
{
|
||||
//console.info('$route changed', to);
|
||||
this.setup();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async setup()
|
||||
{
|
||||
this.debouncedUpdate = debounce(this.update, 300);
|
||||
const schema = typeof this.config === 'string' ? await this.zero.getSchema(this.config) : this.config;
|
||||
this.alias = schema.alias;
|
||||
this.listConfig = compileList(this.zero, schema);
|
||||
this.itemComponentConfig = this.listConfig.componentConfig || { active: false };
|
||||
//this.listConfig.selectable = true;
|
||||
this.columns = this.listConfig.columns.map(column =>
|
||||
{
|
||||
let col = {
|
||||
...column,
|
||||
class: column.options.class || '',
|
||||
column: column,
|
||||
label: column.options.hideLabel ? null : (column.options.label || this.listConfig.templateLabel(column.path)),
|
||||
flex: column.options.width ? { 'flex': '0 1 ' + column.options.width + 'px' } : {}
|
||||
};
|
||||
|
||||
if (column.options.primary)
|
||||
{
|
||||
col.class += ' is-primary';
|
||||
}
|
||||
|
||||
return col;
|
||||
});
|
||||
this.query = { ...this.listConfig.query, ...this.listConfig.queryToParams(this.$route.query) };
|
||||
this.component = !!this.listConfig.link ? 'router-link' : (!!this.listConfig.onClick ? 'button' : 'div');
|
||||
this.filter = { ...this.listConfig.filterOptions };
|
||||
|
||||
if (this.query.filter && this.query.filter.id)
|
||||
{
|
||||
let filters = this.getStoredFilters();
|
||||
let filter = filters.find(x => x.id === this.query.filter.id);
|
||||
|
||||
if (filter)
|
||||
{
|
||||
this.query.filter = { ...filter.filter, id: filter.id };
|
||||
}
|
||||
}
|
||||
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
this.loaded = true;
|
||||
this.load(true);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// load items based on the current query
|
||||
async load(initial)
|
||||
{
|
||||
this.isLoading = true;
|
||||
|
||||
const fetchResult = await this.listConfig.fetch(this.query);
|
||||
|
||||
if (!fetchResult.success)
|
||||
{
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('loaded', fetchResult);
|
||||
this.pages = fetchResult.paging.totalPages;
|
||||
this.count = fetchResult.paging.totalItems;
|
||||
|
||||
this.$emit('count', this.count);
|
||||
|
||||
this.isLoading = false;
|
||||
this.items = fetchResult.data;
|
||||
this.selected = [];
|
||||
|
||||
if (!this.listConfig.preventScroll) //&& this.configuration.scrollToTop)
|
||||
{
|
||||
let container = document.querySelector(this.listConfig.scrollContainerSelector || '.app-main');
|
||||
|
||||
if (container)
|
||||
{
|
||||
this.$nextTick(() => container.scrollTo({ top: 0, behavior: 'smooth' }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// load stored filters from local storage
|
||||
getStoredFilters()
|
||||
{
|
||||
if (!this.filter.editor)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
return JSON.parse(localStorage.getItem(FILTER_KEY_PREFIX + this.filter.editor.alias) || "[]");
|
||||
},
|
||||
|
||||
|
||||
// updates the list (debounced)
|
||||
async update()
|
||||
{
|
||||
if (!this.isLoading)
|
||||
{
|
||||
await this.load();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// a property has changed and therefore we update the URL and table content
|
||||
onChange()
|
||||
{
|
||||
if (!this.listConfig.noQueryString)
|
||||
{
|
||||
const query = this.listConfig.paramsToQuery(this.query, true);
|
||||
this.$router.replace({ path: this.$route.path, params: this.$route.params, query })
|
||||
}
|
||||
else
|
||||
{
|
||||
this.debouncedUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
getLink(item)
|
||||
{
|
||||
if (!this.listConfig.link)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (typeof this.listConfig.link === 'function')
|
||||
{
|
||||
return this.listConfig.link(item);
|
||||
}
|
||||
return {
|
||||
name: this.listConfig.link,
|
||||
params: {
|
||||
id: item.id
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
onRowClick(item)
|
||||
{
|
||||
if (typeof this.listConfig.onClick === 'function')
|
||||
{
|
||||
this.listConfig.onClick(item);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// set the active page
|
||||
setPage(index)
|
||||
{
|
||||
this.query.page = index;
|
||||
this.onChange();
|
||||
},
|
||||
|
||||
// set a new filter
|
||||
setFilter(filter)
|
||||
{
|
||||
this.query.filter = filter;
|
||||
this.onChange();
|
||||
},
|
||||
|
||||
// sort by a column
|
||||
sort(column)
|
||||
{
|
||||
if (this.query.orderBy === column.path && this.query.orderIsDescending)
|
||||
{
|
||||
this.query.orderIsDescending = false;
|
||||
}
|
||||
else if (this.query.orderBy === column.path)
|
||||
{
|
||||
this.query.orderBy = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.query.orderBy = column.path;
|
||||
this.query.orderIsDescending = true;
|
||||
}
|
||||
|
||||
// reset the page on sorting change
|
||||
this.query.page = 1;
|
||||
|
||||
this.onChange();
|
||||
},
|
||||
|
||||
// toggle selection of an item
|
||||
select(item)
|
||||
{
|
||||
if (!item)
|
||||
{
|
||||
if (this.selected.length >= this.items.length)
|
||||
{
|
||||
this.selected = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected = [];
|
||||
this.items.forEach(item =>
|
||||
{
|
||||
this.selected.push(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const index = this.selected.indexOf(item);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
this.selected.splice(index, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('select', this.selected, this);
|
||||
},
|
||||
|
||||
onFieldValueChange(column, ev)
|
||||
{
|
||||
console.info('change', column, ev);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,240 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-tabs">
|
||||
<div role="tablist" class="ui-tabs-list">
|
||||
<button type="button" v-for="(tab, index) in tabs" class="ui-tabs-list-item" :key="index"
|
||||
:class="{ 'is-active': tab.active, 'has-errors': tab.hasErrors, 'is-hidden': tab.hide }"
|
||||
:disabled="tab.disabled"
|
||||
:aria-selected="tab.active ? true : null"
|
||||
role="tab"
|
||||
@click="select(index)">
|
||||
<ui-icon v-if="tab.hasErrors" class="ui-tabs-list-item-error" :size="16" symbol="fth-alert-circle"></ui-icon>
|
||||
<span v-localize="tab.label"></span>
|
||||
<i v-if="tab.countOutput > 0" class="ui-tabs-list-item-count">{{tab.countOutput}}</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ui-tabs-items">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiTabs',
|
||||
|
||||
props: {
|
||||
cache: {
|
||||
type: String
|
||||
},
|
||||
active: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
storageKey: null,
|
||||
tabs: []
|
||||
}),
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.cacheKey = this.cache ? `zero.ui-tabs.cache.${this.cache}` : null;
|
||||
|
||||
if (this.cache)
|
||||
{
|
||||
const cachedActiveTab = localStorage.getItem(this.cacheKey);
|
||||
if (cachedActiveTab !== null)
|
||||
{
|
||||
this.select(+cachedActiveTab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.select(this.active);
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
register(tab)
|
||||
{
|
||||
this.tabs.push(tab);
|
||||
},
|
||||
|
||||
unregister(tab)
|
||||
{
|
||||
this.tabs = this.tabs.splice(this.tabs.indexOf(tab), 1);
|
||||
},
|
||||
|
||||
select(index, ev)
|
||||
{
|
||||
if (ev)
|
||||
{
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
const currentTab = this.tabs[index];
|
||||
|
||||
if (!currentTab || currentTab.disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabs.forEach((tab, tabIndex) =>
|
||||
{
|
||||
tab.active = index === tabIndex;
|
||||
});
|
||||
|
||||
if (this.cache)
|
||||
{
|
||||
localStorage.setItem(this.cacheKey, index);
|
||||
}
|
||||
|
||||
//this.$emit('changed', { tab: selectedTab });
|
||||
//this.activeTabHash = selectedTab.hash;
|
||||
//this.activeTabIndex = this.getTabIndex(selectedTabHash);
|
||||
//this.lastActiveTabHash = this.activeTabHash = selectedTab.hash;
|
||||
//expiringStorage.set(this.storageKey, selectedTab.hash, this.cacheLifetime);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-tabs
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
.ui-header-bar + .ui-tabs > .ui-tabs-list
|
||||
{
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ui-tabs-list
|
||||
{
|
||||
padding: var(--padding) var(--padding) 0;
|
||||
margin-bottom: calc(var(--padding) * -1);
|
||||
//height: 50px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/*.ui-tabs-items > .ui-tab:first-child.ui-box:not(.is-blank)
|
||||
{
|
||||
border-top-left-radius: 0;
|
||||
}*/
|
||||
|
||||
.ui-tabs-items
|
||||
{
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ui-tabs-list-item
|
||||
{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding: 0 var(--padding-m);
|
||||
font-size: var(--font-size);
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
transition: color 0.2s ease;
|
||||
border-radius: var(--radius-inner) var(--radius-inner) 0 0;
|
||||
background: var(--color-box-light);
|
||||
|
||||
& + .ui-tabs-list-item
|
||||
{
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&:hover
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&[disabled]
|
||||
{
|
||||
cursor: default;
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
&.is-hidden
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-active
|
||||
{
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
background: var(--color-box);
|
||||
box-shadow: var(--shadow-short);
|
||||
|
||||
.ui-tabs-list-item-count
|
||||
{
|
||||
background: var(--color-box-light);
|
||||
}
|
||||
}
|
||||
|
||||
&.has-errors
|
||||
{
|
||||
color: var(--color-accent-error);
|
||||
|
||||
&.is-active
|
||||
{
|
||||
color: var(--color-accent-error);
|
||||
|
||||
&:before
|
||||
{
|
||||
background: var(--color-accent-error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child:after
|
||||
{
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 30px;
|
||||
bottom: -30px;
|
||||
height: 30px;
|
||||
background: var(--color-box-light);
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tabs-list-item-count
|
||||
{
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
float: right;
|
||||
padding: 2px 6px;
|
||||
background: var(--color-box);
|
||||
border-radius: 10px;
|
||||
margin-left: 8px;
|
||||
margin-right: -4px;
|
||||
margin-top: -1px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-tabs-list-item-error
|
||||
{
|
||||
display: inline-block;
|
||||
float: left;
|
||||
margin-right: 6px;
|
||||
margin-left: -4px;
|
||||
position: relative;
|
||||
margin-top: -4px;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.ui-tab.ui-box:first-child
|
||||
{
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-tags" :class="{'is-disabled': disabled }">
|
||||
<ui-input-list :value="value" @input="$emit('input', $event)" :add-label="addLabel" :disabled="disabled" :max-items="maxItems" :max-length="maxLength" :plus-button="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiTags',
|
||||
|
||||
props: {
|
||||
addLabel: {
|
||||
type: String,
|
||||
default: '@ui.add'
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxItems: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-tags
|
||||
{
|
||||
.ui-input-list
|
||||
{
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ui-input-list-item
|
||||
{
|
||||
display: inline-grid;
|
||||
min-width: 0;
|
||||
|
||||
.ui-input
|
||||
{
|
||||
min-width: 120px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
& + .ui-input-list-item, & + .ui-button, & + .ui-select-button
|
||||
{
|
||||
margin-top: 0;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<img v-if="media && !hasError" :src="src" @error="onError" />
|
||||
<span v-else><slot></slot></span>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { paths } from '../options';
|
||||
import { useAppStore } from '../modules/applications/store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'uiThumbnail',
|
||||
|
||||
props: {
|
||||
media: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'thumb'
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
hasError: false,
|
||||
appStore: null,
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
this.appStore = useAppStore();
|
||||
},
|
||||
|
||||
computed: {
|
||||
src()
|
||||
{
|
||||
return this.media ? (paths.api.replace('{app}', this.appStore.appId) + '/backoffice/ui/thumbnail/' + this.media + '-' + this.size + '.tmp') : null;
|
||||
},
|
||||
|
||||
onError(ev)
|
||||
{
|
||||
this.hasError = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,191 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-timepicker" :class="{'is-disabled': disabled }">
|
||||
<input type="text" class="ui-input ui-timepicker-input" v-localize:placeholder="placeholder" :value="output" @input="onChange" @focus="onFocus" @blur="onBlur" :disabled="disabled" />
|
||||
<ui-icon v-if="!clear || !value" symbol="fth-calendar" class="ui-timepicker-icon" :size="17" />
|
||||
<button v-if="clear && value" type="button" class="ui-timepicker-input-button" @click="clearInput"><ui-icon symbol="fth-x" :size="15" /></button>
|
||||
|
||||
<ui-dropdown ref="overlay" class="ui-timepicker-overlay" @opened="overlayOpened">
|
||||
<ui-calendar :today="value" @change="onSelect" :options="pickerOptions" />
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { generateId } from '../utils/numbers';
|
||||
import { formatDate, toIsoDate } from '../utils/dates';
|
||||
|
||||
const TIME_FORMAT = 'HH:mm';
|
||||
|
||||
export default {
|
||||
name: 'uiTimepicker',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '@ui.time.select'
|
||||
},
|
||||
clear: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
amPm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
data: () => ({
|
||||
id: null,
|
||||
output: null,
|
||||
pickerOptions: {}
|
||||
}),
|
||||
|
||||
|
||||
watch: {
|
||||
value()
|
||||
{
|
||||
this.updateOutput();
|
||||
},
|
||||
format()
|
||||
{
|
||||
this.updateOutput();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.id = 'timepicker-' + generateId();
|
||||
this.updateOptions();
|
||||
this.updateOutput();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
updateOutput()
|
||||
{
|
||||
this.output = formatDate(this.value, TIME_FORMAT);
|
||||
},
|
||||
|
||||
|
||||
updateOptions()
|
||||
{
|
||||
this.pickerOptions = {
|
||||
enableTime: true,
|
||||
noCalendar: true,
|
||||
maxDate: null,
|
||||
minDate: null,
|
||||
time_24hr: !this.amPm
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
onSelect(date)
|
||||
{
|
||||
let dateStr = toIsoDate(date);
|
||||
this.setValue(dateStr);
|
||||
this.$refs.overlay.hide();
|
||||
document.activeElement.blur();
|
||||
},
|
||||
|
||||
|
||||
onChange(ev)
|
||||
{
|
||||
this.setValue(ev.target.value);
|
||||
},
|
||||
|
||||
|
||||
setValue(value)
|
||||
{
|
||||
this.$emit('change', value);
|
||||
this.$emit('input', value);
|
||||
},
|
||||
|
||||
|
||||
onFocus()
|
||||
{
|
||||
this.$refs.overlay.show();
|
||||
},
|
||||
|
||||
|
||||
onBlur()
|
||||
{
|
||||
if (!this.$refs.overlay.open)
|
||||
{
|
||||
this.$refs.overlay.hide();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
clearInput()
|
||||
{
|
||||
this.setValue(null);
|
||||
},
|
||||
|
||||
|
||||
overlayOpened()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
//pick()
|
||||
//{
|
||||
// this.$el.
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-timepicker
|
||||
{
|
||||
position: relative;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
input[type="text"].ui-timepicker-input
|
||||
{
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.ui-timepicker-icon
|
||||
{
|
||||
position: absolute;
|
||||
right: 13px;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.ui-timepicker-overlay .ui-dropdown
|
||||
{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-timepicker-input-button
|
||||
{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
font-size: var(--font-size);
|
||||
padding-top: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,193 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-toggle" :class="{'is-disabled': disabled, 'is-negative': negative, 'is-active': on, 'is-content-left': contentLeft }">
|
||||
<input type="checkbox" :value="on" @input="onChange" :disabled="disabled" />
|
||||
<span class="ui-toggle-switch" :class="{ 'is-active': on }"><i></i></span>
|
||||
<i class="fth-minus-circle ui-toggle-off-warning" v-if="offContent && !on && offWarning"></i>
|
||||
<span class="ui-toggle-text" v-if="onContent && on" v-localize="onContent"></span>
|
||||
<span class="ui-toggle-text" v-if="offContent && !on" v-localize="offContent"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiToggle',
|
||||
|
||||
props: {
|
||||
on: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
negative: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
onContent: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
offContent: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
offWarning: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
contentLeft: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onChange(ev)
|
||||
{
|
||||
this.$emit('update:on', !this.on);
|
||||
this.$emit('input', !this.on);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-toggle
|
||||
{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 22px;
|
||||
|
||||
input
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.is-disabled input
|
||||
{
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.is-content-left
|
||||
{
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.ui-toggle-text
|
||||
{
|
||||
margin-left: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-toggle-text
|
||||
{
|
||||
margin-top: 1px;
|
||||
margin-left: 12px;
|
||||
|
||||
.ui-toggle.is-active &
|
||||
{
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-toggle-switch
|
||||
{
|
||||
display: inline-block;
|
||||
height: 22px;
|
||||
width: 36px;
|
||||
background: var(--color-toggle);
|
||||
border-radius: 20px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
|
||||
i
|
||||
{
|
||||
display: inline-block;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-radius: 20px;
|
||||
margin: 3px;
|
||||
background: var(--color-toggle-fg);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&.is-active
|
||||
{
|
||||
background: var(--color-toggled);
|
||||
}
|
||||
|
||||
&.is-active i
|
||||
{
|
||||
background: var(--color-toggled-fg);
|
||||
transform: translateX(14px);
|
||||
}
|
||||
|
||||
input:focus + &
|
||||
{
|
||||
border: var(--color-input-focus-border);
|
||||
box-shadow: var(--color-input-focus-shadow);
|
||||
outline: none;
|
||||
|
||||
&.is-active
|
||||
{
|
||||
background-color: var(--color-toggled);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-toggle.is-disabled &
|
||||
{
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-toggle.onbg .ui-toggle-switch:not(.is-active)
|
||||
{
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.ui-toggle.is-negative .ui-toggle-switch.is-active
|
||||
{
|
||||
background: var(--color-negative);
|
||||
border-color: transparent !important;
|
||||
|
||||
i
|
||||
{
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-toggle.is-accent .ui-toggle-switch.is-active
|
||||
{
|
||||
background: var(--color-accent);
|
||||
border-color: transparent !important;
|
||||
|
||||
i
|
||||
{
|
||||
background: var(--color-accent-fg);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-toggle-off-warning
|
||||
{
|
||||
margin: 0 10px 0 -5px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,379 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-tree-item" :class="getClasses(value)" v-on:contextmenu="onRightClicked(value, $event)">
|
||||
<button :disabled="value.disabled" v-if="value.hasChildren" @click="toggle(value)" type="button" class="ui-tree-item-toggle" :style="{ 'width': ((depth * 15) + 32) + 'px' }">
|
||||
<ui-icon class="ui-tree-item-arrow" :symbol="'fth-chevron-' + (value.isOpen ? 'up' : 'down')" :size="14" />
|
||||
</button>
|
||||
<span v-if="!value.hasChildren" class="ui-tree-item-toggle" :style="{ 'width': ((depth * 15) + 32) + 'px' }"></span>
|
||||
<component :disabled="value.disabled" :is="tag" type="button" :to="value.url" class="ui-tree-item-link" @click="onClick(value, $event)">
|
||||
<ui-icon class="ui-tree-item-icon" v-if="value.icon" :class="{'is-dashed': value.isDashed }" :symbol="value.icon" :size="18" />
|
||||
<ui-icon v-if="value.modifier" :title="value.modifier.name" class="ui-tree-item-modifier" :symbol="modifier" :class="modifierClass" :size="10" :stroke="2.5" />
|
||||
<span class="ui-tree-item-text">
|
||||
<ui-localize :value="value.name" />
|
||||
<span class="ui-tree-item-description" v-if="value.description">
|
||||
<br />
|
||||
<ui-localize :value="value.description" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</component>
|
||||
<ui-dot-button :disabled="value.disabled" class="ui-tree-item-actions" v-if="value.hasActions" @click="onActionsClicked(value, $event)" />
|
||||
<span class="ui-tree-item-count" v-if="value.countOutput != null">{{value.countOutput}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'uiTreeItem',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
activeId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
_isActive: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
isLink()
|
||||
{
|
||||
return this.value && this.value.url;
|
||||
},
|
||||
tag()
|
||||
{
|
||||
return this.isLink ? 'router-link' : 'button';
|
||||
},
|
||||
modifier()
|
||||
{
|
||||
return this.value && this.value.modifier ? this.value.modifier.icon.split(' ')[0] : null;
|
||||
},
|
||||
modifierClass()
|
||||
{
|
||||
return this.value && this.value.modifier ? this.value.modifier.icon.split(' ')[1] : null;
|
||||
},
|
||||
isActive()
|
||||
{
|
||||
return this.value && this.isLink && (
|
||||
(!!this.value.id && this.value.id === this.activeId)
|
||||
|| this.value.id == this.$route.params.id
|
||||
|| (this.value.url && !this.value.url.params && this.value.url.name === this.$route.name && !this.$route.params.id)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
isActive(val)
|
||||
{
|
||||
if (val)
|
||||
{
|
||||
this.$emit('setactive', {
|
||||
$el: this.$el,
|
||||
model: this.value
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
if (this.isActive)
|
||||
{
|
||||
this.$emit('setactive', {
|
||||
$el: this.$el,
|
||||
model: this.value
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick(item, ev)
|
||||
{
|
||||
if (this.isLink)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.$emit('click', item, ev);
|
||||
}
|
||||
},
|
||||
|
||||
// get all classes for a tree item
|
||||
getClasses(item)
|
||||
{
|
||||
return {
|
||||
'has-icon': !!item.icon,
|
||||
'has-children': item.hasChildren,
|
||||
'is-inactive': item.isInactive,
|
||||
'is-open': item.isOpen,
|
||||
'is-selected': this.selected || item.isSelected,
|
||||
'is-disabled': item.disabled,
|
||||
'is-active': this.isActive
|
||||
};
|
||||
},
|
||||
|
||||
// toggles children of an item
|
||||
toggle(item)
|
||||
{
|
||||
this.$emit('open', item);
|
||||
},
|
||||
|
||||
// right clicked on an item
|
||||
onRightClicked(item, ev)
|
||||
{
|
||||
if (!item.disabled)
|
||||
{
|
||||
this.$emit('rightclick', item, ev);
|
||||
}
|
||||
},
|
||||
|
||||
// actions button clicked on item
|
||||
onActionsClicked(item, ev)
|
||||
{
|
||||
if (!item.disabled)
|
||||
{
|
||||
this.$emit('actions', item, ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.ui-tree-item
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
font-size: var(--font-size);
|
||||
padding: 0 var(--padding) 0 0;
|
||||
//height: 54px;
|
||||
height: 58px;
|
||||
border-top: 1px dashed var(--color-line-dashed);
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
transition: color 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
.theme-dark &
|
||||
{
|
||||
border-top: 1px dashed var(--color-line);
|
||||
}
|
||||
|
||||
&:hover > .ui-tree-item-actions
|
||||
{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.is-disabled
|
||||
{
|
||||
cursor: not-allowed;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&.is-active:before, &.is-selected:before, &:hover:before
|
||||
{
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-tree-selected);
|
||||
}
|
||||
|
||||
&.is-selected:after
|
||||
{
|
||||
font-family: "Feather";
|
||||
content: "\e83e";
|
||||
font-size: 16px;
|
||||
color: var(--color-primary);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.is-selected .ui-tree-item-text
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tree-header + .ui-tree-item
|
||||
{
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.ui-tree-item-link
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr auto;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
color: var(--color-text);
|
||||
|
||||
&:hover
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.is-active
|
||||
{
|
||||
color: var(--color-text);
|
||||
font-weight: bold;
|
||||
//color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tree-item-text
|
||||
{
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ui-tree-item-description
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.ui-tree-item-toggle
|
||||
{
|
||||
color: var(--color-text-dim);
|
||||
height: 100%;
|
||||
text-align: right;
|
||||
padding-right: 4px;
|
||||
transition: color 0.2s ease;
|
||||
z-index: 1;
|
||||
|
||||
&:hover
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tree-item-icon
|
||||
{
|
||||
position: relative;
|
||||
top: -2px;
|
||||
color: var(--color-text-dim);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.ui-tree-item-icon.is-dashed
|
||||
{
|
||||
stroke-dasharray: 3px;
|
||||
}
|
||||
|
||||
.ui-tree-item:hover .ui-tree-item-icon
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-tree-item.is-active .ui-tree-item-icon
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-tree-item-loading
|
||||
{
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
|
||||
i
|
||||
{
|
||||
background-color: var(--color-bg-shade-4);
|
||||
transform: translateX(-100%) scaleX(1);
|
||||
animation: treeitemloading 1s linear infinite;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes treeitemloading
|
||||
{
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.ui-tree-item-modifier
|
||||
{
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 17px;
|
||||
color: var(--color-text-dim);
|
||||
background: var(--color-tree);
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
.ui-tree-item.is-active &, .ui-tree-item:hover &
|
||||
{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-tree-item.is-active &
|
||||
{
|
||||
background: var(--color-tree-selected);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tree-item-actions
|
||||
{
|
||||
opacity: 0;
|
||||
color: var(--color-text-dim);
|
||||
|
||||
&:focus
|
||||
{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tree-item-count
|
||||
{
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-box-nested);
|
||||
color: var(--color-text);
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding: 0 10px;
|
||||
border-radius: 16px;
|
||||
font-style: normal;
|
||||
grid-column: 3;
|
||||
margin-right: -4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,257 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-tree">
|
||||
<ui-header-bar class="ui-tree-header" :title="header" :back-button="false" v-if="header">
|
||||
<ui-dot-button @click="onActionsClicked(null, $event)" />
|
||||
</ui-header-bar>
|
||||
<slot></slot>
|
||||
<span v-if="status === 'loading'" class="ui-tree-item-loading"><i></i></span>
|
||||
<template v-for="item in items">
|
||||
<ui-tree-item :value="item" :active-id="active" :depth="depth" :selected="selection.indexOf(item.id) > -1"
|
||||
@rightclick="onRightClicked" @click="onSelect(item, $event)" @actions="onActionsClicked" @open="toggle" @setactive="onActiveSet"
|
||||
/>
|
||||
<ui-tree v-if="item.hasChildren && item.isOpen && status != 'loading'" v-bind="{ get, parent: item.id, depth: depth + 1, active, mode, selection, selectionLimit }" @select="onChildSelect" @setactive="onActiveSet">
|
||||
<template v-slot:actions="props">
|
||||
<slot name="actions" v-bind="props"></slot>
|
||||
</template>
|
||||
</ui-tree>
|
||||
</template>
|
||||
<slot name="bottom"></slot>
|
||||
<ui-dropdown ref="dropdown" align="top" theme="dark" class="ui-tree-dropdown">
|
||||
<slot name="actions" v-bind="actionProps"></slot>
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import UiTreeItem from './ui-tree-item.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'uiTree',
|
||||
|
||||
components: { UiTreeItem },
|
||||
|
||||
props: {
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
active: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
parent: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
header: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
get: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
hasActions: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'link'
|
||||
},
|
||||
selection: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectionLimit: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
items: [],
|
||||
status: 'none',
|
||||
actionProps: {
|
||||
item: null
|
||||
}
|
||||
}),
|
||||
|
||||
computed: {
|
||||
actionsDefined()
|
||||
{
|
||||
return this.$slots.hasOwnProperty('actions');
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onActiveSet(val)
|
||||
{
|
||||
this.$emit('setactive', val);
|
||||
},
|
||||
|
||||
// refreshes the whole tree
|
||||
refresh()
|
||||
{
|
||||
this.load(this.parent);
|
||||
},
|
||||
|
||||
|
||||
// loads children of the given parent id or on root if empty
|
||||
load(parent)
|
||||
{
|
||||
this.setStatus('loading', this.items);
|
||||
|
||||
let promise = this.get(parent, this.active);
|
||||
|
||||
promise.then(response =>
|
||||
{
|
||||
this.items = response;
|
||||
this.setStatus('loaded', this.items);
|
||||
})
|
||||
.catch(error =>
|
||||
{
|
||||
console.error(error);
|
||||
this.setStatus('error', this.items, error);
|
||||
// TODO handle errors
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// updates the status for the current tree
|
||||
setStatus(status)
|
||||
{
|
||||
this.status = status;
|
||||
this.$emit('onStatusChange', status);
|
||||
},
|
||||
|
||||
|
||||
// toggles children of an item
|
||||
toggle(item)
|
||||
{
|
||||
item.isOpen = !item.isOpen;
|
||||
},
|
||||
|
||||
|
||||
// selected an item
|
||||
onSelect(item, ev)
|
||||
{
|
||||
if (this.mode === 'select')
|
||||
{
|
||||
let index = this.selection.indexOf(item.id);
|
||||
if (index > -1)
|
||||
{
|
||||
this.selection.splice(index, 1);
|
||||
}
|
||||
else if (this.selectionLimit === 1)
|
||||
{
|
||||
this.selection.splice(0, this.selection.length);
|
||||
this.selection.push(item.id);
|
||||
}
|
||||
else if (this.selection.length < this.selectionLimit)
|
||||
{
|
||||
this.selection.push(item.id);
|
||||
}
|
||||
|
||||
this.$emit('select', this.selectionLimit > 1 ? this.selection : (this.selection.length > 0 ? this.selection[0] : null), ev);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.$emit('select', item, ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onChildSelect(item, ev)
|
||||
{
|
||||
this.$emit('select', item, ev);
|
||||
},
|
||||
|
||||
|
||||
// right clicked on an item
|
||||
onRightClicked(item, ev)
|
||||
{
|
||||
if (this.actionsDefined && (!item || item.hasActions))
|
||||
{
|
||||
ev.preventDefault();
|
||||
this.onActionsClicked(item, ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// actions button clicked on item
|
||||
onActionsClicked(item, ev)
|
||||
{
|
||||
let dropdown = this.$refs.dropdown;
|
||||
|
||||
if (!this.actionsDefined || (item && !item.hasActions) || (typeof this.hasActions === 'function' && !this.hasActions(item)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionProps.item = item;
|
||||
this.actionProps.event = ev;
|
||||
|
||||
dropdown.toggle();
|
||||
|
||||
if (!dropdown.open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.$nextTick(() =>
|
||||
{
|
||||
let target = ev.target;
|
||||
do
|
||||
{
|
||||
if (target.classList.contains('ui-tree-item') || target.classList.contains('ui-tree-header'))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (target = target.parentElement);
|
||||
|
||||
target = target.querySelector('.ui-dot-button');
|
||||
|
||||
var rect = target.getBoundingClientRect();
|
||||
var width = 240;
|
||||
|
||||
var position = {
|
||||
x: rect.left - width + rect.width,
|
||||
y: rect.top + rect.height
|
||||
};
|
||||
|
||||
let element = dropdown.$el.querySelector('.ui-dropdown');
|
||||
|
||||
element.style.top = position.y + 'px';
|
||||
element.style.left = position.x + 'px';
|
||||
element.style.width = width + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.ui-tree
|
||||
{
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ui-tree-dropdown .ui-dropdown
|
||||
{
|
||||
position: fixed;
|
||||
min-width: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-trinity">
|
||||
<header>
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<content>
|
||||
<slot></slot>
|
||||
</content>
|
||||
<footer>
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiTrinity'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-trinity
|
||||
{
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
height: 100vh;
|
||||
|
||||
> header
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
> footer
|
||||
{
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 20px var(--padding);
|
||||
align-items: center;
|
||||
//background: var(--color-overlay-footer);
|
||||
|
||||
> * + *
|
||||
{
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
> content
|
||||
{
|
||||
display: block;
|
||||
padding: var(--padding);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
import { App } from 'vue';
|
||||
import { ZeroRuntime, ZeroInstallOptions } from './index';
|
||||
|
||||
|
||||
export function createZeroPlugin(options?: ZeroInstallOptions)
|
||||
{
|
||||
return {
|
||||
install: (app: App) =>
|
||||
{
|
||||
const zero = new ZeroRuntime(app, options);
|
||||
app.config.globalProperties.zero = zero;
|
||||
|
||||
zero.useZero();
|
||||
zero.usePlugins();
|
||||
zero.useRouter();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
export * from './createZeroPlugin';
|
||||
export * from './zeroRuntime';
|
||||
export * from './types/zero';
|
||||
export * from './types/zeroPlugin';
|
||||
export * from './types/zeroInstallOptions';
|
||||
export * from './types/zeroPluginOptions';
|
||||
@@ -1,18 +0,0 @@
|
||||
import { RouteLocationNormalized, NavigationGuardNext, nav } from 'vue-router';
|
||||
|
||||
export default function (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext)
|
||||
{
|
||||
if (from.matched.length && from.matched[0].instances)
|
||||
{
|
||||
let instance = from.matched[0].instances.default;
|
||||
|
||||
if (instance.$refs['form'] && typeof instance.$refs.form.beforeRouteLeave === 'function')
|
||||
{
|
||||
isGuarded = true;
|
||||
instance.$refs.form.beforeRouteLeave(to, from, res =>
|
||||
{
|
||||
next(res !== false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
|
||||
|
||||
import { Zero } from '../types/zero';
|
||||
import { createWebHistory, Router, RouterOptions } from 'vue-router';
|
||||
import titleGuard from './titleGuard';
|
||||
import formDirtyGuard from './formDirtyGuard';
|
||||
|
||||
export function getRouterConfig(basePath: string, zero: Zero): RouterOptions
|
||||
{
|
||||
let options = {
|
||||
history: createWebHistory(basePath),
|
||||
linkActiveClass: 'is-active',
|
||||
linkExactActiveClass: 'is-active-exact',
|
||||
scrollBehavior(to, from, savedPosition)
|
||||
{
|
||||
return savedPosition ? savedPosition : { x: 0, y: 0 };
|
||||
},
|
||||
routes: []
|
||||
} as RouterOptions;
|
||||
|
||||
options.routes.push({ name: 'dashboard', path: '/', redirect: '/pages' }); // component: () => import('../../dashboard.vue') });
|
||||
options.routes.push({ name: '404', path: '/:pathMatch(.*)', component: () => import('../../notfound.vue') });
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
export function appendRouterGuards(router: Router): Router
|
||||
{
|
||||
//router.beforeEach(formDirtyGuard);
|
||||
//router.beforeEach(titleGuard);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { localize } from '../../services/localization';
|
||||
import { RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
|
||||
|
||||
export default function (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext)
|
||||
{
|
||||
let title = localize('@zero.name');
|
||||
let name = to.meta.name;
|
||||
|
||||
if (!name && to.matched.length > 1)
|
||||
{
|
||||
to.matched.forEach(route =>
|
||||
{
|
||||
if (!name && route.meta.name)
|
||||
{
|
||||
name = route.meta.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!name || to.meta.alias === 'dashboard') //__zero.alias.sections.dashboard)
|
||||
{
|
||||
document.title = title;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
let nameParts = Array.isArray(name) ? name : [name];
|
||||
let translations = [];
|
||||
|
||||
nameParts.forEach(part =>
|
||||
{
|
||||
if (part)
|
||||
{
|
||||
translations.push(localize(part));
|
||||
}
|
||||
});
|
||||
|
||||
title += ': ' + translations.join(' - ');
|
||||
|
||||
document.title = title;
|
||||
|
||||
next();
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Emitter, EventType } from "mitt";
|
||||
import { Component } from "vue";
|
||||
import { ZeroSchema, ZeroSchemaExtension } from "zero/schemas";
|
||||
|
||||
export interface Zero
|
||||
{
|
||||
version: string;
|
||||
/**
|
||||
* access the event hub
|
||||
**/
|
||||
events: Emitter<Record<EventType, unknown>>;
|
||||
options: ZeroOptions;
|
||||
useZero: () => void;
|
||||
useRouter: () => void;
|
||||
usePlugins: () => void;
|
||||
runPlugins: () => void;
|
||||
|
||||
runtimeVariables: Record<string, any>;
|
||||
|
||||
/**
|
||||
* get a defined field type component
|
||||
**/
|
||||
getFieldTypeComponent(alias: string): Component | undefined;
|
||||
|
||||
/**
|
||||
* get a defined list or editor schema
|
||||
**/
|
||||
getSchema(alias: string): Promise<ZeroSchema | null>;
|
||||
|
||||
/**
|
||||
* get all defined extensions for a schema
|
||||
**/
|
||||
getSchemaExtensions(alias: string): ZeroSchemaExtension[];
|
||||
|
||||
/**
|
||||
* get all defined link areas
|
||||
**/
|
||||
linkAreas: ZeroLinkArea[];
|
||||
|
||||
/**
|
||||
* get a defined link area by alias
|
||||
**/
|
||||
getLinkArea(alias: string): ZeroLinkArea | undefined;
|
||||
}
|
||||
|
||||
export interface ZeroOptions
|
||||
{
|
||||
paths: ZeroPathOptions;
|
||||
}
|
||||
|
||||
export interface ZeroPathOptions
|
||||
{
|
||||
root: string;
|
||||
api: string;
|
||||
}
|
||||
|
||||
export interface ZeroLinkArea
|
||||
{
|
||||
alias: string;
|
||||
name: string;
|
||||
component: Component;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
export interface ZeroInstallOptions
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
import { App } from 'vue';
|
||||
import { Zero } from './zero';
|
||||
import { ZeroPluginOptions } from './zeroPluginOptions';
|
||||
|
||||
|
||||
export interface ZeroPlugin
|
||||
{
|
||||
name: string;
|
||||
install: (zero: ZeroPluginOptions) => void;
|
||||
run?: (zero: Zero) => void;
|
||||
}
|
||||
|
||||
//export class Zero
|
||||
//{
|
||||
// _app: App;
|
||||
// _options: ZeroInstallOptions;
|
||||
|
||||
// get version(): string
|
||||
// {
|
||||
// return "0.0.1";
|
||||
// }
|
||||
|
||||
// constructor(app: App, options?: ZeroInstallOptions)
|
||||
// {
|
||||
// this._app = app;
|
||||
// this._options = options || {};
|
||||
// console.info('[zero installed]');
|
||||
// }
|
||||
//}
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { App, Component } from 'vue';
|
||||
import { ZeroSchemaProp } from '../zero';
|
||||
import { ZeroLinkArea } from './zero';
|
||||
import { ZeroSchema, ZeroSchemaExtension } from 'zero/schemas';
|
||||
|
||||
export interface ZeroPluginOptions
|
||||
{
|
||||
vue: App;
|
||||
routes: RouteRecordRaw[];
|
||||
route: (route: RouteRecordRaw) => void;
|
||||
schemas: Record<string, ZeroSchemaProp>;
|
||||
schema: (alias: string, schema: ZeroSchemaProp) => void;
|
||||
schemaExtensions: ZeroSchemaExtension[];
|
||||
extendSchema: (alias: string, extension: ((schema: ZeroSchema) => void)) => void;
|
||||
fieldTypes: Record<string, Component>;
|
||||
fieldType: (alias: string, component: Component) => void;
|
||||
linkAreas: Record<string, ZeroLinkArea>;
|
||||
linkArea: (alias: string, name: string, component: Component) => void;
|
||||
}
|
||||
|
||||
//export class Zero
|
||||
//{
|
||||
// _app: App;
|
||||
// _options: ZeroInstallOptions;
|
||||
|
||||
// get version(): string
|
||||
// {
|
||||
// return "0.0.1";
|
||||
// }
|
||||
|
||||
// constructor(app: App, options?: ZeroInstallOptions)
|
||||
// {
|
||||
// this._app = app;
|
||||
// this._options = options || {};
|
||||
// console.info('[zero installed]');
|
||||
// }
|
||||
//}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { ZeroSchema } from "zero/schemas";
|
||||
import { Zero } from "./types/zero";
|
||||
|
||||
declare module '@vue/runtime-core'
|
||||
{
|
||||
export interface ComponentCustomProperties
|
||||
{
|
||||
/**
|
||||
* zero instance
|
||||
*/
|
||||
zero: Zero
|
||||
}
|
||||
}
|
||||
|
||||
declare type Lazy<T> = () => Promise<T>;
|
||||
declare type ZeroSchemaProp = ZeroSchema | Lazy<ZeroSchema>;
|
||||
@@ -1,226 +0,0 @@
|
||||
|
||||
import { App, Component } from 'vue';
|
||||
import { ZeroInstallOptions } from './types/zeroInstallOptions';
|
||||
import { ZeroPluginOptions } from './types/zeroPluginOptions';
|
||||
import { Zero, ZeroLinkArea, ZeroOptions } from './types/zero';
|
||||
import { createRouter, RouteRecordRaw, RouterOptions } from 'vue-router';
|
||||
import registerDirectives from '../directives/register';
|
||||
import registerComponents from '../components/register';
|
||||
import registerEditorComponents from '../editor/register';
|
||||
import { getRouterConfig, appendRouterGuards } from './router/routerConfig';
|
||||
import
|
||||
{
|
||||
previewPlugin,
|
||||
countryPlugin,
|
||||
applicationPlugin,
|
||||
settingsPlugin,
|
||||
languagePlugin,
|
||||
pagePlugin,
|
||||
mediaPlugin,
|
||||
spacePlugin,
|
||||
mailTemplatePlugin,
|
||||
translationPlugin,
|
||||
integrationPlugin,
|
||||
userPlugin,
|
||||
linksPlugin,
|
||||
pageModulePlugin,
|
||||
searchPlugin
|
||||
} from '../modules';
|
||||
import editorPlugin from '../editor/plugin';
|
||||
import { ZeroSchema, ZeroSchemaExtension } from 'zero/schemas';
|
||||
import { ZeroSchemaProp } from './zero';
|
||||
import * as zeroOptions from '../options';
|
||||
import plugins from '../plugins.generated';
|
||||
import eventHub from '../services/eventhub';
|
||||
import { Emitter, EventType } from 'mitt';
|
||||
|
||||
plugins.push(
|
||||
editorPlugin, previewPlugin, countryPlugin, applicationPlugin, settingsPlugin, languagePlugin,
|
||||
pagePlugin, linksPlugin, mediaPlugin, spacePlugin, mailTemplatePlugin,
|
||||
translationPlugin, integrationPlugin, userPlugin, pageModulePlugin, searchPlugin
|
||||
);
|
||||
|
||||
|
||||
export class ZeroRuntime implements Zero
|
||||
{
|
||||
_app: App;
|
||||
_installOptions: ZeroInstallOptions;
|
||||
_routerConfig: RouterOptions;
|
||||
_schemas: Record<string, ZeroSchemaProp> = {};
|
||||
_schemaExtensions: ZeroSchemaExtension[] = [];
|
||||
_fieldTypes: Record<string, Component> = {};
|
||||
_linkAreas: Record<string, ZeroLinkArea> = {};
|
||||
|
||||
/**
|
||||
* version of zero backoffice
|
||||
**/
|
||||
get version(): string
|
||||
{
|
||||
return "0.0.1";
|
||||
}
|
||||
|
||||
/**
|
||||
* access the event hub
|
||||
**/
|
||||
get events(): Emitter<Record<EventType, unknown>>
|
||||
{
|
||||
return eventHub;
|
||||
}
|
||||
|
||||
/**
|
||||
* options
|
||||
**/
|
||||
options: ZeroOptions;
|
||||
|
||||
/**
|
||||
* runtime variables
|
||||
**/
|
||||
runtimeVariables: Record<string, any> = {};
|
||||
|
||||
constructor(app: App, options?: ZeroInstallOptions)
|
||||
{
|
||||
this._app = app;
|
||||
this._installOptions = options || {};
|
||||
this._routerConfig = getRouterConfig('/zero', this);
|
||||
this.options = zeroOptions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* register core components
|
||||
**/
|
||||
useZero()
|
||||
{
|
||||
let app = this._app;
|
||||
|
||||
registerDirectives(app);
|
||||
registerComponents(app);
|
||||
registerEditorComponents(app);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* create plugin options which are passed to install() for all plugins
|
||||
**/
|
||||
usePlugins()
|
||||
{
|
||||
const pluginOptions = {
|
||||
vue: this._app,
|
||||
routes: this._routerConfig.routes,
|
||||
schemas: this._schemas,
|
||||
schemaExtensions: this._schemaExtensions,
|
||||
fieldTypes: this._fieldTypes,
|
||||
linkAreas: this._linkAreas,
|
||||
route(route: RouteRecordRaw)
|
||||
{
|
||||
this.routes.push(route);
|
||||
},
|
||||
schema(alias: string, schema: ZeroSchemaProp)
|
||||
{
|
||||
this.schemas[alias] = schema;
|
||||
},
|
||||
extendSchema(alias: string, extension: ((schema: ZeroSchema) => void))
|
||||
{
|
||||
this.schemaExtensions.push({ alias, extension });
|
||||
},
|
||||
fieldType(alias: string, component: Component) // TODO v3 allow default field options
|
||||
{
|
||||
this.fieldTypes[alias] = component;
|
||||
},
|
||||
linkArea(alias: string, name: string, component: Component)
|
||||
{
|
||||
this.linkAreas[alias] = { alias, name, component };
|
||||
}
|
||||
} as ZeroPluginOptions;
|
||||
|
||||
// install all plugins
|
||||
plugins.forEach(plugin =>
|
||||
{
|
||||
plugin.install(pluginOptions);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
runPlugins()
|
||||
{
|
||||
plugins.forEach(plugin =>
|
||||
{
|
||||
if (typeof plugin.run === 'function')
|
||||
{
|
||||
plugin.run(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* after all plugins were installed we can create the router
|
||||
* with the designated routes
|
||||
**/
|
||||
useRouter()
|
||||
{
|
||||
const router = createRouter(this._routerConfig);
|
||||
appendRouterGuards(router);
|
||||
this._app.use(router);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get a defined field type component
|
||||
**/
|
||||
getFieldTypeComponent(alias: string): Component | undefined
|
||||
{
|
||||
return this._fieldTypes[alias];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get a defined list or editor schema
|
||||
**/
|
||||
async getSchema(alias: string): Promise<ZeroSchema | null>
|
||||
{
|
||||
let schema: ZeroSchemaProp = this._schemas[alias];
|
||||
|
||||
if (!schema)
|
||||
{
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (typeof schema === 'function')
|
||||
{
|
||||
const res = await schema();
|
||||
schema = res.default as ZeroSchema;
|
||||
}
|
||||
|
||||
schema.alias = alias;
|
||||
|
||||
return Promise.resolve(schema);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get all defined extensions for a schema
|
||||
**/
|
||||
getSchemaExtensions(alias: string): ZeroSchemaExtension[]
|
||||
{
|
||||
return this._schemaExtensions.filter(x => x.alias == alias);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get all defined link areas
|
||||
**/
|
||||
get linkAreas(): ZeroLinkArea[]
|
||||
{
|
||||
return Object.values(this._linkAreas);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get a defined link area by alias
|
||||
**/
|
||||
getLinkArea(alias: string): ZeroLinkArea | undefined
|
||||
{
|
||||
return this._linkAreas[alias];
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard">hi zero</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.dashboard
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
font-size: 48px;
|
||||
font-weight: 100;
|
||||
color: var(--color-text);
|
||||
opacity: 0.2;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
import { App } from 'vue';
|
||||
import vClickOutside from './v-click-outside';
|
||||
import vCurrency from './v-currency';
|
||||
import vDate from './v-date';
|
||||
import vFilesize from './v-filesize';
|
||||
import vLocalize from './v-localize';
|
||||
import vMaxLines from './v-max-lines';
|
||||
import vPlaceholder from './v-placeholder';
|
||||
import vResizeable from './v-resizeable';
|
||||
import vSortable from './v-sortable';
|
||||
import vEncode from './v-encode'
|
||||
import vMultiline from './v-multiline';
|
||||
|
||||
const directives = [
|
||||
{ key: 'click-outside', definition: vClickOutside },
|
||||
{ key: 'currency', definition: vCurrency },
|
||||
{ key: 'date', definition: vDate },
|
||||
{ key: 'filesize', definition: vFilesize },
|
||||
{ key: 'localize', definition: vLocalize },
|
||||
{ key: 'max-lines', definition: vMaxLines },
|
||||
{ key: 'placeholder', definition: vPlaceholder },
|
||||
{ key: 'resizeable', definition: vResizeable },
|
||||
{ key: 'sortable', definition: vSortable },
|
||||
{ key: 'encode', definition: vEncode },
|
||||
{ key: 'multiline', definition: vMultiline }
|
||||
];
|
||||
|
||||
export default function (app: App)
|
||||
{
|
||||
directives.forEach(directive =>
|
||||
{
|
||||
app.directive(directive.key, directive.definition);
|
||||
});
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
|
||||
const isPopup = (popupItem, elements) =>
|
||||
{
|
||||
if (!popupItem || !elements)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0, len = elements.length; i < len; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (popupItem.contains(elements[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (elements[i].contains(popupItem))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const validate = (binding) =>
|
||||
{
|
||||
if (typeof binding.value !== 'function')
|
||||
{
|
||||
console.warn('v-click-outside: provided expression ' + binding.expression + ' is not a function.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* resize an element
|
||||
*/
|
||||
export default {
|
||||
bind(el, binding, vNode)
|
||||
{
|
||||
if (!validate(binding)) return;
|
||||
|
||||
// Define Handler and cache it on the element
|
||||
function handler(e)
|
||||
{
|
||||
if (!vNode.context) return;
|
||||
|
||||
// some components may have related popup item, on which we shall prevent the click outside event handler.
|
||||
var elements = e.path || (e.composedPath && e.composedPath());
|
||||
elements && elements.length > 0 && elements.unshift(e.target);
|
||||
|
||||
if (el.contains(e.target) || isPopup(vNode.context.popupItem, elements) || !el.__vueClickOutside__)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
el.__vueClickOutside__.callback(e);
|
||||
}
|
||||
|
||||
// add Event Listeners
|
||||
el.__vueClickOutside__ = {
|
||||
handler: handler,
|
||||
callback: binding.value
|
||||
};
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
document.addEventListener('click', handler);
|
||||
}, 200);
|
||||
},
|
||||
|
||||
update(el, binding)
|
||||
{
|
||||
if (validate(binding))
|
||||
{
|
||||
el.__vueClickOutside__.callback = binding.value;
|
||||
}
|
||||
},
|
||||
|
||||
unbind(el, binding, vNode)
|
||||
{
|
||||
document.removeEventListener('click', el.__vueClickOutside__.handler);
|
||||
delete el.__vueClickOutside__;
|
||||
}
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
import { toCurrency } from '../utils/numbers';
|
||||
|
||||
/**
|
||||
* Outputs a currency
|
||||
*/
|
||||
export default (el, binding) =>
|
||||
{
|
||||
if (binding.value !== binding.oldValue)
|
||||
{
|
||||
el.innerHTML = toCurrency(binding.value);
|
||||
}
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
import { formatDate } from '../utils/dates';
|
||||
|
||||
/**
|
||||
* Outputs a formatted date
|
||||
*/
|
||||
export default (el, binding) =>
|
||||
{
|
||||
if (binding.value !== binding.oldValue)
|
||||
{
|
||||
if (!binding.value)
|
||||
{
|
||||
el.innerHTML = '-';
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = formatDate(binding.value, binding.arg);
|
||||
}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
import { convertHtmlToText } from '../utils';
|
||||
|
||||
|
||||
export default (el, binding) =>
|
||||
{
|
||||
if (binding.value !== binding.oldValue)
|
||||
{
|
||||
el.innerHTML = convertHtmlToText(binding.value, binding.arg === 'nl');
|
||||
}
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
import { getFilesize } from '../utils/filesize';
|
||||
|
||||
/**
|
||||
* Outputs a filesize
|
||||
*/
|
||||
export default (el, binding) =>
|
||||
{
|
||||
if (binding.value !== binding.oldValue)
|
||||
{
|
||||
el.innerText = getFilesize(binding.value, typeof binding.arg !== 'undefined' ? +binding.arg : 1);
|
||||
}
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
|
||||
import { localize } from '../services/localization';
|
||||
|
||||
/**
|
||||
* Localizes the given property and sets the inner-text of the node to its result
|
||||
*/
|
||||
export default (el, binding) =>
|
||||
{
|
||||
if (binding.value !== binding.oldValue || !el.innerText)
|
||||
{
|
||||
const hasValue = !!binding.value;
|
||||
const isObject = typeof binding.value === 'object';
|
||||
let key = hasValue ? (isObject ? binding.value.key : binding.value) : null;
|
||||
let options = hasValue && isObject ? binding.value : null;
|
||||
let html = isObject && binding.value ? (binding.value.html || binding.arg === 'html') : binding.arg === 'html';
|
||||
|
||||
const result = hasValue ? localize(key, options) : '';
|
||||
|
||||
// set content as html
|
||||
if (html)
|
||||
{
|
||||
el.innerHTML = result;
|
||||
}
|
||||
// set title
|
||||
else if (binding.arg === 'title')
|
||||
{
|
||||
el.title = result;
|
||||
}
|
||||
// set attribute
|
||||
else if (binding.arg)
|
||||
{
|
||||
el.setAttribute(binding.arg, result);
|
||||
}
|
||||
// set content as plain text
|
||||
else
|
||||
{
|
||||
el.innerText = result;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
/**
|
||||
* Limits an element to a maximum number of text lines (can be toggled with click)
|
||||
*/
|
||||
export default (el, binding) =>
|
||||
{
|
||||
if (binding.value !== binding.oldValue)
|
||||
{
|
||||
if (!el.__zero_maxlines)
|
||||
{
|
||||
el.__zero_maxlines = true;
|
||||
el.addEventListener('click', e =>
|
||||
{
|
||||
el.classList.toggle('is-expanded');
|
||||
});
|
||||
}
|
||||
|
||||
el.classList.add('ui-maxlines');
|
||||
el.style.webkitLineClamp = +binding.value > 0 ? +binding.value : null;
|
||||
}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
/**
|
||||
* Converts new line chars to <br>
|
||||
*/
|
||||
export default (el, binding) =>
|
||||
{
|
||||
if (binding.value !== binding.oldValue)
|
||||
{
|
||||
el.innerHTML = binding.value.split('\n').join('<br>');
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user