Skip to content
This repository has been archived by the owner on Nov 29, 2023. It is now read-only.

Commit

Permalink
Merge pull request #916 from eyelidlessness/fix/timepicker-finnish
Browse files Browse the repository at this point in the history
Fix timepicker in Finnish locales
  • Loading branch information
lognaturel authored Sep 28, 2022
2 parents 4f1426a + ba0643c commit e03818e
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 106 deletions.
3 changes: 3 additions & 0 deletions src/js/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
closestAncestorUntil,
getSiblingElement,
} from './dom-utils';
import { initTimeLocalization } from './format';
import inputHelper from './input';
import repeatModule from './repeat';
import tocModule from './toc';
Expand Down Expand Up @@ -263,6 +264,8 @@ Form.prototype.init = function () {
let loadErrors = [];
const that = this;

initTimeLocalization(this.view.html);

this.toc = this.addModule(tocModule);
this.pages = this.addModule(pageModule);
this.langs = this.addModule(languageModule);
Expand Down
99 changes: 59 additions & 40 deletions src/js/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,88 +2,107 @@
* @module format
*/

const NUMBER = '0-9\u0660-\u0669';
const TIME_PART = `[:${NUMBER}]+`;
const MERIDIAN_PART = `[^: ${NUMBER}]+`;
const HAS_MERIDIAN = new RegExp(
`^(${TIME_PART} ?(${MERIDIAN_PART}))|((${MERIDIAN_PART}) ?${TIME_PART})$`
);
import events from './event';

/**
* Transforms time to a cleaned-up localized time.
*
* @param {Date} dt - date object
* @return {string} cleaned-up localized time
* @typedef LocaleState
* @property {string[]} locales
* @property {string} dateString
* @property {string} timeString
* @property {Intl.DateTimeFormat} timeFormatter
*/
function _getCleanLocalTime(dt) {
dt = typeof dt === 'undefined' ? new Date() : dt;

return _cleanSpecialChars(dt.toLocaleTimeString(format.locale));
}
/** @type {LocaleState | null} */
let localeState = null;

const setLocalizedTimeFormatter = () => {
const locales = Intl.getCanonicalLocales(navigator.languages);
const date = new Date(2000, 1, 1, 1, 23, 45);
const dateString = date.toLocaleString();
const timeString = date.toLocaleTimeString();
const timeFormatter = Intl.DateTimeFormat(locales, {
timeStyle: 'short',
});

localeState = {
locales,
dateString,
timeString,
timeFormatter,
};
};

/**
* Remove unneeded and problematic special characters in (date)time string.
*
* @param {string} timeStr - (date)time string to clean up
* @return {string} transformed (date)time string with removed unneeded special characters that cause issues
* @param {HTMLFormElement} [rootElement]
*/
function _cleanSpecialChars(timeStr) {
return timeStr.replace(/[\u200E\u200F]/g, '');
}
const initTimeLocalization = (rootElement) => {
const languageChangeListener = () => {
setLocalizedTimeFormatter();

rootElement?.dispatchEvent(events.ChangeLanguage());
};

addEventListener('languagechange', languageChangeListener);

setLocalizedTimeFormatter();

return removeEventListener.bind(
window,
'languagechange',
languageChangeListener
);
};

/**
* @namespace time
*/
const time = {
// For now we just look at a subset of numbers in Arabic and Latin. There are actually over 20 number scripts and :digit: doesn't work in browsers
/**
* @type {string}
* @type {boolean}
*/
get hour12() {
return this.hasMeridian(_getCleanLocalTime());
const { hour12 } = localeState.timeFormatter.resolvedOptions();

return Boolean(hour12);
},

/**
* @type {string}
*/
get pmNotation() {
return this.meridianNotation(new Date(2000, 1, 1, 23, 0, 0));
},

/**
* @type {string}
*/
get amNotation() {
return this.meridianNotation(new Date(2000, 1, 1, 1, 0, 0));
},

/**
* @type {Function}
* @param {Date} dt - datetime string
* @param {Date} date - datetime string
*/
meridianNotation(dt) {
let matches = _getCleanLocalTime(dt).match(HAS_MERIDIAN);
if (matches && matches.length) {
matches = matches.filter((item) => !!item);
meridianNotation(date) {
const formatted = localeState.timeFormatter.formatToParts(date);
const meridianPart = formatted.find(({ type }) => type === 'dayPeriod');

return matches[matches.length - 1].trim();
if (meridianPart != null) {
return meridianPart.value;
}

return null;
},

/**
* Whether time string has meridian parts
*
* @type {Function}
* @param {string} time - Time string
*/
hasMeridian(time) {
return HAS_MERIDIAN.test(_cleanSpecialChars(time));
return time.includes(this.amNotation) || time.includes(this.pmNotation);
},
};

/**
* @namespace format
*/
const format = {
locale: navigator.language,
};

export { format, time };
export { initTimeLocalization, time };
3 changes: 3 additions & 0 deletions src/widget/datetime/datetimepicker-extended.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class DatetimepickerExtended extends Widget {
)
: '';
}

if (val !== this.value) {
const vals = val.split('T');
const dateVal = vals[0];
Expand All @@ -163,6 +164,8 @@ class DatetimepickerExtended extends Widget {
this.$fakeDateI.datepicker('setDate', dateVal);
this.$fakeTimeI.timepicker('setTime', timeVal);
}

this.$fakeTimeI.timepicker('updateLocalization');
}

get originalInputValue() {
Expand Down
10 changes: 8 additions & 2 deletions src/widget/time/timepicker-extended.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,14 @@ class TimepickerExtended extends Widget {
* Updates widget
*/
update() {
if (this.element.value !== this.value && this.fakeTimeI) {
$(this.fakeTimeI).timepicker('setTime', this.element.value);
if (this.fakeTimeI) {
const $fakeTimeInput = $(this.fakeTimeI);

if (this.element.value !== this.value) {
$fakeTimeInput.timepicker('setTime', this.element.value);
}

$fakeTimeInput.timepicker('updateLocalization');
}
}

Expand Down
56 changes: 39 additions & 17 deletions src/widget/time/timepicker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import $ from 'jquery';
import event from '../../js/event';
import { time } from '../../js/format';

/*!
* Timepicker
*
Expand All @@ -16,6 +18,7 @@ import event from '../../js/event';
(($, window, document) => {
// TIMEPICKER PUBLIC CLASS DEFINITION
const Timepicker = function (element, options) {
this.languages = navigator.languages;
this.widget = '';
this.$element = $(element);
this.defaultTime = options.defaultTime;
Expand All @@ -27,8 +30,6 @@ import event from '../../js/event';
this.secondStep = options.secondStep;
this.snapToStep = options.snapToStep;
this.showInputs = options.showInputs;
this.showMeridian = options.showMeridian;
this.meridianNotation = options.meridianNotation;
this.showSeconds = options.showSeconds;
this.template = options.template;
this.appendWidgetTo = options.appendWidgetTo;
Expand Down Expand Up @@ -56,6 +57,17 @@ import event from '../../js/event';
};

Timepicker.prototype = {
get showMeridian() {
return time.hour12;
},

get meridianNotation() {
return {
am: time.amNotation,
pm: time.pmNotation,
};
},

constructor: Timepicker,
_init() {
const self = this;
Expand Down Expand Up @@ -109,6 +121,9 @@ import event from '../../js/event';
'click',
$.proxy(this.widgetClick, this)
);

this.meridianColumns =
this.$widget[0].querySelectorAll('.meridian-column');
} else {
this.$widget = false;
}
Expand Down Expand Up @@ -356,31 +371,23 @@ import event from '../../js/event';
this.showSeconds
? `<td class="separator">&nbsp;</td><td><a href="#" data-action="incrementSecond"><span class="${this.icons.up}"></span></a></td>`
: ''
}${
this.showMeridian
? `<td class="separator">&nbsp;</td><td class="meridian-column"><a href="#" data-action="toggleMeridian"><span class="${this.icons.up}"></span></a></td>`
: ''
}</tr><tr><td>${hourTemplate}</td> <td class="separator">:</td><td>${minuteTemplate}</td> ${
}<td class="separator meridian-column">&nbsp;</td><td class="meridian-column"><a href="#" data-action="toggleMeridian"><span class="${
this.icons.up
}"></span></a></td></tr><tr><td>${hourTemplate}</td> <td class="separator">:</td><td>${minuteTemplate}</td> ${
this.showSeconds
? `<td class="separator">:</td><td>${secondTemplate}</td>`
: ''
}${
this.showMeridian
? `<td class="separator">&nbsp;</td><td>${meridianTemplate}</td>`
: ''
}</tr><tr><td><a href="#" data-action="decrementHour"><span class="${
}<td class="separator meridian-column">&nbsp;</td><td class="meridian-column">${meridianTemplate}</td></tr><tr><td><a href="#" data-action="decrementHour"><span class="${
this.icons.down
}"></span></a></td><td class="separator"></td><td><a href="#" data-action="decrementMinute"><span class="${
this.icons.down
}"></span></a></td>${
this.showSeconds
? `<td class="separator">&nbsp;</td><td><a href="#" data-action="decrementSecond"><span class="${this.icons.down}"></span></a></td>`
: ''
}${
this.showMeridian
? `<td class="separator">&nbsp;</td><td><a href="#" data-action="toggleMeridian"><span class="${this.icons.down}"></span></a></td>`
: ''
}</tr></table>`;
}<td class="separator meridian-column">&nbsp;</td><td class="meridian-column"><a href="#" data-action="toggleMeridian"><span class="${
this.icons.down
}"></span></a></td></tr></table>`;

switch (this.template) {
case 'dropdown':
Expand Down Expand Up @@ -1171,6 +1178,21 @@ import event from '../../js/event';
.text(this.meridian);
}
}

const { showMeridian } = this;
const meridianDisplay = showMeridian ? 'table-cell' : 'none';

this.meridianColumns.forEach((column) => {
column.style.display = meridianDisplay;
});
},

updateLocalization() {
if (this.languages !== navigator.languages) {
this.languages = navigator.languages;
this.updateFromElementVal();
this.updateWidget();
}
},

updateFromWidgetInputs() {
Expand Down
Loading

0 comments on commit e03818e

Please sign in to comment.