UPDATE: Text spills now to adjacent cells if needed
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
107fc99409
commit
9852ce2504
@@ -11,6 +11,7 @@ mod test_evaluation;
|
||||
mod test_general;
|
||||
mod test_grid_lines;
|
||||
mod test_keyboard_navigation;
|
||||
mod test_last_empty_cell;
|
||||
mod test_on_area_selection;
|
||||
mod test_on_expand_selected_range;
|
||||
mod test_on_paste_styles;
|
||||
|
||||
55
base/src/test/user_model/test_last_empty_cell.rs
Normal file
55
base/src/test/user_model/test_last_empty_cell.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::LAST_ROW;
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// This is the first row, column 5
|
||||
model.set_user_input(0, 3, 5, "todo").unwrap();
|
||||
|
||||
// Row 3 before column 5 should be empty
|
||||
assert_eq!(
|
||||
model
|
||||
.get_last_non_empty_in_row_before_column(0, 3, 4)
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
// Row 3 before column 5 should be 5
|
||||
assert_eq!(
|
||||
model
|
||||
.get_last_non_empty_in_row_before_column(0, 3, 7)
|
||||
.unwrap(),
|
||||
Some(5)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_empty_cell() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
let column_g_range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 7,
|
||||
width: 1,
|
||||
height: LAST_ROW,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||
.unwrap();
|
||||
|
||||
// Column 7 has a style but it is empty
|
||||
assert_eq!(
|
||||
model
|
||||
.get_last_non_empty_in_row_before_column(0, 3, 14)
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -13,8 +13,8 @@ use crate::{
|
||||
},
|
||||
model::Model,
|
||||
types::{
|
||||
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState,
|
||||
Style, VerticalAlignment,
|
||||
Alignment, BorderItem, Cell, CellType, Col, HorizontalAlignment, SheetProperties,
|
||||
SheetState, Style, VerticalAlignment,
|
||||
},
|
||||
utils::is_valid_hex_color,
|
||||
};
|
||||
@@ -1602,6 +1602,64 @@ impl UserModel {
|
||||
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
|
||||
}
|
||||
|
||||
/// Returns the largest column in the row less than a column whose cell has a non empty value.
|
||||
/// If the row is empty, it returns `None`.
|
||||
/// This is useful when rendering a part of a worksheet to know which cells spill over
|
||||
pub fn get_last_non_empty_in_row_before_column(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<Option<i32>, String> {
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
let data = worksheet.sheet_data.get(&row);
|
||||
if let Some(row_data) = data {
|
||||
let mut last_column = None;
|
||||
let mut columns: Vec<i32> = row_data.keys().copied().collect();
|
||||
columns.sort_unstable();
|
||||
for col in columns {
|
||||
if col < column {
|
||||
if let Some(cell) = worksheet.cell(row, col) {
|
||||
if matches!(cell, Cell::EmptyCell { .. }) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
last_column = Some(col);
|
||||
}
|
||||
}
|
||||
Ok(last_column)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the largest column in the row greater than a column whose cell has a non empty value.
|
||||
pub fn get_first_non_empty_in_row_after_column(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<Option<i32>, String> {
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
let data = worksheet.sheet_data.get(&row);
|
||||
if let Some(row_data) = data {
|
||||
let mut columns: Vec<i32> = row_data.keys().copied().collect();
|
||||
// We sort the keys to ensure we are going from left to right
|
||||
columns.sort_unstable();
|
||||
for col in columns {
|
||||
if col > column {
|
||||
if let Some(cell) = worksheet.cell(row, col) {
|
||||
if matches!(cell, Cell::EmptyCell { .. }) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(Some(col));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns a copy of the selected area
|
||||
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
|
||||
let selected_area = self.get_selected_view();
|
||||
|
||||
@@ -705,4 +705,28 @@ impl Model {
|
||||
.delete_defined_name(name, scope)
|
||||
.map_err(|e| to_js_error(e.to_string()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getLastNonEmptyInRowBeforeColumn")]
|
||||
pub fn get_last_non_empty_in_row_before_column(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<Option<i32>, JsError> {
|
||||
self.model
|
||||
.get_last_non_empty_in_row_before_column(sheet, row, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getFirstNonEmptyInRowAfterColumn")]
|
||||
pub fn get_first_non_empty_in_row_after_column(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<Option<i32>, JsError> {
|
||||
self.model
|
||||
.get_first_non_empty_in_row_after_column(sheet, row, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
}
|
||||
|
||||
1162
webapp/IronCalc/package-lock.json
generated
1162
webapp/IronCalc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,3 +19,5 @@ export const LAST_ROW = 1_048_576;
|
||||
|
||||
export const ROW_HEIGH_SCALE = 1;
|
||||
export const COLUMN_WIDTH_SCALE = 1;
|
||||
|
||||
export const cellPadding = 4;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Model } from "@ironcalc/wasm";
|
||||
import type { CellStyle, Model } from "@ironcalc/wasm";
|
||||
import { columnNameFromNumber } from "@ironcalc/wasm";
|
||||
import { getColor } from "../Editor/util";
|
||||
import type { Cell } from "../types";
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
LAST_COLUMN,
|
||||
LAST_ROW,
|
||||
ROW_HEIGH_SCALE,
|
||||
cellPadding,
|
||||
defaultTextColor,
|
||||
gridColor,
|
||||
gridSeparatorColor,
|
||||
@@ -54,6 +55,22 @@ export const defaultCellFontFamily = fonts.regular;
|
||||
export const headerFontFamily = fonts.regular;
|
||||
export const frozenSeparatorWidth = 3;
|
||||
|
||||
type Tuple = [number, number];
|
||||
|
||||
interface TextProperties {
|
||||
row: number;
|
||||
column: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fontSize: number;
|
||||
textColor: string;
|
||||
font: string;
|
||||
underlined: boolean;
|
||||
strike: boolean;
|
||||
lines: [string, number, number, number][];
|
||||
}
|
||||
export default class WorksheetCanvas {
|
||||
sheetWidth: number;
|
||||
|
||||
@@ -93,6 +110,9 @@ export default class WorksheetCanvas {
|
||||
|
||||
refresh: () => void;
|
||||
|
||||
cells: TextProperties[];
|
||||
spills: Map<string, number>;
|
||||
|
||||
constructor(options: CanvasSettings) {
|
||||
this.model = options.model;
|
||||
this.sheetWidth = 0;
|
||||
@@ -116,6 +136,15 @@ export default class WorksheetCanvas {
|
||||
this.onRowHeightChanges = options.onRowHeightChanges;
|
||||
this.resetHeaders();
|
||||
this.cellOutlineHandle = attachOutlineHandle(this);
|
||||
// +1 means the cell is filled with a spill from a cell in the left
|
||||
// -1 means the cell is filled with a spill from a cell in the right
|
||||
this.spills = new Map<string, number>();
|
||||
this.cells = [];
|
||||
}
|
||||
|
||||
removeSpills(): void {
|
||||
this.spills.clear();
|
||||
this.cells = [];
|
||||
}
|
||||
|
||||
setScrollPosition(scrollPosition: { left: number; top: number }): void {
|
||||
@@ -318,7 +347,372 @@ export default class WorksheetCanvas {
|
||||
return left;
|
||||
}
|
||||
|
||||
private renderCell(
|
||||
private getFontStyle(style: CellStyle): {
|
||||
font: string;
|
||||
color: string;
|
||||
fontSize: number;
|
||||
} {
|
||||
const fontSize = style.font?.sz || 13;
|
||||
let font = `${fontSize}px ${defaultCellFontFamily}`;
|
||||
let color = defaultTextColor;
|
||||
|
||||
if (style.font) {
|
||||
color = style.font.color;
|
||||
font = style.font.b ? `bold ${font}` : `400 ${font}`;
|
||||
if (style.font.i) {
|
||||
font = `italic ${font}`;
|
||||
}
|
||||
}
|
||||
|
||||
return { font, color, fontSize };
|
||||
}
|
||||
|
||||
private getAlignment(
|
||||
style: CellStyle,
|
||||
cellType: number,
|
||||
): { horizontal: string; vertical: string } {
|
||||
let horizontal = style.alignment?.horizontal || "general";
|
||||
const vertical = style.alignment?.vertical || "bottom";
|
||||
|
||||
if (horizontal === "general") {
|
||||
if (cellType === 1) {
|
||||
horizontal = "right";
|
||||
} else if (cellType === 4) {
|
||||
horizontal = "center";
|
||||
} else {
|
||||
horizontal = "left";
|
||||
}
|
||||
}
|
||||
|
||||
return { horizontal, vertical };
|
||||
}
|
||||
|
||||
// Computes the text of cells that are off the screen. They are important because their text may spill into the viewport.
|
||||
private computeCellTextLeftRight(
|
||||
selectedSheet: number,
|
||||
row: number,
|
||||
rowHeight: number,
|
||||
topLeftCell: Cell,
|
||||
bottomRightCell: Cell,
|
||||
): void {
|
||||
// First compute the text to the left of the viewport
|
||||
const columnToTheLeft = this.model.getLastNonEmptyInRowBeforeColumn(
|
||||
selectedSheet,
|
||||
row,
|
||||
topLeftCell.column + 1,
|
||||
);
|
||||
if (columnToTheLeft) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, columnToTheLeft);
|
||||
const [textX, textY] = this.getCoordinatesByCell(row, columnToTheLeft);
|
||||
this.computeCellText(
|
||||
row,
|
||||
columnToTheLeft,
|
||||
textX,
|
||||
textY,
|
||||
columnWidth,
|
||||
rowHeight,
|
||||
);
|
||||
}
|
||||
// Compute the text to the right of the viewport
|
||||
const columnToTheRight = this.model.getFirstNonEmptyInRowAfterColumn(
|
||||
selectedSheet,
|
||||
row,
|
||||
bottomRightCell.column,
|
||||
);
|
||||
|
||||
if (columnToTheRight) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, columnToTheRight);
|
||||
const [textX, textY] = this.getCoordinatesByCell(row, columnToTheRight);
|
||||
this.computeCellText(
|
||||
row,
|
||||
columnToTheRight,
|
||||
textX,
|
||||
textY,
|
||||
columnWidth,
|
||||
rowHeight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Goes through all the visible cells and computes their text properties
|
||||
private computeCellsText(): void {
|
||||
const { topLeftCell, bottomRightCell } = this.getVisibleCells();
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
|
||||
this.cells = [];
|
||||
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
|
||||
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
|
||||
|
||||
// Top-left-pane
|
||||
let x = headerColumnWidth + 0.5;
|
||||
let y = headerRowHeight + 0.5;
|
||||
for (let row = 1; row <= frozenRows; row += 1) {
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
this.computeCellTextLeftRight(
|
||||
selectedSheet,
|
||||
row,
|
||||
rowHeight,
|
||||
topLeftCell,
|
||||
bottomRightCell,
|
||||
);
|
||||
|
||||
x = headerColumnWidth + 0.5;
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.computeCellText(row, column, x, y, columnWidth, rowHeight);
|
||||
|
||||
x += columnWidth;
|
||||
}
|
||||
y += rowHeight;
|
||||
}
|
||||
if (frozenRows === 0 && frozenColumns !== 0) {
|
||||
x = headerColumnWidth + 0.5;
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
x += this.getColumnWidth(selectedSheet, column);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are frozen rows draw a separator
|
||||
if (frozenRows) {
|
||||
y += frozenSeparatorWidth;
|
||||
}
|
||||
|
||||
// If there are frozen columns draw a separator
|
||||
if (frozenColumns) {
|
||||
x += frozenSeparatorWidth;
|
||||
}
|
||||
|
||||
const frozenX = x;
|
||||
const frozenY = y;
|
||||
// Top-right pane
|
||||
y = headerRowHeight + 0.5;
|
||||
for (let row = 1; row <= frozenRows; row += 1) {
|
||||
x = frozenX;
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
for (
|
||||
let { column } = topLeftCell;
|
||||
column <= bottomRightCell.column;
|
||||
column += 1
|
||||
) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.computeCellText(row, column, x, y, columnWidth, rowHeight);
|
||||
|
||||
x += columnWidth;
|
||||
}
|
||||
y += rowHeight;
|
||||
}
|
||||
|
||||
// Bottom-left pane
|
||||
y = frozenY;
|
||||
for (let { row } = topLeftCell; row <= bottomRightCell.row; row += 1) {
|
||||
x = headerColumnWidth;
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
|
||||
this.computeCellTextLeftRight(
|
||||
selectedSheet,
|
||||
row,
|
||||
rowHeight,
|
||||
topLeftCell,
|
||||
bottomRightCell,
|
||||
);
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.computeCellText(row, column, x, y, columnWidth, rowHeight);
|
||||
|
||||
x += columnWidth;
|
||||
}
|
||||
y += rowHeight;
|
||||
}
|
||||
|
||||
// Bottom-right pane
|
||||
y = frozenY;
|
||||
for (let { row } = topLeftCell; row <= bottomRightCell.row; row += 1) {
|
||||
x = frozenX;
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
|
||||
for (
|
||||
let { column } = topLeftCell;
|
||||
column <= bottomRightCell.column;
|
||||
column += 1
|
||||
) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.computeCellText(row, column, x, y, columnWidth, rowHeight);
|
||||
|
||||
x += columnWidth;
|
||||
}
|
||||
y += rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the text properties for a cell
|
||||
private computeCellText(
|
||||
row: number,
|
||||
column: number,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
) {
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
|
||||
const style = this.model.getCellStyle(selectedSheet, row, column);
|
||||
|
||||
const { font, color: textColor, fontSize } = this.getFontStyle(style);
|
||||
|
||||
// Number = 1,
|
||||
// Text = 2,
|
||||
// LogicalValue = 4,
|
||||
// ErrorValue = 16,
|
||||
// Array = 64,
|
||||
// CompoundData = 128,
|
||||
const cellType = this.model.getCellType(selectedSheet, row, column);
|
||||
const { horizontal: horizontalAlign, vertical: verticalAlign } =
|
||||
this.getAlignment(style, cellType);
|
||||
|
||||
const wrapText = style.alignment?.wrap_text || false;
|
||||
|
||||
const context = this.ctx;
|
||||
context.font = font;
|
||||
context.fillStyle = textColor;
|
||||
const fullText = this.model.getFormattedCellValue(
|
||||
selectedSheet,
|
||||
row,
|
||||
column,
|
||||
);
|
||||
|
||||
// Is there any better to determine the line height?
|
||||
const lineHeight = fontSize * 1.5;
|
||||
const lines = computeWrappedLines(
|
||||
fullText,
|
||||
wrapText,
|
||||
context,
|
||||
width - cellPadding,
|
||||
);
|
||||
const lineCount = lines.length;
|
||||
let maxWidth = 0;
|
||||
let minX = x;
|
||||
let maxX = x + width;
|
||||
const textProperties = {
|
||||
row,
|
||||
column,
|
||||
x: minX,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
fontSize,
|
||||
textColor,
|
||||
font,
|
||||
underlined: style.font?.u || false,
|
||||
strike: style.font?.strike || false,
|
||||
lines: [] as [string, number, number, number][],
|
||||
};
|
||||
|
||||
lines.forEach((text, line) => {
|
||||
const textWidth = context.measureText(text).width;
|
||||
let textX: number;
|
||||
let textY: number;
|
||||
// The idea is that in the present font-size and default row heigh,
|
||||
// top/bottom and center horizontalAlign coincide
|
||||
const verticalPadding = 4;
|
||||
if (horizontalAlign === "right") {
|
||||
textX = width - cellPadding + x - textWidth / 2;
|
||||
} else if (horizontalAlign === "center") {
|
||||
textX = x + width / 2;
|
||||
} else {
|
||||
// left aligned
|
||||
textX = cellPadding + x + textWidth / 2;
|
||||
}
|
||||
if (verticalAlign === "bottom") {
|
||||
textY =
|
||||
y +
|
||||
height -
|
||||
fontSize / 2 -
|
||||
verticalPadding +
|
||||
(line - lineCount + 1) * lineHeight;
|
||||
} else if (verticalAlign === "center") {
|
||||
textY = y + height / 2 + (line + (1 - lineCount) / 2) * lineHeight;
|
||||
} else {
|
||||
// aligned top
|
||||
textY = y + fontSize / 2 + verticalPadding + line * lineHeight;
|
||||
}
|
||||
textProperties.lines.push([text, textX, textY, textWidth]);
|
||||
minX = Math.min(minX, textX - textWidth / 2);
|
||||
maxX = Math.max(maxX, textX + textWidth / 2);
|
||||
maxWidth = Math.max(maxWidth, textX + textWidth / 2 - x);
|
||||
});
|
||||
// we need to see if the text spills to the right of the cell
|
||||
let leftColumnX = x;
|
||||
let rightColumnX = x + width;
|
||||
if (
|
||||
maxX > rightColumnX &&
|
||||
column < LAST_COLUMN &&
|
||||
this.model.getFormattedCellValue(selectedSheet, row, column + 1) === ""
|
||||
) {
|
||||
let spillColumn = column + 1;
|
||||
// Keep expanding the spill to the right until:
|
||||
// 1. There is a non-empty cell
|
||||
// 2. Reaches the end of the row
|
||||
// 3. There is the end of frozen columns
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
|
||||
while (
|
||||
rightColumnX < maxX &&
|
||||
this.model.getFormattedCellValue(selectedSheet, row, spillColumn) ===
|
||||
"" &&
|
||||
spillColumn <= LAST_COLUMN &&
|
||||
((column < frozenColumns && spillColumn <= frozenColumns) ||
|
||||
column > frozenColumns)
|
||||
) {
|
||||
rightColumnX += this.model.getColumnWidth(selectedSheet, spillColumn);
|
||||
// marks (row, spillColumn) as spilling so we don't draw a border to the left
|
||||
this.spills.set(`${row}-${spillColumn}`, 1);
|
||||
spillColumn += 1;
|
||||
}
|
||||
}
|
||||
// Same thing in the other direction, to the left of the cell
|
||||
const frozenColumnsCount = this.model.getFrozenColumnsCount(selectedSheet);
|
||||
if (
|
||||
minX < leftColumnX &&
|
||||
column > 1 &&
|
||||
this.model.getFormattedCellValue(selectedSheet, row, column - 1) === ""
|
||||
) {
|
||||
let spillColumn = column - 1;
|
||||
// Keep expanding the spill to the left until:
|
||||
// 1. There is a non-empty cell
|
||||
// 2. Reaches the beginning of the row
|
||||
// 3. There is the end of frozen columns
|
||||
while (
|
||||
leftColumnX > minX &&
|
||||
this.model.getFormattedCellValue(selectedSheet, row, spillColumn) ===
|
||||
"" &&
|
||||
spillColumn > 0 &&
|
||||
((column <= frozenColumnsCount && spillColumn <= frozenColumnsCount) ||
|
||||
column > frozenColumnsCount)
|
||||
) {
|
||||
leftColumnX -= this.getColumnWidth(selectedSheet, spillColumn);
|
||||
// marks (row, spillColumn) as spilling so we don't draw a border to the left
|
||||
this.spills.set(`${row}-${spillColumn + 1}`, 1);
|
||||
spillColumn -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (frozenColumnsCount > 0) {
|
||||
const frozenColumnsX =
|
||||
this.getCoordinatesByCell(row, frozenColumnsCount)[0] +
|
||||
this.getColumnWidth(selectedSheet, frozenColumnsCount);
|
||||
if (column > frozenColumnsCount) {
|
||||
leftColumnX = Math.max(leftColumnX, frozenColumnsX);
|
||||
} else {
|
||||
rightColumnX = Math.min(rightColumnX, frozenColumnsX);
|
||||
}
|
||||
}
|
||||
textProperties.x = leftColumnX;
|
||||
textProperties.width = rightColumnX - leftColumnX;
|
||||
this.cells.push(textProperties);
|
||||
}
|
||||
|
||||
/// Renders the cell style: colors, borders, etc. But not the text.
|
||||
private renderCellStyle(
|
||||
row: number,
|
||||
column: number,
|
||||
x: number,
|
||||
@@ -329,6 +723,7 @@ export default class WorksheetCanvas {
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
const style = this.model.getCellStyle(selectedSheet, row, column);
|
||||
|
||||
// first the background
|
||||
let backgroundColor = "#FFFFFF";
|
||||
if (style.fill.fg_color) {
|
||||
backgroundColor = style.fill.fg_color;
|
||||
@@ -336,32 +731,9 @@ export default class WorksheetCanvas {
|
||||
const cellGridColor = this.model.getShowGridLines(selectedSheet)
|
||||
? gridColor
|
||||
: backgroundColor;
|
||||
|
||||
const fontSize = style.font?.sz || 13;
|
||||
let font = `${fontSize}px ${defaultCellFontFamily}`;
|
||||
let textColor = defaultTextColor;
|
||||
if (style.font) {
|
||||
textColor = style.font.color;
|
||||
font = style.font.b ? `bold ${font}` : `400 ${font}`;
|
||||
if (style.font.i) {
|
||||
font = `italic ${font}`;
|
||||
}
|
||||
}
|
||||
let horizontalAlign = "general";
|
||||
if (style.alignment?.horizontal) {
|
||||
horizontalAlign = style.alignment.horizontal;
|
||||
}
|
||||
let verticalAlign = "bottom";
|
||||
if (style.alignment?.vertical) {
|
||||
verticalAlign = style.alignment.vertical;
|
||||
}
|
||||
const wrapText = style.alignment?.wrap_text || false;
|
||||
|
||||
const context = this.ctx;
|
||||
context.font = font;
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(x, y, width, height);
|
||||
context.fillStyle = textColor;
|
||||
|
||||
// Let's do the border
|
||||
// Algorithm:
|
||||
@@ -373,24 +745,13 @@ export default class WorksheetCanvas {
|
||||
// We only set the left and top borders (right and bottom are set later)
|
||||
const border = style.border;
|
||||
|
||||
let borderLeftColor = cellGridColor;
|
||||
let borderLeftWidth = 1;
|
||||
if (border.left) {
|
||||
borderLeftColor = border.left.color;
|
||||
switch (border.left.style) {
|
||||
case "thin":
|
||||
break;
|
||||
case "medium":
|
||||
borderLeftWidth = 2;
|
||||
break;
|
||||
case "thick":
|
||||
borderLeftWidth = 3;
|
||||
}
|
||||
} else {
|
||||
const leftStyle = this.model.getCellStyle(selectedSheet, row, column - 1);
|
||||
if (leftStyle.border.right) {
|
||||
borderLeftColor = leftStyle.border.right.color;
|
||||
switch (leftStyle.border.right.style) {
|
||||
// we skip don't draw a left border if it is marked as a "spill cell"
|
||||
if (this.spills.get(`${row}-${column}`) !== 1) {
|
||||
let borderLeftColor = cellGridColor;
|
||||
let borderLeftWidth = 1;
|
||||
if (border.left) {
|
||||
borderLeftColor = border.left.color;
|
||||
switch (border.left.style) {
|
||||
case "thin":
|
||||
break;
|
||||
case "medium":
|
||||
@@ -399,18 +760,37 @@ export default class WorksheetCanvas {
|
||||
case "thick":
|
||||
borderLeftWidth = 3;
|
||||
}
|
||||
} else if (style.fill.fg_color) {
|
||||
borderLeftColor = style.fill.fg_color;
|
||||
} else if (leftStyle.fill.fg_color) {
|
||||
borderLeftColor = leftStyle.fill.fg_color;
|
||||
} else {
|
||||
const leftStyle = this.model.getCellStyle(
|
||||
selectedSheet,
|
||||
row,
|
||||
column - 1,
|
||||
);
|
||||
if (leftStyle.border.right) {
|
||||
borderLeftColor = leftStyle.border.right.color;
|
||||
switch (leftStyle.border.right.style) {
|
||||
case "thin":
|
||||
break;
|
||||
case "medium":
|
||||
borderLeftWidth = 2;
|
||||
break;
|
||||
case "thick":
|
||||
borderLeftWidth = 3;
|
||||
}
|
||||
} else if (style.fill.fg_color) {
|
||||
borderLeftColor = style.fill.fg_color;
|
||||
} else if (leftStyle.fill.fg_color) {
|
||||
borderLeftColor = leftStyle.fill.fg_color;
|
||||
}
|
||||
}
|
||||
|
||||
context.beginPath();
|
||||
context.strokeStyle = borderLeftColor;
|
||||
context.lineWidth = borderLeftWidth;
|
||||
context.moveTo(x, y);
|
||||
context.lineTo(x, y + height);
|
||||
context.stroke();
|
||||
}
|
||||
context.beginPath();
|
||||
context.strokeStyle = borderLeftColor;
|
||||
context.lineWidth = borderLeftWidth;
|
||||
context.moveTo(x, y);
|
||||
context.lineTo(x, y + height);
|
||||
context.stroke();
|
||||
|
||||
let borderTopColor = cellGridColor;
|
||||
let borderTopWidth = 1;
|
||||
@@ -450,100 +830,56 @@ export default class WorksheetCanvas {
|
||||
context.moveTo(x, y);
|
||||
context.lineTo(x + width, y);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
// Number = 1,
|
||||
// Text = 2,
|
||||
// LogicalValue = 4,
|
||||
// ErrorValue = 16,
|
||||
// Array = 64,
|
||||
// CompoundData = 128,
|
||||
|
||||
const cellType = this.model.getCellType(selectedSheet, row, column);
|
||||
const fullText = this.model.getFormattedCellValue(
|
||||
selectedSheet,
|
||||
row,
|
||||
column,
|
||||
);
|
||||
const padding = 4;
|
||||
if (horizontalAlign === "general") {
|
||||
if (cellType === 1) {
|
||||
horizontalAlign = "right";
|
||||
} else if (cellType === 4) {
|
||||
horizontalAlign = "center";
|
||||
} else {
|
||||
horizontalAlign = "left";
|
||||
}
|
||||
}
|
||||
/// Renders the text in the cell.
|
||||
private renderCellText(textProperties: TextProperties) {
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
font,
|
||||
underlined,
|
||||
strike,
|
||||
fontSize,
|
||||
textColor,
|
||||
lines,
|
||||
} = textProperties;
|
||||
const context = this.ctx;
|
||||
|
||||
context.font = font;
|
||||
context.fillStyle = textColor;
|
||||
// Create a rectangular clipping region
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.rect(x, y, width, height);
|
||||
context.clip();
|
||||
|
||||
// Is there any better to determine the line height?
|
||||
const lineHeight = fontSize * 1.5;
|
||||
const lines = computeWrappedLines(
|
||||
fullText,
|
||||
wrapText,
|
||||
context,
|
||||
width - padding,
|
||||
);
|
||||
const lineCount = lines.length;
|
||||
lines.forEach((line, _) => {
|
||||
const [textContent, textX, textY, textWidth] = line;
|
||||
context.fillText(textContent, textX, textY);
|
||||
|
||||
lines.forEach((text, line) => {
|
||||
const textWidth = context.measureText(text).width;
|
||||
let textX: number;
|
||||
let textY: number;
|
||||
// The idea is that in the present font-size and default row heigh,
|
||||
// top/bottom and center horizontalAlign coincide
|
||||
const verticalPadding = 4;
|
||||
if (horizontalAlign === "right") {
|
||||
textX = width - padding + x - textWidth / 2;
|
||||
} else if (horizontalAlign === "center") {
|
||||
textX = x + width / 2;
|
||||
} else {
|
||||
// left aligned
|
||||
textX = padding + x + textWidth / 2;
|
||||
if (underlined) {
|
||||
// There are no text-decoration in canvas. You have to do the underline yourself.
|
||||
const offset = Math.floor(fontSize / 2);
|
||||
context.beginPath();
|
||||
context.strokeStyle = textColor;
|
||||
context.lineWidth = 1;
|
||||
context.moveTo(textX - textWidth / 2, textY + offset);
|
||||
context.lineTo(textX + textWidth / 2, textY + offset);
|
||||
context.stroke();
|
||||
}
|
||||
if (verticalAlign === "bottom") {
|
||||
textY =
|
||||
y +
|
||||
height -
|
||||
fontSize / 2 -
|
||||
verticalPadding +
|
||||
(line - lineCount + 1) * lineHeight;
|
||||
} else if (verticalAlign === "center") {
|
||||
textY = y + height / 2 + (line + (1 - lineCount) / 2) * lineHeight;
|
||||
} else {
|
||||
// aligned top
|
||||
textY = y + fontSize / 2 + verticalPadding + line * lineHeight;
|
||||
}
|
||||
context.fillText(text, textX, textY);
|
||||
if (style.font) {
|
||||
if (style.font.u) {
|
||||
// There are no text-decoration in canvas. You have to do the underline yourself.
|
||||
const offset = Math.floor(fontSize / 2);
|
||||
context.beginPath();
|
||||
context.strokeStyle = textColor;
|
||||
context.lineWidth = 1;
|
||||
context.moveTo(textX - textWidth / 2, textY + offset);
|
||||
context.lineTo(textX + textWidth / 2, textY + offset);
|
||||
context.stroke();
|
||||
}
|
||||
if (style.font.strike) {
|
||||
// There are no text-decoration in canvas. You have to do the strikethrough yourself.
|
||||
context.beginPath();
|
||||
context.strokeStyle = textColor;
|
||||
context.lineWidth = 1;
|
||||
context.moveTo(textX - textWidth / 2, textY);
|
||||
context.lineTo(textX + textWidth / 2, textY);
|
||||
context.stroke();
|
||||
}
|
||||
if (strike) {
|
||||
// There are no text-decoration in canvas. You have to do the strikethrough yourself.
|
||||
context.beginPath();
|
||||
context.strokeStyle = textColor;
|
||||
context.lineWidth = 1;
|
||||
context.moveTo(textX - textWidth / 2, textY);
|
||||
context.lineTo(textX + textWidth / 2, textY);
|
||||
context.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
// remove the clipping region
|
||||
context.restore();
|
||||
}
|
||||
|
||||
@@ -1399,6 +1735,12 @@ export default class WorksheetCanvas {
|
||||
}
|
||||
|
||||
renderSheet(): void {
|
||||
console.time("renderSheet");
|
||||
this._renderSheet();
|
||||
console.timeEnd("renderSheet");
|
||||
}
|
||||
|
||||
_renderSheet(): void {
|
||||
const context = this.ctx;
|
||||
const { canvas } = this;
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
@@ -1412,6 +1754,7 @@ export default class WorksheetCanvas {
|
||||
this.removeHandles();
|
||||
|
||||
const { topLeftCell, bottomRightCell } = this.getVisibleCells();
|
||||
this.computeCellsText();
|
||||
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
|
||||
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
|
||||
@@ -1424,7 +1767,7 @@ export default class WorksheetCanvas {
|
||||
x = headerColumnWidth + 0.5;
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||
this.renderCellStyle(row, column, x, y, columnWidth, rowHeight);
|
||||
x += columnWidth;
|
||||
}
|
||||
y += rowHeight;
|
||||
@@ -1474,7 +1817,7 @@ export default class WorksheetCanvas {
|
||||
column += 1
|
||||
) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||
this.renderCellStyle(row, column, x, y, columnWidth, rowHeight);
|
||||
x += columnWidth;
|
||||
}
|
||||
y += rowHeight;
|
||||
@@ -1488,7 +1831,7 @@ export default class WorksheetCanvas {
|
||||
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||
this.renderCellStyle(row, column, x, y, columnWidth, rowHeight);
|
||||
|
||||
x += columnWidth;
|
||||
}
|
||||
@@ -1507,13 +1850,18 @@ export default class WorksheetCanvas {
|
||||
column += 1
|
||||
) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||
this.renderCellStyle(row, column, x, y, columnWidth, rowHeight);
|
||||
|
||||
x += columnWidth;
|
||||
}
|
||||
y += rowHeight;
|
||||
}
|
||||
|
||||
// Render all cell texts
|
||||
for (const cell of this.cells) {
|
||||
this.renderCellText(cell);
|
||||
}
|
||||
|
||||
// Draw column headers
|
||||
this.renderColumnHeaders(
|
||||
frozenColumns,
|
||||
|
||||
675
webapp/app.ironcalc.com/frontend/package-lock.json
generated
675
webapp/app.ironcalc.com/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user