forked from MapComplete/MapComplete
Add theme for 'notes'
This commit is contained in:
parent
677a07e3d2
commit
a58ce564c2
20 changed files with 678 additions and 314 deletions
|
@ -10,13 +10,14 @@ import Svg from "../../Svg";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import State from "../../State";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
||||
import BackgroundSelector from "./BackgroundSelector";
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
||||
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
import {QueryParameters} from "../../Logic/Web/QueryParameters";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
|
||||
export default class FilterView extends VariableUiElement {
|
||||
constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) {
|
||||
|
@ -142,151 +143,104 @@ export default class FilterView extends VariableUiElement {
|
|||
if (layer.filters.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const toShow : BaseUIElement [] = []
|
||||
|
||||
const filterIndexes = new Map<string, number>()
|
||||
layer.filters.forEach((f, i) => filterIndexes.set(f.id, i))
|
||||
|
||||
let listFilterElements: [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>][] = layer.filters.map(
|
||||
filter => FilterView.createFilter(filter)
|
||||
);
|
||||
|
||||
listFilterElements.forEach((inputElement, i) =>
|
||||
inputElement[1].addCallback((changed) => {
|
||||
const oldValue = flayer.appliedFilters.data
|
||||
|
||||
if (changed === undefined) {
|
||||
// Lets figure out which filter should be removed
|
||||
// We know this inputElement corresponds with layer.filters[i]
|
||||
// SO, if there is a value in 'oldValue' with this filter, we have to recalculated
|
||||
if (!oldValue.some(f => f.filter === layer.filters[i])) {
|
||||
// The filter to remove is already gone, we can stop
|
||||
return;
|
||||
}
|
||||
} else if (oldValue.some(f => f.filter === changed.filter && f.selected === changed.selected)) {
|
||||
// The changed value is already there
|
||||
return;
|
||||
}
|
||||
const listTagsFilters = Utils.NoNull(
|
||||
listFilterElements.map((input) => input[1].data)
|
||||
);
|
||||
|
||||
flayer.appliedFilters.setData(listTagsFilters);
|
||||
for (const filter of layer.filters) {
|
||||
|
||||
const [ui, actualTags] = FilterView.createFilter(filter)
|
||||
|
||||
ui.SetClass("mt-3")
|
||||
toShow.push(ui)
|
||||
actualTags.addCallback(tagsToFilterFor => {
|
||||
flayer.appliedFilters.data.set(filter.id, tagsToFilterFor)
|
||||
flayer.appliedFilters.ping()
|
||||
})
|
||||
);
|
||||
flayer.appliedFilters.map(dict => dict.get(filter.id))
|
||||
.addCallbackAndRun(filters => actualTags.setData(filters))
|
||||
|
||||
|
||||
}
|
||||
|
||||
flayer.appliedFilters.addCallbackAndRun(appliedFilters => {
|
||||
for (let i = 0; i < layer.filters.length; i++) {
|
||||
const filter = layer.filters[i];
|
||||
let foundMatch = undefined
|
||||
for (const appliedFilter of appliedFilters) {
|
||||
if (appliedFilter.filter === filter) {
|
||||
foundMatch = appliedFilter
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
listFilterElements[i][1].setData(foundMatch)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return new Combine(listFilterElements.map(input => input[0].SetClass("mt-3")))
|
||||
return new Combine(toShow)
|
||||
.SetClass("flex flex-col ml-8 bg-gray-300 rounded-xl p-2")
|
||||
|
||||
}
|
||||
|
||||
// Filter which uses one or more textfields
|
||||
private static createFilterWithFields(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
|
||||
|
||||
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>] {
|
||||
|
||||
if (filterConfig.options[0].fields.length > 0) {
|
||||
|
||||
// Filter which uses one or more textfields
|
||||
const filter = filterConfig.options[0]
|
||||
const mappings = new Map<string, BaseUIElement>()
|
||||
let allValid = new UIEventSource(true)
|
||||
const properties = new UIEventSource<any>({})
|
||||
for (const {name, type} of filter.fields) {
|
||||
const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id)
|
||||
const field = ValidatedTextField.InputForType(type, {
|
||||
value
|
||||
}).SetClass("inline-block")
|
||||
mappings.set(name, field)
|
||||
const stable = value.stabilized(250)
|
||||
stable.addCallbackAndRunD(v => {
|
||||
properties.data[name] = v.toLowerCase();
|
||||
properties.ping()
|
||||
})
|
||||
allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable])
|
||||
const filter = filterConfig.options[0]
|
||||
const mappings = new Map<string, BaseUIElement>()
|
||||
let allValid = new UIEventSource(true)
|
||||
const properties = new UIEventSource<any>({})
|
||||
for (const {name, type} of filter.fields) {
|
||||
const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id)
|
||||
const field = ValidatedTextField.InputForType(type, {
|
||||
value
|
||||
}).SetClass("inline-block")
|
||||
mappings.set(name, field)
|
||||
const stable = value.stabilized(250)
|
||||
stable.addCallbackAndRunD(v => {
|
||||
properties.data[name] = v.toLowerCase();
|
||||
properties.ping()
|
||||
})
|
||||
allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable])
|
||||
}
|
||||
const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), State.state, mappings)
|
||||
const trigger : UIEventSource<FilterState>= allValid.map(isValid => {
|
||||
if (!isValid) {
|
||||
return undefined
|
||||
}
|
||||
const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), State.state, mappings)
|
||||
const neutral = {
|
||||
filter: new FilterConfig({
|
||||
id: filterConfig.id,
|
||||
options: [
|
||||
{
|
||||
question: "--",
|
||||
}
|
||||
]
|
||||
}, "While dynamically constructing a filterconfig"),
|
||||
selected: 0
|
||||
}
|
||||
const trigger = allValid.map(isValid => {
|
||||
if (!isValid) {
|
||||
return neutral
|
||||
}
|
||||
|
||||
// Replace all the field occurences in the tags...
|
||||
const osmTags = Utils.WalkJson(filter.originalTagsSpec,
|
||||
v => {
|
||||
if (typeof v !== "string") {
|
||||
return v
|
||||
}
|
||||
return Utils.SubstituteKeys(v, properties.data)
|
||||
const props = properties.data
|
||||
// Replace all the field occurences in the tags...
|
||||
const tagsSpec = Utils.WalkJson(filter.originalTagsSpec,
|
||||
v => {
|
||||
if (typeof v !== "string") {
|
||||
return v
|
||||
}
|
||||
)
|
||||
// ... which we use below to construct a filter!
|
||||
return {
|
||||
filter: new FilterConfig({
|
||||
id: filterConfig.id,
|
||||
options: [
|
||||
{
|
||||
question: "--",
|
||||
osmTags
|
||||
}
|
||||
]
|
||||
}, "While dynamically constructing a filterconfig"),
|
||||
selected: 0
|
||||
|
||||
for (const key in props) {
|
||||
v = (<string>v).replace("{"+key+"}", props[key])
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
}, [properties])
|
||||
return [tr, trigger];
|
||||
}
|
||||
|
||||
|
||||
if (filterConfig.options.length === 1) {
|
||||
let option = filterConfig.options[0];
|
||||
|
||||
const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6");
|
||||
const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6");
|
||||
|
||||
const toggle = new Toggle(
|
||||
new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"),
|
||||
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass("flex")
|
||||
)
|
||||
.ToggleOnClick()
|
||||
.SetClass("block m-1")
|
||||
|
||||
const selected = {
|
||||
filter: filterConfig,
|
||||
selected: 0
|
||||
const tagsFilter = TagUtils.Tag(tagsSpec)
|
||||
return {
|
||||
currentFilter: tagsFilter,
|
||||
state: JSON.stringify(props)
|
||||
}
|
||||
return [toggle, toggle.isEnabled.map(enabled => enabled ? selected : undefined, [],
|
||||
f => f?.filter === filterConfig && f?.selected === 0)
|
||||
]
|
||||
}
|
||||
}, [properties])
|
||||
|
||||
return [tr, trigger];
|
||||
}
|
||||
|
||||
private static createCheckboxFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
|
||||
let option = filterConfig.options[0];
|
||||
|
||||
const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6");
|
||||
const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6");
|
||||
|
||||
const toggle = new Toggle(
|
||||
new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"),
|
||||
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass("flex")
|
||||
)
|
||||
.ToggleOnClick()
|
||||
.SetClass("block m-1")
|
||||
|
||||
return [toggle, toggle.isEnabled.map(enabled => enabled ? {currentFilter: option.osmTags, state: "true"} : undefined, [],
|
||||
f => f !== undefined)
|
||||
]
|
||||
}
|
||||
private static createMultiFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
|
||||
|
||||
let options = filterConfig.options;
|
||||
|
||||
const values = options.map((f, i) => ({
|
||||
filter: filterConfig, selected: i
|
||||
const values : FilterState[] = options.map((f, i) => ({
|
||||
currentFilter: f.osmTags, state: i
|
||||
}))
|
||||
const radio = new RadioButton(
|
||||
options.map(
|
||||
|
@ -302,8 +256,25 @@ export default class FilterView extends VariableUiElement {
|
|||
i => values[i],
|
||||
[],
|
||||
selected => {
|
||||
return selected?.selected
|
||||
const v = selected?.state
|
||||
if(v === undefined || typeof v === "string"){
|
||||
return undefined
|
||||
}
|
||||
return v
|
||||
}
|
||||
)]
|
||||
}
|
||||
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
|
||||
|
||||
if (filterConfig.options[0].fields.length > 0) {
|
||||
return FilterView.createFilterWithFields(filterConfig)
|
||||
}
|
||||
|
||||
|
||||
if (filterConfig.options.length === 1) {
|
||||
return FilterView.createCheckboxFilter(filterConfig)
|
||||
}
|
||||
|
||||
return FilterView.createMultiFilter(filterConfig)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,7 +111,10 @@ export default class SimpleAddUI extends Toggle {
|
|||
message,
|
||||
state.LastClickLocation.data,
|
||||
confirm,
|
||||
cancel)
|
||||
cancel,
|
||||
() => {
|
||||
isShown.setData(false)
|
||||
})
|
||||
}
|
||||
))
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import {QueryParameters} from "../Logic/Web/QueryParameters";
|
||||
import Constants from "../Models/Constants";
|
||||
import Hash from "../Logic/Web/Hash";
|
||||
|
||||
export class DefaultGuiState {
|
||||
|
@ -46,18 +45,19 @@ export class DefaultGuiState {
|
|||
"false",
|
||||
"Whether or not the current view box is shown"
|
||||
)
|
||||
if (Hash.hash.data === "download") {
|
||||
this.downloadControlIsOpened.setData(true)
|
||||
const states = {
|
||||
download: this.downloadControlIsOpened,
|
||||
filters: this.filterViewIsOpened,
|
||||
copyright: this.copyrightViewIsOpened,
|
||||
currentview: this.currentViewControlIsOpened,
|
||||
welcome: this.welcomeMessageIsOpened
|
||||
}
|
||||
if (Hash.hash.data === "filters") {
|
||||
this.filterViewIsOpened.setData(true)
|
||||
}
|
||||
if (Hash.hash.data === "copyright") {
|
||||
this.copyrightViewIsOpened.setData(true)
|
||||
}if (Hash.hash.data === "currentview") {
|
||||
this.currentViewControlIsOpened.setData(true)
|
||||
}
|
||||
if (Hash.hash.data === "" || Hash.hash.data === undefined || Hash.hash.data === "welcome") {
|
||||
Hash.hash.addCallbackAndRunD(hash => {
|
||||
hash = hash.toLowerCase()
|
||||
states[hash]?.setData(true)
|
||||
})
|
||||
|
||||
if (Hash.hash.data === "" || Hash.hash.data === undefined) {
|
||||
this.welcomeMessageIsOpened.setData(true)
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
loc: { lon: number, lat: number },
|
||||
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
|
||||
cancel: () => void,
|
||||
closePopup: () => void
|
||||
) {
|
||||
|
||||
let preciseInput: LocationInput = undefined
|
||||
|
@ -137,33 +138,26 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
]
|
||||
).SetClass("flex flex-col")
|
||||
).onClick(() => {
|
||||
preset.layerToAddTo.appliedFilters.setData([])
|
||||
|
||||
const appliedFilters = preset.layerToAddTo.appliedFilters;
|
||||
appliedFilters.data.forEach((_, k) => appliedFilters.data.set(k, undefined))
|
||||
appliedFilters.ping()
|
||||
cancel()
|
||||
closePopup()
|
||||
})
|
||||
|
||||
const hasActiveFilter = preset.layerToAddTo.appliedFilters
|
||||
.map(appliedFilters => {
|
||||
const activeFilters = Array.from(appliedFilters.values()).filter(f => f?.currentFilter !== undefined);
|
||||
return activeFilters.length === 0;
|
||||
})
|
||||
|
||||
// If at least one filter is active which _might_ hide a newly added item, this blocks the preset and requests the filter to be disabled
|
||||
const disableFiltersOrConfirm = new Toggle(
|
||||
openLayerOrConfirm,
|
||||
disableFilter,
|
||||
preset.layerToAddTo.appliedFilters.map(filters => {
|
||||
if (filters === undefined || filters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
for (const filter of filters) {
|
||||
if (filter.selected === 0 && filter.filter.options.length === 1) {
|
||||
return false;
|
||||
}
|
||||
if (filter.selected !== undefined) {
|
||||
const tags = filter.filter.options[filter.selected].osmTags
|
||||
if (tags !== undefined && tags["and"]?.length !== 0) {
|
||||
// This actually doesn't filter anything at all
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
})
|
||||
)
|
||||
disableFilter,
|
||||
hasActiveFilter)
|
||||
|
||||
|
||||
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
|
||||
|
|
|
@ -520,7 +520,8 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
guiState: DefaultGuiState,
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
feature: any,
|
||||
onCancel: () => void): BaseUIElement {
|
||||
onCancel: () => void,
|
||||
close: () => void): BaseUIElement {
|
||||
|
||||
async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) {
|
||||
|
||||
|
@ -559,7 +560,7 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
return new ConfirmLocationOfPoint(state, guiState.filterViewIsOpened, presetInfo, Translations.W(args.text), {
|
||||
lon,
|
||||
lat
|
||||
}, confirm, onCancel)
|
||||
}, confirm, onCancel, close)
|
||||
|
||||
}
|
||||
|
||||
|
@ -567,7 +568,7 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
originalFeatureTags,
|
||||
guiState,
|
||||
feature,
|
||||
onCancel): BaseUIElement {
|
||||
onCancel: () => void): BaseUIElement {
|
||||
|
||||
|
||||
const geometry = feature.geometry
|
||||
|
@ -579,7 +580,11 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
guiState,
|
||||
originalFeatureTags,
|
||||
feature,
|
||||
onCancel
|
||||
onCancel,
|
||||
() => {
|
||||
// Close the current popup
|
||||
state.selectedElement.setData(undefined)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,9 @@ import AutoApplyButton from "./Popup/AutoApplyButton";
|
|||
import * as left_right_style_json from "../assets/layers/left_right_style/left_right_style.json";
|
||||
import {OpenIdEditor} from "./BigComponents/CopyrightPanel";
|
||||
import Toggle from "./Input/Toggle";
|
||||
import Img from "./Base/Img";
|
||||
import ValidatedTextField from "./Input/ValidatedTextField";
|
||||
import Link from "./Base/Link";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string,
|
||||
|
@ -53,8 +56,41 @@ export default class SpecialVisualizations {
|
|||
|
||||
public static specialVisualizations = SpecialVisualizations.init()
|
||||
|
||||
private static init(){
|
||||
const specialVisualizations: SpecialVisualization[] =
|
||||
public static HelpMessage() {
|
||||
|
||||
const helpTexts =
|
||||
SpecialVisualizations.specialVisualizations.map(viz => new Combine(
|
||||
[
|
||||
new Title(viz.funcName, 3),
|
||||
viz.docs,
|
||||
viz.args.length > 0 ? new Table(["name", "default", "description"],
|
||||
viz.args.map(arg => {
|
||||
let defaultArg = arg.defaultValue ?? "_undefined_"
|
||||
if (defaultArg == "") {
|
||||
defaultArg = "_empty string_"
|
||||
}
|
||||
return [arg.name, defaultArg, arg.doc];
|
||||
})
|
||||
) : undefined,
|
||||
new Title("Example usage of " + viz.funcName, 4),
|
||||
new FixedUiElement(
|
||||
viz.example ?? "`{" + viz.funcName + "(" + viz.args.map(arg => arg.defaultValue).join(",") + ")}`"
|
||||
).SetClass("literal-code"),
|
||||
|
||||
]
|
||||
));
|
||||
|
||||
return new Combine([
|
||||
new Title("Special tag renderings", 1),
|
||||
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
|
||||
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
|
||||
...helpTexts
|
||||
]
|
||||
).SetClass("flex flex-col");
|
||||
}
|
||||
|
||||
private static init() {
|
||||
const specialVisualizations: SpecialVisualization[] =
|
||||
[
|
||||
{
|
||||
funcName: "all_tags",
|
||||
|
@ -590,12 +626,12 @@ export default class SpecialVisualizations {
|
|||
funcName: "open_in_iD",
|
||||
docs: "Opens the current view in the iD-editor",
|
||||
args: [],
|
||||
constr: (state, feature ) => {
|
||||
constr: (state, feature) => {
|
||||
return new OpenIdEditor(state, undefined, feature.data.id)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
funcName: "clear_location_history",
|
||||
docs: "A button to remove the travelled track information from the device",
|
||||
|
@ -611,29 +647,51 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
{
|
||||
funcName: "close_note",
|
||||
docs: "Button to close a note",
|
||||
args:[
|
||||
docs: "Button to close a note - eventually with a prefixed text",
|
||||
args: [
|
||||
{
|
||||
name:"text",
|
||||
name: "text",
|
||||
doc: "Text to show on this button",
|
||||
},
|
||||
{
|
||||
name:"Id-key",
|
||||
name: "icon",
|
||||
doc: "Icon to show",
|
||||
defaultValue: "checkmark.svg"
|
||||
},
|
||||
{
|
||||
name: "Id-key",
|
||||
doc: "The property name where the ID of the note to close can be found",
|
||||
defaultValue: "id"
|
||||
},
|
||||
{
|
||||
name: "comment",
|
||||
doc: "Text to add onto the note when closing",
|
||||
}
|
||||
],
|
||||
constr: (state, tags, args, guiState) => {
|
||||
const t = Translations.t.notes;
|
||||
const closeButton = new SubtleButton( Svg.checkmark_svg(), t.closeNote)
|
||||
const isClosed = new UIEventSource(false);
|
||||
|
||||
let icon = Svg.checkmark_svg()
|
||||
if (args[2] !== "checkmark.svg" && (args[2] ?? "") !== "") {
|
||||
icon = new Img(args[2])
|
||||
}
|
||||
let textToShow = t.closeNote;
|
||||
if ((args[0] ?? "") !== "") {
|
||||
textToShow = Translations.T(args[0])
|
||||
}
|
||||
|
||||
const closeButton = new SubtleButton(icon, textToShow)
|
||||
const isClosed = tags.map(tags => (tags["closed_at"] ?? "") === "");
|
||||
closeButton.onClick(() => {
|
||||
const id = tags.data[args[1] ?? "id"]
|
||||
if(state.featureSwitchIsTesting.data){
|
||||
if (state.featureSwitchIsTesting.data) {
|
||||
console.log("Not actually closing note...")
|
||||
return;
|
||||
}
|
||||
state.osmConnection.closeNote(id).then(_ => isClosed.setData(true))
|
||||
state.osmConnection.closeNote(id, args[3]).then(_ => {
|
||||
tags.data["closed_at"] = new Date().toISOString();
|
||||
tags.ping()
|
||||
})
|
||||
})
|
||||
return new Toggle(
|
||||
t.isClosed.SetClass("thanks"),
|
||||
|
@ -641,46 +699,157 @@ export default class SpecialVisualizations {
|
|||
isClosed
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
funcName: "add_note_comment",
|
||||
docs: "A textfield to add a comment to a node (with the option to close the note).",
|
||||
args: [
|
||||
{
|
||||
name: "Id-key",
|
||||
doc: "The property name where the ID of the note to close can be found",
|
||||
defaultValue: "id"
|
||||
}
|
||||
],
|
||||
constr: (state, tags, args, guiState) => {
|
||||
|
||||
const t = Translations.t.notes;
|
||||
const textField = ValidatedTextField.InputForType("text", {placeholder: t.addCommentPlaceholder})
|
||||
textField.SetClass("rounded-l border border-grey")
|
||||
const txt = textField.GetValue()
|
||||
|
||||
const addCommentButton = new SubtleButton(undefined, t.addCommentPlaceholder)
|
||||
.onClick(async () => {
|
||||
const id = tags.data[args[1] ?? "id"]
|
||||
|
||||
if (isClosed.data) {
|
||||
await state.osmConnection.reopenNote(id, txt.data)
|
||||
await state.osmConnection.closeNote(id)
|
||||
} else {
|
||||
await state.osmConnection.addCommentToNode(id, txt.data)
|
||||
}
|
||||
const comments: any[] = JSON.parse(tags.data["comments"])
|
||||
const username = state.osmConnection.userDetails.data.name
|
||||
comments.push({
|
||||
"date": new Date().toISOString(),
|
||||
"uid": state.osmConnection.userDetails.data.uid,
|
||||
"user": username,
|
||||
"user_url": "https://www.openstreetmap.org/user/" + username,
|
||||
"action": "commented",
|
||||
"text": txt.data
|
||||
})
|
||||
tags.data["comments"] = JSON.stringify(comments)
|
||||
tags.ping()
|
||||
txt.setData("")
|
||||
|
||||
})
|
||||
|
||||
|
||||
const close = new SubtleButton(undefined, new VariableUiElement(txt.map(txt => {
|
||||
if (txt === undefined || txt === "") {
|
||||
return t.closeNote
|
||||
}
|
||||
return t.addCommentAndClose
|
||||
}))).onClick(() => {
|
||||
const id = tags.data[args[1] ?? "id"]
|
||||
if (state.featureSwitchIsTesting.data) {
|
||||
console.log("Testmode: Not actually closing note...")
|
||||
return;
|
||||
}
|
||||
state.osmConnection.closeNote(id, txt.data).then(_ => {
|
||||
tags.data["closed_at"] = new Date().toISOString();
|
||||
tags.ping()
|
||||
})
|
||||
})
|
||||
|
||||
const reopen = new SubtleButton(undefined, new VariableUiElement(txt.map(txt => {
|
||||
if (txt === undefined || txt === "") {
|
||||
return t.reopenNote
|
||||
}
|
||||
return t.reopenNoteAndComment
|
||||
}))).onClick(() => {
|
||||
const id = tags.data[args[1] ?? "id"]
|
||||
if (state.featureSwitchIsTesting.data) {
|
||||
console.log("Testmode: Not actually reopening note...")
|
||||
return;
|
||||
}
|
||||
state.osmConnection.reopenNote(id, txt.data).then(_ => {
|
||||
tags.data["closed_at"] = undefined;
|
||||
tags.ping()
|
||||
})
|
||||
})
|
||||
|
||||
const isClosed = tags.map(tags => (tags["closed_at"] ?? "") !== "");
|
||||
const stateButtons = new Toggle(reopen, close, isClosed)
|
||||
|
||||
return new Combine([
|
||||
new Title("Add a comment"),
|
||||
textField,
|
||||
new Combine([addCommentButton.SetClass("mr-2"), stateButtons]).SetClass("flex justify-end")
|
||||
]).SetClass("border-2 border-black rounded-xl p-4 block");
|
||||
}
|
||||
},
|
||||
{
|
||||
funcName: "visualize_note_comments",
|
||||
docs: "Visualises the comments for nodes",
|
||||
args: [
|
||||
{
|
||||
name: "commentsKey",
|
||||
doc: "The property name of the comments, which should be stringified json",
|
||||
defaultValue: "comments"
|
||||
}
|
||||
]
|
||||
, constr: (state, tags, args) => {
|
||||
const t = Translations.t.notes;
|
||||
return new VariableUiElement(
|
||||
tags.map(tags => tags[args[0]])
|
||||
.map(commentsStr => {
|
||||
const comments:
|
||||
{
|
||||
"date": string,
|
||||
"uid": number,
|
||||
"user": string,
|
||||
"user_url": string,
|
||||
"action": "closed" | "opened" | "reopened" | "commented",
|
||||
"text": string, "html": string
|
||||
}[] = JSON.parse(commentsStr)
|
||||
|
||||
|
||||
return new Combine(comments
|
||||
.filter(c => c.text !== "")
|
||||
.map(c => {
|
||||
let actionIcon: BaseUIElement = undefined;
|
||||
if (c.action === "opened" || c.action === "reopened") {
|
||||
actionIcon = Svg.note_svg()
|
||||
} else if (c.action === "closed") {
|
||||
actionIcon = Svg.resolved_svg()
|
||||
} else {
|
||||
actionIcon = Svg.addSmall_svg()
|
||||
}
|
||||
|
||||
let user: BaseUIElement
|
||||
if (c.user === undefined) {
|
||||
user = t.anonymous
|
||||
} else {
|
||||
user = new Link(c.user, c.user_url ?? "", true)
|
||||
}
|
||||
|
||||
return new Combine([new Combine([
|
||||
actionIcon.SetClass("mr-4 w-6").SetStyle("flex-shrink: 0"),
|
||||
new FixedUiElement(c.html).SetClass("flex flex-col").SetStyle("margin: 0"),
|
||||
]).SetClass("flex"),
|
||||
new Combine([user.SetClass("mr-2"), c.date]).SetClass("flex justify-end subtle")
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
})).SetClass("flex flex-col")
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||
|
||||
|
||||
return specialVisualizations;
|
||||
}
|
||||
|
||||
|
||||
public static HelpMessage() {
|
||||
|
||||
const helpTexts =
|
||||
SpecialVisualizations.specialVisualizations.map(viz => new Combine(
|
||||
[
|
||||
new Title(viz.funcName, 3),
|
||||
viz.docs,
|
||||
viz.args.length > 0 ? new Table(["name", "default", "description"],
|
||||
viz.args.map(arg => {
|
||||
let defaultArg = arg.defaultValue ?? "_undefined_"
|
||||
if (defaultArg == "") {
|
||||
defaultArg = "_empty string_"
|
||||
}
|
||||
return [arg.name, defaultArg, arg.doc];
|
||||
})
|
||||
) : undefined,
|
||||
new Title("Example usage of "+viz.funcName, 4),
|
||||
new FixedUiElement(
|
||||
viz.example ?? "`{" + viz.funcName + "(" + viz.args.map(arg => arg.defaultValue).join(",") + ")}`"
|
||||
).SetClass("literal-code"),
|
||||
|
||||
]
|
||||
));
|
||||
|
||||
return new Combine([
|
||||
new Title("Special tag renderings", 1),
|
||||
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
|
||||
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
|
||||
...helpTexts
|
||||
]
|
||||
).SetClass("flex flex-col");
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue