Skip to content

Commit

Permalink
fix: update input-date to UTC only.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jenesius committed Aug 16, 2023
1 parent 2b334dd commit c8edbc5
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 118 deletions.
16 changes: 13 additions & 3 deletions project/pages/test/App.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
<template>
<div class="container-examples">
<div :key = "values">Values: {{values}}</div>
<form-field name="created" type = "date" label = "Created" />
<form-field name="created" type = "date" format = "test" label = "Test Format" />

<div style = "background-color: #bac7f8; padding: 10px">
<p>Local date: {{localDate}}</p>
<input-field name = "created" type = "local-date" />
</div>

<form-field name="created" type = "date" label = "Created" placeholder = "Введите дату создания"/>
<form-field name="created" type = "date" mask = "mm-dd-yyyy" label = "Ceated English" />
<form-field name="created" type = "date" mask = "YYYY-MM-DD" label = "Ceated English" placeholder = "Введите дату создания"/>

<form-field name="address.city" label = "Address city" />

Expand All @@ -20,7 +27,7 @@
<button @click = "setDefaultValues">set default values</button>


<div :key = "values">Values: {{values}}</div>

<div :key = "changes">Changes: {{changes}}</div>
<div :key = "pureValue">Pure values: {{pureValue}}</div>
<div :key = "pureAvailabilities">Pure av: {{pureAvailabilities}}</div>
Expand All @@ -34,13 +41,16 @@ import {ref} from "vue";
import WidgetComposite from "./widget-composite.vue";
import WidgetAddress from "./widget-address.vue"
import copyObject from "./../../../src/utils/copy-object";
import InputField from "../../../src/widgets/form-field.vue";
const form = new Form({
name: "main"
});
window.form = form
const localDate = ref("");
const values = ref(0);
setInterval(() => {
values.value = copyObject(form.values);
Expand Down
30 changes: 30 additions & 0 deletions project/pages/test/input-test-date-local.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div>
<input-date
v-bind = "$attrs"
:model-value = "modelValue"
@update:modelValue = "onUpdate"
/>
</div>
</template>

<script setup lang = "ts">
import InputDate from "../../../src/widgets/input-date/input-date.vue";
const props = defineProps<{
modelValue: any
}>()
const emit = defineEmits<{
(event: 'update:modelValue', v: string): void
}>()
// UTC FORMAT -> LOCAL FORMAT
function onUpdate(v: string) {
const date = new Date(v);
emit('update:modelValue', `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`);
}
</script>

<style scoped>
</style>
6 changes: 4 additions & 2 deletions project/pages/test/main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { createApp } from 'vue'
import App from './App.vue'
import {config} from "../../../plugin";
import {config} from "../../../src/index";

import country from "./widget-input-country.vue";
import accountType from "@/pages/test/widget-input-account-type.vue";
import InputCoord from "@/pages/test/input-coord.vue";
import InputFile from "@/pages/test/input-file.vue";
import InputTestDateLocal from "./input-test-date-local.vue";

config({
inputTypes: {
country,
"account-type": accountType,
coord: InputCoord,
file: InputFile
file: InputFile,
'local-date': InputTestDateLocal
},
debug: true
})
Expand Down
2 changes: 1 addition & 1 deletion src/config/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const STORE: IStore = {
defaultType: 'text',
cleanValue: null,
date: {
dateMask: "dd/mm/yyyy",
dateMask: "YYYY/MM/DD",
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
daysWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
calendar: {
Expand Down
59 changes: 36 additions & 23 deletions src/controllers/date-controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
type AvailableMask = 'YYYY' | 'MM' | 'DD' | 'HH' | 'mm'
/**
* @description На данный момент вся работа производится в UTC. Это было сделано для более лаконичного решения. Если
* на проекте есть необходимость записывать дату в другом формате -
* */
export default class DateController {

static get ValidatedPrimaryMask() {
return ['yyyy', 'mm', 'dd', 'HH', 'MM']
/**
* SPECIFICATION: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
* */
static get ValidatedPrimaryMask(): AvailableMask[] {
return ['YYYY', 'MM', 'DD', 'HH', 'mm']
}
/**
* @description Функция предназначена для валидации маски. Вернёт true в случае успеха, или выкинет ошибку, если
Expand All @@ -15,7 +23,16 @@ export default class DateController {
static SplitStringByMask(input: string, primaryMask: string) {

let mask = primaryMask; // Копия маски, которая будем постепенно обрезать
const arr = []
const arr = [];
// Чтобы не зашивать отдельно для валидации, получаем символы, которые входят в состав primary mask construction.
// Мы помещаем все символы в один массив, затем помещаем всё в Set, чтобы убрать дубликаты, затем соединяем их.
const availableLetters = [...new Set(
DateController.ValidatedPrimaryMask.reduce((acc: string[], x) => {
acc.push(...x.split(''))
return acc;
}, []))
].join('');

while(mask.length) {

// Текущая маска начинается со значимого элемента
Expand Down Expand Up @@ -47,7 +64,7 @@ export default class DateController {
const char = mask.charAt(0);
const charOriginalPosition = mask.length - primaryMask.length;

if (/[ymdHM]/.test(char)) throw DateError.WrongConstruction(primaryMask, char, charOriginalPosition);
if (new RegExp(`[${availableLetters}]`).test(char)) throw DateError.WrongConstruction(primaryMask, char, charOriginalPosition);
if (/[a-zA-Z]/.test(char)) throw DateError.UsingUnknownSymbol(primaryMask, char, charOriginalPosition);

// Незначимый символ: * / - + или любой другой отличный от буквы и цифры.
Expand All @@ -74,13 +91,13 @@ export default class DateController {
if (typeof input !== 'string') throw new Error('Input is not string');
const parsedResult = DateController.SplitStringByMask(input, mask);

function get(key: string) {
function get(key: AvailableMask) {
return Number.parseInt(parsedResult.find(item => item.part === key)?.input || '0');
}

// Если какой-то part не закончен и не является последним.
if (parsedResult.find(a => a.construction && !( a.ended || (a.last && a.input.length)))) return null;
return new Date(get('yyyy'), get('mm') - 1, get('dd'), get('HH'), get('MM'))
return new Date(get('YYYY'), get('MM') - 1, get('DD'), get('HH'), get('mm'))
}

/**
Expand All @@ -98,16 +115,20 @@ export default class DateController {
.map(key => DateController.GetValueByMaskPart(date, key))
.join('')
}
static GetValueByMaskPart(date: Date, part: string) {
/**
* @description Используется только для красивой записи. По этому не используется getUTCFullYear, getUTCMonth и т.д.
* */
static GetValueByMaskPart(date: Date, part: AvailableMask | string) {
function pad(v: number, length = 2) {
return String(v).padStart(length, '0');
}

switch (part) {
case 'yyyy': return date.getFullYear()
case 'mm': return pad(date.getMonth() + 1)
case 'dd': return pad(date.getDate())
case 'YYYY': return date.getFullYear()
case 'MM': return pad(date.getMonth() + 1)
case 'DD': return pad(date.getDate())
case 'HH': return pad(date.getHours())
case 'MM': return pad(date.getMinutes())
case 'mm': return pad(date.getMinutes())
default: return part
}
}
Expand All @@ -130,20 +151,11 @@ export default class DateController {
.join('')
}

static isISODate(str: string) {
static isUTCDate(str: string) {
if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str)) return false;
const d = new Date(str);
return d instanceof Date && !isNaN(d.getTime()) && d.toISOString()===str; // valid date
}
static isUTCDate(str: string) {
try {
const dateParsed = new Date(Date.parse(str))
return dateParsed.toUTCString() === str
} catch (e) {
return false;
}
}

}

class DateError extends Error {
Expand All @@ -154,15 +166,16 @@ class DateError extends Error {
return new DateError(
`
In mask You can use only constructions like: ${DateController.ValidatedPrimaryMask.join()}.
The next symbol(construction) "${symbol}" is unknown.
The next symbol(construction) "${symbol}" is unknown.
Mask: ${mask}, Position: ${position}
`)
}
static WrongConstruction(mask: string, symbol: string, position?: number) {
return new DateError(
`
Not full construction was founded: ${symbol}.
You can use only ${DateController.ValidatedPrimaryMask.join()}.
Mask: ${mask}
Mask: ${mask}, Position: ${position}
`
)
}
Expand Down
51 changes: 30 additions & 21 deletions src/widgets/input-date/input-date.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ import clickOutside from "../../utils/click-outside";
import IconCalendar from "../icons/icon-calendar.vue";
import FieldWrap from "../field-wrap.vue";
import {ValidationError} from "../../types";
import STORE from "../../config/store";
import STORE, {IStore} from "../../config/store";

const props = withDefaults(defineProps<{
modelValue: any,
label?: string,
errors: ValidationError[],
mask?: string,
placeholder?: string
placeholder?: string,
}>(), {
mask: () => STORE.date.dateMask
mask: () => STORE.date.dateMask,
})
const emit = defineEmits<{
(e: 'update:modelValue', value: any): void
Expand All @@ -60,15 +60,14 @@ const insideValue = ref("")
const refCalendar = ref();
const calendarStatus = ref(false);


function handleCalendarInput(s: string) {
const date = new Date(s);

emit('update:modelValue', date?.toUTCString());
/**
* @description Принимает дату в формате DateString (new Date().toDateString()).
* */
function handleCalendarInput(calendarStringData: string) {
emitInput(new Date(calendarStringData))
}

let offCalendar: any;

function changeCalendarStatus(status: boolean) {
calendarStatus.value = status;
nextTick(() => {
Expand All @@ -81,20 +80,33 @@ function changeCalendarStatus(status: boolean) {
function handleInput(v: string) {
insideValue.value = v;
if (!DateController.CheckFullerMask(v, props.mask)) return;
setTimeout(() => emitInput(prettyValue.value), 40)
nextTick(() => handleUserHandInput(prettyValue.value))
}

function handleChange(v: string) {
emitInput(v);
handleUserHandInput(v);
}

function emitInput(v: string) {
const r = DateController.ConvertToDate(v, props.mask);
emit('update:modelValue', r?.toUTCString());
/**
* @description Используется только для ручного ввода даты, т.к. далее использует конвертацию в дату по маске, а не
* объект Date
* */
function handleUserHandInput(input: string) {
const date = DateController.ConvertToDate(input, props.mask);
emitInput(date);
}
function emitInput(date: Date | null) {
emit('update:modelValue', date ? date.toISOString() : date);
}



function pretty(s: unknown): string {
if (typeof s !== 'string') return ''

/**
* Является ли дата конечной. В таком случае полученная строка уже не является исходником маски, а может иметь любой
* вид. Для этого используется функция GetPrettyDate.
* */
if (DateController.isUTCDate(s)) return DateController.GetPrettyDate(new Date(s), props.mask)

return DateController.SplitStringByMask(s, props.mask)
Expand All @@ -105,16 +117,13 @@ function pretty(s: unknown): string {
const prettyValue = computed(() => pretty(insideValue.value))
const prettyMask = computed(() => {
if (!insideValue.value) return props.mask;
if (DateController.isUTCDate(insideValue.value)) return '';

return prettyValue.value + DateController.GetRestMask(prettyValue.value, props.mask);
})

// Контролируем валидацию маски.
watch(() => props.mask, () => {
DateController.ValidateMask(prettyMask.value)
}, {immediate: true})

// Контролируем валидацию маски.
watch(() => props.mask, () => DateController.ValidateMask(prettyMask.value), {immediate: true})
// Контролируем внутренне значение поля.
watch(() => props.modelValue, v => insideValue.value = v, {immediate: true})

</script>
Expand Down
Loading

0 comments on commit c8edbc5

Please sign in to comment.