CoolFace
Datasetpublic

gradio/frontend

sourceHugging Faceupdated 7h agoView on Hugging Face
1likes413kdownloads
dataframe_context.ts702 linesDownload Raw Back to context
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 
gradio/frontend · CoolFace