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 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: {

View file

@ -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<any>): TagDependantUIElement {
return new OnlyShowIf(tags,
this._embedded.construct(tags),
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 {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<any>): TagDependantUIElement {
return TagRenderingOptions.tagRendering(tags, this.options);
}
IsKnown(properties: any): boolean {

View file

@ -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<any>
}
export interface TagDependantUIElementConstructor {
construct(dependencies: Dependencies): TagDependantUIElement;
construct(tags: UIEventSource<any>): TagDependantUIElement;
IsKnown(properties: any): boolean;
IsQuestioning(properties: any): boolean;
GetContent(tags: any): Translation;

View file

@ -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 {

View file

@ -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);

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 => {
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();

View file

@ -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 {

View file

@ -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<any>) {
}
}
class ImageCarouselWithUpload extends TagDependantUIElement {
private _imageElement: ImageCarousel;
private _pictureUploader: ImageUploadFlow;
constructor(dependencies: Dependencies) {
super(dependencies.tags);
const tags = dependencies.tags;
constructor(tags: UIEventSource<any>) {
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();
}

View file

@ -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<string>;
private _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
private _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _allDone: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) };
private _connectButton : UIElement;
private readonly _licensePicker: UIElement;
private readonly _tags: UIEventSource<any>;
private readonly _selectedLicence: UIEventSource<string>;
private readonly _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
private readonly _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _allDone: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _connectButton: UIElement;
constructor(
preferedLicense: UIEventSource<string>,
uploadOptions: ((license: string) =>
{
title: string,
description: string,
handleURL: ((url: string) => void),
allDone: (() => void)
})
) {
constructor(tags: UIEventSource<any>) {
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,7 +118,7 @@ export class ImageUploadFlow extends UIElement {
`<label for='fileselector-${this.id}'>` +
label.Render() +
"</label>" +
actualInputElement+
actualInputElement +
"</form>";
return new Combine([
@ -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) {

View file

@ -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("<div class='prev-button'>" +

View file

@ -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<any>;
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([

View file

@ -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<any>;

View file

@ -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<any>;
private readonly currentTags: UIEventSource<any>;
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 }[]
}) {
super(tags);
if (tags === undefined) {
throw "No tags given for a tagrendering..."
}
this.ListenTo(Locale.language);
this.ListenTo(this._editMode);
this.ListenTo(this._questionSkipped);
@ -78,8 +81,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
const self = this;
this.currentTags = this._source.map(tags =>
{
this.currentTags = this._source.map(tags => {
if (options.tagsPreprocessor === undefined) {
return tags;
@ -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);
}
@ -297,7 +300,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
}
let txt = this.ApplyTemplate(mapping.txt);
if(txt.Render().indexOf("<img") >= 0){
if (txt.Render().indexOf("<img") >= 0) {
txt.SetClass("question-option-with-border");
}
const inputEl = new FixedInputElement(txt, mapping.k,
@ -324,14 +327,14 @@ 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;
}
@ -444,7 +447,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return new FixedUiElement("");
}
InnerRender(): string {
private CreateComponent(): UIElement {
if (this.IsQuestioning()
&& (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 =
this.ApplyTemplate(this._question).SetClass('question-text');
return "<div class='question'>" +
new Combine([
return new Combine(["<div class='question'>",
question,
"<br/>",
this._questionElement,
this._friendlyLogin,
]).Render() + "</div>";
this._friendlyLogin, "</div>"
]);
}
if (this.IsQuestioning() || this._editMode.data) {
// Not yet known or questioning, we have to ask a question
return "<div class='question'>" +
new Combine([
"<span class='question-text'>",
this.ApplyTemplate(this._question),
"</span>",
return new Combine([
this.ApplyTemplate(this._question).SetStyle('question-text'),
"<br/>",
"<div>", this._questionElement , "</div>",
"<div>", this._questionElement, "</div>",
this._skipButton,
this._saveButton,
"<br/>",
this._appliedTags
]).Render() +
"</div>"
]).SetClass('question');
}
if (this.IsKnown()) {
@ -484,7 +483,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
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;
}
}

View file

@ -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<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 {
@ -13,10 +86,47 @@ export default class SpecialVisualizations {
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",
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) => {
let keyname = args[0];
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 {

View file

@ -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;

View file

@ -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) {

View file

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

View file

@ -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();