forked from MapComplete/MapComplete
Merge branches
This commit is contained in:
commit
00fb99defe
117 changed files with 3104 additions and 1424 deletions
|
@ -123,6 +123,7 @@ export class AddButton extends UIElement {
|
|||
const self = this;
|
||||
|
||||
htmlElement.onclick = function (event) {
|
||||
// @ts-ignore
|
||||
if(event.consumed){
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export class Button extends UIElement {
|
|||
}
|
||||
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
|
||||
return "<form>" +
|
||||
"<button id='button-"+this.id+"' type='button' "+this._clss+">" + this._text.Render() + "</button>" +
|
||||
|
|
|
@ -4,6 +4,7 @@ import { FilteredLayer } from "../../Logic/FilteredLayer";
|
|||
|
||||
|
||||
export class CheckBox extends UIElement{
|
||||
private data: UIEventSource<boolean>;
|
||||
|
||||
private readonly _data: UIEventSource<boolean>;
|
||||
private readonly _showEnabled: string|UIElement;
|
||||
|
@ -21,7 +22,7 @@ export class CheckBox extends UIElement{
|
|||
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
if (this._data.data) {
|
||||
return this._showEnabled;
|
||||
} else {
|
||||
|
|
31
UI/Base/Combine.ts
Normal file
31
UI/Base/Combine.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export default class Combine extends UIElement {
|
||||
private uiElements: (string | UIElement)[];
|
||||
|
||||
constructor(uiElements: (string | UIElement)[]) {
|
||||
super(undefined);
|
||||
this.uiElements = uiElements;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let elements = "";
|
||||
for (const element of this.uiElements) {
|
||||
if (element instanceof UIElement) {
|
||||
elements += element.Render();
|
||||
} else {
|
||||
elements += element;
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
for (const element of this.uiElements) {
|
||||
if (element instanceof UIElement) {
|
||||
element.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
|
||||
export class DropDownUI extends UIElement {
|
||||
|
||||
selectedElement: UIEventSource<string>
|
||||
private _label: string;
|
||||
private _values: { value: string; shown: string }[];
|
||||
|
||||
constructor(label: string, values: { value: string, shown: string }[],
|
||||
selectedElement: UIEventSource<string> = undefined) {
|
||||
super(undefined);
|
||||
this._label = label;
|
||||
this._values = values;
|
||||
this.selectedElement = selectedElement ?? new UIEventSource<string>(values[0].value);
|
||||
if(selectedElement.data === undefined){
|
||||
this.selectedElement.setData(values[0].value)
|
||||
}
|
||||
const self = this;
|
||||
this.selectedElement.addCallback(() => {
|
||||
self.InnerUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected InnerRender(): string {
|
||||
|
||||
let options = "";
|
||||
for (const value of this._values) {
|
||||
options += "<option value='" + value.value + "'>" + value.shown + "</option>"
|
||||
}
|
||||
|
||||
return "<form>" +
|
||||
"<label for='dropdown-" + this.id + "'>" + this._label + "</label>" +
|
||||
"<select name='dropdown-" + this.id + "' id='dropdown-" + this.id + "'>" +
|
||||
options +
|
||||
"</select>" +
|
||||
"</form>";
|
||||
}
|
||||
|
||||
InnerUpdate() {
|
||||
const self = this;
|
||||
const e = document.getElementById("dropdown-" + this.id);
|
||||
if(e === null){
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (this.selectedElement.data !== e.value) {
|
||||
// @ts-ignore
|
||||
e.value = this.selectedElement.data;
|
||||
}
|
||||
e.onchange = function () {
|
||||
// @ts-ignore
|
||||
const selectedValue = e.options[e.selectedIndex].value;
|
||||
console.log("Putting data", selectedValue)
|
||||
self.selectedElement.setData(selectedValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -5,10 +5,10 @@ export class FixedUiElement extends UIElement {
|
|||
|
||||
constructor(html: string) {
|
||||
super(undefined);
|
||||
this._html = html;
|
||||
this._html = html ?? "";
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
return this._html;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {UIInputElement} from "./UIInputElement";
|
||||
|
||||
|
||||
export class TextField<T> extends UIInputElement<T> {
|
||||
|
||||
public value: UIEventSource<string> = new UIEventSource<string>("");
|
||||
/**
|
||||
* Pings and has the value data
|
||||
*/
|
||||
public enterPressed = new UIEventSource<string>(undefined);
|
||||
private _placeholder: UIEventSource<string>;
|
||||
private _mapping: (string) => T;
|
||||
|
||||
constructor(placeholder: UIEventSource<string>,
|
||||
mapping: ((string) => T)) {
|
||||
super(placeholder);
|
||||
this._placeholder = placeholder;
|
||||
this._mapping = mapping;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.value.map(this._mapping);
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
return "<form onSubmit='return false' class='form-text-field'>" +
|
||||
"<input type='text' placeholder='" + (this._placeholder.data ?? "") + "' id='text-" + this.id + "'>" +
|
||||
"</form>";
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const field = document.getElementById('text-' + this.id);
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
// @ts-ignore
|
||||
self.value.setData(field.value);
|
||||
};
|
||||
|
||||
field.addEventListener("keyup", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
// @ts-ignore
|
||||
self.enterPressed.setData(field.value);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
Clear() {
|
||||
const field = document.getElementById('text-' + this.id);
|
||||
if (field !== undefined) {
|
||||
// @ts-ignore
|
||||
field.value = "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
|
||||
export abstract class UIInputElement<T> extends UIElement{
|
||||
|
||||
abstract GetValue() : UIEventSource<T>;
|
||||
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {UIInputElement} from "./UIInputElement";
|
||||
|
||||
export class UIRadioButton<T> extends UIInputElement<T> {
|
||||
|
||||
public readonly SelectedElementIndex: UIEventSource<number>
|
||||
= new UIEventSource<number>(null);
|
||||
|
||||
private readonly _elements: UIEventSource<UIElement[]>
|
||||
private _selectFirstAsDefault: boolean;
|
||||
private _valueMapping: (i: number) => T;
|
||||
|
||||
constructor(elements: UIEventSource<UIElement[]>,
|
||||
valueMapping: ((i: number) => T),
|
||||
selectFirstAsDefault = true) {
|
||||
super(elements);
|
||||
this._elements = elements;
|
||||
this._selectFirstAsDefault = selectFirstAsDefault;
|
||||
const self = this;
|
||||
this._valueMapping = valueMapping;
|
||||
this.SelectedElementIndex.addCallback(() => {
|
||||
self.InnerUpdate(undefined);
|
||||
})
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.SelectedElementIndex.map(this._valueMapping);
|
||||
}
|
||||
|
||||
|
||||
private IdFor(i) {
|
||||
return 'radio-' + this.id + '-' + i;
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
|
||||
let body = "";
|
||||
let i = 0;
|
||||
for (const el of this._elements.data) {
|
||||
const htmlElement =
|
||||
'<input type="radio" id="' + this.IdFor(i) + '" name="radiogroup-' + this.id + '">' +
|
||||
'<label for="' + this.IdFor(i) + '">' + el.Render() + '</label>' +
|
||||
'<br>';
|
||||
body += htmlElement;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return "<form id='" + this.id + "-form'>" + body + "</form>";
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
const self = this;
|
||||
|
||||
function checkButtons() {
|
||||
for (let i = 0; i < self._elements.data.length; i++) {
|
||||
const el = document.getElementById(self.IdFor(i));
|
||||
// @ts-ignore
|
||||
if (el.checked) {
|
||||
self.SelectedElementIndex.setData(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const el = document.getElementById(this.id);
|
||||
el.addEventListener("change",
|
||||
function () {
|
||||
checkButtons();
|
||||
}
|
||||
);
|
||||
|
||||
if (this.SelectedElementIndex.data == null) {
|
||||
if (this._selectFirstAsDefault) {
|
||||
const el = document.getElementById(this.IdFor(0));
|
||||
if (el) {
|
||||
// @ts-ignore
|
||||
el.checked = true;
|
||||
checkButtons();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// We check that what is selected matches the previous rendering
|
||||
var checked = -1;
|
||||
var expected = this.SelectedElementIndex.data;
|
||||
if (expected) {
|
||||
|
||||
for (let i = 0; i < self._elements.data.length; i++) {
|
||||
const el = document.getElementById(self.IdFor(i));
|
||||
// @ts-ignore
|
||||
if (el.checked) {
|
||||
checked = i;
|
||||
}
|
||||
}
|
||||
if (expected != checked) {
|
||||
const el = document.getElementById(this.IdFor(expected));
|
||||
// @ts-ignore
|
||||
el.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
import {UIInputElement} from "./UIInputElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {UIRadioButton} from "./UIRadioButton";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {TextField} from "./TextField";
|
||||
import {FixedUiElement} from "./FixedUiElement";
|
||||
|
||||
|
||||
export class UIRadioButtonWithOther<T> extends UIInputElement<T> {
|
||||
private readonly _radioSelector: UIRadioButton<T>;
|
||||
private readonly _freeformText: TextField<T>;
|
||||
private readonly _value: UIEventSource<T> = new UIEventSource<T>(undefined)
|
||||
|
||||
constructor(choices: UIElement[],
|
||||
otherChoiceTemplate: string,
|
||||
placeholder: string,
|
||||
choiceToValue: ((i: number) => T),
|
||||
stringToValue: ((string: string) => T)) {
|
||||
super(undefined);
|
||||
const self = this;
|
||||
|
||||
this._freeformText = new TextField(
|
||||
new UIEventSource<string>(placeholder),
|
||||
stringToValue);
|
||||
|
||||
|
||||
const otherChoiceElement = new FixedUiElement(
|
||||
otherChoiceTemplate.replace("$$$", this._freeformText.Render()));
|
||||
choices.push(otherChoiceElement);
|
||||
|
||||
this._radioSelector = new UIRadioButton(new UIEventSource(choices),
|
||||
(i) => {
|
||||
if (i === undefined || i === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (i + 1 >= choices.length) {
|
||||
return this._freeformText.GetValue().data
|
||||
}
|
||||
return choiceToValue(i);
|
||||
},
|
||||
false);
|
||||
|
||||
this._radioSelector.GetValue().addCallback(
|
||||
(i) => {
|
||||
self._value.setData(i);
|
||||
});
|
||||
this._freeformText.GetValue().addCallback((str) => {
|
||||
self._value.setData(str);
|
||||
}
|
||||
);
|
||||
this._freeformText.onClick(() => {
|
||||
self._radioSelector.SelectedElementIndex.setData(choices.length - 1);
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
return this._radioSelector.Render();
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
this._radioSelector.Update();
|
||||
this._freeformText.Update();
|
||||
}
|
||||
|
||||
}
|
|
@ -12,16 +12,8 @@ export class VariableUiElement extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
return this._html.data;
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
if(this._innerUpdate !== undefined){
|
||||
this._innerUpdate(htmlElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import {UIElement} from "./UIElement";
|
||||
import {UIEventSource} from "./UIEventSource";
|
||||
import {OsmConnection} from "../Logic/OsmConnection";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
||||
export class CenterMessageBox extends UIElement {
|
||||
|
||||
|
@ -34,17 +35,17 @@ export class CenterMessageBox extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
|
||||
if (this._centermessage.data != "") {
|
||||
return this._centermessage.data;
|
||||
}
|
||||
if (this._queryRunning.data) {
|
||||
return "Data wordt geladen...";
|
||||
return Translations.t.centerMessage.loadingData.Render();
|
||||
} else if (this._zoomInMore.data) {
|
||||
return "Zoom in om de data te zien en te bewerken";
|
||||
return Translations.t.centerMessage.zoomIn.Render();
|
||||
}
|
||||
return "Klaar!";
|
||||
return Translations.t.centerMessage.ready.Render();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -10,10 +10,18 @@ import {TagRenderingOptions} from "../Customizations/TagRendering";
|
|||
import {OsmLink} from "../Customizations/Questions/OsmLink";
|
||||
import {WikipediaLink} from "../Customizations/Questions/WikipediaLink";
|
||||
import {And} from "../Logic/TagsFilter";
|
||||
import {TagDependantUIElement} from "../Customizations/UIElementConstructor";
|
||||
import {TagDependantUIElement, TagDependantUIElementConstructor} from "../Customizations/UIElementConstructor";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
||||
export class FeatureInfoBox extends UIElement {
|
||||
|
||||
/**
|
||||
* The actual GEOJSON-object, with geometry and stuff
|
||||
*/
|
||||
private _feature: any;
|
||||
/**
|
||||
* The tags, wrapped in a global event source
|
||||
*/
|
||||
private _tagsES: UIEventSource<any>;
|
||||
private _changes: Changes;
|
||||
private _userDetails: UIEventSource<UserDetails>;
|
||||
|
@ -24,31 +32,49 @@ export class FeatureInfoBox extends UIElement {
|
|||
private _wikipedialink: UIElement;
|
||||
|
||||
|
||||
|
||||
private _infoboxes: TagDependantUIElement[];
|
||||
private _questions: QuestionPicker;
|
||||
|
||||
private _oneSkipped = Translations.t.general.oneSkippedQuestion.Clone();
|
||||
private _someSkipped = Translations.t.general.skippedQuestions.Clone();
|
||||
|
||||
constructor(
|
||||
feature: any,
|
||||
tagsES: UIEventSource<any>,
|
||||
title: TagRenderingOptions,
|
||||
elementsToShow: TagRenderingOptions[],
|
||||
title: TagRenderingOptions | UIElement,
|
||||
elementsToShow: TagDependantUIElementConstructor[],
|
||||
changes: Changes,
|
||||
userDetails: UIEventSource<UserDetails>
|
||||
) {
|
||||
super(tagsES);
|
||||
this._feature = feature;
|
||||
this._tagsES = tagsES;
|
||||
this._changes = changes;
|
||||
this._userDetails = userDetails;
|
||||
this.ListenTo(userDetails);
|
||||
|
||||
const deps = {tags:this._tagsES , changes:this._changes}
|
||||
|
||||
const deps = {tags: this._tagsES, changes: this._changes}
|
||||
|
||||
this._infoboxes = [];
|
||||
elementsToShow = elementsToShow ?? []
|
||||
|
||||
const self = this;
|
||||
for (const tagRenderingOption of elementsToShow) {
|
||||
this._infoboxes.push(
|
||||
self._infoboxes.push(
|
||||
tagRenderingOption.construct(deps));
|
||||
}
|
||||
function initTags() {
|
||||
self._infoboxes = []
|
||||
for (const tagRenderingOption of elementsToShow) {
|
||||
self._infoboxes.push(
|
||||
tagRenderingOption.construct(deps));
|
||||
}
|
||||
self.Update();
|
||||
}
|
||||
|
||||
this._someSkipped.onClick(initTags)
|
||||
this._oneSkipped.onClick(initTags)
|
||||
|
||||
|
||||
title = title ?? new TagRenderingOptions(
|
||||
{
|
||||
|
@ -56,10 +82,14 @@ export class FeatureInfoBox extends UIElement {
|
|||
}
|
||||
)
|
||||
|
||||
this._title = new TagRenderingOptions(title.options).construct(deps);
|
||||
this._osmLink =new OsmLink().construct(deps);
|
||||
if (title instanceof UIElement) {
|
||||
this._title = title;
|
||||
} else {
|
||||
this._title = new TagRenderingOptions(title.options).construct(deps);
|
||||
}
|
||||
this._osmLink = new OsmLink().construct(deps);
|
||||
this._wikipedialink = new WikipediaLink().construct(deps);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -67,13 +97,16 @@ export class FeatureInfoBox extends UIElement {
|
|||
|
||||
|
||||
const info = [];
|
||||
const questions : TagDependantUIElement[] = [];
|
||||
|
||||
const questions: TagDependantUIElement[] = [];
|
||||
let skippedQuestions = 0;
|
||||
for (const infobox of this._infoboxes) {
|
||||
if (infobox.IsKnown()) {
|
||||
info.push(infobox);
|
||||
} else if (infobox.IsQuestioning()) {
|
||||
questions.push(infobox);
|
||||
} else {
|
||||
// This question is neither known nor questioning -> it was skipped
|
||||
skippedQuestions++;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -93,6 +126,10 @@ export class FeatureInfoBox extends UIElement {
|
|||
}
|
||||
|
||||
questionsHtml = mostImportantQuestion.Render();
|
||||
} else if (skippedQuestions == 1) {
|
||||
questionsHtml = this._oneSkipped.Render();
|
||||
} else if (skippedQuestions > 0) {
|
||||
questionsHtml = this._someSkipped.Render();
|
||||
}
|
||||
|
||||
return "<div class='featureinfobox'>" +
|
||||
|
@ -115,4 +152,6 @@ export class FeatureInfoBox extends UIElement {
|
|||
"</div>";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
/**
|
||||
* Keeps 'messagebox' and 'messageboxmobile' in sync, shows a 'close' button on the latter one
|
||||
*/
|
||||
import {UIEventSource} from "./UIEventSource";
|
||||
import {UIElement} from "./UIElement";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
||||
export class MessageBoxHandler {
|
||||
private _uielement: UIEventSource<() => UIElement>;
|
||||
/**
|
||||
* Handles the full screen popup on mobile
|
||||
*/
|
||||
export class FullScreenMessageBoxHandler {
|
||||
|
||||
private _uielement: UIEventSource<UIElement>;
|
||||
|
||||
constructor(uielement: UIEventSource<() => UIElement>,
|
||||
constructor(uielement: UIEventSource<UIElement>,
|
||||
onClear: (() => void)) {
|
||||
this._uielement = uielement;
|
||||
this.listenTo(uielement);
|
||||
|
@ -22,14 +24,13 @@ export class MessageBoxHandler {
|
|||
}
|
||||
}
|
||||
|
||||
new VariableUiElement(new UIEventSource<string>("<h2>Naar de kaart</h2>"),
|
||||
() => {
|
||||
document.getElementById("to-the-map").onclick = function () {
|
||||
uielement.setData(undefined);
|
||||
onClear();
|
||||
}
|
||||
}
|
||||
).AttachTo("to-the-map");
|
||||
Translations.t.general.returnToTheMap
|
||||
.onClick(() => {
|
||||
console.log("Clicked 'return to the map'")
|
||||
uielement.setData(undefined);
|
||||
onClear();
|
||||
})
|
||||
.AttachTo("to-the-map-h2");
|
||||
|
||||
|
||||
}
|
||||
|
@ -45,7 +46,6 @@ export class MessageBoxHandler {
|
|||
update() {
|
||||
const wrapper = document.getElementById("messagesboxmobilewrapper");
|
||||
const gen = this._uielement.data;
|
||||
console.log("Generator: ", gen);
|
||||
if (gen === undefined) {
|
||||
wrapper.classList.add("hidden")
|
||||
if (location.hash !== "") {
|
||||
|
@ -55,12 +55,8 @@ export class MessageBoxHandler {
|
|||
}
|
||||
location.hash = "#element"
|
||||
wrapper.classList.remove("hidden");
|
||||
/* gen()
|
||||
?.HideOnEmpty(true)
|
||||
?.AttachTo("messagesbox")
|
||||
?.Activate();*/
|
||||
|
||||
gen()
|
||||
gen
|
||||
?.HideOnEmpty(true)
|
||||
?.AttachTo("messagesboxmobile")
|
||||
?.Activate();
|
|
@ -23,10 +23,10 @@ export class ImageCarouselConstructor implements TagDependantUIElementConstructo
|
|||
return 0;
|
||||
}
|
||||
|
||||
construct(tags: UIEventSource<any>, changes: Changes): TagDependantUIElement {
|
||||
return new ImageCarousel(tags, changes);
|
||||
construct(dependencies: { tags: UIEventSource<any>, changes: Changes }): TagDependantUIElement {
|
||||
return new ImageCarousel(dependencies.tags, dependencies.changes);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export class ImageCarousel extends TagDependantUIElement {
|
||||
|
|
|
@ -34,14 +34,14 @@ class ImageCarouselWithUpload extends TagDependantUIElement {
|
|||
const changes = dependencies.changes;
|
||||
this._imageElement = new ImageCarousel(tags, changes);
|
||||
const userDetails = changes.login.userDetails;
|
||||
const license = changes.login.GetPreference( "mapcomplete-pictures-license");
|
||||
const license = changes.login.GetPreference( "pictures-license");
|
||||
this._pictureUploader = new OsmImageUploadHandler(tags,
|
||||
userDetails, license,
|
||||
changes, this._imageElement.slideshow).getUI();
|
||||
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
return this._imageElement.Render() +
|
||||
this._pictureUploader.Render();
|
||||
}
|
||||
|
|
|
@ -3,20 +3,21 @@ import {UIEventSource} from "./UIEventSource";
|
|||
import $ from "jquery"
|
||||
import {Imgur} from "../Logic/Imgur";
|
||||
import {UserDetails} from "../Logic/OsmConnection";
|
||||
import {DropDownUI} from "./Base/DropDownUI";
|
||||
import {DropDown} from "./Input/DropDown";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
||||
export class ImageUploadFlow extends UIElement {
|
||||
private _licensePicker: UIElement;
|
||||
private _selectedLicence: UIEventSource<string>;
|
||||
private _licenseExplanation: UIElement;
|
||||
private _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
|
||||
private _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) };
|
||||
private _userdetails: UIEventSource<UserDetails>;
|
||||
|
||||
constructor(
|
||||
userInfo: UIEventSource<UserDetails>,
|
||||
preferedLicense : UIEventSource<string>,
|
||||
preferedLicense: UIEventSource<string>,
|
||||
uploadOptions: ((license: string) =>
|
||||
{
|
||||
title: string,
|
||||
|
@ -30,70 +31,63 @@ export class ImageUploadFlow extends UIElement {
|
|||
this.ListenTo(userInfo);
|
||||
this._uploadOptions = uploadOptions;
|
||||
this.ListenTo(this._isUploading);
|
||||
this.ListenTo(this._didFail);
|
||||
|
||||
const licensePicker = new DropDownUI("Jouw foto wordt gepubliceerd ",
|
||||
|
||||
const licensePicker = new DropDown(Translations.t.image.willBePublished,
|
||||
[
|
||||
{value: "CC0", shown: "in het publiek domein"},
|
||||
{value: "CC-BY-SA 4.0", shown: "onder een CC-BY-SA-licentie"},
|
||||
{value: "CC-BY 4.0", shown: "onder een CC-BY-licentie"}
|
||||
{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}
|
||||
],
|
||||
preferedLicense
|
||||
);
|
||||
this._licensePicker = licensePicker;
|
||||
this._selectedLicence = licensePicker.selectedElement;
|
||||
this._selectedLicence = licensePicker.GetValue();
|
||||
|
||||
|
||||
const licenseExplanations = {
|
||||
"CC-BY-SA 4.0":
|
||||
"<b>Creative Commonse met naamsvermelding en gelijk delen</b><br/>" +
|
||||
"Je foto mag door iedereen gratis gebruikt worden, als ze je naam vermelden én ze afgeleide werken met deze licentie en attributie delen.",
|
||||
"CC-BY 4.0":
|
||||
"<b>Creative Commonse met naamsvermelding</b> <br/>" +
|
||||
"Je foto mag door iedereen gratis gebruikt worden, als ze je naam vermelden",
|
||||
"CC0":
|
||||
"<b>Geen copyright</b><br/> Je foto mag door iedereen voor alles gebruikt worden"
|
||||
}
|
||||
this._licenseExplanation = new VariableUiElement(
|
||||
this._selectedLicence.map((license) => {
|
||||
return licenseExplanations[license]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
|
||||
if (!this._userdetails.data.loggedIn) {
|
||||
return "<div class='activate-osm-authentication'>Gelieve je aan te melden om een foto toe te voegen of vragen te beantwoorden</div>";
|
||||
return `<div class='activate-osm-authentication'>${Translations.t.image.pleaseLogin.Render()}</div>`;
|
||||
}
|
||||
|
||||
let uploadingMessage = "";
|
||||
if (this._isUploading.data == 1) {
|
||||
return "<b>Bezig met een foto te uploaden...</b>"
|
||||
return `<b>${Translations.t.image.uploadingPicture.Render()}</b>`
|
||||
}
|
||||
if (this._isUploading.data > 0) {
|
||||
return "<b>Bezig met uploaden, nog " + this._isUploading.data + " foto's te gaan...</b>"
|
||||
uploadingMessage = "<b>Uploading multiple pictures, " + this._isUploading.data + " left...</b>"
|
||||
}
|
||||
|
||||
if(this._didFail.data){
|
||||
uploadingMessage += "<b>Some images failed to upload. Imgur migth be down or you might block third-party API's (e.g. by using Brave or UMatrix)</b><br/>"
|
||||
}
|
||||
|
||||
return "" +
|
||||
"<div class='imageflow'>" +
|
||||
|
||||
|
||||
"<label for='fileselector-" + this.id + "'>" +
|
||||
|
||||
|
||||
"<div class='imageflow-file-input-wrapper'>" +
|
||||
"<img src='./assets/camera-plus.svg' alt='upload image'/> " +
|
||||
"<span class='imageflow-add-picture'>Voeg foto toe</span>" +
|
||||
"<div class='break'></div>"+
|
||||
`<span class='imageflow-add-picture'>${Translations.t.image.addPicture.R()}</span>` +
|
||||
"<div class='break'></div>" +
|
||||
"</div>" +
|
||||
this._licensePicker.Render() +
|
||||
|
||||
|
||||
this._licensePicker.Render() + "<br/>" +
|
||||
uploadingMessage +
|
||||
|
||||
"</label>" +
|
||||
|
||||
|
||||
"<input id='fileselector-" + this.id + "' " +
|
||||
"type='file' " +
|
||||
"class='imageflow-file-input' " +
|
||||
"accept='image/*' name='picField' size='24' multiple='multiple' alt=''" +
|
||||
"/>" +
|
||||
|
||||
|
||||
"</div>"
|
||||
;
|
||||
}
|
||||
|
@ -128,11 +122,12 @@ export class ImageUploadFlow extends UIElement {
|
|||
function () {
|
||||
console.log("All uploads completed")
|
||||
opts.allDone();
|
||||
},
|
||||
function(failReason) {
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
100
UI/Input/DropDown.ts
Normal file
100
UI/Input/DropDown.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import instantiate = WebAssembly.instantiate;
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export class DropDown<T> extends InputElement<T> {
|
||||
|
||||
private readonly _label: UIElement;
|
||||
private readonly _values: { value: T; shown: UIElement }[];
|
||||
|
||||
private readonly _value;
|
||||
|
||||
constructor(label: string | UIElement,
|
||||
values: { value: T, shown: string | UIElement }[],
|
||||
value: UIEventSource<T> = undefined) {
|
||||
super(undefined);
|
||||
this._value = value ?? new UIEventSource<T>(undefined);
|
||||
this._label = Translations.W(label);
|
||||
this._values = values.map(v => {
|
||||
return {
|
||||
value: v.value,
|
||||
shown: Translations.W(v.shown)
|
||||
}
|
||||
}
|
||||
);
|
||||
for (const v of this._values) {
|
||||
this.ListenTo(v.shown._source);
|
||||
}
|
||||
this.ListenTo(this._value)
|
||||
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
ShowValue(t: T): boolean {
|
||||
if (!this.IsValid(t)) {
|
||||
return false;
|
||||
}
|
||||
this._value.setData(t);
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
for (const value of this._values) {
|
||||
if (value.value === t) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
if(this._values.length <=1){
|
||||
return "";
|
||||
}
|
||||
|
||||
let options = "";
|
||||
for (let i = 0; i < this._values.length; i++) {
|
||||
options += "<option value='" + i + "'>" + this._values[i].shown.InnerRender() + "</option>"
|
||||
|
||||
}
|
||||
return "<form>" +
|
||||
"<label for='dropdown-" + this.id + "'>" + this._label.Render() + "</label>" +
|
||||
"<select name='dropdown-" + this.id + "' id='dropdown-" + this.id + "'>" +
|
||||
options +
|
||||
"</select>" +
|
||||
"</form>";
|
||||
}
|
||||
|
||||
protected InnerUpdate(element) {
|
||||
|
||||
|
||||
var e = document.getElementById("dropdown-" + this.id);
|
||||
if(e === null){
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
e.onchange = (() => {
|
||||
// @ts-ignore
|
||||
var index = parseInt(e.selectedIndex);
|
||||
self._value.setData(self._values[index].value);
|
||||
|
||||
});
|
||||
|
||||
var t = this._value.data;
|
||||
for (let i = 0; i < this._values.length ; i++) {
|
||||
const value = this._values[i];
|
||||
if (value.value == t) {
|
||||
// @ts-ignore
|
||||
e.selectedIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
35
UI/Input/FixedInputElement.ts
Normal file
35
UI/Input/FixedInputElement.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
|
||||
export class FixedInputElement<T> extends InputElement<T> {
|
||||
private rendering: UIElement;
|
||||
private value: UIEventSource<T>;
|
||||
|
||||
constructor(rendering: UIElement | string, value: T) {
|
||||
super(undefined);
|
||||
this.value = new UIEventSource<T>(value);
|
||||
this.rendering = typeof (rendering) === 'string' ? new FixedUiElement(rendering) : rendering;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
ShowValue(t: T): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.rendering.Render();
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return t == this.value.data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
11
UI/Input/InputElement.ts
Normal file
11
UI/Input/InputElement.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export abstract class InputElement<T> extends UIElement{
|
||||
|
||||
abstract GetValue() : UIEventSource<T>;
|
||||
|
||||
abstract IsValid(t: T) : boolean;
|
||||
|
||||
}
|
41
UI/Input/InputElementWrapper.ts
Normal file
41
UI/Input/InputElementWrapper.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
|
||||
export class InputElementWrapper<T> extends InputElement<T>{
|
||||
private pre: UIElement ;
|
||||
private input: InputElement<T>;
|
||||
private post: UIElement ;
|
||||
|
||||
constructor(
|
||||
pre: UIElement | string,
|
||||
input: InputElement<T>,
|
||||
post: UIElement | string
|
||||
|
||||
) {
|
||||
super(undefined);
|
||||
this.pre = typeof(pre) === 'string' ? new FixedUiElement(pre) : pre
|
||||
this.input = input;
|
||||
this.post =typeof(post) === 'string' ? new FixedUiElement(post) : post
|
||||
}
|
||||
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.input.GetValue();
|
||||
}
|
||||
|
||||
ShowValue(t: T) {
|
||||
return this.input.ShowValue(t);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.pre.Render() + this.input.Render() + this.post.Render();
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this.input.IsValid(t);
|
||||
}
|
||||
|
||||
}
|
146
UI/Input/RadioButton.ts
Normal file
146
UI/Input/RadioButton.ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {InputElement} from "./InputElement";
|
||||
|
||||
export class RadioButton<T> extends InputElement<T> {
|
||||
|
||||
private readonly _selectedElementIndex: UIEventSource<number>
|
||||
= new UIEventSource<number>(null);
|
||||
|
||||
private value: UIEventSource<T>;
|
||||
private readonly _elements: InputElement<T>[]
|
||||
private _selectFirstAsDefault: boolean;
|
||||
|
||||
|
||||
constructor(elements: InputElement<T>[],
|
||||
selectFirstAsDefault = true) {
|
||||
super(undefined);
|
||||
this._elements = elements;
|
||||
this._selectFirstAsDefault = selectFirstAsDefault;
|
||||
const self = this;
|
||||
|
||||
|
||||
this.value =
|
||||
UIEventSource.flatten(this._selectedElementIndex.map(
|
||||
(selectedIndex) => {
|
||||
if (selectedIndex !== undefined && selectedIndex !== null) {
|
||||
return elements[selectedIndex].GetValue()
|
||||
}
|
||||
}
|
||||
), elements.map(e => e.GetValue()));
|
||||
|
||||
this.value.addCallback((t) => {
|
||||
self.ShowValue(t);
|
||||
})
|
||||
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
// If an element is clicked, the radio button corresponding with it should be selected as well
|
||||
elements[i].onClick(() => {
|
||||
self._selectedElementIndex.setData(i);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
for (const inputElement of this._elements) {
|
||||
if (inputElement.IsValid(t)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
|
||||
private IdFor(i) {
|
||||
return 'radio-' + this.id + '-' + i;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
let body = "";
|
||||
let i = 0;
|
||||
for (const el of this._elements) {
|
||||
const htmlElement =
|
||||
'<input type="radio" id="' + this.IdFor(i) + '" name="radiogroup-' + this.id + '">' +
|
||||
'<label for="' + this.IdFor(i) + '">' + el.Render() + '</label>' +
|
||||
'<br>';
|
||||
body += htmlElement;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return "<form id='" + this.id + "-form'>" + body + "</form>";
|
||||
}
|
||||
|
||||
public ShowValue(t: T): boolean {
|
||||
if (t === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (!this.IsValid(t)) {
|
||||
return false;
|
||||
}
|
||||
// We check that what is selected matches the previous rendering
|
||||
for (let i = 0; i < this._elements.length; i++) {
|
||||
const e = this._elements[i];
|
||||
if (e.IsValid(t)) {
|
||||
this._selectedElementIndex.setData(i);
|
||||
e.GetValue().setData(t);
|
||||
const radio = document.getElementById(this.IdFor(i));
|
||||
// @ts-ignore
|
||||
radio?.checked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
const self = this;
|
||||
|
||||
function checkButtons() {
|
||||
for (let i = 0; i < self._elements.length; i++) {
|
||||
const el = document.getElementById(self.IdFor(i));
|
||||
// @ts-ignore
|
||||
if (el.checked) {
|
||||
self._selectedElementIndex.setData(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const el = document.getElementById(this.id);
|
||||
el.addEventListener("change",
|
||||
function () {
|
||||
checkButtons();
|
||||
}
|
||||
);
|
||||
if (this._selectedElementIndex.data !== null) {
|
||||
const el = document.getElementById(this.IdFor(this._selectedElementIndex.data));
|
||||
if (el) {
|
||||
// @ts-ignore
|
||||
el.checked = true;
|
||||
checkButtons();
|
||||
}
|
||||
} else if (this._selectFirstAsDefault) {
|
||||
this.ShowValue(this.value.data);
|
||||
if (this._selectedElementIndex.data === null || this._selectedElementIndex.data === undefined) {
|
||||
const el = document.getElementById(this.IdFor(0));
|
||||
if (el) {
|
||||
// @ts-ignore
|
||||
el.checked = true;
|
||||
checkButtons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
}
|
119
UI/Input/TextField.ts
Normal file
119
UI/Input/TextField.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {InputElement} from "./InputElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
|
||||
export class TextField<T> extends InputElement<T> {
|
||||
|
||||
private value: UIEventSource<string>;
|
||||
private mappedValue: UIEventSource<T>;
|
||||
/**
|
||||
* Pings and has the value data
|
||||
*/
|
||||
public enterPressed = new UIEventSource<string>(undefined);
|
||||
private _placeholder: UIElement;
|
||||
private _fromString?: (string: string) => T;
|
||||
private _toString: (t: T) => string;
|
||||
|
||||
constructor(options: {
|
||||
placeholder?: string | UIElement,
|
||||
toString: (t: T) => string,
|
||||
fromString: (string: string) => T,
|
||||
value?: UIEventSource<T>
|
||||
}) {
|
||||
super(undefined);
|
||||
const self = this;
|
||||
this.value = new UIEventSource<string>("");
|
||||
|
||||
this.mappedValue = options?.value ?? new UIEventSource<T>(undefined);
|
||||
this.mappedValue.addCallback(() => self.InnerUpdate());
|
||||
|
||||
// @ts-ignore
|
||||
this._fromString = options.fromString ?? ((str) => (str))
|
||||
this.value.addCallback((str) => this.mappedValue.setData(options.fromString(str)));
|
||||
this.mappedValue.addCallback((t) => this.value.setData(options.toString(t)));
|
||||
|
||||
|
||||
this._placeholder = Translations.W(options.placeholder ?? "");
|
||||
this.ListenTo(this._placeholder._source);
|
||||
this._toString = options.toString ?? ((t) => ("" + t));
|
||||
|
||||
|
||||
this.mappedValue.addCallback((t) => {
|
||||
if (t === undefined || t === null) {
|
||||
return;
|
||||
}
|
||||
const field = document.getElementById('text-' + this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
field.value = options.toString(t);
|
||||
})
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.mappedValue;
|
||||
}
|
||||
|
||||
ShowValue(t: T): boolean {
|
||||
if (!this.IsValid(t)) {
|
||||
return false;
|
||||
}
|
||||
this.mappedValue.setData(t);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return "<form onSubmit='return false' class='form-text-field'>" +
|
||||
"<input type='text' placeholder='" + this._placeholder.InnerRender() + "' id='text-" + this.id + "'>" +
|
||||
"</form>";
|
||||
}
|
||||
|
||||
InnerUpdate() {
|
||||
const field = document.getElementById('text-' + this.id);
|
||||
if (field === null) {
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
// @ts-ignore
|
||||
self.value.setData(field.value);
|
||||
};
|
||||
|
||||
field.addEventListener("keyup", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
// @ts-ignore
|
||||
self.enterPressed.setData(field.value);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.IsValid(this.mappedValue.data)) {
|
||||
const expected = this._toString(this.mappedValue.data);
|
||||
// @ts-ignore
|
||||
if (field.value !== expected) {
|
||||
// @ts-ignore
|
||||
field.value = expected;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
if(t === undefined || t === null){
|
||||
return false;
|
||||
}
|
||||
const result = this._toString(t);
|
||||
return result !== undefined && result !== null;
|
||||
}
|
||||
|
||||
Clear() {
|
||||
const field = document.getElementById('text-' + this.id);
|
||||
if (field !== undefined) {
|
||||
// @ts-ignore
|
||||
field.value = "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ export class PendingChanges extends UIElement {
|
|||
})
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
if (this._isSaving.data) {
|
||||
return "<span class='alert'>Saving</span>";
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {UIEventSource} from "./UIEventSource";
|
||||
import {UIElement} from "./UIElement";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
||||
export class SaveButton extends UIElement {
|
||||
private _value: UIEventSource<any>;
|
||||
|
@ -12,14 +13,14 @@ export class SaveButton extends UIElement {
|
|||
this._value = value;
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
if (this._value.data === undefined ||
|
||||
this._value.data === null
|
||||
|| this._value.data === ""
|
||||
) {
|
||||
return "<span class='save-non-active'>Opslaan</span>"
|
||||
return "<span class='save-non-active'>"+Translations.t.general.save.Render()+"</span>"
|
||||
}
|
||||
return "<span class='save'>Opslaan</span>";
|
||||
return "<span class='save'>"+Translations.t.general.save.Render()+"</span>";
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,26 @@
|
|||
import {UIElement} from "./UIElement";
|
||||
import {TextField} from "./Base/TextField";
|
||||
import {TextField} from "./Input/TextField";
|
||||
import {UIEventSource} from "./UIEventSource";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import {Geocoding} from "../Logic/Geocoding";
|
||||
import {Basemap} from "../Logic/Basemap";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Translation from "./i18n/Translation";
|
||||
import Locale from "./i18n/Locale";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
||||
|
||||
export class SearchAndGo extends UIElement {
|
||||
|
||||
private _placeholder = new UIEventSource("Zoek naar een locatie...")
|
||||
private _searchField = new TextField(this._placeholder);
|
||||
private _placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
|
||||
private _searchField = new TextField<string>({
|
||||
placeholder: new VariableUiElement(
|
||||
this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language])
|
||||
),
|
||||
fromString: str => str,
|
||||
toString: str => str
|
||||
}
|
||||
);
|
||||
|
||||
private _foundEntries = new UIEventSource([]);
|
||||
private _map: Basemap;
|
||||
|
@ -33,14 +44,14 @@ export class SearchAndGo extends UIElement {
|
|||
|
||||
// Triggered by 'enter' or onclick
|
||||
private RunSearch() {
|
||||
const searchString = this._searchField.value.data;
|
||||
const searchString = this._searchField.GetValue().data;
|
||||
this._searchField.Clear();
|
||||
this._placeholder.setData("Bezig met zoeken...");
|
||||
this._placeholder.setData(Translations.t.general.search.searching);
|
||||
const self = this;
|
||||
Geocoding.Search(searchString, this._map, (result) => {
|
||||
|
||||
if (result.length == 0) {
|
||||
this._placeholder.setData("Niets gevonden");
|
||||
this._placeholder.setData(Translations.t.general.search.nothing);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -50,16 +61,15 @@ export class SearchAndGo extends UIElement {
|
|||
[bb[1], bb[3]]
|
||||
]
|
||||
self._map.map.fitBounds(bounds);
|
||||
this._placeholder.setData("Zoek naar een locatie...");
|
||||
this._placeholder.setData(Translations.t.general.search.search);
|
||||
},
|
||||
() => {
|
||||
this._placeholder.setData("Niets gevonden: er ging iets mis");
|
||||
this._placeholder.setData(Translations.t.general.search.error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
// "<img class='search' src='./assets/search.svg' alt='Search'> " +
|
||||
InnerRender(): string {
|
||||
return this._searchField.Render() +
|
||||
this._goButton.Render();
|
||||
|
||||
|
|
|
@ -15,17 +15,17 @@ export class SimpleAddUI extends UIElement {
|
|||
private _addButtons: UIElement[];
|
||||
private _lastClickLocation: UIEventSource<{ lat: number; lon: number }>;
|
||||
private _changes: Changes;
|
||||
private _selectedElement: UIEventSource<any>;
|
||||
private _selectedElement: UIEventSource<{feature: any}>;
|
||||
private _dataIsLoading: UIEventSource<boolean>;
|
||||
private _userDetails: UIEventSource<UserDetails>;
|
||||
|
||||
constructor(zoomlevel: UIEventSource<{ zoom: number }>,
|
||||
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||
changes: Changes,
|
||||
selectedElement: UIEventSource<any>,
|
||||
selectedElement: UIEventSource<{feature: any}>,
|
||||
dataIsLoading: UIEventSource<boolean>,
|
||||
userDetails: UIEventSource<UserDetails>,
|
||||
addButtons: { name: string; icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }[],
|
||||
addButtons: { name: UIElement; icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }[],
|
||||
) {
|
||||
super(zoomlevel);
|
||||
this._zoomlevel = zoomlevel;
|
||||
|
@ -42,37 +42,36 @@ export class SimpleAddUI extends UIElement {
|
|||
// <button type='button'> looks SO retarded
|
||||
// the default type of button is 'submit', which performs a POST and page reload
|
||||
const button =
|
||||
new Button(new FixedUiElement("Voeg hier een " + option.name + " toe"),
|
||||
new Button(new FixedUiElement("Add a " + option.name.Render() + " here"),
|
||||
this.CreatePoint(option));
|
||||
this._addButtons.push(button);
|
||||
}
|
||||
}
|
||||
|
||||
private CreatePoint(option: { name: string; icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }) {
|
||||
private CreatePoint(option: {icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }) {
|
||||
const self = this;
|
||||
return () => {
|
||||
|
||||
console.log("Creating a new ", option.name, " at last click location");
|
||||
const loc = self._lastClickLocation.data;
|
||||
let feature = self._changes.createElement(option.tags, loc.lat, loc.lon);
|
||||
option.layerToAddTo.AddNewElement(feature);
|
||||
self._selectedElement.setData(feature.properties);
|
||||
self._selectedElement.setData({feature: feature});
|
||||
}
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
const header = "<h2>Geen selectie</h2>" +
|
||||
"Je klikte ergens waar er nog geen gezochte data is.<br/>";
|
||||
InnerRender(): string {
|
||||
const header = "<h2>No data here</h2>" +
|
||||
"You clicked somewhere where no data is known yet.<br/>";
|
||||
if (!this._userDetails.data.loggedIn) {
|
||||
return header + "<a class='activate-osm-authentication'>Gelieve je aan te melden om een nieuw punt toe te voegen</a>"
|
||||
return header + "<a class='activate-osm-authentication'>Please log in to add a new point</a>"
|
||||
}
|
||||
|
||||
if (this._zoomlevel.data.zoom < 19) {
|
||||
return header + "Zoom verder in om een element toe te voegen.";
|
||||
return header + "Zoom in further to add a point.";
|
||||
}
|
||||
|
||||
if (this._dataIsLoading.data) {
|
||||
return header + "De data is nog aan het laden. Nog even geduld, dan kan je een punt toevoegen";
|
||||
return header + "The data is still loading. Please wait a bit before you add a new point";
|
||||
}
|
||||
|
||||
var html = "";
|
||||
|
@ -83,10 +82,6 @@ export class SimpleAddUI extends UIElement {
|
|||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
for (const button of this._addButtons) {
|
||||
button.Update();
|
||||
}
|
||||
this._userDetails.data.osmConnection.registerActivateOsmAUthenticationClass();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import {UIEventSource} from "./UIEventSource";
|
||||
import instantiate = WebAssembly.instantiate;
|
||||
|
||||
export abstract class UIElement {
|
||||
|
||||
|
||||
private static nextId: number = 0;
|
||||
|
||||
public readonly id: string;
|
||||
|
@ -20,12 +19,13 @@ export abstract class UIElement {
|
|||
|
||||
public ListenTo(source: UIEventSource<any>) {
|
||||
if (source === undefined) {
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
const self = this;
|
||||
source.addCallback(() => {
|
||||
self.Update();
|
||||
})
|
||||
return this;
|
||||
}
|
||||
|
||||
private _onClick: () => void;
|
||||
|
@ -35,14 +35,13 @@ export abstract class UIElement {
|
|||
this.Update();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
Update(): void {
|
||||
let element = document.getElementById(this.id);
|
||||
if (element === null || element === undefined) {
|
||||
if (element === undefined || element === null) {
|
||||
// The element is not painted
|
||||
return;
|
||||
}
|
||||
|
||||
element.innerHTML = this.InnerRender();
|
||||
if (this._hideIfEmpty) {
|
||||
if (element.innerHTML === "") {
|
||||
|
@ -84,7 +83,8 @@ export abstract class UIElement {
|
|||
}
|
||||
|
||||
// Called after the HTML has been replaced. Can be used for css tricks
|
||||
InnerUpdate(htmlElement : HTMLElement){}
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
}
|
||||
|
||||
Render(): string {
|
||||
return "<span class='uielement' id='" + this.id + "'>" + this.InnerRender() + "</span>"
|
||||
|
@ -93,15 +93,14 @@ export abstract class UIElement {
|
|||
AttachTo(divId: string) {
|
||||
let element = document.getElementById(divId);
|
||||
if (element === null) {
|
||||
console.log("SEVERE: could not attach UIElement to ", divId);
|
||||
return;
|
||||
throw "SEVERE: could not attach UIElement to " + divId;
|
||||
}
|
||||
element.innerHTML = this.Render();
|
||||
this.Update();
|
||||
return this;
|
||||
}
|
||||
|
||||
protected abstract InnerRender(): string;
|
||||
public abstract InnerRender(): string;
|
||||
|
||||
public Activate(): void {
|
||||
for (const i in this) {
|
||||
|
@ -121,5 +120,6 @@ export abstract class UIElement {
|
|||
public IsEmpty(): boolean {
|
||||
return this.InnerRender() === "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
export class UIEventSource<T>{
|
||||
|
||||
public data : T;
|
||||
public data: T;
|
||||
private _callbacks = [];
|
||||
|
||||
constructor(data: T) {
|
||||
|
@ -27,15 +27,32 @@ export class UIEventSource<T>{
|
|||
}
|
||||
}
|
||||
|
||||
public map<J>(f: ((T) => J),
|
||||
extraSources : UIEventSource<any>[] = []): UIEventSource<J> {
|
||||
const self = 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 map<J>(f: ((T) => J),
|
||||
extraSources: UIEventSource<any>[] = []): UIEventSource<J> {
|
||||
const self = this;
|
||||
|
||||
const update = function () {
|
||||
newSource.setData(f(self.data));
|
||||
newSource.ping();
|
||||
}
|
||||
|
||||
|
||||
this.addCallback(update);
|
||||
for (const extraSource of extraSources) {
|
||||
extraSource.addCallback(update);
|
||||
|
@ -49,5 +66,16 @@ export class UIEventSource<T>{
|
|||
|
||||
}
|
||||
|
||||
|
||||
public syncWith(otherSource: UIEventSource<T>){
|
||||
this.addCallback((latest) => otherSource.setData(latest));
|
||||
const self = this;
|
||||
otherSource.addCallback((latest) => self.setData(latest));
|
||||
if(this.data === undefined){
|
||||
this.setData(otherSource.data);
|
||||
}else{
|
||||
otherSource.setData(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -5,6 +5,7 @@ import {Basemap} from "../Logic/Basemap";
|
|||
import L from "leaflet";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
||||
/**
|
||||
* Handles and updates the user badge
|
||||
|
@ -15,12 +16,15 @@ export class UserBadge extends UIElement {
|
|||
private _logout: UIElement;
|
||||
private _basemap: Basemap;
|
||||
private _homeButton: UIElement;
|
||||
private _languagePicker: UIElement;
|
||||
|
||||
|
||||
constructor(userDetails: UIEventSource<UserDetails>,
|
||||
pendingChanges: UIElement,
|
||||
languagePicker: UIElement,
|
||||
basemap: Basemap) {
|
||||
super(userDetails);
|
||||
this._languagePicker = languagePicker;
|
||||
this._userDetails = userDetails;
|
||||
this._pendingChanges = pendingChanges;
|
||||
this._basemap = basemap;
|
||||
|
@ -57,10 +61,10 @@ export class UserBadge extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
protected InnerRender(): string {
|
||||
InnerRender(): string {
|
||||
const user = this._userDetails.data;
|
||||
if (!user.loggedIn) {
|
||||
return "<div class='activate-osm-authentication'>Klik hier om aan te melden bij OSM</div>";
|
||||
return "<div class='activate-osm-authentication'>" + Translations.t.general.loginWithOpenStreetMap.R()+ "</div>";
|
||||
}
|
||||
|
||||
|
||||
|
@ -113,6 +117,7 @@ export class UserBadge extends UIElement {
|
|||
" <a href='https://www.openstreetmap.org/user/" + user.name + "/history' target='_blank'><img class='small-userbadge-icon' src='./assets/star.svg' alt='star'/> " + user.csCount +
|
||||
"</a></span> " +
|
||||
this._logout.Render() +
|
||||
this._languagePicker.Render() +
|
||||
this._pendingChanges.Render() +
|
||||
"</p>" +
|
||||
|
||||
|
|
24
UI/i18n/Locale.ts
Normal file
24
UI/i18n/Locale.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {OsmConnection} from "../../Logic/OsmConnection";
|
||||
|
||||
|
||||
export default class Locale {
|
||||
public static language: UIEventSource<string> = Locale.getInitialLanguage();
|
||||
|
||||
private static getInitialLanguage() {
|
||||
// The key to save in local storage
|
||||
const LANGUAGE_KEY = 'language'
|
||||
|
||||
const lng = new UIEventSource("en");
|
||||
const saved = localStorage.getItem(LANGUAGE_KEY);
|
||||
lng.setData(saved);
|
||||
|
||||
|
||||
lng.addCallback(data => {
|
||||
console.log("Selected language", data);
|
||||
localStorage.setItem(LANGUAGE_KEY, data)
|
||||
});
|
||||
|
||||
return lng;
|
||||
}
|
||||
}
|
43
UI/i18n/Translation.ts
Normal file
43
UI/i18n/Translation.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { UIElement } from "../UIElement"
|
||||
import Locale from "./Locale"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
|
||||
export default class Translation extends UIElement {
|
||||
get txt(): string {
|
||||
const txt = this.translations[Locale.language.data];
|
||||
if (txt !== undefined) {
|
||||
return txt;
|
||||
}
|
||||
const en = this.translations["en"];
|
||||
console.warn("No translation for language ", Locale.language.data, "for", en);
|
||||
if (en !== undefined) {
|
||||
return en;
|
||||
}
|
||||
for (const i in this.translations) {
|
||||
return this.translations[i]; // Return a random language
|
||||
}
|
||||
return "Missing translation"
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.txt
|
||||
}
|
||||
|
||||
public readonly translations: object
|
||||
|
||||
constructor(translations: object) {
|
||||
super(Locale.language)
|
||||
this.translations = translations
|
||||
}
|
||||
|
||||
|
||||
public R(): string {
|
||||
return new Translation(this.translations).Render();
|
||||
}
|
||||
|
||||
public Clone(){
|
||||
return new Translation(this.translations)
|
||||
}
|
||||
|
||||
}
|
394
UI/i18n/Translations.ts
Normal file
394
UI/i18n/Translations.ts
Normal file
|
@ -0,0 +1,394 @@
|
|||
import Translation from "./Translation";
|
||||
import T from "./Translation";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
|
||||
export default class Translations {
|
||||
|
||||
constructor() {
|
||||
throw "Translations is static. If you want to intitialize a new translation, use the singular form"
|
||||
}
|
||||
|
||||
|
||||
static t = {
|
||||
cyclofix: {
|
||||
title: new T({
|
||||
en: 'Cyclofix bicycle infrastructure',
|
||||
nl: 'Cyclofix fietsinfrastructuur',
|
||||
fr: 'TODO: FRENCH TRANSLATION'
|
||||
}),
|
||||
description: new T({
|
||||
en: "On this map we want to collect data about the whereabouts of bicycle pumps and public racks in Brussels." +
|
||||
"As a result, cyclists will be able to quickly find the nearest infrastructure for their needs.",
|
||||
nl: "Op deze kaart willen we gegevens verzamelen over de locatie van fietspompen en openbare stelplaatsen in Brussel." +
|
||||
"Hierdoor kunnen fietsers snel de dichtstbijzijnde infrastructuur vinden die voldoet aan hun behoeften.",
|
||||
fr: "Sur cette carte, nous voulons collecter des données sur la localisation des pompes à vélo et des supports publics à Bruxelles." +
|
||||
"Les cyclistes pourront ainsi trouver rapidement l'infrastructure la plus proche de leurs besoins."
|
||||
}),
|
||||
freeFormPlaceholder: new T({en: 'specify', nl: 'specifieer', fr: 'TODO: fr'}),
|
||||
parking: {
|
||||
name: new T({en: 'bike parking', nl: 'fietsparking', fr: 'TODO: fr'}),
|
||||
title: new T({en: 'Bike parking', nl: 'Fietsparking', fr: 'TODO: fr'}),
|
||||
type: {
|
||||
render: new T({
|
||||
en: 'This is a bicycle parking of the type: {bicycle_parking}',
|
||||
nl: 'Dit is een fietsenparking van het type: {bicycle_parking}',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
template: new T({en: 'Some other type: $$$', nl: 'Een ander type: $$$', fr: 'TODO: fr'}),
|
||||
question: new T({
|
||||
en: 'What is the type of this bicycle parking?',
|
||||
nl: 'Van welk type is deze fietsenparking?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
eg: new T({en: ", for example", nl: ", bijvoorbeeld"}),
|
||||
stands: new T({en: 'Staple racks', nl: 'Nietjes', fr: 'TODO: fr'}),
|
||||
wall_loops: new T({en: 'Wheel rack/loops', nl: 'Wielrek/lussen', fr: 'TODO: fr'}),
|
||||
handlebar_holder: new T({en: 'Handlebar holder', nl: 'Stuurhouder', fr: 'TODO: fr'}),
|
||||
shed: new T({en: 'Shed', nl: 'Schuur', fr: 'TODO: fr'}),
|
||||
rack: new T({en: 'Rack', nl: 'Rek', fr: 'TODO: fr'}),
|
||||
"two-tier": new T({en: 'Two-tiered', nl: 'Dubbel (twee verdiepingen)', fr: 'TODO: fr'}),
|
||||
},
|
||||
|
||||
operator: {
|
||||
render: new T({
|
||||
en: 'This bike parking is operated by {operator}',
|
||||
nl: 'Deze fietsenparking wordt beheerd door {operator}',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
template: new T({en: 'A different operator: $$$', nl: 'Een andere beheerder: $$$', fr: 'TODO: fr'}),
|
||||
question: new T({
|
||||
en: 'Who operates this bike station (name of university, shop, city...)?',
|
||||
nl: 'Wie beheert deze fietsenparking (naam universiteit, winkel, stad...)?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
private: new T({
|
||||
en: 'Operated by a private person',
|
||||
nl: 'Wordt beheerd door een privépersoon',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
}
|
||||
},
|
||||
station: {
|
||||
name: new T({
|
||||
en: 'bike station (repair, pump or both)',
|
||||
nl: 'fietsstation (herstel, pomp of allebei)',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
title: new T({en: 'Bike station', nl: 'Fietsstation', fr: 'TODO: fr'}),
|
||||
manometer: {
|
||||
question: new T({
|
||||
en: 'Does the pump have a pressure indicator or manometer?',
|
||||
nl: 'Heeft deze pomp een luchtdrukmeter?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
yes: new T({en: 'There is a manometer', nl: 'Er is een luchtdrukmeter', fr: 'TODO: fr'}),
|
||||
no: new T({en: 'There is no manometer', nl: 'Er is geen luchtdrukmeter', fr: 'TODO: fr'}),
|
||||
broken: new T({
|
||||
en: 'There is manometer but it is broken',
|
||||
nl: 'Er is een luchtdrukmeter maar die is momenteel defect',
|
||||
fr: 'TODO: fr'
|
||||
})
|
||||
},
|
||||
electric: {
|
||||
question: new T({
|
||||
en: 'Is this an electric bike pump?',
|
||||
nl: 'Is dit een electrische fietspomp?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
manual: new T({en: 'Manual pump', nl: 'Manuele pomp', fr: 'TODO: fr'}),
|
||||
electric: new T({en: 'Electrical pump', nl: 'Electrische pomp', fr: 'TODO: fr'})
|
||||
},
|
||||
operational: {
|
||||
question: new T({
|
||||
en: 'Is the bike pump still operational?',
|
||||
nl: 'Werkt de fietspomp nog?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
operational: new T({
|
||||
en: 'The bike pump is operational',
|
||||
nl: 'De fietspomp werkt nog',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
broken: new T({en: 'The bike pump is broken', nl: 'De fietspomp is kapot', fr: 'TODO: fr'})
|
||||
},
|
||||
valves: {
|
||||
question: new T({
|
||||
en: 'What valves are supported?',
|
||||
nl: 'Welke ventielen werken er met de pomp?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
default: new T({
|
||||
en: 'There is a default head, so Dunlop, Sclaverand and auto',
|
||||
nl: 'Er is een standaard aansluiting, die dus voor Dunlop, Sclaverand en auto\'s werkt',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
dunlop: new T({en: 'Only Dunlop', nl: 'Enkel Dunlop', fr: 'TODO: fr'}),
|
||||
sclaverand: new T({
|
||||
en: 'Only Sclaverand (also known as Presta)',
|
||||
nl: 'Enkel Sclaverand (ook gekend als Presta)',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
auto: new T({en: 'Only for cars', nl: 'Enkel voor auto\'s', fr: 'TODO: fr'}),
|
||||
render: new T({
|
||||
en: 'This pump supports the following valves: {valves}',
|
||||
nl: 'Deze pomp werkt met de volgende ventielen: {valves}',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
template: new T({
|
||||
en: 'Some other valve(s): $$$',
|
||||
nl: 'Een ander type ventiel(en): $$$',
|
||||
fr: 'TODO: fr'
|
||||
})
|
||||
},
|
||||
chain: {
|
||||
question: new T({
|
||||
en: 'Does this bike station have a special tool to repair your bike chain?',
|
||||
nl: 'Heeft dit fietsstation een speciale reparatieset voor je ketting?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
yes: new T({
|
||||
en: 'There is a chain tool',
|
||||
nl: 'Er is een reparatieset voor je ketting',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
no: new T({
|
||||
en: 'There is no chain tool',
|
||||
nl: 'Er is geen reparatieset voor je ketting',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
},
|
||||
operator: {
|
||||
render: new T({
|
||||
en: 'This bike station is operated by {operator}',
|
||||
nl: 'Dit fietsstation wordt beheerd door {operator}',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
template: new T({en: 'A different operator: $$$', nl: 'Een andere beheerder: $$$', fr: 'TODO: fr'}),
|
||||
question: new T({
|
||||
en: 'Who operates this bike station (name of university, shop, city...)?',
|
||||
nl: 'Wie beheert dit fietsstation (naam universiteit, winkel, stad...)?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
private: new T({
|
||||
en: 'Operated by a private person',
|
||||
nl: 'Wordt beheerd door een privépersoon',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
},
|
||||
services: {
|
||||
question: new T({
|
||||
en: 'Which services are available at this bike station?',
|
||||
nl: 'Welke functies biedt dit fietsstation?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
pump: new T({
|
||||
en: 'There is only a pump available',
|
||||
nl: 'Er is enkel een pomp beschikbaar',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
tools: new T({
|
||||
en: 'There are only tools (screwdrivers, pliers...) available',
|
||||
nl: 'Er is enkel gereedschap beschikbaar (schroevendraaier, tang...)',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
both: new T({
|
||||
en: 'There are both tools and a pump available',
|
||||
nl: 'Er is zowel een pomp als gereedschap beschikbaar',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
},
|
||||
stand: {
|
||||
question: new T({
|
||||
en: 'Does this bike station have a hook to suspend your bike with or a stand to elevate it?',
|
||||
nl: 'Heeft dit fietsstation een haak of standaard om je fiets op te hangen/zetten?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
yes: new T({en: 'There is a hook or stand', nl: 'Er is een haak of standaard', fr: 'TODO: fr'}),
|
||||
no: new T({en: 'There is no hook or stand', nl: 'Er is geen haak of standaard', fr: 'TODO: fr'}),
|
||||
}
|
||||
},
|
||||
shop: {
|
||||
name: new T({en: 'bike shop', nl: 'fietswinkel', fr: 'TODO: fr'}),
|
||||
|
||||
title: new T({en: 'Bike shop', nl: 'Fietszaak', fr: 'TODO: fr'}),
|
||||
titleRepair: new T({en: 'Bike repair', nl: 'Fietsenmaker', fr: 'TODO: fr'}),
|
||||
titleShop: new T({en: 'Bike repair/shop', nl: 'Fietswinkel', fr: 'TODO: fr'}),
|
||||
|
||||
titleNamed: new T({en: 'Bike repair/shop', nl: 'Fietszaak {name}', fr: 'TODO: fr'}),
|
||||
titleRepairNamed: new T({en: 'Bike shop', nl: 'Fietsenmaker {name}', fr: 'TODO: fr'}),
|
||||
titleShopNamed: new T({en: 'Bike repair/shop', nl: 'Fietswinkel {name}', fr: 'TODO: fr'}),
|
||||
|
||||
|
||||
|
||||
retail: {
|
||||
question: new T({
|
||||
en: 'Does this shop sell bikes?',
|
||||
nl: 'Verkoopt deze winkel fietsen?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
yes: new T({en: 'This shop sells bikes', nl: 'Deze winkel verkoopt fietsen', fr: 'TODO: fr'}),
|
||||
no: new T({
|
||||
en: 'This shop doesn\'t sell bikes',
|
||||
nl: 'Deze winkel verkoopt geen fietsen',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
},
|
||||
repair: {
|
||||
question: new T({
|
||||
en: 'Does this shop repair bikes?',
|
||||
nl: 'Verkoopt deze winkel fietsen?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
yes: new T({en: 'This shop repairs bikes', nl: 'Deze winkel herstelt fietsen', fr: 'TODO: fr'}),
|
||||
no: new T({
|
||||
en: 'This shop doesn\'t repair bikes',
|
||||
nl: 'Deze winkel herstelt geen fietsen',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
sold: new T({en: 'This shop only repairs bikes bought here', nl: 'Deze winkel herstelt enkel fietsen die hier werden gekocht', fr: 'TODO: fr'}),
|
||||
brand: new T({en: 'This shop only repairs bikes of a certain brand', nl: 'Deze winkel herstelt enkel fietsen van een bepaald merk', fr: 'TODO: fr'}),
|
||||
},
|
||||
rental: {
|
||||
question: new T({
|
||||
en: 'Does this shop rent out bikes?',
|
||||
nl: 'Verhuurt deze winkel fietsen?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
yes: new T({en: 'This shop rents out bikes', nl: 'Deze winkel verhuurt fietsen', fr: 'TODO: fr'}),
|
||||
no: new T({
|
||||
en: 'This shop doesn\'t rent out bikes',
|
||||
nl: 'Deze winkel verhuurt geen fietsen',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
},
|
||||
pump: {
|
||||
question: new T({
|
||||
en: 'Does this shop offer a bike pump for use by anyone?',
|
||||
nl: 'Biedt deze winkel een fietspomp aan voor iedereen?',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
yes: new T({
|
||||
en: 'This shop offers a bike pump for anyone',
|
||||
nl: 'Deze winkel biedt geen fietspomp aan voor eender wie',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
no: new T({
|
||||
en: 'This shop doesn\'t offer a bike pump for anyone',
|
||||
nl: 'Deze winkel biedt een fietspomp aan voor iedereen',
|
||||
fr: 'TODO: fr'
|
||||
})
|
||||
},
|
||||
qName: {
|
||||
question: new T({en: 'What is the name of this bicycle shop?', nl: 'Wat is de naam van deze fietszaak?', fr: 'TODO: fr'}),
|
||||
render: new T({en: 'This bicycle shop is called {name}', nl: 'Deze fietszaak heet <b>{name}</b>', fr: 'TODO: fr'}),
|
||||
template: new T({en: 'This bicycle shop is called: $$$', nl: 'Deze fietszaak heet: <b>$$$</b>', fr: 'TODO: fr'})
|
||||
},
|
||||
secondHand: {
|
||||
question: new T({en: 'Does this shop sell second-hand bikes?', nl: 'Verkoopt deze winkel tweedehands fietsen?', fr: 'TODO: fr'}),
|
||||
yes: new T({en: 'This shop sells second-hand bikes', nl: 'Deze winkel verkoopt tweedehands fietsen', fr: 'TODO: fr'}),
|
||||
no: new T({en: 'This shop doesn\'t sell second-hand bikes', nl: 'Deze winkel verkoopt geen tweedehands fietsen', fr: 'TODO: fr'}),
|
||||
only: new T({en: 'This shop only sells second-hand bikes', nl: 'Deze winkel verkoopt enkel tweedehands fietsen', fr: 'TODO: fr'}),
|
||||
},
|
||||
diy: {
|
||||
question: new T({en: 'Are there tools here to repair your own bike?', nl: 'Biedt deze winkel gereedschap aan om je fiets zelf te herstellen?', fr: 'TODO: fr'}),
|
||||
yes: new T({en: 'This shop offers tools for DIY repair', nl: 'Deze winkel biedt gereedschap aan om je fiets zelf te herstellen', fr: 'TODO: fr'}),
|
||||
no: new T({en: 'This shop doesn\'t offer tools for DIY repair', nl: 'Deze winkel biedt geen gereedschap aan om je fiets zelf te herstellen', fr: 'TODO: fr'}),
|
||||
}
|
||||
}
|
||||
},
|
||||
image: {
|
||||
addPicture: new T({en: 'Add picture', nl: 'Voeg foto toe', fr: 'TODO: fr'}),
|
||||
uploadingPicture: new T({
|
||||
en: 'Uploading your picture...',
|
||||
nl: 'Bezig met een foto te uploaden...',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
pleaseLogin: new T({
|
||||
en: 'Please login to add a picure or to answer questions',
|
||||
nl: 'Gelieve je aan te melden om een foto toe te voegen of vragen te beantwoorden',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
willBePublished: new T({
|
||||
en: 'Your picture will be published: ',
|
||||
nl: 'Jouw foto wordt gepubliceerd: ',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
cco: new T({en: 'in the public domain', nl: 'in het publiek domein', fr: 'TODO: fr'}),
|
||||
ccbs: new T({en: 'under the CC-BY-SA-license', nl: 'onder de CC-BY-SA-licentie', fr: 'TODO: fr'}),
|
||||
ccb: new T({en: 'under the CC-BY-license', nl: 'onder de CC-BY-licentie', fr: 'TODO: fr'})
|
||||
},
|
||||
centerMessage: {
|
||||
loadingData: new T({en: 'Loading data...', nl: 'Data wordt geladen...', fr: 'TODO: fr'}),
|
||||
zoomIn: new T({
|
||||
en: 'Zoom in to view or edit the data',
|
||||
nl: 'Zoom in om de data te zien en te bewerken',
|
||||
fr: 'TODO: fr'
|
||||
}),
|
||||
ready: new T({en: 'Done!', nl: 'Klaar!', fr: 'TODO: fr'}),
|
||||
},
|
||||
general: {
|
||||
loginWithOpenStreetMap: new T({en: "Login with OpenStreetMap", nl: "Aanmelden met OpenStreetMap"}),
|
||||
getStarted: new T({
|
||||
en: "<span class='activate-osm-authentication'>Login with OpenStreetMap</span> or <a href='https://www.openstreetmap.org/user/new' target='_blank'>make a free account to get started</a>",
|
||||
nl: "<span class='activate-osm-authentication'>Meld je aan met je OpenStreetMap-account</span> of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak snel en gratis een account om te beginnen/a>",
|
||||
}),
|
||||
welcomeBack: new T({
|
||||
en: "You are logged in, welcome back!",
|
||||
nl: "Je bent aangemeld. Welkom terug!"
|
||||
}),
|
||||
search: {
|
||||
search: new Translation({
|
||||
en: "Search a location",
|
||||
nl: "Zoek naar een locatie"
|
||||
}),
|
||||
searching: new Translation({
|
||||
en: "Searching...",
|
||||
nl: "Aan het zoeken..."
|
||||
}),
|
||||
nothing: new Translation({
|
||||
en: "Nothing found...",
|
||||
nl: "Niet gevonden..."
|
||||
}),
|
||||
error: new Translation({
|
||||
en: "Something went wrong...",
|
||||
nl: "Niet gelukt..."
|
||||
})
|
||||
|
||||
},
|
||||
returnToTheMap: new T({
|
||||
en: "Return to the map",
|
||||
nl: "Naar de kaart"
|
||||
}),
|
||||
save: new T({
|
||||
en: "Save",
|
||||
nl: "Opslaan"
|
||||
}),
|
||||
cancel: new T({
|
||||
en: "Cancel",
|
||||
nl: "Annuleren"
|
||||
}),
|
||||
skip: new T({
|
||||
en: "Skip this question",
|
||||
nl: "Vraag overslaan"
|
||||
}),
|
||||
oneSkippedQuestion: new T({
|
||||
en: "One question is skipped",
|
||||
nl: "Een vraag is overgeslaan"
|
||||
}),
|
||||
skippedQuestions: new T({
|
||||
en: "Some questions are skipped",
|
||||
nl: "Sommige vragen zijn overgeslaan"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public static W(s: string | UIElement): UIElement {
|
||||
if (s instanceof UIElement) {
|
||||
return s;
|
||||
}
|
||||
return new FixedUiElement(s);
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue