diff --git a/R/sysdata.rda b/R/sysdata.rda index c001f94cd..f97a55dff 100644 Binary files a/R/sysdata.rda and b/R/sysdata.rda differ diff --git a/inst/components/sidebar.min.js b/inst/components/sidebar.min.js index 476c5bed1..92990fe57 100644 --- a/inst/components/sidebar.min.js +++ b/inst/components/sidebar.min.js @@ -1,3 +1,3 @@ /*! bslib 0.4.2.9000 | (c) 2012-2023 RStudio, PBC. | License: MIT + file LICENSE */ -"use strict";(()=>{var p=window.Shiny?Shiny.InputBinding:class{};function b(a,e){window.Shiny&&Shiny.inputBindings.register(new a,"bslib."+e)}function g(a){if($(a).data("window-resize-observer"))return;let e=new Event("resize"),t=new ResizeObserver(()=>{window.dispatchEvent(e)});t.observe(a),$(a).data("window-resize-observer",t)}var i=class{constructor(e){if(i.instanceMap.set(e,this),this.layout={container:e,main:e.querySelector(":scope > .main"),sidebar:e.querySelector(":scope > .sidebar"),toggle:e.querySelector(":scope > .collapse-toggle")},!this.layout.toggle)throw new Error("Tried to initialize a non-collapsible sidebar.");this._initEventListeners(),this._initSidebarCounters(),this._initDesktop(),e.removeAttribute("data-bslib-sidebar-init");let t=e.querySelector(":scope > script[data-bslib-sidebar-init]");t&&e.removeChild(t)}get isClosed(){return this.layout.container.classList.contains(i.classes.COLLAPSE)}static getInstance(e){return i.instanceMap.get(e)}static initCollapsibleAll(){if(document.readyState==="loading"){i.onReadyScheduled||(i.onReadyScheduled=!0,document.addEventListener("DOMContentLoaded",()=>{i.initCollapsibleAll()}));return}let e=`.${i.classes.LAYOUT}[data-bslib-sidebar-init]`;if(!document.querySelector(e))return;document.querySelectorAll(e).forEach(n=>new i(n))}_initEventListeners(){var n;let{sidebar:e,toggle:t}=this.layout;t.addEventListener("click",r=>{r.preventDefault(),this.toggle("toggle")}),(n=t.querySelector(".collapse-icon"))==null||n.addEventListener("transitionend",()=>{this._finalizeState(),$(e).trigger("toggleCollapse.sidebarInputBinding")})}_initSidebarCounters(){let{container:e}=this.layout,t=`.${i.classes.LAYOUT}> .main > .${i.classes.LAYOUT}:not([data-bslib-sidebar-open="always"])`;if(!(e.querySelector(t)===null))return;function r(s){return s=s?s.parentElement:null,s&&s.classList.contains("main")&&(s=s.parentElement),s&&s.classList.contains(i.classes.LAYOUT)?s:null}let l=[e],d=r(e);for(;d;)l.unshift(d),d=r(d);let u={left:0,right:0};l.forEach(function(s,y){s.style.setProperty("--bslib-sidebar-counter",y.toString());let f=s.classList.contains("sidebar-right")?u.right++:u.left++;s.style.setProperty("--bslib-sidebar-overlap-counter",f.toString())})}_initDesktop(){var n;let{container:e}=this.layout;if(((n=e.dataset.bslibSidebarOpen)==null?void 0:n.trim())!=="desktop")return;window.getComputedStyle(e).getPropertyValue("--bslib-sidebar-js-init-collapsed").trim()==="true"&&this.toggle("close")}toggle(e){typeof e=="undefined"&&(e="toggle");let{container:t,main:n,sidebar:r}=this.layout,l=this.isClosed;if(["open","close","toggle"].indexOf(e)===-1)throw new Error(`Unknown method ${e}`);e==="toggle"&&(e=l?"open":"close"),!(l&&e==="close"||!l&&e==="open")&&(g(n),e==="open"&&(r.hidden=!1),t.classList.add(i.classes.TRANSITIONING),t.classList.toggle(i.classes.COLLAPSE))}_finalizeState(){let{container:e,sidebar:t,toggle:n}=this.layout;e.classList.remove(i.classes.TRANSITIONING),t.hidden=this.isClosed,n.ariaExpanded=this.isClosed?"false":"true"}},o=i;o.classes={LAYOUT:"bslib-sidebar-layout",COLLAPSE:"sidebar-collapsed",TRANSITIONING:"transitioning"},o.onReadyScheduled=!1,o.instanceMap=new WeakMap;var c=class extends p{find(e){return $(e).find(`.${o.classes.LAYOUT} > .bslib-sidebar-input`)}getValue(e){let t=o.getInstance(e.parentElement);return t?t.isClosed:!1}setValue(e,t){let n=t?"open":"close";this.receiveMessage(e,{method:n})}subscribe(e,t){$(e).on("toggleCollapse.sidebarInputBinding",function(n){t(!0)})}unsubscribe(e){$(e).off(".sidebarInputBinding")}receiveMessage(e,t){let n=o.getInstance(e.parentElement);n&&n.toggle(t.method)}};b(c,"sidebar");window.bslib=window.bslib||{};window.bslib.Sidebar=o;})(); +"use strict";(()=>{var g=window.Shiny?Shiny.InputBinding:class{};function y(b,e){window.Shiny&&Shiny.inputBindings.register(new b,"bslib."+e)}var u=class{constructor(){this.resizeObserverEntries=[],this.resizeObserver=new ResizeObserver(e=>{let t=new Event("resize");if(window.dispatchEvent(t),!window.Shiny)return;let i=[];for(let r of e)r.target instanceof HTMLElement&&r.target.querySelector(".shiny-bound-output")&&r.target.querySelectorAll(".shiny-bound-output").forEach(o=>{if(i.includes(o))return;let{binding:l,onResize:d}=$(o).data("shinyOutputBinding");if(!l||!l.resize)return;let s=o.shinyResizeObserver;if(s&&s!==this||(s||(o.shinyResizeObserver=this),d(o),i.push(o),!o.classList.contains("shiny-plot-output")))return;let c=o.querySelector('img:not([width="100%"])');c&&c.setAttribute("width","100%")})})}observe(e){this.resizeObserver.observe(e),this.resizeObserverEntries.push(e)}unobserve(e){let t=this.resizeObserverEntries.indexOf(e);t<0||(this.resizeObserver.unobserve(e),this.resizeObserverEntries.splice(t,1))}flush(){this.resizeObserverEntries.forEach(e=>{document.body.contains(e)||this.unobserve(e)})}};var n=class{constructor(e){if(n.instanceMap.set(e,this),this.layout={container:e,main:e.querySelector(":scope > .main"),sidebar:e.querySelector(":scope > .sidebar"),toggle:e.querySelector(":scope > .collapse-toggle")},!this.layout.toggle)throw new Error("Tried to initialize a non-collapsible sidebar.");this._initEventListeners(),this._initSidebarCounters(),this._initDesktop(),n.shinyResizeObserver.observe(this.layout.main),e.removeAttribute("data-bslib-sidebar-init");let t=e.querySelector(":scope > script[data-bslib-sidebar-init]");t&&e.removeChild(t)}get isClosed(){return this.layout.container.classList.contains(n.classes.COLLAPSE)}static getInstance(e){return n.instanceMap.get(e)}static initCollapsibleAll(e=!0){if(document.readyState==="loading"){n.onReadyScheduled||(n.onReadyScheduled=!0,document.addEventListener("DOMContentLoaded",()=>{n.initCollapsibleAll(!1)}));return}let t=`.${n.classes.LAYOUT}[data-bslib-sidebar-init]`;if(!document.querySelector(t))return;e&&n.shinyResizeObserver.flush(),document.querySelectorAll(t).forEach(r=>new n(r))}_initEventListeners(){var i;let{sidebar:e,toggle:t}=this.layout;t.addEventListener("click",r=>{r.preventDefault(),this.toggle("toggle")}),(i=t.querySelector(".collapse-icon"))==null||i.addEventListener("transitionend",()=>{this._finalizeState(),$(e).trigger("toggleCollapse.sidebarInputBinding")})}_initSidebarCounters(){let{container:e}=this.layout,t=`.${n.classes.LAYOUT}> .main > .${n.classes.LAYOUT}:not([data-bslib-sidebar-open="always"])`;if(!(e.querySelector(t)===null))return;function r(s){return s=s?s.parentElement:null,s&&s.classList.contains("main")&&(s=s.parentElement),s&&s.classList.contains(n.classes.LAYOUT)?s:null}let o=[e],l=r(e);for(;l;)o.unshift(l),l=r(l);let d={left:0,right:0};o.forEach(function(s,c){s.style.setProperty("--bslib-sidebar-counter",c.toString());let f=s.classList.contains("sidebar-right")?d.right++:d.left++;s.style.setProperty("--bslib-sidebar-overlap-counter",f.toString())})}_initDesktop(){var i;let{container:e}=this.layout;if(((i=e.dataset.bslibSidebarOpen)==null?void 0:i.trim())!=="desktop")return;window.getComputedStyle(e).getPropertyValue("--bslib-sidebar-js-init-collapsed").trim()==="true"&&this.toggle("close")}toggle(e){typeof e=="undefined"&&(e="toggle");let{container:t,sidebar:i}=this.layout,r=this.isClosed;if(["open","close","toggle"].indexOf(e)===-1)throw new Error(`Unknown method ${e}`);e==="toggle"&&(e=r?"open":"close"),!(r&&e==="close"||!r&&e==="open")&&(e==="open"&&(i.hidden=!1),t.classList.add(n.classes.TRANSITIONING),t.classList.toggle(n.classes.COLLAPSE))}_finalizeState(){let{container:e,sidebar:t,toggle:i}=this.layout;e.classList.remove(n.classes.TRANSITIONING),t.hidden=this.isClosed,i.ariaExpanded=this.isClosed?"false":"true"}},a=n;a.shinyResizeObserver=new u,a.classes={LAYOUT:"bslib-sidebar-layout",COLLAPSE:"sidebar-collapsed",TRANSITIONING:"transitioning"},a.onReadyScheduled=!1,a.instanceMap=new WeakMap;var p=class extends g{find(e){return $(e).find(`.${a.classes.LAYOUT} > .bslib-sidebar-input`)}getValue(e){let t=a.getInstance(e.parentElement);return t?t.isClosed:!1}setValue(e,t){let i=t?"open":"close";this.receiveMessage(e,{method:i})}subscribe(e,t){$(e).on("toggleCollapse.sidebarInputBinding",function(i){t(!0)})}unsubscribe(e){$(e).off(".sidebarInputBinding")}receiveMessage(e,t){let i=a.getInstance(e.parentElement);i&&i.toggle(t.method)}};y(p,"sidebar");window.bslib=window.bslib||{};window.bslib.Sidebar=a;})(); //# sourceMappingURL=sidebar.min.js.map diff --git a/inst/components/sidebar.min.js.map b/inst/components/sidebar.min.js.map index f46cf9f01..82c5f6e30 100644 --- a/inst/components/sidebar.min.js.map +++ b/inst/components/sidebar.min.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../../srcts/src/components/_utils.ts", "../../srcts/src/components/sidebar.ts"], - "sourcesContent": ["import type { HtmlDep } from \"rstudio-shiny/srcts/types/src/shiny/render\";\n\nimport type { InputBinding as InputBindingType } from \"rstudio-shiny/srcts/types/src/bindings/input\";\n\n// Exclude undefined from T\ntype NotUndefined = T extends undefined ? never : T;\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst InputBinding = (\n window.Shiny ? Shiny.InputBinding : class {}\n) as typeof InputBindingType;\n\nfunction registerBinding(\n inputBindingClass: new () => InputBindingType,\n name: string\n): void {\n if (window.Shiny) {\n Shiny.inputBindings.register(new inputBindingClass(), \"bslib.\" + name);\n }\n}\n\n// Return true if the key exists on the object and the value is not undefined.\n//\n// This method is mainly used in input bindings' `receiveMessage` method.\n// Since we know that the values are sent by Shiny via `{jsonlite}`,\n// then we know that there are no `undefined` values. `null` is possible, but not `undefined`.\nfunction hasDefinedProperty<\n Prop extends keyof X,\n X extends { [key: string]: any }\n>(\n obj: X,\n prop: Prop\n): obj is X & { [key in NonNullable]: NotUndefined } {\n return (\n Object.prototype.hasOwnProperty.call(obj, prop) && obj[prop] !== undefined\n );\n}\n\n// TODO: Shiny should trigger resize events when the output\n// https://github.com/rstudio/shiny/pull/3682\nfunction doWindowResizeOnElementResize(el: HTMLElement): void {\n if ($(el).data(\"window-resize-observer\")) {\n return;\n }\n const resizeEvent = new Event(\"resize\");\n const ro = new ResizeObserver(() => {\n window.dispatchEvent(resizeEvent);\n });\n ro.observe(el);\n $(el).data(\"window-resize-observer\", ro);\n}\n\nexport {\n InputBinding,\n registerBinding,\n hasDefinedProperty,\n doWindowResizeOnElementResize,\n};\nexport type { HtmlDep };\n", "import {\n InputBinding,\n registerBinding,\n doWindowResizeOnElementResize,\n} from \"./_utils\";\n\n/**\n * Methods for programmatically toggling the state of the sidebar. These methods\n * describe the desired state of the sidebar: `\"close\"` and `\"open\"` transition\n * the sidebar to the desired state, unless the sidebar is already in that\n * state. `\"toggle\"` transitions the sidebar to the state opposite of its\n * current state.\n * @typedef {SidebarToggleMethod}\n */\ntype SidebarToggleMethod = \"close\" | \"open\" | \"toggle\";\n\n/**\n * Data received by the input binding's `receiveMessage` method.\n * @typedef {SidebarMessageData}\n */\ntype SidebarMessageData = {\n method: SidebarToggleMethod;\n};\n\n/**\n * The DOM elements that make up the sidebar. `main`, `sidebar`, and `toggle`\n * are all direct children of `container` (in that order).\n * @interface SidebarComponents\n * @typedef {SidebarComponents}\n */\ninterface SidebarComponents {\n /**\n * The `layout_sidebar()` parent container, with class\n * `Sidebar.classes.LAYOUT`.\n * @type {HTMLElement}\n */\n container: HTMLElement;\n /**\n * The main content area of the sidebar layout.\n * @type {HTMLElement}\n */\n main: HTMLElement;\n /**\n * The sidebar container of the sidebar layout.\n * @type {HTMLElement}\n */\n sidebar: HTMLElement;\n /**\n * The toggle button that is used to toggle the sidebar state.\n * @type {HTMLElement}\n */\n toggle: HTMLElement;\n}\n\n/**\n * The bslib sidebar component class. This class is only used for collapsible\n * sidebars.\n *\n * @class Sidebar\n * @typedef {Sidebar}\n */\nclass Sidebar {\n /**\n * The DOM elements that make up the sidebar, see `SidebarComponents`.\n * @private\n * @type {SidebarComponents}\n */\n private layout: SidebarComponents;\n /**\n * Creates an instance of a collapsible bslib Sidebar.\n * @constructor\n * @param {HTMLElement} container\n */\n constructor(container: HTMLElement) {\n Sidebar.instanceMap.set(container, this);\n this.layout = {\n container,\n main: container.querySelector(\":scope > .main\") as HTMLElement,\n sidebar: container.querySelector(\":scope > .sidebar\") as HTMLElement,\n toggle: container.querySelector(\n \":scope > .collapse-toggle\"\n ) as HTMLElement,\n } as SidebarComponents;\n\n if (!this.layout.toggle) {\n throw new Error(\"Tried to initialize a non-collapsible sidebar.\");\n }\n\n this._initEventListeners();\n this._initSidebarCounters();\n this._initDesktop();\n\n container.removeAttribute(\"data-bslib-sidebar-init\");\n const initScript = container.querySelector(\n \":scope > script[data-bslib-sidebar-init]\"\n );\n if (initScript) {\n container.removeChild(initScript);\n }\n }\n\n /**\n * Read the current state of the sidebar. Note that, when calling this method,\n * the sidebar may be transitioning into the state returned by this method.\n *\n * @description\n * The sidebar state works as follows, starting from the open state. When the\n * sidebar is closed:\n * 1. We add both the `COLLAPSE` and `TRANSITIONING` classes to the sidebar.\n * 2. The sidebar collapse begins to animate. On desktop devices, and where it\n * is supported, we transition the `grid-template-columns` property of the\n * sidebar layout. On mobile, the sidebar is hidden immediately. In both\n * cases, the collapse icon rotates and we use this rotation to determine\n * when the transition is complete.\n * 3. If another sidebar state toggle is requested while closing the sidebar,\n * we remove the `COLLAPSE` class and the animation immediately starts to\n * reverse.\n * 4. When the `transition` is complete, we remove the `TRANSITIONING` class.\n * @readonly\n * @type {boolean}\n */\n get isClosed(): boolean {\n return this.layout.container.classList.contains(Sidebar.classes.COLLAPSE);\n }\n\n /**\n * Static classes related to the sidebar layout or state.\n * @public\n * @static\n * @readonly\n * @type {{ LAYOUT: string; COLLAPSE: string; TRANSITIONING: string; }}\n */\n public static readonly classes = {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n LAYOUT: \"bslib-sidebar-layout\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n COLLAPSE: \"sidebar-collapsed\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n TRANSITIONING: \"transitioning\",\n };\n\n /**\n * If sidebars are initialized before the DOM is ready, we re-schedule the\n * initialization to occur on DOMContentLoaded.\n * @private\n * @static\n * @type {boolean}\n */\n private static onReadyScheduled = false;\n /**\n * A map of initialized sidebars to their respective Sidebar instances.\n * @private\n * @static\n * @type {WeakMap}\n */\n private static instanceMap: WeakMap = new WeakMap();\n\n /**\n * Given a sidebar container, return the Sidebar instance associated with it.\n * @public\n * @static\n * @param {HTMLElement} el\n * @returns {(Sidebar | undefined)}\n */\n public static getInstance(el: HTMLElement): Sidebar | undefined {\n return Sidebar.instanceMap.get(el);\n }\n\n /**\n * Initialize all collapsible sidebars on the page.\n * @public\n * @static\n */\n public static initCollapsibleAll(): void {\n if (document.readyState === \"loading\") {\n if (!Sidebar.onReadyScheduled) {\n Sidebar.onReadyScheduled = true;\n document.addEventListener(\"DOMContentLoaded\", () => {\n Sidebar.initCollapsibleAll();\n });\n }\n return;\n }\n\n const initSelector = `.${Sidebar.classes.LAYOUT}[data-bslib-sidebar-init]`;\n if (!document.querySelector(initSelector)) {\n // no sidebars to initialize\n return;\n }\n\n const containers = document.querySelectorAll(initSelector);\n containers.forEach((container) => new Sidebar(container as HTMLElement));\n }\n\n /**\n * Initialize event listeners for the sidebar toggle button.\n * @private\n */\n private _initEventListeners(): void {\n const { sidebar, toggle } = this.layout;\n\n toggle.addEventListener(\"click\", (ev) => {\n ev.preventDefault();\n this.toggle(\"toggle\");\n });\n\n // Remove the transitioning class when the transition ends. We watch the\n // collapse toggle icon because it's guaranteed to transition, whereas the\n // sidebar doesn't animate on mobile (or in browsers where animating\n // grid-template-columns is not supported).\n toggle\n .querySelector(\".collapse-icon\")\n ?.addEventListener(\"transitionend\", () => {\n this._finalizeState();\n $(sidebar).trigger(\"toggleCollapse.sidebarInputBinding\");\n });\n }\n\n /**\n * Initialize nested sidebar counters.\n *\n * @description\n * This function walks up the DOM tree, adding CSS variables to each direct\n * parent sidebar layout that count the layout's position in the stack of\n * nested layouts. We use these counters to keep the collapse toggles from\n * overlapping. Note that always-open sidebars that don't have collapse\n * toggles break the chain of nesting.\n * @private\n */\n private _initSidebarCounters(): void {\n const { container } = this.layout;\n\n const selectorChildLayouts =\n `.${Sidebar.classes.LAYOUT}` +\n \"> .main > \" +\n `.${Sidebar.classes.LAYOUT}:not([data-bslib-sidebar-open=\"always\"])`;\n\n const isInnermostLayout =\n container.querySelector(selectorChildLayouts) === null;\n\n if (!isInnermostLayout) {\n // There are sidebar layouts nested within this layout; defer to children\n return;\n }\n\n function nextSidebarParent(el: HTMLElement | null): HTMLElement | null {\n el = el ? el.parentElement : null;\n if (el && el.classList.contains(\"main\")) {\n // .bslib-sidebar-layout > .main > .bslib-sidebar-layout\n el = el.parentElement;\n }\n if (el && el.classList.contains(Sidebar.classes.LAYOUT)) {\n return el;\n }\n return null;\n }\n\n const layouts = [container];\n let parent = nextSidebarParent(container);\n\n while (parent) {\n // Add parent to front of layouts array, so we sort outer -> inner\n layouts.unshift(parent);\n parent = nextSidebarParent(parent);\n }\n\n const count = { left: 0, right: 0 };\n layouts.forEach(function (x: HTMLElement, i: number): void {\n x.style.setProperty(\"--bslib-sidebar-counter\", i.toString());\n const isRight = x.classList.contains(\"sidebar-right\");\n const thisCount = isRight ? count.right++ : count.left++;\n x.style.setProperty(\n \"--bslib-sidebar-overlap-counter\",\n thisCount.toString()\n );\n });\n }\n\n /**\n * Initialize the sidebar's initial state when `open = \"desktop\"`.\n * @private\n */\n private _initDesktop(): void {\n const { container } = this.layout;\n // If sidebar is marked open='desktop'...\n if (container.dataset.bslibSidebarOpen?.trim() !== \"desktop\") {\n return;\n }\n\n // then close sidebar on mobile\n const initCollapsed = window\n .getComputedStyle(container)\n .getPropertyValue(\"--bslib-sidebar-js-init-collapsed\");\n\n if (initCollapsed.trim() === \"true\") {\n this.toggle(\"close\");\n }\n }\n\n /**\n * Toggle the sidebar's open/closed state.\n * @public\n * @param {SidebarToggleMethod | undefined} method Whether to `\"open\"`,\n * `\"close\"` or `\"toggle\"` the sidebar. If `.toggle()` is called without an\n * argument, it will toggle the sidebar's state.\n */\n public toggle(method: SidebarToggleMethod | undefined): void {\n if (typeof method === \"undefined\") {\n method = \"toggle\";\n }\n\n const { container, main, sidebar } = this.layout;\n const isClosed = this.isClosed;\n\n if ([\"open\", \"close\", \"toggle\"].indexOf(method) === -1) {\n throw new Error(`Unknown method ${method}`);\n }\n\n if (method === \"toggle\") {\n method = isClosed ? \"open\" : \"close\";\n }\n\n if ((isClosed && method === \"close\") || (!isClosed && method === \"open\")) {\n // nothing to do, sidebar is already in the desired state\n return;\n }\n\n // Make sure outputs resize properly when the sidebar is opened/closed\n doWindowResizeOnElementResize(main);\n\n if (method === \"open\") {\n // unhide sidebar immediately when opening,\n // otherwise the sidebar is hidden on transitionend\n sidebar.hidden = false;\n }\n\n // Add a transitioning class just before adding COLLAPSE_CLASS since we want\n // some of the transitioning styles to apply before the collapse state\n container.classList.add(Sidebar.classes.TRANSITIONING);\n container.classList.toggle(Sidebar.classes.COLLAPSE);\n }\n\n /**\n * When the sidebar open/close transition ends, finalize the sidebar's state.\n * @private\n */\n private _finalizeState(): void {\n const { container, sidebar, toggle } = this.layout;\n container.classList.remove(Sidebar.classes.TRANSITIONING);\n sidebar.hidden = this.isClosed;\n toggle.ariaExpanded = this.isClosed ? \"false\" : \"true\";\n }\n}\n\n/**\n * A Shiny input binding for a sidebar.\n * @class SidebarInputBinding\n * @typedef {SidebarInputBinding}\n * @extends {InputBinding}\n */\nclass SidebarInputBinding extends InputBinding {\n find(scope: HTMLElement) {\n return $(scope).find(`.${Sidebar.classes.LAYOUT} > .bslib-sidebar-input`);\n }\n\n getValue(el: HTMLElement): boolean {\n const sb = Sidebar.getInstance(el.parentElement as HTMLElement);\n return sb ? sb.isClosed : false;\n }\n\n setValue(el: HTMLElement, value: boolean): void {\n const method = value ? \"open\" : \"close\";\n this.receiveMessage(el, { method });\n }\n\n subscribe(el: HTMLElement, callback: (x: boolean) => void) {\n $(el).on(\n \"toggleCollapse.sidebarInputBinding\",\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n function (event) {\n callback(true);\n }\n );\n }\n\n unsubscribe(el: HTMLElement) {\n $(el).off(\".sidebarInputBinding\");\n }\n\n receiveMessage(el: HTMLElement, data: SidebarMessageData) {\n const sb = Sidebar.getInstance(el.parentElement as HTMLElement);\n if (sb) sb.toggle(data.method);\n }\n}\n\nregisterBinding(SidebarInputBinding, \"sidebar\");\n\n// attach Sidebar class to window for global usage\n(window as any).bslib = (window as any).bslib || {};\n(window as any).bslib.Sidebar = Sidebar;\n"], - "mappings": "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", - "names": ["InputBinding", "registerBinding", "inputBindingClass", "name", "doWindowResizeOnElementResize", "el", "resizeEvent", "ro", "_Sidebar", "container", "initScript", "el", "initSelector", "_a", "sidebar", "toggle", "ev", "selectorChildLayouts", "nextSidebarParent", "layouts", "parent", "count", "x", "i", "thisCount", "method", "main", "isClosed", "doWindowResizeOnElementResize", "Sidebar", "SidebarInputBinding", "InputBinding", "scope", "sb", "value", "callback", "event", "data", "registerBinding"] + "sources": ["../../srcts/src/components/_utils.ts", "../../srcts/src/components/_shinyResizeObserver.ts", "../../srcts/src/components/sidebar.ts"], + "sourcesContent": ["import type { HtmlDep } from \"rstudio-shiny/srcts/types/src/shiny/render\";\n\nimport type { InputBinding as InputBindingType } from \"rstudio-shiny/srcts/types/src/bindings/input\";\n\n// Exclude undefined from T\ntype NotUndefined = T extends undefined ? never : T;\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst InputBinding = (\n window.Shiny ? Shiny.InputBinding : class {}\n) as typeof InputBindingType;\n\nfunction registerBinding(\n inputBindingClass: new () => InputBindingType,\n name: string\n): void {\n if (window.Shiny) {\n Shiny.inputBindings.register(new inputBindingClass(), \"bslib.\" + name);\n }\n}\n\n// Return true if the key exists on the object and the value is not undefined.\n//\n// This method is mainly used in input bindings' `receiveMessage` method.\n// Since we know that the values are sent by Shiny via `{jsonlite}`,\n// then we know that there are no `undefined` values. `null` is possible, but not `undefined`.\nfunction hasDefinedProperty<\n Prop extends keyof X,\n X extends { [key: string]: any }\n>(\n obj: X,\n prop: Prop\n): obj is X & { [key in NonNullable]: NotUndefined } {\n return (\n Object.prototype.hasOwnProperty.call(obj, prop) && obj[prop] !== undefined\n );\n}\n\n// TODO: Shiny should trigger resize events when the output\n// https://github.com/rstudio/shiny/pull/3682\nfunction doWindowResizeOnElementResize(el: HTMLElement): void {\n if ($(el).data(\"window-resize-observer\")) {\n return;\n }\n const resizeEvent = new Event(\"resize\");\n const ro = new ResizeObserver(() => {\n window.dispatchEvent(resizeEvent);\n });\n ro.observe(el);\n $(el).data(\"window-resize-observer\", ro);\n}\n\nexport {\n InputBinding,\n registerBinding,\n hasDefinedProperty,\n doWindowResizeOnElementResize,\n};\nexport type { HtmlDep };\n", "/**\n * A resize observer that ensures Shiny outputs resize during or just after\n * their parent container size changes. Useful, in particular, for sidebar\n * transitions or for full-screen card transitions.\n *\n * @class ShinyResizeObserver\n * @typedef {ShinyResizeObserver}\n */\nclass ShinyResizeObserver {\n /**\n * The actual ResizeObserver instance.\n * @private\n * @type {ResizeObserver}\n */\n private resizeObserver: ResizeObserver;\n /**\n * An array of elements that are currently being watched by the Resize\n * Observer.\n *\n * @details\n * We don't currently have lifecycle hooks that allow us to unobserve elements\n * when they are removed from the DOM. As a result, we need to manually check\n * that the elements we're watching still exist in the DOM. This array keeps\n * track of the elements we're watching so that we can check them later.\n * @private\n * @type {HTMLElement[]}\n */\n private resizeObserverEntries: HTMLElement[];\n\n /**\n * Watch containers for size changes and ensure that Shiny outputs and\n * htmlwidgets within resize appropriately.\n *\n * @details\n * The ShinyResizeObserver is used to watch the containers, such as Sidebars\n * and Cards for size changes, in particular when the sidebar state is toggled\n * or the card body is expanded full screen. It performs two primary tasks:\n *\n * 1. Dispatches a `resize` event on the window object. This is necessary to\n * ensure that Shiny outputs resize appropriately. In general, the window\n * resizing is throttled and the output update occurs when the transition\n * is complete.\n * 2. If an output with a resize method on the output binding is detected, we\n * directly call the `.onResize()` method of the binding. This ensures that\n * htmlwidgets transition smoothly. In static mode, htmlwidgets does this\n * already.\n *\n * @note\n * This resize observer also handles race conditions in some complex\n * fill-based layouts with multiple outputs (e.g., plotly), where shiny\n * initializes with the correct sizing, but in-between the 1st and last\n * renderValue(), the size of the output containers can change, meaning every\n * output but the 1st gets initialized with the wrong size during their\n * renderValue(). Then, after the render phase, shiny won't know to trigger a\n * resize since all the widgets will return to their original size (and thus,\n * Shiny thinks there isn't any resizing to do). The resize observer works\n * around this by ensuring that the output is resized whenever its container\n * size changes.\n * @constructor\n */\n constructor() {\n this.resizeObserverEntries = [];\n this.resizeObserver = new ResizeObserver((entries) => {\n const resizeEvent = new Event(\"resize\");\n window.dispatchEvent(resizeEvent);\n\n // the rest of this callback is only relevant in Shiny apps\n if (!window.Shiny) return;\n\n const resized = [] as HTMLElement[];\n\n for (const entry of entries) {\n if (!(entry.target instanceof HTMLElement)) continue;\n if (!entry.target.querySelector(\".shiny-bound-output\")) continue;\n\n entry.target\n .querySelectorAll(\".shiny-bound-output\")\n .forEach((el) => {\n if (resized.includes(el)) return;\n\n const { binding, onResize } = $(el).data(\"shinyOutputBinding\");\n if (!binding || !binding.resize) return;\n\n // if this output is owned by another observer, skip it\n const owner = (el as any).shinyResizeObserver;\n if (owner && owner !== this) return;\n // mark this output as owned by this shinyResizeObserver instance\n if (!owner) (el as any).shinyResizeObserver = this;\n\n // trigger immediate resizing of outputs with a resize method\n onResize(el);\n // only once per output and resize event\n resized.push(el);\n\n // set plot images to 100% width temporarily during the transition\n if (!el.classList.contains(\"shiny-plot-output\")) return;\n const img = el.querySelector(\n 'img:not([width=\"100%\"])'\n );\n if (img) img.setAttribute(\"width\", \"100%\");\n });\n }\n });\n }\n\n /**\n * Observe an element for size changes.\n * @param {HTMLElement} el - The element to observe.\n */\n observe(el: HTMLElement): void {\n this.resizeObserver.observe(el);\n this.resizeObserverEntries.push(el);\n }\n\n /**\n * Stop observing an element for size changes.\n * @param {HTMLElement} el - The element to stop observing.\n */\n unobserve(el: HTMLElement): void {\n const idxEl = this.resizeObserverEntries.indexOf(el);\n if (idxEl < 0) return;\n\n this.resizeObserver.unobserve(el);\n this.resizeObserverEntries.splice(idxEl, 1);\n }\n\n /**\n * This method checks that we're not continuing to watch elements that no\n * longer exist in the DOM. If any are found, we stop observing them and\n * remove them from our array of observed elements.\n *\n * @private\n * @static\n */\n flush(): void {\n this.resizeObserverEntries.forEach((el) => {\n if (!document.body.contains(el)) this.unobserve(el);\n });\n }\n}\n\nexport { ShinyResizeObserver };\n", "import { InputBinding, registerBinding } from \"./_utils\";\nimport { ShinyResizeObserver } from \"./_shinyResizeObserver\";\n\n/**\n * Methods for programmatically toggling the state of the sidebar. These methods\n * describe the desired state of the sidebar: `\"close\"` and `\"open\"` transition\n * the sidebar to the desired state, unless the sidebar is already in that\n * state. `\"toggle\"` transitions the sidebar to the state opposite of its\n * current state.\n * @typedef {SidebarToggleMethod}\n */\ntype SidebarToggleMethod = \"close\" | \"open\" | \"toggle\";\n\n/**\n * Data received by the input binding's `receiveMessage` method.\n * @typedef {SidebarMessageData}\n */\ntype SidebarMessageData = {\n method: SidebarToggleMethod;\n};\n\n/**\n * The DOM elements that make up the sidebar. `main`, `sidebar`, and `toggle`\n * are all direct children of `container` (in that order).\n * @interface SidebarComponents\n * @typedef {SidebarComponents}\n */\ninterface SidebarComponents {\n /**\n * The `layout_sidebar()` parent container, with class\n * `Sidebar.classes.LAYOUT`.\n * @type {HTMLElement}\n */\n container: HTMLElement;\n /**\n * The main content area of the sidebar layout.\n * @type {HTMLElement}\n */\n main: HTMLElement;\n /**\n * The sidebar container of the sidebar layout.\n * @type {HTMLElement}\n */\n sidebar: HTMLElement;\n /**\n * The toggle button that is used to toggle the sidebar state.\n * @type {HTMLElement}\n */\n toggle: HTMLElement;\n}\n\n/**\n * The bslib sidebar component class. This class is only used for collapsible\n * sidebars.\n *\n * @class Sidebar\n * @typedef {Sidebar}\n */\nclass Sidebar {\n /**\n * The DOM elements that make up the sidebar, see `SidebarComponents`.\n * @private\n * @type {SidebarComponents}\n */\n private layout: SidebarComponents;\n\n /**\n * A Shiny-specific resize observer that ensures Shiny outputs in the main\n * content areas of the sidebar resize appropriately.\n * @private\n * @type {ShinyResizeObserver}\n * @static\n */\n private static shinyResizeObserver = new ShinyResizeObserver();\n\n /**\n * Creates an instance of a collapsible bslib Sidebar.\n * @constructor\n * @param {HTMLElement} container\n */\n constructor(container: HTMLElement) {\n Sidebar.instanceMap.set(container, this);\n this.layout = {\n container,\n main: container.querySelector(\":scope > .main\") as HTMLElement,\n sidebar: container.querySelector(\":scope > .sidebar\") as HTMLElement,\n toggle: container.querySelector(\n \":scope > .collapse-toggle\"\n ) as HTMLElement,\n } as SidebarComponents;\n\n if (!this.layout.toggle) {\n throw new Error(\"Tried to initialize a non-collapsible sidebar.\");\n }\n\n this._initEventListeners();\n this._initSidebarCounters();\n this._initDesktop();\n\n // Start watching the main content area for size changes to ensure Shiny\n // outputs resize appropriately during sidebar transitions.\n Sidebar.shinyResizeObserver.observe(this.layout.main);\n\n container.removeAttribute(\"data-bslib-sidebar-init\");\n const initScript = container.querySelector(\n \":scope > script[data-bslib-sidebar-init]\"\n );\n if (initScript) {\n container.removeChild(initScript);\n }\n }\n\n /**\n * Read the current state of the sidebar. Note that, when calling this method,\n * the sidebar may be transitioning into the state returned by this method.\n *\n * @description\n * The sidebar state works as follows, starting from the open state. When the\n * sidebar is closed:\n * 1. We add both the `COLLAPSE` and `TRANSITIONING` classes to the sidebar.\n * 2. The sidebar collapse begins to animate. On desktop devices, and where it\n * is supported, we transition the `grid-template-columns` property of the\n * sidebar layout. On mobile, the sidebar is hidden immediately. In both\n * cases, the collapse icon rotates and we use this rotation to determine\n * when the transition is complete.\n * 3. If another sidebar state toggle is requested while closing the sidebar,\n * we remove the `COLLAPSE` class and the animation immediately starts to\n * reverse.\n * 4. When the `transition` is complete, we remove the `TRANSITIONING` class.\n * @readonly\n * @type {boolean}\n */\n get isClosed(): boolean {\n return this.layout.container.classList.contains(Sidebar.classes.COLLAPSE);\n }\n\n /**\n * Static classes related to the sidebar layout or state.\n * @public\n * @static\n * @readonly\n * @type {{ LAYOUT: string; COLLAPSE: string; TRANSITIONING: string; }}\n */\n public static readonly classes = {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n LAYOUT: \"bslib-sidebar-layout\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n COLLAPSE: \"sidebar-collapsed\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n TRANSITIONING: \"transitioning\",\n };\n\n /**\n * If sidebars are initialized before the DOM is ready, we re-schedule the\n * initialization to occur on DOMContentLoaded.\n * @private\n * @static\n * @type {boolean}\n */\n private static onReadyScheduled = false;\n /**\n * A map of initialized sidebars to their respective Sidebar instances.\n * @private\n * @static\n * @type {WeakMap}\n */\n private static instanceMap: WeakMap = new WeakMap();\n\n /**\n * Given a sidebar container, return the Sidebar instance associated with it.\n * @public\n * @static\n * @param {HTMLElement} el\n * @returns {(Sidebar | undefined)}\n */\n public static getInstance(el: HTMLElement): Sidebar | undefined {\n return Sidebar.instanceMap.get(el);\n }\n\n /**\n * Initialize all collapsible sidebars on the page.\n * @public\n * @static\n * @param {boolean} [flushResizeObserver=true] When `true`, we remove\n * non-existent elements from the ResizeObserver. This is required\n * periodically to prevent memory leaks. To avoid over-checking, we only flush\n * the ResizeObserver when initializing sidebars after page load.\n */\n public static initCollapsibleAll(flushResizeObserver = true): void {\n if (document.readyState === \"loading\") {\n if (!Sidebar.onReadyScheduled) {\n Sidebar.onReadyScheduled = true;\n document.addEventListener(\"DOMContentLoaded\", () => {\n Sidebar.initCollapsibleAll(false);\n });\n }\n return;\n }\n\n const initSelector = `.${Sidebar.classes.LAYOUT}[data-bslib-sidebar-init]`;\n if (!document.querySelector(initSelector)) {\n // no sidebars to initialize\n return;\n }\n\n if (flushResizeObserver) Sidebar.shinyResizeObserver.flush();\n\n const containers = document.querySelectorAll(initSelector);\n containers.forEach((container) => new Sidebar(container as HTMLElement));\n }\n\n /**\n * Initialize event listeners for the sidebar toggle button.\n * @private\n */\n private _initEventListeners(): void {\n const { sidebar, toggle } = this.layout;\n\n toggle.addEventListener(\"click\", (ev) => {\n ev.preventDefault();\n this.toggle(\"toggle\");\n });\n\n // Remove the transitioning class when the transition ends. We watch the\n // collapse toggle icon because it's guaranteed to transition, whereas the\n // sidebar doesn't animate on mobile (or in browsers where animating\n // grid-template-columns is not supported).\n toggle\n .querySelector(\".collapse-icon\")\n ?.addEventListener(\"transitionend\", () => {\n this._finalizeState();\n $(sidebar).trigger(\"toggleCollapse.sidebarInputBinding\");\n });\n }\n\n /**\n * Initialize nested sidebar counters.\n *\n * @description\n * This function walks up the DOM tree, adding CSS variables to each direct\n * parent sidebar layout that count the layout's position in the stack of\n * nested layouts. We use these counters to keep the collapse toggles from\n * overlapping. Note that always-open sidebars that don't have collapse\n * toggles break the chain of nesting.\n * @private\n */\n private _initSidebarCounters(): void {\n const { container } = this.layout;\n\n const selectorChildLayouts =\n `.${Sidebar.classes.LAYOUT}` +\n \"> .main > \" +\n `.${Sidebar.classes.LAYOUT}:not([data-bslib-sidebar-open=\"always\"])`;\n\n const isInnermostLayout =\n container.querySelector(selectorChildLayouts) === null;\n\n if (!isInnermostLayout) {\n // There are sidebar layouts nested within this layout; defer to children\n return;\n }\n\n function nextSidebarParent(el: HTMLElement | null): HTMLElement | null {\n el = el ? el.parentElement : null;\n if (el && el.classList.contains(\"main\")) {\n // .bslib-sidebar-layout > .main > .bslib-sidebar-layout\n el = el.parentElement;\n }\n if (el && el.classList.contains(Sidebar.classes.LAYOUT)) {\n return el;\n }\n return null;\n }\n\n const layouts = [container];\n let parent = nextSidebarParent(container);\n\n while (parent) {\n // Add parent to front of layouts array, so we sort outer -> inner\n layouts.unshift(parent);\n parent = nextSidebarParent(parent);\n }\n\n const count = { left: 0, right: 0 };\n layouts.forEach(function (x: HTMLElement, i: number): void {\n x.style.setProperty(\"--bslib-sidebar-counter\", i.toString());\n const isRight = x.classList.contains(\"sidebar-right\");\n const thisCount = isRight ? count.right++ : count.left++;\n x.style.setProperty(\n \"--bslib-sidebar-overlap-counter\",\n thisCount.toString()\n );\n });\n }\n\n /**\n * Initialize the sidebar's initial state when `open = \"desktop\"`.\n * @private\n */\n private _initDesktop(): void {\n const { container } = this.layout;\n // If sidebar is marked open='desktop'...\n if (container.dataset.bslibSidebarOpen?.trim() !== \"desktop\") {\n return;\n }\n\n // then close sidebar on mobile\n const initCollapsed = window\n .getComputedStyle(container)\n .getPropertyValue(\"--bslib-sidebar-js-init-collapsed\");\n\n if (initCollapsed.trim() === \"true\") {\n this.toggle(\"close\");\n }\n }\n\n /**\n * Toggle the sidebar's open/closed state.\n * @public\n * @param {SidebarToggleMethod | undefined} method Whether to `\"open\"`,\n * `\"close\"` or `\"toggle\"` the sidebar. If `.toggle()` is called without an\n * argument, it will toggle the sidebar's state.\n */\n public toggle(method: SidebarToggleMethod | undefined): void {\n if (typeof method === \"undefined\") {\n method = \"toggle\";\n }\n\n const { container, sidebar } = this.layout;\n const isClosed = this.isClosed;\n\n if ([\"open\", \"close\", \"toggle\"].indexOf(method) === -1) {\n throw new Error(`Unknown method ${method}`);\n }\n\n if (method === \"toggle\") {\n method = isClosed ? \"open\" : \"close\";\n }\n\n if ((isClosed && method === \"close\") || (!isClosed && method === \"open\")) {\n // nothing to do, sidebar is already in the desired state\n return;\n }\n\n if (method === \"open\") {\n // unhide sidebar immediately when opening,\n // otherwise the sidebar is hidden on transitionend\n sidebar.hidden = false;\n }\n\n // Add a transitioning class just before adding COLLAPSE_CLASS since we want\n // some of the transitioning styles to apply before the collapse state\n container.classList.add(Sidebar.classes.TRANSITIONING);\n container.classList.toggle(Sidebar.classes.COLLAPSE);\n }\n\n /**\n * When the sidebar open/close transition ends, finalize the sidebar's state.\n * @private\n */\n private _finalizeState(): void {\n const { container, sidebar, toggle } = this.layout;\n container.classList.remove(Sidebar.classes.TRANSITIONING);\n sidebar.hidden = this.isClosed;\n toggle.ariaExpanded = this.isClosed ? \"false\" : \"true\";\n }\n}\n\n/**\n * A Shiny input binding for a sidebar.\n * @class SidebarInputBinding\n * @typedef {SidebarInputBinding}\n * @extends {InputBinding}\n */\nclass SidebarInputBinding extends InputBinding {\n find(scope: HTMLElement) {\n return $(scope).find(`.${Sidebar.classes.LAYOUT} > .bslib-sidebar-input`);\n }\n\n getValue(el: HTMLElement): boolean {\n const sb = Sidebar.getInstance(el.parentElement as HTMLElement);\n return sb ? sb.isClosed : false;\n }\n\n setValue(el: HTMLElement, value: boolean): void {\n const method = value ? \"open\" : \"close\";\n this.receiveMessage(el, { method });\n }\n\n subscribe(el: HTMLElement, callback: (x: boolean) => void) {\n $(el).on(\n \"toggleCollapse.sidebarInputBinding\",\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n function (event) {\n callback(true);\n }\n );\n }\n\n unsubscribe(el: HTMLElement) {\n $(el).off(\".sidebarInputBinding\");\n }\n\n receiveMessage(el: HTMLElement, data: SidebarMessageData) {\n const sb = Sidebar.getInstance(el.parentElement as HTMLElement);\n if (sb) sb.toggle(data.method);\n }\n}\n\nregisterBinding(SidebarInputBinding, \"sidebar\");\n\n// attach Sidebar class to window for global usage\n(window as any).bslib = (window as any).bslib || {};\n(window as any).bslib.Sidebar = Sidebar;\n"], + "mappings": "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", + "names": ["InputBinding", "registerBinding", "inputBindingClass", "name", "ShinyResizeObserver", "entries", "resizeEvent", "resized", "entry", "el", "binding", "onResize", "owner", "img", "idxEl", "_Sidebar", "container", "initScript", "el", "flushResizeObserver", "initSelector", "_a", "sidebar", "toggle", "ev", "selectorChildLayouts", "nextSidebarParent", "layouts", "parent", "count", "x", "i", "thisCount", "method", "isClosed", "Sidebar", "ShinyResizeObserver", "SidebarInputBinding", "InputBinding", "scope", "sb", "value", "callback", "event", "data", "registerBinding"] } diff --git a/srcts/src/components/_shinyResizeObserver.ts b/srcts/src/components/_shinyResizeObserver.ts new file mode 100644 index 000000000..ec40e2f0d --- /dev/null +++ b/srcts/src/components/_shinyResizeObserver.ts @@ -0,0 +1,142 @@ +/** + * A resize observer that ensures Shiny outputs resize during or just after + * their parent container size changes. Useful, in particular, for sidebar + * transitions or for full-screen card transitions. + * + * @class ShinyResizeObserver + * @typedef {ShinyResizeObserver} + */ +class ShinyResizeObserver { + /** + * The actual ResizeObserver instance. + * @private + * @type {ResizeObserver} + */ + private resizeObserver: ResizeObserver; + /** + * An array of elements that are currently being watched by the Resize + * Observer. + * + * @details + * We don't currently have lifecycle hooks that allow us to unobserve elements + * when they are removed from the DOM. As a result, we need to manually check + * that the elements we're watching still exist in the DOM. This array keeps + * track of the elements we're watching so that we can check them later. + * @private + * @type {HTMLElement[]} + */ + private resizeObserverEntries: HTMLElement[]; + + /** + * Watch containers for size changes and ensure that Shiny outputs and + * htmlwidgets within resize appropriately. + * + * @details + * The ShinyResizeObserver is used to watch the containers, such as Sidebars + * and Cards for size changes, in particular when the sidebar state is toggled + * or the card body is expanded full screen. It performs two primary tasks: + * + * 1. Dispatches a `resize` event on the window object. This is necessary to + * ensure that Shiny outputs resize appropriately. In general, the window + * resizing is throttled and the output update occurs when the transition + * is complete. + * 2. If an output with a resize method on the output binding is detected, we + * directly call the `.onResize()` method of the binding. This ensures that + * htmlwidgets transition smoothly. In static mode, htmlwidgets does this + * already. + * + * @note + * This resize observer also handles race conditions in some complex + * fill-based layouts with multiple outputs (e.g., plotly), where shiny + * initializes with the correct sizing, but in-between the 1st and last + * renderValue(), the size of the output containers can change, meaning every + * output but the 1st gets initialized with the wrong size during their + * renderValue(). Then, after the render phase, shiny won't know to trigger a + * resize since all the widgets will return to their original size (and thus, + * Shiny thinks there isn't any resizing to do). The resize observer works + * around this by ensuring that the output is resized whenever its container + * size changes. + * @constructor + */ + constructor() { + this.resizeObserverEntries = []; + this.resizeObserver = new ResizeObserver((entries) => { + const resizeEvent = new Event("resize"); + window.dispatchEvent(resizeEvent); + + // the rest of this callback is only relevant in Shiny apps + if (!window.Shiny) return; + + const resized = [] as HTMLElement[]; + + for (const entry of entries) { + if (!(entry.target instanceof HTMLElement)) continue; + if (!entry.target.querySelector(".shiny-bound-output")) continue; + + entry.target + .querySelectorAll(".shiny-bound-output") + .forEach((el) => { + if (resized.includes(el)) return; + + const { binding, onResize } = $(el).data("shinyOutputBinding"); + if (!binding || !binding.resize) return; + + // if this output is owned by another observer, skip it + const owner = (el as any).shinyResizeObserver; + if (owner && owner !== this) return; + // mark this output as owned by this shinyResizeObserver instance + if (!owner) (el as any).shinyResizeObserver = this; + + // trigger immediate resizing of outputs with a resize method + onResize(el); + // only once per output and resize event + resized.push(el); + + // set plot images to 100% width temporarily during the transition + if (!el.classList.contains("shiny-plot-output")) return; + const img = el.querySelector( + 'img:not([width="100%"])' + ); + if (img) img.setAttribute("width", "100%"); + }); + } + }); + } + + /** + * Observe an element for size changes. + * @param {HTMLElement} el - The element to observe. + */ + observe(el: HTMLElement): void { + this.resizeObserver.observe(el); + this.resizeObserverEntries.push(el); + } + + /** + * Stop observing an element for size changes. + * @param {HTMLElement} el - The element to stop observing. + */ + unobserve(el: HTMLElement): void { + const idxEl = this.resizeObserverEntries.indexOf(el); + if (idxEl < 0) return; + + this.resizeObserver.unobserve(el); + this.resizeObserverEntries.splice(idxEl, 1); + } + + /** + * This method checks that we're not continuing to watch elements that no + * longer exist in the DOM. If any are found, we stop observing them and + * remove them from our array of observed elements. + * + * @private + * @static + */ + flush(): void { + this.resizeObserverEntries.forEach((el) => { + if (!document.body.contains(el)) this.unobserve(el); + }); + } +} + +export { ShinyResizeObserver }; diff --git a/srcts/src/components/sidebar.ts b/srcts/src/components/sidebar.ts index d06eebaa6..4c0638e91 100644 --- a/srcts/src/components/sidebar.ts +++ b/srcts/src/components/sidebar.ts @@ -1,8 +1,5 @@ -import { - InputBinding, - registerBinding, - doWindowResizeOnElementResize, -} from "./_utils"; +import { InputBinding, registerBinding } from "./_utils"; +import { ShinyResizeObserver } from "./_shinyResizeObserver"; /** * Methods for programmatically toggling the state of the sidebar. These methods @@ -66,6 +63,16 @@ class Sidebar { * @type {SidebarComponents} */ private layout: SidebarComponents; + + /** + * A Shiny-specific resize observer that ensures Shiny outputs in the main + * content areas of the sidebar resize appropriately. + * @private + * @type {ShinyResizeObserver} + * @static + */ + private static shinyResizeObserver = new ShinyResizeObserver(); + /** * Creates an instance of a collapsible bslib Sidebar. * @constructor @@ -90,6 +97,10 @@ class Sidebar { this._initSidebarCounters(); this._initDesktop(); + // Start watching the main content area for size changes to ensure Shiny + // outputs resize appropriately during sidebar transitions. + Sidebar.shinyResizeObserver.observe(this.layout.main); + container.removeAttribute("data-bslib-sidebar-init"); const initScript = container.querySelector( ":scope > script[data-bslib-sidebar-init]" @@ -170,13 +181,17 @@ class Sidebar { * Initialize all collapsible sidebars on the page. * @public * @static + * @param {boolean} [flushResizeObserver=true] When `true`, we remove + * non-existent elements from the ResizeObserver. This is required + * periodically to prevent memory leaks. To avoid over-checking, we only flush + * the ResizeObserver when initializing sidebars after page load. */ - public static initCollapsibleAll(): void { + public static initCollapsibleAll(flushResizeObserver = true): void { if (document.readyState === "loading") { if (!Sidebar.onReadyScheduled) { Sidebar.onReadyScheduled = true; document.addEventListener("DOMContentLoaded", () => { - Sidebar.initCollapsibleAll(); + Sidebar.initCollapsibleAll(false); }); } return; @@ -188,6 +203,8 @@ class Sidebar { return; } + if (flushResizeObserver) Sidebar.shinyResizeObserver.flush(); + const containers = document.querySelectorAll(initSelector); containers.forEach((container) => new Sidebar(container as HTMLElement)); } @@ -309,7 +326,7 @@ class Sidebar { method = "toggle"; } - const { container, main, sidebar } = this.layout; + const { container, sidebar } = this.layout; const isClosed = this.isClosed; if (["open", "close", "toggle"].indexOf(method) === -1) { @@ -325,9 +342,6 @@ class Sidebar { return; } - // Make sure outputs resize properly when the sidebar is opened/closed - doWindowResizeOnElementResize(main); - if (method === "open") { // unhide sidebar immediately when opening, // otherwise the sidebar is hidden on transitionend