[WIP] add string length indicator and max-length message

This commit is contained in:
Martin Raifer
2022-11-25 17:30:43 +01:00
parent 5751e80b93
commit 5091966056
9 changed files with 134 additions and 33 deletions

View File

@@ -2324,6 +2324,36 @@ div.combobox {
color: #333;
}
.form-field-input-wrap {
position: relative;
}
.form-field-input-wrap span.length-indicator-wrap {
visibility: hidden;
position: absolute;
top: -5px;
left: 0;
right: 0;
}
.form-field-input-wrap > textarea:focus + span.length-indicator-wrap,
.form-field-input-wrap > textarea:focus + div.combobox-caret + span.length-indicator-wrap {
visibility: visible;
}
.form-field-input-wrap span.length-indicator {
display: block;
left: 0;
right: 0;
height: 4px;
background-color: #7092ff;
border-right-style: solid;
border-right-color: lightgray;
}
.form-field-input-wrap span.length-indicator.limit-reached {
border-right-color: red;
}
/* Field Help
------------------------------------------------------- */

View File

@@ -782,6 +782,7 @@ en:
foot: ft
# abbreviation of inches
inch: in
max_length_reached: "Please keep in mind that texts cannot be longer than a maximum of {maxChars} characters. Anything exceeding that length will be truncated."
background:
title: Background
description: Background Settings

View File

@@ -19,7 +19,7 @@ import { presetManager } from '../presets';
import { rendererBackground, rendererFeatures, rendererMap, rendererPhotos } from '../renderer';
import { services } from '../services';
import { uiInit } from '../ui/init';
import { utilKeybinding, utilRebind, utilStringQs, utilUnicodeCharsTruncated } from '../util';
import { utilKeybinding, utilRebind, utilStringQs, utilCleanOsmString } from '../util';
export function coreContext() {
@@ -220,26 +220,9 @@ export function coreContext() {
context.maxCharsForTagValue = () => 255;
context.maxCharsForRelationRole = () => 255;
function cleanOsmString(val, maxChars) {
// be lenient with input
if (val === undefined || val === null) {
val = '';
} else {
val = val.toString();
}
// remove whitespace
val = val.trim();
// use the canonical form of the string
if (val.normalize) val = val.normalize('NFC');
// trim to the number of allowed characters
return utilUnicodeCharsTruncated(val, maxChars);
}
context.cleanTagKey = (val) => cleanOsmString(val, context.maxCharsForTagKey());
context.cleanTagValue = (val) => cleanOsmString(val, context.maxCharsForTagValue());
context.cleanRelationRole = (val) => cleanOsmString(val, context.maxCharsForRelationRole());
context.cleanTagKey = (val) => utilCleanOsmString(val, context.maxCharsForTagKey());
context.cleanTagValue = (val) => utilCleanOsmString(val, context.maxCharsForTagValue());
context.cleanRelationRole = (val) => utilCleanOsmString(val, context.maxCharsForRelationRole());
/* History */

View File

@@ -156,7 +156,7 @@ osmEntity.prototype = {
changed = true;
merged[k] = utilUnicodeCharsTruncated(
utilArrayUnion(t1.split(/;\s*/), t2.split(/;\s*/)).join(';'),
255 // avoid exceeding character limit; see also services/osm.js -> maxCharsForTagValue()
255 // avoid exceeding character limit; see also context.maxCharsForTagValue()
);
}
}

View File

@@ -7,11 +7,13 @@ import {
utilNoAuto,
utilRebind
} from '../../util';
import { uiLengthIndicator } from '../length_indicator';
export function uiFieldTextarea(field, context) {
var dispatch = d3_dispatch('change');
var input = d3_select(null);
var _lengthIndicator = uiLengthIndicator(context.maxCharsForTagValue());
var _tags;
@@ -22,6 +24,7 @@ export function uiFieldTextarea(field, context) {
wrap = wrap.enter()
.append('div')
.attr('class', 'form-field-input-wrap form-field-input-' + field.type)
.style('position', 'relative')
.merge(wrap);
input = wrap.selectAll('textarea')
@@ -35,22 +38,23 @@ export function uiFieldTextarea(field, context) {
.on('blur', change())
.on('change', change())
.merge(input);
}
wrap.call(_lengthIndicator);
function change(onInput) {
return function() {
function change(onInput) {
return function() {
var val = utilGetSetValue(input);
if (!onInput) val = context.cleanTagValue(val);
var val = utilGetSetValue(input);
if (!onInput) val = context.cleanTagValue(val);
// don't override multiple values with blank string
if (!val && Array.isArray(_tags[field.key])) return;
// don't override multiple values with blank string
if (!val && Array.isArray(_tags[field.key])) return;
var t = {};
t[field.key] = val || undefined;
dispatch.call('change', this, t, onInput);
};
var t = {};
t[field.key] = val || undefined;
dispatch.call('change', this, t, onInput);
};
}
}
@@ -63,6 +67,10 @@ export function uiFieldTextarea(field, context) {
.attr('title', isMixed ? tags[field.key].filter(Boolean).join('\n') : undefined)
.attr('placeholder', isMixed ? t('inspector.multiple_values') : (field.placeholder() || t('inspector.unknown')))
.classed('mixed', isMixed);
if (!isMixed) {
_lengthIndicator.update(tags[field.key]);
}
};

View File

@@ -33,6 +33,7 @@ export { uiIssuesInfo } from './issues_info';
export { uiKeepRightDetails } from './keepRight_details';
export { uiKeepRightEditor } from './keepRight_editor';
export { uiKeepRightHeader } from './keepRight_header';
export { uiLengthIndicator } from './length_indicator';
export { uiLasso } from './lasso';
export { uiLoading } from './loading';
export { uiMapInMap } from './map_in_map';

View File

@@ -0,0 +1,58 @@
import { select as d3_select } from 'd3-selection';
import { t } from '../core/localizer';
import {
utilUnicodeCharsCount,
utilCleanOsmString
} from '../util';
import { uiPopover } from './popover';
export function uiLengthIndicator(maxChars) {
var _wrap = d3_select(null);
var _tooltip = uiPopover('tooltip')
.placement('bottom')
.hasArrow(true)
.content(() => selection => {
selection.text('');
t.append('inspector.max_length_reached', { maxChars })(selection);
});
var lengthIndicator = function(selection) {
_wrap = selection.selectAll('span.length-indicator-wrap').data([0]);
_wrap = _wrap.enter()
.append('span')
.merge(_wrap)
.classed('length-indicator-wrap', true);
selection.call(_tooltip);
};
lengthIndicator.update = function(val) {
const strLen = utilUnicodeCharsCount(utilCleanOsmString(val, Number.POSITIVE_INFINITY));
var lengthIndicator = _wrap.selectAll('span.length-indicator')
.data([strLen]);
lengthIndicator = lengthIndicator.enter()
.append('span')
.merge(lengthIndicator)
.classed('length-indicator', true)
.classed('limit-reached', d => d > maxChars)
.style('border-right-width', d => `${Math.abs(maxChars - d) * 2}px`)
.style('margin-right', d => d > maxChars
? `${(maxChars - d) * 2}px`
: 0)
.style('opacity', d => d > maxChars * 0.8
? Math.min(1, (d / maxChars - 0.8) / (1 - 0.8))
: 0)
.style('pointer-events', d => d > maxChars * 0.8 ? null: 'none');
if (strLen > maxChars) {
_tooltip.show();
} else {
_tooltip.hide();
}
}
return lengthIndicator;
}

View File

@@ -54,3 +54,4 @@ export { utilUnicodeCharsCount } from './util';
export { utilUnicodeCharsTruncated } from './util';
export { utilUniqueDomId } from './util';
export { utilWrap } from './util';
export { utilCleanOsmString } from './util';

View File

@@ -629,3 +629,22 @@ export function utilOldestID(ids) {
return ids[oldestIDIndex];
}
// returns a normalized and truncated string to `maxChars` utf-8 characters
export function utilCleanOsmString(val, maxChars) {
// be lenient with input
if (val === undefined || val === null) {
val = '';
} else {
val = val.toString();
}
// remove whitespace
val = val.trim();
// use the canonical form of the string
if (val.normalize) val = val.normalize('NFC');
// trim to the number of allowed characters
return utilUnicodeCharsTruncated(val, maxChars);
}