forked from MapComplete/MapComplete
Add ToC to generated pages
This commit is contained in:
parent
b4529e4f63
commit
752538ec14
18 changed files with 346 additions and 243 deletions
|
@ -47,18 +47,10 @@ export default class Combine extends BaseUIElement {
|
|||
return el;
|
||||
}
|
||||
|
||||
public getToC(): Title[]{
|
||||
const titles = []
|
||||
for (const uiElement of this.uiElements) {
|
||||
if(uiElement instanceof Combine){
|
||||
titles.push(...uiElement.getToC())
|
||||
}else if(uiElement instanceof Title){
|
||||
titles.push(uiElement)
|
||||
}
|
||||
}
|
||||
return titles
|
||||
|
||||
|
||||
public getElements(): BaseUIElement[]{
|
||||
return this.uiElements
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,25 +1,26 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class FixedUiElement extends BaseUIElement {
|
||||
private _html: string;
|
||||
public readonly content: string;
|
||||
|
||||
constructor(html: string) {
|
||||
super();
|
||||
this._html = html ?? "";
|
||||
this.content = html ?? "";
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._html;
|
||||
return this.content;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this._html;
|
||||
return this.content;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const e = document.createElement("span")
|
||||
e.innerHTML = this._html
|
||||
e.innerHTML = this.content
|
||||
return e;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -10,7 +10,7 @@ export default class List extends BaseUIElement {
|
|||
super();
|
||||
this._ordered = ordered;
|
||||
this.uiElements = Utils.NoNull(uiElements)
|
||||
.map(Translations.W);
|
||||
.map(s => Translations.W(s));
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
|
|
123
UI/Base/TableOfContents.ts
Normal file
123
UI/Base/TableOfContents.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import Combine from "./Combine";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import Title from "./Title";
|
||||
import List from "./List";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
import Link from "./Link";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class TableOfContents extends Combine {
|
||||
|
||||
private readonly titles: Title[]
|
||||
|
||||
constructor(elements: Combine | Title[], options?: {
|
||||
noTopLevel: false | boolean,
|
||||
maxDepth?: number
|
||||
}) {
|
||||
let titles: Title[]
|
||||
if (elements instanceof Combine) {
|
||||
titles = TableOfContents.getTitles(elements.getElements())
|
||||
} else {
|
||||
titles = elements
|
||||
}
|
||||
|
||||
let els: { level: number, content: BaseUIElement }[] = []
|
||||
for (const title of titles) {
|
||||
let content: BaseUIElement
|
||||
if (title.title instanceof Translation) {
|
||||
content = title.title.Clone()
|
||||
}else if(title.title instanceof FixedUiElement){
|
||||
content = title.title
|
||||
}else if(Utils.runningFromConsole){
|
||||
content = new FixedUiElement(title.AsMarkdown())
|
||||
} else {
|
||||
content = new FixedUiElement(title.title.ConstructElement().innerText)
|
||||
}
|
||||
|
||||
const vis = new Link(content, "#" + title.id)
|
||||
|
||||
Hash.hash.addCallbackAndRun(h => {
|
||||
if (h === title.id) {
|
||||
vis.SetClass("font-bold")
|
||||
} else {
|
||||
vis.RemoveClass("font-bold")
|
||||
}
|
||||
})
|
||||
els.push({level: title.level, content: vis})
|
||||
|
||||
}
|
||||
const minLevel = Math.min(...els.map(e => e.level))
|
||||
if (options?.noTopLevel) {
|
||||
els = els.filter(e => e.level !== minLevel )
|
||||
}
|
||||
|
||||
if(options?.maxDepth){
|
||||
els = els.filter(e => e.level <= (options.maxDepth + minLevel))
|
||||
}
|
||||
|
||||
|
||||
super(TableOfContents.mergeLevel(els).map(el => el.SetClass("mt-2")));
|
||||
this.SetClass("flex flex-col")
|
||||
this.titles = titles;
|
||||
}
|
||||
|
||||
private static getTitles(elements: BaseUIElement[]): Title[]{
|
||||
const titles = []
|
||||
for (const uiElement of elements){
|
||||
if(uiElement instanceof Combine){
|
||||
titles.push(...TableOfContents.getTitles(uiElement.getElements()))
|
||||
}else if(uiElement instanceof Title){
|
||||
titles.push(uiElement)
|
||||
}
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
private static mergeLevel(elements: { level: number, content: BaseUIElement }[]): BaseUIElement[] {
|
||||
const maxLevel = Math.max(...elements.map(e => e.level))
|
||||
const minLevel = Math.min(...elements.map(e => e.level))
|
||||
if (maxLevel === minLevel) {
|
||||
return elements.map(e => e.content)
|
||||
}
|
||||
const result: { level: number, content: BaseUIElement } [] = []
|
||||
let running: BaseUIElement[] = []
|
||||
for (const element of elements) {
|
||||
if (element.level === maxLevel) {
|
||||
running.push(element.content)
|
||||
continue
|
||||
}
|
||||
if (running.length !== undefined) {
|
||||
result.push({
|
||||
content: new List(running),
|
||||
level: maxLevel - 1
|
||||
})
|
||||
running = []
|
||||
}
|
||||
result.push(element)
|
||||
}
|
||||
if (running.length !== undefined) {
|
||||
result.push({
|
||||
content: new List(running),
|
||||
level: maxLevel - 1
|
||||
})
|
||||
}
|
||||
|
||||
return TableOfContents.mergeLevel(result)
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
const depthIcons = ["1."," -"," +"," *"]
|
||||
const lines = ["## Table of contents\n"];
|
||||
const minLevel = Math.min(...this.titles.map(t => t.level))
|
||||
for (const title of this.titles) {
|
||||
const prefix = depthIcons[title.level - minLevel] ?? " ~"
|
||||
const text = title.title.AsMarkdown().replace("\n","")
|
||||
const link = title.id
|
||||
lines.push(prefix + " ["+text+"](#"+link+")")
|
||||
}
|
||||
|
||||
return lines.join("\n")+"\n\n"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
|
||||
export default class Title extends BaseUIElement {
|
||||
public readonly title: BaseUIElement;
|
||||
|
@ -17,7 +16,19 @@ export default class Title extends BaseUIElement {
|
|||
this.title = embedded
|
||||
}
|
||||
this.level = level;
|
||||
this.id = this.title.ConstructElement()?.innerText?.replace(/ /g, '_') ?? ""
|
||||
|
||||
let innerText : string = undefined;
|
||||
if(typeof embedded === "string" ) {
|
||||
innerText = embedded
|
||||
}else if(embedded instanceof FixedUiElement){
|
||||
innerText = embedded.content
|
||||
}else{
|
||||
this.title.ConstructElement()?.innerText
|
||||
}
|
||||
|
||||
this.id = innerText?.replace(/ /g, '-')
|
||||
?.replace(/[?#.;:/]/, "")
|
||||
?.toLowerCase() ?? ""
|
||||
this.SetClass(Title.defaultClassesPerLevel[level] ?? "")
|
||||
}
|
||||
|
||||
|
|
|
@ -553,13 +553,15 @@ export default class ValidatedTextField {
|
|||
return input;
|
||||
}
|
||||
|
||||
public static HelpText(): string {
|
||||
const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n")
|
||||
public static HelpText(): BaseUIElement {
|
||||
const explanations : BaseUIElement[]=
|
||||
ValidatedTextField.tpList.map(type =>
|
||||
new Combine([new Title(type.name,3), type.explanation]).SetClass("flex flex-col"))
|
||||
return new Combine([
|
||||
new Title("Available types for text fields", 1),
|
||||
"The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them",
|
||||
explanations
|
||||
]).SetClass("flex flex-col").AsMarkdown()
|
||||
...explanations
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
private static tp(name: string,
|
||||
|
|
|
@ -5,97 +5,10 @@ import Title from "./Base/Title";
|
|||
import Toggleable, {Accordeon} from "./Base/Toggleable";
|
||||
import List from "./Base/List";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
import Link from "./Base/Link";
|
||||
import LanguagePicker from "./LanguagePicker";
|
||||
import Hash from "../Logic/Web/Hash";
|
||||
import {Translation} from "./i18n/Translation";
|
||||
import {SubtleButton} from "./Base/SubtleButton";
|
||||
import Svg from "../Svg";
|
||||
|
||||
class TableOfContents extends Combine {
|
||||
|
||||
private readonly titles: Title[]
|
||||
|
||||
constructor(elements: Combine | Title[], options: {
|
||||
noTopLevel: false | boolean,
|
||||
maxDepth?: number
|
||||
}) {
|
||||
let titles: Title[]
|
||||
if (elements instanceof Combine) {
|
||||
titles = elements.getToC()
|
||||
} else {
|
||||
titles = elements
|
||||
}
|
||||
|
||||
let els: { level: number, content: BaseUIElement }[] = []
|
||||
for (const title of titles) {
|
||||
let content: BaseUIElement
|
||||
if (title.title instanceof Translation) {
|
||||
content = title.title.Clone()
|
||||
} else {
|
||||
content = new FixedUiElement(title.title.ConstructElement().innerText)
|
||||
}
|
||||
|
||||
const vis = new Link(content, "#" + title.id)
|
||||
|
||||
Hash.hash.addCallbackAndRun(h => {
|
||||
if (h === title.id) {
|
||||
vis.SetClass("font-bold")
|
||||
} else {
|
||||
vis.RemoveClass("font-bold")
|
||||
}
|
||||
})
|
||||
els.push({level: title.level, content: vis})
|
||||
|
||||
}
|
||||
if (options.noTopLevel) {
|
||||
const minLevel = Math.min(...els.map(e => e.level))
|
||||
els = els.filter(e => e.level !== minLevel && e.level <= (options.maxDepth + minLevel))
|
||||
}
|
||||
|
||||
|
||||
super(TableOfContents.mergeLevel(els).map(el => el.SetClass("mt-2")));
|
||||
this.SetClass("flex flex-col")
|
||||
this.titles = titles;
|
||||
}
|
||||
|
||||
private static mergeLevel(elements: { level: number, content: BaseUIElement }[]): BaseUIElement[] {
|
||||
const maxLevel = Math.max(...elements.map(e => e.level))
|
||||
const minLevel = Math.min(...elements.map(e => e.level))
|
||||
if (maxLevel === minLevel) {
|
||||
return elements.map(e => e.content)
|
||||
}
|
||||
const result: { level: number, content: BaseUIElement } [] = []
|
||||
let running: BaseUIElement[] = []
|
||||
for (const element of elements) {
|
||||
if (element.level === maxLevel) {
|
||||
running.push(element.content)
|
||||
continue
|
||||
}
|
||||
if (running.length !== undefined) {
|
||||
result.push({
|
||||
content: new List(running),
|
||||
level: maxLevel - 1
|
||||
})
|
||||
running = []
|
||||
}
|
||||
result.push(element)
|
||||
}
|
||||
if (running.length !== undefined) {
|
||||
result.push({
|
||||
content: new List(running),
|
||||
level: maxLevel - 1
|
||||
})
|
||||
}
|
||||
|
||||
return TableOfContents.mergeLevel(result)
|
||||
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return super.AsMarkdown();
|
||||
}
|
||||
}
|
||||
import TableOfContents from "./Base/TableOfContents";
|
||||
|
||||
class Snippet extends Toggleable {
|
||||
constructor(translations, ...extraContent: BaseUIElement[]) {
|
||||
|
|
40
UI/QueryParameterDocumentation.ts
Normal file
40
UI/QueryParameterDocumentation.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import BaseUIElement from "./BaseUIElement";
|
||||
import Combine from "./Base/Combine";
|
||||
import Title from "./Base/Title";
|
||||
import List from "./Base/List";
|
||||
import Translations from "./i18n/Translations";
|
||||
import {QueryParameters} from "../Logic/Web/QueryParameters";
|
||||
|
||||
export default class QueryParameterDocumentation {
|
||||
|
||||
private static QueryParamDocsIntro = ([
|
||||
new Title("URL-parameters and URL-hash", 1),
|
||||
"This document gives an overview of which URL-parameters can be used to influence MapComplete.",
|
||||
new Title("What is a URL parameter?", 2),
|
||||
"\"URL-parameters are extra parts of the URL used to set the state.",
|
||||
"For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, " +
|
||||
"the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely: ",
|
||||
new List([
|
||||
"The url-parameter `lat` is `51.0` in this instance",
|
||||
"The url-parameter `lon` is `4.3` in this instance",
|
||||
"The url-parameter `z` is `5` in this instance",
|
||||
"The url-parameter `test` is `true` in this instance"
|
||||
].map(s => Translations.W(s))
|
||||
),
|
||||
"Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case."
|
||||
])
|
||||
|
||||
public static GenerateQueryParameterDocs(): BaseUIElement {
|
||||
const docs : (string | BaseUIElement)[] = [...QueryParameterDocumentation.QueryParamDocsIntro];
|
||||
for (const key in QueryParameters.documentation) {
|
||||
const c = new Combine([
|
||||
new Title(key, 2),
|
||||
QueryParameters.documentation[key],
|
||||
QueryParameters.defaults[key] === undefined ? "No default value set" : `The default value is _${QueryParameters.defaults[key]}_`
|
||||
|
||||
])
|
||||
docs.push(c)
|
||||
}
|
||||
return new Combine(docs).SetClass("flex flex-col")
|
||||
}
|
||||
}
|
|
@ -698,16 +698,10 @@ export default class SpecialVisualizations {
|
|||
]
|
||||
));
|
||||
|
||||
|
||||
const toc = new List(
|
||||
SpecialVisualizations.specialVisualizations.map(viz => new Link(viz.funcName, "#" + viz.funcName))
|
||||
)
|
||||
|
||||
return new Combine([
|
||||
new Title("Special tag renderings", 3),
|
||||
new Title("Special tag renderings", 1),
|
||||
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
|
||||
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
|
||||
toc,
|
||||
...helpTexts
|
||||
]
|
||||
).SetClass("flex flex-col");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue