Themes: add possibility to add an icon to 'render' (just like with mappings), add contact:mastodon support as general question, add mastodon question to hackerspaces

This commit is contained in:
Pieter Vander Vennet 2023-08-10 14:10:06 +02:00
parent 64648f7bb4
commit 03aafbe99c
8 changed files with 237 additions and 99 deletions

View file

@ -112,6 +112,7 @@
"website",
"email",
"phone",
"mastodon",
{
"builtin": "opening_hours_24_7",
"override": {

View file

@ -173,11 +173,13 @@
"render": {
"*": "<a href='tel:{phone}'>{phone}</a>"
},
"icon": "./assets/layers/questions/phone.svg",
"mappings": [
{
"if": "contact:phone~*",
"then": "<a href='tel:{contact:phone}'>{contact:phone}</a>",
"hideInAnswer": true
"hideInAnswer": true,
"icon": "./assets/layers/questions/phone.svg"
}
],
"freeform": {
@ -188,6 +190,21 @@
]
}
},
{
"id": "mastodon",
"description": "Shows and asks for the mastodon handle",
"question": {
"en": "What is the Mastodon-handle of {title()}?"
},
"freeform": {
"key": "contact:mastodon",
"type": "fediverse"
},
"render": {
"*": "{fediverse_link(contact:mastodon)}"
},
"icon": "./assets/svg/mastodon.svg"
},
{
"id": "osmlink",
"render": {
@ -205,6 +222,7 @@
"render": {
"*": "<a href='mailto:{email}' target='_blank'>{email}</a>"
},
"icon": "./assets/svg/envelope.svg",
"labels": [
"contact"
],
@ -236,6 +254,7 @@
"mappings": [
{
"if": "contact:email~*",
"icon": "./assets/svg/envelope.svg",
"then": "<a href='mailto:{contact:email}' target='_blank'>{contact:email}</a>",
"hideInAnswer": true
}
@ -253,6 +272,7 @@
"labels": [
"contact"
],
"icon": "./assets/layers/icons/website.svg",
"question": {
"en": "What is the website of {title()}?",
"nl": "Wat is de website van {title()}?",
@ -292,7 +312,8 @@
{
"if": "contact:website~*",
"then": "<a href='{contact:website}' rel='nofollow noopener noreferrer' target='_blank'>{contact:website}</a>",
"hideInAnswer": true
"hideInAnswer": true,
"icon": "./assets/layers/icons/website.svg"
}
]
},

View file

@ -610,6 +610,11 @@
"feedback": "This is not a valid email address",
"noAt": "An e-mail address must contain an @"
},
"fediverse": {
"description": "A fediverse handle, often @username@server.tld",
"feedback": "A fediverse handle consists of @username@server.tld or is a link to a profile",
"invalidHost": "{host} is not a valid hostname"
},
"float": {
"description": "a number",
"feedback": "This is not a number"

View file

@ -41,6 +41,26 @@ export interface TagRenderingConfigJson {
| Record<string, string>
| { special: Record<string, string | Record<string, string>> & { type: string } }
/**
* An icon shown next to the rendering; typically shown pretty small
* This is only shown next to the "render" value
* Type: icon
*/
icon?:
| string
| {
/**
* The path to the icon
* Type: icon
*/
path: string
/**
* A hint to mapcomplete on how to render this icon within the mapping.
* This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged)
*/
class?: "small" | "medium" | "large" | string
}
/**
* Only show this tagrendering (or ask the question) if the selected object also matches the tags specified as `condition`.
*

View file

@ -10,15 +10,16 @@ import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title"
import Link from "../../UI/Base/Link"
import List from "../../UI/Base/List"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
import {MappingConfigJson, QuestionableTagRenderingConfigJson,} from "./Json/QuestionableTagRenderingConfigJson"
import {FixedUiElement} from "../../UI/Base/FixedUiElement"
import {Paragraph} from "../../UI/Base/Paragraph"
import Svg from "../../Svg"
import Validators, {ValidatorType} from "../../UI/InputElement/Validators"
export interface Icon {
}
export interface Mapping {
readonly if: UploadableTag
readonly ifnot?: UploadableTag
@ -45,6 +46,8 @@ export interface Mapping {
export default class TagRenderingConfig {
public readonly id: string
public readonly render?: TypedTranslation<object>
public readonly renderIcon?: string
public readonly renderIconClass?: string
public readonly question?: TypedTranslation<object>
public readonly questionhint?: TypedTranslation<object>
public readonly condition?: TagsFilter
@ -122,6 +125,13 @@ export default class TagRenderingConfig {
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
this.description = Translations.T(json.description, translationKey + ".description")
this.condition = TagUtils.Tag(json.condition ?? {and: []}, `${context}.condition`)
if (typeof json.icon === "string") {
this.renderIcon = json.icon
this.renderIconClass = "small"
}else if (typeof json.icon === "object"){
this.renderIcon = json.icon.path
this.renderIconClass = json.icon.class
}
this.metacondition = TagUtils.Tag(
json.metacondition ?? {and: []},
`${context}.metacondition`
@ -238,15 +248,17 @@ export default class TagRenderingConfig {
if (txt.indexOf("{" + this.freeform.key + ":") >= 0) {
continue
}
if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) {
continue
}
if (
this.freeform.type === "opening_hours" &&
txt.indexOf("{opening_hours_table(") >= 0
) {
continue
}
const keyFirstArg = ["canonical", "fediverse_link"]
if (keyFirstArg.some(funcName => txt.indexOf(`{${funcName}(${this.freeform.key}`) >= 0)) {
continue
}
if (
this.freeform.type === "wikidata" &&
txt.indexOf("{wikipedia(" + this.freeform.key) >= 0
@ -532,7 +544,7 @@ export default class TagRenderingConfig {
*/
public GetRenderValueWithImage(
tags: Record<string, string>
): { then: TypedTranslation<any>; icon?: string } | undefined {
): { then: TypedTranslation<any>; icon?: string, iconClass?: string } | undefined {
if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) {
return undefined
@ -551,7 +563,7 @@ export default class TagRenderingConfig {
}
if (this.freeform?.key === undefined || tags[this.freeform.key] !== undefined) {
return { then: this.render }
return {then: this.render, icon: this.renderIcon, iconClass: this.renderIconClass}
}
return undefined

View file

@ -18,6 +18,7 @@ import ColorValidator from "./Validators/ColorValidator"
import BaseUIElement from "../BaseUIElement"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
import FediverseValidator from "./Validators/FediverseValidator";
export type ValidatorType = (typeof Validators.availableTypes)[number]
@ -39,6 +40,7 @@ export default class Validators {
"phone",
"opening_hours",
"color",
"fediverse"
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -58,6 +60,7 @@ export default class Validators {
new PhoneValidator(),
new OpeningHoursValidator(),
new ColorValidator(),
new FediverseValidator()
]
private static _byType = Validators._byTypeConstructor()

View file

@ -0,0 +1,63 @@
import {Validator} from "../Validator"
import {Translation} from "../../i18n/Translation";
import Translations from "../../i18n/Translations";
export default class FediverseValidator extends Validator {
public static readonly usernameAtServer: RegExp = /^@?(\w+)@((\w|\.)+)$/
constructor() {
super("fediverse", "Validates fediverse addresses and normalizes them into `@username@server`-format");
}
/**
* Returns an `@username@host`
* @param s
*/
reformat(s: string): string {
if(!s.startsWith("@")){
s = "@"+s
}
if (s.match(FediverseValidator.usernameAtServer)) {
return s
}
try {
const url = new URL(s)
const path = url.pathname
if (path.match(/^\/\w+$/)) {
return `@${path.substring(1)}@${url.hostname}`;
}
} catch (e) {
// Nothing to do here
}
return undefined
}
getFeedback(s: string): Translation | undefined {
const match = s.match(FediverseValidator.usernameAtServer)
console.log("Match:", match)
if (match) {
const host = match[2]
try {
const url = new URL("https://" + host)
return undefined
} catch (e) {
return Translations.t.validation.fediverse.invalidHost.Subs({host})
}
}
try {
const url = new URL(s)
const path = url.pathname
if (path.match(/^\/\w+$/)) {
return undefined
}
} catch (e) {
// Nothing to do here
}
return Translations.t.validation.fediverse.feedback
}
isValid(s): boolean {
return this.getFeedback(s) === undefined
}
}

View file

@ -3,11 +3,7 @@ import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title"
import Table from "./Base/Table"
import {
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import {RenderingSpecification, SpecialVisualization, SpecialVisualizationState,} from "./SpecialVisualization"
import {HistogramViz} from "./Popup/HistogramViz"
import {MinimapViz} from "./Popup/MinimapViz"
import {ShareLinkViz} from "./Popup/ShareLinkViz"
@ -58,11 +54,7 @@ import LanguagePicker from "./LanguagePicker"
import Link from "./Base/Link"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import NearbyImages, {
NearbyImageOptions,
P4CPicture,
SelectOneNearbyImage,
} from "./Popup/NearbyImages"
import NearbyImages, {NearbyImageOptions, P4CPicture, SelectOneNearbyImage,} from "./Popup/NearbyImages"
import {Tag} from "../Logic/Tags/Tag"
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"
import {And} from "../Logic/Tags/And"
@ -82,6 +74,7 @@ import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonV
import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"
import {OpenJosm} from "./BigComponents/OpenJosm"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import FediverseValidator from "./InputElement/Validators/FediverseValidator";
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -1344,6 +1337,26 @@ export default class SpecialVisualizations {
)
},
},
{
funcName: "fediverse_link",
docs: "Converts a fediverse username or link into a clickable link",
args: [{
name: "key",
doc: "The attribute-name containing the link",
required: true
}],
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
const key = argument[0]
const validator = new FediverseValidator()
return new VariableUiElement(tagSource.map(tags => tags[key]).map(fediAccount => {
fediAccount = validator.reformat(fediAccount)
const [_, username, host] = fediAccount.match(FediverseValidator.usernameAtServer)
return new Link(fediAccount, "https://" + host + "/@" + username, true)
}
))
}
}
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))