From c39ae71c0fc98b8227b77b444dc2293cb463d58b Mon Sep 17 00:00:00 2001 From: Bill Dwyer Date: Tue, 8 Nov 2016 17:10:37 -0800 Subject: [PATCH] Blueprint Table (#30) * Blueprint Table Blueprint Table is a highly configurable UI component that provides standard UX for presenting tabular data. It is built using [React](https://facebook.github.io/react/) but usable with any front end framework. **Features** - Fixed column headers and row indices columns - Resizable columns and rows - Double-click column resize handles for auto-sizing - Viewport-only rendering for scale (a.k.a. _lazy_ rendering) - Robust selections (columns, rows, or multiple cell regions) - Right-click to copy cell region contents - Inline editable column headers & cells - Unit test covered - Documented - Feature gallery --- CONTRIBUTING.md | 156 ++++ Gulpfile.js | 11 +- circle.yml | 4 +- package.json | 3 + packages/docs/package.json | 1 + packages/docs/src/common/resolveExample.ts | 2 + packages/docs/src/docs.scss | 1 + packages/table/README.md | 21 + .../table/config/webpack.config.preview.js | 53 ++ packages/table/examples/index.ts | 8 + packages/table/examples/sumo.json | 752 +++++++++++++++++ .../table/examples/tableDollarExample.tsx | 19 + .../table/examples/tableEditableExample.tsx | 116 +++ .../table/examples/tableSortableExample.tsx | 228 ++++++ packages/table/package.json | 57 ++ packages/table/preview/index.html | 135 ++++ packages/table/preview/index.tsx | 468 +++++++++++ packages/table/preview/nav.tsx | 36 + packages/table/preview/perf.html | 30 + packages/table/preview/perf.tsx | 178 ++++ packages/table/preview/require-shim.d.ts | 7 + packages/table/preview/store.ts | 121 +++ packages/table/src/_docs.scss | 224 ++++++ packages/table/src/cell/_borders.scss | 105 +++ packages/table/src/cell/_cell.scss | 42 + packages/table/src/cell/_common.scss | 62 ++ packages/table/src/cell/cell.tsx | 35 + packages/table/src/cell/editableCell.tsx | 83 ++ packages/table/src/cell/formats/_formats.scss | 66 ++ .../table/src/cell/formats/jsonFormat.tsx | 50 ++ .../src/cell/formats/truncatedFormat.tsx | 78 ++ packages/table/src/column.tsx | 38 + packages/table/src/common/_variables.scss | 51 ++ packages/table/src/common/clipboard.ts | 96 +++ packages/table/src/common/grid.ts | 405 ++++++++++ packages/table/src/common/index.ts | 10 + packages/table/src/common/rect.ts | 109 +++ packages/table/src/common/roundSize.tsx | 52 ++ packages/table/src/common/utils.ts | 199 +++++ packages/table/src/headers/_common.scss | 19 + packages/table/src/headers/_headers.scss | 256 ++++++ packages/table/src/headers/columnHeader.tsx | 197 +++++ .../table/src/headers/columnHeaderCell.tsx | 214 +++++ packages/table/src/headers/editableName.tsx | 54 ++ packages/table/src/headers/rowHeader.tsx | 191 +++++ packages/table/src/headers/rowHeaderCell.tsx | 79 ++ packages/table/src/index.ts | 84 ++ .../table/src/interactions/_interactions.scss | 90 +++ packages/table/src/interactions/draggable.tsx | 291 +++++++ packages/table/src/interactions/menus.tsx | 110 +++ packages/table/src/interactions/resizable.tsx | 167 ++++ .../table/src/interactions/resizeHandle.tsx | 138 ++++ .../table/src/interactions/resizeSensor.ts | 93 +++ .../table/src/interactions/selectable.tsx | 138 ++++ packages/table/src/layers/_layers.scss | 58 ++ packages/table/src/layers/guides.tsx | 52 ++ packages/table/src/layers/regions.tsx | 52 ++ packages/table/src/locator.ts | 122 +++ packages/table/src/regions.ts | 501 ++++++++++++ packages/table/src/table.scss | 152 ++++ packages/table/src/table.tsx | 759 ++++++++++++++++++ packages/table/src/tableBody.tsx | 177 ++++ packages/table/test/cellTests.tsx | 43 + packages/table/test/clipboardTests.ts | 25 + packages/table/test/columnHeaderCellTests.tsx | 91 +++ packages/table/test/columnTests.tsx | 52 ++ packages/table/test/editableCellTests.tsx | 59 ++ packages/table/test/editableNameTests.tsx | 53 ++ packages/table/test/formatsTests.tsx | 114 +++ packages/table/test/gridTests.ts | 180 +++++ packages/table/test/guidesTests.tsx | 49 ++ packages/table/test/harness.ts | 116 +++ packages/table/test/index.ts | 27 + packages/table/test/locatorTests.tsx | 60 ++ packages/table/test/menusTests.tsx | 74 ++ packages/table/test/mocks/table.tsx | 54 ++ packages/table/test/rectTests.ts | 51 ++ packages/table/test/regionsTests.ts | 161 ++++ packages/table/test/resizableTests.tsx | 119 +++ packages/table/test/roundSizeTests.tsx | 47 ++ packages/table/test/selectableTests.tsx | 214 +++++ packages/table/test/selectionTests.tsx | 154 ++++ packages/table/test/tableBodyTests.tsx | 22 + packages/table/test/tableTests.tsx | 160 ++++ packages/table/test/tsconfig.json | 23 + packages/table/test/utilsTests.ts | 128 +++ packages/table/tsconfig.json | 25 + packages/table/typings.json | 7 + .../table/typings/globals/es6-shim/index.d.ts | 666 +++++++++++++++ .../typings/globals/es6-shim/typings.json | 8 + packages/table/typings/tsd.d.ts | 1 + scripts/docsDist.sh | 3 +- scripts/preview.sh | 13 +- 93 files changed, 10900 insertions(+), 5 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 packages/table/README.md create mode 100644 packages/table/config/webpack.config.preview.js create mode 100644 packages/table/examples/index.ts create mode 100644 packages/table/examples/sumo.json create mode 100644 packages/table/examples/tableDollarExample.tsx create mode 100644 packages/table/examples/tableEditableExample.tsx create mode 100644 packages/table/examples/tableSortableExample.tsx create mode 100644 packages/table/package.json create mode 100644 packages/table/preview/index.html create mode 100644 packages/table/preview/index.tsx create mode 100644 packages/table/preview/nav.tsx create mode 100644 packages/table/preview/perf.html create mode 100644 packages/table/preview/perf.tsx create mode 100644 packages/table/preview/require-shim.d.ts create mode 100644 packages/table/preview/store.ts create mode 100644 packages/table/src/_docs.scss create mode 100644 packages/table/src/cell/_borders.scss create mode 100644 packages/table/src/cell/_cell.scss create mode 100644 packages/table/src/cell/_common.scss create mode 100644 packages/table/src/cell/cell.tsx create mode 100644 packages/table/src/cell/editableCell.tsx create mode 100644 packages/table/src/cell/formats/_formats.scss create mode 100644 packages/table/src/cell/formats/jsonFormat.tsx create mode 100644 packages/table/src/cell/formats/truncatedFormat.tsx create mode 100644 packages/table/src/column.tsx create mode 100644 packages/table/src/common/_variables.scss create mode 100644 packages/table/src/common/clipboard.ts create mode 100644 packages/table/src/common/grid.ts create mode 100644 packages/table/src/common/index.ts create mode 100644 packages/table/src/common/rect.ts create mode 100644 packages/table/src/common/roundSize.tsx create mode 100644 packages/table/src/common/utils.ts create mode 100644 packages/table/src/headers/_common.scss create mode 100644 packages/table/src/headers/_headers.scss create mode 100644 packages/table/src/headers/columnHeader.tsx create mode 100644 packages/table/src/headers/columnHeaderCell.tsx create mode 100644 packages/table/src/headers/editableName.tsx create mode 100644 packages/table/src/headers/rowHeader.tsx create mode 100644 packages/table/src/headers/rowHeaderCell.tsx create mode 100644 packages/table/src/index.ts create mode 100644 packages/table/src/interactions/_interactions.scss create mode 100644 packages/table/src/interactions/draggable.tsx create mode 100644 packages/table/src/interactions/menus.tsx create mode 100644 packages/table/src/interactions/resizable.tsx create mode 100644 packages/table/src/interactions/resizeHandle.tsx create mode 100644 packages/table/src/interactions/resizeSensor.ts create mode 100644 packages/table/src/interactions/selectable.tsx create mode 100644 packages/table/src/layers/_layers.scss create mode 100644 packages/table/src/layers/guides.tsx create mode 100644 packages/table/src/layers/regions.tsx create mode 100644 packages/table/src/locator.ts create mode 100644 packages/table/src/regions.ts create mode 100644 packages/table/src/table.scss create mode 100644 packages/table/src/table.tsx create mode 100644 packages/table/src/tableBody.tsx create mode 100644 packages/table/test/cellTests.tsx create mode 100644 packages/table/test/clipboardTests.ts create mode 100644 packages/table/test/columnHeaderCellTests.tsx create mode 100644 packages/table/test/columnTests.tsx create mode 100644 packages/table/test/editableCellTests.tsx create mode 100644 packages/table/test/editableNameTests.tsx create mode 100644 packages/table/test/formatsTests.tsx create mode 100644 packages/table/test/gridTests.ts create mode 100644 packages/table/test/guidesTests.tsx create mode 100644 packages/table/test/harness.ts create mode 100644 packages/table/test/index.ts create mode 100644 packages/table/test/locatorTests.tsx create mode 100644 packages/table/test/menusTests.tsx create mode 100644 packages/table/test/mocks/table.tsx create mode 100644 packages/table/test/rectTests.ts create mode 100644 packages/table/test/regionsTests.ts create mode 100644 packages/table/test/resizableTests.tsx create mode 100644 packages/table/test/roundSizeTests.tsx create mode 100644 packages/table/test/selectableTests.tsx create mode 100644 packages/table/test/selectionTests.tsx create mode 100644 packages/table/test/tableBodyTests.tsx create mode 100644 packages/table/test/tableTests.tsx create mode 100644 packages/table/test/tsconfig.json create mode 100644 packages/table/test/utilsTests.ts create mode 100644 packages/table/tsconfig.json create mode 100644 packages/table/typings.json create mode 100644 packages/table/typings/globals/es6-shim/index.d.ts create mode 100644 packages/table/typings/globals/es6-shim/typings.json create mode 100644 packages/table/typings/tsd.d.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..9f42f4eb79 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,156 @@ +# Contributing + +:+1: :confetti_ball: :tada: Thanks for taking the time to contribute! :tada: :confetti_ball: :+1: + +#### Table Of Contents + +[Getting Started](#getting-started) + - [Installation](#installation) + +[Developing](#developing) + - [Running](#running) + - [Coding](#coding) + - [Submitting](#submitting) + +[Releasing](#releasing) + + +## Getting Started + +### Installation + +First, clone the Blueprint monorepo: + +```sh +git clone git@github.com:palantir/blueprint.git +``` + +> **Install Node.js** +> +> If you haven't already, you'll need to install node.js. Use an [official +> installer](https://nodejs.org/en/download/), or use a utility for managing +> versions such as [Node Version Manager](https://github.com/creationix/nvm). + +Go to the root directory of this repository and +install its dependencies: + +```sh +cd blueprint +npm install +npm run bootstrap +``` + +## Developing + +Start a new feature branch. We use a format like `[your-intials]/[short-name]`. + +For example: + * `bd/laser-beams` + * `bd/jittery-cells` + * `bd/css-class-renames` + +To create a new branch for development, run: + +```sh +git fetch +git checkout -b your-initials/your-branch-name origin/master +``` + + +### Running + +Many of the packages have their own compile-and-watch preview tasks which will +help you confirm that your features are working. + +To start the compile-and-watch task, run the default script in the package +directory. + +```sh +cd packages/table +npm start +``` + +Once the preview server is running, navigate to +[http://localhost:8080](http://localhost:8080). + + +### Coding + +While reviewing your changes, we use +[`previews`](https://github.com/palantir/blueprint/blob/master/packages/table/preview) +to demonstrate the functionality of various table features. The relevant package +previews will also be automatically added to your pull request. + +Feel free to modify the examples in the previews to test your code changes. When +you modify the code in `src/` or `preview/`, the code will be automatically +recompiled and you can simply refresh to see the result. + + +### Submitting + +Once you're satisfied with your changes, you'll need to make sure your code +will pass code review. + +Your code must be **test covered** and **lint free**. + +Run all unit tests and measure code coverage: + +```sh +npm run test +``` + +If the coverage check fails, you may have to add a new suite of tests or +modify existing tests in `test/`. + +Run code and style linters across all code: + +```sh +npm run lint +``` + +These commands must complete successfully, otherwise our CI will automatically +fail your pull-request. + + +### Submit a pull-request + +Once your code works and the tests and linter pass, you can submit your +code for review! + +```sh +git add --all . +git commit +git push +``` + +When committing, write a thorough description of your work so that reviewers +and future developers can understand your code changes. + +Once pushed, navigate to the [code tab of the Github site](https://github.com/palantir/blueprint) +and you should see an option to create a pull request from the recently pushed +commit. + +After your PR is created, our CI server will pick it up, run tests, and run a +server with your version of the preview so that other engineers can verify +your feature works as intended. + +After this point, you may be asked to make modification to your code to adhere +to coherent code style or to fix bugs you may have not noticed. Once you get +approvals from 1 or 2 repository owners, we will merge your code! +:confetti_ball: :tada: + + +## Releasing + +:warning: These steps should only be done by a repository owner. + + +#### Publishing to the NPM registry + +The library is automatically published to the NPM registry via `scripts/publish.sh` from CI nodes. + +To cut a new NPM release, run these steps. + +1. Update the version number in `package.json`. +2. Commit, push, pull-request, and merge. +3. [Draft a new release](https://github.com/palantir/blueprint/releases/new) from that commit. diff --git a/Gulpfile.js b/Gulpfile.js index 27eb60eae3..8741b2bb1e 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -6,14 +6,14 @@ const projects = [ { id: "core", - cwd: "packages/core", + cwd: "packages/core/", dependencies: [], sass: true, typescript: true, karma: true, }, { id: "datetime", - cwd: "packages/datetime", + cwd: "packages/datetime/", dependencies: ["core"], sass: true, typescript: true, @@ -46,6 +46,13 @@ const projects = [ cwd: "packages/landing/", dependencies: ["core"], sass: true, + }, { + id: "table", + cwd: "packages/table/", + dependencies: ["core"], + karma: true, + sass: true, + typescript: true, }, ]; diff --git a/circle.yml b/circle.yml index c8fe269d15..390a7d0654 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,8 @@ general: - packages/core/coverage - packages/datetime/coverage - packages/docs - - packages/landing + - packages/landing/dist + - packages/table/preview machine: node: version: 6.1.0 @@ -12,6 +13,7 @@ dependencies: - packages/core/node_modules - packages/datetime/node_modules - packages/docs/node_modules + - packages/table/node_modules - packages/landing/node_modules # non-zero exit codes in dependencies will fail the build early # so these following commands will block the build and prevent tests diff --git a/package.json b/package.json index 02ce182d22..9d5a8e1e9d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "3.4.0", "private": true, "description": "Omnibus project for Palantir's UI library for web applications.", + "scripts": { + "bootstrap": "lerna bootstrap" + }, "dependencies": { "@types/assertion-error": "1.0.30", "@types/chai": "3.4.34", diff --git a/packages/docs/package.json b/packages/docs/package.json index 3fa2b8ffb0..ea9e2bfe83 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -7,6 +7,7 @@ "dependencies": { "@blueprint/core": "*", "@blueprint/datetime": "*", + "@blueprint/table": "*", "chroma-js": "^1.1.1", "classnames": "^2.2.5", "dom4": "^1.8.3", diff --git a/packages/docs/src/common/resolveExample.ts b/packages/docs/src/common/resolveExample.ts index ff121f4af1..aeebd0d65e 100644 --- a/packages/docs/src/common/resolveExample.ts +++ b/packages/docs/src/common/resolveExample.ts @@ -7,10 +7,12 @@ import * as React from "react"; import * as CoreExamples from "@blueprint/core/examples"; import * as DateExamples from "@blueprint/datetime/examples"; +import * as TableExamples from "@blueprint/table/examples"; const Examples: ExampleMap = { core: CoreExamples as any, datetime: DateExamples as any, + table: TableExamples as any, }; export type ExampleComponentClass = React.ComponentClass<{ getTheme: () => string }>; diff --git a/packages/docs/src/docs.scss b/packages/docs/src/docs.scss index 0305aaf45f..5b59ee1ff1 100644 --- a/packages/docs/src/docs.scss +++ b/packages/docs/src/docs.scss @@ -5,6 +5,7 @@ @import "../node_modules/@blueprint/core/dist/blueprint.css"; @import "../node_modules/@blueprint/datetime/dist/blueprint-datetime.css"; +@import "../node_modules/@blueprint/table/dist/table.css"; @import "../node_modules/blueprint-syntax/dist/atom-blueprint-syntax-light.css"; @import "../node_modules/blueprint-syntax/dist/atom-blueprint-syntax-dark.css"; diff --git a/packages/table/README.md b/packages/table/README.md new file mode 100644 index 0000000000..eeaa73b48c --- /dev/null +++ b/packages/table/README.md @@ -0,0 +1,21 @@ +Table +--- + +Blueprint Table is a highly configurable UI component that provides standard UX +for presenting tabular data. It is built using [React](https://facebook.github.io/react/) +but usable with any front end framework. + +#### Features + +- Fixed column headers and row indices columns +- Resizable columns and rows + - Double-click column resize handles for auto-sizing +- Viewport-only rendering for scale (a.k.a. _lazy_ rendering) +- Robust selections (columns, rows, or multiple cell regions) +- Right-click to copy cell region contents +- Inline editable column headers & cells + +#### Roadmap (upcoming features) + +- Column reordering +- Paging component diff --git a/packages/table/config/webpack.config.preview.js b/packages/table/config/webpack.config.preview.js new file mode 100644 index 0000000000..61a83aea98 --- /dev/null +++ b/packages/table/config/webpack.config.preview.js @@ -0,0 +1,53 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 + */ + +const path = require("path"); +const resolve = (p) => path.join(__dirname, "..", p); +const ExtractTextPlugin = require("extract-text-webpack-plugin"); + +module.exports = { + + entry: { + index: resolve("preview/index.tsx"), + perf: resolve("preview/perf.tsx") + }, + + resolve: { + extensions: ["", ".js", ".ts", ".tsx"], + alias: { + react: resolve("/node_modules/react"), + }, + }, + + ts: { + compilerOptions: { declaration: false }, + }, + + module: { + loaders: [ + { + test: /\.tsx?$/, + loader: "ts-loader" + }, { + test: /\.css$/, + loader: ExtractTextPlugin.extract("style", "css"), + }, { + test: /\.(eot|ttf|woff|woff2)$/, + // We need to resolve to an absolute path so that this loader + // can be applied to CSS in other projects (i.e. packages/core) + loader: require.resolve("file-loader") + "?name=fonts/[name].[ext]" + }, + ], + }, + + plugins: [ + new ExtractTextPlugin("[name].css"), + ], + + output: { + filename: "[name].bundle.js", + path: resolve("preview/dist/") + } +}; diff --git a/packages/table/examples/index.ts b/packages/table/examples/index.ts new file mode 100644 index 0000000000..8923fb030b --- /dev/null +++ b/packages/table/examples/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 + */ + +export * from "./tableDollarExample"; +export * from "./tableEditableExample"; +export * from "./tableSortableExample"; diff --git a/packages/table/examples/sumo.json b/packages/table/examples/sumo.json new file mode 100644 index 0000000000..e9ead5e3d5 --- /dev/null +++ b/packages/table/examples/sumo.json @@ -0,0 +1,752 @@ +[ + [ + "Hakuho", + "Y1e", + "15-0 Y", + "Y1e", + "14-1 Y", + "Y1e", + "11-4 J", + "Y1e", + "14-1 Y", + "Y1e", + "0-3-12", + "Y1w", + "12-3 J" + ], + [ + "Kakuryu", + "Y1w", + "10-5", + "Y2e", + "0-1-14", + "Y2e", + "0-0-15", + "Y2e", + "12-3 J", + "Y1w", + "12-3 Y", + "Y1e", + "9-6" + ], + [ + "Harumafuji", + "Y2e", + "11-4 J", + "Y1w", + "10-5", + "Y1w", + "11-4 J", + "Y1w", + "1-1-13", + "Y2e", + "0-0-15", + "Y2e", + "13-2 Y" + ], + [ + "Kisenosato", + "O1e", + "11-4 J", + "O1e", + "9-6", + "O1e", + "11-4 J", + "O1e", + "10-5", + "O1w", + "11-4", + "O1w", + "10-5" + ], + [ + "Kotoshogiku", + "O1w", + "9-6", + "O1w", + "8-7", + "O1w", + "6-9", + "O2e", + "8-7", + "O2w", + "11-4", + "O2e", + "8-6-1" + ], + [ + "Goeido", + "O2w", + "8-7", + "O2w", + "8-7", + "O2w", + "8-6-1", + "O1w", + "9-6", + "O2e", + "7-8", + "O2w", + "8-7" + ], + [ + "Aoiyama", + "S1e", + "5-10", + "M3w", + "5-10", + "M6w", + "9-6", + "M2w", + "8-7", + "M1e", + "7-8", + "M2e", + "7-8" + ], + [ + "Ichinojo", + "S1w", + "6-9", + "M1w", + "9-6", + "K1w", + "8-7", + "S1w", + "4-11", + "M4e", + "9-6", + "M1e", + "6-9" + ], + [ + "Takayasu", + "K1e", + "6-9", + "M3e", + "3-12", + "M8w", + "10-5", + "M2e", + "6-9", + "M3w", + "1-3-11", + "M12w", + "9-6" + ], + [ + "Tochiozan", + "K1w", + "7-8", + "M1e", + "10-5", + "K1e", + "8-7", + "S1e", + "10-5", + "S1e", + "8-7", + "S1e", + "8-7" + ], + [ + "Takarafuji", + "M1e", + "7-8", + "M2w", + "8-7", + "M1e", + "9-6", + "K1e", + "4-11", + "M4w", + "4-11", + "M8w", + "10-5" + ], + [ + "Tochinoshin", + "M1w", + "6-9", + "M4w", + "8-7", + "M1w", + "9-6", + "M1e", + "8-7", + "K1e", + "10-5", + "K1e", + "7-8" + ], + [ + "Terunofuji", + "M2e", + "8-7", + "S1e", + "13-2 J", + "S1e", + "12-3 Y", + "O2w", + "11-4", + "O1e", + "12-3 D", + "O1e", + "9-6" + ], + [ + "Ikioi", + "M2w", + "1-14", + "M13e", + "8-7", + "M10e", + "10-5", + "M3e", + "2-13", + "M12e", + "11-4", + "M4e", + "12-3 J" + ], + [ + "Endo", + "M3e", + "6-9", + "M5w", + "4-2-9", + "M9w", + "6-9", + "M12e", + "10-5", + "M7e", + "8-7", + "M4w", + "4-11" + ], + [ + "Aminishiki", + "M3w", + "6-9", + "M6e", + "8-3-4", + "M2w", + "6-9", + "M4e", + "6-9", + "M6e", + "8-7", + "M3w", + "8-7" + ], + [ + "Toyonoshima", + "M4e", + "7-8", + "M5e", + "8-7", + "M2e", + "4-11", + "M7w", + "7-8", + "M8w", + "10-5", + "M3e", + "5-9-1" + ], + [ + "Jokoryu", + "M4w", + "5-7-3", + "M9w", + "5-10", + "M15e", + "5-10", + "J3e", + "7-8", + "J4e", + "8-7", + "J3w", + "9-6" + ], + [ + "Kaisei", + "M5e", + "7-8", + "M6w", + "5-10", + "M11e", + "10-5", + "M3w", + "6-9", + "M5w", + "6-9", + "M7e", + "9-6" + ], + [ + "Chiyotairyu", + "M5w", + "1-6-8", + "J1w", + "7-8", + "J2w", + "9-6", + "M13w", + "8-4-3", + "M11e", + "6-9", + "M13e", + "8-7" + ], + [ + "Okinoumi", + "M6e", + "9-6", + "S1w", + "0-4-11", + "M10w", + "9-6", + "M5e", + "11-4", + "K1w", + "6-9", + "M2w", + "5-10" + ], + [ + "Toyohibiki", + "M6w", + "3-12", + "M15e", + "8-7", + "M12w", + "6-9", + "M14e", + "5-10", + "J3e", + "10-5", + "M13w", + "7-8" + ], + [ + "Kyokutenho", + "M7e", + "5-10", + "M11w", + "6-9", + "M14w", + "8-7", + "M11w", + "3-12", + "", + "", + "", + "" + ], + [ + "Chiyootori", + "M7w", + "5-8-2", + "M12e", + "11-4", + "M4e", + "0-2-13", + "J1e", + "9-6", + "M12w", + "6-9", + "M15w", + "10-5" + ], + [ + "Myogiryu", + "M8e", + "9-6", + "K1w", + "8-7", + "S1w", + "7-8", + "K1w", + "8-7", + "S1w", + "8-7", + "S1w", + "2-13" + ], + [ + "Sadanoumi", + "M8w", + "9-6", + "M2e", + "7-8", + "M3e", + "8-7", + "M1w", + "6-9", + "M3e", + "6-9", + "M5w", + "5-10" + ], + [ + "Tamawashi", + "M9e", + "10-5", + "K1e", + "4-11", + "M5w", + "6-9", + "M7e", + "8-7", + "M5e", + "4-11", + "M9e", + "8-7" + ], + [ + "Takekaze", + "M9w", + "9-6", + "M4e", + "4-11", + "M8e", + "8-7", + "M4w", + "5-10", + "M8e", + "5-10", + "M12e", + "7-8" + ], + [ + "Sokokurai", + "M10e", + "6-9", + "M13w", + "9-6", + "M7e", + "1-4-10", + "J2e", + "9-6", + "M14w", + "8-7", + "M10e", + "9-6" + ], + [ + "Homarefuji", + "M10w", + "8-7", + "M7e", + "6-9", + "M9e", + "7-8", + "M9w", + "6-9", + "M11w", + "9-6", + "M6w", + "3-12" + ], + [ + "Shohozan", + "M11e", + "8-7", + "M8e", + "1-14", + "J3w", + "7-8", + "J4w", + "6-9", + "J6w", + "13-2 Y", + "M10w", + "12-3 J" + ], + [ + "Yoshikaze", + "M11w", + "8-7", + "M9e", + "5-10", + "M14e", + "10-5", + "M8e", + "12-3 J", + "M1w", + "11-4", + "K1w", + "8-7" + ], + [ + "Kyokushuho", + "M12e", + "8-7", + "M10e", + "7-8", + "M11w", + "9-6", + "M6e", + "5-10", + "M10w", + "8-7", + "M7w", + "9-6" + ], + [ + "Arawashi", + "M12w", + "7-8", + "M14w", + "8-7", + "M12e", + "2-13", + "J7e", + "9-6", + "J3w", + "7-8", + "J5e", + "8-7" + ], + [ + "Osunaarashi", + "M13e", + "8-7", + "M11e", + "11-4", + "M3w", + "4-4-7", + "M8w", + "11-4", + "M2e", + "8-7", + "M1w", + "5-9-1" + ], + [ + "Tokitenku", + "M13w", + "9-6", + "M8w", + "3-12", + "J1e", + "10-5", + "M11e", + "6-9", + "M13w", + "7-8", + "M14w", + "0-0-15" + ], + [ + "Kotoyuki", + "M14e", + "8-7", + "M12w", + "6-9", + "M15w", + "8-7", + "M12w", + "8-7", + "M10e", + "9-6", + "M6e", + "8-7" + ], + [ + "Chiyomaru", + "M14w", + "7-8", + "M16w", + "8-7", + "M13w", + "3-12", + "J5w", + "9-6", + "J1w", + "1-8-6", + "J14e", + "9-6" + ], + [ + "Sadanofuji", + "M15e", + "8-7", + "M14e", + "9-6", + "M7w", + "6-9", + "M9e", + "10-5", + "M2w", + "2-13", + "M9w", + "4-11" + ], + [ + "Kagamio", + "M15w", + "7-8", + "J1e", + "4-11", + "J9e", + "12-3 Y", + "M14w", + "9-6", + "M9w", + "4-11", + "J1w", + "6-9" + ], + [ + "Tokushoryu", + "M16e", + "11-4 J", + "M7w", + "8-7", + "M4w", + "6-9", + "M5w", + "7-8", + "M6w", + "6-9", + "M8e", + "8-7" + ], + [ + "Tosayutaka", + "M16w", + "0-2-13", + "J11e", + "6-9", + "J14w", + "7-8", + "Ms1w", + "1-6", + "Ms20w", + "0-0-7", + "Ms60w", + "0-0-7" + ], + [ + "Amuru", + "J1e", + "8-7", + "M16e", + "7-8", + "M16w", + "9-6", + "M10w", + "8-7", + "M7w", + "8-7", + "M5e", + "4-11" + ], + [ + "Wakanosato", + "J1w", + "5-10", + "J4w", + "6-9", + "J7w", + "5-10", + "J11w", + "4-11", + "Ms4e", + "0-0", + "", + "" + ], + [ + "Asasekiryu", + "J2e", + "5-10", + "J5w", + "8-7", + "J3e", + "7-8", + "J4e", + "10-5", + "M15e", + "7-8", + "M16e", + "3-12" + ], + [ + "Gagamaru", + "J2w", + "11-4", + "M15w", + "11-4", + "M6e", + "7-8", + "M6w", + "6-9", + "M9e", + "6-9", + "M11e", + "8-7" + ], + [ + "Kitataiki", + "J3e", + "13-2 Y", + "M10w", + "9-6", + "M5e", + "4-11", + "M10e", + "5-10", + "M14e", + "7-8", + "M15e", + "7-8" + ], + [ + "Daido", + "J3w", + "6-9", + "J5e", + "6-9", + "J8w", + "6-9", + "J11e", + "4-11", + "Ms3e", + "5-2", + "J13w", + "6-9" + ], + [ + "Asahisho", + "J4e", + "9-6", + "J2w", + "5-10", + "J8e", + "9-6", + "J3w", + "6-9", + "J6e", + "6-9", + "J8w", + "6-9" + ], + [ + "Takanoiwa", + "J4w", + "6-9", + "J6w", + "11-4", + "M16e", + "7-8", + "M16e", + "6-9", + "J2w", + "9-6", + "J1e", + "8-7" + ] +] diff --git a/packages/table/examples/tableDollarExample.tsx b/packages/table/examples/tableDollarExample.tsx new file mode 100644 index 0000000000..4c778d900e --- /dev/null +++ b/packages/table/examples/tableDollarExample.tsx @@ -0,0 +1,19 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as React from "react"; +import { Cell, Column, Table } from "../src"; +import BaseExample from "@blueprint/core/examples/common/baseExample"; + +export class TableDollarExample extends BaseExample<{}> { + public render() { + const renderCell = (rowIndex: number) => {`$${(rowIndex * 10).toFixed(2)}`}; + return ( + + +
+ ); + } +} diff --git a/packages/table/examples/tableEditableExample.tsx b/packages/table/examples/tableEditableExample.tsx new file mode 100644 index 0000000000..beb1145126 --- /dev/null +++ b/packages/table/examples/tableEditableExample.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as React from "react"; +import { Intent } from "@blueprint/core"; +import BaseExample from "@blueprint/core/examples/common/baseExample"; +import { Column, ColumnHeaderCell, EditableCell, EditableName, Table } from "../src"; + +export class TableEditableExample extends BaseExample<{}> { + + public static dataKey = (rowIndex: number, columnIndex: number) => { + return `${rowIndex}-${columnIndex}`; + } + + public state = { + columnNames: [ + "Please", + "Rename", + "Me", + ], + sparseCellData: { + "1-1": "editable", + "3-1": "validation 123", + } as { [key: string]: string }, + sparseCellIntent: { + "3-1": Intent.DANGER, + } as { [key: string]: Intent }, + sparseColumnIntents: [] as Intent[], + }; + + public render() { + const columns = this.state.columnNames.map((_: string, index: number) => { + return (); + }); + return ({columns}
); + } + + public renderCell = (rowIndex: number, columnIndex: number) => { + const dataKey = TableEditableExample.dataKey(rowIndex, columnIndex); + const value = this.state.sparseCellData[dataKey]; + return (); + } + + public renderColumnHeader = (columnIndex: number) => { + const renderName = (name: string) => { + return (); + }; + return (); + } + + private isValidValue(value: string) { + return /^[a-zA-Z]*$/.test(value); + } + + private nameValidator = (index: number) => { + return (name: string) => { + const intent = this.isValidValue(name) ? null : Intent.DANGER; + this.setArrayState("sparseColumnIntents", index, intent); + this.setArrayState("columnNames", index, name); + }; + } + + private nameSetter = (index: number) => { + return (name: string) => { + this.setArrayState("columnNames", index, name); + }; + } + + private cellValidator = (rowIndex: number, columnIndex: number) => { + const dataKey = TableEditableExample.dataKey(rowIndex, columnIndex); + return (value: string) => { + const intent = this.isValidValue(value) ? null : Intent.DANGER; + this.setSparseState("sparseCellIntent", dataKey, intent); + this.setSparseState("sparseCellData", dataKey, value); + }; + } + + private cellSetter = (rowIndex: number, columnIndex: number) => { + const dataKey = TableEditableExample.dataKey(rowIndex, columnIndex); + return (value: string) => { + const intent = this.isValidValue(value) ? null : Intent.DANGER; + this.setSparseState("sparseCellData", dataKey, value); + this.setSparseState("sparseCellIntent", dataKey, intent); + }; + } + + private setArrayState(key: string, index: number, value: T) { + const values = (this.state as any)[key].slice() as T[]; + values[index] = value; + this.setState({ [key] : values }); + } + + private setSparseState(stateKey: string, dataKey: string, value: T) { + const values = Object.assign({}, (this.state as any)[stateKey]) as {[key: string]: T}; + values[dataKey] = value; + this.setState({ [stateKey] : values }); + } +} diff --git a/packages/table/examples/tableSortableExample.tsx b/packages/table/examples/tableSortableExample.tsx new file mode 100644 index 0000000000..22b45e3925 --- /dev/null +++ b/packages/table/examples/tableSortableExample.tsx @@ -0,0 +1,228 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as React from "react"; +import { Menu, MenuItem } from "@blueprint/core"; +import { + Cell, + Column, + ColumnHeaderCell, + CopyCellsMenuItem, + IMenuContext, + SelectionModes, + Table, + Utils, +} from "../src"; +import BaseExample from "@blueprint/core/examples/common/baseExample"; + +// tslint:disable-next-line:no-var-requires +const sumo = require("./sumo.json") as any[]; + +interface ICellLookup { + (rowIndex: number, columnIndex: number): any; +} + +interface ISortCallback { + (columnIndex: number, comparator: (a: any, b: any) => number): void; +} + +abstract class AbstractSortableColumn { + constructor(protected name: string, protected index: number) { + } + + public getColumn(getCellData: ICellLookup, sortColumn: ISortCallback) { + const menu = this.renderMenu(sortColumn); + const renderCell = (rowIndex: number, columnIndex: number) => + {getCellData(rowIndex, columnIndex)}; + const renderColumnHeader = () => ; + return (); + } + + protected abstract renderMenu(sortColumn: ISortCallback): React.ReactElement<{}>; +} + +class TextSortableColumn extends AbstractSortableColumn { + protected renderMenu(sortColumn: ISortCallback) { + const sortAsc = () => sortColumn(this.index, (a, b) => (this.compare(a, b))); + const sortDesc = () => sortColumn(this.index, (a, b) => (this.compare(b, a))); + return ( + + + ); + } + + private compare(a: any, b: any) { + return a.toString().localeCompare(b); + } +} + +class RankSortableColumn extends AbstractSortableColumn { + private static RANK_PATTERN = /([YOSKMJ])([0-9]+)(e|w)/i; + private static TITLES: {[key: string]: number} = { + "Y": 0, // Yokozuna + "O": 1, // Ozeki + "S": 2, // Sekiwake + "K": 3, // Komusubi + "M": 4, // Maegashira + "J": 5, // Juryo + }; + + protected renderMenu(sortColumn: ISortCallback) { + const sortAsc = () => sortColumn(this.index, (a, b) => (this.compare(a, b))); + const sortDesc = () => sortColumn(this.index, (a, b) => (this.compare(b, a))); + return ( + + + ); + } + + private toRank(str: string) { + const match = RankSortableColumn.RANK_PATTERN.exec(str); + if (match == null) { + return 1000; + } + const [title, rank, side] = match.slice(1); + return RankSortableColumn.TITLES[title] * 100 + (side === "e" ? 0 : 1) + (parseInt(rank, 10) * 2); + } + + private compare(a: any, b: any) { + return this.toRank(a) - this.toRank(b); + } +} + +class RecordSortableColumn extends AbstractSortableColumn { + private static WIN_LOSS_PATTERN = /^([0-9]+)(-([0-9]+))?(-([0-9]+)) ?.*/; + + protected renderMenu(sortColumn: ISortCallback) { + // tslint:disable:jsx-no-lambda + return ( + (sortColumn(this.index, this.transformCompare(this.toWins, false)))} + text="Sort Wins Asc" + /> + (sortColumn(this.index, this.transformCompare(this.toWins, true)))} + text="Sort Wins Desc" + /> + (sortColumn(this.index, this.transformCompare(this.toLosses, false)))} + text="Sort Losses Asc" + /> + (sortColumn(this.index, this.transformCompare(this.toLosses, true)))} + text="Sort Losses Desc" + /> + (sortColumn(this.index, this.transformCompare(this.toTies, false)))} + text="Sort Ties Asc" + /> + (sortColumn(this.index, this.transformCompare(this.toTies, true)))} + text="Sort Ties Desc" + /> + ); + // tslint:enable:jsx-no-lambda + } + + private transformCompare(transform: (a: any) => any, reverse: boolean) { + if (reverse) { + return (a: any, b: any) => (transform(b) - transform(a)); + } else { + return (a: any, b: any) => (transform(a) - transform(b)); + } + } + + private toWins(a: any) { + const match = RecordSortableColumn.WIN_LOSS_PATTERN.exec(a); + return (match == null) ? -1 : parseInt(match[1], 10); + } + + private toTies(a: any) { + const match = RecordSortableColumn.WIN_LOSS_PATTERN.exec(a); + return (match == null || match[3] == null) ? -1 : parseInt(match[3], 10); + } + + private toLosses(a: any) { + const match = RecordSortableColumn.WIN_LOSS_PATTERN.exec(a); + return (match == null) ? -1 : parseInt(match[5], 10); + } +} + +export class TableSortableExample extends BaseExample<{}> { + public state = { + columns: [ + new TextSortableColumn("Rikishi", 0), + new RankSortableColumn("Rank - Hatsu Basho", 1), + new RecordSortableColumn("Record - Hatsu Basho", 2), + new RankSortableColumn("Rank - Haru Basho", 3), + new RecordSortableColumn("Record - Haru Basho", 4), + new RankSortableColumn("Rank - Natsu Basho", 5), + new RecordSortableColumn("Record - Natsu Basho", 6), + new RankSortableColumn("Rank - Nagoya Basho", 7), + new RecordSortableColumn("Record - Nagoya Basho", 8), + new RankSortableColumn("Rank - Aki Basho", 9), + new RecordSortableColumn("Record - Aki Basho", 10), + new RankSortableColumn("Rank - Kyūshū Basho", 11), + new RecordSortableColumn("Record - Kyūshū Basho", 12), + ], + data: sumo, + sortedIndexMap : [] as number[], + }; + + public render() { + const numRows = this.state.data.length; + const columns = this.state.columns.map((col) => (col.getColumn(this.getCellData, this.sortColumn))); + return ( + + {columns} +
+ ); + } + + private getCellData = (rowIndex: number, columnIndex: number) => { + const sortedRowIndex = this.state.sortedIndexMap[rowIndex]; + if (sortedRowIndex != null) { + rowIndex = sortedRowIndex; + } + return this.state.data[rowIndex][columnIndex]; + } + + private renderBodyContextMenu = (context: IMenuContext) => { + return ( + + ); + }; + + private sortColumn = (columnIndex: number, comparator: (a: any, b: any) => number) => { + const { data } = this.state; + const sortedIndexMap = Utils.times(data.length, (i: number) => (i)); + sortedIndexMap.sort((a: number, b: number) => { + return comparator( + data[a][columnIndex], + data[b][columnIndex] + ); + }); + this.setState({ sortedIndexMap }); + } +} diff --git a/packages/table/package.json b/packages/table/package.json new file mode 100644 index 0000000000..39ebd53df6 --- /dev/null +++ b/packages/table/package.json @@ -0,0 +1,57 @@ +{ + "name": "@blueprint/table", + "version": "1.4.1", + "author": "Blueprint Team ", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "style": "dist/table.css", + "scripts": { + "build:gulp": "$(cd ..; npm bin)/gulp typescript-compile-table sass-compile-table", + "build:preview": "webpack --config config/webpack.config.preview.js", + "build": "npm-run-all clean build:gulp build:preview", + "clean:dist": "rm -rf dist", + "clean:preview": "rm -rf preview/dist", + "clean": "npm-run-all clean:dist clean:preview", + "lint": "$(cd ..; npm bin)/gulp typescript-lint-table sass-lint-table", + "serve": "http-server .", + "start": "npm-run-all build -p watch serve", + "test": "$(cd ..; npm bin)/gulp karma-table", + "watch": "onchange 'src/**' 'preview/*.ts*' -- npm-run-all build:gulp build:preview" + }, + "dependencies": { + "@blueprint/core": ">=3.0.0-beta.6", + "classnames": "^2.2", + "es6-shim": "^0.35", + "react": "^15.0.1 || ^0.14" + }, + "devDependencies": { + "css-loader": "^0.25.0", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.9.0", + "http-server": "^0.9.0", + "npm-run-all": "^1.7.0", + "onchange": "^3.0.0", + "pure-render-decorator": "^1.1.1", + "react-addons-css-transition-group": "^15.3.0", + "react-addons-test-utils": "^15.1.0", + "react-dom": "^15.1.0", + "style-loader": "^0.13.1", + "typescript": "^2.0.6", + "webpack": "^1.13.0" + }, + "repository": { + "type": "git", + "url": "git@github.com:palantir/blueprint.git" + }, + "keywords": [ + "palantir", + "blueprint", + "theme", + "react", + "day", + "picker", + "date", + "time" + ], + "license": "Apache-2.0" +} diff --git a/packages/table/preview/index.html b/packages/table/preview/index.html new file mode 100644 index 0000000000..1d4765d5db --- /dev/null +++ b/packages/table/preview/index.html @@ -0,0 +1,135 @@ + + + + + Table + + + + + + +

Basic Table

+
+

+ +

Column Type Formats

+
+

+ +

Editable Column Names and Cells

+

Validation with alpha characters only.

+
+

+ +

Big Table

+
+

+ +

All Selection Modes, Body Context Menu

+
+

+ +

Ghost Inline

+
+

+ +

Ghost Cells

+
+

+ +

Ledger

+
+

+ +

Intents, Inline, & No Resizing

+
+

+ +

Add/Remove/Rearrange Columns

+
+
+
- Remove Column
+
+ Add Column
+
Swap Columns
+

+ +

Too Small, No Row Headers

+
+

+ +

Custom Row Headers

+
+

+ +

Styled Region Groups, "Active" Columns

+
+

+ +

Menus & Extended Headers

+
+

+ +

Custom Column Header Wrapper

+
+

+ +

Named Columns

+
+

+ +

Stacking Context

+
+

+ + + + diff --git a/packages/table/preview/index.tsx b/packages/table/preview/index.tsx new file mode 100644 index 0000000000..bd90d5f78c --- /dev/null +++ b/packages/table/preview/index.tsx @@ -0,0 +1,468 @@ +/** + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 + * + * Demonstrates sample usage of the table component. + */ + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import { Intent, Menu, MenuItem, MenuDivider } from "@blueprint/core"; + +import { + Cell, + Column, + ColumnHeaderCell, + CopyCellsMenuItem, + EditableCell, + EditableName, + IColumnHeaderCellProps, + IColumnProps, + HorizontalCellDivider, + IMenuContext, + IRegion, + JSONFormat, + RegionCardinality, + Regions, + RowHeaderCell, + SelectionModes, + Table, + Utils, +} from "../src/index"; + +import { Nav } from "./nav"; +ReactDOM.render(