UPDATE: Adds wrapping!

This commit is contained in:
Nicolás Hatcher
2025-02-27 22:54:08 +01:00
committed by Nicolás Hatcher Andrés
parent 4f627b4363
commit b62256963a
4 changed files with 106 additions and 28 deletions

View File

@@ -32,6 +32,7 @@ import {
Type, Type,
Underline, Underline,
Undo2, Undo2,
WrapText,
} from "lucide-react"; } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -64,6 +65,7 @@ type ToolbarProperties = {
onToggleStrike: (v: boolean) => void; onToggleStrike: (v: boolean) => void;
onToggleHorizontalAlign: (v: string) => void; onToggleHorizontalAlign: (v: string) => void;
onToggleVerticalAlign: (v: string) => void; onToggleVerticalAlign: (v: string) => void;
onToggleWrapText: (v: boolean) => void;
onCopyStyles: () => void; onCopyStyles: () => void;
onTextColorPicked: (hex: string) => void; onTextColorPicked: (hex: string) => void;
onFillColorPicked: (hex: string) => void; onFillColorPicked: (hex: string) => void;
@@ -81,6 +83,7 @@ type ToolbarProperties = {
strike: boolean; strike: boolean;
horizontalAlign: HorizontalAlignment; horizontalAlign: HorizontalAlignment;
verticalAlign: VerticalAlignment; verticalAlign: VerticalAlignment;
wrapText: boolean;
canEdit: boolean; canEdit: boolean;
numFmt: string; numFmt: string;
showGridLines: boolean; showGridLines: boolean;
@@ -206,6 +209,30 @@ function Toolbar(properties: ToolbarProperties) {
</StyledButton> </StyledButton>
</FormatMenu> </FormatMenu>
<Divider /> <Divider />
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(-1);
}}
title={t("toolbar.decrease_font_size")}
>
<Minus />
</StyledButton>
<FontSizeBox>{properties.fontSize}</FontSizeBox>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(1);
}}
title={t("toolbar.increase_font_size")}
>
<Plus />
</StyledButton>
<Divider />
<StyledButton <StyledButton
type="button" type="button"
$pressed={properties.bold} $pressed={properties.bold}
@@ -254,31 +281,6 @@ function Toolbar(properties: ToolbarProperties) {
> >
<Type /> <Type />
</StyledButton> </StyledButton>
<Divider />
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(-1);
}}
title={t("toolbar.decrease_font_size")}
>
<Minus />
</StyledButton>
<FontSizeBox>{properties.fontSize}</FontSizeBox>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(1);
}}
title={t("toolbar.increase_font_size")}
>
<Plus />
</StyledButton>
<Divider />
<StyledButton <StyledButton
type="button" type="button"
$pressed={false} $pressed={false}
@@ -367,6 +369,17 @@ function Toolbar(properties: ToolbarProperties) {
> >
<ArrowDownToLine /> <ArrowDownToLine />
</StyledButton> </StyledButton>
<StyledButton
type="button"
$pressed={properties.wrapText === true}
onClick={() => {
properties.onToggleWrapText(!properties.wrapText);
}}
disabled={!canEdit}
title={t("toolbar.wrap_text")}
>
<WrapText />
</StyledButton>
<Divider /> <Divider />
<StyledButton <StyledButton

View File

@@ -112,6 +112,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
updateRangeStyle("alignment.vertical", value); updateRangeStyle("alignment.vertical", value);
}; };
const onToggleWrapText = (value: boolean) => {
updateRangeStyle("alignment.wrap_text", `${value}`);
};
const onTextColorPicked = (hex: string) => { const onTextColorPicked = (hex: string) => {
updateRangeStyle("font.color", hex); updateRangeStyle("font.color", hex);
}; };
@@ -532,6 +536,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
onToggleStrike={onToggleStrike} onToggleStrike={onToggleStrike}
onToggleHorizontalAlign={onToggleHorizontalAlign} onToggleHorizontalAlign={onToggleHorizontalAlign}
onToggleVerticalAlign={onToggleVerticalAlign} onToggleVerticalAlign={onToggleVerticalAlign}
onToggleWrapText={onToggleWrapText}
onCopyStyles={onCopyStyles} onCopyStyles={onCopyStyles}
onTextColorPicked={onTextColorPicked} onTextColorPicked={onTextColorPicked}
onFillColorPicked={onFillColorPicked} onFillColorPicked={onFillColorPicked}
@@ -639,6 +644,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
verticalAlign={ verticalAlign={
style.alignment?.vertical ? style.alignment.vertical : "bottom" style.alignment?.vertical ? style.alignment.vertical : "bottom"
} }
wrapText={style.alignment?.wrap_text || false}
canEdit={true} canEdit={true}
numFmt={style.num_fmt} numFmt={style.num_fmt}
showGridLines={model.getShowGridLines(model.getSelectedSheet())} showGridLines={model.getShowGridLines(model.getSelectedSheet())}

View File

@@ -70,6 +70,52 @@ function hexToRGBA10Percent(colorHex: string): string {
return `rgba(${red}, ${green}, ${blue}, ${alpha})`; return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
} }
/**
* Splits the given text into multiple lines. If `wrapText` is true, it applies word-wrapping
* based on the specified canvas context, maximum width, and horizontal padding.
*
* - First, the text is split by newline characters so that explicit newlines are respected.
* - If wrapping is enabled, each line is further split into words and measured against the
* available width. Whenever adding an extra word would exceed
* this limit, a new line is started.
*
* @param text The text to split into lines.
* @param wrapText Whether to apply word-wrapping or just return text split by newlines.
* @param context The `CanvasRenderingContext2D` used for measuring text width.
* @param width The maximum width for each line.
* @returns An array of lines (strings), each fitting within the specified width if wrapping is enabled.
*/
function computeWrappedLines(
text: string,
wrapText: boolean,
context: CanvasRenderingContext2D,
width: number,
): string[] {
// Split the text into lines
const rawLines = text.split("\n");
if (!wrapText) {
// If there is no wrapping, return the raw lines
return rawLines;
}
const wrappedLines = [];
for (const line of rawLines) {
const words = line.split(" ");
let currentLine = words[0];
for (const word of words) {
const testLine = `${currentLine} ${word}`;
const textWidth = context.measureText(testLine).width;
if (textWidth < width) {
currentLine = testLine;
} else {
wrappedLines.push(currentLine);
currentLine = word;
}
}
wrappedLines.push(currentLine);
}
return wrappedLines;
}
export default class WorksheetCanvas { export default class WorksheetCanvas {
sheetWidth: number; sheetWidth: number;
@@ -371,6 +417,7 @@ export default class WorksheetCanvas {
if (style.alignment?.vertical) { if (style.alignment?.vertical) {
verticalAlign = style.alignment.vertical; verticalAlign = style.alignment.vertical;
} }
const wrapText = style.alignment?.wrap_text || false;
const context = this.ctx; const context = this.ctx;
context.font = font; context.font = font;
@@ -496,9 +543,14 @@ export default class WorksheetCanvas {
context.rect(x, y, width, height); context.rect(x, y, width, height);
context.clip(); context.clip();
// Is there any better parameter? // Is there any better to determine the line height?
const lineHeight = fontSize * 1.5; const lineHeight = fontSize * 1.5;
const lines = fullText.split("\n"); const lines = computeWrappedLines(
fullText,
wrapText,
context,
width - padding,
);
const lineCount = lines.length; const lineCount = lines.length;
lines.forEach((text, line) => { lines.forEach((text, line) => {
@@ -682,13 +734,19 @@ export default class WorksheetCanvas {
if (fullText === "") { if (fullText === "") {
continue; continue;
} }
const width = this.getColumnWidth(sheet, column);
const style = this.model.getCellStyle(sheet, row, column); const style = this.model.getCellStyle(sheet, row, column);
const fontSize = style.font.sz; const fontSize = style.font.sz;
const lineHeight = fontSize * 1.5; const lineHeight = fontSize * 1.5;
let font = `${fontSize}px ${defaultCellFontFamily}`; let font = `${fontSize}px ${defaultCellFontFamily}`;
font = style.font.b ? `bold ${font}` : `400 ${font}`; font = style.font.b ? `bold ${font}` : `400 ${font}`;
this.ctx.font = font; this.ctx.font = font;
const lines = fullText.split("\n"); const lines = computeWrappedLines(
fullText,
style.alignment?.wrap_text || false,
this.ctx,
width,
);
const lineCount = lines.length; const lineCount = lines.length;
// This is computed so that the y position of the text is independent of the vertical alignment // This is computed so that the y position of the text is independent of the vertical alignment
const textHeight = (lineCount - 1) * lineHeight + 8 + fontSize; const textHeight = (lineCount - 1) * lineHeight + 8 + fontSize;

View File

@@ -26,6 +26,7 @@
"vertical_align_middle": " Align middle", "vertical_align_middle": " Align middle",
"vertical_align_top": "Align top", "vertical_align_top": "Align top",
"selected_png": "Export Selected area as PNG", "selected_png": "Export Selected area as PNG",
"wrap_text": "Wrap text",
"format_menu": { "format_menu": {
"auto": "Auto", "auto": "Auto",
"number": "Number", "number": "Number",