Path: blob/main/src/format/dashboard/format-dashboard-shared.ts
6451 views
/*1* format-dashboard-shared.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/5import { warning } from "../../deno_ral/log.ts";6import { kTitle } from "../../config/constants.ts";7import { Format, Metadata } from "../../config/types.ts";8import { Document, Element } from "../../core/deno-dom.ts";9import { gitHubContext } from "../../core/github.ts";10import { formatResourcePath } from "../../core/resources.ts";11import { sassLayer } from "../../core/sass.ts";12import { formatDarkMode } from "../html/format-html-info.ts";13import { kBootstrapDependencyName } from "../html/format-html-shared.ts";1415export const kDashboard = "dashboard";1617export const kDTTableSentinel = "data-dt-support";1819// Carries the layout for a given row or column20export const kLayoutAttr = "data-layout";21export const kFillAttr = "data-fill";22export const kLayoutFill = "fill";23export const kLayoutFlow = "flow";24export type Layout = "fill" | "flow" | string;2526export const kCardClass = "card";2728export const kDashboardGridSkip = "grid-skip";2930export const kNavButtons = "nav-buttons";3132export const kDontMutateTags = ["P", "SCRIPT"];3334export interface NavButton {35href: string;36text?: string;37icon?: string;38rel?: string;39target?: string;40title?: string;41["aria-label"]?: string;42}4344export interface DashboardMeta {45orientation: "rows" | "columns";46scrolling: boolean;47expandable: boolean;48hasDarkMode: boolean;49[kNavButtons]: NavButton[];50}5152export const kValueboxClass = "valuebox";5354export function dashboardScssLayer() {55// Inject a quarto dashboard scss file into the bootstrap scss layer56const dashboardScss = formatResourcePath(57"dashboard",58"quarto-dashboard.scss",59);60const dashboardLayer = sassLayer(dashboardScss);61const dashboardScssDependency = {62dependency: kBootstrapDependencyName,63key: dashboardScss,64quarto: {65name: "quarto-dashboard.css",66...dashboardLayer,67},68};69return dashboardScssDependency;70}7172export async function dashboardMeta(format: Format): Promise<DashboardMeta> {73const dashboardRaw = format.metadata as Metadata;74const orientation = dashboardRaw && dashboardRaw.orientation === "columns"75? "columns"76: "rows";77const scrolling = dashboardRaw.scrolling === true;78const expandable = dashboardRaw.expandable !== false;79const dashboardTitle = format.metadata[kTitle] as string | undefined;8081const processNavbarButton = async (buttonRaw: unknown) => {82if (typeof buttonRaw === "string") {83if (kNavButtonAliases[buttonRaw] !== undefined) {84return kNavButtonAliases[buttonRaw](dashboardTitle);85}86return undefined;87} else {88return buttonRaw as NavButton;89}90};9192const navbarButtons = [];93const navbarButtonsRaw = format.metadata[kNavButtons];94if (Array.isArray(navbarButtonsRaw)) {95for (const btnRaw of navbarButtonsRaw) {96const btn = await processNavbarButton(btnRaw);97if (btn) {98navbarButtons.push(btn);99}100}101} else {102const btn = await processNavbarButton(navbarButtonsRaw);103if (btn) {104navbarButtons.push(btn);105}106}107108const hasDarkMode = formatDarkMode(format) !== undefined;109110return {111orientation,112scrolling,113expandable,114hasDarkMode,115[kNavButtons]: navbarButtons,116};117}118119export interface Attr {120id?: string;121classes?: string[];122attributes?: Record<string, string>;123}124125export function hasFlowLayout(el: Element) {126return el.getAttribute(kLayoutAttr) === kLayoutFlow;127}128129// Generic helper function for making elements130export function makeEl(131name: string,132attr: Attr,133doc: Document,134) {135const el = doc.createElement(name);136if (attr.id) {137el.id = attr.id;138}139140for (const cls of attr.classes || []) {141el.classList.add(cls);142}143144const attribs = attr.attributes || {};145for (const key of Object.keys(attribs)) {146el.setAttribute(key, attribs[key]);147}148149return el;150}151152// Processes an attribute, then remove it153export const processAndRemoveAttr = (154el: Element,155attr: string,156process: (el: Element, attrValue: string) => void,157defaultValue?: string,158) => {159// See whether this card is expandable160const resolvedAttr = el.getAttribute(attr);161if (resolvedAttr !== null) {162process(el, resolvedAttr);163el.removeAttribute(attr);164} else if (defaultValue) {165process(el, defaultValue);166}167};168169// Wraps other processing functions and makes sure that170// the value has css units before passing it along171export const ensureCssUnits = (172fn: (el: Element, attrValue: string) => void,173) => {174return (el: Element, attrValue: string) => {175if (attrValue === "0") {176// Zero is allowed without units177fn(el, attrValue);178} else {179// This ends with a number and it isn't zero, make it px180const attrWithUnits = attrValue.match(kEndsWithNumber)181? `${attrValue}px`182: attrValue;183fn(el, attrWithUnits);184}185};186};187const kEndsWithNumber = /[0-9]$/;188189// Converts the value of an attribute to a style on the190// element itself191export const attrToStyle = (style: string) => {192return (el: Element, attrValue: string) => {193const newStyle: string[] = [];194195const currentStyle = el.getAttribute("style");196if (currentStyle !== null) {197newStyle.push(currentStyle);198}199newStyle.push(`${style}: ${attrValue};`);200el.setAttribute("style", newStyle.join(" "));201};202};203204// Converts an attribute on a card to a style applied to205// the card body(ies)206export const attrToCardBodyStyle = (style: string) => {207return (el: Element, attrValue: string) => {208const cardBodyNodes = el.querySelectorAll(".card-body");209for (const cardBodyNode of cardBodyNodes) {210const cardBodyEl = cardBodyNode as Element;211const newStyle: string[] = [];212213const currentStyle = el.getAttribute("style");214if (currentStyle !== null) {215newStyle.push(currentStyle);216}217newStyle.push(`${style}: ${attrValue};`);218cardBodyEl.setAttribute("style", newStyle.join(" "));219}220};221};222223export const applyClasses = (el: Element, clz: string[]) => {224for (const cls of clz) {225el.classList.add(cls);226}227};228229export const applyAttributes = (el: Element, attr: Record<string, string>) => {230for (const key of Object.keys(attr)) {231el.setAttribute(key, attr[key]);232}233};234const kNavButtonAliases: Record<235string,236(text?: string) => Promise<NavButton | undefined>237> = {238linkedin: (text?: string) => {239return Promise.resolve({240icon: "linkedin",241title: "LinkedIn",242href: `https://www.linkedin.com/sharing/share-offsite/?url=|url|&title=${243text ? encodeURI(text) : undefined244}`,245});246},247facebook: (_text?: string) => {248return Promise.resolve({249icon: "facebook",250title: "Facebook",251href: "https://www.facebook.com/sharer/sharer.php?u=|url|",252});253},254reddit: (text?: string) => {255return Promise.resolve({256icon: "reddit",257title: "Reddit",258href: `https://reddit.com/submit?url=|url|&title=${259text ? encodeURI(text) : undefined260}`,261});262},263twitter: (text?: string) => {264return Promise.resolve({265icon: "twitter",266title: "Twitter",267href: `https://twitter.com/intent/tweet?url=|url|&text=${268text ? encodeURI(text) : undefined269}`,270});271},272github: async (_text?: string) => {273const context = await gitHubContext(Deno.cwd());274if (context.repoUrl) {275return {276icon: "github",277title: "GitHub",278href: context.repoUrl,279} as NavButton;280} else {281warning(282"Unable to determine GitHub repository for the `github` nav-button. Is this directory a GitHub repository?",283);284return undefined;285}286},287};288289290