Add hint field

This commit is contained in:
philipp lang 2024-04-09 01:08:49 +02:00
parent b59bc28f64
commit a92c549b95
18 changed files with 463 additions and 10 deletions

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -9,7 +9,141 @@
<body style="padding: 50px">
<event-form
style="--primary: hsl(181, 75%, 26%); --secondary: hsl(181, 75%, 35%); --font: hsl(181, 84%, 78%); --circle: hsl(181, 86%, 16%)"
value='{"sections":[{"name":"ff","intro":"ff","fields":[{"name":"Mitglieder","type":"NamiField","columns":{"mobile":2,"tablet":4,"desktop":6},"default":[],"required":false,"description":"Mitglieder","key":"members"},{"name":"Bezirk","type":"GroupField","columns":{"mobile":2,"tablet":4,"desktop":6},"default":"","required":true,"parent_field":null,"parent_group":120,"key":"bezirk"},{"name":"Stamm","type":"GroupField","columns":{"mobile":2,"tablet":4,"desktop":6},"default":"","required":true,"parent_field":"bezirk","parent_group":null,"key":"stamm"}]}]}'
value='{"sections":[
{
"name": "tet",
"intro": "",
"fields": [
{
"name": "checkbox",
"type": "CheckboxField",
"columns": {
"mobile": 2,
"tablet": 4,
"desktop": 6
},
"value": false,
"required": true,
"nami_type": null,
"for_members": true,
"special_type": null,
"description": "Sed laoreet mattis sem, vel sodales leo ullamcorper ut. Aliquam hendrerit vestibulum ex, sit amet consequat dolor donec.",
"hint": "Quisque justo leo, ultricies vestibulum.",
"key": "checkbox"
},
{
"name": "checkboxes",
"hint": "Quisque justo leo, ultricies vestibulum.",
"type": "CheckboxesField",
"columns": {
"mobile": 2,
"tablet": 4,
"desktop": 6
},
"value": [],
"required": false,
"nami_type": null,
"for_members": true,
"special_type": null,
"options": [
"Integer molestie enim vitae enim tellus.",
"Cras ut magna ac metus rutrum efficitur."
],
"key": "checkboxes"
},
{
"name": "date",
"hint": "Quisque justo leo, ultricies vestibulum.",
"type": "DateField",
"columns": {
"mobile": 2,
"tablet": 4,
"desktop": 6
},
"value": null,
"required": false,
"nami_type": null,
"for_members": true,
"special_type": null,
"max_today": false,
"key": "date"
},
{
"name": "dropdown",
"hint": "Quisque justo leo, ultricies vestibulum.",
"type": "DropdownField",
"columns": {
"mobile": 2,
"tablet": 4,
"desktop": 6
},
"value": null,
"required": false,
"nami_type": null,
"for_members": true,
"special_type": null,
"options": [
"a",
"v"
],
"key": "dropdown"
},
{
"name": "radio",
"hint": "Quisque justo leo, ultricies vestibulum.",
"type": "RadioField",
"columns": {
"mobile": 2,
"tablet": 4,
"desktop": 6
},
"value": null,
"required": false,
"nami_type": null,
"for_members": true,
"special_type": null,
"options": [
"a",
"v"
],
"key": "radio"
},
{
"name": "text",
"hint": "Quisque justo leo, ultricies vestibulum.",
"type": "TextField",
"columns": {
"mobile": 2,
"tablet": 4,
"desktop": 6
},
"value": "",
"required": false,
"nami_type": null,
"for_members": true,
"special_type": null,
"key": "text"
},
{
"name": "textarea",
"hint": "Quisque justo leo, ultricies vestibulum.",
"type": "TextareaField",
"columns": {
"mobile": 2,
"tablet": 4,
"desktop": 6
},
"value": "",
"required": false,
"nami_type": null,
"for_members": true,
"special_type": null,
"rows": 5,
"key": "textarea"
}
]
}
]}'
url="http://localhost:8000"
editable
></event-form>

48
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.17",
"floating-vue": "^5.2.2",
"lodash": "^4.17.21",
"prettier": "^3.2.5",
"vue": "^3.3.11",
@ -63,6 +64,27 @@
"node": ">=12"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
"integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
"dependencies": {
"@floating-ui/utils": "^0.2.1"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz",
"integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==",
"dependencies": {
"@floating-ui/core": "^1.1.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -764,6 +786,24 @@
"node": ">=8"
}
},
"node_modules/floating-vue": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.2.2.tgz",
"integrity": "sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==",
"dependencies": {
"@floating-ui/dom": "~1.1.1",
"vue-resize": "^2.0.0-alpha.1"
},
"peerDependencies": {
"@nuxt/kit": "^3.2.0",
"vue": "^3.2.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
}
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
@ -1812,6 +1852,14 @@
}
}
},
"node_modules/vue-resize": {
"version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz",
"integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==",
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue3-carousel": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/vue3-carousel/-/vue3-carousel-0.3.1.tgz",

View File

@ -12,6 +12,7 @@
"dependencies": {
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.17",
"floating-vue": "^5.2.2",
"lodash": "^4.17.21",
"prettier": "^3.2.5",
"vue": "^3.3.11",

View File

@ -1,10 +1,13 @@
<template>
<span class="absolute text-gray-600 flex bg-white items-center -top-3 px-1 text-xs @sm:text-sm" :class="{'left-0': inline, 'left-2': !inline}">
<span v-text="name"></span> <span v-show="required" class="text-red-800 ml-1">*</span>
<hint :value="hint" class="ml-2" v-if="hint" small></hint>
</span>
</template>
<script setup>
import Hint from './Hint.vue';
defineProps({
name: {},
required: {},
@ -12,5 +15,10 @@ defineProps({
type: Boolean,
default: () => false,
},
hint: {
required: false,
validator: (value) => value === null || typeof value === 'string',
default: () => null,
},
});
</script>

24
src/components/Hint.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<div v-tooltip="value" :class="{'w-4 h-4': small, 'w-4 h-4 sm:w-6 sm:h-6': !small}" class="rounded-full flex-none flex items-center justify-center bg-font text-primary">
<info-icon :class="{'w-3 h-3': small, 'w-3 h-3 sm:w-4 sm:h-4': !small}"></info-icon>
</div>
</template>
<script setup>
import InfoIcon from './icons/InfoIcon.vue';
import useFloating from '../composables/useFloating.js';
defineProps({
value: {
required: true,
validator: (value) => typeof value === 'string',
},
small: {
required: false,
type: Boolean,
default: () => false,
},
});
const {vTooltip} = useFloating();
</script>

View File

@ -1,5 +1,5 @@
<template>
<v-checkbox :id="innerId" :name="field.key" :label="field.description" :required="field.required" v-model="inner"></v-checkbox>
<v-checkbox :id="innerId" v-model="inner" :name="field.key" :label="field.description" :required="field.required" :hint="field.hint"></v-checkbox>
</template>
<script setup>

View File

@ -9,7 +9,7 @@
</label>
</div>
<field-label :name="field.name" :required="field.required" inline></field-label>
<field-label :name="field.name" :required="field.required" :hint="field.hint" inline></field-label>
</div>
</template>

View File

@ -9,6 +9,9 @@
placeholder=""
class="bg-white rounded-lg focus:outline-none text-gray-600 text-left py-1 px-2 @sm:py-2 text-sm @sm:text-base @sm:px-3 w-full"
/>
<div v-if="field.hint" class="absolute right-0 mr-2 flex items-center h-full">
<hint :value="field.hint"></hint>
</div>
<field-label :name="field.name" :required="field.required"></field-label>
</label>
</template>
@ -17,6 +20,7 @@
import {computed} from 'vue';
import FieldLabel from '../FieldLabel.vue';
import dayjs from 'dayjs';
import Hint from '../Hint.vue';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({

View File

@ -1,5 +1,5 @@
<template>
<v-dropdown v-model="inner" :label="field.name" :id="innerId" :name="innerId" :options="innerOptions" :required="field.required"></v-dropdown>
<v-dropdown v-model="inner" :label="field.name" :id="innerId" :name="innerId" :options="innerOptions" :required="field.required" :hint="field.hint"></v-dropdown>
</template>
<script setup>

View File

@ -9,7 +9,7 @@
</label>
</div>
<field-label :name="field.name" :required="field.required" inline></field-label>
<field-label :name="field.name" :required="field.required" :hint="field.hint" inline></field-label>
</div>
</template>

View File

@ -1,5 +1,5 @@
<template>
<v-text :required="field.required" :name="field.key" :label="field.name" :id="innerId" v-model="inner"></v-text>
<v-text :required="field.required" :name="field.key" :label="field.name" :id="innerId" :hint="field.hint" v-model="inner"></v-text>
</template>
<script setup>

View File

@ -7,6 +7,9 @@
:rows="field.rows"
class="bg-white rounded-lg focus:outline-none text-gray-600 text-left py-1 px-2 @sm:py-2 text-sm @sm:text-base @sm:px-3 w-full"
/>
<div v-if="field.hint" class="absolute right-0 mr-2 flex items-start mt-2 top-0 h-full">
<hint :value="field.hint"></hint>
</div>
<field-label :name="field.name" :required="field.required"></field-label>
</label>
</template>
@ -14,6 +17,7 @@
<script setup>
import {computed} from 'vue';
import FieldLabel from '../FieldLabel.vue';
import Hint from '../Hint.vue';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
@ -35,6 +39,11 @@ const props = defineProps({
id: {
required: false,
},
hint: {
required: false,
validator: (value) => value === null || typeof value === 'string',
default: () => null,
},
});
const innerId = computed(() => (props.id ? props.id : props.field.key));

View File

@ -11,6 +11,7 @@
<span v-text="label"></span>
<span v-show="required" class="text-red-800">*</span>
</span>
<hint :value="hint" class="ml-2" v-if="hint"></hint>
</label>
</div>
</div>
@ -18,6 +19,7 @@
<script setup>
import {computed} from 'vue';
import Hint from '../Hint.vue';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
@ -43,6 +45,11 @@ const props = defineProps({
type: Boolean,
default: () => false,
},
hint: {
required: false,
validator: (value) => value === null || typeof value === 'string',
default: () => null,
},
});
const inner = computed({

View File

@ -9,6 +9,9 @@
<option :value="null">-- kein --</option>
<option v-for="(option, index) in options" :key="index" :value="option.id" v-text="option.name"></option>
</select>
<div v-if="hint" class="absolute right-0 mr-2 flex items-center h-full">
<hint :value="hint"></hint>
</div>
<field-label :name="label" class="group-[.info]:bg-blue-200" :required="required"></field-label>
</label>
</template>
@ -16,6 +19,7 @@
<script setup>
import {computed} from 'vue';
import FieldLabel from '../FieldLabel.vue';
import Hint from '../Hint.vue';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
@ -44,6 +48,11 @@ const props = defineProps({
required: true,
type: Array,
},
hint: {
required: false,
validator: (value) => value === null || typeof value === 'string',
default: () => null,
},
});
const inner = computed({

View File

@ -9,6 +9,9 @@
@keypress="$emit('keypress', $event)"
class="bg-white group-[.info]:bg-blue-200 rounded-lg focus:outline-none text-gray-600 text-left w-full py-1 @sm:py-2 @sm:group-[.info]:py-1 px-2 @sm:px-3 @sm:group-[.info]:px-2 text-sm @sm:text-base @sm:group-[.info]:text-sm"
/>
<div v-if="hint" class="absolute right-0 mr-2 flex items-center h-full">
<hint :value="hint"></hint>
</div>
<field-label :name="label" class="group-[.info]:bg-blue-200" :required="required"></field-label>
</label>
</template>
@ -16,6 +19,7 @@
<script setup>
import {computed} from 'vue';
import FieldLabel from '../FieldLabel.vue';
import Hint from '../Hint.vue';
const emit = defineEmits(['update:modelValue', 'keypress']);
const props = defineProps({
@ -45,6 +49,11 @@ const props = defineProps({
type: String,
default: () => 'text',
},
hint: {
required: false,
validator: (value) => value === null || typeof value === 'string',
default: () => null,
},
});
const inner = computed({

View File

@ -0,0 +1,11 @@
import {vTooltip, options} from 'floating-vue';
options.themes.tooltip.delay.show = 0;
options.themes.tooltip.html = true;
options.themes.tooltip.handleResize = true;
export default function useFloating() {
return {
vTooltip,
};
}

183
src/css/floating.css Normal file
View File

@ -0,0 +1,183 @@
.resize-observer[data-v-b329ee4c] {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
border: none;
background-color: transparent;
pointer-events: none;
display: block;
overflow: hidden;
opacity: 0;
}
.resize-observer[data-v-b329ee4c] object {
display: block;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
pointer-events: none;
z-index: -1;
}
.v-popper__popper {
z-index: 10000;
top: 0;
left: 0;
outline: none;
}
.v-popper__popper.v-popper__popper--hidden {
visibility: hidden;
opacity: 0;
transition:
opacity 0.15s,
visibility 0.15s;
pointer-events: none;
}
.v-popper__popper.v-popper__popper--shown {
visibility: visible;
opacity: 1;
transition: opacity 0.15s;
}
.v-popper__popper.v-popper__popper--skip-transition,
.v-popper__popper.v-popper__popper--skip-transition > .v-popper__wrapper {
transition: none !important;
}
.v-popper__backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
}
.v-popper__inner {
position: relative;
box-sizing: border-box;
overflow-y: auto;
}
.v-popper__inner > div {
position: relative;
z-index: 1;
max-width: inherit;
max-height: inherit;
}
.v-popper__arrow-container {
position: absolute;
width: 10px;
height: 10px;
}
.v-popper__popper--arrow-overflow .v-popper__arrow-container,
.v-popper__popper--no-positioning .v-popper__arrow-container {
display: none;
}
.v-popper__arrow-inner,
.v-popper__arrow-outer {
border-style: solid;
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
}
.v-popper__arrow-inner {
visibility: hidden;
border-width: 7px;
}
.v-popper__arrow-outer {
border-width: 6px;
}
.v-popper__popper[data-popper-placement^='top'] .v-popper__arrow-inner,
.v-popper__popper[data-popper-placement^='bottom'] .v-popper__arrow-inner {
left: -2px;
}
.v-popper__popper[data-popper-placement^='top'] .v-popper__arrow-outer,
.v-popper__popper[data-popper-placement^='bottom'] .v-popper__arrow-outer {
left: -1px;
}
.v-popper__popper[data-popper-placement^='top'] .v-popper__arrow-inner,
.v-popper__popper[data-popper-placement^='top'] .v-popper__arrow-outer {
border-bottom-width: 0;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
}
.v-popper__popper[data-popper-placement^='top'] .v-popper__arrow-inner {
top: -2px;
}
.v-popper__popper[data-popper-placement^='bottom'] .v-popper__arrow-container {
top: 0;
}
.v-popper__popper[data-popper-placement^='bottom'] .v-popper__arrow-inner,
.v-popper__popper[data-popper-placement^='bottom'] .v-popper__arrow-outer {
border-top-width: 0;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
}
.v-popper__popper[data-popper-placement^='bottom'] .v-popper__arrow-inner {
top: -4px;
}
.v-popper__popper[data-popper-placement^='bottom'] .v-popper__arrow-outer {
top: -6px;
}
.v-popper__popper[data-popper-placement^='left'] .v-popper__arrow-inner,
.v-popper__popper[data-popper-placement^='right'] .v-popper__arrow-inner {
top: -2px;
}
.v-popper__popper[data-popper-placement^='left'] .v-popper__arrow-outer,
.v-popper__popper[data-popper-placement^='right'] .v-popper__arrow-outer {
top: -1px;
}
.v-popper__popper[data-popper-placement^='right'] .v-popper__arrow-inner,
.v-popper__popper[data-popper-placement^='right'] .v-popper__arrow-outer {
border-left-width: 0;
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
}
.v-popper__popper[data-popper-placement^='right'] .v-popper__arrow-inner {
left: -4px;
}
.v-popper__popper[data-popper-placement^='right'] .v-popper__arrow-outer {
left: -6px;
}
.v-popper__popper[data-popper-placement^='left'] .v-popper__arrow-container {
right: -10px;
}
.v-popper__popper[data-popper-placement^='left'] .v-popper__arrow-inner,
.v-popper__popper[data-popper-placement^='left'] .v-popper__arrow-outer {
border-right-width: 0;
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
}
.v-popper__popper[data-popper-placement^='left'] .v-popper__arrow-inner {
left: -2px;
}
.v-popper--theme-tooltip .v-popper__inner {
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 6px;
padding: 7px 12px 6px;
}
.v-popper--theme-tooltip .v-popper__arrow-outer {
border-color: #000c;
}
.v-popper--theme-dropdown .v-popper__inner {
background: #fff;
color: #000;
border-radius: 6px;
border: 1px solid #ddd;
box-shadow: 0 6px 30px #0000001a;
}
.v-popper--theme-dropdown .v-popper__arrow-inner {
visibility: visible;
border-color: #fff;
}
.v-popper--theme-dropdown .v-popper__arrow-outer {
border-color: #ddd;
}

View File

@ -2,12 +2,14 @@ import {defineCustomElement} from 'vue';
import classes from './style.css?inline';
import carousel from 'vue3-carousel/dist/carousel.css?inline';
import carouselStyle from './carousel.css?inline';
import 'vue3-toastify/dist/index.css';
import EventIndex from './EventIndex.ce.vue';
import EventForm from './EventForm.ce.vue';
import EventDescription from './EventDescription.ce.vue';
import axios from 'axios';
import 'vue3-toastify/dist/index.css';
import './css/floating.css';
window.axios = axios;
axios.defaults.baseURL = window.document.querySelector('[name="adrema_base_url"]').content;
@ -19,7 +21,11 @@ window.hasKeys = function (object, expected) {
var givenKeys = JSON.stringify(Object.keys(object).sort());
var expectedKeys = JSON.stringify(expected.sort());
if (givenKeys !== expectedKeys) {
console.log('Fields ' + givenKeys + ' dont match ' + expectedKeys);
var overloadKeys = Object.keys(object).filter((v) => !expected.includes(v));
var missingKeys = expected.filter((v) => !Object.keys(object).includes(v));
overloadKeys.length && console.log('keys not expected, but given: ' + overloadKeys);
missingKeys.length && console.log('keys expected, but missing: ' + missingKeys);
return false;
}
@ -27,7 +33,7 @@ window.hasKeys = function (object, expected) {
};
window.globalFieldRules = function () {
return ['value', 'special_type', 'nami_type', 'for_members', 'key', 'columns', 'name', 'type'];
return ['value', 'special_type', 'nami_type', 'for_members', 'key', 'columns', 'name', 'type', 'hint'];
};
EventForm.styles = [classes, carousel, carouselStyle];