From 62f471df1e97cdf1a34e65f59d9bd63970807dd8 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 11 Jun 2021 22:51:45 +0200 Subject: [PATCH] More refactoring, still very broken --- Logic/Actors/TitleHandler.ts | 69 ++++--- Logic/Web/Imgur.ts | 6 +- Logic/Web/ImgurUploader.ts | 41 +++++ UI/Base/FileSelectorButton.ts | 62 +++++++ UI/Base/VariableUIElement.ts | 16 +- UI/BigComponents/LicensePicker.ts | 19 ++ UI/BigComponents/UploadFlowStateUI.ts | 54 ++++++ UI/Image/DeleteImage.ts | 5 +- UI/Image/ImageCarousel.ts | 15 +- UI/Image/ImageUploadFlow.ts | 256 +++++++++----------------- UI/Image/ImgurImage.ts | 11 +- UI/Image/MapillaryImage.ts | 11 +- UI/Image/SimpleImageElement.ts | 15 -- UI/Image/SlideShow.ts | 60 +++--- UI/Image/WikimediaImage.ts | 11 +- UI/Input/Checkboxes.ts | 22 ++- UI/Input/ColorPicker.ts | 47 ++--- UI/Popup/TagRenderingAnswer.ts | 8 +- UI/Reviews/ReviewElement.ts | 7 +- UI/Reviews/ReviewForm.ts | 26 +-- UI/Reviews/SingleReview.ts | 5 +- UI/SpecialVisualizations.ts | 11 +- UI/SubstitutedTranslation.ts | 7 +- 23 files changed, 428 insertions(+), 356 deletions(-) create mode 100644 Logic/Web/ImgurUploader.ts create mode 100644 UI/Base/FileSelectorButton.ts create mode 100644 UI/BigComponents/LicensePicker.ts create mode 100644 UI/BigComponents/UploadFlowStateUI.ts delete mode 100644 UI/Image/SimpleImageElement.ts diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index adb03a188..b6f890614 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -6,8 +6,11 @@ import {UIElement} from "../../UI/UIElement"; import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"; import {ElementStorage} from "../ElementStorage"; import Combine from "../../UI/Base/Combine"; +import BaseUIElement from "../../UI/BaseUIElement"; +import {FixedUiElement} from "../../UI/Base/FixedUiElement"; -class TitleElement extends UIElement { +class TitleElement extends UIEventSource { + private readonly _layoutToUse: UIEventSource; private readonly _selectedFeature: UIEventSource; private readonly _allElementsStorage: ElementStorage; @@ -15,41 +18,43 @@ class TitleElement extends UIElement { constructor(layoutToUse: UIEventSource, selectedFeature: UIEventSource, allElementsStorage: ElementStorage) { - super(layoutToUse); + super("MapComplete"); + this._layoutToUse = layoutToUse; this._selectedFeature = selectedFeature; this._allElementsStorage = allElementsStorage; - this.ListenTo(Locale.language); - this.ListenTo(this._selectedFeature) - } + + this.syncWith( + this._selectedFeature.map( + selected => { + const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ??"MapComplete" - InnerRender(): string { + if(selected === undefined){ + return defaultTitle + } - const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ?? "MapComplete" - const feature = this._selectedFeature.data; - - if (feature === undefined) { - return defaultTitle; - } + const layout = layoutToUse.data; + const tags = selected.properties; - const layout = this._layoutToUse.data; - const properties = this._selectedFeature.data.properties; + for (const layer of layout.layers) { + if (layer.title === undefined) { + continue; + } + if (layer.source.osmTags.matchesProperties(tags)) { + const title = new TagRenderingAnswer(tags, layer.title) + return new Combine([defaultTitle, " | ", title]).ConstructElement().innerText; + } + } - for (const layer of layout.layers) { - if (layer.title === undefined) { - continue; - } - if (layer.source.osmTags.matchesProperties(properties)) { - const tags = this._allElementsStorage.getEventSourceById(feature.properties.id); - if (tags == undefined) { - return defaultTitle; + return defaultTitle } - const title = new TagRenderingAnswer(tags, layer.title) - return new Combine([defaultTitle, " | ", title]).Render(); - } - } - return defaultTitle; + , [Locale.language, layoutToUse] + ) + + ) + + } } @@ -58,14 +63,8 @@ export default class TitleHandler { constructor(layoutToUse: UIEventSource, selectedFeature: UIEventSource, allElementsStorage: ElementStorage) { - - selectedFeature.addCallbackAndRun(_ => { - const title = new TitleElement(layoutToUse, selectedFeature, allElementsStorage) - const d = document.createElement('div'); - d.innerHTML = title.InnerRenderAsString(); - // We pass everything into a div to strip out images etc... - document.title = (d.textContent || d.innerText); + new TitleElement(layoutToUse, selectedFeature, allElementsStorage).addCallbackAndRun(title => { + document.title = title }) - } } \ No newline at end of file diff --git a/Logic/Web/Imgur.ts b/Logic/Web/Imgur.ts index 27a10535d..d48771e11 100644 --- a/Logic/Web/Imgur.ts +++ b/Logic/Web/Imgur.ts @@ -10,11 +10,8 @@ export class Imgur { handleSuccessfullUpload: ((imageURL: string) => void), allDone: (() => void), onFail: ((reason: string) => void), - offset:number) { + offset:number = 0) { - if(offset === undefined){ - throw "Offset undefined - not uploading to prevent to much uploads!" - } if (blobs.length == offset) { allDone(); return; @@ -36,6 +33,7 @@ export class Imgur { } + static getDescriptionOfImage(url: string, handleDescription: ((license: LicenseInfo) => void)) { diff --git a/Logic/Web/ImgurUploader.ts b/Logic/Web/ImgurUploader.ts new file mode 100644 index 000000000..b21228b5e --- /dev/null +++ b/Logic/Web/ImgurUploader.ts @@ -0,0 +1,41 @@ +import {UIEventSource} from "../UIEventSource"; +import {Imgur} from "./Imgur"; + +export default class ImgurUploader { + + public queue: UIEventSource; + public failed: UIEventSource; + public success: UIEventSource + private readonly _handleSuccessUrl: (string) => void; + + constructor(handleSuccessUrl: (string) => void) { + this._handleSuccessUrl = handleSuccessUrl; + } + + public uploadMany(title: string, description: string, files: FileList) { + for (let i = 0; i < files.length; i++) { + this.queue.data.push(files.item(i).name) + } + this.queue.ping() + + const self = this; + this.queue.setData([...self.queue.data]) + Imgur.uploadMultiple(title, + description, + files, + function (url) { + console.log("File saved at", url); + self.success.setData([...self.success.data, url]); + this. handleSuccessUrl(url); + }, + function () { + console.log("All uploads completed"); + }, + + function (failReason) { + console.log("Upload failed due to ", failReason) + self.failed.setData([...self.failed.data, failReason]) + } + ); + } +} \ No newline at end of file diff --git a/UI/Base/FileSelectorButton.ts b/UI/Base/FileSelectorButton.ts new file mode 100644 index 000000000..996479069 --- /dev/null +++ b/UI/Base/FileSelectorButton.ts @@ -0,0 +1,62 @@ +import BaseUIElement from "../BaseUIElement"; +import {InputElement} from "../Input/InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; + +export default class FileSelectorButton extends InputElement { + + IsSelected: UIEventSource; + private readonly _value = new UIEventSource(undefined); + private readonly _label: BaseUIElement; + private readonly _acceptType: string; + + constructor(label: BaseUIElement, acceptType: string = "image/*") { + super(); + this._label = label; + this._acceptType = acceptType; + } + + GetValue(): UIEventSource { + return this._value; + } + + IsValid(t: FileList): boolean { + return true; + } + + protected InnerConstructElement(): HTMLElement { + const self = this; + const el = document.createElement("form") + { + const label = document.createElement("label") + label.appendChild(this._label.ConstructElement()) + el.appendChild(label) + } + { + const actualInputElement = document.createElement("input"); + actualInputElement.style.cssText = "display:none"; + actualInputElement.type = "file"; + actualInputElement.accept = this._acceptType; + actualInputElement.name = "picField"; + actualInputElement.multiple = true; + + actualInputElement.onchange = () => { + if (actualInputElement.files !== null) { + self._value.setData(actualInputElement.files) + } + } + + el.addEventListener('submit', e => { + if (actualInputElement.files !== null) { + self._value.setData(actualInputElement.files) + } + e.preventDefault() + }) + + el.appendChild(actualInputElement) + } + + return undefined; + } + + +} \ No newline at end of file diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index 2d813a0aa..ca38f64d9 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -5,24 +5,28 @@ export class VariableUiElement extends BaseUIElement { private _element : HTMLElement; - constructor(contents: UIEventSource) { + constructor(contents: UIEventSource) { super(); this._element = document.createElement("span") const el = this._element contents.addCallbackAndRun(contents => { - while(el.firstChild){ + while (el.firstChild) { el.removeChild( el.lastChild ) } - - if(contents === undefined){ + + if (contents === undefined) { return } - if(typeof contents === "string"){ + if (typeof contents === "string") { el.innerHTML = contents - }else{ + } else if (contents instanceof Array) { + for (const content of contents) { + el.appendChild(content.ConstructElement()) + } + }else{ el.appendChild(contents.ConstructElement()) } }) diff --git a/UI/BigComponents/LicensePicker.ts b/UI/BigComponents/LicensePicker.ts new file mode 100644 index 000000000..43d2cac1c --- /dev/null +++ b/UI/BigComponents/LicensePicker.ts @@ -0,0 +1,19 @@ +import {DropDown} from "../Input/DropDown"; +import Translations from "../i18n/Translations"; +import State from "../../State"; + +export default class LicensePicker extends DropDown{ + + constructor() { + super(Translations.t.image.willBePublished, + [ + {value: "CC0", shown: Translations.t.image.cco}, + {value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs}, + {value: "CC-BY 4.0", shown: Translations.t.image.ccb} + ], + State.state.osmConnection.GetPreference("pictures-license") + ) + this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left"); + } + +} \ No newline at end of file diff --git a/UI/BigComponents/UploadFlowStateUI.ts b/UI/BigComponents/UploadFlowStateUI.ts new file mode 100644 index 000000000..9c00649af --- /dev/null +++ b/UI/BigComponents/UploadFlowStateUI.ts @@ -0,0 +1,54 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Translations from "../i18n/Translations"; + +/** + * Shows that 'images are uploading', 'all images are uploaded' as relevant... + */ +export default class UploadFlowStateUI extends UIElement{ + + private readonly _element: BaseUIElement + + constructor(queue: UIEventSource, failed: UIEventSource, success: UIEventSource) { + super(); + const t = Translations.t.image; + + this._element = new VariableUiElement( + + queue.map(queue => { + const failedReasons = failed.data + const successCount = success.data.length + const pendingCount = queue.length - successCount - failedReasons.length; + + let stateMessages : BaseUIElement[] = [] + + if(pendingCount == 1){ + stateMessages.push(t.uploadingPicture.Clone().SetClass("alert")) + } + if(pendingCount > 1){ + stateMessages.push(t.uploadingMultiple.Subs({count: ""+pendingCount}).SetClass("alert")) + } + if(failedReasons.length > 0){ + stateMessages.push(t.uploadFailed.Clone().SetClass("alert")) + } + if(successCount > 0 && pendingCount == 0){ + stateMessages.push(t.uploadDone.SetClass("thanks")) + } + + stateMessages.forEach(msg => msg.SetStyle("display: block ruby")) + + return stateMessages + }, [failed, success]) + + + ); + + + } + + protected InnerRender(): string | BaseUIElement { + return this._element + } +} \ No newline at end of file diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index 19c281465..df6de5e2e 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -6,14 +6,15 @@ import Combine from "../Base/Combine"; import State from "../../State"; import Svg from "../../Svg"; import {Tag} from "../../Logic/Tags/Tag"; +import BaseUIElement from "../BaseUIElement"; export default class DeleteImage extends UIElement { private readonly key: string; private readonly tags: UIEventSource; - private readonly isDeletedBadge: UIElement; - private readonly deleteDialog: UIElement; + private readonly isDeletedBadge: BaseUIElement; + private readonly deleteDialog: BaseUIElement; constructor(key: string, tags: UIEventSource) { super(tags); diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 3352c64c3..2528aa32b 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -6,16 +6,17 @@ import DeleteImage from "./DeleteImage"; import {WikimediaImage} from "./WikimediaImage"; import {ImgurImage} from "./ImgurImage"; import {MapillaryImage} from "./MapillaryImage"; -import {SimpleImageElement} from "./SimpleImageElement"; +import BaseUIElement from "../BaseUIElement"; +import Img from "../Base/Img"; export class ImageCarousel extends UIElement{ - public readonly slideshow: UIElement; + public readonly slideshow: BaseUIElement; constructor(images: UIEventSource<{key: string, url:string}[]>, tags: UIEventSource) { super(images); const uiElements = images.map((imageURLS: {key: string, url:string}[]) => { - const uiElements: UIElement[] = []; + const uiElements: BaseUIElement[] = []; for (const url of imageURLS) { let image = ImageCarousel.CreateImageElement(url.url) if(url.key !== undefined){ @@ -41,7 +42,7 @@ export class ImageCarousel extends UIElement{ * @param url * @constructor */ - private static CreateImageElement(url: string): UIElement { + private static CreateImageElement(url: string): BaseUIElement { // @ts-ignore if (url.startsWith("File:")) { return new WikimediaImage(url); @@ -53,11 +54,11 @@ export class ImageCarousel extends UIElement{ } else if (url.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) { return new MapillaryImage(url); } else { - return new SimpleImageElement(new UIEventSource(url)); + return new Img(url); } } - InnerRender(): string { - return this.slideshow.Render(); + InnerRender() { + return this.slideshow; } } \ No newline at end of file diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 13a844049..9182406d2 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -1,207 +1,119 @@ -import $ from "jquery" 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 Translations from "../i18n/Translations"; import Svg from "../../Svg"; import {Tag} from "../../Logic/Tags/Tag"; import BaseUIElement from "../BaseUIElement"; +import LicensePicker from "../BigComponents/LicensePicker"; +import Toggle from "../Input/Toggle"; +import FileSelectorButton from "../Base/FileSelectorButton"; +import ImgurUploader from "../../Logic/Web/ImgurUploader"; +import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; +import LayerConfig from "../../Customizations/JSON/LayerConfig"; export class ImageUploadFlow extends UIElement { - private readonly _licensePicker: BaseUIElement; + + private readonly _element: BaseUIElement; + + 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; + + private readonly _imagePrefix: string; - constructor(tags: UIEventSource, imagePrefix: string = "image") { + constructor(tagsSource: UIEventSource, imagePrefix: string = "image") { super(State.state.osmConnection.userDetails); - this._tags = tags; this._imagePrefix = imagePrefix; - this.ListenTo(this._isUploading); - this.ListenTo(this._didFail); - this.ListenTo(this._allDone); - const licensePicker = new DropDown(Translations.t.image.willBePublished, - [ - {value: "CC0", shown: Translations.t.image.cco}, - {value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs}, - {value: "CC-BY 4.0", shown: Translations.t.image.ccb} - ], - State.state.osmConnection.GetPreference("pictures-license") - ).SetClass("flex flex-col sm:flex-row"); - licensePicker.SetStyle("float:left"); + const uploader = new ImgurUploader(url => { + // A file was uploaded - we add it to the tags of the object - const t = Translations.t.image; - - this._licensePicker = licensePicker; - this._selectedLicence = licensePicker.GetValue(); - - this._connectButton = t.pleaseLogin.Clone() - .onClick(() => State.state.osmConnection.AttemptLogin()) - .SetClass("login-button-friendly"); - - } - - InnerRender(): string { - - if(!State.state.featureSwitchUserbadge.data){ - return ""; - } - - const t = Translations.t.image; - if (State.state.osmConnection.userDetails === undefined) { - return ""; // No user details -> logging in is probably disabled or smthing - } - - if (!State.state.osmConnection.userDetails.data.loggedIn) { - return this._connectButton.Render(); - } - - let currentState: UIElement[] = []; - if (this._isUploading.data == 1) { - currentState.push(t.uploadingPicture); - } else if (this._isUploading.data > 0) { - currentState.push(t.uploadingMultiple.Subs({count: ""+this._isUploading.data})); - } - - if (this._didFail.data) { - currentState.push(t.uploadFailed); - } - - if (this._allDone.data) { - currentState.push(t.uploadDone) - } - - let currentStateHtml : UIElement = new FixedUiElement(""); - if (currentState.length > 0) { - currentStateHtml = new Combine(currentState); - if (!this._allDone.data) { - currentStateHtml.SetClass("alert"); - }else{ - currentStateHtml.SetClass("thanks"); + const tags = tagsSource.data + let key = imagePrefix + if (tags[imagePrefix] !== undefined) { + let freeIndex = 0; + while (tags[imagePrefix + ":" + freeIndex] !== undefined) { + freeIndex++; + } + key = imagePrefix + ":" + freeIndex; } - currentStateHtml.SetStyle("display:block ruby") - } + console.log("Adding image:" + key, url); + State.state.changes.addTag(tags.id, new Tag(key, url)); + }) - const extraInfo = new Combine([ - Translations.t.image.respectPrivacy.SetStyle("font-size:small;"), - "
", - this._licensePicker, - "
", - currentStateHtml, - "
" - ]); + const licensePicker = new LicensePicker() + + const t = Translations.t.image; const label = new Combine([ Svg.camera_plus_svg().SetStyle("width: 36px;height: 36px;padding: 0.1em;margin-top: 5px;border-radius: 0;float: left;display:block"), Translations.t.image.addPicture ]).SetClass("image-upload-flow-button") - - const actualInputElement = - ``; - - const form = "
" + - `" + - actualInputElement + - "
"; + const fileSelector = new FileSelectorButton(label) + fileSelector.GetValue().addCallback(filelist => { + if (filelist === undefined) { + return; + } - return new Combine([ - form, - extraInfo + console.log("Received images from the user, starting upload") + const license = this._selectedLicence.data ?? "CC0" + + const tags = this._tags.data; + + const layout = State.state.layoutToUse.data + let matchingLayer: LayerConfig = undefined + for (const layer of layout.layers) { + if (layer.source.osmTags.matchesProperties(tags)) { + matchingLayer = layer; + break; + } + } + + + const title = matchingLayer?.title?.GetRenderValue(tags)?.ConstructElement().innerText ?? tags.name ?? "Unknown area"; + const description = [ + "author:" + State.state.osmConnection.userDetails.data.name, + "license:" + license, + "osmid:" + tags.id, + ].join("\n"); + + uploader.uploadMany(title, description, filelist) + + }) + + + const uploadStateUi = new UploadFlowStateUI(uploader.queue, uploader.failed, uploader.success) + + const uploadFlow: BaseUIElement = new Combine([ + fileSelector, + Translations.t.image.respectPrivacy.SetStyle("font-size:small;"), + licensePicker, + uploadStateUi ]).SetClass("image-upload-flow") - .SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;") - .Render(); - } + .SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;"); - private handleSuccessfulUpload(url) { - const tags = this._tags.data; - let key = this._imagePrefix; - if (tags[this._imagePrefix] !== undefined) { - - let freeIndex = 0; - while (tags[this._imagePrefix + ":" + freeIndex] !== undefined) { - freeIndex++; - } - key = this._imagePrefix + ":" + freeIndex; - } - console.log("Adding image:" + key, url); - State.state.changes.addTag(tags.id, new Tag(key, url)); - } - - private handleFiles(files) { - console.log("Received images from the user, starting upload") - 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 + const pleaseLoginButton = t.pleaseLogin.Clone() + .onClick(() => State.state.osmConnection.AttemptLogin()) + .SetClass("login-button-friendly"); + this._element = new Toggle( + new Toggle( + /*We can show the actual upload button!*/ + uploadFlow, + /* User not logged in*/ pleaseLoginButton, + State.state.osmConnection.userDetails.map(userinfo => userinfo.loggedIn) + ), + undefined /* Nothing as the user badge is disabled*/, State.state.featureSwitchUserbadge ) + } - InnerUpdate(htmlElement: HTMLElement) { - this._licensePicker.Update() - const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement - const selector = document.getElementById('fileselector-' + this.id) - const self = this - - function submitHandler() { - self.handleFiles($(selector).prop('files')) - } - - if (selector != null && form != null) { - selector.onchange = function () { - submitHandler() - } - form.addEventListener('submit', e => { - e.preventDefault() - submitHandler() - }) - } + protected InnerRender(): string | BaseUIElement { + return this._element; } + } \ No newline at end of file diff --git a/UI/Image/ImgurImage.ts b/UI/Image/ImgurImage.ts index e8639dae4..176e16372 100644 --- a/UI/Image/ImgurImage.ts +++ b/UI/Image/ImgurImage.ts @@ -4,7 +4,8 @@ import {LicenseInfo} from "../../Logic/Web/Wikimedia"; import {Imgur} from "../../Logic/Web/Imgur"; import Combine from "../Base/Combine"; import Attribution from "./Attribution"; -import {SimpleImageElement} from "./SimpleImageElement"; +import BaseUIElement from "../BaseUIElement"; +import Img from "../Base/Img"; export class ImgurImage extends UIElement { @@ -35,11 +36,11 @@ export class ImgurImage extends UIElement { } - InnerRender(): string { - const image = new SimpleImageElement( new UIEventSource (this._imageLocation)); + InnerRender(): BaseUIElement { + const image = new Img( this._imageLocation); if(this._imageMeta.data === null){ - return image.Render(); + return image; } const meta = this._imageMeta.data; @@ -48,7 +49,7 @@ export class ImgurImage extends UIElement { new Attribution(meta.artist, meta.license, undefined), ]).SetClass('block relative') - .Render(); + ; } diff --git a/UI/Image/MapillaryImage.ts b/UI/Image/MapillaryImage.ts index 53c0e53e9..9b2edf565 100644 --- a/UI/Image/MapillaryImage.ts +++ b/UI/Image/MapillaryImage.ts @@ -3,9 +3,10 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import {LicenseInfo} from "../../Logic/Web/Wikimedia"; import {Mapillary} from "../../Logic/Web/Mapillary"; import Svg from "../../Svg"; -import {SimpleImageElement} from "./SimpleImageElement"; import Combine from "../Base/Combine"; import Attribution from "./Attribution"; +import Img from "../Base/Img"; +import BaseUIElement from "../BaseUIElement"; export class MapillaryImage extends UIElement { @@ -40,19 +41,19 @@ export class MapillaryImage extends UIElement { } - InnerRender(): string { + InnerRender(): BaseUIElement { const url = `https://images.mapillary.com/${this._imageLocation}/thumb-640.jpg?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`; - const image = new SimpleImageElement(new UIEventSource(url)) + const image = new Img(url) const meta = this._imageMeta?.data; if (!meta) { - return image.Render(); + return image; } return new Combine([ image, new Attribution(meta.artist, meta.license, Svg.mapillary_svg()) - ]).SetClass("relative block").Render(); + ]).SetClass("relative block"); } diff --git a/UI/Image/SimpleImageElement.ts b/UI/Image/SimpleImageElement.ts deleted file mode 100644 index c17f0fa49..000000000 --- a/UI/Image/SimpleImageElement.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; - - -export class SimpleImageElement extends UIElement { - - constructor(source: UIEventSource) { - super(source); - } - - InnerRender(): string { - return "img"; - } - -} \ No newline at end of file diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 1021a3c03..8cf7b0719 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -1,46 +1,22 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import Combine from "../Base/Combine"; -// @ts-ignore -import $ from "jquery" +import BaseUIElement from "../BaseUIElement"; -export class SlideShow extends UIElement { +export class SlideShow extends BaseUIElement { - private readonly _embeddedElements: UIEventSource + private readonly _element: HTMLElement; + constructor( - embeddedElements: UIEventSource) { - super(embeddedElements); - this._embeddedElements = embeddedElements; - this._embeddedElements.addCallbackAndRun(elements => { - for (const element of elements ?? []) { - element.SetClass("slick-carousel-content") - } - }) - - } - - InnerRender(): string { - return new Combine( - this._embeddedElements.data, - ).SetClass("block slick-carousel") - .Render(); - } - - Update() { - super.Update(); - for (const uiElement of this._embeddedElements.data) { - uiElement.Update(); - } - } - - protected InnerUpdate(htmlElement: HTMLElement) { + embeddedElements: UIEventSource) { + super() + + const el = document.createElement("div") + this._element = el; + + el.classList.add("slick-carousel") require("slick-carousel") - if(this._embeddedElements.data.length == 0){ - return; - } // @ts-ignore - $('.slick-carousel').not('.slick-initialized').slick({ + el.slick({ autoplay: true, arrows: true, dots: true, @@ -48,8 +24,18 @@ export class SlideShow extends UIElement { variableWidth: true, centerMode: true, centerPadding: "60px", - adaptive: true + adaptive: true }); + embeddedElements.addCallbackAndRun(elements => { + for (const element of elements ?? []) { + element.SetClass("slick-carousel-content") + } + }); + + } + + protected InnerConstructElement(): HTMLElement { + return this._element; } } \ No newline at end of file diff --git a/UI/Image/WikimediaImage.ts b/UI/Image/WikimediaImage.ts index ceae32202..754c8dab5 100644 --- a/UI/Image/WikimediaImage.ts +++ b/UI/Image/WikimediaImage.ts @@ -4,8 +4,9 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import Svg from "../../Svg"; import Link from "../Base/Link"; import Combine from "../Base/Combine"; -import {SimpleImageElement} from "./SimpleImageElement"; import Attribution from "./Attribution"; +import BaseUIElement from "../BaseUIElement"; +import Img from "../Base/Img"; export class WikimediaImage extends UIElement { @@ -34,14 +35,14 @@ export class WikimediaImage extends UIElement { } - InnerRender(): string { + InnerRender(): BaseUIElement { const url = Wikimedia.ImageNameToUrl(this._imageLocation, 500, 400) .replace(/'/g, '%27'); - const image = new SimpleImageElement(new UIEventSource(url)) + const image = new Img(url) const meta = this._imageMeta?.data; if (!meta) { - return image.Render(); + return image; } new Link(Svg.wikimedia_commons_white_img, `https://commons.wikimedia.org/wiki/${this._imageLocation}`, true) @@ -50,7 +51,7 @@ export class WikimediaImage extends UIElement { return new Combine([ image, new Attribution(meta.artist, meta.license, Svg.wikimedia_commons_white_svg()) - ]).SetClass("relative block").Render() + ]).SetClass("relative block") } diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index cd0dbcac9..aa378beb8 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -2,6 +2,7 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import {Utils} from "../../Utils"; import {UIElement} from "../UIElement"; +import BaseUIElement from "../BaseUIElement"; /** * Supports multi-input @@ -10,15 +11,24 @@ export default class CheckBoxes extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; - private readonly _elements: UIElement[] + private readonly _elements: BaseUIElement[] + + +private readonly _element : HTMLElement - - constructor(elements: UIElement[]) { - super(undefined); + constructor(elements: BaseUIElement[]) { + super(); this._elements = Utils.NoNull(elements); - this.value = new UIEventSource([]) - this.ListenTo(this.value); + + + const el = document.createElement() + this._element = el; + + } + + protected InnerConstructElement(): HTMLElement { + return this._element } diff --git a/UI/Input/ColorPicker.ts b/UI/Input/ColorPicker.ts index 2fe6adf0d..ea0abda60 100644 --- a/UI/Input/ColorPicker.ts +++ b/UI/Input/ColorPicker.ts @@ -4,46 +4,33 @@ import {UIEventSource} from "../../Logic/UIEventSource"; export default class ColorPicker extends InputElement { private readonly value: UIEventSource - +private readonly _element : HTMLElement constructor( - value?: UIEventSource + value: UIEventSource = new UIEventSource(undefined) ) { super(); - this.value = value ?? new UIEventSource(undefined); - const self = this; + this.value = value ; + + const el = document.createElement("input") + this._element = el; + + el.type = "color" + this.value.addCallbackAndRun(v => { if(v === undefined){ return; } - self.SetValue(v); + el.value =v }); + + el.oninput = () => { + const hex = el.value; + value.setData(hex); + } } - - InnerRender(): string { - return ``; - } - - private SetValue(color: string){ - const field = document.getElementById("color-" + this.id); - if (field === undefined || field === null) { - return; - } - // @ts-ignore - field.value = color; - } - - protected InnerUpdate() { - const field = document.getElementById("color-" + this.id); - if (field === undefined || field === null) { - return; - } - const self = this; - field.oninput = () => { - const hex = field["value"]; - self.value.setData(hex); - } - + protected InnerConstructElement(): HTMLElement { + return this._element; } GetValue(): UIEventSource { diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 1138da448..dc880d304 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -6,6 +6,7 @@ import Combine from "../Base/Combine"; import {SubstitutedTranslation} from "../SubstitutedTranslation"; import {Translation} from "../i18n/Translation"; import {TagUtils} from "../../Logic/Tags/TagUtils"; +import BaseUIElement from "../BaseUIElement"; /*** * Displays the correct value for a known tagrendering @@ -13,7 +14,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils"; export default class TagRenderingAnswer extends UIElement { private readonly _tags: UIEventSource; private _configuration: TagRenderingConfig; - private _content: UIElement; + private _content: BaseUIElement; private readonly _contentClass: string; private _contentStyle: string; @@ -30,7 +31,7 @@ export default class TagRenderingAnswer extends UIElement { this.SetStyle("word-wrap: anywhere;"); } - InnerRender(): string | UIElement{ + InnerRender(): string | BaseUIElement{ if (this._configuration.condition !== undefined) { if (!this._configuration.condition.matchesProperties(this._tags.data)) { return ""; @@ -74,8 +75,7 @@ export default class TagRenderingAnswer extends UIElement { this._content = valuesToRender[0]; } else { this._content = new Combine(["
    ", - ...valuesToRender.map(tr => new Combine(["
  • ", tr, "
  • "])) - , + ...valuesToRender.map(tr => new Combine(["
  • ", tr, "
  • "])) , "
" ]) diff --git a/UI/Reviews/ReviewElement.ts b/UI/Reviews/ReviewElement.ts index cdf6659e9..38b9f226a 100644 --- a/UI/Reviews/ReviewElement.ts +++ b/UI/Reviews/ReviewElement.ts @@ -8,13 +8,14 @@ import {UIElement} from "../UIElement"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; import SingleReview from "./SingleReview"; +import BaseUIElement from "../BaseUIElement"; export default class ReviewElement extends UIElement { private readonly _reviews: UIEventSource; private readonly _subject: string; - private readonly _middleElement: UIElement; + private readonly _middleElement: BaseUIElement; - constructor(subject: string, reviews: UIEventSource, middleElement: UIElement) { + constructor(subject: string, reviews: UIEventSource, middleElement: BaseUIElement) { super(reviews); this._middleElement = middleElement; if (reviews === undefined) { @@ -26,7 +27,7 @@ export default class ReviewElement extends UIElement { - InnerRender(): UIElement { + InnerRender(): BaseUIElement { const elements = []; const revs = this._reviews.data; diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index 59d86c792..ce2ac3423 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import {InputElement} from "../Input/InputElement"; import {Review} from "../../Logic/Web/Review"; import {UIEventSource} from "../../Logic/UIEventSource"; @@ -10,16 +9,18 @@ import {VariableUiElement} from "../Base/VariableUIElement"; import {SaveButton} from "../Popup/SaveButton"; import CheckBoxes from "../Input/Checkboxes"; import UserDetails from "../../Logic/Osm/OsmConnection"; +import BaseUIElement from "../BaseUIElement"; +import Toggle from "../Input/Toggle"; export default class ReviewForm extends InputElement { private readonly _value: UIEventSource; - private readonly _comment: UIElement; - private readonly _stars: UIElement; - private _saveButton: UIElement; - private readonly _isAffiliated: UIElement; + private readonly _comment: BaseUIElement; + private readonly _stars: BaseUIElement; + private _saveButton: BaseUIElement; + private readonly _isAffiliated: BaseUIElement; private userDetails: UIEventSource; - private readonly _postingAs: UIElement; + private readonly _postingAs: BaseUIElement; constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), userDetails: UIEventSource) { @@ -86,13 +87,9 @@ export default class ReviewForm extends InputElement { return this._value; } - InnerRender(): UIElement { + InnerConstructElement(): HTMLElement { - if(!this.userDetails.data.loggedIn){ - return Translations.t.reviews.plz_login; - } - - return new Combine([ + const form = new Combine([ new Combine([this._stars, this._postingAs]).SetClass("review-form-top"), this._comment, new Combine([ @@ -103,6 +100,11 @@ export default class ReviewForm extends InputElement { Translations.t.reviews.tos.SetClass("subtle") ]) .SetClass("review-form") + + + return new Toggle(form, Translations.t.reviews.plz_login, + this.userDetails.map(userdetails => userdetails.loggedIn)) + .ConstructElement() } IsSelected: UIEventSource = new UIEventSource(false); diff --git a/UI/Reviews/SingleReview.ts b/UI/Reviews/SingleReview.ts index b6f913add..a595ba7f9 100644 --- a/UI/Reviews/SingleReview.ts +++ b/UI/Reviews/SingleReview.ts @@ -5,6 +5,7 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import {Utils} from "../../Utils"; import ReviewElement from "./ReviewElement"; +import BaseUIElement from "../BaseUIElement"; export default class SingleReview extends UIElement{ private _review: Review; @@ -13,7 +14,7 @@ export default class SingleReview extends UIElement{ this._review = review; } - public static GenStars(rating: number): UIElement { + public static GenStars(rating: number): BaseUIElement { if (rating === undefined) { return Translations.t.reviews.no_rating; } @@ -26,7 +27,7 @@ export default class SingleReview extends UIElement{ scoreTen % 2 == 1 ? "" : "" ]).SetClass("flex w-max") } - InnerRender(): UIElement { + InnerRender(): BaseUIElement { const d = this._review.date; let review = this._review; const el= new Combine( diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 0307fa7d1..1c46e4fc5 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -1,4 +1,3 @@ -import {UIElement} from "./UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; import {VariableUiElement} from "./Base/VariableUIElement"; import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; @@ -17,12 +16,15 @@ import OpeningHoursVisualization from "./OpeningHours/OhVisualization"; import State from "../State"; import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; +import BaseUIElement from "./BaseUIElement"; export default class SpecialVisualizations { + + public static specialVisualizations: { funcName: string, - constr: ((state: State, tagSource: UIEventSource, argument: string[]) => UIElement), + constr: ((state: State, tagSource: UIEventSource, argument: string[]) => BaseUIElement), docs: string, example?: string, args: { name: string, defaultValue?: string, doc: string }[] @@ -36,6 +38,9 @@ export default class SpecialVisualizations { return new VariableUiElement(tags.map(tags => { const parts = []; for (const key in tags) { + if(!tags.hasOwnProperty(key)){ + continue; + } parts.push(key + "=" + tags[key]); } return parts.join("
") @@ -179,7 +184,7 @@ export default class SpecialVisualizations { } ] - static HelpMessage: UIElement = SpecialVisualizations.GenHelpMessage(); + static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); private static GenHelpMessage() { diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 980503db0..e63a7f8e3 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -6,11 +6,12 @@ import Combine from "./Base/Combine"; import State from "../State"; import {FixedUiElement} from "./Base/FixedUiElement"; import SpecialVisualizations from "./SpecialVisualizations"; +import BaseUIElement from "./BaseUIElement"; export class SubstitutedTranslation extends UIElement { private readonly tags: UIEventSource; private readonly translation: Translation; - private content: UIElement[]; + private content: BaseUIElement[]; private constructor( translation: Translation, @@ -54,7 +55,7 @@ export class SubstitutedTranslation extends UIElement { return new Combine(this.content); } - private CreateContent(): UIElement[] { + private CreateContent(): BaseUIElement[] { let txt = this.translation?.txt; if (txt === undefined) { return [] @@ -64,7 +65,7 @@ export class SubstitutedTranslation extends UIElement { return this.EvaluateSpecialComponents(txt); } - private EvaluateSpecialComponents(template: string): UIElement[] { + private EvaluateSpecialComponents(template: string): BaseUIElement[] { for (const knownSpecial of SpecialVisualizations.specialVisualizations) {