diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index 6a8462b1b..ef58bb555 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -23,8 +23,6 @@ import * as bike_shops from "../../assets/layers/bike_shop/bike_shop.json" import * as maps from "../../assets/layers/maps/maps.json" import * as information_boards from "../../assets/layers/information_board/information_board.json" import {Utils} from "../../Utils"; -import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; -import {ImageCarouselConstructor} from "../../UI/Image/ImageCarousel"; import State from "../../State"; export class FromJSON { @@ -139,23 +137,15 @@ export class FromJSON { if (typeof json === "string") { switch (json) { - case "picture": { - return new ImageCarouselWithUploadConstructor() - } case "pictures": { - return new ImageCarouselWithUploadConstructor() - } - case "image": { - return new ImageCarouselWithUploadConstructor() + json = "{image_carousel()}{image_upload()}"; + break; } case "images": { - return new ImageCarouselWithUploadConstructor() - } - case "picturesNoUpload": { - return new ImageCarouselConstructor() + json = "{image_carousel()}{image_upload()}"; } } - + console.warn("Possible literal rendering:", json) return new TagRenderingOptions({ freeform: { diff --git a/Customizations/OnlyShowIf.ts b/Customizations/OnlyShowIf.ts index 297c02d58..a9c4a4310 100644 --- a/Customizations/OnlyShowIf.ts +++ b/Customizations/OnlyShowIf.ts @@ -16,9 +16,9 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{ this._embedded = embedded; } - construct(dependencies): TagDependantUIElement { - return new OnlyShowIf(dependencies.tags, - this._embedded.construct(dependencies), + construct(tags: UIEventSource): TagDependantUIElement { + return new OnlyShowIf(tags, + this._embedded.construct(tags), this._tagsFilter); } diff --git a/Customizations/TagRenderingOptions.ts b/Customizations/TagRenderingOptions.ts index 6ad7e3a4d..4081d5eed 100644 --- a/Customizations/TagRenderingOptions.ts +++ b/Customizations/TagRenderingOptions.ts @@ -1,4 +1,4 @@ -import {Dependencies, TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; +import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; import {TagsFilter, TagUtils} from "../Logic/Tags"; import {OnlyShowIfConstructor} from "./OnlyShowIf"; import {UIEventSource} from "../Logic/UIEventSource"; @@ -137,8 +137,8 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[] }) => TagDependantUIElement; - construct(dependencies: Dependencies): TagDependantUIElement { - return TagRenderingOptions.tagRendering(dependencies.tags, this.options); + construct(tags: UIEventSource): TagDependantUIElement { + return TagRenderingOptions.tagRendering(tags, this.options); } IsKnown(properties: any): boolean { diff --git a/Customizations/UIElementConstructor.ts b/Customizations/UIElementConstructor.ts index 25e29b4b1..64b55ae79 100644 --- a/Customizations/UIElementConstructor.ts +++ b/Customizations/UIElementConstructor.ts @@ -2,14 +2,9 @@ import {UIElement} from "../UI/UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; import Translation from "../UI/i18n/Translation"; - -export interface Dependencies { - tags: UIEventSource -} - export interface TagDependantUIElementConstructor { - construct(dependencies: Dependencies): TagDependantUIElement; + construct(tags: UIEventSource): TagDependantUIElement; IsKnown(properties: any): boolean; IsQuestioning(properties: any): boolean; GetContent(tags: any): Translation; diff --git a/InitUiElements.ts b/InitUiElements.ts index c8d5327a1..7c5a2ea87 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -7,7 +7,6 @@ import Combine from "./UI/Base/Combine"; import {UIElement} from "./UI/UIElement"; import {MoreScreen} from "./UI/MoreScreen"; import {FilteredLayer} from "./Logic/FilteredLayer"; -import {FeatureInfoBox} from "./UI/FeatureInfoBox"; import {Basemap} from "./Logic/Leaflet/Basemap"; import State from "./State"; import {WelcomeMessage} from "./UI/WelcomeMessage"; @@ -37,6 +36,7 @@ import {FromJSON} from "./Customizations/JSON/FromJSON"; import {Utils} from "./Utils"; import BackgroundSelector from "./UI/BackgroundSelector"; import AvailableBaseLayers from "./Logic/AvailableBaseLayers"; +import {FeatureInfoBox} from "./UI/Popup/FeatureInfoBox"; export class InitUiElements { diff --git a/Logic/Leaflet/StrayClickHandler.ts b/Logic/Leaflet/StrayClickHandler.ts index b717e97ff..ca7fcdca3 100644 --- a/Logic/Leaflet/StrayClickHandler.ts +++ b/Logic/Leaflet/StrayClickHandler.ts @@ -17,7 +17,7 @@ export class StrayClickHandler { const map = State.state.bm.map; State.state.filteredLayers.data.forEach((filteredLayer) => { filteredLayer.isDisplayed.addCallback(isEnabled => { - if(isEnabled){ + if(isEnabled && self._lastMarker){ // When a layer is activated, we remove the 'last click location' in order to force the user to reclick // This reclick might be at a location where a feature now appeared... map.removeLayer(self._lastMarker); diff --git a/Logic/Osm/OsmImageUploadHandler.ts b/Logic/Osm/OsmImageUploadHandler.ts deleted file mode 100644 index 1bfe7fa92..000000000 --- a/Logic/Osm/OsmImageUploadHandler.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Helps in uplaoding, by generating the rigth title, decription and by adding the tag to the changeset - */ -import {UIEventSource} from "../UIEventSource"; -import {ImageUploadFlow} from "../../UI/ImageUploadFlow"; -import {SlideShow} from "../../UI/SlideShow"; -import State from "../../State"; -import {Tag} from "../Tags"; - -export class OsmImageUploadHandler { - private readonly _tags: UIEventSource; - private readonly _slideShow: SlideShow; - private readonly _preferedLicense: UIEventSource; - - constructor(tags: UIEventSource, - preferedLicense: UIEventSource, - slideShow : SlideShow - ) { - this._slideShow = slideShow; // To move the slideshow (if any) to the last, just added element - this._tags = tags; - this._preferedLicense = preferedLicense; - } - - private generateOptions(license: string) { - const tags = this._tags.data; - const self = this; - license = license ?? "CC0" - const title = tags.name ?? "Unknown area"; - const description = [ - "author:" + State.state.osmConnection.userDetails.data.name, - "license:" + license, - "wikidata:" + tags.wikidata, - "osmid:" + tags.id, - "name:" + tags.name - ].join("\n"); - - const changes = State.state.changes; - return { - title: title, - description: description, - handleURL: (url) => { - - let key = "image"; - if (tags["image"] !== undefined) { - - let freeIndex = 0; - while (tags["image:" + freeIndex] !== undefined) { - freeIndex++; - } - key = "image:" + freeIndex; - } - console.log("Adding image:" + key, url); - changes.addTag(tags.id, new Tag(key, url)); - self._slideShow.MoveTo(-1); // set the last (thus newly added) image) to view - }, - allDone: () => { - } - } - } - - getUI(): ImageUploadFlow { - const self = this; - return new ImageUploadFlow( - this._preferedLicense, - function (license) { - return self.generateOptions(license) - } - ); - } - - -} \ No newline at end of file diff --git a/UI/CustomGenerator/TagRenderingPreview.ts b/UI/CustomGenerator/TagRenderingPreview.ts index e728912a5..8ec0357b0 100644 --- a/UI/CustomGenerator/TagRenderingPreview.ts +++ b/UI/CustomGenerator/TagRenderingPreview.ts @@ -40,7 +40,7 @@ export default class TagRenderingPreview extends UIElement { new VariableUiElement(es.map(tagRenderingConfig => { try { const tr = FromJSON.TagRendering(tagRenderingConfig, "preview") - .construct({tags: self.previewTagValue}); + .construct(self.previewTagValue); return tr.Render(); } catch (e) { return new Combine(["Could not show this tagrendering:", e.message]).Render(); diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 47b6601f0..61303056c 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -1,38 +1,11 @@ import {UIElement} from "../UIElement"; import {ImageSearcher} from "../../Logic/ImageSearcher"; -import {SlideShow} from "../SlideShow"; +import {SlideShow} from "./SlideShow"; import {UIEventSource} from "../../Logic/UIEventSource"; -import { - Dependencies, - TagDependantUIElement, - TagDependantUIElementConstructor -} from "../../Customizations/UIElementConstructor"; -import Translation from "../i18n/Translation"; +import {TagDependantUIElement} from "../../Customizations/UIElementConstructor"; import Combine from "../Base/Combine"; import DeleteImage from "./DeleteImage"; -export class ImageCarouselConstructor implements TagDependantUIElementConstructor { - IsKnown(properties: any): boolean { - return true; - } - - IsQuestioning(properties: any): boolean { - return false; - } - - Priority(): number { - return 0; - } - - construct(dependencies: Dependencies): TagDependantUIElement { - return new ImageCarousel(dependencies.tags); - } - - GetContent(tags: any): Translation { - return new Translation({"en":"Images without upload"}); - } - -} export class ImageCarousel extends TagDependantUIElement { diff --git a/UI/Image/ImageCarouselWithUpload.ts b/UI/Image/ImageCarouselWithUpload.ts index 91bdd6d58..89f40ae75 100644 --- a/UI/Image/ImageCarouselWithUpload.ts +++ b/UI/Image/ImageCarouselWithUpload.ts @@ -1,13 +1,8 @@ -import { - Dependencies, - TagDependantUIElement, - TagDependantUIElementConstructor -} from "../../Customizations/UIElementConstructor"; +import {TagDependantUIElement, TagDependantUIElementConstructor} from "../../Customizations/UIElementConstructor"; import {ImageCarousel} from "./ImageCarousel"; -import {ImageUploadFlow} from "../ImageUploadFlow"; -import {OsmImageUploadHandler} from "../../Logic/Osm/OsmImageUploadHandler"; -import State from "../../State"; +import {ImageUploadFlow} from "./ImageUploadFlow"; import Translation from "../i18n/Translation"; +import {UIEventSource} from "../../Logic/UIEventSource"; export default class ImageCarouselWithUploadConstructor implements TagDependantUIElementConstructor{ @@ -24,20 +19,25 @@ export default class ImageCarouselWithUploadConstructor implements TagDependantU } GetContent(tags: any): Translation { - return new Translation({"en":"Image carousel with uploader"}); + return new Translation({"*": "Image carousel with uploader"}); } } +class OsmImageUploadHandler { + constructor(tags: UIEventSource) { + + } + +} + class ImageCarouselWithUpload extends TagDependantUIElement { private _imageElement: ImageCarousel; private _pictureUploader: ImageUploadFlow; - constructor(dependencies: Dependencies) { - super(dependencies.tags); - const tags = dependencies.tags; + constructor(tags: UIEventSource) { + super(tags); this._imageElement = new ImageCarousel(tags); - const license = State.state.osmConnection.GetPreference( "pictures-license"); - this._pictureUploader = new OsmImageUploadHandler(tags, license, this._imageElement.slideshow).getUI(); + this._pictureUploader = new OsmImageUploadHandler(tags).getUI(); } diff --git a/UI/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts similarity index 60% rename from UI/ImageUploadFlow.ts rename to UI/Image/ImageUploadFlow.ts index dee52fae5..13cff6c19 100644 --- a/UI/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -1,34 +1,27 @@ -import {UIElement} from "./UIElement"; import $ from "jquery" -import {DropDown} from "./Input/DropDown"; -import Translations from "./i18n/Translations"; -import Combine from "./Base/Combine"; -import State from "../State"; -import {UIEventSource} from "../Logic/UIEventSource"; -import {Imgur} from "../Logic/Web/Imgur"; -import {FixedUiElement} from "./Base/FixedUiElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {UIElement} from "../UIElement"; +import State from "../../State"; +import Combine from "../Base/Combine"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {Imgur} from "../../Logic/Web/Imgur"; +import {DropDown} from "../Input/DropDown"; +import {Tag} from "../../Logic/Tags"; +import Translations from "../i18n/Translations"; export class ImageUploadFlow extends UIElement { - private _licensePicker: UIElement; - private _selectedLicence: UIEventSource; - private _isUploading: UIEventSource = new UIEventSource(0) - private _didFail: UIEventSource = new UIEventSource(false); - private _allDone: UIEventSource = new UIEventSource(false); - private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) }; - private _connectButton : UIElement; - - constructor( - preferedLicense: UIEventSource, - uploadOptions: ((license: string) => - { - title: string, - description: string, - handleURL: ((url: string) => void), - allDone: (() => void) - }) - ) { + private readonly _licensePicker: UIElement; + private readonly _tags: UIEventSource; + private readonly _selectedLicence: UIEventSource; + private readonly _isUploading: UIEventSource = new UIEventSource(0) + private readonly _didFail: UIEventSource = new UIEventSource(false); + private readonly _allDone: UIEventSource = new UIEventSource(false); + private readonly _connectButton: UIElement; + + constructor(tags: UIEventSource) { super(State.state.osmConnection.userDetails); - this._uploadOptions = uploadOptions; + this._tags = tags; + this.ListenTo(this._isUploading); this.ListenTo(this._didFail); this.ListenTo(this._allDone); @@ -39,7 +32,7 @@ export class ImageUploadFlow extends UIElement { {value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs}, {value: "CC-BY 4.0", shown: Translations.t.image.ccb} ], - preferedLicense + State.state.osmConnection.GetPreference("pictures-license") ); licensePicker.SetStyle("float:left"); @@ -54,7 +47,6 @@ export class ImageUploadFlow extends UIElement { } - InnerRender(): string { const t = Translations.t.image; @@ -126,9 +118,9 @@ export class ImageUploadFlow extends UIElement { `" + - actualInputElement+ + actualInputElement + ""; - + return new Combine([ form, extraInfo @@ -136,6 +128,65 @@ export class ImageUploadFlow extends UIElement { .Render(); } + + private handleSuccessfulUpload(url) { + const tags = this._tags.data; + let key = "image"; + if (tags["image"] !== undefined) { + + let freeIndex = 0; + while (tags["image:" + freeIndex] !== undefined) { + freeIndex++; + } + key = "image:" + freeIndex; + } + console.log("Adding image:" + key, url); + State.state.changes.addTag(tags.id, new Tag(key, url)); + } + + private handleFiles(files) { + this._isUploading.setData(files.length); + this._allDone.setData(false); + + if (this._selectedLicence.data === undefined) { + this._selectedLicence.setData("CC0"); + } + + + const tags = this._tags.data; + const title = tags.name ?? "Unknown area"; + const description = [ + "author:" + State.state.osmConnection.userDetails.data.name, + "license:" + (this._selectedLicence.data ?? "CC0"), + "wikidata:" + tags.wikidata, + "osmid:" + tags.id, + "name:" + tags.name + ].join("\n"); + + const self = this; + + Imgur.uploadMultiple(title, + description, + files, + function (url) { + console.log("File saved at", url); + self._isUploading.setData(self._isUploading.data - 1); + self.handleSuccessfulUpload(url); + }, + function () { + console.log("All uploads completed"); + self._allDone.setData(true); + }, + function (failReason) { + console.log("Upload failed due to ", failReason) + // No need to call something from the options -> we handle this here + self._didFail.setData(true); + self._isUploading.data--; + self._isUploading.ping(); + }, 0 + ) + } + InnerUpdate(htmlElement: HTMLElement) { super.InnerUpdate(htmlElement); const user = State.state.osmConnection.userDetails.data; @@ -147,34 +198,7 @@ export class ImageUploadFlow extends UIElement { function submitHandler() { const files = $(selector).prop('files'); - self._isUploading.setData(files.length); - self._allDone.setData(false); - - if(self._selectedLicence.data === undefined){ - self._selectedLicence.setData("CC0"); - } - - const opts = self._uploadOptions(self._selectedLicence.data); - - Imgur.uploadMultiple(opts.title, opts.description, files, - function (url) { - console.log("File saved at", url); - self._isUploading.setData(self._isUploading.data - 1); - opts.handleURL(url); - }, - function () { - console.log("All uploads completed"); - self._allDone.setData(true); - opts.allDone(); - }, - function(failReason) { - console.log("Upload failed due to ", failReason) - // No need to call something from the options -> we handle this here - self._didFail.setData(true); - self._isUploading.data--; - self._isUploading.ping(); - },0 - ) + self.handleFiles(files) } if (selector != null && form != null) { diff --git a/UI/SlideShow.ts b/UI/Image/SlideShow.ts similarity index 85% rename from UI/SlideShow.ts rename to UI/Image/SlideShow.ts index cc18476cc..b9ff6f45e 100644 --- a/UI/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -1,6 +1,6 @@ -import {UIElement} from "./UIElement"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import {UIEventSource} from "../Logic/UIEventSource"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {UIElement} from "../UIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; export class SlideShow extends UIElement { @@ -15,6 +15,12 @@ export class SlideShow extends UIElement { super(embeddedElements); this._embeddedElements = embeddedElements; this.ListenTo(this._currentSlide); + this._embeddedElements + .stabilized(1000) + .addCallback(embedded => { + // Always move to the last image - but at most once per second + this._currentSlide.setData(this._embeddedElements.data.length - 1); + }); const self = this; this._prev = new FixedUiElement("
" + diff --git a/UI/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts similarity index 79% rename from UI/FeatureInfoBox.ts rename to UI/Popup/FeatureInfoBox.ts index 14427e93c..48eb4a187 100644 --- a/UI/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -1,16 +1,15 @@ -import {UIElement} from "./UIElement"; -import {VerticalCombine} from "./Base/VerticalCombine"; -import {OsmLink} from "../Customizations/Questions/OsmLink"; -import {WikipediaLink} from "../Customizations/Questions/WikipediaLink"; -import {And} from "../Logic/Tags"; -import {TagDependantUIElement, TagDependantUIElementConstructor} from "../Customizations/UIElementConstructor"; -import Translations from "./i18n/Translations"; -import {Changes} from "../Logic/Osm/Changes"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import State from "../State"; -import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; -import {UIEventSource} from "../Logic/UIEventSource"; -import Combine from "./Base/Combine"; +import {VerticalCombine} from "../Base/VerticalCombine"; +import {UIElement} from "../UIElement"; +import Combine from "../Base/Combine"; +import {WikipediaLink} from "../../Customizations/Questions/WikipediaLink"; +import {OsmLink} from "../../Customizations/Questions/OsmLink"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions"; +import State from "../../State"; +import {And} from "../../Logic/Tags"; +import {TagDependantUIElement, TagDependantUIElementConstructor} from "../../Customizations/UIElementConstructor"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import Translations from "../i18n/Translations"; export class FeatureInfoBox extends UIElement { @@ -22,7 +21,6 @@ export class FeatureInfoBox extends UIElement { * The tags, wrapped in a global event source */ private readonly _tagsES: UIEventSource; - private readonly _changes: Changes; private readonly _title: UIElement; private readonly _infoboxes: TagDependantUIElement[]; @@ -37,10 +35,13 @@ export class FeatureInfoBox extends UIElement { ) { super(tagsES); this._feature = feature; - this._tagsES = tagsES; + this._tagsES = tagsES + if(tagsES === undefined){ + throw "No Tags event source given" + } this.ListenTo(State.state.osmConnection.userDetails); this.SetClass("featureinfobox"); - const deps = {tags: this._tagsES, changes: this._changes} + const tags = this._tagsES; this._infoboxes = []; elementsToShow = elementsToShow ?? [] @@ -48,13 +49,13 @@ export class FeatureInfoBox extends UIElement { const self = this; for (const tagRenderingOption of elementsToShow) { self._infoboxes.push( - tagRenderingOption.construct(deps)); + tagRenderingOption.construct(tags)); } function initTags() { self._infoboxes.splice(0, self._infoboxes.length); for (const tagRenderingOption of elementsToShow) { self._infoboxes.push( - tagRenderingOption.construct(deps)); + tagRenderingOption.construct(tags)); } self.Update(); } @@ -74,7 +75,7 @@ export class FeatureInfoBox extends UIElement { } else if (title instanceof UIElement) { renderedTitle = title; } else { - renderedTitle = title.construct(deps); + renderedTitle = title.construct(tags); } @@ -83,10 +84,10 @@ export class FeatureInfoBox extends UIElement { .SetClass("title-font") const osmLink = new OsmLink() - .construct(deps) + .construct(tags) .SetStyle("width: 24px; display:block;") const wikipedialink = new WikipediaLink() - .construct(deps) + .construct(tags) .SetStyle("width: 24px; display:block;") this._title = new Combine([ diff --git a/UI/SaveButton.ts b/UI/Popup/SaveButton.ts similarity index 82% rename from UI/SaveButton.ts rename to UI/Popup/SaveButton.ts index a9b9b7e3f..ae4bfeb75 100644 --- a/UI/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -1,6 +1,6 @@ -import {UIElement} from "./UIElement"; -import Translations from "./i18n/Translations"; -import {UIEventSource} from "../Logic/UIEventSource"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {UIElement} from "../UIElement"; +import Translations from "../i18n/Translations"; export class SaveButton extends UIElement { private _value: UIEventSource; diff --git a/UI/TagRendering.ts b/UI/Popup/TagRendering.ts similarity index 83% rename from UI/TagRendering.ts rename to UI/Popup/TagRendering.ts index f29e3ce7b..30947630b 100644 --- a/UI/TagRendering.ts +++ b/UI/Popup/TagRendering.ts @@ -1,23 +1,23 @@ -import {UIEventSource} from "../Logic/UIEventSource"; -import {And, Tag, TagsFilter, TagUtils} from "../Logic/Tags"; -import Translations from "../UI/i18n/Translations"; -import Locale from "../UI/i18n/Locale"; -import Translation from "../UI/i18n/Translation"; -import Combine from "../UI/Base/Combine"; -import {TagDependantUIElement} from "../Customizations/UIElementConstructor"; -import {UIElement} from "./UIElement"; -import {VariableUiElement} from "./Base/VariableUIElement"; -import InputElementMap from "./Input/InputElementMap"; -import {InputElement} from "./Input/InputElement"; +import {UIElement} from "../UIElement"; +import Translation from "../i18n/Translation"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import InputElementMap from "../Input/InputElementMap"; +import CheckBoxes from "../Input/Checkboxes"; +import Combine from "../Base/Combine"; +import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags"; +import {InputElement} from "../Input/InputElement"; import {SaveButton} from "./SaveButton"; -import {RadioButton} from "./Input/RadioButton"; -import {FixedInputElement} from "./Input/FixedInputElement"; -import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import ValidatedTextField from "./Input/ValidatedTextField"; -import CheckBoxes from "./Input/Checkboxes"; -import State from "../State"; -import SpecialVisualizations from "./SpecialVisualizations"; +import {RadioButton} from "../Input/RadioButton"; +import {FixedInputElement} from "../Input/FixedInputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import ValidatedTextField from "../Input/ValidatedTextField"; +import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions"; +import State from "../../State"; +import {SubstitutedTranslation} from "../SpecialVisualizations"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import Translations from "../i18n/Translations"; +import {TagDependantUIElement} from "../../Customizations/UIElementConstructor"; +import Locale from "../i18n/Locale"; export class TagRendering extends UIElement implements TagDependantUIElement { @@ -25,7 +25,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { private readonly _question: string | Translation; private readonly _mapping: { k: TagsFilter, txt: string | Translation, priority?: number }[]; - private currentTags: UIEventSource; + private readonly currentTags: UIEventSource; private readonly _freeform: { @@ -50,7 +50,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { private readonly _questionSkipped: UIEventSource = new UIEventSource(false); private readonly _editMode: UIEventSource = new UIEventSource(false); - + static injectFunction() { // This is a workaround as not to import tagrendering into TagREnderingOptions TagRenderingOptions.tagRendering = (tags, options) => new TagRendering(tags, options); @@ -71,15 +71,17 @@ export class TagRendering extends UIElement implements TagDependantUIElement { mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[] }) { super(tags); + if (tags === undefined) { + throw "No tags given for a tagrendering..." + } this.ListenTo(Locale.language); this.ListenTo(this._editMode); this.ListenTo(this._questionSkipped); this.ListenTo(State.state?.osmConnection?.userDetails); const self = this; - - this.currentTags = this._source.map(tags => - { + + this.currentTags = this._source.map(tags => { if (options.tagsPreprocessor === undefined) { return tags; @@ -90,7 +92,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { newTags[k] = tags[k]; } // ... in order to safely edit them here - options.tagsPreprocessor(newTags); + options.tagsPreprocessor(newTags); return newTags; } ); @@ -98,7 +100,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { if (options.question !== undefined) { this._question = options.question; } - + this._mapping = []; this._freeform = options.freeform; @@ -138,7 +140,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { if (csCount < State.userJourney.tagsVisibleAt) { return ""; } - + if (tags === undefined) { return Translations.t.general.noTagsSelected.SetClass("subtle").Render(); } @@ -186,6 +188,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { }, [Locale.language]); // And at last, set up the skip button this._skipButton = new VariableUiElement(cancelContents).onClick(cancel); + } @@ -295,14 +298,14 @@ export class TagRendering extends UIElement implements TagDependantUIElement { (t0, t1) => t0.isEquivalent(t1) ); } - + let txt = this.ApplyTemplate(mapping.txt); - if(txt.Render().indexOf("= 0){ + if (txt.Render().indexOf("= 0) { txt.SetClass("question-option-with-border"); } const inputEl = new FixedInputElement(txt, mapping.k, (t0, t1) => t1.isEquivalent(t0)); - + return inputEl; } @@ -324,17 +327,17 @@ export class TagRendering extends UIElement implements TagDependantUIElement { let type = prepost[1]; let isTextArea = false; - if(type === "text"){ + if (type === "text") { isTextArea = true; type = "string"; } - - if(ValidatedTextField.AllTypes[type] === undefined){ - console.error("Type:",type, ValidatedTextField.AllTypes) - throw "Unkown type: "+type; + + if (ValidatedTextField.AllTypes[type] === undefined) { + console.error("Type:", type, ValidatedTextField.AllTypes) + throw "Unkown type: " + type; } - + const pickString = (string: any) => { if (string === "" || string === undefined) { @@ -444,47 +447,43 @@ export class TagRendering extends UIElement implements TagDependantUIElement { return new FixedUiElement(""); } - InnerRender(): string { - if (this.IsQuestioning() + private CreateComponent(): UIElement { + + + if (this.IsQuestioning() && (State.state !== undefined) // If State.state is undefined, we are testing/custom theme building -> show regular save && !State.state.osmConnection.userDetails.data.loggedIn) { - + const question = this.ApplyTemplate(this._question).SetClass('question-text'); - return "
" + - new Combine([ - question, - "
", - this._questionElement, - this._friendlyLogin, - ]).Render() + "
"; + return new Combine(["
", + question, + "
", + this._questionElement, + this._friendlyLogin, "
" + ]); } if (this.IsQuestioning() || this._editMode.data) { // Not yet known or questioning, we have to ask a question - - return "
" + - new Combine([ - "", - this.ApplyTemplate(this._question), - "", - "
", - "
", this._questionElement , "
", - this._skipButton, - this._saveButton, - "
", - this._appliedTags - ]).Render() + - "
" + return new Combine([ + this.ApplyTemplate(this._question).SetStyle('question-text'), + "
", + "
", this._questionElement, "
", + this._skipButton, + this._saveButton, + "
", + this._appliedTags + ]).SetClass('question'); } if (this.IsKnown()) { const answer = this.RenderAnswer(); - + if (answer.IsEmpty()) { - return ""; + return new FixedUiElement(""); } @@ -499,37 +498,35 @@ export class TagRendering extends UIElement implements TagDependantUIElement { return new Combine([ answer, this._editButton]) - .SetStyle(answerStyle) - .Render(); + .SetStyle(answerStyle); } - return answer.SetStyle(answerStyle).Render(); + return answer.SetStyle(answerStyle); } - return ""; + return new FixedUiElement(""); } + InnerRender(): string { + return this.CreateComponent().Render(); + } + + + protected InnerUpdate(htmlElement: HTMLElement) { + this._editButton.Update(); + } + + private answerCache = {} private ApplyTemplate(template: string | Translation): UIElement { - if (template === undefined || template === null) { - return undefined; + const tr = Translations.WT(template); + if (this.answerCache[tr.id]) { + return this.answerCache[tr.id]; } - - const knownSpecials : {funcName: string, constr: ((arg: string) => UIElement)}[]= SpecialVisualizations.specialVisualizations.map( - special => ({ - funcName: special.funcName, - constr: arg => special.constr(this.currentTags, arg.split(",")) - }) - ) - - return new VariableUiElement(this.currentTags.map(tags => { - return Translations.WT(template) - .Subs(tags) - .EvaluateSpecialComponents(knownSpecials) - .InnerRender() - })).ListenTo(Locale.language); + // We have to cache these elemnts, otherwise it is to slow + const el = new SubstitutedTranslation(tr, this.currentTags); + this.answerCache[tr.id] = el; + return el; } - - -} \ No newline at end of file +} diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index c7ccae9a4..9b67045d6 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -3,6 +3,79 @@ import OpeningHoursVisualization from "./OhVisualization"; import {UIEventSource} from "../Logic/UIEventSource"; import {VariableUiElement} from "./Base/VariableUIElement"; import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; +import {ImageCarousel} from "./Image/ImageCarousel"; +import Translation from "./i18n/Translation"; +import Combine from "./Base/Combine"; +import {FixedUiElement} from "./Base/FixedUiElement"; +import Locale from "../UI/i18n/Locale"; +import {ImageUploadFlow} from "./Image/ImageUploadFlow"; + +export class SubstitutedTranslation extends UIElement { + private readonly tags: UIEventSource; + private readonly translation: Translation; + private content: UIElement; + + constructor( + translation: Translation, + tags: UIEventSource) { + super(tags); + this.translation = translation; + this.tags = tags; + const self = this; + Locale.language.addCallbackAndRun(() => { + self.content = self.CreateContent(); + self.Update(); + }) + } + + InnerRender(): string { + return this.content.Render(); + } + + + CreateContent(): UIElement { + let txt = this.translation?.txt; + if (txt === undefined) { + return new FixedUiElement("") + } + const tags = this.tags.data; + for (const key in tags) { + // Poor mans replace all + txt = txt.split("{" + key + "}").join(tags[key]); + } + + + return new Combine(this.EvaluateSpecialComponents(txt)); + } + + public EvaluateSpecialComponents(template: string): UIElement[] { + + for (const knownSpecial of SpecialVisualizations.specialVisualizations) { + + // NOte: the '.*?' in the regex reads as 'any character, but in a non-greedy way' + const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)}(.*)`); + if (matched != null) { + + // We found a special component that should be brought to live + const partBefore = this.EvaluateSpecialComponents(matched[1]); + const argument = matched[2]; + const partAfter = this.EvaluateSpecialComponents(matched[3]); + try { + const args = argument.trim().split(",").map(str => str.trim()); + const element = knownSpecial.constr(this.tags, args); + return [...partBefore, element, ...partAfter] + } catch (e) { + console.error(e); + return [...partBefore, ...partAfter] + } + } + } + + // IF we end up here, no changes have to be made + return [new FixedUiElement(template)]; + } + +} export default class SpecialVisualizations { @@ -13,18 +86,55 @@ export default class SpecialVisualizations { args: { name: string, defaultValue?: string, doc: string }[] }[] = - [{ - funcName: "opening_hours_table", - docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.", - args: [{name: "key", defaultValue: "opening_hours", doc: "The tag from which the table is constructed"}], - constr: (tagSource: UIEventSource, args) => { - let keyname = args[0]; - if (keyname === undefined || keyname === "") { - keyname = keyname ?? "opening_hours" + [ + { + funcName: "image_carousel", + docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", + args: [{ + name: "image tag(s)", + defaultValue: "image,image:*,wikidata,wikipedia,wikimedia_commons", + doc: "Image tag(s) where images are searched" + }], + constr: (tags, args) => { + if (args.length > 0) { + console.error("TODO HANDLE THESE ARGS") // TODO FIXME + + } + return new ImageCarousel(tags); } - return new OpeningHoursVisualization(tagSource, keyname) - } - }, + }, + + { + funcName: "image_upload", + docs: "Creates a button where a user can upload an image to IMGUR", + args: [{ + doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", + defaultValue: "image", name: "image-key" + }], + constr: (tags, args) => { + if (args.length > 0) { + console.error("TODO HANDLE THESE ARGS") // TODO FIXME + + } + return new ImageUploadFlow(tags) + } + }, + { + funcName: "opening_hours_table", + docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.", + args: [{ + name: "key", + defaultValue: "opening_hours", + doc: "The tag from which the table is constructed" + }], + constr: (tagSource: UIEventSource, args) => { + let keyname = args[0]; + if (keyname === undefined || keyname === "") { + keyname = keyname ?? "opening_hours" + } + return new OpeningHoursVisualization(tagSource, keyname) + } + }, { funcName: "live", diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index 2aab69562..cc2af6ca6 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -40,39 +40,7 @@ export default class Translation extends UIElement { } - public EvaluateSpecialComponents(knownSpecials: { funcName: string, constr: ((call: string) => UIElement) }[]): UIElement { - const newTranslations = {}; - for (const lang in this.translations) { - let template: string = this.translations[lang]; - - for (const knownSpecial of knownSpecials) { - - do { - const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*)\\)}(.*)`); - if (matched === null) { - break; - } - const partBefore = matched[1]; - const argument = matched[2]; - const partAfter = matched[3]; - - try { - - const element = knownSpecial.constr(argument).Render(); - template = partBefore + element + partAfter; - } catch (e) { - console.error(e); - template = partBefore + partAfter; - } - - } while (true); - - - } - newTranslations[lang] = template; - } - return new Translation(newTranslations); - } + get txt(): string { diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index b8afffc3b..604a920ca 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -968,12 +968,18 @@ export default class Translations { return s; } + private static wtcache = {} public static WT(s: string | Translation): Translation { if(s === undefined){ return undefined; } if (typeof (s) === "string") { - return new Translation({en: s}); + if(Translations.wtcache[s]){ + return Translations.wtcache[s]; + } + const tr = new Translation({en: s}); + Translations.wtcache[s]= tr; + return tr; } if (s instanceof Translation) { return s; diff --git a/customGenerator.ts b/customGenerator.ts index 74dd29084..1d28b4693 100644 --- a/customGenerator.ts +++ b/customGenerator.ts @@ -1,10 +1,10 @@ import {UIEventSource} from "./Logic/UIEventSource"; import {GenerateEmpty} from "./UI/CustomGenerator/GenerateEmpty"; -import {TagRendering} from "./UI/TagRendering"; import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; import {OsmConnection} from "./Logic/Osm/OsmConnection"; import CustomGeneratorPanel from "./UI/CustomGenerator/CustomGeneratorPanel"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; +import {TagRendering} from "./UI/Popup/TagRendering"; let layout = GenerateEmpty.createEmptyLayout(); if (window.location.hash.length > 10) { diff --git a/index.css b/index.css index 868347f7d..9d0e87ff6 100644 --- a/index.css +++ b/index.css @@ -367,6 +367,7 @@ body { } .question { + display: block; margin-top: 1em; background-color: #e5f5ff; padding: 1em; diff --git a/index.ts b/index.ts index 70c8f6c1f..015f88b00 100644 --- a/index.ts +++ b/index.ts @@ -6,7 +6,7 @@ import {QueryParameters} from "./Logic/Web/QueryParameters"; import {UIEventSource} from "./Logic/UIEventSource"; import * as $ from "jquery"; import {FromJSON} from "./Customizations/JSON/FromJSON"; -import {TagRendering} from "./UI/TagRendering"; +import {TagRendering} from "./UI/Popup/TagRendering"; TagRendering.injectFunction();