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

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

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;
}
}