Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/dashboard/format-dashboard-shared.ts
6451 views
1
/*
2
* format-dashboard-shared.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
import { warning } from "../../deno_ral/log.ts";
7
import { kTitle } from "../../config/constants.ts";
8
import { Format, Metadata } from "../../config/types.ts";
9
import { Document, Element } from "../../core/deno-dom.ts";
10
import { gitHubContext } from "../../core/github.ts";
11
import { formatResourcePath } from "../../core/resources.ts";
12
import { sassLayer } from "../../core/sass.ts";
13
import { formatDarkMode } from "../html/format-html-info.ts";
14
import { kBootstrapDependencyName } from "../html/format-html-shared.ts";
15
16
export const kDashboard = "dashboard";
17
18
export const kDTTableSentinel = "data-dt-support";
19
20
// Carries the layout for a given row or column
21
export const kLayoutAttr = "data-layout";
22
export const kFillAttr = "data-fill";
23
export const kLayoutFill = "fill";
24
export const kLayoutFlow = "flow";
25
export type Layout = "fill" | "flow" | string;
26
27
export const kCardClass = "card";
28
29
export const kDashboardGridSkip = "grid-skip";
30
31
export const kNavButtons = "nav-buttons";
32
33
export const kDontMutateTags = ["P", "SCRIPT"];
34
35
export interface NavButton {
36
href: string;
37
text?: string;
38
icon?: string;
39
rel?: string;
40
target?: string;
41
title?: string;
42
["aria-label"]?: string;
43
}
44
45
export interface DashboardMeta {
46
orientation: "rows" | "columns";
47
scrolling: boolean;
48
expandable: boolean;
49
hasDarkMode: boolean;
50
[kNavButtons]: NavButton[];
51
}
52
53
export const kValueboxClass = "valuebox";
54
55
export function dashboardScssLayer() {
56
// Inject a quarto dashboard scss file into the bootstrap scss layer
57
const dashboardScss = formatResourcePath(
58
"dashboard",
59
"quarto-dashboard.scss",
60
);
61
const dashboardLayer = sassLayer(dashboardScss);
62
const dashboardScssDependency = {
63
dependency: kBootstrapDependencyName,
64
key: dashboardScss,
65
quarto: {
66
name: "quarto-dashboard.css",
67
...dashboardLayer,
68
},
69
};
70
return dashboardScssDependency;
71
}
72
73
export async function dashboardMeta(format: Format): Promise<DashboardMeta> {
74
const dashboardRaw = format.metadata as Metadata;
75
const orientation = dashboardRaw && dashboardRaw.orientation === "columns"
76
? "columns"
77
: "rows";
78
const scrolling = dashboardRaw.scrolling === true;
79
const expandable = dashboardRaw.expandable !== false;
80
const dashboardTitle = format.metadata[kTitle] as string | undefined;
81
82
const processNavbarButton = async (buttonRaw: unknown) => {
83
if (typeof buttonRaw === "string") {
84
if (kNavButtonAliases[buttonRaw] !== undefined) {
85
return kNavButtonAliases[buttonRaw](dashboardTitle);
86
}
87
return undefined;
88
} else {
89
return buttonRaw as NavButton;
90
}
91
};
92
93
const navbarButtons = [];
94
const navbarButtonsRaw = format.metadata[kNavButtons];
95
if (Array.isArray(navbarButtonsRaw)) {
96
for (const btnRaw of navbarButtonsRaw) {
97
const btn = await processNavbarButton(btnRaw);
98
if (btn) {
99
navbarButtons.push(btn);
100
}
101
}
102
} else {
103
const btn = await processNavbarButton(navbarButtonsRaw);
104
if (btn) {
105
navbarButtons.push(btn);
106
}
107
}
108
109
const hasDarkMode = formatDarkMode(format) !== undefined;
110
111
return {
112
orientation,
113
scrolling,
114
expandable,
115
hasDarkMode,
116
[kNavButtons]: navbarButtons,
117
};
118
}
119
120
export interface Attr {
121
id?: string;
122
classes?: string[];
123
attributes?: Record<string, string>;
124
}
125
126
export function hasFlowLayout(el: Element) {
127
return el.getAttribute(kLayoutAttr) === kLayoutFlow;
128
}
129
130
// Generic helper function for making elements
131
export function makeEl(
132
name: string,
133
attr: Attr,
134
doc: Document,
135
) {
136
const el = doc.createElement(name);
137
if (attr.id) {
138
el.id = attr.id;
139
}
140
141
for (const cls of attr.classes || []) {
142
el.classList.add(cls);
143
}
144
145
const attribs = attr.attributes || {};
146
for (const key of Object.keys(attribs)) {
147
el.setAttribute(key, attribs[key]);
148
}
149
150
return el;
151
}
152
153
// Processes an attribute, then remove it
154
export const processAndRemoveAttr = (
155
el: Element,
156
attr: string,
157
process: (el: Element, attrValue: string) => void,
158
defaultValue?: string,
159
) => {
160
// See whether this card is expandable
161
const resolvedAttr = el.getAttribute(attr);
162
if (resolvedAttr !== null) {
163
process(el, resolvedAttr);
164
el.removeAttribute(attr);
165
} else if (defaultValue) {
166
process(el, defaultValue);
167
}
168
};
169
170
// Wraps other processing functions and makes sure that
171
// the value has css units before passing it along
172
export const ensureCssUnits = (
173
fn: (el: Element, attrValue: string) => void,
174
) => {
175
return (el: Element, attrValue: string) => {
176
if (attrValue === "0") {
177
// Zero is allowed without units
178
fn(el, attrValue);
179
} else {
180
// This ends with a number and it isn't zero, make it px
181
const attrWithUnits = attrValue.match(kEndsWithNumber)
182
? `${attrValue}px`
183
: attrValue;
184
fn(el, attrWithUnits);
185
}
186
};
187
};
188
const kEndsWithNumber = /[0-9]$/;
189
190
// Converts the value of an attribute to a style on the
191
// element itself
192
export const attrToStyle = (style: string) => {
193
return (el: Element, attrValue: string) => {
194
const newStyle: string[] = [];
195
196
const currentStyle = el.getAttribute("style");
197
if (currentStyle !== null) {
198
newStyle.push(currentStyle);
199
}
200
newStyle.push(`${style}: ${attrValue};`);
201
el.setAttribute("style", newStyle.join(" "));
202
};
203
};
204
205
// Converts an attribute on a card to a style applied to
206
// the card body(ies)
207
export const attrToCardBodyStyle = (style: string) => {
208
return (el: Element, attrValue: string) => {
209
const cardBodyNodes = el.querySelectorAll(".card-body");
210
for (const cardBodyNode of cardBodyNodes) {
211
const cardBodyEl = cardBodyNode as Element;
212
const newStyle: string[] = [];
213
214
const currentStyle = el.getAttribute("style");
215
if (currentStyle !== null) {
216
newStyle.push(currentStyle);
217
}
218
newStyle.push(`${style}: ${attrValue};`);
219
cardBodyEl.setAttribute("style", newStyle.join(" "));
220
}
221
};
222
};
223
224
export const applyClasses = (el: Element, clz: string[]) => {
225
for (const cls of clz) {
226
el.classList.add(cls);
227
}
228
};
229
230
export const applyAttributes = (el: Element, attr: Record<string, string>) => {
231
for (const key of Object.keys(attr)) {
232
el.setAttribute(key, attr[key]);
233
}
234
};
235
const kNavButtonAliases: Record<
236
string,
237
(text?: string) => Promise<NavButton | undefined>
238
> = {
239
linkedin: (text?: string) => {
240
return Promise.resolve({
241
icon: "linkedin",
242
title: "LinkedIn",
243
href: `https://www.linkedin.com/sharing/share-offsite/?url=|url|&title=${
244
text ? encodeURI(text) : undefined
245
}`,
246
});
247
},
248
facebook: (_text?: string) => {
249
return Promise.resolve({
250
icon: "facebook",
251
title: "Facebook",
252
href: "https://www.facebook.com/sharer/sharer.php?u=|url|",
253
});
254
},
255
reddit: (text?: string) => {
256
return Promise.resolve({
257
icon: "reddit",
258
title: "Reddit",
259
href: `https://reddit.com/submit?url=|url|&title=${
260
text ? encodeURI(text) : undefined
261
}`,
262
});
263
},
264
twitter: (text?: string) => {
265
return Promise.resolve({
266
icon: "twitter",
267
title: "Twitter",
268
href: `https://twitter.com/intent/tweet?url=|url|&text=${
269
text ? encodeURI(text) : undefined
270
}`,
271
});
272
},
273
github: async (_text?: string) => {
274
const context = await gitHubContext(Deno.cwd());
275
if (context.repoUrl) {
276
return {
277
icon: "github",
278
title: "GitHub",
279
href: context.repoUrl,
280
} as NavButton;
281
} else {
282
warning(
283
"Unable to determine GitHub repository for the `github` nav-button. Is this directory a GitHub repository?",
284
);
285
return undefined;
286
}
287
},
288
};
289
290