From 057b4a31927453eab7f15308179c2546f4236e0c Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 18 Jan 2022 02:26:21 +0100 Subject: [PATCH] Improve URL handling: typing http(s) is not needed anymore for URLs --- UI/Input/ValidatedTextField.ts | 148 +++++++++++++++++++++++---------- test.ts | 8 +- 2 files changed, 111 insertions(+), 45 deletions(-) diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 4a64cbe8eb..c1101efb4b 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -22,12 +22,18 @@ import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import Table from "../Base/Table"; import Combine from "../Base/Combine"; import Title from "../Base/Title"; +import InputElementMap from "./InputElementMap"; interface TextFieldDef { name: string, explanation: string, isValid: ((s: string, country?: () => string) => boolean), reformat?: ((s: string, country?: () => string) => string), + /** + * Modification to make before the string is uploaded to OSM + */ + postprocess?: (s: string) => string; + undoPostprocess?: (s: string) => string; inputHelper?: (value: UIEventSource, options?: { location: [number, number], mapBackgroundLayer?: UIEventSource, @@ -187,10 +193,89 @@ class OpeningHoursTextField implements TextFieldDef { return new OpeningHoursInput(value, prefix, postfix) } } + +class UrlTextfieldDef implements TextFieldDef { + + name = "url" + explanation = "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user" + inputmode: "url" + + postprocess(str: string) { + if (str === undefined) { + return undefined + } + if (!str.startsWith("http://") || !str.startsWith("https://")) { + return "https://" + str + } + return str; + } + + undoPostprocess(str: string) { + if (str === undefined) { + return undefined + } + if (str.startsWith("http://")) { + return str.substr("http://".length) + } + if (str.startsWith("https://")) { + return str.substr("https://".length) + } + return str; + } + + reformat(str: string): string { + try { + let url: URL + str = str.toLowerCase() + if (!str.startsWith("http://") && !str.startsWith("https://") && !str.startsWith("http:")) { + url = new URL("https://" + str) + } else { + 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", + "campaignid","campaign","AdGroupId","AdGroup","TargetId","msclkid"] + for (const dontLike of blacklistedTrackingParams) { + url.searchParams.delete(dontLike.toLowerCase() ) + } + let cleaned = url.toString(); + if (cleaned.endsWith("/") && !str.endsWith("/")) { + // Do not add a trailing '/' if it wasn't typed originally + cleaned = cleaned.substr(0, cleaned.length - 1) + } + + if (cleaned.startsWith("https://")) { + cleaned = cleaned.substr("https://".length) + } + + return cleaned; + } catch (e) { + console.error(e) + return undefined; + } + } + + isValid(str: string): boolean { + try { + if (!str.startsWith("http://") && !str.startsWith("https://") && + !str.startsWith("http:")) { + str = "https://" + str + } + const url = new URL(str); + const dotIndex = url.host.indexOf(".") + return dotIndex > 0 && url.host[url.host.length - 1 ] !== "."; + } catch (e) { + return false; + } + } +} + export default class ValidatedTextField { public static tpList: TextFieldDef[] = [ - + ValidatedTextField.tp( "string", "A basic string"), @@ -350,54 +435,23 @@ export default class ValidatedTextField { "email", "An email adress", (str) => { - if(str.startsWith("mailto:")){ + if (str.startsWith("mailto:")) { str = str.substring("mailto:".length) } return EmailValidator.validate(str); }, str => { - if(str === undefined){return undefined} - if(str.startsWith("mailto:")){ + if (str === undefined) { + return undefined + } + if (str.startsWith("mailto:")) { str = str.substring("mailto:".length) } return str; }, undefined, "email"), - ValidatedTextField.tp( - "url", - "A url", - (str) => { - try { - new URL(str); - return true; - } catch (e) { - return false; - } - }, - (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"] - for (const dontLike of blacklistedTrackingParams) { - url.searchParams.delete(dontLike) - } - let cleaned = url.toString(); - if (cleaned.endsWith("/") && !str.endsWith("/")) { - // Do not add a trailing '/' if it wasn't typed originally - cleaned = cleaned.substr(0, cleaned.length - 1) - } - return cleaned; - } catch (e) { - console.error(e) - return undefined; - } - }, - undefined, - "url"), + new UrlTextfieldDef(), ValidatedTextField.tp( "phone", "A phone number", @@ -405,13 +459,13 @@ export default class ValidatedTextField { if (str === undefined) { return false; } - if(str.startsWith("tel:")){ + if (str.startsWith("tel:")) { str = str.substring("tel:".length) } return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false }, (str, country: () => string) => { - if(str.startsWith("tel:")){ + if (str.startsWith("tel:")) { str = str.substring("tel:".length) } return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational(); @@ -491,7 +545,7 @@ export default class ValidatedTextField { options.inputMode = tp.inputmode; - if(tp.inputmode === "text") { + if (tp.inputmode === "text") { options.htmlType = "area" } @@ -568,13 +622,21 @@ export default class ValidatedTextField { a => [a, a] ).SetClass("block w-full"); } + if (tp.postprocess !== undefined) { + input = new InputElementMap(input, + (a, b) => a === b, + tp.postprocess, + tp.undoPostprocess + ) + } + return input; } public static HelpText(): BaseUIElement { - const explanations : BaseUIElement[]= + const explanations: BaseUIElement[] = ValidatedTextField.tpList.map(type => - new Combine([new Title(type.name,3), type.explanation]).SetClass("flex flex-col")) + new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")) return new Combine([ new Title("Available types for text fields", 1), "The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them", diff --git a/test.ts b/test.ts index ec4354d348..7b98702cdf 100644 --- a/test.ts +++ b/test.ts @@ -1,2 +1,6 @@ -import NewNoteUi from "./UI/Popup/NewNoteUi"; -import {UIEventSource} from "./Logic/UIEventSource"; +import ValidatedTextField from "./UI/Input/ValidatedTextField"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; + +const tf = ValidatedTextField.InputForType("url") +tf.AttachTo("maindiv") +new VariableUiElement(tf.GetValue()).AttachTo("extradiv") \ No newline at end of file