Add colour input, add inputmode-hints to have specialized keyboards on mobile

This commit is contained in:
pietervdvn 2021-05-11 02:39:51 +02:00
parent 1f0b20f5d4
commit 8774b887d8
10 changed files with 406 additions and 155 deletions

View file

@ -41,7 +41,7 @@ export interface TagRenderingConfigJson {
type?: string,
/**
* If a value is added with the textfield, these extra tag is addded.
* Usefull to add a 'fixme=freeform textfield used - to be checked'
* Useful to add a 'fixme=freeform textfield used - to be checked'
**/
addExtraTags?: string[];
},

View file

@ -2,7 +2,7 @@ import { Utils } from "../Utils";
export default class Constants {
public static vNumber = "0.7.1-rc1";
public static vNumber = "0.7.2-dev";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {

60
UI/Input/ColorPicker.ts Normal file
View file

@ -0,0 +1,60 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {Utils} from "../../Utils";
export default class ColorPicker extends InputElement<string> {
private readonly value: UIEventSource<string>
constructor(
value?: UIEventSource<string>
) {
super();
this.value = value ?? new UIEventSource<string>(undefined);
const self = this;
this.value.addCallbackAndRun(v => {
if(v === undefined){
return;
}
self.SetValue(v);
});
}
InnerRender(): string {
return `<span id="${this.id}"><input type='color' id='color-${this.id}'></span>`;
}
private SetValue(color: string){
const field = document.getElementById("color-" + this.id);
if (field === undefined || field === null) {
return;
}
// @ts-ignore
field.value = color;
}
protected InnerUpdate() {
const field = document.getElementById("color-" + this.id);
if (field === undefined || field === null) {
return;
}
const self = this;
field.oninput = () => {
const hex = field["value"];
self.value.setData(hex);
}
}
GetValue(): UIEventSource<string> {
return this.value;
}
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
IsValid(t: string): boolean {
return false;
}
}

View file

@ -10,6 +10,7 @@ export class TextField extends InputElement<string> {
private readonly _placeholder: UIElement;
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _htmlType: string;
private readonly _inputMode : string;
private readonly _textAreaRows: number;
private readonly _isValid: (string,country) => boolean;
@ -20,6 +21,7 @@ export class TextField extends InputElement<string> {
value?: UIEventSource<string>,
textArea?: boolean,
htmlType?: string,
inputMode?: string,
label?: UIElement,
textAreaRows?: number,
isValid?: ((s: string, country?: () => string) => boolean)
@ -36,6 +38,7 @@ export class TextField extends InputElement<string> {
this._isValid = options.isValid ?? ((str, country) => true);
this._placeholder = Translations.W(options.placeholder ?? "");
this._inputMode = options.inputMode;
this.ListenTo(this._placeholder._source);
this.onClick(() => {
@ -72,11 +75,15 @@ export class TextField extends InputElement<string> {
if (this._label != undefined) {
label = this._label.Render();
}
let inputMode = ""
if(this._inputMode !== undefined){
inputMode = `inputmode="${this._inputMode}" `
}
return new Combine([
`<span id="${this.id}">`,
`<form onSubmit='return false' class='form-text-field'>`,
label,
`<input type='${this._htmlType}' placeholder='${placeholder}' id='txt-${this.id}'/>`,
`<input type='${this._htmlType}' ${inputMode} placeholder='${placeholder}' id='txt-${this.id}'/>`,
`</form>`,
`</span>`
]).Render();
@ -134,9 +141,6 @@ export class TextField extends InputElement<string> {
}
public SetCursorPosition(i: number) {
if(this._htmlType !== "text" && this._htmlType !== "area"){
return;
}
const field = document.getElementById('txt-' + this.id);
if(field === undefined || field === null){
return;

View file

@ -10,53 +10,36 @@ import CombinedInputElement from "./CombinedInputElement";
import SimpleDatePicker from "./SimpleDatePicker";
import OpeningHoursInput from "../OpeningHours/OpeningHoursInput";
import DirectionInput from "./DirectionInput";
import ColorPicker from "./ColorPicker";
import {Utils} from "../../Utils";
interface TextFieldDef {
name: string,
explanation: string,
isValid: ((s: string, country?:() => string) => boolean),
isValid: ((s: string, country?: () => string) => boolean),
reformat?: ((s: string, country?: () => string) => string),
inputHelper?: (value: UIEventSource<string>, options?: {
location: [number, number]
}) => InputElement<string>,
inputmode?: string
}
export default class ValidatedTextField {
private static tp(name: string,
explanation: string,
isValid?: ((s: string, country?: () => string) => boolean),
reformat?: ((s: string, country?: () => string) => string),
inputHelper?: (value: UIEventSource<string>, options?:{
location: [number, number]
}) => InputElement<string>): TextFieldDef {
if (isValid === undefined) {
isValid = () => true;
}
if (reformat === undefined) {
reformat = (str, _) => str;
}
return {
name: name,
explanation: explanation,
isValid: isValid,
reformat: reformat,
inputHelper: inputHelper
}
}
public static tpList: TextFieldDef[] = [
ValidatedTextField.tp(
"string",
"A basic string"),
ValidatedTextField.tp(
"text",
"A string, but allows input of longer strings more comfortably (a text area)"),
"A string, but allows input of longer strings more comfortably (a text area)",
undefined,
undefined,
undefined,
"text"),
ValidatedTextField.tp(
"date",
"A date",
@ -87,44 +70,63 @@ export default class ValidatedTextField {
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))
}),
},
undefined,
undefined,
"numeric"),
ValidatedTextField.tp(
"nat",
"A positive number or zero",
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
}),
},
undefined,
undefined,
"numeric"),
ValidatedTextField.tp(
"pnat",
"A strict positive number",
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0
}),
},
undefined,
undefined,
"numeric"),
ValidatedTextField.tp(
"direction",
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)",
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360
},str => str,
}, str => str,
(value) => {
return new DirectionInput(value);
}
return new DirectionInput(value);
},
"numeric"
),
ValidatedTextField.tp(
"float",
"A decimal",
(str) => !isNaN(Number(str))),
(str) => !isNaN(Number(str)),
undefined,
undefined,
"decimal"),
ValidatedTextField.tp(
"pfloat",
"A positive decimal (incl zero)",
(str) => !isNaN(Number(str)) && Number(str) >= 0),
(str) => !isNaN(Number(str)) && Number(str) >= 0,
undefined,
undefined,
"decimal"),
ValidatedTextField.tp(
"email",
"An email adress",
(str) => EmailValidator.validate(str)),
(str) => EmailValidator.validate(str),
undefined,
undefined,
"email"),
ValidatedTextField.tp(
"url",
"A url",
@ -135,18 +137,19 @@ export default class ValidatedTextField {
} catch (e) {
return false;
}
}, (str) => {
},
(str) => {
try {
const url = new URL(str);
const blacklistedTrackingParams = [
"fbclid",// Oh god, how I hate the fbclid. Let it burn, burn in hell!
"gclid",
"cmpid", "agid", "utm", "utm_source","utm_medium"]
"cmpid", "agid", "utm", "utm_source", "utm_medium"]
for (const dontLike of blacklistedTrackingParams) {
url.searchParams.delete(dontLike)
}
let cleaned = url.toString();
if(cleaned.endsWith("/") && !str.endsWith("/")){
if (cleaned.endsWith("/") && !str.endsWith("/")) {
// Do not add a trailing '/' if it wasn't typed originally
cleaned = cleaned.substr(0, cleaned.length - 1)
}
@ -155,7 +158,9 @@ export default class ValidatedTextField {
console.error(e)
return undefined;
}
}),
},
undefined,
"url"),
ValidatedTextField.tp(
"phone",
"A phone number",
@ -165,26 +170,35 @@ export default class ValidatedTextField {
}
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false
},
(str, country: () => string) => parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational()
(str, country: () => string) => parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational(),
undefined,
"tel"
),
ValidatedTextField.tp(
"opening_hours",
"Has extra elements to easily input when a POI is opened",
(s, country) => true,
str => str,
() => true,
str => str,
(value) => {
return new OpeningHoursInput(value);
}
),
ValidatedTextField.tp(
"color",
"Shows a color picker",
() => true,
str => str,
(value) => {
return new ColorPicker(value.map(color => {
return Utils.ColourNameToHex(color ?? "");
}, [], str => Utils.HexToColourName(str)))
}
)
]
private static allTypesDict(){
const types = {};
for (const tp of ValidatedTextField.tpList) {
types[tp.name] = tp;
}
return types;
}
/**
* {string (typename) --> TextFieldDef}
*/
public static AllTypes = ValidatedTextField.allTypesDict();
public static TypeDropdown(): DropDown<string> {
const values: { value: string, shown: string }[] = [];
@ -195,15 +209,12 @@ export default class ValidatedTextField {
return new DropDown<string>("", values)
}
/**
* {string (typename) --> TextFieldDef}
*/
public static AllTypes = ValidatedTextField.allTypesDict();
public static InputForType(type: string, options?: {
placeholder?: string | UIElement,
value?: UIEventSource<string>,
textArea?: boolean,
htmlType?: string,
textArea?:boolean,
inputMode?:string,
textAreaRows?: number,
isValid?: ((s: string, country: () => string) => boolean),
country?: () => string,
@ -218,16 +229,16 @@ export default class ValidatedTextField {
if (options.isValid) {
const optValid = options.isValid;
isValid = (str, country) => {
if(str === undefined){
if (str === undefined) {
return false;
}
return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country);
}
}else{
} else {
isValid = isValidTp;
}
options.isValid = isValid;
options.inputMode = tp.inputmode;
let input: InputElement<string> = new TextField(options);
if (tp.reformat) {
input.GetValue().addCallbackAndRun(str => {
@ -240,7 +251,7 @@ export default class ValidatedTextField {
}
if (tp.inputHelper) {
input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(),{
input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(), {
location: options.location
}));
}
@ -270,7 +281,7 @@ export default class ValidatedTextField {
const textField = ValidatedTextField.InputForType(type);
return new InputElementMap(textField, (n0, n1) => n0 === n1, fromString, toString)
}
public static KeyInput(allowEmpty: boolean = false): InputElement<string> {
function fromString(str) {
@ -299,8 +310,6 @@ export default class ValidatedTextField {
return new InputElementMap(textfield, isSame, fromString, toString);
}
static Mapped<T>(fromString: (str) => T, toString: (T) => string, options?: {
placeholder?: string | UIElement,
type?: string,
@ -323,4 +332,46 @@ export default class ValidatedTextField {
);
}
public static HelpText(): string {
const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n")
return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations
}
private static tp(name: string,
explanation: string,
isValid?: ((s: string, country?: () => string) => boolean),
reformat?: ((s: string, country?: () => string) => string),
inputHelper?: (value: UIEventSource<string>, options?: {
location: [number, number]
}) => InputElement<string>,
inputmode?: string): TextFieldDef {
if (isValid === undefined) {
isValid = () => true;
}
if (reformat === undefined) {
reformat = (str, _) => str;
}
return {
name: name,
explanation: explanation,
isValid: isValid,
reformat: reformat,
inputHelper: inputHelper,
inputmode: inputmode
}
}
private static allTypesDict() {
const types = {};
for (const tp of ValidatedTextField.tpList) {
types[tp.name] = tp;
}
return types;
}
}

View file

@ -1,6 +1,6 @@
import * as $ from "jquery"
import {type} from "os";
import * as colors from "./assets/colors.json"
export class Utils {
/**
@ -305,6 +305,69 @@ export class Utils {
element.click();
}
public static ColourNameToHex(color: string): string{
return colors[color.toLowerCase()] ?? color;
}
public static HexToColourName(hex : string): string{
hex = hex.toLowerCase()
if(!hex.startsWith("#")){
return hex;
}
const c = Utils.color(hex);
let smallestDiff = Number.MAX_VALUE;
let bestColor = undefined;
for (const color in colors) {
if(!colors.hasOwnProperty(color)){
continue;
}
const foundhex = colors[color];
if(typeof foundhex !== "string"){
continue
}
if(foundhex === hex){
return color
}
const diff = this.colorDiff(Utils.color(foundhex), c)
if(diff > 50){
continue;
}
if(diff < smallestDiff){
smallestDiff = diff;
bestColor = color;
}
}
return bestColor ?? hex;
}
private static colorDiff(c0 : {r: number, g: number, b: number}, c1: {r: number, g: number, b: number}){
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) +Math.abs(c0.b - c1.b) ;
}
private static color(hex: string) : {r: number, g: number, b: number}{
if(hex.startsWith == undefined){
console.trace("WUT?", hex)
throw "wut?"
}
if(!hex.startsWith("#")){
return undefined;
}
if(hex.length === 4){
return {
r : parseInt(hex.substr(1, 1), 16),
g : parseInt(hex.substr(2, 1), 16),
b : parseInt(hex.substr(3, 1), 16),
}
}
return {
r : parseInt(hex.substr(1, 2), 16),
g : parseInt(hex.substr(3, 2), 16),
b : parseInt(hex.substr(5, 2), 16),
}
}
}
export interface TileRange{

150
assets/colors.json Normal file
View file

@ -0,0 +1,150 @@
{
"aliceblue": "#f0f8ff",
"antiquewhite": "#faebd7",
"aqua": "#00ffff",
"aquamarine": "#7fffd4",
"azure": "#f0ffff",
"beige": "#f5f5dc",
"bisque": "#ffe4c4",
"black": "#000000",
"blanchedalmond": "#ffebcd",
"blue": "#0000ff",
"blueviolet": "#8a2be2",
"brown": "#a52a2a",
"burlywood": "#deb887",
"cadetblue": "#5f9ea0",
"chartreuse": "#7fff00",
"chocolate": "#d2691e",
"coral": "#ff7f50",
"cornflowerblue": "#6495ed",
"cornsilk": "#fff8dc",
"crimson": "#dc143c",
"cyan": "#00ffff",
"darkblue": "#00008b",
"darkcyan": "#008b8b",
"darkgoldenrod": "#b8860b",
"darkgray": "#a9a9a9",
"darkgrey": "#a9a9a9",
"darkgreen": "#006400",
"darkkhaki": "#bdb76b",
"darkmagenta": "#8b008b",
"darkolivegreen": "#556b2f",
"darkorange": "#ff8c00",
"darkorchid": "#9932cc",
"darkred": "#8b0000",
"darksalmon": "#e9967a",
"darkseagreen": "#8fbc8f",
"darkslateblue": "#483d8b",
"darkslategray": "#2f4f4f",
"darkslategrey": "#2f4f4f",
"darkturquoise": "#00ced1",
"darkviolet": "#9400d3",
"deeppink": "#ff1493",
"deepskyblue": "#00bfff",
"dimgray": "#696969",
"dimgrey": "#696969",
"dodgerblue": "#1e90ff",
"firebrick": "#b22222",
"floralwhite": "#fffaf0",
"forestgreen": "#228b22",
"fuchsia": "#ff00ff",
"gainsboro": "#dcdcdc",
"ghostwhite": "#f8f8ff",
"gold": "#ffd700",
"goldenrod": "#daa520",
"gray": "#808080",
"grey": "#808080",
"green": "#008000",
"greenyellow": "#adff2f",
"honeydew": "#f0fff0",
"hotpink": "#ff69b4",
"indianred": "#cd5c5c",
"indigo": "#4b0082",
"ivory": "#fffff0",
"khaki": "#f0e68c",
"lavender": "#e6e6fa",
"lavenderblush": "#fff0f5",
"lawngreen": "#7cfc00",
"lemonchiffon": "#fffacd",
"lightblue": "#add8e6",
"lightcoral": "#f08080",
"lightcyan": "#e0ffff",
"lightgoldenrodyellow": "#fafad2",
"lightgray": "#d3d3d3",
"lightgrey": "#d3d3d3",
"lightgreen": "#90ee90",
"lightpink": "#ffb6c1",
"lightsalmon": "#ffa07a",
"lightseagreen": "#20b2aa",
"lightskyblue": "#87cefa",
"lightslategray": "#778899",
"lightslategrey": "#778899",
"lightsteelblue": "#b0c4de",
"lightyellow": "#ffffe0",
"lime": "#00ff00",
"limegreen": "#32cd32",
"linen": "#faf0e6",
"magenta": "#ff00ff",
"maroon": "#800000",
"mediumaquamarine": "#66cdaa",
"mediumblue": "#0000cd",
"mediumorchid": "#ba55d3",
"mediumpurple": "#9370db",
"mediumseagreen": "#3cb371",
"mediumslateblue": "#7b68ee",
"mediumspringgreen": "#00fa9a",
"mediumturquoise": "#48d1cc",
"mediumvioletred": "#c71585",
"midnightblue": "#191970",
"mintcream": "#f5fffa",
"mistyrose": "#ffe4e1",
"moccasin": "#ffe4b5",
"navajowhite": "#ffdead",
"navy": "#000080",
"oldlace": "#fdf5e6",
"olive": "#808000",
"olivedrab": "#6b8e23",
"orange": "#ffa500",
"orangered": "#ff4500",
"orchid": "#da70d6",
"palegoldenrod": "#eee8aa",
"palegreen": "#98fb98",
"paleturquoise": "#afeeee",
"palevioletred": "#db7093",
"papayawhip": "#ffefd5",
"peachpuff": "#ffdab9",
"peru": "#cd853f",
"pink": "#ffc0cb",
"plum": "#dda0dd",
"powderblue": "#b0e0e6",
"purple": "#800080",
"rebeccapurple": "#663399",
"red": "#ff0000",
"rosybrown": "#bc8f8f",
"royalblue": "#4169e1",
"saddlebrown": "#8b4513",
"salmon": "#fa8072",
"sandybrown": "#f4a460",
"seagreen": "#2e8b57",
"seashell": "#fff5ee",
"sienna": "#a0522d",
"silver": "#c0c0c0",
"skyblue": "#87ceeb",
"slateblue": "#6a5acd",
"slategray": "#708090",
"slategrey": "#708090",
"snow": "#fffafa",
"springgreen": "#00ff7f",
"steelblue": "#4682b4",
"tan": "#d2b48c",
"teal": "#008080",
"thistle": "#d8bfd8",
"tomato": "#ff6347",
"turquoise": "#40e0d0",
"violet": "#ee82ee",
"wheat": "#f5deb3",
"white": "#ffffff",
"whitesmoke": "#f5f5f5",
"yellow": "#ffff00",
"yellowgreen": "#9acd32"
}

View file

@ -6,6 +6,7 @@ import {UIElement} from "../UI/UIElement";
import SimpleMetaTagger from "../Logic/SimpleMetaTagger";
import Combine from "../UI/Base/Combine";
import {ExtraFunction} from "../Logic/ExtraFunction";
import ValidatedTextField from "../UI/Input/ValidatedTextField";
@ -19,6 +20,6 @@ function WriteFile(filename, html: UIElement) : void {
WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage)
WriteFile("./Docs/CalculatedTags.md", new Combine([SimpleMetaTagger.HelpText(), ExtraFunction.HelpText()]))
writeFileSync("./Docs/SpecialInputElements", ValidatedTextField.HelpText());
console.log("Generated docs")

View file

@ -18,85 +18,8 @@
<div id="maindiv">'maindiv' not attached</div>
<div id="extradiv">'extradiv' not attached</div>
<script>
const cacheElement = {
"freshness": "2021-04-21T09:50:28.000Z",
feature:
{
"type": "Feature",
"id": "way/912515518",
"properties": {
"id": "way/912515518",
"name": "Speelbos De Reukens",
"playground": "forest",
"leisure": "playground",
"operator": "The world!"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
4.378341436386108,
51.120101600003316
],
[
4.378175139427185,
51.11954264284114
],
[
4.3786633014678955,
51.119963544947566
],
[
4.379317760467529,
51.119525806677146
],
[
4.379017353057861,
51.11997027935011
],
[
4.379714727401733,
51.12028679516189
],
[
4.379006624221802,
51.120313732577664
],
[
4.378706216812134,
51.120744729093836
],
[
4.378384351730347,
51.120306998225196
],
[
4.377686977386475,
51.120306998225196
],
[
4.378341436386108,
51.120101600003316
]
]
]
}
}
}
const cache = [cacheElement]
localStorage.setItem("cached-featuresspeelplekken", JSON.stringify(cache))
</script>
<script src="./test.ts"></script>
<iframe src="http://127.0.0.1:1234/index.html" height="500px" width="600px"></iframe>
<iframe src="http://127.0.0.1:1234/index.html?layout=bookcases&fs-userbadge=false&fs-search=false&fs-welcome-message=false&fs-layers=false&fs-geolocation=false"
height="500px" width="600px"></iframe>
</body>
</html>

View file

@ -1,4 +1,3 @@
import ValidatedTextField from "./UI/Input/ValidatedTextField";
alert("Hello world!")
ValidatedTextField.InputForType("phone").AttachTo("maindiv")