gradio/frontend
1413k
1import { getContext, setContext } from "svelte";2import { dequal } from "dequal";3import { writable, get } from "svelte/store";4import { sort_table_data } from "../utils/table_utils";5import type { CellValue } from "../types";6import { tick } from "svelte";7import {8 handle_selection,9 get_next_cell_coordinates,10 get_range_selection,11 move_cursor12} from "../utils/selection_utils";13 14export const DATAFRAME_KEY = Symbol("dataframe");15 16export type SortDirection = "asc" | "desc";17export type FilterDatatype = "string" | "number";18export type CellCoordinate = [number, number];19 20interface DataFrameState {21 config: {22 show_fullscreen_button: boolean;23 show_copy_button: boolean;24 show_search: "none" | "search" | "filter";25 show_row_numbers: boolean;26 editable: boolean;27 pinned_columns: number;28 show_label: boolean;29 line_breaks: boolean;30 wrap: boolean;31 max_height: number;32 column_widths: string[];33 max_chars?: number;34 static_columns?: CellValue[];35 };36 current_search_query: string | null;37 sort_state: {38 sort_columns: { col: number; direction: SortDirection }[];39 row_order: number[];40 initial_data: {41 data: { id: string; value: CellValue }[][];42 display_value: string[][] | null;43 styling: string[][] | null;44 } | null;45 };46 filter_state: {47 filter_columns: {48 col: number;49 datatype: FilterDatatype;50 filter: string;51 value: string;52 }[];53 initial_data: {54 data: { id: string; value: CellValue }[][];55 display_value: string[][] | null;56 styling: string[][] | null;57 } | null;58 };59 ui_state: {60 active_cell_menu: { row: number; col: number; x: number; y: number } | null;61 active_header_menu: { col: number; x: number; y: number } | null;62 selected_cells: CellCoordinate[];63 selected: CellCoordinate | false;64 editing: CellCoordinate | false;65 header_edit: number | false;66 selected_header: number | false;67 active_button: {68 type: "header" | "cell";69 row?: number;70 col: number;71 } | null;72 copy_flash: boolean;73 };74}75 76interface DataFrameActions {77 handle_search: (query: string | null) => void;78 handle_sort: (col: number, direction: SortDirection) => void;79 handle_filter: (80 col: number,81 datatype: FilterDatatype,82 filter: string,83 value: string84 ) => void;85 get_sort_status: (name: string, headers: string[]) => "none" | "asc" | "desc";86 sort_data: (87 data: any[][],88 display_value: string[][] | null,89 styling: string[][] | null90 ) => void;91 update_row_order: (data: any[][]) => void;92 filter_data: (data: any[][]) => any[][];93 add_row: (data: any[][], make_id: () => string, index?: number) => any[][];94 add_col: (95 data: any[][],96 headers: string[],97 make_id: () => string,98 index?: number99 ) => { data: any[][]; headers: string[] };100 add_row_at: (101 data: any[][],102 index: number,103 position: "above" | "below",104 make_id: () => string105 ) => any[][];106 add_col_at: (107 data: any[][],108 headers: string[],109 index: number,110 position: "left" | "right",111 make_id: () => string112 ) => { data: any[][]; headers: string[] };113 delete_row: (data: any[][], index: number) => any[][];114 delete_col: (115 data: any[][],116 headers: string[],117 index: number118 ) => { data: any[][]; headers: string[] };119 delete_row_at: (data: any[][], index: number) => any[][];120 delete_col_at: (121 data: any[][],122 headers: string[],123 index: number124 ) => { data: any[][]; headers: string[] };125 trigger_change: (126 data: any[][],127 headers: any[],128 previous_data: any[][],129 previous_headers: string[],130 value_is_output: boolean,131 dispatch: (e: "change" | "input" | "edit", detail?: any) => void132 ) => Promise<void>;133 reset_sort_state: () => void;134 reset_filter_state: () => void;135 set_active_cell_menu: (136 menu: { row: number; col: number; x: number; y: number } | null137 ) => void;138 set_active_header_menu: (139 menu: { col: number; x: number; y: number } | null140 ) => void;141 set_selected_cells: (cells: CellCoordinate[]) => void;142 set_selected: (selected: CellCoordinate | false) => void;143 set_editing: (editing: CellCoordinate | false) => void;144 clear_ui_state: () => void;145 set_header_edit: (header_index: number | false) => void;146 set_selected_header: (header_index: number | false) => void;147 handle_header_click: (col: number, editable: boolean) => void;148 end_header_edit: (key: string) => void;149 get_selected_cells: () => CellCoordinate[];150 get_active_cell_menu: () => {151 row: number;152 col: number;153 x: number;154 y: number;155 } | null;156 get_active_button: () => {157 type: "header" | "cell";158 row?: number;159 col: number;160 } | null;161 set_active_button: (162 button: { type: "header" | "cell"; row?: number; col: number } | null163 ) => void;164 set_copy_flash: (value: boolean) => void;165 handle_cell_click: (event: MouseEvent, row: number, col: number) => void;166 toggle_cell_menu: (event: MouseEvent, row: number, col: number) => void;167 toggle_cell_button: (row: number, col: number) => void;168 handle_select_column: (col: number) => void;169 handle_select_row: (row: number) => void;170 get_next_cell_coordinates: typeof get_next_cell_coordinates;171 get_range_selection: typeof get_range_selection;172 move_cursor: typeof move_cursor;173}174 175export interface DataFrameContext {176 state: ReturnType<typeof writable<DataFrameState>>;177 actions: DataFrameActions;178 data?: any[][];179 headers?: { id: string; value: string }[];180 display_value?: string[][] | null;181 styling?: string[][] | null;182 els?: Record<183 string,184 { cell: HTMLTableCellElement | null; input: HTMLTextAreaElement | null }185 >;186 parent_element?: HTMLElement;187 get_data_at?: (row: number, col: number) => CellValue;188 get_column?: (col: number) => CellValue[];189 get_row?: (row: number) => CellValue[];190 dispatch?: (e: "change" | "select" | "search" | "edit", detail?: any) => void;191}192 193function create_actions(194 state: ReturnType<typeof writable<DataFrameState>>,195 context: DataFrameContext196): DataFrameActions {197 const update_state = (198 updater: (s: DataFrameState) => Partial<DataFrameState>199 ): void => state.update((s) => ({ ...s, ...updater(s) }));200 201 const add_row = (202 data: any[][],203 make_id: () => string,204 index?: number205 ): any[][] => {206 const new_row = data[0]?.length207 ? Array(data[0].length)208 .fill(null)209 .map(() => ({ value: "", id: make_id() }))210 : [{ value: "", id: make_id() }];211 const new_data = [...data];212 index !== undefined213 ? new_data.splice(index, 0, new_row)214 : new_data.push(new_row);215 return new_data;216 };217 218 const add_col = (219 data: any[][],220 headers: string[],221 make_id: () => string,222 index?: number223 ): { data: any[][]; headers: string[] } => {224 const new_headers = [...headers, `Header ${headers.length + 1}`];225 const new_data = data.map((row) => [...row, { value: "", id: make_id() }]);226 if (index !== undefined) {227 new_headers.splice(index, 0, new_headers.pop()!);228 new_data.forEach((row) => row.splice(index, 0, row.pop()!));229 }230 return { data: new_data, headers: new_headers };231 };232 233 const update_array = (234 source: { id: string; value: CellValue }[][] | string[][] | null,235 target: any[] | null | undefined236 ): void => {237 if (source && target) {238 target.splice(0, target.length, ...JSON.parse(JSON.stringify(source)));239 }240 };241 242 return {243 handle_search: (query) =>244 update_state((s) => ({ current_search_query: query })),245 handle_sort: (col, direction) =>246 update_state((s) => {247 const sort_cols = s.sort_state.sort_columns.filter(248 (c) => c.col !== col249 );250 if (251 !s.sort_state.sort_columns.some(252 (c) => c.col === col && c.direction === direction253 )254 ) {255 sort_cols.push({ col, direction });256 }257 258 const initial_data =259 s.sort_state.initial_data ||260 (context.data && sort_cols.length > 0261 ? {262 data: JSON.parse(JSON.stringify(context.data)),263 display_value: context.display_value264 ? JSON.parse(JSON.stringify(context.display_value))265 : null,266 styling: context.styling267 ? JSON.parse(JSON.stringify(context.styling))268 : null269 }270 : null);271 272 return {273 sort_state: {274 ...s.sort_state,275 sort_columns: sort_cols.slice(-3),276 initial_data: initial_data277 }278 };279 }),280 handle_filter: (col, datatype, filter, value) =>281 update_state((s) => {282 const filter_cols = s.filter_state.filter_columns.some(283 (c) => c.col === col284 )285 ? s.filter_state.filter_columns.filter((c) => c.col !== col)286 : [287 ...s.filter_state.filter_columns,288 { col, datatype, filter, value }289 ];290 291 const initial_data =292 s.filter_state.initial_data ||293 (context.data && filter_cols.length > 0294 ? {295 data: JSON.parse(JSON.stringify(context.data)),296 display_value: context.display_value297 ? JSON.parse(JSON.stringify(context.display_value))298 : null,299 styling: context.styling300 ? JSON.parse(JSON.stringify(context.styling))301 : null302 }303 : null);304 305 return {306 filter_state: {307 ...s.filter_state,308 filter_columns: filter_cols,309 initial_data: initial_data310 }311 };312 }),313 get_sort_status: (name, headers) => {314 const s = get(state);315 const sort_item = s.sort_state.sort_columns.find(316 (item) => headers[item.col] === name317 );318 return sort_item ? sort_item.direction : "none";319 },320 sort_data: (data, display_value, styling) => {321 const {322 sort_state: { sort_columns }323 } = get(state);324 if (sort_columns.length)325 sort_table_data(data, display_value, styling, sort_columns);326 },327 update_row_order: (data) =>328 update_state((s) => ({329 sort_state: {330 ...s.sort_state,331 row_order:332 s.sort_state.sort_columns.length && data[0]333 ? [...Array(data.length)]334 .map((_, i) => i)335 .sort((a, b) => {336 for (const { col, direction } of s.sort_state337 .sort_columns) {338 const comp =339 (data[a]?.[col]?.value ?? "") <340 (data[b]?.[col]?.value ?? "")341 ? -1342 : 1;343 if (comp) return direction === "asc" ? comp : -comp;344 }345 return 0;346 })347 : [...Array(data.length)].map((_, i) => i)348 }349 })),350 filter_data: (data) => {351 const query = get(state).current_search_query?.toLowerCase();352 return query353 ? data.filter((row) =>354 row.some((cell) =>355 String(cell?.value).toLowerCase().includes(query)356 )357 )358 : data;359 },360 add_row,361 add_col,362 add_row_at: (data, index, position, make_id) =>363 add_row(data, make_id, position === "above" ? index : index + 1),364 add_col_at: (data, headers, index, position, make_id) =>365 add_col(data, headers, make_id, position === "left" ? index : index + 1),366 delete_row: (data, index) =>367 data.length > 1 ? data.filter((_, i) => i !== index) : data,368 delete_col: (data, headers, index) =>369 headers.length > 1370 ? {371 data: data.map((row) => row.filter((_, i) => i !== index)),372 headers: headers.filter((_, i) => i !== index)373 }374 : { data, headers },375 delete_row_at: (data, index) =>376 data.length > 1377 ? [...data.slice(0, index), ...data.slice(index + 1)]378 : data,379 delete_col_at: (data, headers, index) =>380 headers.length > 1381 ? {382 data: data.map((row) => [383 ...row.slice(0, index),384 ...row.slice(index + 1)385 ]),386 headers: [...headers.slice(0, index), ...headers.slice(index + 1)]387 }388 : { data, headers },389 trigger_change: async (390 data,391 headers,392 previous_data,393 previous_headers,394 value_is_output,395 dispatch396 ) => {397 const s = get(state);398 if (s.current_search_query) return;399 400 const current_headers = headers.map((h) => h.value);401 const current_data = data.map((row) => row.map((cell) => cell.value));402 403 if (404 !dequal(current_data, previous_data) ||405 !dequal(current_headers, previous_headers)406 ) {407 if (!dequal(current_headers, previous_headers)) {408 update_state((s) => ({409 sort_state: { sort_columns: [], row_order: [], initial_data: null },410 filter_state: { filter_columns: [], initial_data: null }411 }));412 }413 dispatch("change", {414 data: data.map((row) => row.map((cell) => cell.value)),415 headers: current_headers,416 metadata: null417 });418 const index = s.ui_state.selected;419 if (index) {420 dispatch("edit", {421 index,422 value: data[index[0]][index[1]].value,423 previous_value: previous_data[index[0]][index[1]]424 });425 }426 if (!value_is_output) dispatch("input");427 }428 },429 reset_sort_state: () =>430 update_state((s) => {431 if (s.sort_state.initial_data && context.data) {432 const original = s.sort_state.initial_data;433 434 update_array(original.data, context.data);435 update_array(original.display_value, context.display_value);436 update_array(original.styling, context.styling);437 }438 439 return {440 sort_state: { sort_columns: [], row_order: [], initial_data: null }441 };442 }),443 reset_filter_state: () =>444 update_state((s) => {445 if (s.filter_state.initial_data && context.data) {446 const original = s.filter_state.initial_data;447 448 update_array(original.data, context.data);449 update_array(original.display_value, context.display_value);450 update_array(original.styling, context.styling);451 }452 453 return {454 filter_state: { filter_columns: [], initial_data: null }455 };456 }),457 set_active_cell_menu: (menu) =>458 update_state((s) => ({459 ui_state: { ...s.ui_state, active_cell_menu: menu }460 })),461 set_active_header_menu: (menu) =>462 update_state((s) => ({463 ui_state: { ...s.ui_state, active_header_menu: menu }464 })),465 set_selected_cells: (cells) =>466 update_state((s) => ({467 ui_state: { ...s.ui_state, selected_cells: cells }468 })),469 set_selected: (selected) =>470 update_state((s) => ({ ui_state: { ...s.ui_state, selected } })),471 set_editing: (editing) =>472 update_state((s) => ({ ui_state: { ...s.ui_state, editing } })),473 clear_ui_state: () =>474 update_state((s) => ({475 ui_state: {476 active_cell_menu: null,477 active_header_menu: null,478 selected_cells: [],479 selected: false,480 editing: false,481 header_edit: false,482 selected_header: false,483 active_button: null,484 copy_flash: false485 }486 })),487 set_header_edit: (header_index) =>488 update_state((s) => ({489 ui_state: {490 ...s.ui_state,491 selected_cells: [],492 selected_header: header_index,493 header_edit: header_index494 }495 })),496 set_selected_header: (header_index) =>497 update_state((s) => ({498 ui_state: {499 ...s.ui_state,500 selected_header: header_index,501 selected: false,502 selected_cells: []503 }504 })),505 handle_header_click: (col, editable) =>506 update_state((s) => ({507 ui_state: {508 ...s.ui_state,509 active_cell_menu: null,510 active_header_menu: null,511 selected: false,512 selected_cells: [],513 selected_header: col,514 header_edit: editable ? col : false515 }516 })),517 end_header_edit: (key) => {518 if (["Escape", "Enter", "Tab"].includes(key)) {519 update_state((s) => ({520 ui_state: { ...s.ui_state, selected: false, header_edit: false }521 }));522 }523 },524 get_selected_cells: () => get(state).ui_state.selected_cells,525 get_active_cell_menu: () => get(state).ui_state.active_cell_menu,526 get_active_button: () => get(state).ui_state.active_button,527 set_active_button: (button) =>528 update_state((s) => ({529 ui_state: { ...s.ui_state, active_button: button }530 })),531 set_copy_flash: (value) =>532 update_state((s) => ({ ui_state: { ...s.ui_state, copy_flash: value } })),533 handle_cell_click: (event, row, col) => {534 event.preventDefault();535 event.stopPropagation();536 537 const s = get(state);538 if (s.config.show_row_numbers && col === -1) return;539 540 let actual_row = row;541 if (s.current_search_query && context.data) {542 const filtered_indices: number[] = [];543 context.data.forEach((dataRow, idx) => {544 if (545 dataRow.some((cell) =>546 String(cell?.value)547 .toLowerCase()548 .includes(s.current_search_query?.toLowerCase() || "")549 )550 ) {551 filtered_indices.push(idx);552 }553 });554 actual_row = filtered_indices[row] ?? row;555 }556 557 const cells = handle_selection(558 [actual_row, col],559 s.ui_state.selected_cells,560 event561 );562 update_state((s) => ({563 ui_state: {564 ...s.ui_state,565 active_cell_menu: null,566 active_header_menu: null,567 selected_header: false,568 header_edit: false,569 selected_cells: cells,570 selected: cells[0]571 }572 }));573 574 if (s.config.editable && cells.length === 1) {575 update_state((s) => ({576 ui_state: { ...s.ui_state, editing: [actual_row, col] }577 }));578 tick().then(() =>579 context.els![context.data![actual_row][col].id]?.input?.focus()580 );581 } else {582 // ensure parent has focus for keyboard navigation583 tick().then(() => {584 if (context.parent_element) {585 context.parent_element.focus();586 }587 });588 }589 590 context.dispatch?.("select", {591 index: [actual_row, col],592 col_value: context.get_column!(col),593 row_value: context.get_row!(actual_row),594 value: context.get_data_at!(actual_row, col)595 });596 },597 toggle_cell_menu: (event, row, col) => {598 event.stopPropagation();599 const current_menu = get(state).ui_state.active_cell_menu;600 if (current_menu?.row === row && current_menu.col === col) {601 update_state((s) => ({602 ui_state: { ...s.ui_state, active_cell_menu: null }603 }));604 } else {605 const cell = (event.target as HTMLElement).closest("td");606 if (cell) {607 const rect = cell.getBoundingClientRect();608 update_state((s) => ({609 ui_state: {610 ...s.ui_state,611 active_cell_menu: { row, col, x: rect.right, y: rect.bottom }612 }613 }));614 }615 }616 },617 toggle_cell_button: (row, col) => {618 const current_button = get(state).ui_state.active_button;619 const new_button =620 current_button?.type === "cell" &&621 current_button.row === row &&622 current_button.col === col623 ? null624 : { type: "cell" as const, row, col };625 update_state((s) => ({626 ui_state: { ...s.ui_state, active_button: new_button }627 }));628 },629 handle_select_column: (col) => {630 if (!context.data) return;631 const cells = context.data.map((_, row) => [row, col] as CellCoordinate);632 update_state((s) => ({633 ui_state: {634 ...s.ui_state,635 selected_cells: cells,636 selected: cells[0],637 editing: false638 }639 }));640 setTimeout(() => context.parent_element?.focus(), 0);641 },642 handle_select_row: (row) => {643 if (!context.data || !context.data[0]) return;644 const cells = context.data[0].map(645 (_, col) => [row, col] as CellCoordinate646 );647 update_state((s) => ({648 ui_state: {649 ...s.ui_state,650 selected_cells: cells,651 selected: cells[0],652 editing: false653 }654 }));655 setTimeout(() => context.parent_element?.focus(), 0);656 },657 get_next_cell_coordinates,658 get_range_selection,659 move_cursor660 };661}662 663export function create_dataframe_context(664 config: DataFrameState["config"]665): DataFrameContext {666 const state = writable<DataFrameState>({667 config,668 current_search_query: null,669 sort_state: { sort_columns: [], row_order: [], initial_data: null },670 filter_state: { filter_columns: [], initial_data: null },671 ui_state: {672 active_cell_menu: null,673 active_header_menu: null,674 selected_cells: [],675 selected: false,676 editing: false,677 header_edit: false,678 selected_header: false,679 active_button: null,680 copy_flash: false681 }682 });683 684 const context: DataFrameContext = { state, actions: null as any };685 context.actions = create_actions(state, context);686 687 const instance_id = Symbol(688 `dataframe_${Math.random().toString(36).substring(2)}`689 );690 setContext(instance_id, context);691 setContext(DATAFRAME_KEY, { instance_id, context });692 693 return context;694}695 696export function get_dataframe_context(): DataFrameContext {697 const ctx = getContext<{ instance_id: symbol; context: DataFrameContext }>(698 DATAFRAME_KEY699 );700 return ctx?.context ?? getContext<DataFrameContext>(DATAFRAME_KEY);701}702 