This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
// map from unicode eastern arabic number characters to arabic numbers
|
||||
const EASTERN_ARABIC_DIGIT_MAP = {
|
||||
'\\u0660': '0',
|
||||
'\\u0661': '1',
|
||||
'\\u0662': '2',
|
||||
'\\u0663': '3',
|
||||
'\\u0664': '4',
|
||||
'\\u0665': '5',
|
||||
'\\u0666': '6',
|
||||
'\\u0667': '7',
|
||||
'\\u0668': '8',
|
||||
'\\u0669': '9'
|
||||
};
|
||||
|
||||
/**
|
||||
* Escapes the given string so it can be safely used in a regexp.
|
||||
*
|
||||
* @param {string} string
|
||||
* @return {string}
|
||||
*/
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses eastern arabic number characters to arabic numbers (0-9)
|
||||
*
|
||||
* @param {string} digits
|
||||
* @return {string}
|
||||
*/
|
||||
function parseEasternArabicDigits(digits) {
|
||||
return digits.replace(/[\u0660-\u0669]/g, function (char) {
|
||||
const unicode = '\\u0' + char.charCodeAt(0).toString(16);
|
||||
return EASTERN_ARABIC_DIGIT_MAP[unicode];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @param {Date} testTime
|
||||
* @return {string | null}
|
||||
*/
|
||||
function getAmOrPmString(locale, testTime) {
|
||||
const testTimeString = testTime.toLocaleTimeString(locale);
|
||||
|
||||
// AM/PM string is anything from one letter in eastern arabic to standard two letters,
|
||||
// to having space in between, dots ...
|
||||
// cannot disqualify whitespace since some locales use a. m. / p. m.
|
||||
// TODO when more scripts support is added (than Arabic), need to exclude those numbers too
|
||||
const amOrPmRegExp = /[^\d\u0660-\u0669]/;
|
||||
|
||||
const matches =
|
||||
// In most locales, the time ends with AM/PM:
|
||||
testTimeString.match(new RegExp(`${amOrPmRegExp.source}+$`, 'g')) ||
|
||||
// In some locales, the time starts with AM/PM e.g in Chinese:
|
||||
testTimeString.match(new RegExp(`^${amOrPmRegExp.source}+`, 'g'));
|
||||
|
||||
return matches && matches[0].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @return {string | null}
|
||||
*/
|
||||
export function getSeparator(locale) {
|
||||
let timeString = TEST_PM_TIME.toLocaleTimeString(locale);
|
||||
|
||||
// Since the next regex picks first non-number-whitespace,
|
||||
// need to discard possible PM from beginning (eg. chinese locale)
|
||||
const pmString = getPmString(locale);
|
||||
if (pmString && timeString.startsWith(pmString)) {
|
||||
timeString = timeString.replace(pmString, '');
|
||||
}
|
||||
|
||||
const matches = timeString.match(/[^\u0660-\u0669\s\d]/);
|
||||
return matches && matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for either an AM or PM token in the given time string
|
||||
* depending on what is provided in `amOrPmString`.
|
||||
*
|
||||
* The search is case and space insensitive.
|
||||
*
|
||||
* @example
|
||||
* `searchAmOrPmToken('1 P M', 'PM')` => `'P M'`
|
||||
*
|
||||
* @example
|
||||
* `searchAmOrPmToken('1 a.m.', 'A. M.')` => `a.m.`
|
||||
*
|
||||
* @param {string} timeString
|
||||
* @param {string} amOrPmString
|
||||
* @return {string | null}
|
||||
*/
|
||||
export function searchAmOrPmToken(timeString, amOrPmString) {
|
||||
if (!amOrPmString) return null;
|
||||
|
||||
// Create a regexp string for searching for AM/PM without space-sensitivity.
|
||||
const tokenRegExpString = amOrPmString.split(/\s*/).map(escapeRegExp).join('\\s*');
|
||||
|
||||
// Create a regexp without case-sensitivity.
|
||||
const tokenRegExp = new RegExp(tokenRegExpString, 'i');
|
||||
|
||||
// Match the regexp against the time string.
|
||||
const tokenMatches = timeString.match(tokenRegExp);
|
||||
if (tokenMatches) {
|
||||
return tokenMatches[0];
|
||||
}
|
||||
}
|
||||
|
||||
export const TEST_PM_TIME = new Date('August 19, 1975 23:15:30');
|
||||
|
||||
export const TEST_AM_TIME = new Date('August 19, 1975 05:15:30');
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @return {string}
|
||||
*/
|
||||
export function getPmString(locale) {
|
||||
return getAmOrPmString(locale, TEST_PM_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @return {string}
|
||||
*/
|
||||
export function getAmString(locale) {
|
||||
return getAmOrPmString(locale, TEST_AM_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} digits
|
||||
* @return {number}
|
||||
*/
|
||||
export function parseDigitsIntoInteger(digits) {
|
||||
return parseInt(parseEasternArabicDigits(digits));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} milliseconds
|
||||
* @return {number}
|
||||
*/
|
||||
export function parseMillisecondsIntoInteger(milliseconds) {
|
||||
milliseconds = parseEasternArabicDigits(milliseconds);
|
||||
// digits are either .1 .01 or .001 so need to "shift"
|
||||
if (milliseconds.length === 1) {
|
||||
milliseconds += '00';
|
||||
} else if (milliseconds.length === 2) {
|
||||
milliseconds += '0';
|
||||
}
|
||||
return parseInt(milliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} timeString
|
||||
* @param {number} milliseconds
|
||||
* @param {string} amString
|
||||
* @param {string} pmString
|
||||
* @return {string}
|
||||
*/
|
||||
export function formatMilliseconds(timeString, milliseconds, amString, pmString) {
|
||||
// might need to inject milliseconds between seconds and AM/PM
|
||||
let cleanedTimeString = timeString;
|
||||
if (timeString.endsWith(amString)) {
|
||||
cleanedTimeString = timeString.replace(' ' + amString, '');
|
||||
} else if (timeString.endsWith(pmString)) {
|
||||
cleanedTimeString = timeString.replace(' ' + pmString, '');
|
||||
}
|
||||
if (milliseconds) {
|
||||
let millisecondsString = milliseconds < 10 ? '0' : '';
|
||||
millisecondsString += milliseconds < 100 ? '0' : '';
|
||||
millisecondsString += milliseconds;
|
||||
cleanedTimeString += '.' + millisecondsString;
|
||||
} else {
|
||||
cleanedTimeString += '.000';
|
||||
}
|
||||
if (timeString.endsWith(amString)) {
|
||||
cleanedTimeString = cleanedTimeString + ' ' + amString;
|
||||
} else if (timeString.endsWith(pmString)) {
|
||||
cleanedTimeString = cleanedTimeString + ' ' + pmString;
|
||||
}
|
||||
return cleanedTimeString;
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
TEST_PM_TIME,
|
||||
formatMilliseconds,
|
||||
parseMillisecondsIntoInteger,
|
||||
parseDigitsIntoInteger,
|
||||
getAmString,
|
||||
getPmString,
|
||||
getSeparator,
|
||||
searchAmOrPmToken
|
||||
} from './helpers.js';
|
||||
import { TimePicker } from '@vaadin/time-picker/src/vaadin-time-picker.js';
|
||||
|
||||
// Execute callback when predicate returns true.
|
||||
// Try again later if predicate returns false.
|
||||
function when(predicate, callback, timeout = 0) {
|
||||
if (predicate()) {
|
||||
callback();
|
||||
} else {
|
||||
setTimeout(() => when(predicate, callback, 200), timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function parseISO(text) {
|
||||
// The default i18n parser of the web component is ISO 8601 compliant.
|
||||
const timeObject = TimePicker.properties.i18n.value().parseTime(text);
|
||||
|
||||
// The web component returns an object with string values
|
||||
// while the connector expects number values.
|
||||
return {
|
||||
hours: parseInt(timeObject.hours || 0),
|
||||
minutes: parseInt(timeObject.minutes || 0),
|
||||
seconds: parseInt(timeObject.seconds || 0),
|
||||
milliseconds: parseInt(timeObject.milliseconds || 0)
|
||||
}
|
||||
};
|
||||
|
||||
window.Vaadin.Flow.timepickerConnector = {};
|
||||
window.Vaadin.Flow.timepickerConnector.initLazy = (timepicker) => {
|
||||
// Check whether the connector was already initialized for the timepicker
|
||||
if (timepicker.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
timepicker.$connector = {};
|
||||
|
||||
timepicker.$connector.setLocale = (locale) => {
|
||||
// capture previous value if any
|
||||
let previousValueObject;
|
||||
if (timepicker.value && timepicker.value !== '') {
|
||||
previousValueObject = parseISO(timepicker.value);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check whether the locale is supported by the browser or not
|
||||
TEST_PM_TIME.toLocaleTimeString(locale);
|
||||
} catch (e) {
|
||||
locale = 'en-US';
|
||||
// FIXME should do a callback for server to throw an exception ?
|
||||
throw new Error(
|
||||
'vaadin-time-picker: The locale ' +
|
||||
locale +
|
||||
' is not supported, falling back to default locale setting(en-US).'
|
||||
);
|
||||
}
|
||||
|
||||
// 1. 24 or 12 hour clock, if latter then what are the am/pm strings ?
|
||||
const pmString = getPmString(locale);
|
||||
const amString = getAmString(locale);
|
||||
|
||||
// 2. What is the separator ?
|
||||
const separator = getSeparator(locale);
|
||||
|
||||
const includeSeconds = function () {
|
||||
return timepicker.step && timepicker.step < 60;
|
||||
};
|
||||
|
||||
const includeMilliSeconds = function () {
|
||||
return timepicker.step && timepicker.step < 1;
|
||||
};
|
||||
|
||||
let cachedTimeString;
|
||||
let cachedTimeObject;
|
||||
|
||||
timepicker.i18n = {
|
||||
formatTime(timeObject) {
|
||||
if (!timeObject) return;
|
||||
|
||||
const timeToBeFormatted = new Date();
|
||||
timeToBeFormatted.setHours(timeObject.hours);
|
||||
timeToBeFormatted.setMinutes(timeObject.minutes);
|
||||
timeToBeFormatted.setSeconds(timeObject.seconds !== undefined ? timeObject.seconds : 0);
|
||||
|
||||
// the web component expects the correct granularity used for the time string,
|
||||
// thus need to format the time object in correct granularity by passing the format options
|
||||
let localeTimeString = timeToBeFormatted.toLocaleTimeString(locale, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: includeSeconds() ? 'numeric' : undefined
|
||||
});
|
||||
|
||||
// milliseconds not part of the time format API
|
||||
if (includeMilliSeconds()) {
|
||||
localeTimeString = formatMilliseconds(localeTimeString, timeObject.milliseconds, amString, pmString);
|
||||
}
|
||||
|
||||
return localeTimeString;
|
||||
},
|
||||
|
||||
parseTime(timeString) {
|
||||
if (timeString && timeString === cachedTimeString && cachedTimeObject) {
|
||||
return cachedTimeObject;
|
||||
}
|
||||
|
||||
if (!timeString) {
|
||||
// when nothing is returned, the component shows the invalid state for the input
|
||||
return;
|
||||
}
|
||||
|
||||
const amToken = searchAmOrPmToken(timeString, amString);
|
||||
const pmToken = searchAmOrPmToken(timeString, pmString);
|
||||
|
||||
const numbersOnlyTimeString = timeString
|
||||
.replace(amToken || '', '')
|
||||
.replace(pmToken || '', '')
|
||||
.trim();
|
||||
|
||||
// A regexp that allows to find the numbers with optional separator and continuing searching after it.
|
||||
const numbersRegExp = new RegExp('([\\d\\u0660-\\u0669]){1,2}(?:' + separator + ')?', 'g');
|
||||
|
||||
let hours = numbersRegExp.exec(numbersOnlyTimeString);
|
||||
if (hours) {
|
||||
hours = parseDigitsIntoInteger(hours[0].replace(separator, ''));
|
||||
// handle 12 am -> 0
|
||||
// do not do anything if am & pm are not used or if those are the same,
|
||||
// as with locale bg-BG there is always ч. at the end of the time
|
||||
if (amToken !== pmToken) {
|
||||
if (hours === 12 && amToken) {
|
||||
hours = 0;
|
||||
}
|
||||
if (hours !== 12 && pmToken) {
|
||||
hours += 12;
|
||||
}
|
||||
}
|
||||
const minutes = numbersRegExp.exec(numbersOnlyTimeString);
|
||||
const seconds = minutes && numbersRegExp.exec(numbersOnlyTimeString);
|
||||
// detecting milliseconds from input, expects am/pm removed from end, eg. .0 or .00 or .000
|
||||
const millisecondRegExp = /[[\.][\d\u0660-\u0669]{1,3}$/;
|
||||
// reset to end or things can explode
|
||||
let milliseconds = seconds && includeMilliSeconds() && millisecondRegExp.exec(numbersOnlyTimeString);
|
||||
// handle case where last numbers are seconds and . is the separator (invalid regexp match)
|
||||
if (milliseconds && milliseconds['index'] <= seconds['index']) {
|
||||
milliseconds = undefined;
|
||||
}
|
||||
// hours is a number at this point, others are either arrays or null
|
||||
// the string in [0] from the arrays includes the separator too
|
||||
cachedTimeObject = hours !== undefined && {
|
||||
hours: hours,
|
||||
minutes: minutes ? parseDigitsIntoInteger(minutes[0].replace(separator, '')) : 0,
|
||||
seconds: seconds ? parseDigitsIntoInteger(seconds[0].replace(separator, '')) : 0,
|
||||
milliseconds:
|
||||
minutes && seconds && milliseconds
|
||||
? parseMillisecondsIntoInteger(milliseconds[0].replace('.', ''))
|
||||
: 0
|
||||
};
|
||||
cachedTimeString = timeString;
|
||||
return cachedTimeObject;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (previousValueObject) {
|
||||
when(
|
||||
() => timepicker.$,
|
||||
() => {
|
||||
const newValue = timepicker.i18n.formatTime(previousValueObject);
|
||||
// FIXME works but uses private API, needs fixes in web component
|
||||
if (timepicker.inputElement.value !== newValue) {
|
||||
timepicker.inputElement.value = newValue;
|
||||
timepicker.$.comboBox.value = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user