forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
04ecdad1bb
61 changed files with 702 additions and 705 deletions
|
@ -72,6 +72,11 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
|
|||
return
|
||||
}
|
||||
|
||||
if (neededTiles.total > 100) {
|
||||
console.error("Too much tiles to download!")
|
||||
return
|
||||
}
|
||||
|
||||
this.isRunning.setData(true)
|
||||
try {
|
||||
const tileNumbers = Tiles.MapRange(neededTiles, (x, y) => {
|
||||
|
@ -133,7 +138,6 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
|
|||
}
|
||||
|
||||
private async LoadTile(z: number, x: number, y: number): Promise<void> {
|
||||
console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend)
|
||||
if (z >= 22) {
|
||||
throw "This is an absurd high zoom level"
|
||||
}
|
||||
|
@ -145,6 +149,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
|
|||
if (this._downloadedTiles.has(index)) {
|
||||
return
|
||||
}
|
||||
console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend)
|
||||
this._downloadedTiles.add(index)
|
||||
|
||||
const bbox = BBox.fromTile(z, x, y)
|
||||
|
|
|
@ -51,10 +51,9 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
|||
*/
|
||||
public readonly layoutToUse: LayoutConfig
|
||||
|
||||
public readonly featureSwitchUserbadge: UIEventSource<boolean>
|
||||
public readonly featureSwitchEnableLogin: UIEventSource<boolean>
|
||||
public readonly featureSwitchSearch: UIEventSource<boolean>
|
||||
public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>
|
||||
public readonly featureSwitchAddNew: UIEventSource<boolean>
|
||||
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>
|
||||
public readonly featureSwitchCommunityIndex: UIEventSource<boolean>
|
||||
public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean>
|
||||
|
@ -78,10 +77,10 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
|||
|
||||
// Helper function to initialize feature switches
|
||||
|
||||
this.featureSwitchUserbadge = FeatureSwitchUtils.initSwitch(
|
||||
"fs-userbadge",
|
||||
this.featureSwitchEnableLogin = FeatureSwitchUtils.initSwitch(
|
||||
"fs-enable-login",
|
||||
layoutToUse?.enableUserBadge ?? true,
|
||||
"Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode."
|
||||
"Disables/Enables logging in and thus disables editing all together. This effectively puts MapComplete into read-only mode."
|
||||
)
|
||||
this.featureSwitchSearch = FeatureSwitchUtils.initSwitch(
|
||||
"fs-search",
|
||||
|
@ -99,11 +98,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
|||
layoutToUse?.enableLayers ?? true,
|
||||
"Disables/Enables the filter view"
|
||||
)
|
||||
this.featureSwitchAddNew = FeatureSwitchUtils.initSwitch(
|
||||
"fs-add-new",
|
||||
layoutToUse?.enableAddNewPoints ?? true,
|
||||
"Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)"
|
||||
)
|
||||
|
||||
this.featureSwitchWelcomeMessage = FeatureSwitchUtils.initSwitch(
|
||||
"fs-welcome-message",
|
||||
true,
|
||||
|
@ -201,12 +196,6 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
|||
)
|
||||
)
|
||||
|
||||
this.featureSwitchUserbadge.addCallbackAndRun((userbadge) => {
|
||||
if (!userbadge) {
|
||||
this.featureSwitchAddNew.setData(false)
|
||||
}
|
||||
})
|
||||
|
||||
this.backgroundLayerId = QueryParameters.GetQueryParameter(
|
||||
"background",
|
||||
layoutToUse?.defaultBackgroundId ?? "osm",
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import Hash from "./Hash"
|
||||
import { Utils } from "../../Utils"
|
||||
import doc = Mocha.reporters.doc
|
||||
|
||||
export class QueryParameters {
|
||||
static defaults: Record<string, string> = {}
|
||||
static documentation: Map<string, string> = new Map<string, string>()
|
||||
private static order: string[] = ["layout", "test", "z", "lat", "lon"]
|
||||
protected static readonly _wasInitialized: Set<string> = new Set()
|
||||
protected static readonly knownSources: Record<string, UIEventSource<string>> = {}
|
||||
private static order: string[] = ["layout", "test", "z", "lat", "lon"]
|
||||
private static initialized = false
|
||||
|
||||
public static GetQueryParameter(
|
||||
|
@ -74,6 +73,7 @@ export class QueryParameters {
|
|||
this.init()
|
||||
return QueryParameters._wasInitialized.has(key)
|
||||
}
|
||||
|
||||
public static initializedParameters(): ReadonlyArray<string> {
|
||||
return Array.from(QueryParameters._wasInitialized.keys())
|
||||
}
|
||||
|
@ -108,14 +108,12 @@ export class QueryParameters {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query parameters of the page location
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
private static Serialize() {
|
||||
const parts = []
|
||||
public static GetParts(exclude?: Set<string>) {
|
||||
const parts: string[] = []
|
||||
for (const key of QueryParameters.order) {
|
||||
if (exclude?.has(key)) {
|
||||
continue
|
||||
}
|
||||
if (QueryParameters.knownSources[key]?.data === undefined) {
|
||||
continue
|
||||
}
|
||||
|
@ -134,6 +132,16 @@ export class QueryParameters {
|
|||
encodeURIComponent(QueryParameters.knownSources[key].data)
|
||||
)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query parameters of the page location
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
private static Serialize() {
|
||||
const parts = QueryParameters.GetParts()
|
||||
if (!Utils.runningFromConsole) {
|
||||
// Don't pollute the history every time a parameter changes
|
||||
try {
|
||||
|
@ -151,4 +159,8 @@ export class QueryParameters {
|
|||
QueryParameters._wasInitialized.clear()
|
||||
QueryParameters.order = []
|
||||
}
|
||||
|
||||
static GetDefaultFor(key: string): string {
|
||||
return QueryParameters.defaults[key]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,25 @@
|
|||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import Hash from "./Hash"
|
||||
import { MenuState } from "../../Models/MenuState"
|
||||
|
||||
export default class ThemeViewStateHashActor {
|
||||
private readonly _state: ThemeViewState
|
||||
|
||||
public static readonly documentation = [
|
||||
"The URL-hash can contain multiple values:",
|
||||
"",
|
||||
"- The id of the currently selected object, e.g. `node/1234`",
|
||||
"- The currently opened menu view",
|
||||
"- The base64-encoded JSON-file specifying a custom theme (only when loading)",
|
||||
"",
|
||||
"### Possible hashes to open a menu",
|
||||
"",
|
||||
"The possible hashes are:",
|
||||
"",
|
||||
MenuState._menuviewTabs.map((tab) => "`menu:" + tab + "`").join(","),
|
||||
MenuState._themeviewTabs.map((tab) => "`theme-menu:" + tab + "`").join(","),
|
||||
]
|
||||
|
||||
/**
|
||||
* Converts the hash to the appropriate themeview state and, vice versa, sets the hash.
|
||||
*
|
||||
|
@ -100,7 +116,7 @@ export default class ThemeViewStateHashActor {
|
|||
|
||||
private loadStateFromHash(hash: string) {
|
||||
const state = this._state
|
||||
const parts = hash.split(";")
|
||||
const parts = hash.split(":")
|
||||
outer: for (const { toggle, name, showOverOthers, submenu } of state.guistate.allToggles) {
|
||||
for (const part of parts) {
|
||||
if (part === name) {
|
||||
|
|
|
@ -50,11 +50,15 @@ export class MenuState {
|
|||
)
|
||||
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
|
||||
constructor(themeid: string = "") {
|
||||
constructor(shouldOpenWelcomeMessage: boolean, themeid: string = "") {
|
||||
// Note: this class is _not_ responsible to update the Hash, @see ThemeViewStateHashActor for this
|
||||
if (themeid) {
|
||||
themeid += "-"
|
||||
}
|
||||
this.themeIsOpened = LocalStorageSource.GetParsed(themeid + "thememenuisopened", true)
|
||||
this.themeIsOpened = LocalStorageSource.GetParsed(
|
||||
themeid + "thememenuisopened",
|
||||
shouldOpenWelcomeMessage
|
||||
)
|
||||
this.themeViewTabIndex = LocalStorageSource.GetParsed(themeid + "themeviewtabindex", 0)
|
||||
this.themeViewTab = this.themeViewTabIndex.sync(
|
||||
(i) => MenuState._themeviewTabs[i],
|
||||
|
|
|
@ -23,6 +23,27 @@ export interface TagRenderingConfigJson {
|
|||
| Translatable
|
||||
| { special: Record<string, string | Record<string, string>> & { type: string } }
|
||||
|
||||
/**
|
||||
* question: what icon should be shown next to the 'render' value?
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* question: When should this item be shown?
|
||||
|
|
|
@ -19,6 +19,8 @@ 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 +47,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
|
||||
|
@ -58,7 +62,7 @@ export default class TagRenderingConfig {
|
|||
|
||||
public readonly freeform?: {
|
||||
readonly key: string
|
||||
readonly type: string
|
||||
readonly type: ValidatorType
|
||||
readonly placeholder: Translation
|
||||
readonly addExtraTags: UploadableTag[]
|
||||
readonly inline: boolean
|
||||
|
@ -124,6 +128,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`
|
||||
|
@ -135,7 +146,17 @@ export default class TagRenderingConfig {
|
|||
) {
|
||||
throw `Freeform.addExtraTags should be a list of strings - not a single string (at ${context})`
|
||||
}
|
||||
const type = json.freeform.type ?? "string"
|
||||
if (
|
||||
json.freeform.type &&
|
||||
Validators.availableTypes.indexOf(<any>json.freeform.type) < 0
|
||||
) {
|
||||
throw `At ${context}: invalid type, perhaps you meant ${Utils.sortedByLevenshteinDistance(
|
||||
json.freeform.key,
|
||||
<any>Validators.availableTypes,
|
||||
(s) => <any>s
|
||||
)}`
|
||||
}
|
||||
const type: ValidatorType = <any>json.freeform.type ?? "string"
|
||||
|
||||
let placeholder: Translation = Translations.T(json.freeform.placeholder)
|
||||
if (placeholder === undefined) {
|
||||
|
@ -230,19 +251,21 @@ export default class TagRenderingConfig {
|
|||
if (txt.indexOf("{" + this.freeform.key + ":") >= 0) {
|
||||
continue
|
||||
}
|
||||
if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (txt.indexOf("{translated(" + this.freeform.key + ")") >= 0) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
this.freeform.type === "opening_hours" &&
|
||||
txt.indexOf("{opening_hours_table(") >= 0
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const keyFirstArg = ["canonical", "fediverse_link", "translated"]
|
||||
if (
|
||||
keyFirstArg.some(
|
||||
(funcName) => txt.indexOf(`{${funcName}(${this.freeform.key}`) >= 0
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
this.freeform.type === "wikidata" &&
|
||||
txt.indexOf("{wikipedia(" + this.freeform.key) >= 0
|
||||
|
@ -528,7 +551,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
|
||||
|
@ -547,7 +570,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
|
||||
|
@ -628,7 +651,7 @@ export default class TagRenderingConfig {
|
|||
*
|
||||
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
|
||||
* @param multiSelectedMapping (Only used if multiAnswer == true): all the mappings that must be applied. Set multiSelectedMapping[mappings.length] to use the freeform as well
|
||||
* @param currentProperties: The current properties of the object for which the question should be answered
|
||||
* @param currentProperties The current properties of the object for which the question should be answered
|
||||
*/
|
||||
public constructChangeSpecification(
|
||||
freeformValue: string | undefined,
|
||||
|
@ -691,38 +714,42 @@ export default class TagRenderingConfig {
|
|||
return undefined
|
||||
}
|
||||
return and
|
||||
} else {
|
||||
// Is at least one mapping shown in the answer?
|
||||
const someMappingIsShown = this.mappings.some((m) => {
|
||||
if (typeof m.hideInAnswer === "boolean") {
|
||||
return !m.hideInAnswer
|
||||
}
|
||||
const isHidden = m.hideInAnswer.matchesProperties(currentProperties)
|
||||
return !isHidden
|
||||
})
|
||||
// If all mappings are hidden for the current tags, we can safely assume that we should use the freeform key
|
||||
const useFreeform =
|
||||
freeformValue !== undefined &&
|
||||
(singleSelectedMapping === this.mappings.length || !someMappingIsShown)
|
||||
if (useFreeform) {
|
||||
return new And([
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
])
|
||||
} else if (singleSelectedMapping !== undefined) {
|
||||
return new And([
|
||||
this.mappings[singleSelectedMapping].if,
|
||||
...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
|
||||
])
|
||||
} else {
|
||||
console.warn("TagRenderingConfig.ConstructSpecification has a weird fallback for", {
|
||||
freeformValue,
|
||||
singleSelectedMapping,
|
||||
multiSelectedMapping,
|
||||
currentProperties,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Is at least one mapping shown in the answer?
|
||||
const someMappingIsShown = this.mappings.some((m) => {
|
||||
if (typeof m.hideInAnswer === "boolean") {
|
||||
return !m.hideInAnswer
|
||||
}
|
||||
const isHidden = m.hideInAnswer.matchesProperties(currentProperties)
|
||||
return !isHidden
|
||||
})
|
||||
// If all mappings are hidden for the current tags, we can safely assume that we should use the freeform key
|
||||
const useFreeform =
|
||||
freeformValue !== undefined &&
|
||||
(singleSelectedMapping === this.mappings.length ||
|
||||
!someMappingIsShown ||
|
||||
singleSelectedMapping === undefined)
|
||||
if (useFreeform) {
|
||||
return new And([
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
])
|
||||
} else if (singleSelectedMapping !== undefined) {
|
||||
return new And([
|
||||
this.mappings[singleSelectedMapping].if,
|
||||
...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
|
||||
])
|
||||
} else {
|
||||
console.error("TagRenderingConfig.ConstructSpecification has a weird fallback for", {
|
||||
freeformValue,
|
||||
singleSelectedMapping,
|
||||
multiSelectedMapping,
|
||||
currentProperties,
|
||||
useFreeform,
|
||||
})
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -110,15 +110,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
constructor(layout: LayoutConfig) {
|
||||
this.layout = layout
|
||||
this.guistate = new MenuState(layout.id)
|
||||
this.featureSwitches = new FeatureSwitchState(layout)
|
||||
this.guistate = new MenuState(
|
||||
this.featureSwitches.featureSwitchWelcomeMessage.data,
|
||||
layout.id
|
||||
)
|
||||
this.map = new UIEventSource<MlMap>(undefined)
|
||||
const initial = new InitialMapPositioning(layout)
|
||||
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
||||
const geolocationState = new GeoLocationState()
|
||||
|
||||
this.featureSwitches = new FeatureSwitchState(layout)
|
||||
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
||||
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchUserbadge
|
||||
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
|
||||
|
||||
this.osmConnection = new OsmConnection({
|
||||
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
||||
|
@ -465,7 +468,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
new ShowDataLayer(this.map, {
|
||||
features: new FilteringFeatureSource(last_click_layer, last_click),
|
||||
doShowLayer: new ImmutableStore(true),
|
||||
doShowLayer: this.featureSwitches.featureSwitchEnableLogin,
|
||||
layer: last_click_layer.layerDef,
|
||||
selectedElement: this.selectedElement,
|
||||
selectedLayer: this.selectedLayer,
|
||||
|
|
121
src/UI/BigComponents/ShareScreen.svelte
Normal file
121
src/UI/BigComponents/ShareScreen.svelte
Normal file
|
@ -0,0 +1,121 @@
|
|||
<script lang="ts">/**
|
||||
* A screen showing:
|
||||
* - A link to share the current view
|
||||
* - Some query parameters that can be enabled/disabled
|
||||
* - The code to embed MC as IFrame
|
||||
*/
|
||||
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import { Utils } from "../../Utils";
|
||||
import Svg from "../../Svg";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline";
|
||||
|
||||
export let state: ThemeViewState;
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
|
||||
let url = window.location;
|
||||
let linkToShare: string = undefined;
|
||||
/**
|
||||
* In some cases (local deploys, custom themes), we need to set the URL to `/theme.html?layout=xyz` instead of `/xyz?...`
|
||||
*/
|
||||
let needsThemeRedirect = url.port !== "" || url.hostname.match(/^[0-9]/) || !state.layout.official;
|
||||
let layoutId = state.layout.id;
|
||||
let baseLink = url.protocol + "//" + url.host + "/" + (needsThemeRedirect ? "theme.html?layout=" + layoutId + "&" : layoutId + "?");
|
||||
|
||||
let showWelcomeMessage = true;
|
||||
let enableLogin = true;
|
||||
$: {
|
||||
const layout = state.layout;
|
||||
let excluded = Utils.NoNull([
|
||||
showWelcomeMessage ? undefined : "fs-welcome-message",
|
||||
enableLogin ? undefined : "fs-enable-login"
|
||||
]);
|
||||
linkToShare = baseLink + QueryParameters.GetParts(new Set(excluded))
|
||||
.concat(excluded.map(k => k + "=" + false))
|
||||
.join("&");
|
||||
if (layout.definitionRaw !== undefined) {
|
||||
linkToShare += "&userlayout=" + (layout.definedAtUrl ?? layout.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function shareCurrentLink() {
|
||||
await navigator.share({
|
||||
title: Translations.W(state.layout.title)?.ConstructElement().textContent ?? "MapComplete",
|
||||
text: Translations.W(state.layout.description)?.ConstructElement().textContent ?? "",
|
||||
url: linkToShare
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
let isCopied = false;
|
||||
|
||||
async function copyCurrentLink() {
|
||||
await navigator.clipboard.writeText(linkToShare);
|
||||
isCopied = true;
|
||||
await Utils.waitFor(5000);
|
||||
isCopied = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
<Tr t={tr.intro} />
|
||||
<div class="flex">
|
||||
{#if typeof navigator?.share === "function"}
|
||||
<button class="w-8 h-8 p-1 shrink-0" on:click={shareCurrentLink}>
|
||||
<ToSvelte construct={Svg.share_svg()} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if navigator.clipboard !== undefined}
|
||||
<button class="w-8 h-8 p-1 shrink-0 no-image-background" on:click={copyCurrentLink}>
|
||||
<DocumentDuplicateIcon />
|
||||
</button>
|
||||
{/if}
|
||||
<div class="literal-code" on:click={e => Utils.selectTextIn(e.target)}>
|
||||
{linkToShare}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
|
||||
{#if isCopied}
|
||||
<Tr t={tr.copiedToClipboard} cls="thanks m-2" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<Tr t={ tr.embedIntro} />
|
||||
|
||||
|
||||
<div class="flex flex-col my-1 link-underline">
|
||||
|
||||
<label>
|
||||
<input bind:checked={showWelcomeMessage} type="checkbox" />
|
||||
<Tr t={tr.fsWelcomeMessage} />
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
<input bind:checked={enableLogin} type="checkbox" />
|
||||
<Tr t={tr.fsUserbadge} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="literal-code m-1">
|
||||
<span class="literal-code iframe-code-block"> <br />
|
||||
<iframe src="${url}" <br />
|
||||
allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" <br />
|
||||
title="${state.layout.title?.txt ?? "MapComplete" } with MapComplete"> <br />
|
||||
</iframe> <br />
|
||||
</span>
|
||||
</div>
|
||||
<Tr t={tr.documentation} cls="link-underline"/>
|
||||
</div>
|
|
@ -1,256 +0,0 @@
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Svg from "../../Svg"
|
||||
import Combine from "../Base/Combine"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import Translations from "../i18n/Translations"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { CheckBox } from "../Input/Checkboxes"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import LZString from "lz-string"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class ShareScreen extends Combine {
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
const layout = state?.layout
|
||||
const tr = Translations.t.general.sharescreen
|
||||
|
||||
const optionCheckboxes: InputElement<boolean>[] = []
|
||||
const optionParts: Store<string>[] = []
|
||||
|
||||
const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
|
||||
optionCheckboxes.push(includeLocation)
|
||||
|
||||
const currentLocation = state.mapProperties.location
|
||||
const zoom = state.mapProperties.zoom
|
||||
|
||||
optionParts.push(
|
||||
includeLocation.GetValue().map(
|
||||
(includeL) => {
|
||||
if (currentLocation === undefined) {
|
||||
return null
|
||||
}
|
||||
if (includeL) {
|
||||
return [
|
||||
["z", zoom.data],
|
||||
["lat", currentLocation.data?.lat],
|
||||
["lon", currentLocation.data?.lon],
|
||||
]
|
||||
.filter((p) => p[1] !== undefined)
|
||||
.map((p) => p[0] + "=" + p[1])
|
||||
.join("&")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
[currentLocation, zoom]
|
||||
)
|
||||
)
|
||||
|
||||
function fLayerToParam(flayer: {
|
||||
isDisplayed: UIEventSource<boolean>
|
||||
layerDef: LayerConfig
|
||||
}) {
|
||||
if (flayer.isDisplayed.data) {
|
||||
return null // Being displayed is the default
|
||||
}
|
||||
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
|
||||
}
|
||||
|
||||
const currentLayer: Store<
|
||||
{ id: string; name: string | Record<string, string> } | undefined
|
||||
> = state.mapProperties.rasterLayer.map((l) => l?.properties)
|
||||
const currentBackground = new VariableUiElement(
|
||||
currentLayer.map((layer) => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" })
|
||||
})
|
||||
)
|
||||
const includeCurrentBackground = new CheckBox(currentBackground, true)
|
||||
optionCheckboxes.push(includeCurrentBackground)
|
||||
optionParts.push(
|
||||
includeCurrentBackground.GetValue().map(
|
||||
(includeBG) => {
|
||||
if (includeBG) {
|
||||
return "background=" + currentLayer.data?.id
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
[currentLayer]
|
||||
)
|
||||
)
|
||||
|
||||
const includeLayerChoices = new CheckBox(tr.fsIncludeCurrentLayers, true)
|
||||
optionCheckboxes.push(includeLayerChoices)
|
||||
|
||||
optionParts.push(
|
||||
includeLayerChoices.GetValue().map(
|
||||
(includeLayerSelection) => {
|
||||
if (includeLayerSelection) {
|
||||
return Utils.NoNull(
|
||||
Array.from(state.layerState.filteredLayers.values()).map(fLayerToParam)
|
||||
).join("&")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
Array.from(state.layerState.filteredLayers.values()).map(
|
||||
(flayer) => flayer.isDisplayed
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const switches = [
|
||||
{ urlName: "fs-userbadge", human: tr.fsUserbadge },
|
||||
{ urlName: "fs-search", human: tr.fsSearch },
|
||||
{ urlName: "fs-welcome-message", human: tr.fsWelcomeMessage },
|
||||
{ urlName: "fs-layers", human: tr.fsLayers },
|
||||
{ urlName: "fs-add-new", human: tr.fsAddNew },
|
||||
{ urlName: "fs-geolocation", human: tr.fsGeolocation },
|
||||
]
|
||||
|
||||
for (const swtch of switches) {
|
||||
const checkbox = new CheckBox(Translations.W(swtch.human))
|
||||
optionCheckboxes.push(checkbox)
|
||||
optionParts.push(
|
||||
checkbox.GetValue().map((isEn) => {
|
||||
if (isEn) {
|
||||
return null
|
||||
} else {
|
||||
return `${swtch.urlName}=false`
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (layout.definitionRaw !== undefined) {
|
||||
optionParts.push(new UIEventSource("userlayout=" + (layout.definedAtUrl ?? layout.id)))
|
||||
}
|
||||
|
||||
const options = new Combine(optionCheckboxes).SetClass("flex flex-col")
|
||||
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
|
||||
const host = window.location.host
|
||||
let path = window.location.pathname
|
||||
path = path.substr(0, path.lastIndexOf("/"))
|
||||
let id = layout.id.toLowerCase()
|
||||
if (layout.definitionRaw !== undefined) {
|
||||
id = "theme.html"
|
||||
}
|
||||
let literalText = `https://${host}${path}/${id}`
|
||||
|
||||
let hash = ""
|
||||
if (layout.definedAtUrl === undefined && layout.definitionRaw !== undefined) {
|
||||
hash = "#" + LZString.compressToBase64(Utils.MinifyJSON(layout.definitionRaw))
|
||||
}
|
||||
const parts = Utils.NoEmpty(
|
||||
Utils.NoNull(optionParts.map((eventSource) => eventSource.data))
|
||||
)
|
||||
if (parts.length === 0) {
|
||||
return literalText + hash
|
||||
}
|
||||
return literalText + "?" + parts.join("&") + hash
|
||||
}, optionParts)
|
||||
|
||||
const iframeCode = new VariableUiElement(
|
||||
url.map((url) => {
|
||||
return `<span class='literal-code iframe-code-block'>
|
||||
<iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${
|
||||
layout.title?.txt ?? "MapComplete"
|
||||
} with MapComplete"></iframe>
|
||||
</span>`
|
||||
})
|
||||
)
|
||||
|
||||
const linkStatus = new UIEventSource<string | Translation>("")
|
||||
const link = new VariableUiElement(
|
||||
url.map(
|
||||
(url) =>
|
||||
`<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`
|
||||
)
|
||||
).onClick(async () => {
|
||||
const shareData = {
|
||||
title: Translations.W(layout.title)?.ConstructElement().textContent ?? "",
|
||||
text: Translations.W(layout.description)?.ConstructElement().textContent ?? "",
|
||||
url: url.data,
|
||||
}
|
||||
|
||||
function rejected() {
|
||||
const copyText = document.getElementById("code-link--copyable")
|
||||
|
||||
// @ts-ignore
|
||||
copyText.select()
|
||||
// @ts-ignore
|
||||
copyText.setSelectionRange(0, 99999) /*For mobile devices*/
|
||||
|
||||
document.execCommand("copy")
|
||||
const copied = tr.copiedToClipboard.Clone()
|
||||
copied.SetClass("thanks")
|
||||
linkStatus.setData(copied)
|
||||
}
|
||||
|
||||
try {
|
||||
navigator
|
||||
.share(shareData)
|
||||
.then(() => {
|
||||
const thx = tr.thanksForSharing.Clone()
|
||||
thx.SetClass("thanks")
|
||||
linkStatus.setData(thx)
|
||||
}, rejected)
|
||||
.catch(rejected)
|
||||
} catch (err) {
|
||||
rejected()
|
||||
}
|
||||
})
|
||||
|
||||
let downloadThemeConfig: BaseUIElement = undefined
|
||||
if (layout.definitionRaw !== undefined) {
|
||||
const downloadThemeConfigAsJson = new SubtleButton(
|
||||
Svg.download_svg(),
|
||||
new Combine([tr.downloadCustomTheme, tr.downloadCustomThemeHelp.SetClass("subtle")])
|
||||
.onClick(() => {
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
layout.definitionRaw,
|
||||
layout.id + ".mapcomplete-theme-definition.json",
|
||||
{
|
||||
mimetype: "application/json",
|
||||
}
|
||||
)
|
||||
})
|
||||
.SetClass("flex flex-col")
|
||||
)
|
||||
let editThemeConfig: BaseUIElement = undefined
|
||||
if (layout.definedAtUrl === undefined) {
|
||||
const patchedDefinition = JSON.parse(layout.definitionRaw)
|
||||
patchedDefinition["language"] = Object.keys(patchedDefinition.title)
|
||||
editThemeConfig = new SubtleButton(
|
||||
Svg.pencil_svg(),
|
||||
"Edit this theme on the custom theme generator",
|
||||
{
|
||||
url: `https://pietervdvn.github.io/mc/legacy/070/customGenerator.html#${btoa(
|
||||
JSON.stringify(patchedDefinition)
|
||||
)}`,
|
||||
}
|
||||
)
|
||||
}
|
||||
downloadThemeConfig = new Combine([
|
||||
downloadThemeConfigAsJson,
|
||||
editThemeConfig,
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
super([
|
||||
tr.intro,
|
||||
link,
|
||||
new VariableUiElement(linkStatus),
|
||||
downloadThemeConfig,
|
||||
tr.addToHomeScreen,
|
||||
tr.embedIntro,
|
||||
options,
|
||||
iframeCode,
|
||||
])
|
||||
this.SetClass("flex flex-col link-underline")
|
||||
}
|
||||
}
|
|
@ -45,9 +45,7 @@
|
|||
<Tr t={layout.description} />
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general} />
|
||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
<Tr t={layout.descriptionTail} />
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
return
|
||||
}
|
||||
|
||||
if (unit && isNaN(Number(v))) {
|
||||
if (unit !== undefined && isNaN(Number(v))) {
|
||||
value.setData(undefined)
|
||||
return
|
||||
}
|
||||
|
@ -75,6 +75,7 @@
|
|||
feedback?.setData(undefined)
|
||||
value.setData(v + (selectedUnit.data ?? ""))
|
||||
}
|
||||
|
||||
|
||||
onDestroy(_value.addCallbackAndRun((_) => setValues()))
|
||||
onDestroy(value.addCallbackAndRunD(fromUpstream => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Translations from "../i18n/Translations"
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
/**
|
||||
* A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback.
|
||||
|
@ -16,13 +16,13 @@ export abstract class Validator {
|
|||
/**
|
||||
* What HTML-inputmode to use
|
||||
*/
|
||||
public readonly inputmode?: string
|
||||
public readonly inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'
|
||||
public readonly textArea: boolean
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
explanation: string | BaseUIElement,
|
||||
inputmode?: string,
|
||||
inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search',
|
||||
textArea?: false | boolean
|
||||
) {
|
||||
this.name = name
|
||||
|
|
|
@ -22,6 +22,7 @@ import SimpleTagValidator from "./Validators/SimpleTagValidator"
|
|||
import ImageUrlValidator from "./Validators/ImageUrlValidator"
|
||||
import TagKeyValidator from "./Validators/TagKeyValidator"
|
||||
import TranslationValidator from "./Validators/TranslationValidator"
|
||||
import FediverseValidator from "./Validators/FediverseValidator"
|
||||
|
||||
export type ValidatorType = (typeof Validators.availableTypes)[number]
|
||||
|
||||
|
@ -47,6 +48,7 @@ export default class Validators {
|
|||
"simple_tag",
|
||||
"key",
|
||||
"translation",
|
||||
"fediverse",
|
||||
] as const
|
||||
|
||||
public static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
|
@ -70,6 +72,7 @@ export default class Validators {
|
|||
new SimpleTagValidator(),
|
||||
new TagKeyValidator(),
|
||||
new TranslationValidator(),
|
||||
new FediverseValidator(),
|
||||
]
|
||||
|
||||
private static _byType = Validators._byTypeConstructor()
|
||||
|
|
63
src/UI/InputElement/Validators/FediverseValidator.ts
Normal file
63
src/UI/InputElement/Validators/FediverseValidator.ts
Normal 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
|
||||
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { Validator } from "../Validator"
|
||||
import { ValidatorType } from "../Validators";
|
||||
|
||||
export default class FloatValidator extends Validator {
|
||||
inputmode = "decimal"
|
||||
inputmode: "decimal" = "decimal"
|
||||
|
||||
constructor(name?: string, explanation?: string) {
|
||||
constructor(name?: ValidatorType, explanation?: string) {
|
||||
super(name ?? "float", explanation ?? "A decimal number", "decimal")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
<script lang="ts">
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import { InformationCircleIcon, TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import type { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
||||
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig"
|
||||
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction"
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import { DeleteFlowState } from "./DeleteFlowState"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini";
|
||||
import type { OsmId, OsmTags } from "../../../Models/OsmFeature";
|
||||
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig";
|
||||
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils";
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction";
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
import { DeleteFlowState } from "./DeleteFlowState";
|
||||
import { twJoin } from "tailwind-merge";
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let deleteConfig: DeleteConfig
|
||||
|
@ -83,10 +83,9 @@
|
|||
</script>
|
||||
|
||||
{#if $canBeDeleted === false && !hasSoftDeletion}
|
||||
<div class="low-interaction flex">
|
||||
<InformationCircleIcon class="h-6 w-6" />
|
||||
<div class="low-interaction flex flex-col">
|
||||
<Tr t={$canBeDeletedReason} />
|
||||
<Tr class="subtle" t={t.useSomethingElse} />
|
||||
<Tr cls="subtle" t={t.useSomethingElse} />
|
||||
</div>
|
||||
{:else}
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
|
|
|
@ -38,11 +38,12 @@
|
|||
let selectedMapping: number = undefined
|
||||
let checkedMappings: boolean[]
|
||||
$: {
|
||||
let tgs = $tags
|
||||
mappings = config.mappings?.filter((m) => {
|
||||
if (typeof m.hideInAnswer === "boolean") {
|
||||
return !m.hideInAnswer
|
||||
}
|
||||
return m.hideInAnswer.matchesProperties(tags.data)
|
||||
return !m.hideInAnswer.matchesProperties(tgs)
|
||||
})
|
||||
// We received a new config -> reinit
|
||||
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
|
||||
|
@ -59,7 +60,7 @@
|
|||
if (config.freeform?.key) {
|
||||
if (!config.multiAnswer) {
|
||||
// Somehow, setting multianswer freeform values is broken if this is not set
|
||||
freeformInput.setData(tags.data[config.freeform.key])
|
||||
freeformInput.setData(tgs[config.freeform.key])
|
||||
}
|
||||
} else {
|
||||
freeformInput.setData(undefined)
|
||||
|
@ -69,7 +70,7 @@
|
|||
export let selectedTags: TagsFilter = undefined
|
||||
|
||||
let mappings: Mapping[] = config?.mappings
|
||||
let searchTerm: Store<string> = new UIEventSource("")
|
||||
let searchTerm: UIEventSource<string> = new UIEventSource("")
|
||||
|
||||
$: {
|
||||
try {
|
||||
|
|
|
@ -6,6 +6,7 @@ import Translations from "./i18n/Translations"
|
|||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
|
||||
|
||||
export default class QueryParameterDocumentation {
|
||||
private static QueryParamDocsIntro = [
|
||||
|
@ -60,6 +61,7 @@ export default class QueryParameterDocumentation {
|
|||
public static GenerateQueryParameterDocs(): BaseUIElement {
|
||||
const docs: (string | BaseUIElement)[] = [
|
||||
...QueryParameterDocumentation.QueryParamDocsIntro,
|
||||
...ThemeViewStateHashActor.documentation,
|
||||
]
|
||||
this.UrlParamDocs().forEach((value, key) => {
|
||||
const c = new Combine([
|
||||
|
|
|
@ -82,6 +82,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
|
||||
|
@ -1374,6 +1375,43 @@ 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))
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import MapControlButton from "./Base/MapControlButton.svelte"
|
||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
||||
import If from "./Base/If.svelte"
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl"
|
||||
import type { Feature } from "geojson"
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import Filterview from "./BigComponents/Filterview.svelte"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import type { MapProperties } from "../Models/MapProperties"
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import MapControlButton from "./Base/MapControlButton.svelte";
|
||||
import ToSvelte from "./Base/ToSvelte.svelte";
|
||||
import If from "./Base/If.svelte";
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl";
|
||||
import type { Feature } from "geojson";
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import Filterview from "./BigComponents/Filterview.svelte";
|
||||
import ThemeViewState from "../Models/ThemeViewState";
|
||||
import type { MapProperties } from "../Models/MapProperties";
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||
import Translations from "./i18n/Translations";
|
||||
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
||||
import FloatOver from "./Base/FloatOver.svelte"
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
|
||||
import Constants from "../Models/Constants"
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import LoginToggle from "./Base/LoginToggle.svelte"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
|
||||
import ModalRight from "./Base/ModalRight.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
|
||||
import Svg from "../Svg"
|
||||
import { ShareScreen } from "./BigComponents/ShareScreen"
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
|
||||
import IfHidden from "./Base/IfHidden.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import { OpenJosm } from "./BigComponents/OpenJosm"
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte"
|
||||
import LanguagePicker from "./LanguagePicker"
|
||||
import Locale from "./i18n/Locale"
|
||||
import Tr from "./Base/Tr.svelte";
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
||||
import FloatOver from "./Base/FloatOver.svelte";
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
|
||||
import Constants from "../Models/Constants";
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
import LoginToggle from "./Base/LoginToggle.svelte";
|
||||
import LoginButton from "./Base/LoginButton.svelte";
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel";
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
|
||||
import ModalRight from "./Base/ModalRight.svelte";
|
||||
import { Utils } from "../Utils";
|
||||
import Hotkeys from "./Base/Hotkeys";
|
||||
import { VariableUiElement } from "./Base/VariableUIElement";
|
||||
import SvelteUIElement from "./Base/SvelteUIElement";
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte";
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
|
||||
import Svg from "../Svg";
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers";
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers";
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
|
||||
import IfHidden from "./Base/IfHidden.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import { OpenJosm } from "./BigComponents/OpenJosm";
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte";
|
||||
import LanguagePicker from "./LanguagePicker";
|
||||
import Locale from "./i18n/Locale";
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte";
|
||||
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
|
@ -314,11 +314,12 @@
|
|||
|
||||
<ToSvelte construct={() => new CopyrightPanel(state)} slot="content3" />
|
||||
|
||||
<div slot="title4">
|
||||
<div slot="title4" class="flex">
|
||||
<ToSvelte construct={Svg.share_svg().SetClass("w-4 h-4")} />
|
||||
<Tr t={Translations.t.general.sharescreen.title} />
|
||||
</div>
|
||||
<div class="m-2" slot="content4">
|
||||
<ToSvelte construct={() => new ShareScreen(state)} />
|
||||
<ShareScreen {state}/>
|
||||
</div>
|
||||
</TabbedGroup>
|
||||
</FloatOver>
|
||||
|
|
19
src/Utils.ts
19
src/Utils.ts
|
@ -221,6 +221,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
* Utils.Round7(12.123456789) // => 12.1234568
|
||||
*/
|
||||
public static Round7(i: number): number {
|
||||
if (i == undefined) {
|
||||
return undefined
|
||||
}
|
||||
return Math.round(i * 10000000) / 10000000
|
||||
}
|
||||
|
||||
|
@ -1211,6 +1214,22 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
return new Date(str)
|
||||
}
|
||||
|
||||
public static selectTextIn(node) {
|
||||
if (document.body["createTextRange"]) {
|
||||
const range = document.body["createTextRange"]()
|
||||
range.moveToElementText(node)
|
||||
range.select()
|
||||
} else if (window.getSelection) {
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(node)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
} else {
|
||||
console.warn("Could not select text in node: Unsupported browser.")
|
||||
}
|
||||
}
|
||||
|
||||
public static sortedByLevenshteinDistance<T>(
|
||||
reference: string,
|
||||
ts: T[],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue