Refactoring fullscreenhandling

This commit is contained in:
pietervdvn 2021-01-25 03:12:09 +01:00
parent e1a4c75391
commit 00f610c589
23 changed files with 346 additions and 245 deletions

View file

@ -12,7 +12,6 @@ import CenterMessageBox from "./UI/CenterMessageBox";
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import UserBadge from "./UI/BigComponents/UserBadge"; import UserBadge from "./UI/BigComponents/UserBadge";
import SearchAndGo from "./UI/BigComponents/SearchAndGo"; import SearchAndGo from "./UI/BigComponents/SearchAndGo";
import FullScreenMessageBox from "./UI/FullScreenMessageBoxHandler";
import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler"; import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import {Utils} from "./Utils"; import {Utils} from "./Utils";
@ -33,7 +32,6 @@ import FeatureSwitched from "./UI/Base/FeatureSwitched";
import LayerConfig from "./Customizations/JSON/LayerConfig"; import LayerConfig from "./Customizations/JSON/LayerConfig";
import ShowDataLayer from "./UI/ShowDataLayer"; import ShowDataLayer from "./UI/ShowDataLayer";
import Hash from "./Logic/Web/Hash"; import Hash from "./Logic/Web/Hash";
import HistoryHandling from "./Logic/Actors/HistoryHandling";
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
export class InitUiElements { export class InitUiElements {
@ -118,8 +116,6 @@ export class InitUiElements {
} }
new HistoryHandling(Hash.hash, State.state.fullScreenMessage);
InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => { InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => {
new UserBadge().AttachTo('userbadge'); new UserBadge().AttachTo('userbadge');
}); });
@ -129,9 +125,6 @@ export class InitUiElements {
}); });
new FullScreenMessageBox().AttachTo("messagesboxmobile");
InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => {
InitUiElements.InitWelcomeMessage() InitUiElements.InitWelcomeMessage()
}); });
@ -214,18 +207,18 @@ export class InitUiElements {
private static InitWelcomeMessage() { private static InitWelcomeMessage() {
const isOpened = new UIEventSource<boolean>(true); const isOpened = new UIEventSource<boolean>(true);
const fullOptions = new FullWelcomePaneWithTabs(() => isOpened.setData(false)); const fullOptions = new FullWelcomePaneWithTabs(() => {
console.log("Closing the welcome message...")
isOpened.setData(false);
});
// ?-Button on Desktop, opens panel with close-X. // ?-Button on Desktop, opens panel with close-X.
const help = Svg.help_svg().SetClass("open-welcome-button block"); const help = Svg.help_svg().SetClass("open-welcome-button block");
const close = Svg.close_svg().SetClass("close-welcome-button");
const checkbox = new CheckBox( const checkbox = new CheckBox(
new Combine([
close,
fullOptions fullOptions
.SetClass("welcomeMessage") .SetClass("welcomeMessage")
.onClick(() => {/*Catch the click*/ .onClick(() => {/*Catch the click*/
})]), }),
help help
, isOpened , isOpened
).AttachTo("messagesbox"); ).AttachTo("messagesbox");
@ -252,15 +245,8 @@ export class InitUiElements {
const layerControlPanel = new LayerControlPanel( const layerControlPanel = new LayerControlPanel(
() => State.state.layerControlIsOpened.setData(false)) () => State.state.layerControlIsOpened.setData(false))
.SetClass("block p-1 rounded-full"); .SetClass("block p-1 rounded-full");
const closeButton = Svg.close_svg()
.SetClass("layer-selection-toggle")
.SetStyle(" background: var(--subtle-detail-color);")
const checkbox = new CheckBox( const checkbox = new CheckBox(
new Combine([ layerControlPanel,
closeButton,
layerControlPanel])
.SetClass("flex flex-row")
,
Svg.layers_svg().SetClass("layer-selection-toggle"), Svg.layers_svg().SetClass("layer-selection-toggle"),
State.state.layerControlIsOpened State.state.layerControlIsOpened
).AttachTo("layer-selection"); ).AttachTo("layer-selection");
@ -388,7 +374,6 @@ export class InitUiElements {
State.state.selectedElement, State.state.selectedElement,
State.state.filteredLayers, State.state.filteredLayers,
State.state.leafletMap, State.state.leafletMap,
State.state.fullScreenMessage,
() => { () => {
return new SimpleAddUI( return new SimpleAddUI(
() => State.state.LastClickLocation.setData(undefined) () => State.state.LastClickLocation.setData(undefined)

View file

@ -17,7 +17,6 @@ export default class StrayClickHandler {
selectedElement: UIEventSource<string>, selectedElement: UIEventSource<string>,
filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource<boolean> }[]>, filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource<boolean> }[]>,
leafletMap: UIEventSource<L.Map>, leafletMap: UIEventSource<L.Map>,
fullscreenMessage: UIEventSource<{content: UIElement, hashText: string}>,
uiToShow: (() => UIElement)) { uiToShow: (() => UIElement)) {
this._uiToShow = uiToShow; this._uiToShow = uiToShow;
const self = this; const self = this;

View file

@ -3,31 +3,70 @@ import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import Translations from "../../UI/i18n/Translations"; import Translations from "../../UI/i18n/Translations";
import Locale from "../../UI/i18n/Locale"; import Locale from "../../UI/i18n/Locale";
import {UIElement} from "../../UI/UIElement"; import {UIElement} from "../../UI/UIElement";
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
import {ElementStorage} from "../ElementStorage";
import Combine from "../../UI/Base/Combine";
export default class TitleHandler{ class TitleElement extends UIElement {
constructor(layoutToUse: UIEventSource<LayoutConfig>, fullScreenMessage: UIEventSource<{ content: UIElement, hashText: string, titleText?: UIElement }>) { private readonly _layoutToUse: UIEventSource<LayoutConfig>;
private readonly _selectedFeature: UIEventSource<any>;
private readonly _allElementsStorage: ElementStorage;
constructor(layoutToUse: UIEventSource<LayoutConfig>,
layoutToUse.map((layoutToUse) => { selectedFeature: UIEventSource<any>,
return Translations.WT(layoutToUse?.title)?.txt ?? "MapComplete" allElementsStorage : ElementStorage) {
}, [Locale.language] super(layoutToUse);
).addCallbackAndRun((title) => { this._layoutToUse = layoutToUse;
document.title = title this._selectedFeature = selectedFeature;
}); this._allElementsStorage = allElementsStorage;
this.ListenTo(Locale.language);
fullScreenMessage.addCallbackAndRun(selected => { this.dumbMode = false;
const title = Translations.WT(layoutToUse.data?.title)?.txt ?? "MapComplete"
if(selected?.titleText?.data === undefined){
document.title = title
}else{
selected.titleText.Update();
var d = document.createElement('div');
d.innerHTML = selected.titleText.InnerRender();
const poi = (d.textContent || d.innerText)
document.title = title + " | " + poi;
} }
})
InnerRender(): string {
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ?? "MapComplete"
const feature = this._selectedFeature.data;
if(feature === undefined){
return defaultTitle;
}
const layout = this._layoutToUse.data;
const properties = this._selectedFeature.data.properties;
for (const layer of layout.layers) {
if(layer.title === undefined){
continue;
}
if (layer.overpassTags.matchesProperties(properties)) {
const title = new TagRenderingAnswer(
this._allElementsStorage.getEventSourceFor(feature),
layer.title
)
return new Combine([defaultTitle," | ", title]).Render();
}
}
return defaultTitle;
}
}
export default class TitleHandler {
constructor(layoutToUse: UIEventSource<LayoutConfig>,
selectedFeature: UIEventSource<any>,
allElementsStorage : ElementStorage) {
new TitleElement(layoutToUse, selectedFeature, allElementsStorage)
.addCallbackAndRun(contents => {
const d = document.createElement('div');
d.innerHTML = contents;
// We pass everything into a div to strip out images etc...
document.title = (d.textContent || d.innerText);
});
} }
} }

View file

@ -74,7 +74,7 @@ export default class MetaTagging {
const tagsSource = State.state.allElements.getEventSourceFor(feature); const tagsSource = State.state.allElements.getEventSourceFor(feature);
tagsSource.ping(); tagsSource.ping();
} catch (e) { } catch (e) {
console.error(e) console.warn(e)
} }
}); });
} }

View file

@ -4,7 +4,7 @@ import {Utils} from "../../Utils";
export class OsmPreferences { export class OsmPreferences {
public preferences = new UIEventSource<any>({}); public preferences = new UIEventSource<any>({}, "all-osm-preferences");
public preferenceSources: any = {} public preferenceSources: any = {}
private auth: any; private auth: any;
private userDetails: UIEventSource<UserDetails>; private userDetails: UIEventSource<UserDetails>;
@ -29,7 +29,7 @@ export class OsmPreferences {
return this.longPreferences[prefix + key]; return this.longPreferences[prefix + key];
} }
const source = new UIEventSource<string>(undefined); const source = new UIEventSource<string>(undefined, "long-osm-preference:"+prefix+key);
this.longPreferences[prefix + key] = source; this.longPreferences[prefix + key] = source;
const allStartWith = prefix + key + "-combined"; const allStartWith = prefix + key + "-combined";
@ -106,7 +106,7 @@ export class OsmPreferences {
if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) { if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
this.UpdatePreferences(); this.UpdatePreferences();
} }
const pref = new UIEventSource<string>(this.preferences.data[key]); const pref = new UIEventSource<string>(this.preferences.data[key],"osm-preference:"+key);
pref.addCallback((v) => { pref.addCallback((v) => {
this.SetPreference(key, v); this.SetPreference(key, v);
}); });

View file

@ -1,14 +1,64 @@
export class UIEventSource<T>{ export class UIEventSource<T> {
public data: T; public data: T;
private readonly tag: string;
private _callbacks = []; private _callbacks = [];
constructor(data: T) { private static allSources : UIEventSource<any>[] = UIEventSource.PrepPerf();
static PrepPerf(){
// @ts-ignore
window.mcperf = () => {
console.log(UIEventSource.allSources.length, "uieventsources created");
const copy = [...UIEventSource.allSources];
copy.sort((a,b) => b._callbacks.length - a._callbacks.length);
console.log("Topten is:")
for (let i = 0; i < 10; i++) {
console.log(copy[i].tag, copy[i]);
}
}
return [];
}
constructor(data: T, tag: string = "") {
this.tag = tag;
this.data = data; this.data = data;
UIEventSource.allSources.push(this);
}
public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data);
source.addCallback((latestData) => {
sink.setData(latestData?.data);
});
for (const possibleSource of possibleSources) {
possibleSource?.addCallback(() => {
sink.setData(source.data?.data);
})
}
return sink;
}
public static Chronic(millis: number, asLong: () => boolean = undefined): UIEventSource<Date> {
const source = new UIEventSource<Date>(undefined);
function run() {
source.setData(new Date());
if (asLong === undefined || asLong()) {
window.setTimeout(run, millis);
}
}
run();
return source;
} }
public addCallback(callback: ((latestData: T) => void)): UIEventSource<T> { public addCallback(callback: ((latestData: T) => void)): UIEventSource<T> {
if(callback === console.log){ if (callback === console.log) {
// This ^^^ actually works! // This ^^^ actually works!
throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead."
} }
@ -36,25 +86,9 @@ export class UIEventSource<T>{
} }
} }
public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data);
source.addCallback((latestData) => {
sink.setData(latestData?.data);
});
for (const possibleSource of possibleSources) {
possibleSource?.addCallback(() => {
sink.setData(source.data?.data);
})
}
return sink;
}
public map<J>(f: ((T) => J), public map<J>(f: ((T) => J),
extraSources: UIEventSource<any>[] = [], extraSources: UIEventSource<any>[] = [],
g: ((J) => T) = undefined ): UIEventSource<J> { g: ((J) => T) = undefined): UIEventSource<J> {
const self = this; const self = this;
const newSource = new UIEventSource<J>( const newSource = new UIEventSource<J>(
@ -70,7 +104,7 @@ export class UIEventSource<T>{
extraSource?.addCallback(update); extraSource?.addCallback(update);
} }
if(g !== undefined) { if (g !== undefined) {
newSource.addCallback((latest) => { newSource.addCallback((latest) => {
self.setData(g(latest)); self.setData(g(latest));
}) })
@ -79,7 +113,6 @@ export class UIEventSource<T>{
return newSource; return newSource;
} }
public syncWith(otherSource: UIEventSource<T>, reverseOverride = false): UIEventSource<T> { public syncWith(otherSource: UIEventSource<T>, reverseOverride = false): UIEventSource<T> {
this.addCallback((latest) => otherSource.setData(latest)); this.addCallback((latest) => otherSource.setData(latest));
const self = this; const self = this;
@ -94,7 +127,7 @@ export class UIEventSource<T>{
return this; return this;
} }
public stabilized(millisToStabilize) : UIEventSource<T>{ public stabilized(millisToStabilize): UIEventSource<T> {
const newSource = new UIEventSource<T>(this.data); const newSource = new UIEventSource<T>(this.data);
@ -103,7 +136,7 @@ export class UIEventSource<T>{
currentCallback++; currentCallback++;
const thisCallback = currentCallback; const thisCallback = currentCallback;
window.setTimeout(() => { window.setTimeout(() => {
if(thisCallback === currentCallback){ if (thisCallback === currentCallback) {
newSource.setData(latestData); newSource.setData(latestData);
} }
}, millisToStabilize) }, millisToStabilize)
@ -112,19 +145,4 @@ export class UIEventSource<T>{
return newSource; return newSource;
} }
public static Chronic(millis: number, asLong: () => boolean = undefined): UIEventSource<Date> {
const source = new UIEventSource<Date>(undefined);
function run() {
source.setData(new Date());
if (asLong === undefined || asLong()) {
window.setTimeout(run, millis);
}
}
run();
return source;
}
} }

View file

@ -5,7 +5,7 @@ export class LocalStorageSource {
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
try { try {
const saved = localStorage.getItem(key); const saved = localStorage.getItem(key);
const source = new UIEventSource<string>(saved ?? defaultValue); const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:"+key);
source.addCallback((data) => { source.addCallback((data) => {
localStorage.setItem(key, data); localStorage.setItem(key, data);

View file

@ -72,10 +72,6 @@ export default class State {
* The message that should be shown at the center of the screen * The message that should be shown at the center of the screen
*/ */
public readonly centerMessage = new UIEventSource<string>(""); public readonly centerMessage = new UIEventSource<string>("");
/**
This message is shown full screen on mobile devices
*/
public readonly fullScreenMessage = new UIEventSource<{ content: UIElement, hashText: string, titleText?: UIElement }>(undefined)
/** /**
The latest element that was selected - used to generate the right UI at the right place The latest element that was selected - used to generate the right UI at the right place
@ -244,7 +240,7 @@ export default class State {
} }
}).ping() }).ping()
new TitleHandler(this.layoutToUse, this.fullScreenMessage); new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements);
this.allElements = new ElementStorage(); this.allElements = new ElementStorage();

View file

@ -1,22 +1,25 @@
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
export default class LazyElement extends UIElement { export default class LazyElement<T extends UIElement> extends UIElement {
private _content: UIElement = undefined; public Activate: (onElement?: (element: T) => void) => void;
private _content: T = undefined;
private readonly _loadingContent: string;
public Activate: () => void; constructor(content: (() => T), loadingContent = "Rendering...") {
private _loadingContent: string;
constructor(content: (() => UIElement), loadingContent = "Rendering...") {
super(); super();
this._loadingContent = loadingContent; this._loadingContent = loadingContent;
this.dumbMode = false; this.dumbMode = false;
const self = this; const self = this;
this.Activate = () => { this.Activate = (onElement?: (element: T) => void) => {
console.log("ACTIVATED")
if (this._content === undefined) { if (this._content === undefined) {
self._content = content(); self._content = content();
} }
if (onElement) {
onElement(self._content)
}
self.Update(); self.Update();
} }
} }
@ -28,4 +31,6 @@ export default class LazyElement extends UIElement {
return this._content.InnerRender(); return this._content.InnerRender();
} }
} }

View file

@ -1,6 +1,5 @@
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import Svg from "../../Svg"; import Svg from "../../Svg";
import State from "../../State";
import Combine from "./Combine"; import Combine from "./Combine";
import Ornament from "./Ornament"; import Ornament from "./Ornament";
@ -8,22 +7,38 @@ import Ornament from "./Ornament";
* Wraps some contents into a panel that scrolls the content _under_ the title * Wraps some contents into a panel that scrolls the content _under_ the title
*/ */
export default class ScrollableFullScreen extends UIElement { export default class ScrollableFullScreen extends UIElement {
private static _isInited = false;
private title: UIElement;
private content: UIElement;
private _component: UIElement; private _component: UIElement;
private elementsToRestore: Set<HTMLElement> = new Set<HTMLElement>();
constructor(title: UIElement, content: UIElement, onClose: (() => void)) { constructor(title: UIElement, content: UIElement, onClose: (() => void)) {
super(); super();
this.dumbMode = false; this.content = content;
const returnToTheMap = Svg.back_svg().onClick(() => { this.title = title;
console.log("Clicked back!"); if (!ScrollableFullScreen._isInited) {
this.RestoreLeaflet(); ScrollableFullScreen._isInited = ScrollableFullScreen.PreparePatchesForFullscreen();
if (onClose() !== undefined) {
console.error("WARNING: onClose is not defined")
onClose();
} }
}).SetClass("block sm:hidden mb-2 bg-blue-50 rounded-full w-12 h-12 p-1.5") if (onClose === undefined) {
console.error("ScrollableFullScreen initialized without onClose!")
}
this.dumbMode = false;
const returnToTheMap =
new Combine([
Svg.back_svg().SetClass("block sm:hidden"),
Svg.close_svg().SetClass("hidden sm:block")
])
.onClick(() => {
console.log("Clicked back!");
ScrollableFullScreen.RestoreLeaflet();
if (onClose !== undefined) {
onClose();
} else {
console.error("WARNING: onClose is not defined")
}
}).SetClass("mb-2 bg-blue-50 rounded-full w-12 h-12 p-1.5")
title.SetClass("block w-full") title.SetClass("block w-full text-2xl font-bold p-2 pl-4")
const ornament = new Combine([new Ornament().SetStyle("height:5em;")]) const ornament = new Combine([new Ornament().SetStyle("height:5em;")])
.SetClass("block sm:hidden h-5") .SetClass("block sm:hidden h-5")
@ -32,39 +47,110 @@ export default class ScrollableFullScreen extends UIElement {
new Combine([ new Combine([
new Combine([ new Combine([
new Combine([returnToTheMap, title]) new Combine([returnToTheMap, title])
.AddClass("border-b-2 border-black shadow sm:shadow-none bg-white p-2 pb-0 sm:p-0 flex overflow-x-hidden flex-shrink-0 max-h-20vh"), .SetClass("border-b-2 border-black shadow sm:shadow-none bg-white p-2 pb-0 sm:p-0 flex overflow-x-hidden flex-shrink-0 max-h-20vh"),
new Combine(["<span>", content, "</span>", ornament]) new Combine(["<span>", content, "</span>", ornament])
.SetClass("block p-2 sm:pt-4 w-full h-screen landscape:h-screen sm:h-full sm:w-full overflow-y-auto overflow-x-hidden"), .SetClass("block p-2 sm:pt-4 w-full h-screen landscape:h-screen sm:h-full sm:w-full overflow-y-auto overflow-x-hidden"),
// We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide // We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide
]).SetClass("block flex flex-col relative bg-white") ]).SetClass("block flex flex-col relative bg-white")
]).SetClass("fixed top-0 left-0 right-0 h-screen w-screen sm:max-h-65vh sm:w-auto"); ]).SetClass("fixed top-0 left-0 right-0 h-screen w-screen sm:max-h-65vh sm:w-auto sm:relative");
} }
private static HideClutter(htmlElement: HTMLElement) {
const whiteList = new Set<Element>();
do {
if(htmlElement === null){
break;
}
if (htmlElement.classList.contains("clutter")) {
// Don't hide the parent element
whiteList.add(htmlElement)
}
htmlElement = htmlElement.parentElement;
} while (htmlElement != null)
const clutter = document.getElementsByClassName("clutter");
for (let i = 0; i < clutter.length; ++i) {
if (whiteList.has(clutter[i])) {
continue;
}
const classlist = clutter[i].classList;
if (classlist.contains("clutter-hidden")) {
continue;
}
classlist.add("clutter-hidden");
}
}
/**
* Adds the 'clutter' class (which merely acts as a tag) onto some elements, e.g. the leaflet attributions
* @constructor
*/
private static PreparePatchesForFullscreen(): boolean {
const toHide = document.getElementsByClassName("leaflet-control-container");
for (let i = 0; i < toHide.length; ++i) {
toHide[i].classList.add("clutter");
}
return true;
}
private static PatchLeaflet(htmlElement) {
if(htmlElement === null){
return;
}
do {
// A leaflet workaround: in order for fullscreen to work, we need to get the parent element which does a transform3d and remove/read the transform
if (htmlElement.style.transform !== "") {
if (!htmlElement.classList.contains("no-transform")) {
htmlElement.classList.add("no-transform");
htmlElement.classList.add("scrollable-fullscreen-no-transform")
}
}
htmlElement = htmlElement.parentElement;
} while (htmlElement != null)
}
private static RestoreLeaflet() {
console.log("Restoring")
const noTransf = document.getElementsByClassName("scrollable-fullscreen-no-transform");
for (let i = 0; i < noTransf.length; ++i) {
noTransf[i].classList.remove("no-transform");
noTransf[i].classList.remove("scrollable-fullscreen-no-transform");
}
let clutter = document.getElementsByClassName("clutter-hidden");
do {
for (let i = 0; i < clutter.length; ++i) {
clutter[i].classList.remove("clutter-hidden");
}
clutter = document.getElementsByClassName("clutter-hidden");
} while (clutter.length > 0)
}
InnerRender(): string { InnerRender(): string {
return this._component.Render(); return this._component.Render();
} }
Update() {
console.log("Updating the scrollableFullScreen")
super.Update();
this._component.Update();
}
public PrepFullscreen(htmlElement = undefined) {
htmlElement = htmlElement ?? document.getElementById(this.id);
ScrollableFullScreen.PatchLeaflet(htmlElement);
ScrollableFullScreen.HideClutter(htmlElement);
}
protected InnerUpdate(htmlElement: HTMLElement) { protected InnerUpdate(htmlElement: HTMLElement) {
this.PrepFullscreen(htmlElement)
do {
// A leaflet workaround: in order for fullscreen to work, we need to get the parent element which does a transform3d and remove/read the transform
if (htmlElement.style.transform !== "") {
this.elementsToRestore.add(htmlElement);
htmlElement.classList.add("no-transform")
}
htmlElement = htmlElement.parentElement;
} while (htmlElement != null)
super.InnerUpdate(htmlElement); super.InnerUpdate(htmlElement);
} }
private RestoreLeaflet() {
this.elementsToRestore.forEach(
el => el.classList.remove("no-transform")
);
}
} }

View file

@ -24,9 +24,9 @@ export class SubtleButton extends UIElement{
} else { } else {
img = imageUrl; img = imageUrl;
} }
img.AddClass("block flex items-center justify-center h-11 w-11 flex-shrink0") img.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0")
this.image = new Combine([img]) this.image = new Combine([img])
.AddClass("flex-shrink-0"); .SetClass("flex-shrink-0");
} }
@ -53,7 +53,7 @@ export class SubtleButton extends UIElement{
return new Combine([ return new Combine([
this.image, this.image,
this.message, this.message,
]).AddClass("block flex p-3 my-2 bg-blue-100 rounded-lg hover:shadow-xl hover:bg-blue-200") ]).SetClass("block flex p-3 my-2 bg-blue-100 rounded-lg hover:shadow-xl hover:bg-blue-200")
.Render(); .Render();
} }

View file

@ -7,22 +7,22 @@ export default class IndexText extends Combine {
constructor() { constructor() {
super([ super([
new FixedUiElement(`<img class="w-12 h-12 sm:h-24 sm:w-24" src="./assets/svg/logo.svg" alt="MapComplete Logo">`) new FixedUiElement(`<img class="w-12 h-12 sm:h-24 sm:w-24" src="./assets/svg/logo.svg" alt="MapComplete Logo">`)
.AddClass("flex-none m-3"), .SetClass("flex-none m-3"),
new Combine([ new Combine([
Translations.t.index.title Translations.t.index.title
.AddClass("text-2xl tracking-tight font-extrabold text-gray-900 sm:text-5xl md:text-6xl block text-gray-800 xl:inline"), .SetClass("text-2xl tracking-tight font-extrabold text-gray-900 sm:text-5xl md:text-6xl block text-gray-800 xl:inline"),
Translations.t.index.intro.AddClass( Translations.t.index.intro.SetClass(
"mt-3 text-base font-semibold text-gray-500 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0"), "mt-3 text-base font-semibold text-gray-500 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0"),
Translations.t.index.pickTheme.AddClass("mt-3 text-base text-green-600 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0") Translations.t.index.pickTheme.SetClass("mt-3 text-base text-green-600 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0")
]).AddClass("flex flex-col sm:text-center lg:text-left m-1 mt-2 md:m-2 md:mt-4") ]).SetClass("flex flex-col sm:text-center lg:text-left m-1 mt-2 md:m-2 md:mt-4")
]); ]);
this.AddClass("flex flex-row"); this.SetClass("flex flex-row");
} }
} }

View file

@ -22,7 +22,7 @@ export default class SearchAndGo extends UIElement {
); );
private _foundEntries = new UIEventSource([]); private _foundEntries = new UIEventSource([]);
private _goButton = Svg.search_ui().AddClass('w-8 h-8 full-rounded border-black float-right'); private _goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right');
constructor() { constructor() {
super(undefined); super(undefined);

View file

@ -1,32 +0,0 @@
import {UIElement} from "./UIElement";
import State from "../State";
import Combine from "./Base/Combine";
/**
* Handles the full screen popup on mobile
*/
export default class FullScreenMessageBox extends UIElement {
private _content: UIElement;
constructor() {
super(State.state.fullScreenMessage);
this.HideOnEmpty(true);
}
InnerRender(): string {
if (State.state.fullScreenMessage.data === undefined) {
return "";
}
this._content = State.state.fullScreenMessage.data.content;
return new Combine([this._content])
.SetClass("block max-h-screen h-screen overflow-x-hidden overflow-y-auto bg-white p-0").Render();
}
protected InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
}
}

View file

@ -9,29 +9,41 @@ import State from "../../State";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import ScrollableFullScreen from "../Base/ScrollableFullScreen";
export default class FeatureInfoBox extends UIElement { export default class FeatureInfoBox extends ScrollableFullScreen {
private _component: UIElement;
public title: UIElement ;
constructor( constructor(
tags: UIEventSource<any>, tags: UIEventSource<any>,
layerConfig: LayerConfig, layerConfig: LayerConfig,
onClose: () => {} onClose: () => void
) { ) {
super(); super(
FeatureInfoBox.GenerateTitleBar(tags, layerConfig),
FeatureInfoBox.GenerateContent(tags, layerConfig),
onClose
);
if (layerConfig === undefined) { if (layerConfig === undefined) {
throw "Undefined layerconfig" throw "Undefined layerconfig"
} }
}
private static GenerateTitleBar( tags: UIEventSource<any>,
layerConfig: LayerConfig): UIElement{
const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI", undefined)) const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI", undefined))
.AddClass("text-2xl break-words font-bold p-2"); .SetClass("text-2xl break-words font-bold p-2");
this.title = title;
const titleIcons = new Combine( const titleIcons = new Combine(
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon) layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon)
.AddClass("block w-8 h-8 align-baseline box-content p-0.5"))) .SetClass("block w-8 h-8 align-baseline box-content p-0.5")))
.AddClass("flex flex-row flex-wrap pt-1 items-center mr-2"); .SetClass("flex flex-row flex-wrap pt-1 items-center mr-2");
return new Combine([
new Combine([title, titleIcons]).SetClass("flex flex-grow justify-between")
])
}
private static GenerateContent(tags: UIEventSource<any>,
layerConfig: LayerConfig): UIElement{
let questionBox: UIElement = undefined; let questionBox: UIElement = undefined;
if (State.state.featureSwitchUserbadge.data) { if (State.state.featureSwitchUserbadge.data) {
@ -53,20 +65,12 @@ export default class FeatureInfoBox extends UIElement {
} }
const tail = new Combine([]).SetClass("only-on-mobile"); const tail = new Combine([]).SetClass("only-on-mobile");
const content = new Combine([ return new Combine([
...renderings, ...renderings,
tail.SetClass("featureinfobox-tail") tail.SetClass("featureinfobox-tail")
] ]
) )
const titleBar = new Combine([
new Combine([title, titleIcons]).SetClass("flex flex-grow justify-between")
])
this._component = new ScrollableFullScreen(titleBar, content, onClose)
}
InnerRender(): string {
return this._component.Render();
} }
} }

View file

@ -21,7 +21,7 @@ export default class TagRenderingAnswer extends UIElement {
if (configuration === undefined) { if (configuration === undefined) {
throw "Trying to generate a tagRenderingAnswer without configuration..." throw "Trying to generate a tagRenderingAnswer without configuration..."
} }
this.AddClass("flex items-center flex-row text-lg") this.SetClass("flex items-center flex-row text-lg")
} }
InnerRender(): string { InnerRender(): string {

View file

@ -88,7 +88,7 @@ export default class TagRenderingQuestion extends UIElement {
return tags.asHumanString(true, true); return tags.asHumanString(true, true);
} }
) )
).AddClass("block") ).SetClass("block")
} }
private GenerateInputElement(): InputElement<TagsFilter> { private GenerateInputElement(): InputElement<TagsFilter> {

View file

@ -45,7 +45,7 @@ export default class ShowDataLayer {
let geoLayer = self.CreateGeojsonLayer(feats) let geoLayer = self.CreateGeojsonLayer(feats)
if (layoutToUse.clustering.minNeededElements <= features.data.length) { if (layoutToUse.clustering.minNeededElements <= features.data.length) {
const cl = window["L"]; // This is a dirty workaround, the clustering plugin binds to the L of the window, not of the namespace or something const cl = window["L"]; // This is a dirty workaround, the clustering plugin binds to the L of the window, not of the namespace or something
const cluster = cl.markerClusterGroup({ disableClusteringAtZoom: layoutToUse.clustering.maxZoom }); const cluster = cl.markerClusterGroup({disableClusteringAtZoom: layoutToUse.clustering.maxZoom});
cluster.addLayer(geoLayer); cluster.addLayer(geoLayer);
geoLayer = cluster; geoLayer = cluster;
} }
@ -60,22 +60,22 @@ export default class ShowDataLayer {
features.addCallbackAndRun(() => update()); features.addCallbackAndRun(() => update());
leafletMap.addCallback(() => update()); leafletMap.addCallback(() => update());
State.state.selectedElement.addCallback(feature => { State.state.selectedElement.addCallback(feature => {
if(feature === undefined){ if (feature === undefined) {
return; return;
} }
const id = feature.properties.id+feature.geometry.type+feature._matching_layer_id; const id = feature.properties.id + feature.geometry.type + feature._matching_layer_id;
const action = self._onSelectedTrigger[id]; const action = self._onSelectedTrigger[id];
if(action){ if (action) {
action(); action();
} }
}); });
Hash.hash.addCallbackAndRun(id => { Hash.hash.addCallbackAndRun(id => {
// This is a bit of an edge case: if the hash becomes an id to search, we have to show the corresponding popup // This is a bit of an edge case: if the hash becomes an id to search, we have to show the corresponding popup
if(State.state.selectedElement !== undefined){ if (State.state.selectedElement !== undefined) {
return; // Something is already selected, we don't have to apply this fix return; // Something is already selected, we don't have to apply this fix
} }
const action = self._onSelectedTrigger[id]; const action = self._onSelectedTrigger[id];
if(action){ if (action) {
action(); action();
} }
}) })
@ -126,11 +126,16 @@ export default class ShowDataLayer {
const tags = State.state.allElements.getEventSourceFor(feature); const tags = State.state.allElements.getEventSourceFor(feature);
const uiElement: LazyElement = new LazyElement(() => new FeatureInfoBox(tags, layer, () => popup.closePopup()), const uiElement: LazyElement<FeatureInfoBox> = new LazyElement(() => new FeatureInfoBox(tags, layer, () => {
console.log("Closing the popup!")
State.state.selectedElement.setData(undefined);
popup.remove();
}),
"<div style='height: 90vh'>Rendering</div>"); "<div style='height: 90vh'>Rendering</div>");
popup.setContent(uiElement.Render()); popup.setContent(uiElement.Render());
popup.on('remove', () => { popup.on('remove', () => {
if(!popup.isOpen()){ if (!popup.isOpen()) {
return; return;
} }
State.state.selectedElement.setData(undefined); State.state.selectedElement.setData(undefined);
@ -140,27 +145,29 @@ export default class ShowDataLayer {
// But at least it'll be visible already // But at least it'll be visible already
leafletLayer.on("click", (e) => { leafletLayer.on("click", () => {
// We set the element as selected... // We set the element as selected...
uiElement.Activate();
uiElement.Activate(e => e.PrepFullscreen());
State.state.selectedElement.setData(feature); State.state.selectedElement.setData(feature);
}); });
const id = feature.properties.id+feature.geometry.type+feature._matching_layer_id; const id = feature.properties.id + feature.geometry.type + feature._matching_layer_id;
this._onSelectedTrigger[id] this._onSelectedTrigger[id]
= () => { = () => {
if(popup.isOpen()){ if (popup.isOpen()) {
return; return;
} }
leafletLayer.openPopup(); leafletLayer.openPopup();
uiElement.Activate(); uiElement.Activate(e => e.PrepFullscreen());
State.state.selectedElement.setData(feature); State.state.selectedElement.setData(feature);
} }
this._onSelectedTrigger[feature.properties.id.replace("/","_")] = this._onSelectedTrigger[id]; this._onSelectedTrigger[feature.properties.id.replace("/", "_")] = this._onSelectedTrigger[id];
if (feature.properties.id.replace(/\//g, "_") === Hash.hash.data) { if (feature.properties.id.replace(/\//g, "_") === Hash.hash.data && State.state.selectedElement.data === undefined) {
// This element is in the URL, so this is a share link // This element is in the URL, so this is a share link
// We open the relevant popup straight away // We open the relevant popup straight away
uiElement.Activate(); console.log("Opening the popup due to sharelink")
uiElement.Activate(e => e.PrepFullscreen());
popup.setContent(uiElement.Render()); popup.setContent(uiElement.Render());
const center = GeoOperations.centerpoint(feature).geometry.coordinates; const center = GeoOperations.centerpoint(feature).geometry.coordinates;

View file

@ -149,16 +149,12 @@ export abstract class UIElement extends UIEventSource<string> {
return this.InnerRender() === ""; return this.InnerRender() === "";
} }
public SetClass(clss: string): UIElement {
return this.AddClass(clss);
}
/** /**
* Adds all the relevant classes, space seperated * Adds all the relevant classes, space seperated
* @param clss * @param clss
* @constructor * @constructor
*/ */
public AddClass(clss: string) { public SetClass(clss: string) {
this.dumbMode = false; this.dumbMode = false;
const all = clss.split(" "); const all = clss.split(" ");
let recordedChange = false; let recordedChange = false;

View file

@ -79,10 +79,3 @@
max-height: 30vh; max-height: 30vh;
border-radius: 1em; border-radius: 1em;
} }
.hidden {
/* This is used by the slideshow, to hide non-active slides*/
display: none !important;
}

View file

@ -32,15 +32,21 @@
} }
@media only screen and (max-width: 640px), only screen and (max-height: 640px) { @media only screen and (max-width: 640px) {
.no-transform { .no-transform {
/*This is a workaround to let popup contents escape the popup on mobile*/ /*This is a workaround to let popup contents escape the popup on mobile - see scrollableFullScreen.ts*/
transform: none !important; transform: none !important;
} }
.clutter-hidden {
/*This is a workaround to let popup contents escape the popup on mobile - see scrollableFullScreen.ts*/
visibility: hidden !important;
}
} }
:root { :root {
--subtle-detail-color: #e5f5ff; --subtle-detail-color: #e5f5ff;
--subtle-detail-color-contrast: black; --subtle-detail-color-contrast: black;
@ -57,6 +63,11 @@
--return-to-the-map-height: 2em; --return-to-the-map-height: 2em;
} }
.hide-when-fullscreen-is-shown {
/*Clutter is actually a class indicating that the element should be hidden when a scrollableFullScreen is opened
It doesn't actually define any rules*/
}
html, body { html, body {
height: 100%; height: 100%;
min-height: 100vh; min-height: 100vh;
@ -333,7 +344,6 @@ a {
.welcomeMessage { .welcomeMessage {
display: block; display: block;
margin-left: 4em;
max-width: calc(100vw - 5em); max-width: calc(100vw - 5em);
width: 40em; width: 40em;
max-height: calc(100vh - 15em); max-height: calc(100vh - 15em);

View file

@ -44,11 +44,7 @@
<!-- DECORATION 0 END --> <!-- DECORATION 0 END -->
</div> </div>
<div class="only-on-mobile"> <div id="topleft-tools" class="z-index-above-map clutter">
<div id="messagesboxmobile"></div>
</div>
<div id="topleft-tools" class="z-index-above-map">
<div id="userbadge-and-search" class="p-3"> <div id="userbadge-and-search" class="p-3">
<div id="userbadge" class="shadow rounded-3xl overflow-hidden"></div> <div id="userbadge" class="shadow rounded-3xl overflow-hidden"></div>
<div id="searchbox" class="shadow rounded-3xl overflow-hidden"></div> <div id="searchbox" class="shadow rounded-3xl overflow-hidden"></div>
@ -57,13 +53,13 @@
<div id="help-button-mobile"></div> <div id="help-button-mobile"></div>
</div> </div>
<div id="layer-selection" class="absolute bottom-3 left-3 rounded-3xl overflow-hidden"></div> <div id="layer-selection" class="absolute bottom-3 left-3 rounded-3xl overflow-hidden clutter"></div>
<div id="centermessage" class="absolute rounded-3xl h-24 left-24 right-24 top-56 bg-white p-3 pt-5 sm:pt-8 text-xl font-bold text-center"> <div id="centermessage" class="absolute rounded-3xl h-24 left-24 right-24 top-56 bg-white p-3 pt-5 sm:pt-8 text-xl font-bold text-center">
Loading MapComplete, hang on... Loading MapComplete, hang on...
</div> </div>
<div id="geolocate-button"></div> <div id="geolocate-button" class="clutter"></div>
<div id="leafletDiv"></div> <div id="leafletDiv"></div>
<script src="./index.ts"></script> <script src="./index.ts"></script>

View file

@ -119,11 +119,10 @@ if (layoutFromBase64.startsWith("wiki:")) {
} else { } else {
// We fall through: no theme loaded: just show a few buttons // We fall through: no theme loaded: just show a few buttons
State.state = new State(undefined); State.state = new State(undefined);
document.getElementById("messagesboxmobile").remove();
new Combine([new MoreScreen(true) new Combine([new MoreScreen(true)
.SetStyle("pointer-events: all;"), .SetStyle("pointer-events: all;"),
Translations.t.general.openStreetMapIntro Translations.t.general.openStreetMapIntro
]).AddClass("block m-5 lg:w-3/4 lg:ml-40") ]).SetClass("block m-5 lg:w-3/4 lg:ml-40")
.AttachTo("topleft-tools"); .AttachTo("topleft-tools");
} }
window.addEventListener('contextmenu', function (e) { // Not compatible with IE < 9 window.addEventListener('contextmenu', function (e) { // Not compatible with IE < 9