Skip to content

Commit

Permalink
Merge pull request #185 from OpenHistoricalMap/1ec5-date-field-644
Browse files Browse the repository at this point in the history
Add date field type
  • Loading branch information
erictheise authored Dec 20, 2023
2 parents 0d5dfb0 + 4e338f3 commit ac14034
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 1 deletion.
18 changes: 17 additions & 1 deletion css/80_app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1952,8 +1952,10 @@ a.hide-toggle {
}


/* Field - roadheight and roadspeed
/* Field - date, roadheight, and roadspeed
------------------------------------------------------- */
.form-field-input-date input.date-year,
.form-field-input-date input.date-month,
.form-field-input-roadheight input.roadheight-number,
.form-field-input-roadheight input.roadheight-secondary-number,
.form-field-input-roadspeed input.roadspeed-number {
Expand All @@ -1964,23 +1966,29 @@ a.hide-toggle {
flex: 0 1 auto;
width: 60px;
}
.form-field-input-date input.date-era,
.form-field-input-date input.date-day,
.form-field-input-roadspeed input.roadspeed-unit {
flex: 0 1 auto;
width: 80px;
}
.ideditor[dir='ltr'] .form-field-input-date > input:first-of-type,
.ideditor[dir='ltr'] .form-field-input-roadheight > input:first-of-type,
.ideditor[dir='ltr'] .form-field-input-roadspeed > input:first-of-type {
border-radius: 0 0 0 4px;
}
.ideditor[dir='rtl'] .form-field-input-date > input:first-of-type,
.ideditor[dir='rtl'] .form-field-input-roadheight > input:first-of-type,
.ideditor[dir='rtl'] .form-field-input-roadspeed > input:first-of-type {
border-radius: 0 0 4px 0;
}
.ideditor[dir='ltr'] .form-field-input-date > input:last-of-type,
.ideditor[dir='ltr'] .form-field-input-roadheight > input:last-of-type,
.ideditor[dir='ltr'] .form-field-input-roadspeed > input:last-of-type {
border-left: 0;
border-radius: 0 0 4px 0;
}
.ideditor[dir='rtl'] .form-field-input-date > input:last-of-type,
.ideditor[dir='rtl'] .form-field-input-roadheight > input:last-of-type,
.ideditor[dir='rtl'] .form-field-input-roadspeed > input:last-of-type {
border-right: 0;
Expand Down Expand Up @@ -2048,6 +2056,14 @@ a.hide-toggle {
}


/* Field - Date
------------------------------------------------------- */
.ideditor .form-field-input-date > input.date-main + input.date-main,
.ideditor .form-field-input-date > .combobox-caret + input.date-main {
border-left: 0;
}


/* Field - Address
------------------------------------------------------- */
.form-field-input-address {
Expand Down
7 changes: 7 additions & 0 deletions data/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,13 @@ en:
foot: ft
# abbreviation of inches
inch: in
date:
year: Year
# placeholder for era (BCE or CE)
era: Era
month: Month
# placeholder for day of the month
day: Day
max_length_reached: "This string is longer than the maximum length of {maxChars} characters. Anything exceeding that length will be truncated."
background:
title: Background
Expand Down
1 change: 1 addition & 0 deletions modules/presets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function addHistoricalPresets(presets) {
* Adds fields specific to OpenHistoricalMap.
*/
function addHistoricalFields(fields) {
fields.start_date.type = 'date';
fields.end_date = {
...fields.start_date,
key: 'end_date'
Expand Down
268 changes: 268 additions & 0 deletions modules/ui/fields/date.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import * as countryCoder from '@ideditor/country-coder';

import { uiCombobox } from '../combobox';
import { t, localizer } from '../../core/localizer';
import { utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent } from '../../util';


export function uiFieldDate(field, context) {
let dispatch = d3_dispatch('change');
let yearInput = d3_select(null);
let eraInput = d3_select(null);
let monthInput = d3_select(null);
let dayInput = d3_select(null);
let _entityIDs = [];
let _tags;

let dateTimeFormat = new Intl.DateTimeFormat(localizer.languageCode(), {
year: 'numeric',
era: 'short',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
});
let formatParts = dateTimeFormat.formatToParts(new Date()).map(p => p.type);

/**
* Returns the localized name of the era in the given format.
*
* @param year A representative year within the era.
*/
function getEraName(year, format) {
let longFormat = new Intl.DateTimeFormat(localizer.languageCode(), {
year: 'numeric',
era: format,
timeZone: 'UTC',
});
let eraDate = new Date(Date.UTC(0, 0, 1));
eraDate.setUTCFullYear(year);
let parts = longFormat.formatToParts(eraDate);
let eraPart = parts.find(p => p.type === 'era');
return eraPart && eraPart.value;
}

let bceName = getEraName(0, 'short');
let ceName = getEraName(1, 'short');
let eraNames = [
{
id: 'bce',
value: bceName,
title: bceName,
display: selection => selection.append('span')
.attr('class', 'localized-text')
.text(bceName ? bceName : 'BCE'),
terms: [getEraName(0, 'long'), getEraName(0, 'narrow')],
},
{
id: 'ce',
value: ceName,
title: ceName,
display: selection => selection.append('span')
.attr('class', 'localized-text')
.text(ceName ? ceName : 'CE'),
terms: [getEraName(1, 'long'), getEraName(1, 'narrow')],
},
];
let eraCombo = uiCombobox(context, 'date-era')
.data(eraNames);

/// Returns the localized name of a month in the given format.
function getMonthName(month, format) {
let longFormat = new Intl.DateTimeFormat(localizer.languageCode(), {
month: format,
timeZone: 'UTC',
});
let parts = longFormat.formatToParts(new Date(Date.UTC(0, month, 1)));
let monthPart = parts.find(p => p.type === 'month');
return monthPart && monthPart.value;
}

let monthNames = Array.from({length: 12}, (_, i) => getMonthName(i, 'long'))
.filter(m => m);
let alternativeMonthNames = Array.from({length: 12}, (_, i) => {
return ['numeric', '2-digit', 'long', 'short', 'narrow']
.map(format => getMonthName(i, format))
.filter(m => m);
});

let monthCombo = uiCombobox(context, 'date-month')
.data(monthNames.map((monthName, i) => {
return {
id: i + 1,
value: monthName,
title: monthName,
display: selection => selection.append('span')
.attr('class', 'localized-text')
.text(monthName),
terms: alternativeMonthNames[i],
};
}));

function date(selection) {

var wrap = selection.selectAll('.form-field-input-wrap')
.data([0]);

wrap = wrap.enter()
.append('div')
.attr('class', 'form-field-input-wrap form-field-input-' + field.type)
.merge(wrap);

yearInput = wrap.selectAll('input.date-year')
.data([0]);
eraInput = wrap.selectAll('input.date-era')
.data([0]);
monthInput = wrap.selectAll('input.date-month')
.data([0]);
dayInput = wrap.selectAll('input.date-day')
.data([0]);

formatParts.forEach(part => {
switch (part) {
case 'year':
yearInput = yearInput.enter()
.append('input')
.attr('type', 'number')
.attr('class', 'date-main date-year')
.attr('id', field.domId)
.call(utilNoAuto)
.merge(yearInput);
break;
case 'era':
eraInput = eraInput.enter()
.append('input')
.attr('type', 'text')
.attr('class', 'date-main date-era')
.call(eraCombo)
.merge(eraInput);
break;
case 'month':
monthInput = monthInput.enter()
.append('input')
.attr('type', 'text')
.attr('class', 'date-main date-month')
.call(monthCombo)
.merge(monthInput);
break;
case 'day':
dayInput = dayInput.enter()
.append('input')
.attr('type', 'number')
.attr('class', 'date-main date-day')
.call(utilNoAuto)
.merge(dayInput);
break;
}
});

yearInput
.on('change', change)
.on('blur', change);
eraInput
.on('change', change)
.on('blur', change);
monthInput
.on('change', change)
.on('blur', change);
dayInput
.on('change', change)
.on('blur', change);
}


function change() {
var tag = {};
var yearValue = utilGetSetValue(yearInput).trim();
var eraValue = utilGetSetValue(eraInput).trim();
var monthValue = utilGetSetValue(monthInput).trim();
var dayValue = utilGetSetValue(dayInput).trim();

// don't override multiple values with blank string
if (!yearValue && Array.isArray(_tags[field.key])) return;

if (!yearValue) {
tag[field.key] = undefined;
} else if (isNaN(yearValue)) {
tag[field.key] = context.cleanTagValue(yearValue);
} else {
let value = '';
let year = parseInt(context.cleanTagValue(yearValue), 10);
if (eraValue === bceName) {
value += '-' + String(year - 1).padStart(4, '0');
} else {
value += String(year).padStart(4, '0');
}
let month = context.cleanTagValue(monthValue);
if (monthNames.includes(month)) {
month = monthNames.indexOf(month) + 1;
value += '-' + String(month).padStart(2, '0');
let day = parseInt(context.cleanTagValue(dayValue), 10);
if (!isNaN(day)) {
value += '-' + String(day).padStart(2, '0');
}
}
tag[field.key] = value;
}

dispatch.call('change', this, tag);
}


date.tags = function(tags) {
_tags = tags;

var yearValue = tags[field.key];
var eraValue;
var monthValue;
var dayValue;
var isMixed = Array.isArray(yearValue);

if (!isMixed && yearValue) {
let parts = yearValue.match(/^(-?\d+)(?:-(\d\d))?(?:-(\d\d))?$/);
if (parts && parts[1]) {
yearValue = parseInt(parts[1], 10);
if (yearValue < 1) {
yearValue = -yearValue + 1;
eraValue = bceName;
} else {
eraValue = ceName;
}

if (parts[2]) {
monthValue = monthNames[parseInt(parts[2], 10) - 1] || parts[2];
}

if (parts[3]) {
dayValue = parseInt(parts[3], 10);
}
}
}

utilGetSetValue(yearInput, typeof yearValue === 'number' ? yearValue : '')
.attr('title', isMixed ? yearValue.filter(Boolean).join('\n') : null)
.attr('placeholder', isMixed ? t('inspector.multiple_values') : t('inspector.date.year'))
.classed('mixed', isMixed);
utilGetSetValue(eraInput, typeof eraValue === 'string' ? eraValue : '')
.attr('placeholder', t('inspector.date.era'));
utilGetSetValue(monthInput, typeof monthValue === 'string' ? monthValue : '')
.attr('placeholder', t('inspector.date.month'));
utilGetSetValue(dayInput, typeof dayValue === 'number' ? dayValue : '')
.attr('placeholder', t('inspector.date.day'));
};


date.focus = function() {
let node = yearInput.selectAll('input').node();
if (node) node.focus();
};


date.entityIDs = function(val) {
_entityIDs = val;
};


return utilRebind(date, dispatch, 'on');
}
3 changes: 3 additions & 0 deletions modules/ui/fields/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './combo';
export * from './input';
export * from './access';
export * from './address';
export * from './date';
export * from './directional_combo';
export * from './lanes';
export * from './localized';
Expand Down Expand Up @@ -46,6 +47,7 @@ import {

import { uiFieldAccess } from './access';
import { uiFieldAddress } from './address';
import { uiFieldDate } from './date';
import { uiFieldDirectionalCombo } from './directional_combo';
import { uiFieldLanes } from './lanes';
import { uiFieldLocalized } from './localized';
Expand All @@ -63,6 +65,7 @@ export var uiFields = {
colour: uiFieldColour,
combo: uiFieldCombo,
cycleway: uiFieldDirectionalCombo,
date: uiFieldDate,
defaultCheck: uiFieldDefaultCheck,
directionalCombo: uiFieldDirectionalCombo,
email: uiFieldEmail,
Expand Down

0 comments on commit ac14034

Please sign in to comment.