🙃
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use crate::cell::{Cell, CellRef};
|
use crate::cell::{Cell, CellRef};
|
||||||
use crate::parser::*;
|
use crate::parser::*;
|
||||||
use crate::tokenizer::Literal;
|
use crate::tokenizer::Literal;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
@@ -34,15 +34,17 @@ impl Evaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let eval: Eval;
|
let eval: Eval;
|
||||||
|
let deps: HashSet<CellRef>;
|
||||||
|
|
||||||
if let Some(c) = raw_val.chars().nth(0)
|
if let Some(c) = raw_val.chars().nth(0)
|
||||||
&& c == '='
|
&& c == '='
|
||||||
{
|
{
|
||||||
eval = self.evaluate(raw_val[1..].to_owned())?;
|
(eval, deps) = self.evaluate(raw_val[1..].to_owned())?;
|
||||||
|
// for dep in deps {}
|
||||||
} else {
|
} else {
|
||||||
match self.evaluate(raw_val.to_owned()) {
|
match self.evaluate(raw_val.to_owned()) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
eval = e;
|
(eval, deps) = e;
|
||||||
}
|
}
|
||||||
Err(_) => eval = Eval::Literal(Literal::String(raw_val.to_owned())),
|
Err(_) => eval = Eval::Literal(Literal::String(raw_val.to_owned())),
|
||||||
}
|
}
|
||||||
@@ -61,14 +63,28 @@ impl Evaluator {
|
|||||||
|
|
||||||
Ok((cell.raw(), cell.eval()))
|
Ok((cell.raw(), cell.eval()))
|
||||||
}
|
}
|
||||||
|
pub fn add_cell_dep(&mut self, cell_ref: CellRef, dep_ref: CellRef) -> Result<(), String> {
|
||||||
pub fn evaluate(&mut self, str: String) -> Result<Eval, String> {
|
if !self.cells.contains_key(&cell_ref) {
|
||||||
let (mut expr, mut deps) = parse(&str)?;
|
return Err(format!("Cell at {:?} not found.", cell_ref));
|
||||||
|
|
||||||
self.evaluate_expr(&mut expr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_expr(&mut self, expr: &mut Expr) -> Result<Eval, String> {
|
if let Some(cell) = self.cells.get_mut(&cell_ref) {
|
||||||
|
cell.add_i_dep(dep_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&mut self, str: String) -> Result<(Eval, HashSet<CellRef>), String> {
|
||||||
|
let (expr, deps) = parse(&str)?;
|
||||||
|
|
||||||
|
match self.evaluate_expr(&expr) {
|
||||||
|
Ok(it) => Ok((it, deps)),
|
||||||
|
Err(it) => Err(it),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_expr(&mut self, expr: &Expr) -> Result<Eval, String> {
|
||||||
let res = match expr {
|
let res = match expr {
|
||||||
Expr::Literal(lit) => Eval::Literal(lit.clone()),
|
Expr::Literal(lit) => Eval::Literal(lit.clone()),
|
||||||
Expr::CellRef(re) => self.get_cell(re.to_owned())?.1,
|
Expr::CellRef(re) => self.get_cell(re.to_owned())?.1,
|
||||||
|
|||||||
@@ -1,43 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
const socket = new WebSocket("ws://localhost:7050");
|
|
||||||
|
|
||||||
socket.onmessage = (event) => {
|
|
||||||
const message = event.data;
|
|
||||||
console.log("Received message from server:", message);
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onopen = () => {
|
|
||||||
console.log("WebSocket connection established.");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connection opened
|
|
||||||
|
|
||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
const numRows = 1000; // Increased to show performance
|
const numRows = 1000;
|
||||||
const numCols = 100; // Increased to show performance
|
const numCols = 100;
|
||||||
const rowHeight = 30; // px
|
const rowHeight = 30; // px
|
||||||
const colWidth = 150; // px
|
const colWidth = 150; // px
|
||||||
const rowNumberWidth = 50; // px
|
const rowNumberWidth = 50; // px
|
||||||
const colHeaderHeight = 30; // px
|
const colHeaderHeight = 30; // px
|
||||||
|
|
||||||
// --- State ---
|
// --- State (Svelte 5 runes) ---
|
||||||
let gridData: string[][];
|
let gridData = $state<string[][] | null>(new Array());
|
||||||
let columnLabels: string[] = [];
|
let columnLabels = $state<string[]>([]);
|
||||||
let activeCell: [number, number] | null = null;
|
let activeCell = $state<[number, number] | null>(null);
|
||||||
let viewportElement: HTMLElement;
|
|
||||||
let viewportWidth = 0;
|
|
||||||
let viewportHeight = 0;
|
|
||||||
let scrollTop = 0;
|
|
||||||
let scrollLeft = 0;
|
|
||||||
|
|
||||||
// --- Helper Functions ---
|
let viewportEl = $state<HTMLElement | null>(null);
|
||||||
/**
|
let viewportWidth = $state(0);
|
||||||
* Generates Excel-style column labels (A, B, ..., Z, AA, AB, ...).
|
let viewportHeight = $state(0);
|
||||||
*/
|
let scrollTop = $state(0);
|
||||||
|
let scrollLeft = $state(0);
|
||||||
|
|
||||||
|
let socket: WebSocket | null = null;
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
function generateColumnLabels(count: number): string[] {
|
function generateColumnLabels(count: number): string[] {
|
||||||
const labels = [];
|
const labels: string[] = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let label = "";
|
let label = "";
|
||||||
let temp = i;
|
let temp = i;
|
||||||
@@ -52,64 +39,71 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
columnLabels = generateColumnLabels(numCols);
|
columnLabels = generateColumnLabels(numCols);
|
||||||
gridData = Array(numRows)
|
gridData = Array.from({ length: numRows }, () =>
|
||||||
.fill(null)
|
Array<string>(numCols).fill(""),
|
||||||
.map(() => Array(numCols).fill(""));
|
);
|
||||||
|
|
||||||
|
socket = new WebSocket("ws://localhost:7050");
|
||||||
|
socket.onmessage = (e) =>
|
||||||
|
console.log("Received message from server:", e.data);
|
||||||
|
socket.onopen = () => console.log("WebSocket connection established.");
|
||||||
|
return () => socket?.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Event Handlers ---
|
// --- Events ---
|
||||||
function handleGridBlur(e: FocusEvent) {
|
function handleGridBlur(e: FocusEvent) {
|
||||||
if (!(e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)) {
|
const target = e.currentTarget as HTMLElement;
|
||||||
activeCell = null;
|
const next = e.relatedTarget as Node | null;
|
||||||
}
|
if (!target.contains(next)) activeCell = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCellBlur(row: number, col: number) {
|
function handleCellBlur(row: number, col: number) {
|
||||||
// This function runs when a cell loses focus.
|
|
||||||
// You can add your logic here, e.g., validation, calculations, etc.
|
|
||||||
console.log(
|
console.log(
|
||||||
`Cell (${row + 1}, ${columnLabels[col]}) lost focus. Value:`,
|
`Cell (${row + 1}, ${columnLabels[col]}) lost focus. Value:`,
|
||||||
gridData[row][col],
|
gridData![row][col],
|
||||||
);
|
);
|
||||||
|
socket?.send(
|
||||||
socket.send(
|
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
row: row + 1,
|
row: row + 1,
|
||||||
col: columnLabels[col],
|
col: columnLabels[col],
|
||||||
value: gridData[row][col],
|
value: gridData![row][col],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Reactive Calculations for Virtualization ---
|
// --- Scroll math (account for header/row gutters) ---
|
||||||
// $: is a Svelte feature that re-runs code when its dependencies change.
|
// How far into the *grid area* (excluding headers) we've scrolled:
|
||||||
|
const scrollDX = $derived(Math.max(0, scrollLeft - rowNumberWidth));
|
||||||
|
const scrollDY = $derived(Math.max(0, scrollTop - colHeaderHeight));
|
||||||
|
|
||||||
// Calculate which rows/cols are visible based on scroll position
|
// Viewport pixels actually available to show cells (excluding sticky gutters):
|
||||||
$: startRow = Math.max(0, Math.floor(scrollTop / rowHeight));
|
const innerW = $derived(Math.max(0, viewportWidth - rowNumberWidth));
|
||||||
$: endRow = Math.min(
|
const innerH = $derived(Math.max(0, viewportHeight - colHeaderHeight));
|
||||||
numRows,
|
|
||||||
startRow + Math.ceil(viewportHeight / rowHeight) + 1,
|
// Virtualization windows:
|
||||||
);
|
const startCol = $derived(Math.max(0, Math.floor(scrollDX / colWidth)));
|
||||||
$: startCol = Math.max(0, Math.floor(scrollLeft / colWidth));
|
const endCol = $derived(
|
||||||
$: endCol = Math.min(
|
Math.min(numCols, startCol + Math.ceil(innerW / colWidth) + 1),
|
||||||
numCols,
|
|
||||||
startCol + Math.ceil(viewportWidth / colWidth) + 1,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create arrays of only the visible items to render
|
const startRow = $derived(Math.max(0, Math.floor(scrollDY / rowHeight)));
|
||||||
$: visibleRows = Array(endRow - startRow)
|
const endRow = $derived(
|
||||||
.fill(0)
|
Math.min(numRows, startRow + Math.ceil(innerH / rowHeight) + 1),
|
||||||
.map((_, i) => startRow + i);
|
);
|
||||||
$: visibleCols = Array(endCol - startCol)
|
|
||||||
.fill(0)
|
const visibleCols = $derived(
|
||||||
.map((_, i) => startCol + i);
|
Array.from({ length: endCol - startCol }, (_, i) => startCol + i),
|
||||||
|
);
|
||||||
|
const visibleRows = $derived(
|
||||||
|
Array.from({ length: endRow - startRow }, (_, i) => startRow + i),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-full h-[85vh] p-4 bg-background text-foreground rounded-lg border flex flex-col"
|
class="w-full h-[85vh] p-4 bg-background text-foreground rounded-lg border flex flex-col"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative grid-container"
|
class="grid-container relative"
|
||||||
on:focusout={handleGridBlur}
|
on:focusout={handleGridBlur}
|
||||||
style="
|
style="
|
||||||
--row-height: {rowHeight}px;
|
--row-height: {rowHeight}px;
|
||||||
@@ -118,43 +112,45 @@
|
|||||||
--col-header-height: {colHeaderHeight}px;
|
--col-header-height: {colHeaderHeight}px;
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<!-- The scrollable viewport provides the scrollbars -->
|
<!-- Single, real scroll container -->
|
||||||
<div
|
<div
|
||||||
class="viewport"
|
class="viewport"
|
||||||
bind:this={viewportElement}
|
bind:this={viewportEl}
|
||||||
bind:clientWidth={viewportWidth}
|
bind:clientWidth={viewportWidth}
|
||||||
bind:clientHeight={viewportHeight}
|
bind:clientHeight={viewportHeight}
|
||||||
on:scroll={(e) => {
|
on:scroll={(e) => {
|
||||||
scrollTop = e.currentTarget.scrollTop;
|
const el = e.currentTarget as HTMLElement;
|
||||||
scrollLeft = e.currentTarget.scrollLeft;
|
scrollTop = el.scrollTop;
|
||||||
|
scrollLeft = el.scrollLeft;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<!-- Sizer div creates the full scrollable area -->
|
<!-- Sizer creates true scrollable area (includes gutters + grid) -->
|
||||||
<div
|
<div
|
||||||
class="total-sizer"
|
class="total-sizer"
|
||||||
style:width="{numCols * colWidth}px"
|
style:width={`${rowNumberWidth + numCols * colWidth}px`}
|
||||||
style:height="{numRows * rowHeight}px"
|
style:height={`${colHeaderHeight + numRows * rowHeight}px`}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- The Renderer sits on top of the viewport and handles drawing -->
|
<!-- Top-left sticky corner -->
|
||||||
{#if gridData}
|
|
||||||
<div class="renderer">
|
|
||||||
<!-- Top-left corner -->
|
|
||||||
<div
|
<div
|
||||||
class="top-left-corner"
|
class="top-left-corner"
|
||||||
|
style="top: 0; left: 0;"
|
||||||
class:active-header-corner={activeCell !== null}
|
class:active-header-corner={activeCell !== null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Visible Column Headers -->
|
<!-- Column headers (stick to top, scroll horizontally with grid) -->
|
||||||
<div
|
<div
|
||||||
class="col-headers-container"
|
class="col-headers-container"
|
||||||
style:transform="translateX(-{scrollLeft}px)"
|
style="
|
||||||
|
top: 0;
|
||||||
|
left: {rowNumberWidth}px;
|
||||||
|
transform: translateX(-{scrollDX}px);
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{#each visibleCols as j (j)}
|
{#each visibleCols as j (j)}
|
||||||
<div
|
<div
|
||||||
class="header-cell"
|
class="header-cell"
|
||||||
style:left="{j * colWidth}px"
|
style:left={`${j * colWidth}px`}
|
||||||
class:active-header={activeCell !== null && activeCell[1] === j}
|
class:active-header={activeCell !== null && activeCell[1] === j}
|
||||||
>
|
>
|
||||||
{columnLabels[j]}
|
{columnLabels[j]}
|
||||||
@@ -162,15 +158,19 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Visible Row Headers -->
|
<!-- Row headers (stick to left, scroll vertically with grid) -->
|
||||||
<div
|
<div
|
||||||
class="row-headers-container"
|
class="row-headers-container"
|
||||||
style:transform="translateY(-{scrollTop}px)"
|
style="
|
||||||
|
top: {colHeaderHeight}px;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-{scrollDY}px);
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{#each visibleRows as i (i)}
|
{#each visibleRows as i (i)}
|
||||||
<div
|
<div
|
||||||
class="row-number-cell"
|
class="row-number-cell"
|
||||||
style:top="{i * rowHeight}px"
|
style:top={`${i * rowHeight}px`}
|
||||||
class:active-header={activeCell !== null && activeCell[0] === i}
|
class:active-header={activeCell !== null && activeCell[0] === i}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
@@ -178,27 +178,34 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Visible Grid Cells -->
|
<!-- Visible grid cells (overlay; move opposite the inner scroll) -->
|
||||||
|
{#if gridData}
|
||||||
<div
|
<div
|
||||||
class="cells-container"
|
class="cells-container"
|
||||||
style:transform="translate(-{scrollLeft}px, -{scrollTop}px)"
|
style="
|
||||||
|
top: {colHeaderHeight}px;
|
||||||
|
left: {rowNumberWidth}px;
|
||||||
|
transform: translate(-{scrollDX}px, -{scrollDY}px);
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{#each visibleRows as i (i)}
|
{#each visibleRows as i (i)}
|
||||||
{#each visibleCols as j (j)}
|
{#each visibleCols as j (j)}
|
||||||
<div
|
<div
|
||||||
class="grid-cell"
|
class="grid-cell"
|
||||||
style:top="{i * rowHeight}px"
|
style:top={`${i * rowHeight}px`}
|
||||||
style:left="{j * colWidth}px"
|
style:left={`${j * colWidth}px`}
|
||||||
>
|
>
|
||||||
<!-- Using a standard input to ensure styles are applied correctly -->
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={gridData[i][j]}
|
value={gridData[i][j]}
|
||||||
class="cell-input"
|
class="cell-input"
|
||||||
|
on:input={(e) =>
|
||||||
|
(gridData[i][j] = (
|
||||||
|
e.currentTarget as HTMLInputElement
|
||||||
|
).value)}
|
||||||
on:focus={() => (activeCell = [i, j])}
|
on:focus={() => (activeCell = [i, j])}
|
||||||
on:blur={() => handleCellBlur(i, j)}
|
on:blur={() => handleCellBlur(i, j)}
|
||||||
/>
|
/>
|
||||||
<!-- Active cell indicator with fill handle -->
|
|
||||||
{#if activeCell && activeCell[0] === i && activeCell[1] === j}
|
{#if activeCell && activeCell[0] === i && activeCell[1] === j}
|
||||||
<div class="active-cell-indicator">
|
<div class="active-cell-indicator">
|
||||||
<div class="fill-handle" />
|
<div class="fill-handle" />
|
||||||
@@ -208,50 +215,39 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.grid-container {
|
.grid-container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
Helvetica, Arial, sans-serif;
|
Helvetica, Arial, sans-serif;
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewport {
|
.viewport {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.total-sizer {
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.renderer {
|
.total-sizer {
|
||||||
position: absolute;
|
/* occupies the scroll area; other layers are absolutely positioned on top */
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Layered overlays inside the viewport */
|
||||||
.top-left-corner,
|
.top-left-corner,
|
||||||
.col-headers-container,
|
.col-headers-container,
|
||||||
.row-headers-container,
|
.row-headers-container,
|
||||||
.cells-container {
|
.cells-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
z-index: 1;
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-left-corner {
|
.top-left-corner {
|
||||||
@@ -262,25 +258,22 @@
|
|||||||
border-bottom: 1px solid hsl(var(--border));
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-header-corner {
|
.active-header-corner {
|
||||||
background-color: hsl(var(--accent));
|
background-color: hsl(var(--accent));
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-headers-container {
|
.col-headers-container {
|
||||||
top: 0;
|
|
||||||
left: var(--row-number-width);
|
|
||||||
height: var(--col-header-height);
|
height: var(--col-header-height);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-headers-container {
|
.row-headers-container {
|
||||||
top: var(--col-header-height);
|
|
||||||
left: 0;
|
|
||||||
width: var(--row-number-width);
|
width: var(--row-number-width);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cells-container {
|
.cells-container {
|
||||||
top: var(--col-header-height);
|
|
||||||
left: var(--row-number-width);
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,12 +313,10 @@
|
|||||||
height: var(--row-height);
|
height: var(--row-height);
|
||||||
border-right: 1px solid hsl(var(--border) / 0.7);
|
border-right: 1px solid hsl(var(--border) / 0.7);
|
||||||
border-bottom: 1px solid hsl(var(--border) / 0.7);
|
border-bottom: 1px solid hsl(var(--border) / 0.7);
|
||||||
pointer-events: auto;
|
|
||||||
background-color: hsl(var(--background));
|
background-color: hsl(var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-input {
|
.cell-input {
|
||||||
/* Overriding all default input styles */
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
@@ -342,7 +333,7 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
/* Ensure no focus styles are added by the browser or libraries */
|
|
||||||
.cell-input:focus,
|
.cell-input:focus,
|
||||||
.cell-input:focus-visible {
|
.cell-input:focus-visible {
|
||||||
border: 1px solid red;
|
border: 1px solid red;
|
||||||
@@ -358,6 +349,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill-handle {
|
.fill-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -4px;
|
bottom: -4px;
|
||||||
|
|||||||
Reference in New Issue
Block a user