gradio/frontend
1413k
1<script lang="ts">2 import { onMount, tick, createEventDispatcher } from "svelte";3 4 export let items: any[][] = [];5 6 export let max_height: number;7 export let actual_height: number;8 export let table_scrollbar_width: number;9 export let start = 0;10 export let end = 20;11 export let selected: number | false;12 export let disable_scroll = false;13 export let show_scroll_button = false;14 export let viewport: HTMLTableElement;15 export let label: string | null = null;16 17 const dispatch = createEventDispatcher<{18 scroll_top: number;19 }>();20 21 let height = "100%";22 23 let average_height = 30;24 let bottom = 0;25 let contents: HTMLTableSectionElement;26 let head_height = 0;27 let foot_height = 0;28 let height_map: number[] = [];29 let mounted: boolean;30 let rows: HTMLCollectionOf<HTMLTableRowElement>;31 let top = 0;32 let viewport_height = 200;33 let visible: { index: number; data: any[] }[] = [];34 let viewport_box: DOMRectReadOnly;35 36 $: viewport_height = viewport_box?.height || 200;37 38 const is_browser = typeof window !== "undefined";39 const raf = is_browser40 ? window.requestAnimationFrame41 : (cb: (...args: any[]) => void) => cb();42 43 $: {44 if (mounted && viewport_height && viewport.offsetParent) {45 (sortedItems, raf(refresh_height_map));46 }47 }48 49 async function refresh_height_map(): Promise<void> {50 if (!viewport) {51 return;52 }53 54 if (sortedItems.length < start) {55 await scroll_to_index(sortedItems.length - 1, { behavior: "auto" });56 }57 58 const scrollTop = Math.max(0, viewport.scrollTop);59 show_scroll_button = scrollTop > 100;60 table_scrollbar_width = viewport.offsetWidth - viewport.clientWidth;61 62 // acquire height map for currently visible rows63 for (let v = 0; v < rows.length; v += 1) {64 height_map[start + v] = rows[v].getBoundingClientRect().height;65 }66 let i = 0;67 let y = head_height;68 // loop items to find new start69 while (i < sortedItems.length) {70 const row_height = height_map[i] || average_height;71 // keep a page of rows buffered above72 if (y + row_height > scrollTop - max_height) {73 start = i;74 top = y - head_height;75 break;76 }77 y += row_height;78 i += 1;79 }80 81 let content_height = head_height;82 while (i < sortedItems.length) {83 const row_height = height_map[i] || average_height;84 content_height += row_height;85 i += 1;86 // keep a page of rows buffered below87 if (content_height - head_height > 3 * max_height) {88 break;89 }90 }91 92 end = i;93 const remaining = sortedItems.length - end;94 95 const scrollbar_height = viewport.offsetHeight - viewport.clientHeight;96 if (scrollbar_height > 0) {97 content_height += scrollbar_height;98 }99 100 let filtered_height_map = height_map.filter((v) => typeof v === "number");101 average_height =102 filtered_height_map.reduce((a, b) => a + b, 0) /103 filtered_height_map.length || 30;104 105 bottom = remaining * average_height;106 if (!isFinite(bottom)) {107 bottom = 200000;108 }109 height_map.length = sortedItems.length;110 while (i < sortedItems.length) {111 i += 1;112 height_map[i] = average_height;113 }114 if (max_height && content_height > max_height) {115 actual_height = max_height;116 } else {117 actual_height = content_height;118 }119 }120 121 $: scroll_and_render(selected);122 123 async function scroll_and_render(n: number | false): Promise<void> {124 raf(async () => {125 if (typeof n !== "number") return;126 const direction = typeof n !== "number" ? false : is_in_view(n);127 if (direction === true) {128 return;129 }130 if (direction === "back") {131 await scroll_to_index(n, { behavior: "instant" });132 }133 134 if (direction === "forwards") {135 await scroll_to_index(n, { behavior: "instant" }, true);136 }137 });138 }139 140 function is_in_view(n: number): "back" | "forwards" | true {141 const current = rows && rows[n - start];142 if (!current && n < start) {143 return "back";144 }145 if (!current && n >= end - 1) {146 return "forwards";147 }148 149 const { top: viewport_top } = viewport.getBoundingClientRect();150 const { top, bottom } = current.getBoundingClientRect();151 152 if (top - viewport_top < 37) {153 return "back";154 }155 156 if (bottom - viewport_top > viewport_height) {157 return "forwards";158 }159 160 return true;161 }162 163 export async function scroll_to_index(164 index: number,165 opts: ScrollToOptions,166 align_end = false167 ): Promise<void> {168 await tick();169 170 const _itemHeight = average_height;171 172 let distance = index * _itemHeight;173 if (align_end) {174 distance = distance - viewport_height + _itemHeight + head_height;175 }176 177 const scrollbar_height = viewport.offsetHeight - viewport.clientHeight;178 if (scrollbar_height > 0) {179 distance += scrollbar_height;180 }181 182 const _opts = {183 top: distance,184 behavior: "smooth" as ScrollBehavior,185 ...opts186 };187 188 viewport.scrollTo(_opts);189 }190 191 $: sortedItems = items;192 193 $: visible = is_browser194 ? sortedItems.slice(start, end).map((data, i) => {195 return { index: i + start, data };196 })197 : sortedItems198 .slice(0, (max_height / sortedItems.length) * average_height + 1)199 .map((data, i) => {200 return { index: i + start, data };201 });202 203 onMount(() => {204 rows = contents.children as HTMLCollectionOf<HTMLTableRowElement>;205 mounted = true;206 });207</script>208 209<svelte-virtual-table-viewport>210 <div>211 <table212 class="table"213 class:disable-scroll={disable_scroll}214 bind:this={viewport}215 bind:contentRect={viewport_box}216 on:scroll={refresh_height_map}217 style="height: {height}; --bw-svt-p-top: {top}px; --bw-svt-p-bottom: {bottom}px; --bw-svt-head-height: {head_height}px; --bw-svt-foot-height: {foot_height}px; --bw-svt-avg-row-height: {average_height}px; --max-height: {max_height}px"218 >219 {#if label && label.length !== 0}220 <caption class="sr-only">{label}</caption>221 {/if}222 <thead class="thead" bind:offsetHeight={head_height}>223 <slot name="thead" />224 </thead>225 <tbody bind:this={contents} class="tbody">226 {#if visible.length && visible[0].data.length}227 {#each visible as item (item.data[0].id)}228 <slot name="tbody" item={item.data} index={item.index}>229 <tr>230 <td>Missing Table Row</td>231 </tr>232 </slot>233 {/each}234 {/if}235 </tbody>236 <tfoot class="tfoot" bind:offsetHeight={foot_height}>237 <slot name="tfoot" />238 </tfoot>239 </table>240 </div>241</svelte-virtual-table-viewport>242 243<style type="text/css">244 table {245 position: relative;246 overflow: auto;247 -webkit-overflow-scrolling: touch;248 max-height: var(--max-height);249 box-sizing: border-box;250 display: block;251 padding: 0;252 margin: 0;253 color: var(--body-text-color);254 font-size: var(--input-text-size);255 line-height: var(--line-md);256 font-family: var(--font-mono);257 border-spacing: 0;258 width: 100%;259 scroll-snap-type: x proximity;260 border-collapse: separate;261 scrollbar-width: thin;262 scrollbar-color: rgba(128, 128, 128, 0.5) transparent;263 }264 265 table::-webkit-scrollbar {266 width: 4px;267 height: 4px;268 }269 270 table::-webkit-scrollbar-track {271 background: transparent;272 }273 274 table::-webkit-scrollbar-thumb {275 background-color: rgba(128, 128, 128, 0.5);276 border-radius: 4px;277 }278 279 table:hover {280 scrollbar-color: rgba(160, 160, 160, 0.7) transparent;281 }282 283 table:hover::-webkit-scrollbar-thumb {284 background-color: rgba(160, 160, 160, 0.7);285 border-radius: 4px;286 width: 4px;287 }288 289 @media (hover: none) {290 table {291 scrollbar-color: rgba(160, 160, 160, 0.7) transparent;292 }293 294 table::-webkit-scrollbar-thumb {295 background-color: rgba(160, 160, 160, 0.7);296 border-radius: 4px;297 }298 }299 300 @media (pointer: coarse) {301 table::-webkit-scrollbar {302 width: 8px;303 height: 8px;304 }305 }306 307 table :is(thead, tfoot, tbody) {308 display: table;309 table-layout: fixed;310 width: 100%;311 box-sizing: border-box;312 }313 314 tbody {315 overflow-x: scroll;316 overflow-y: hidden;317 }318 319 table tbody {320 padding-top: var(--bw-svt-p-top);321 padding-bottom: var(--bw-svt-p-bottom);322 }323 tbody {324 position: relative;325 box-sizing: border-box;326 border: 0px solid currentColor;327 }328 329 tbody > :global(tr:last-child) {330 border: none;331 }332 333 table :global(td) {334 scroll-snap-align: start;335 }336 337 tbody :global(td.pinned-column) {338 position: sticky;339 z-index: 3;340 }341 342 tbody :global(tr:nth-child(odd)) :global(td.pinned-column) {343 background: var(--table-odd-background-fill);344 }345 346 tbody :global(tr:nth-child(even)) :global(td.pinned-column) {347 background: var(--table-even-background-fill);348 }349 350 tbody :global(td.last-pinned) {351 border-right: 1px solid var(--border-color-primary);352 }353 354 thead {355 position: sticky;356 top: 0;357 left: 0;358 background: var(--background-fill-primary);359 z-index: 7;360 }361 362 thead :global(th) {363 background: var(--table-even-background-fill) !important;364 }365 366 thead :global(th.pinned-column) {367 position: sticky;368 z-index: 7;369 background: var(--table-even-background-fill) !important;370 }371 372 thead :global(th.last-pinned) {373 border-right: 1px solid var(--border-color-primary);374 }375 376 .table.disable-scroll {377 overflow: hidden !important;378 }379</style>380 