This is an example that demonstrates the deprecated 'table-document'
DataObject.

We maintain 'table-document' to support migrating legacy document.

The Table-View sample is unused, has no tests, and should be removed.
This commit is contained in:
Daniel Lehenbauer 2024-03-13 11:20:02 -07:00 коммит произвёл GitHub
Родитель 8d526c6f12
Коммит e4c933f3a3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
24 изменённых файлов: 3 добавлений и 1441 удалений

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,14 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
module.exports = {
extends: [
require.resolve("@fluidframework/eslint-config-fluid/minimal-deprecated"),
"prettier",
],
rules: {
"@typescript-eslint/strict-boolean-expressions": "off",
},
};

52
examples/data-objects/table-view/.gitignore поставляемый
Просмотреть файл

@ -1,52 +0,0 @@
# Compiled TypeScript and CSS
dist
lib
# Babel
public/scripts/es5
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
.cache-loader
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
# Typings
typings
# Debug log from npm
npm-debug.log
# Code coverage
nyc
.nyc_output/
# Chart dependencies
**/charts/*.tgz
# Generated modules
intel_modules/
temp_modules/

Просмотреть файл

@ -1,6 +0,0 @@
nyc
*.log
**/*.tsbuildinfo
src/test
dist/test
**/_api-extractor-temp/**

Просмотреть файл

@ -1,81 +0,0 @@
# @fluid-example/table-view
## 2.0.0-rc.2.0.0
Dependency updates only.
## 2.0.0-rc.1.0.0
Dependency updates only.
## 2.0.0-internal.8.0.0
Dependency updates only.
## 2.0.0-internal.7.4.0
Dependency updates only.
## 2.0.0-internal.7.3.0
Dependency updates only.
## 2.0.0-internal.7.2.0
Dependency updates only.
## 2.0.0-internal.7.1.0
Dependency updates only.
## 2.0.0-internal.7.0.0
Dependency updates only.
## 2.0.0-internal.6.4.0
Dependency updates only.
## 2.0.0-internal.6.3.0
Dependency updates only.
## 2.0.0-internal.6.2.0
Dependency updates only.
## 2.0.0-internal.6.1.0
Dependency updates only.
## 2.0.0-internal.6.0.0
Dependency updates only.
## 2.0.0-internal.5.4.0
Dependency updates only.
## 2.0.0-internal.5.3.0
Dependency updates only.
## 2.0.0-internal.5.2.0
Dependency updates only.
## 2.0.0-internal.5.1.0
Dependency updates only.
## 2.0.0-internal.5.0.0
Dependency updates only.
## 2.0.0-internal.4.4.0
Dependency updates only.
## 2.0.0-internal.4.1.0
Dependency updates only.

Просмотреть файл

@ -1,21 +0,0 @@
Copyright (c) Microsoft Corporation and contributors. All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Просмотреть файл

@ -1,34 +0,0 @@
# @fluid-example/table-view
**Table View** is a basic table/grid view built on top of the `@fluid-example/table-document` data object.
Since Table View uses the data model provided by Table Document it only uses it's DDS to store a reference
to the created Table Document.
<!-- AUTO-GENERATED-CONTENT:START (README_EXAMPLE_GETTING_STARTED_SECTION:usesTinylicious=FALSE) -->
<!-- prettier-ignore-start -->
<!-- NOTE: This section is automatically generated using @fluid-tools/markdown-magic. Do not update these generated contents directly. -->
## Getting Started
You can run this example using the following steps:
1. Enable [corepack](https://nodejs.org/docs/latest-v16.x/api/corepack.html) by running `corepack enable`.
1. Run `pnpm install` and `pnpm run build:fast --nolint` from the `FluidFramework` root directory.
- For an even faster build, you can add the package name to the build command, like this:
`pnpm run build:fast --nolint @fluid-example/table-view`
1. Run `pnpm start` from this directory and open <http://localhost:8080> in a web browser to see the app running.
<!-- prettier-ignore-end -->
<!-- AUTO-GENERATED-CONTENT:END -->
## Data model
Table View uses the following distributed data structures:
- SharedDirectory - root
Table View creates the following Fluid objects:
- `@fluid-example/table-document`

Просмотреть файл

@ -1,92 +0,0 @@
{
"name": "@fluid-example/table-view",
"version": "2.0.0-rc.2.0.0",
"private": true,
"description": "Chaincode component that provides a view for a table-document.",
"homepage": "https://fluidframework.com",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/FluidFramework.git",
"directory": "examples/data-objects/table-view"
},
"license": "MIT",
"author": "Microsoft and contributors",
"sideEffects": [
"./src/publicpath.ts"
],
"main": "lib/index.js",
"module": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "fluid-build . --task build",
"build:compile": "fluid-build . --task compile",
"build:copy": "copyfiles -u 1 \"src/**/*.css\" lib/",
"build:esnext": "tsc",
"clean": "rimraf --glob dist lib \"**/*.tsbuildinfo\" \"**/*.build.log\"",
"eslint": "eslint --format stylish src",
"eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout",
"format": "npm run prettier:fix",
"lint": "npm run prettier && npm run eslint",
"lint:fix": "npm run prettier:fix && npm run eslint:fix",
"prepack": "npm run webpack",
"prettier": "prettier --check . --cache --ignore-path ../../../.prettierignore",
"prettier:fix": "prettier --write . --cache --ignore-path ../../../.prettierignore",
"start": "webpack serve --config webpack.config.cjs",
"start:docker": "webpack serve --config webpack.config.cjs --env mode=docker",
"start:r11s": "webpack serve --config webpack.config.cjs --env mode=r11s",
"start:spo": "webpack serve --config webpack.config.cjs --env mode=spo",
"start:spo-df": "webpack serve --config webpack.config.cjs --env mode=spo-df",
"start:tinylicious": "webpack serve --config webpack.config.cjs --env mode=tinylicious",
"webpack": "webpack --env production",
"webpack:dev": "webpack --env development"
},
"dependencies": {
"@fluid-example/example-utils": "workspace:~",
"@fluid-example/table-document": "workspace:~",
"@fluidframework/aqueduct": "workspace:~",
"@fluidframework/container-definitions": "workspace:~",
"@fluidframework/core-interfaces": "workspace:~",
"@fluidframework/matrix": "workspace:~",
"@fluidframework/runtime-utils": "workspace:~",
"@fluidframework/sequence": "workspace:~",
"@tiny-calc/micro": "0.0.0-alpha.5",
"@tiny-calc/nano": "0.0.0-alpha.5",
"react": "^17.0.1"
},
"devDependencies": {
"@fluid-example/webpack-fluid-loader": "workspace:~",
"@fluidframework/build-common": "^2.0.3",
"@fluidframework/build-tools": "^0.34.0",
"@fluidframework/eslint-config-fluid": "^5.1.0",
"@types/node": "^18.19.0",
"@types/react": "^17.0.44",
"copyfiles": "^2.4.1",
"css-loader": "^1.0.0",
"eslint": "~8.55.0",
"prettier": "~3.0.3",
"rimraf": "^4.4.0",
"source-map-loader": "^2.0.0",
"style-loader": "^1.0.0",
"ts-loader": "^9.3.0",
"typescript": "~5.1.6",
"url-loader": "^2.1.0",
"webpack": "^5.82.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "~4.6.0",
"webpack-merge": "^5.8.0"
},
"fluid": {
"browser": {
"umd": {
"files": [
"dist/main.bundle.js"
],
"library": "main"
}
}
},
"typeValidation": {
"disabled": true,
"broken": {}
}
}

Просмотреть файл

@ -1,8 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
module.exports = {
...require("@fluidframework/build-common/prettier.config.cjs"),
};

Просмотреть файл

@ -1,70 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
const enum StyleIndex {
Near = 0,
Middle = 1,
Far = 2,
}
export class BorderRect {
public get min() {
return [Math.min(this.start[0], this.end[0]), Math.min(this.start[1], this.end[1])];
}
public get max() {
return [Math.max(this.start[0], this.end[0]), Math.max(this.start[1], this.end[1])];
}
public start = [NaN, NaN];
public end = [NaN, NaN];
constructor(private readonly styles: string[][]) {}
public reset() {
this.start = [NaN, NaN];
this.end = [NaN, NaN];
}
public intersect(row: number, col: number) {
const min = this.min;
const max = this.max;
return this.inRange(min[0], row, max[0]) && this.inRange(min[1], col, max[1]);
}
public getStyle(row: number, col: number) {
if (!this.intersect(row, col)) {
return "";
}
const min = this.min;
const max = this.max;
const vert = this.getStyleIndices(min[0], row, max[0]);
const horiz = this.getStyleIndices(min[1], col, max[1]);
return vert.reduce((vertAccum, vertIndex) => {
const vertStyles = this.styles[vertIndex];
return horiz.reduce(
(horizAccum, horizIndex) => `${horizAccum} ${vertStyles[horizIndex]}`,
vertAccum,
);
}, "");
}
private inRange(min: number, value: number, max: number) {
return min <= value && value <= max;
}
private getStyleIndices(min: number, value: number, max: number) {
const styles: number[] = [];
if (value === min) {
styles.push(StyleIndex.Near);
}
if (value === max) {
styles.push(StyleIndex.Far);
}
if (styles.length === 0) {
styles.push(StyleIndex.Middle);
}
return styles;
}
}

Просмотреть файл

@ -1,6 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
declare let __webpack_public_path__: string;

Просмотреть файл

@ -1,525 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { colIndexToName } from "@fluid-example/table-document";
import { SharedMatrix } from "@fluidframework/matrix";
import { ISheetlet, createSheetletProducer } from "@tiny-calc/micro";
import type { IMatrixProducer } from "@tiny-calc/nano";
import { BorderRect } from "./borderstyle.js";
import * as styles from "./index.css";
// eslint-disable-next-line unicorn/no-unsafe-regex
const numberExp = /^[+-]?\d*\.?\d+(?:[Ee][+-]?\d+)?$/;
const enum KeyCode {
tab = "Tab", // 9
enter = "Enter", // 13
escape = "Escape", // 27
arrowLeft = "ArrowLeft", // 37
arrowUp = "ArrowUp", // 38
arrowRight = "ArrowRight", // 39
arrowDown = "ArrowDown", // 40
}
// Extract Value type from createSheetletProducer requirements. (Value is not exported.)
type GridContentType = Parameters<typeof createSheetletProducer>[0] extends IMatrixProducer<infer T>
? T
: never;
export class GridView {
private get numRows() {
return this.matrix.rowCount;
}
private get numCols() {
return this.matrix.colCount;
}
public readonly root;
private _startRow = 0;
public get startRow() {
return this._startRow;
}
public set startRow(value: number) {
this._startRow = value;
this.cancelInput();
this.refreshCells();
}
private readonly cols = document.createElement("tr");
private readonly tbody = document.createElement("tbody");
private readonly inputBox = document.createElement("input");
private tdText?: Node;
private readonly selection = new BorderRect([
[`${styles.selectedTL}`, `${styles.selectedT}`, `${styles.selectedTR}`],
[`${styles.selectedL}`, `${styles.selected}`, `${styles.selectedR}`],
[`${styles.selectedBL}`, `${styles.selectedB}`, `${styles.selectedBR}`],
]);
private readonly maxRows = 10;
private readonly sheetlet: ISheetlet;
private generateDom() {
const root = document.createElement("table");
root.classList.add(styles.view);
root.tabIndex = 0;
const caption = document.createElement("caption");
const captionSpan = document.createElement("span");
captionSpan.textContent = "Table";
caption.append(captionSpan);
const head = document.createElement("thead");
head.append(this.cols);
root.append(caption, head, this.tbody);
return root;
}
constructor(
private readonly matrix: SharedMatrix<GridContentType>,
private readonly getFormula: () => string,
private readonly setFormula: (val: string) => void,
private readonly setSelectionSummary: (val: string) => void,
) {
this.root = this.generateDom();
this.root.addEventListener("click", this.onGridClick as EventListener);
this.tbody.addEventListener("pointerdown", this.cellPointerDown as EventListener);
this.tbody.addEventListener("pointermove", this.cellPointerMove as EventListener);
this.inputBox.classList.add(styles.inputBox);
this.inputBox.addEventListener("keydown", this.cellKeyDown);
this.inputBox.addEventListener("input", this.cellInput);
const blank = document.createElement("th");
this.cols.appendChild(blank);
this.sheetlet = createSheetletProducer(matrix);
this.setupMatrixConsumer();
this.refreshCells();
}
private setupMatrixConsumer() {
let scheduled = false;
const scheduleGridRefresh = () => {
if (scheduled) {
return;
}
requestAnimationFrame(() => {
scheduled = false;
this.refreshCells();
});
scheduled = true;
};
const invalidateCells = (
rowStart: number,
colStart: number,
rowCount: number,
colCount: number,
) => {
for (let row = rowStart; row < rowStart + rowCount; row++) {
for (let col = colStart; col < colStart + colCount; col++) {
this.sheetlet.invalidate(row, col);
}
}
scheduleGridRefresh();
};
const matrixReader = {
rowsChanged() {
scheduleGridRefresh();
},
colsChanged() {
scheduleGridRefresh();
},
cellsChanged(rowStart: number, colStart: number, rowCount: number, colCount: number) {
invalidateCells(rowStart, colStart, rowCount, colCount);
},
};
this.matrix.openMatrix(matrixReader);
}
private refreshCell(td: HTMLTableCellElement, row: number, col: number) {
const className = this.selection.getStyle(row, col);
if (td.className !== className) {
td.className = className;
}
// While the cell is being edited, we use the <td>'s content to size the table to the
// formula. Don't synchronize it now.
if (this.inputBox.parentElement !== td) {
const value = this.sheetlet.evaluateCell(row, col);
const text = `\u200B${value ?? ""}`;
if (td.textContent !== text) {
td.textContent = text;
}
}
}
private readonly refreshCells = () => {
let row = this.startRow;
const numRows = Math.min(this.numRows, row + this.maxRows);
{
let tr = this.tbody.firstElementChild;
while (tr) {
const next = tr.nextElementSibling;
if (row < numRows) {
let col = -1;
for (const td of tr.children) {
if (col < 0) {
td.textContent = `${row + 1}`;
} else {
this.refreshCell(td as HTMLTableCellElement, row, col);
}
col++;
}
// Append any missing columns
for (; col < this.numCols; col++) {
const td = document.createElement("td");
this.refreshCell(td, row, col);
tr.appendChild(td);
}
} else {
tr.remove();
}
tr = next;
row++;
}
}
// Append any missing rows
for (; row < numRows; row++) {
const tr = document.createElement("tr");
const th = document.createElement("th");
th.textContent = `${row + 1}`;
tr.appendChild(th);
for (let col = 0; col < this.numCols; col++) {
const td = document.createElement("td");
this.refreshCell(td, row, col);
tr.appendChild(td);
}
this.tbody.appendChild(tr);
}
// Append any missing col headers
for (let col = this.cols.childElementCount - 1; col < this.numCols; col++) {
const th = document.createElement("th");
// Skip placeholder <th> above the row number column.
if (col >= 0) {
th.textContent = `${colIndexToName(col)}`;
}
this.cols.append(th);
}
this.refreshFormulaInput();
this.refreshNumberSummary();
};
private readonly onGridClick = (e: MouseEvent) => {
const maybeTd = this.getCellFromEvent(e);
if (maybeTd) {
const [row, col] = this.getRowColFromTd(maybeTd);
if (row < 0 && col >= 0) {
this.selection.start = [0, col];
this.selection.end = [this.numRows - 1, col];
this.refreshCells();
} else if (col < 0 && row >= 0) {
this.selection.start = [row, 0];
this.selection.end = [row, this.numCols - 1];
this.refreshCells();
} else if (col >= 0) {
this.moveInputToPosition(row, col, e.shiftKey);
}
}
};
private readonly cellPointerDown = (e: PointerEvent) => {
const maybeTd = this.getCellFromEvent(e);
if (maybeTd) {
this.commitInput();
const [row, col] = this.getRowColFromTd(maybeTd);
if (col >= 0) {
this.selection.start = this.selection.end = [row, col];
this.refreshCells();
}
}
};
private readonly cellPointerMove = (e: PointerEvent) => {
if (!e.buttons) {
return;
}
const maybeTd = this.getCellFromEvent(e);
if (maybeTd) {
const [row, col] = this.getRowColFromTd(maybeTd);
if (col >= 0) {
this.commitInput();
this.inputBox.remove();
this.selection.end = [row, col];
this.refreshCells();
}
}
};
private readonly cancelInput = () => {
const maybeParent = this.inputBox.parentElement as HTMLTableCellElement | null;
if (maybeParent) {
this.inputBox.remove();
const [row, col] = this.getRowColFromTd(maybeParent);
this.refreshCell(maybeParent, row, col);
}
};
private parseInput(input: string) {
if (numberExp.exec(input)) {
const asNumber = Number(input);
if (!isNaN(asNumber)) {
return asNumber;
}
}
return input;
}
private commitInput() {
const maybeParent = this.inputBox.parentElement as HTMLTableCellElement | null;
if (maybeParent) {
const [row, col] = this.getRowColFromTd(maybeParent);
const previous = this.matrix.getCell(row, col);
const current = this.parseInput(this.inputBox.value);
if (previous !== current) {
this.matrix.setCell(row, col, current);
this.sheetlet.invalidate(row, col);
}
this.refreshCell(maybeParent, row, col);
}
}
private moveInputToPosition(row: number, col: number, extendSelection: boolean) {
const newParent = this.getTdFromRowCol(row, col);
if (newParent) {
this.commitInput();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.tdText = newParent.firstChild!;
console.assert(
this.tdText.nodeType === Node.TEXT_NODE,
"TableData text has wrong node type!",
);
const value = this.matrix.getCell(row, col);
this.inputBox.value = `${value ?? ""}`;
newParent.appendChild(this.inputBox);
this.cellInput();
this.tdText.textContent = `\u200B${this.inputBox.value}`;
this.inputBox.focus();
this.selection.end = [row, col];
if (!extendSelection) {
this.selection.start = this.selection.end;
}
this.refreshCells();
}
// 'getTdFromRowCol(..)' return false if row/col are outside the sheet range.
return !!newParent;
}
private moveInputByOffset(e: KeyboardEvent, rowOffset: number, colOffset: number) {
let _colOffset = colOffset;
// Allow the left/right arrow keys to move the caret inside the inputBox until the caret
// is in the first/last character position. Then move the inputBox.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (e.target === this.inputBox && this.inputBox.selectionStart! >= 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const x = this.inputBox.selectionStart! + _colOffset;
if (0 <= x && x <= this.inputBox.value.length) {
_colOffset = 0;
if (rowOffset === 0) {
return;
}
}
}
// If we're moving 'inputBox' prevent the arrow keys from moving the caret. If we don't do this,
// our 'setSelectionRange()' below will appear off-by-one, and up/down in the top/bottom cells
// will behave like home/end respectively.
e.preventDefault();
const parent = this.inputBox.parentElement as HTMLTableCellElement;
const [row, col] = this.getRowColFromTd(parent);
if (this.moveInputToPosition(row + rowOffset, col + colOffset, e.shiftKey)) {
// If we moved horizontally, move the caret to the beginning/end of the input as appropriate.
const caretPosition = colOffset > 0 ? 0 : this.inputBox.value.length;
this.inputBox.setSelectionRange(caretPosition, caretPosition);
}
}
private readonly cellInput = () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.tdText!.textContent = `\u200B${this.inputBox.value}`;
this.refreshFormulaInput();
};
private readonly cellKeyDown = (e: KeyboardEvent) => {
switch (e.code) {
case KeyCode.escape: {
this.cancelInput();
break;
}
case KeyCode.arrowUp: {
this.moveInputByOffset(e, /* rowOffset: */ -1, /* colOffset */ 0);
break;
}
case KeyCode.enter: {
this.commitInput(); /* fall-through */
}
case KeyCode.arrowDown: {
this.moveInputByOffset(e, /* rowOffset: */ 1, /* colOffset */ 0);
break;
}
case KeyCode.arrowLeft: {
this.moveInputByOffset(e, /* rowOffset: */ 0, /* colOffset */ -1);
break;
}
case KeyCode.tab: {
e.preventDefault(); /* fall-through */
}
case KeyCode.arrowRight: {
this.moveInputByOffset(e, /* rowOffset: */ 0, /* colOffset */ 1);
}
default:
break;
}
};
public readonly formulaKeypress = (e: KeyboardEvent) => {
if (e.code === KeyCode.enter) {
this.updateSelectionFromFormulaInput();
}
};
public readonly formulaFocusOut = () => {
this.updateSelectionFromFormulaInput();
};
private getCellFromEvent(e: Event) {
const target = e.target as HTMLElement;
return target.nodeName === "TD" || target.nodeName === "TH"
? (target as HTMLTableCellElement)
: undefined;
}
// Map the given 'id' string in the from 'row,col' to an array of 2 integers [row, col].
private getRowColFromTd(td: HTMLTableDataCellElement) {
const colOffset = td.cellIndex;
const rowOffset = (td.parentElement as HTMLTableRowElement).rowIndex;
// The '-1' are to account for Row/Columns headings. Note that even though the column
// headings our outside the body, it still impacts their cellIndex.
return [this.startRow + rowOffset - 1, colOffset - 1];
}
private getTdFromRowCol(row: number, col: number) {
let _row = row;
_row -= this.startRow;
// Column heading are outside the <tbody> in <thead>, and therefore we do not need
// to make adjustments when indexing into children.
const rows = this.tbody.children;
if (_row < 0 || _row >= rows.length) {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cols = rows.item(_row)!.children;
// Row headings are inside the <tbody>, therefore we need to adjust our column
// index by +/-1 to skip them.
return 0 <= col && col < cols.length - 1 && cols.item(col + 1);
}
private refreshFormulaInput() {
if (this.selection.start === this.selection.end) {
const [row, col] = this.selection.start;
// The formula bar should always show raw values, but when a cell is
// selected for edit it will be showing the raw value
const cellValue = this.matrix.getCell(row, col);
this.setFormula(`${cellValue ?? ""}`);
} else {
this.setFormula("<multiple selection>");
}
}
private updateSelectionFromFormulaInput() {
// Don't handle multiple selection yet
if (this.selection.start === this.selection.end) {
const [row, col] = this.selection.start;
const selectedCell = this.getTdFromRowCol(row, col) as HTMLTableDataCellElement;
if (selectedCell) {
const previous = this.matrix.getCell(row, col);
const current = this.parseInput(this.getFormula());
if (previous !== current) {
selectedCell.textContent = `\u200B${current}`;
this.matrix.setCell(row, col, current);
this.sheetlet.invalidate(row, col);
}
}
}
}
private refreshNumberSummary() {
const [rowStart, colStart] = this.selection.start;
const [rowEnd, colEnd] = this.selection.end;
const colStartLetter = this.numberToColumnLetter(colStart);
const colEndLetter = this.numberToColumnLetter(colEnd);
const averageFormula = `=AVERAGE(${colStartLetter}${rowStart + 1}:${colEndLetter}${
rowEnd + 1
})`;
const countFormula = `=COUNT(${colStartLetter}${rowStart + 1}:${colEndLetter}${
rowEnd + 1
})`;
const sumFormula = `=SUM(${colStartLetter}${rowStart + 1}:${colEndLetter}${rowEnd + 1})`;
const avg = this.sheetlet.evaluateFormula(averageFormula);
const count = this.sheetlet.evaluateFormula(countFormula);
const sum = this.sheetlet.evaluateFormula(sumFormula);
if ((count as number) > 1) {
this.setSelectionSummary(`Average:${avg} Count:${count} Sum:${sum}`);
} else {
this.setSelectionSummary("\u200B");
}
}
private numberToColumnLetter(index: number): string {
let _index = index;
let colString = String.fromCharCode((_index % 26) + 65);
_index = _index / 26;
while (_index >= 1) {
colString = String.fromCharCode((_index % 26) + 64) + colString;
_index = _index / 26;
}
return colString;
}
}

Просмотреть файл

@ -1,124 +0,0 @@
*,
*:before,
*:after {
box-sizing: border-box;
}
.view {
background-color: transparent;
}
table.view {
font: normal 11pt sanserif;
margin: auto;
border-collapse: separate;
border-spacing: 0px;
--select-border: ridge thin #88f;
user-select: none;
}
table.view tbody tr:nth-child(odd) td {
background: rgba(0, 0, 0, 0.05);
}
table.view:focus {
outline: none;
}
table.view:focus-within tfoot tr {
visibility: visible;
transition-duration: 0.5s;
}
table.view th {
font: normal 8pt sanserif;
border: solid transparent thin;
margin: 0px;
padding: 0px 4px;
text-align: center;
opacity: 0;
transition-duration: 0.5s;
}
table.view:focus-within th {
opacity: 1;
border: solid lightgray thin;
transition-duration: 0.5s;
}
table.view:focus-within table {
background-color: white;
transition-duration: 0.5s;
}
table.view td {
position: relative;
padding: 4px;
border: solid lightgray thin;
white-space: pre;
min-width: 63px;
}
.inputBox {
position: absolute;
padding: 4px;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: transparent;
border: none;
font: normal 11pt sanserif;
width: 100%;
border: 0px;
}
table.view caption {
text-align: center;
padding: 4px 0px 0px 0px;
}
/* Note: Must use 'table.view td.' in selector to override default style (more specific styles win). */
/* Note: Border must be at least 2px to avoid gaps. */
table.view:focus-within td.selectedTL,
table.view:focus-within td.selectedT,
table.view:focus-within td.selectedTR {
border-top: var(--select-border);
}
table.view:focus-within td.selectedTL,
table.view:focus-within td.selectedL,
table.view:focus-within td.selectedBL {
border-left: var(--select-border);
}
table.view:focus-within td.selectedTR,
table.view:focus-within td.selectedR,
table.view:focus-within td.selectedBR {
border-right: var(--select-border);
}
table.view:focus-within td.selectedBL,
table.view:focus-within td.selectedB,
table.view:focus-within td.selectedBR {
border-bottom: var(--select-border);
}
table.view:focus-within td.selectedTL,
table.view:focus-within td.selectedT,
table.view:focus-within td.selectedTR,
table.view:focus-within td.selectedL,
table.view:focus-within td.selected,
table.view:focus-within td.selectedR,
table.view:focus-within td.selectedBL,
table.view:focus-within td.selectedB,
table.view:focus-within td.selectedBR {
background-color: rgba(17, 102, 238, 0.1);
}
.grid {
max-width: 100%;
max-height: 50%;
padding: 0px 0px 8px 0px;
overflow: auto;
}

Просмотреть файл

@ -1,17 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
export const grid: string;
export const view: string;
export const inputBox: string;
export const selectedTL: string;
export const selectedT: string;
export const selectedTR: string;
export const selectedL: string;
export const selectedBL: string;
export const selectedR: string;
export const selectedBR: string;
export const selectedB: string;
export const selected: string;

Просмотреть файл

@ -1,11 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
// set the base path for all dynamic imports first
// eslint-disable-next-line import/no-unassigned-import
import "./publicpath.js";
export { fluidExport } from "./runtime.js";
export { TableModel, tableModelType } from "./tableModel.js";

Просмотреть файл

@ -1,11 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
// We assume the current script runs at the base path. Simply extract out its filename and then use that path
// as the base
const base = (document.currentScript as HTMLScriptElement).src;
__webpack_public_path__ = base.substr(0, base.lastIndexOf("/") + 1);
export {};

Просмотреть файл

@ -1,27 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { ContainerViewRuntimeFactory } from "@fluid-example/example-utils";
import { createDataStoreFactory } from "@fluidframework/runtime-utils";
import React from "react";
import { TableModel, tableModelType } from "./tableModel.js";
import { TableView } from "./tableView.js";
const tableModelFactory = createDataStoreFactory(
tableModelType,
import(/* webpackChunkName: "table-view", webpackPreload: true */ "./tableModel").then((m) =>
m.TableModel.getFactory(),
),
);
const tableViewCallback = (model: TableModel) => React.createElement(TableView, { model });
/**
* This does setup for the Container. The ContainerViewRuntimeFactory will instantiate a single Fluid object to use
* as our model (using the factory we provide), and the view callback we provide will pair that model with an
* appropriate view.
* @internal
*/
export const fluidExport = new ContainerViewRuntimeFactory(tableModelFactory, tableViewCallback);

Просмотреть файл

@ -1,45 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { SharedMatrix } from "@fluidframework/matrix";
import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct";
import { IFluidHandle } from "@fluidframework/core-interfaces";
/**
* @internal
*/
export const tableModelType = "@fluid-example/table-view";
const matrixKey = "matrixKey";
/**
* @internal
*/
export class TableModel extends DataObject {
public static getFactory() {
return factory;
}
private _tableMatrix: SharedMatrix | undefined;
public get tableMatrix() {
if (this._tableMatrix === undefined) {
throw new Error("Table matrix not fully initialized");
}
return this._tableMatrix;
}
protected async initializingFirstTime() {
const matrix = SharedMatrix.create(this.runtime);
this.root.set(matrixKey, matrix.handle);
matrix.insertRows(0, 5);
matrix.insertCols(0, 8);
}
protected async hasInitialized(): Promise<void> {
this._tableMatrix = await this.root.get<IFluidHandle<SharedMatrix>>(matrixKey)?.get();
}
}
const factory = new DataObjectFactory(tableModelType, TableModel, [SharedMatrix.getFactory()], {});

Просмотреть файл

@ -1,89 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import React, { useEffect, useRef } from "react";
import { GridView } from "./grid.js";
import * as styles from "./index.css";
import { TableModel } from "./tableModel.js";
interface ITableViewProps {
model: TableModel;
}
export const TableView: React.FC<ITableViewProps> = (props: ITableViewProps) => {
const { model } = props;
const formulaInputRef = useRef<HTMLInputElement>(null);
const selectionSummarySpanRef = useRef<HTMLSpanElement>(null);
const goToInputRef = useRef<HTMLInputElement>(null);
const gridRootRef = useRef<HTMLDivElement>(null);
const gridView = useRef<GridView>();
const getFormula = () => formulaInputRef.current?.value ?? "";
const setFormula = (val: string) => {
if (formulaInputRef.current !== null) {
formulaInputRef.current.value = val;
}
};
const setSelectionSummary = (val: string) => {
if (selectionSummarySpanRef.current !== null) {
selectionSummarySpanRef.current.textContent = val;
}
};
useEffect(() => {
gridView.current = new GridView(
model.tableMatrix,
getFormula,
setFormula,
setSelectionSummary,
);
if (gridRootRef.current !== null) {
while (gridRootRef.current.firstChild !== null) {
gridRootRef.current.removeChild(gridRootRef.current.firstChild);
}
gridRootRef.current.append(gridView.current.root);
}
}, [model]);
const executeGoTo = () => {
if (gridView.current !== undefined && goToInputRef.current !== null) {
gridView.current.startRow = parseInt(goToInputRef.current.value, 10) - 1;
}
};
return (
<div>
<button onClick={() => model.tableMatrix.insertRows(model.tableMatrix.rowCount, 1)}>
R+
</button>
<button onClick={() => model.tableMatrix.insertCols(model.tableMatrix.colCount, 1)}>
C+
</button>
<button onClick={() => model.tableMatrix.insertRows(model.tableMatrix.rowCount, 10)}>
R++
</button>
<button onClick={() => model.tableMatrix.insertCols(model.tableMatrix.colCount, 10)}>
C++
</button>
<div>
<input
type="text"
ref={formulaInputRef}
onKeyPress={(e) => {
gridView.current?.formulaKeypress(e.nativeEvent);
}}
onBlur={() => {
gridView.current?.formulaFocusOut();
}}
placeholder="Formula input"
/>
<div ref={gridRootRef} className={styles.grid}></div>
<span ref={selectionSummarySpanRef}></span>
</div>
<input type="text" ref={goToInputRef} onChange={executeGoTo} />
</div>
);
};

Просмотреть файл

@ -1,11 +0,0 @@
{
"extends": "@fluidframework/build-common/ts-common-config.json",
"compilerOptions": {
"module": "esnext",
"jsx": "react",
"rootDir": "./src",
"outDir": "lib",
"types": ["react"],
},
"include": ["src/**/*"],
}

Просмотреть файл

@ -1,105 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
const fluidRoute = require("@fluid-example/webpack-fluid-loader");
const path = require("path");
const { merge } = require("webpack-merge");
const webpack = require("webpack");
module.exports = (env) => {
const isProduction = env?.production;
const styleLocalIdentName = isProduction ? "[hash:base64:5]" : "[local]-[hash:base64:5]";
return merge(
{
entry: "./src/index.ts",
resolve: {
extensionAlias: {
".js": [".ts", ".tsx", ".js", ".cjs", ".mjs"],
},
extensions: [".mjs", ".ts", ".tsx", ".js"],
fallback: {
dgram: false,
fs: false,
net: false,
tls: false,
child_process: false,
},
},
devtool: "source-map",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: "ts-loader",
options: {
compilerOptions: {
module: "esnext",
},
},
},
],
exclude: /node_modules/,
},
{
test: /\.m?js$/,
use: [require.resolve("source-map-loader")],
enforce: "pre",
},
{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true,
localIdentName: styleLocalIdentName,
},
},
],
},
{
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
loader: "url-loader",
options: {
limit: 10000,
},
},
{
test: /\.html$/,
loader: "html-loader",
},
],
},
output: {
filename: "[name].bundle.js",
chunkFilename: "[name].async.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/dist/",
library: "[name]",
libraryTarget: "umd",
globalObject: "self",
},
plugins: [
new webpack.ProvidePlugin({
process: "process/browser",
}),
],
// This impacts which files are watched by the dev server (and likely by webpack if watch is true).
// This should be configurable under devServer.static.watch
// (see https://github.com/webpack/webpack-dev-server/blob/master/migration-v4.md) but that does not seem to work.
// The CLI options for disabling watching don't seem to work either, so this may be a symptom of using webpack4 with the newer webpack-cli and webpack-dev-server.
watchOptions: {
ignored: "**/node_modules/**",
},
},
isProduction ? require("./webpack.prod.cjs") : require("./webpack.dev.cjs"),
fluidRoute.devServerConfig(__dirname, env),
);
};

Просмотреть файл

@ -1,9 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
module.exports = {
mode: "development",
devtool: "inline-source-map",
};

Просмотреть файл

@ -1,14 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
// const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
mode: "production",
devtool: "source-map",
// plugins: [
// new BundleAnalyzerPlugin()
// ]
};

Просмотреть файл

@ -2637,73 +2637,6 @@ importers:
rimraf: 4.4.1
typescript: 5.1.6
examples/data-objects/table-view:
specifiers:
'@fluid-example/example-utils': workspace:~
'@fluid-example/table-document': workspace:~
'@fluid-example/webpack-fluid-loader': workspace:~
'@fluidframework/aqueduct': workspace:~
'@fluidframework/build-common': ^2.0.3
'@fluidframework/build-tools': ^0.34.0
'@fluidframework/container-definitions': workspace:~
'@fluidframework/core-interfaces': workspace:~
'@fluidframework/eslint-config-fluid': ^5.1.0
'@fluidframework/matrix': workspace:~
'@fluidframework/runtime-utils': workspace:~
'@fluidframework/sequence': workspace:~
'@tiny-calc/micro': 0.0.0-alpha.5
'@tiny-calc/nano': 0.0.0-alpha.5
'@types/node': ^18.19.0
'@types/react': ^17.0.44
copyfiles: ^2.4.1
css-loader: ^1.0.0
eslint: ~8.55.0
prettier: ~3.0.3
react: ^17.0.1
rimraf: ^4.4.0
source-map-loader: ^2.0.0
style-loader: ^1.0.0
ts-loader: ^9.3.0
typescript: ~5.1.6
url-loader: ^2.1.0
webpack: ^5.82.0
webpack-cli: ^4.9.2
webpack-dev-server: ~4.6.0
webpack-merge: ^5.8.0
dependencies:
'@fluid-example/example-utils': link:../../utils/example-utils
'@fluid-example/table-document': link:../table-document
'@fluidframework/aqueduct': link:../../../packages/framework/aqueduct
'@fluidframework/container-definitions': link:../../../packages/common/container-definitions
'@fluidframework/core-interfaces': link:../../../packages/common/core-interfaces
'@fluidframework/matrix': link:../../../packages/dds/matrix
'@fluidframework/runtime-utils': link:../../../packages/runtime/runtime-utils
'@fluidframework/sequence': link:../../../packages/dds/sequence
'@tiny-calc/micro': 0.0.0-alpha.5
'@tiny-calc/nano': 0.0.0-alpha.5
react: 17.0.2
devDependencies:
'@fluid-example/webpack-fluid-loader': link:../../utils/webpack-fluid-loader
'@fluidframework/build-common': 2.0.3
'@fluidframework/build-tools': 0.34.0_h467wi3sy6j67ifywrn7x7qf4m
'@fluidframework/eslint-config-fluid': 5.1.0_bpztyfltmpuv6lhsgzfwtmxhte
'@types/node': 18.19.1
'@types/react': 17.0.71
copyfiles: 2.4.1
css-loader: 1.0.1_webpack@5.89.0
eslint: 8.55.0
prettier: 3.0.3
rimraf: 4.4.1
source-map-loader: 2.0.2_webpack@5.89.0
style-loader: 1.3.0_webpack@5.89.0
ts-loader: 9.5.1_535b6rdv6xzubhvm4whegmtnju
typescript: 5.1.6
url-loader: 2.3.0_webpack@5.89.0
webpack: 5.89.0_webpack-cli@4.10.0
webpack-cli: 4.10.0_o3vqr5s7sd4b3hfy35jxofofzu
webpack-dev-server: 4.6.0_xufw7vsm4hgw2efzchq5tzqpge
webpack-merge: 5.10.0
examples/data-objects/todo:
specifiers:
'@fluid-example/example-utils': workspace:~
@ -21510,6 +21443,7 @@ packages:
resolution: {integrity: sha512-ldmINj6PDn5SaeIT9STO/P1O2rhPZjSkIoWUaXjfuBAEcE5bflwh5+WTFShofJcpnaCVpGannXuDS0+qn6Ctfg==}
dependencies:
'@tiny-calc/nano': 0.0.0-alpha.5
dev: true
/@tiny-calc/nano/0.0.0-alpha.5:
resolution: {integrity: sha512-Hs37tz9ZtvK21/5s4tjt5RBa/PFHKYS0AzvdxiXuSd3+AKQN2ygxw7uwD9j0DIG9qONddg1vIASO77JIGyZzyw==}