diff --git a/Logic/Web/ImgurUploader.ts b/Logic/Web/ImgurUploader.ts index b21228b5e..851b9c53f 100644 --- a/Logic/Web/ImgurUploader.ts +++ b/Logic/Web/ImgurUploader.ts @@ -3,9 +3,9 @@ import {Imgur} from "./Imgur"; export default class ImgurUploader { - public queue: UIEventSource; - public failed: UIEventSource; - public success: UIEventSource + public readonly queue: UIEventSource = new UIEventSource([]); + public readonly failed: UIEventSource = new UIEventSource([]); + public readonly success: UIEventSource = new UIEventSource([]); private readonly _handleSuccessUrl: (string) => void; constructor(handleSuccessUrl: (string) => void) { diff --git a/UI/Base/TabbedComponent.ts b/UI/Base/TabbedComponent.ts index 0244b0e7b..46c602132 100644 --- a/UI/Base/TabbedComponent.ts +++ b/UI/Base/TabbedComponent.ts @@ -15,8 +15,17 @@ export class TabbedComponent extends Combine { for (let i = 0; i < elements.length; i++) { let element = elements[i]; const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i)) + openedTabSrc.addCallbackAndRun(selected => { + if(selected === i){ + header.SetClass("tab-active") + header.RemoveClass("tab-non-active") + }else{ + header.SetClass("tab-non-active") + header.RemoveClass("tab-active") + } + }) const content = Translations.W(element.content) - content.SetClass("tab-content") + content.SetClass("relative p-4 w-full inline-block") contentElements.push(content); const tab = header.SetClass("block tab-single-header") tabs.push(tab) diff --git a/UI/BigComponents/LicensePicker.ts b/UI/BigComponents/LicensePicker.ts index 43d2cac1c..f87d8c775 100644 --- a/UI/BigComponents/LicensePicker.ts +++ b/UI/BigComponents/LicensePicker.ts @@ -1,6 +1,7 @@ import {DropDown} from "../Input/DropDown"; import Translations from "../i18n/Translations"; import State from "../../State"; +import {UIEventSource} from "../../Logic/UIEventSource"; export default class LicensePicker extends DropDown{ @@ -11,7 +12,7 @@ export default class LicensePicker extends DropDown{ {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") + State.state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource("CC0") ) this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left"); } diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 2528aa32b..4a29d6a0b 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import {SlideShow} from "./SlideShow"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; @@ -8,33 +7,35 @@ import {ImgurImage} from "./ImgurImage"; import {MapillaryImage} from "./MapillaryImage"; import BaseUIElement from "../BaseUIElement"; import Img from "../Base/Img"; +import Toggle from "../Input/Toggle"; -export class ImageCarousel extends UIElement{ +export class ImageCarousel extends Toggle { - public readonly slideshow: BaseUIElement; - - constructor(images: UIEventSource<{key: string, url:string}[]>, tags: UIEventSource) { - super(images); - const uiElements = images.map((imageURLS: {key: string, url:string}[]) => { + constructor(images: UIEventSource<{ key: string, url: string }[]>, tags: UIEventSource) { + const uiElements = images.map((imageURLS: { key: string, url: string }[]) => { const uiElements: BaseUIElement[] = []; for (const url of imageURLS) { let image = ImageCarousel.CreateImageElement(url.url) - if(url.key !== undefined){ + if (url.key !== undefined) { image = new Combine([ image, new DeleteImage(url.key, tags).SetClass("delete-image-marker absolute top-0 left-0 pl-3") ]).SetClass("relative"); } - image - .SetClass("w-full block") + image + .SetClass("w-full block") + .SetStyle("min-width: 50px; background: grey;") uiElements.push(image); } return uiElements; }); - this.slideshow = new SlideShow(uiElements); + super( + new SlideShow(uiElements).SetClass("w-full"), + undefined, + uiElements.map(els => els.length > 0) + ) this.SetClass("block w-full"); - this.slideshow.SetClass("w-full"); } /*** @@ -57,8 +58,4 @@ export class ImageCarousel extends UIElement{ return new Img(url); } } - - InnerRender() { - return this.slideshow; - } } \ No newline at end of file diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 1cdae491a..8de6ad4f9 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -1,5 +1,4 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import State from "../../State"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; @@ -13,22 +12,9 @@ import ImgurUploader from "../../Logic/Web/ImgurUploader"; import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; -export class ImageUploadFlow extends UIElement { - - private readonly _element: BaseUIElement; - - - private readonly _tags: UIEventSource; - private readonly _selectedLicence: UIEventSource; - - - private readonly _imagePrefix: string; +export class ImageUploadFlow extends Toggle { constructor(tagsSource: UIEventSource, imagePrefix: string = "image") { - super(State.state.osmConnection.userDetails); - this._imagePrefix = imagePrefix; - - const uploader = new ImgurUploader(url => { // A file was uploaded - we add it to the tags of the object @@ -50,9 +36,10 @@ export class ImageUploadFlow extends UIElement { 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") + Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1"), + Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3") + ]).SetClass("p-2 border-4 border-black rounded-full text-4xl font-bold h-full align-middle w-full flex justify-center") + const fileSelector = new FileSelectorButton(label) fileSelector.GetValue().addCallback(filelist => { if (filelist === undefined) { @@ -60,13 +47,13 @@ export class ImageUploadFlow extends UIElement { } console.log("Received images from the user, starting upload") - const license = this._selectedLicence.data ?? "CC0" + const license = licensePicker.GetValue().data ?? "CC0" - const tags = this._tags.data; + const tags = tagsSource.data; - const layout = State.state.layoutToUse.data + const layout = State.state?.layoutToUse?.data let matchingLayer: LayerConfig = undefined - for (const layer of layout.layers) { + for (const layer of layout?.layers ?? []) { if (layer.source.osmTags.matchesProperties(tags)) { matchingLayer = layer; break; @@ -90,30 +77,27 @@ export class ImageUploadFlow extends UIElement { const uploadFlow: BaseUIElement = new Combine([ fileSelector, - Translations.t.image.respectPrivacy.SetStyle("font-size:small;"), + Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"), licensePicker, uploadStateUi - ]).SetClass("image-upload-flow") - .SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;"); + ]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center") const pleaseLoginButton = t.pleaseLogin.Clone() .onClick(() => State.state.osmConnection.AttemptLogin()) .SetClass("login-button-friendly"); - this._element = new Toggle( + super( new Toggle( /*We can show the actual upload button!*/ uploadFlow, /* User not logged in*/ pleaseLoginButton, - State.state.osmConnection.userDetails.map(userinfo => userinfo.loggedIn) + State.state?.osmConnection?.isLoggedIn ), - undefined /* Nothing as the user badge is disabled*/, State.state.featureSwitchUserbadge + undefined /* Nothing as the user badge is disabled*/, + State.state.featureSwitchUserbadge ) } - protected InnerRender(): string | BaseUIElement { - return this._element; - } } \ No newline at end of file diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 8cf7b0719..692bb8748 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -1,41 +1,59 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; +import $ from "jquery" export class SlideShow extends BaseUIElement { - private readonly _element: HTMLElement; - - constructor( - embeddedElements: UIEventSource) { - super() - - const el = document.createElement("div") - this._element = el; - - el.classList.add("slick-carousel") - require("slick-carousel") - // @ts-ignore - el.slick({ - autoplay: true, - arrows: true, - dots: true, - lazyLoad: 'progressive', - variableWidth: true, - centerMode: true, - centerPadding: "60px", - adaptive: true - }); - embeddedElements.addCallbackAndRun(elements => { - for (const element of elements ?? []) { - element.SetClass("slick-carousel-content") - } - }); + private readonly embeddedElements: UIEventSource; + constructor(embeddedElements: UIEventSource) { + super() + this.embeddedElements = embeddedElements; } protected InnerConstructElement(): HTMLElement { - return this._element; + const el = document.createElement("div") + el.classList.add("slic-carousel") + + el.onchange = () => { + console.log("Parent is now ", el.parentElement) + } + + const mutationObserver = new MutationObserver(mutations => { + console.log("Mutations are: ", mutations) + + + mutationObserver.disconnect() + require("slick-carousel") + // @ts-ignore + el.slick({ + autoplay: true, + arrows: true, + dots: true, + lazyLoad: 'progressive', + variableWidth: true, + centerMode: true, + centerPadding: "60px", + adaptive: true + }); + }) + + mutationObserver.observe(el, { + childList: true, + characterData: true, + subtree: true + }) + + + this.embeddedElements.addCallbackAndRun(elements => { + for (const element of elements ?? []) { + element.SetClass("slick-carousel-content") + el.appendChild(element.ConstructElement()) + } + }); + + return el; } } \ No newline at end of file diff --git a/UI/Input/FileSelectorButton.ts b/UI/Input/FileSelectorButton.ts index 996479069..c6e1cf1f8 100644 --- a/UI/Input/FileSelectorButton.ts +++ b/UI/Input/FileSelectorButton.ts @@ -1,9 +1,10 @@ import BaseUIElement from "../BaseUIElement"; -import {InputElement} from "../Input/InputElement"; +import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; export default class FileSelectorButton extends InputElement { + private static _nextid; IsSelected: UIEventSource; private readonly _value = new UIEventSource(undefined); private readonly _label: BaseUIElement; @@ -13,6 +14,8 @@ export default class FileSelectorButton extends InputElement { super(); this._label = label; this._acceptType = acceptType; + this.SetClass("block cursor-pointer") + label.SetClass("cursor-pointer") } GetValue(): UIEventSource { @@ -26,36 +29,37 @@ export default class FileSelectorButton extends InputElement { 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; + const label = document.createElement("label") + label.appendChild(this._label.ConstructElement()) + el.appendChild(label) - actualInputElement.onchange = () => { - if (actualInputElement.files !== null) { - self._value.setData(actualInputElement.files) - } + const actualInputElement = document.createElement("input"); + actualInputElement.style.cssText = "display:none"; + actualInputElement.type = "file"; + actualInputElement.accept = this._acceptType; + actualInputElement.name = "picField"; + actualInputElement.multiple = true; + actualInputElement.id = "fileselector" + FileSelectorButton._nextid; + FileSelectorButton._nextid++; + + label.htmlFor = actualInputElement.id; + + 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; + el.addEventListener('submit', e => { + if (actualInputElement.files !== null) { + self._value.setData(actualInputElement.files) + } + e.preventDefault() + }) + + el.appendChild(actualInputElement) + + return el; } diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index fb0ecc32f..1f8f743d5 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -8,9 +8,10 @@ import Svg from "../../Svg"; import {VariableUiElement} from "../Base/VariableUIElement"; import {SaveButton} from "../Popup/SaveButton"; import CheckBoxes from "../Input/Checkboxes"; -import UserDetails from "../../Logic/Osm/OsmConnection"; +import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; import BaseUIElement from "../BaseUIElement"; import Toggle from "../Input/Toggle"; +import State from "../../State"; export default class ReviewForm extends InputElement { @@ -19,19 +20,19 @@ export default class ReviewForm extends InputElement { private readonly _stars: BaseUIElement; private _saveButton: BaseUIElement; private readonly _isAffiliated: BaseUIElement; - private userDetails: UIEventSource; private readonly _postingAs: BaseUIElement; + private readonly _osmConnection: OsmConnection; - constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), userDetails: UIEventSource) { + constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), osmConnection: OsmConnection) { super(); - this.userDetails = userDetails; + this._osmConnection = osmConnection; const t = Translations.t.reviews; this._value = new UIEventSource({ made_by_user: new UIEventSource(true), rating: undefined, comment: undefined, - author: userDetails.data.name, + author: osmConnection.userDetails.data.name, affiliated: false, date: new Date() }); @@ -48,7 +49,7 @@ export default class ReviewForm extends InputElement { const self = this; this._postingAs = - new Combine([t.posting_as, new VariableUiElement(userDetails.map((ud: UserDetails) => ud.name)).SetClass("review-author")]) + new Combine([t.posting_as, new VariableUiElement(osmConnection.userDetails.map((ud: UserDetails) => ud.name)).SetClass("review-author")]) .SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-left: auto;") this._saveButton = new SaveButton(this._value.map(r => self.IsValid(r)), undefined) @@ -100,10 +101,12 @@ export default class ReviewForm extends InputElement { Translations.t.reviews.tos.SetClass("subtle") ]) .SetClass("review-form") + + const connection = this._osmConnection; + const login = Translations.t.reviews.plz_login.Clone().onClick(() => connection.AttemptLogin()) - - return new Toggle(form, Translations.t.reviews.plz_login.Clone(), - this.userDetails.map(userdetails => userdetails.loggedIn)).ToggleOnClick() + return new Toggle(form,login , + connection.isLoggedIn) .ConstructElement() } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 1c46e4fc5..2bcae1027 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -107,7 +107,7 @@ export default class SpecialVisualizations { state.mangroveIdentity, state.osmConnection._dryRun ); - const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection.userDetails); + const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection); return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form); } }, @@ -160,7 +160,7 @@ export default class SpecialVisualizations { ], constr: (state: State, tagSource: UIEventSource, args) => { if (window.navigator.share) { - const title = state.layoutToUse.data.title.txt; + const title = state?.layoutToUse?.data?.title?.txt ?? "MapComplete"; let name = tagSource.data.name; if (name) { name = `${name} (${title})` @@ -174,7 +174,7 @@ export default class SpecialVisualizations { return new ShareButton(Svg.share_ui(), { title: name, url: url, - text: state.layoutToUse.data.shortDescription.txt + text: state?.layoutToUse?.data?.shortDescription?.txt ?? "MapComplete" }) } else { return new FixedUiElement("") diff --git a/css/tabbedComponent.css b/css/tabbedComponent.css index d895fc9ae..a4b7bc182 100644 --- a/css/tabbedComponent.css +++ b/css/tabbedComponent.css @@ -30,17 +30,6 @@ } -.tab-content { - z-index: 5002; - background-color: var(--background-color); - color: var(--foreground-color); - position: relative; - padding: 1em; - display: inline-block; - width: 100%; - box-sizing: border-box; -} - .tab-single-header { border-top-left-radius: 1em; border-top-right-radius: 1em; diff --git a/test.ts b/test.ts index 1ee32d409..46333efa3 100644 --- a/test.ts +++ b/test.ts @@ -1,27 +1,31 @@ -import {RadioButton} from "./UI/Input/RadioButton"; -import {FixedInputElement} from "./UI/Input/FixedInputElement"; -import {SubstitutedTranslation} from "./UI/SubstitutedTranslation"; import {UIEventSource} from "./Logic/UIEventSource"; -import {Translation} from "./UI/i18n/Translation"; -import TagRenderingAnswer from "./UI/Popup/TagRenderingAnswer"; -import TagRenderingConfig from "./Customizations/JSON/TagRenderingConfig"; -import EditableTagRendering from "./UI/Popup/EditableTagRendering"; +import SpecialVisualizations from "./UI/SpecialVisualizations"; +import State from "./State"; +import Combine from "./UI/Base/Combine"; +import {FixedUiElement} from "./UI/Base/FixedUiElement"; const tagsSource = new UIEventSource({ id:'id', name:'name', - surface:'asphalt' + surface:'asphalt', + image: "https://i.imgur.com/kX3rl3v.jpg", + "image:1": "https://i.imgur.com/kX3rl3v.jpg", + _country:"be", + // "opening_hours":"mo-fr 09:00-18:00" }) -const config = new TagRenderingConfig({ - render: "Rendering {name} {id} {surface}" -}, null, "test") +const state = new State(undefined) +State.state = state -new EditableTagRendering( - tagsSource, - config -).AttachTo("extradiv") +const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { + try{ - -window.v = tagsSource \ No newline at end of file + return new Combine([spec.funcName, spec.constr(state, tagsSource, spec.args.map(a => a.defaultValue ?? "")).SetClass("block")]) + .SetClass("flex flex-col border border-black p-2 m-2"); + }catch(e){ + console.error(e) + return new FixedUiElement("Could not construct "+spec.funcName+" due to "+e).SetClass("alert") + } +}) +new Combine(allSpecials).AttachTo("maindiv") \ No newline at end of file