Optimize rendering

This commit is contained in:
Pieter Vander Vennet 2020-10-14 12:15:09 +02:00
parent 8babafaadb
commit a721d3137a
21 changed files with 361 additions and 362 deletions

View file

@ -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 maps from "../../assets/layers/maps/maps.json"
import * as information_boards from "../../assets/layers/information_board/information_board.json" import * as information_boards from "../../assets/layers/information_board/information_board.json"
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload";
import {ImageCarouselConstructor} from "../../UI/Image/ImageCarousel";
import State from "../../State"; import State from "../../State";
export class FromJSON { export class FromJSON {
@ -139,23 +137,15 @@ export class FromJSON {
if (typeof json === "string") { if (typeof json === "string") {
switch (json) { switch (json) {
case "picture": {
return new ImageCarouselWithUploadConstructor()
}
case "pictures": { case "pictures": {
return new ImageCarouselWithUploadConstructor() json = "{image_carousel()}{image_upload()}";
} break;
case "image": {
return new ImageCarouselWithUploadConstructor()
} }
case "images": { case "images": {
return new ImageCarouselWithUploadConstructor() json = "{image_carousel()}{image_upload()}";
}
case "picturesNoUpload": {
return new ImageCarouselConstructor()
} }
} }
console.warn("Possible literal rendering:", json)
return new TagRenderingOptions({ return new TagRenderingOptions({
freeform: { freeform: {

View file

@ -16,9 +16,9 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
this._embedded = embedded; this._embedded = embedded;
} }
construct(dependencies): TagDependantUIElement { construct(tags: UIEventSource<any>): TagDependantUIElement {
return new OnlyShowIf(dependencies.tags, return new OnlyShowIf(tags,
this._embedded.construct(dependencies), this._embedded.construct(tags),
this._tagsFilter); this._tagsFilter);
} }

View file

@ -1,4 +1,4 @@
import {Dependencies, TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagsFilter, TagUtils} from "../Logic/Tags"; import {TagsFilter, TagUtils} from "../Logic/Tags";
import {OnlyShowIfConstructor} from "./OnlyShowIf"; import {OnlyShowIfConstructor} from "./OnlyShowIf";
import {UIEventSource} from "../Logic/UIEventSource"; 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 }[] mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[]
}) => TagDependantUIElement; }) => TagDependantUIElement;
construct(dependencies: Dependencies): TagDependantUIElement { construct(tags: UIEventSource<any>): TagDependantUIElement {
return TagRenderingOptions.tagRendering(dependencies.tags, this.options); return TagRenderingOptions.tagRendering(tags, this.options);
} }
IsKnown(properties: any): boolean { IsKnown(properties: any): boolean {

View file

@ -2,14 +2,9 @@ import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation"; import Translation from "../UI/i18n/Translation";
export interface Dependencies {
tags: UIEventSource<any>
}
export interface TagDependantUIElementConstructor { export interface TagDependantUIElementConstructor {
construct(dependencies: Dependencies): TagDependantUIElement; construct(tags: UIEventSource<any>): TagDependantUIElement;
IsKnown(properties: any): boolean; IsKnown(properties: any): boolean;
IsQuestioning(properties: any): boolean; IsQuestioning(properties: any): boolean;
GetContent(tags: any): Translation; GetContent(tags: any): Translation;

View file

@ -7,7 +7,6 @@ import Combine from "./UI/Base/Combine";
import {UIElement} from "./UI/UIElement"; import {UIElement} from "./UI/UIElement";
import {MoreScreen} from "./UI/MoreScreen"; import {MoreScreen} from "./UI/MoreScreen";
import {FilteredLayer} from "./Logic/FilteredLayer"; import {FilteredLayer} from "./Logic/FilteredLayer";
import {FeatureInfoBox} from "./UI/FeatureInfoBox";
import {Basemap} from "./Logic/Leaflet/Basemap"; import {Basemap} from "./Logic/Leaflet/Basemap";
import State from "./State"; import State from "./State";
import {WelcomeMessage} from "./UI/WelcomeMessage"; import {WelcomeMessage} from "./UI/WelcomeMessage";
@ -37,6 +36,7 @@ import {FromJSON} from "./Customizations/JSON/FromJSON";
import {Utils} from "./Utils"; import {Utils} from "./Utils";
import BackgroundSelector from "./UI/BackgroundSelector"; import BackgroundSelector from "./UI/BackgroundSelector";
import AvailableBaseLayers from "./Logic/AvailableBaseLayers"; import AvailableBaseLayers from "./Logic/AvailableBaseLayers";
import {FeatureInfoBox} from "./UI/Popup/FeatureInfoBox";
export class InitUiElements { export class InitUiElements {

View file

@ -17,7 +17,7 @@ export class StrayClickHandler {
const map = State.state.bm.map; const map = State.state.bm.map;
State.state.filteredLayers.data.forEach((filteredLayer) => { State.state.filteredLayers.data.forEach((filteredLayer) => {
filteredLayer.isDisplayed.addCallback(isEnabled => { 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 // 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... // This reclick might be at a location where a feature now appeared...
map.removeLayer(self._lastMarker); map.removeLayer(self._lastMarker);

View file

@ -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<any>;
private readonly _slideShow: SlideShow;
private readonly _preferedLicense: UIEventSource<string>;
constructor(tags: UIEventSource<any>,
preferedLicense: UIEventSource<string>,
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)
}
);
}
}

View file

@ -40,7 +40,7 @@ export default class TagRenderingPreview extends UIElement {
new VariableUiElement(es.map(tagRenderingConfig => { new VariableUiElement(es.map(tagRenderingConfig => {
try { try {
const tr = FromJSON.TagRendering(tagRenderingConfig, "preview") const tr = FromJSON.TagRendering(tagRenderingConfig, "preview")
.construct({tags: self.previewTagValue}); .construct(self.previewTagValue);
return tr.Render(); return tr.Render();
} catch (e) { } catch (e) {
return new Combine(["Could not show this tagrendering:", e.message]).Render(); return new Combine(["Could not show this tagrendering:", e.message]).Render();

View file

@ -1,38 +1,11 @@
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import {ImageSearcher} from "../../Logic/ImageSearcher"; import {ImageSearcher} from "../../Logic/ImageSearcher";
import {SlideShow} from "../SlideShow"; import {SlideShow} from "./SlideShow";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import { import {TagDependantUIElement} from "../../Customizations/UIElementConstructor";
Dependencies,
TagDependantUIElement,
TagDependantUIElementConstructor
} from "../../Customizations/UIElementConstructor";
import Translation from "../i18n/Translation";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import DeleteImage from "./DeleteImage"; 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 { export class ImageCarousel extends TagDependantUIElement {

View file

@ -1,13 +1,8 @@
import { import {TagDependantUIElement, TagDependantUIElementConstructor} from "../../Customizations/UIElementConstructor";
Dependencies,
TagDependantUIElement,
TagDependantUIElementConstructor
} from "../../Customizations/UIElementConstructor";
import {ImageCarousel} from "./ImageCarousel"; import {ImageCarousel} from "./ImageCarousel";
import {ImageUploadFlow} from "../ImageUploadFlow"; import {ImageUploadFlow} from "./ImageUploadFlow";
import {OsmImageUploadHandler} from "../../Logic/Osm/OsmImageUploadHandler";
import State from "../../State";
import Translation from "../i18n/Translation"; import Translation from "../i18n/Translation";
import {UIEventSource} from "../../Logic/UIEventSource";
export default class ImageCarouselWithUploadConstructor implements TagDependantUIElementConstructor{ export default class ImageCarouselWithUploadConstructor implements TagDependantUIElementConstructor{
@ -24,20 +19,25 @@ export default class ImageCarouselWithUploadConstructor implements TagDependantU
} }
GetContent(tags: any): Translation { GetContent(tags: any): Translation {
return new Translation({"en":"Image carousel with uploader"}); return new Translation({"*": "Image carousel with uploader"});
} }
} }
class OsmImageUploadHandler {
constructor(tags: UIEventSource<any>) {
}
}
class ImageCarouselWithUpload extends TagDependantUIElement { class ImageCarouselWithUpload extends TagDependantUIElement {
private _imageElement: ImageCarousel; private _imageElement: ImageCarousel;
private _pictureUploader: ImageUploadFlow; private _pictureUploader: ImageUploadFlow;
constructor(dependencies: Dependencies) { constructor(tags: UIEventSource<any>) {
super(dependencies.tags); super(tags);
const tags = dependencies.tags;
this._imageElement = new ImageCarousel(tags); this._imageElement = new ImageCarousel(tags);
const license = State.state.osmConnection.GetPreference( "pictures-license"); this._pictureUploader = new OsmImageUploadHandler(tags).getUI();
this._pictureUploader = new OsmImageUploadHandler(tags, license, this._imageElement.slideshow).getUI();
} }

View file

@ -1,34 +1,27 @@
import {UIElement} from "./UIElement";
import $ from "jquery" import $ from "jquery"
import {DropDown} from "./Input/DropDown"; import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "./i18n/Translations"; import {UIElement} from "../UIElement";
import Combine from "./Base/Combine"; import State from "../../State";
import State from "../State"; import Combine from "../Base/Combine";
import {UIEventSource} from "../Logic/UIEventSource"; import {FixedUiElement} from "../Base/FixedUiElement";
import {Imgur} from "../Logic/Web/Imgur"; import {Imgur} from "../../Logic/Web/Imgur";
import {FixedUiElement} from "./Base/FixedUiElement"; import {DropDown} from "../Input/DropDown";
import {Tag} from "../../Logic/Tags";
import Translations from "../i18n/Translations";
export class ImageUploadFlow extends UIElement { export class ImageUploadFlow extends UIElement {
private _licensePicker: UIElement; private readonly _licensePicker: UIElement;
private _selectedLicence: UIEventSource<string>; private readonly _tags: UIEventSource<any>;
private _isUploading: UIEventSource<number> = new UIEventSource<number>(0) private readonly _selectedLicence: UIEventSource<string>;
private _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false); private readonly _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
private _allDone: UIEventSource<boolean> = new UIEventSource<boolean>(false); private readonly _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) }; private readonly _allDone: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _connectButton : UIElement; private readonly _connectButton: UIElement;
constructor( constructor(tags: UIEventSource<any>) {
preferedLicense: UIEventSource<string>,
uploadOptions: ((license: string) =>
{
title: string,
description: string,
handleURL: ((url: string) => void),
allDone: (() => void)
})
) {
super(State.state.osmConnection.userDetails); super(State.state.osmConnection.userDetails);
this._uploadOptions = uploadOptions; this._tags = tags;
this.ListenTo(this._isUploading); this.ListenTo(this._isUploading);
this.ListenTo(this._didFail); this.ListenTo(this._didFail);
this.ListenTo(this._allDone); 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-SA 4.0", shown: Translations.t.image.ccbs},
{value: "CC-BY 4.0", shown: Translations.t.image.ccb} {value: "CC-BY 4.0", shown: Translations.t.image.ccb}
], ],
preferedLicense State.state.osmConnection.GetPreference("pictures-license")
); );
licensePicker.SetStyle("float:left"); licensePicker.SetStyle("float:left");
@ -54,7 +47,6 @@ export class ImageUploadFlow extends UIElement {
} }
InnerRender(): string { InnerRender(): string {
const t = Translations.t.image; const t = Translations.t.image;
@ -136,6 +128,65 @@ export class ImageUploadFlow extends UIElement {
.Render(); .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) { InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement); super.InnerUpdate(htmlElement);
const user = State.state.osmConnection.userDetails.data; const user = State.state.osmConnection.userDetails.data;
@ -147,34 +198,7 @@ export class ImageUploadFlow extends UIElement {
function submitHandler() { function submitHandler() {
const files = $(selector).prop('files'); const files = $(selector).prop('files');
self._isUploading.setData(files.length); self.handleFiles(files)
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
)
} }
if (selector != null && form != null) { if (selector != null && form != null) {

View file

@ -1,6 +1,6 @@
import {UIElement} from "./UIElement"; import {UIEventSource} from "../../Logic/UIEventSource";
import {FixedUiElement} from "./Base/FixedUiElement"; import {UIElement} from "../UIElement";
import {UIEventSource} from "../Logic/UIEventSource"; import {FixedUiElement} from "../Base/FixedUiElement";
export class SlideShow extends UIElement { export class SlideShow extends UIElement {
@ -15,6 +15,12 @@ export class SlideShow extends UIElement {
super(embeddedElements); super(embeddedElements);
this._embeddedElements = embeddedElements; this._embeddedElements = embeddedElements;
this.ListenTo(this._currentSlide); 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; const self = this;
this._prev = new FixedUiElement("<div class='prev-button'>" + this._prev = new FixedUiElement("<div class='prev-button'>" +

View file

@ -1,16 +1,15 @@
import {UIElement} from "./UIElement"; import {VerticalCombine} from "../Base/VerticalCombine";
import {VerticalCombine} from "./Base/VerticalCombine"; import {UIElement} from "../UIElement";
import {OsmLink} from "../Customizations/Questions/OsmLink"; import Combine from "../Base/Combine";
import {WikipediaLink} from "../Customizations/Questions/WikipediaLink"; import {WikipediaLink} from "../../Customizations/Questions/WikipediaLink";
import {And} from "../Logic/Tags"; import {OsmLink} from "../../Customizations/Questions/OsmLink";
import {TagDependantUIElement, TagDependantUIElementConstructor} from "../Customizations/UIElementConstructor"; import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "./i18n/Translations"; import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions";
import {Changes} from "../Logic/Osm/Changes"; import State from "../../State";
import {FixedUiElement} from "./Base/FixedUiElement"; import {And} from "../../Logic/Tags";
import State from "../State"; import {TagDependantUIElement, TagDependantUIElementConstructor} from "../../Customizations/UIElementConstructor";
import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; import {FixedUiElement} from "../Base/FixedUiElement";
import {UIEventSource} from "../Logic/UIEventSource"; import Translations from "../i18n/Translations";
import Combine from "./Base/Combine";
export class FeatureInfoBox extends UIElement { export class FeatureInfoBox extends UIElement {
@ -22,7 +21,6 @@ export class FeatureInfoBox extends UIElement {
* The tags, wrapped in a global event source * The tags, wrapped in a global event source
*/ */
private readonly _tagsES: UIEventSource<any>; private readonly _tagsES: UIEventSource<any>;
private readonly _changes: Changes;
private readonly _title: UIElement; private readonly _title: UIElement;
private readonly _infoboxes: TagDependantUIElement[]; private readonly _infoboxes: TagDependantUIElement[];
@ -37,10 +35,13 @@ export class FeatureInfoBox extends UIElement {
) { ) {
super(tagsES); super(tagsES);
this._feature = feature; 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.ListenTo(State.state.osmConnection.userDetails);
this.SetClass("featureinfobox"); this.SetClass("featureinfobox");
const deps = {tags: this._tagsES, changes: this._changes} const tags = this._tagsES;
this._infoboxes = []; this._infoboxes = [];
elementsToShow = elementsToShow ?? [] elementsToShow = elementsToShow ?? []
@ -48,13 +49,13 @@ export class FeatureInfoBox extends UIElement {
const self = this; const self = this;
for (const tagRenderingOption of elementsToShow) { for (const tagRenderingOption of elementsToShow) {
self._infoboxes.push( self._infoboxes.push(
tagRenderingOption.construct(deps)); tagRenderingOption.construct(tags));
} }
function initTags() { function initTags() {
self._infoboxes.splice(0, self._infoboxes.length); self._infoboxes.splice(0, self._infoboxes.length);
for (const tagRenderingOption of elementsToShow) { for (const tagRenderingOption of elementsToShow) {
self._infoboxes.push( self._infoboxes.push(
tagRenderingOption.construct(deps)); tagRenderingOption.construct(tags));
} }
self.Update(); self.Update();
} }
@ -74,7 +75,7 @@ export class FeatureInfoBox extends UIElement {
} else if (title instanceof UIElement) { } else if (title instanceof UIElement) {
renderedTitle = title; renderedTitle = title;
} else { } else {
renderedTitle = title.construct(deps); renderedTitle = title.construct(tags);
} }
@ -83,10 +84,10 @@ export class FeatureInfoBox extends UIElement {
.SetClass("title-font") .SetClass("title-font")
const osmLink = new OsmLink() const osmLink = new OsmLink()
.construct(deps) .construct(tags)
.SetStyle("width: 24px; display:block;") .SetStyle("width: 24px; display:block;")
const wikipedialink = new WikipediaLink() const wikipedialink = new WikipediaLink()
.construct(deps) .construct(tags)
.SetStyle("width: 24px; display:block;") .SetStyle("width: 24px; display:block;")
this._title = new Combine([ this._title = new Combine([

View file

@ -1,6 +1,6 @@
import {UIElement} from "./UIElement"; import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "./i18n/Translations"; import {UIElement} from "../UIElement";
import {UIEventSource} from "../Logic/UIEventSource"; import Translations from "../i18n/Translations";
export class SaveButton extends UIElement { export class SaveButton extends UIElement {
private _value: UIEventSource<any>; private _value: UIEventSource<any>;

View file

@ -1,23 +1,23 @@
import {UIEventSource} from "../Logic/UIEventSource"; import {UIElement} from "../UIElement";
import {And, Tag, TagsFilter, TagUtils} from "../Logic/Tags"; import Translation from "../i18n/Translation";
import Translations from "../UI/i18n/Translations"; import {VariableUiElement} from "../Base/VariableUIElement";
import Locale from "../UI/i18n/Locale"; import InputElementMap from "../Input/InputElementMap";
import Translation from "../UI/i18n/Translation"; import CheckBoxes from "../Input/Checkboxes";
import Combine from "../UI/Base/Combine"; import Combine from "../Base/Combine";
import {TagDependantUIElement} from "../Customizations/UIElementConstructor"; import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags";
import {UIElement} from "./UIElement"; import {InputElement} from "../Input/InputElement";
import {VariableUiElement} from "./Base/VariableUIElement";
import InputElementMap from "./Input/InputElementMap";
import {InputElement} from "./Input/InputElement";
import {SaveButton} from "./SaveButton"; import {SaveButton} from "./SaveButton";
import {RadioButton} from "./Input/RadioButton"; import {RadioButton} from "../Input/RadioButton";
import {FixedInputElement} from "./Input/FixedInputElement"; import {FixedInputElement} from "../Input/FixedInputElement";
import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; import {UIEventSource} from "../../Logic/UIEventSource";
import {FixedUiElement} from "./Base/FixedUiElement"; import ValidatedTextField from "../Input/ValidatedTextField";
import ValidatedTextField from "./Input/ValidatedTextField"; import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions";
import CheckBoxes from "./Input/Checkboxes"; import State from "../../State";
import State from "../State"; import {SubstitutedTranslation} from "../SpecialVisualizations";
import SpecialVisualizations 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 { 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 _question: string | Translation;
private readonly _mapping: { k: TagsFilter, txt: string | Translation, priority?: number }[]; private readonly _mapping: { k: TagsFilter, txt: string | Translation, priority?: number }[];
private currentTags: UIEventSource<any>; private readonly currentTags: UIEventSource<any>;
private readonly _freeform: { private readonly _freeform: {
@ -71,6 +71,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[] mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[]
}) { }) {
super(tags); super(tags);
if (tags === undefined) {
throw "No tags given for a tagrendering..."
}
this.ListenTo(Locale.language); this.ListenTo(Locale.language);
this.ListenTo(this._editMode); this.ListenTo(this._editMode);
this.ListenTo(this._questionSkipped); this.ListenTo(this._questionSkipped);
@ -78,8 +81,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
const self = this; const self = this;
this.currentTags = this._source.map(tags => this.currentTags = this._source.map(tags => {
{
if (options.tagsPreprocessor === undefined) { if (options.tagsPreprocessor === undefined) {
return tags; return tags;
@ -186,6 +188,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
}, [Locale.language]); }, [Locale.language]);
// And at last, set up the skip button // And at last, set up the skip button
this._skipButton = new VariableUiElement(cancelContents).onClick(cancel); this._skipButton = new VariableUiElement(cancelContents).onClick(cancel);
} }
@ -444,7 +447,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return new FixedUiElement(""); return new FixedUiElement("");
} }
InnerRender(): string {
private CreateComponent(): UIElement {
if (this.IsQuestioning() if (this.IsQuestioning()
&& (State.state !== undefined) // If State.state is undefined, we are testing/custom theme building -> show regular save && (State.state !== undefined) // If State.state is undefined, we are testing/custom theme building -> show regular save
@ -452,31 +457,25 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
const question = const question =
this.ApplyTemplate(this._question).SetClass('question-text'); this.ApplyTemplate(this._question).SetClass('question-text');
return "<div class='question'>" + return new Combine(["<div class='question'>",
new Combine([
question, question,
"<br/>", "<br/>",
this._questionElement, this._questionElement,
this._friendlyLogin, this._friendlyLogin, "</div>"
]).Render() + "</div>"; ]);
} }
if (this.IsQuestioning() || this._editMode.data) { if (this.IsQuestioning() || this._editMode.data) {
// Not yet known or questioning, we have to ask a question // Not yet known or questioning, we have to ask a question
return new Combine([
return "<div class='question'>" + this.ApplyTemplate(this._question).SetStyle('question-text'),
new Combine([
"<span class='question-text'>",
this.ApplyTemplate(this._question),
"</span>",
"<br/>", "<br/>",
"<div>", this._questionElement, "</div>", "<div>", this._questionElement, "</div>",
this._skipButton, this._skipButton,
this._saveButton, this._saveButton,
"<br/>", "<br/>",
this._appliedTags this._appliedTags
]).Render() + ]).SetClass('question');
"</div>"
} }
if (this.IsKnown()) { if (this.IsKnown()) {
@ -484,7 +483,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
const answer = this.RenderAnswer(); const answer = this.RenderAnswer();
if (answer.IsEmpty()) { if (answer.IsEmpty()) {
return ""; return new FixedUiElement("");
} }
@ -499,37 +498,35 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return new Combine([ return new Combine([
answer, answer,
this._editButton]) this._editButton])
.SetStyle(answerStyle) .SetStyle(answerStyle);
.Render();
} }
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 { private ApplyTemplate(template: string | Translation): UIElement {
if (template === undefined || template === null) { const tr = Translations.WT(template);
return undefined; if (this.answerCache[tr.id]) {
return this.answerCache[tr.id];
}
// 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;
} }
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);
}
} }

View file

@ -3,6 +3,79 @@ import OpeningHoursVisualization from "./OhVisualization";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import {VariableUiElement} from "./Base/VariableUIElement"; import {VariableUiElement} from "./Base/VariableUIElement";
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; 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<any>;
private readonly translation: Translation;
private content: UIElement;
constructor(
translation: Translation,
tags: UIEventSource<any>) {
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 { export default class SpecialVisualizations {
@ -13,10 +86,47 @@ export default class SpecialVisualizations {
args: { name: string, defaultValue?: string, doc: string }[] args: { name: string, defaultValue?: string, doc: string }[]
}[] = }[] =
[{ [
{
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);
}
},
{
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", 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'.", 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"}], args: [{
name: "key",
defaultValue: "opening_hours",
doc: "The tag from which the table is constructed"
}],
constr: (tagSource: UIEventSource<any>, args) => { constr: (tagSource: UIEventSource<any>, args) => {
let keyname = args[0]; let keyname = args[0];
if (keyname === undefined || keyname === "") { if (keyname === undefined || keyname === "") {

View file

@ -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 { get txt(): string {

View file

@ -968,12 +968,18 @@ export default class Translations {
return s; return s;
} }
private static wtcache = {}
public static WT(s: string | Translation): Translation { public static WT(s: string | Translation): Translation {
if(s === undefined){ if(s === undefined){
return undefined; return undefined;
} }
if (typeof (s) === "string") { 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) { if (s instanceof Translation) {
return s; return s;

View file

@ -1,10 +1,10 @@
import {UIEventSource} from "./Logic/UIEventSource"; import {UIEventSource} from "./Logic/UIEventSource";
import {GenerateEmpty} from "./UI/CustomGenerator/GenerateEmpty"; import {GenerateEmpty} from "./UI/CustomGenerator/GenerateEmpty";
import {TagRendering} from "./UI/TagRendering";
import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
import {OsmConnection} from "./Logic/Osm/OsmConnection"; import {OsmConnection} from "./Logic/Osm/OsmConnection";
import CustomGeneratorPanel from "./UI/CustomGenerator/CustomGeneratorPanel"; import CustomGeneratorPanel from "./UI/CustomGenerator/CustomGeneratorPanel";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import {TagRendering} from "./UI/Popup/TagRendering";
let layout = GenerateEmpty.createEmptyLayout(); let layout = GenerateEmpty.createEmptyLayout();
if (window.location.hash.length > 10) { if (window.location.hash.length > 10) {

View file

@ -367,6 +367,7 @@ body {
} }
.question { .question {
display: block;
margin-top: 1em; margin-top: 1em;
background-color: #e5f5ff; background-color: #e5f5ff;
padding: 1em; padding: 1em;

View file

@ -6,7 +6,7 @@ import {QueryParameters} from "./Logic/Web/QueryParameters";
import {UIEventSource} from "./Logic/UIEventSource"; import {UIEventSource} from "./Logic/UIEventSource";
import * as $ from "jquery"; import * as $ from "jquery";
import {FromJSON} from "./Customizations/JSON/FromJSON"; import {FromJSON} from "./Customizations/JSON/FromJSON";
import {TagRendering} from "./UI/TagRendering"; import {TagRendering} from "./UI/Popup/TagRendering";
TagRendering.injectFunction(); TagRendering.injectFunction();