mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
|
import {MomentTimezone} from 'app/client/lib/imports';
|
||
|
import {ACIndexImpl} from 'app/client/lib/ACIndex';
|
||
|
import {ACSelectItem, buildACSelect} from 'app/client/lib/ACSelect';
|
||
|
import {testId} from "app/client/ui2018/cssVars";
|
||
|
import {nativeCompare} from 'app/common/gutil';
|
||
|
import {IDisposableOwner, Observable} from 'grainjs';
|
||
|
|
||
|
/**
|
||
|
* Returns the ordered list of offsets for names at time timestamp. See timezoneOptions for details
|
||
|
* on the sorting order.
|
||
|
*/
|
||
|
// exported for testing
|
||
|
export function timezoneOptionsImpl(
|
||
|
timestamp: number, names: string[], moment: MomentTimezone
|
||
|
): ACSelectItem[] {
|
||
|
// What we want is moment(timestamp) but the dynamic import with our compiling settings produces
|
||
|
// "moment is not a function". The following is equivalent, and easier than fixing import setup.
|
||
|
const m = moment.unix(timestamp / 1000);
|
||
|
|
||
|
const options = names.map((value) => ({
|
||
|
cleanText: value.toLowerCase().trim(),
|
||
|
value,
|
||
|
label: `(GMT${m.tz(value).format('Z')}) ${value}`,
|
||
|
// A quick test reveal that it is a bit more efficient (~0.02ms) to get the offset using
|
||
|
// `moment.tz.Zone#parse` than creating a Moment instance for each zone and then getting the
|
||
|
// offset with `moment#utcOffset`.
|
||
|
offset: -moment.tz.zone(value)!.parse(timestamp)
|
||
|
}));
|
||
|
options.sort((a, b) => nativeCompare(a.offset, b.offset) || nativeCompare(a.value, b.value));
|
||
|
return options;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the array of IOptionFull<string> expected by `select` to create the list of timezones
|
||
|
* options. The returned list is sorted based on the current offset (GMT-11:00 before GMT-10:00),
|
||
|
* and then on alphabetical order of the name.
|
||
|
*/
|
||
|
function timezoneOptions(moment: MomentTimezone): ACSelectItem[] {
|
||
|
return timezoneOptionsImpl(Date.now(), moment.tz.names(), moment);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a textbox with an autocomplete dropdown to select a time zone.
|
||
|
* Usage: dom.create(buildTZAutocomplete, momentModule, valueObs, saveCallback)
|
||
|
*/
|
||
|
export function buildTZAutocomplete(
|
||
|
owner: IDisposableOwner,
|
||
|
moment: MomentTimezone,
|
||
|
valueObs: Observable<string>,
|
||
|
save: (value: string) => Promise<void>|void
|
||
|
) {
|
||
|
// Set a large maxResults, since it's sometimes nice to see all supported timezones (there are
|
||
|
// fewer than 1000 in practice).
|
||
|
const acIndex = new ACIndexImpl<ACSelectItem>(timezoneOptions(moment), 1000, true);
|
||
|
|
||
|
// Only save valid time zones. If there is no selected item, we'll auto-select and save only
|
||
|
// when there is a good match.
|
||
|
const saveTZ = (value: string, item: ACSelectItem|undefined) => {
|
||
|
if (!item) {
|
||
|
const results = acIndex.search(value);
|
||
|
if (results.selectIndex >= 0 && results.items.length > 0) {
|
||
|
item = results.items[results.selectIndex];
|
||
|
value = item.value;
|
||
|
}
|
||
|
}
|
||
|
if (!item) { throw new Error("Invalid time zone"); }
|
||
|
if (value !== valueObs.get()) {
|
||
|
return save(value);
|
||
|
}
|
||
|
};
|
||
|
return buildACSelect(owner,
|
||
|
{acIndex, valueObs, save: saveTZ},
|
||
|
testId("tz-autocomplete")
|
||
|
);
|
||
|
}
|