chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
@@ -0,0 +1,64 @@
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
* ┃ Copyright (c) 2017, the Perspective Authors. ┃
* ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
* ┃ This file is part of the Perspective library, distributed under the terms ┃
* ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
*/
.psp-header-border:not(.psp-is-top):not(.psp-header-leaf) {
/* right */
box-shadow: 1px 0px var(--psp-inactive--border-color, #8b868045);
}
.psp-header-group {
/* bottom */
box-shadow: 0px 10px 0 -9px var(--psp-inactive--border-color, #8b868045);
}
.psp-is-top {
/* top-miter-right */
box-shadow: 5px 4px 0px -4px var(--psp-inactive--border-color, #8b868045);
}
.psp-is-top.psp-header-group:not(.psp-header-group-corner) {
/* top-miter-right and bottom */
box-shadow:
5px 4px 0px -4px var(--psp-inactive--border-color, #8b868045),
0px 10px 0 -9px var(--psp-inactive--border-color, #8b868045);
}
.psp-header-border.psp-header-group {
&:not(.psp-is-top):not(.psp-header-group-corner) {
/* right and bottom */
box-shadow:
1px 0px var(--psp-inactive--border-color, #8b868045),
0px 10px 0 -9px var(--psp-inactive--border-color, #8b868045);
}
}
perspective-viewer[settings]
tr.rt-autosize
.psp-header-leaf.psp-header-border:not(.psp-menu-enabled) {
box-shadow: 1px 0px var(--psp-inactive--border-color, #8b868045);
}
:host-context(perspective-viewer[settings])
tr.rt-autosize
.psp-header-leaf.psp-header-border:not(.psp-menu-enabled) {
box-shadow: 1px 0px var(--psp-inactive--border-color, #8b868045);
}
.psp-header-leaf.psp-header-border {
/* bottom-miter-right */
box-shadow: 5px -4px 0px -4px var(--psp-inactive--border-color, #8b868045);
}
tr:only-child th {
box-shadow: none !important;
}
@@ -0,0 +1,6 @@
@import "./pro.css";
@import "./mitered-headers.css";
@import "./row-hover.css";
@import "./sub-cell-scroll.css";
@import "./scrollbar.css";
@import "./regular_table.css";
+197
View File
@@ -0,0 +1,197 @@
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
* ┃ Copyright (c) 2017, the Perspective Authors. ┃
* ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
* ┃ This file is part of the Perspective library, distributed under the terms ┃
* ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
*/
regular-table {
padding: 0;
margin: 12px 0 0 12px;
font-family: inherit;
--psp-datagrid--hover--border-color: var(--psp-inactive--color, #c5c9d080);
div[tabindex] {
outline: none;
}
& > div {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
th {
text-align: center;
}
/* Header groups should overflow and not contribute to auto-sizing. */
thead tr:not(.rt-autosize) th {
overflow: hidden;
max-width: 0px;
}
thead tr:last-child .rt-float,
tbody .rt-float {
text-align: right;
}
thead .rt-integer,
tbody .rt-integer {
text-align: right;
}
tbody th {
text-align: left;
}
span.rt-tree-container {
display: flex;
align-items: center;
height: 100%;
}
thead .rt-string,
tbody .rt-string,
thead .rt-date,
tbody .rt-date,
thead .rt-datetime,
tbody .rt-datetime {
text-align: left;
}
/* frozen rows */
thead tr:last-child th {
border-bottom: 1px solid #8b868045;
}
tbody tr:first-child td,
tbody tr:first-child th {
border-top: 1px solid transparent !important;
}
th {
position: relative;
}
tr th span.rt-tree-group {
margin-left: 5px;
margin-right: 15px;
border-left: 1px solid #eee;
height: 100%;
}
td,
th {
white-space: nowrap;
padding-right: 5px;
padding-left: 5px;
padding-top: 0px;
padding-bottom: 0px;
height: var(--psp-datagrid--row--height, 19px);
}
table * {
box-sizing: border-box;
}
table {
position: absolute;
color: #666;
outline: none;
}
span.rt-row-header-icon {
color: #aaa;
padding-right: 4px;
font-family: var(--psp-button--font-family);
}
span.rt-column-header-icon {
font-size: 10px;
padding-left: 3px;
display: inline-block;
width: 10px;
font-family: var(--psp-button--font-family);
}
span.rt-row-header-icon:hover {
color: #1a7da1;
text-shadow: 0px 0px 3px #1a7da1;
}
.rt-selected td {
background-color: #eee;
}
.rt-cell-clip {
overflow: hidden;
text-overflow: ellipsis;
}
/* OPTIONAL zebra striping */
/* @zebra-stripe-color: rgb(238,238,238); */
/* tbody tr:nth-child(even) td:not(.rt-group-header) { */
/* background: @zebra-stripe-color; */
/* } */
/* tbody tr:nth-child(even) span.rt-group-name { */
/* background: @zebra-stripe-color; */
/* } */
td span.rt-group-name,
th span.rt-group-name {
margin-right: -5px;
padding-right: 5px;
padding-left: 8px;
flex: 1;
height: 100%;
}
th span.rt-group-name {
text-align: left;
}
td th span.rt-group-leaf,
th span.rt-group-leaf {
margin-left: 16px;
height: 100%;
}
.rt-column-resize {
height: 100%;
width: 10px;
position: absolute;
top: 0;
right: 0;
cursor: col-resize;
}
a {
color: var(--psp-datagrid--pos-cell--color);
}
a:visited {
color: var(--psp-active--color);
}
td.psp-null:after,
th.psp-null:after {
content: var(--psp-label--null--content, "-");
color: var(--psp-label--null--color, inherit);
font-size: var(--psp-label--null--font-size, inherit);
vertical-align: middle;
}
}
@@ -0,0 +1,625 @@
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
* ┃ Copyright (c) 2017, the Perspective Authors. ┃
* ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
* ┃ This file is part of the Perspective library, distributed under the terms ┃
* ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
*/
/*
/* Row Selection */
/* // TODO this makes the UI flash a CSS layout for a millsiecond when toggling */
/* // settings butit could be fixed. */
/* perspective-viewer:not([settings]) { */
/* @include settings-not-open; */
/* } */
/* :host-context(perspective-viewer:not([settings])) { */
/* @include settings-not-open; */
/* } */
/* @mixin settings-not-open { */
/* regular-table table tr.rt-autosize + tr th { */
/* height: 0px; */
/* span { */
/* display: none; */
/* } */
/* } */
/* } */
perspective-viewer,
:host {
.psp-menu-enabled {
padding: 0 6px;
font-size: 8px;
border-radius: 3px 3px 0 0;
}
.psp-menu-enabled:hover,
tr:not(.rt-autosize) th.psp-menu-open {
color: var(--psp--background-color);
background-color: var(--psp--color);
border-bottom-color: var(--psp--color);
cursor: pointer;
}
tr:not(.rt-autosize) th.psp-menu-open {
&:before {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
height: 8px;
width: 10px;
-webkit-mask-image: var(--psp-icon--column-settings--mask-image);
mask-image: var(--psp-icon--column-settings--mask-image);
margin-right: 4px;
background-color: var(--psp--background-color);
}
}
td.psp-menu-open {
box-shadow:
inset -2px 0px 0px var(--psp--color),
inset 2px 0px 0px var(--psp--color);
}
tr:first-child td.psp-menu-open {
border-top-color: var(--psp--color) !important;
}
tr:last-child td.psp-menu-open {
box-shadow:
inset -2px 0px 0px var(--psp--color),
inset 2px 0px 0px var(--psp--color),
inset 0px -2px 0px var(--psp--color);
}
}
perspective-viewer,
:host-context(perspective-viewer) {
regular-table table thead tr:last-child th {
border-bottom-width: 1px;
border-bottom-color: var(--psp-inactive--border-color, #8b868045);
}
}
.psp-sort-enabled:hover {
cursor: pointer;
}
regular-table.flat-group-rollup-mode.vertical-row-headers
th.psp-tree-label:not(:last-of-type) {
writing-mode: vertical-lr;
}
.psp-select-region-inactive,
:hover .psp-select-region-inactive,
:hover th.psp-tree-leaf.psp-select-region-inactive,
:hover th.psp-tree-label.psp-select-region-inactive {
background-color: var(--psp-inactive--color) !important;
color: var(--psp--background-color) !important;
border-color: var(--psp--background-color) !important;
}
.psp-error {
color: red;
}
td:focus {
outline: #666;
outline-style: dotted;
outline-width: 1px;
}
perspective-viewer.dragging,
:host-context(perspective-viewer.dragging) {
regular-table {
pointer-events: none;
}
}
.psp-header-border:last-child {
border-right-width: 0px;
}
.psp-header-sort-desc:after {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
width: 14px;
height: 11px;
margin-left: 4px;
-webkit-mask-image: var(--psp-icon--sort-desc--mask-image);
mask-image: var(--psp-icon--sort-desc--mask-image);
}
.psp-header-sort-asc:after {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
width: 14px;
height: 11px;
margin-left: 4px;
-webkit-mask-image: var(--psp-icon--sort-asc--mask-image);
mask-image: var(--psp-icon--sort-asc--mask-image);
}
.psp-header-sort-col-desc:after {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
width: 14px;
height: 11px;
margin-left: 4px;
-webkit-mask-image: var(--psp-icon--sort-col-desc--mask-image);
mask-image: var(--psp-icon--sort-col-desc--mask-image);
}
.psp-header-sort-col-asc:after {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
width: 14px;
height: 11px;
margin-left: 4px;
-webkit-mask-image: var(--psp-icon--sort-col-asc--mask-image);
mask-image: var(--psp-icon--sort-col-asc--mask-image);
}
.psp-header-sort-abs-desc:after {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
width: 14px;
height: 11px;
margin-left: 4px;
-webkit-mask-image: var(--psp-icon--sort-abs-desc--mask-image);
mask-image: var(--psp-icon--sort-abs-desc--mask-image);
}
.psp-header-sort-abs-asc:after {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
width: 14px;
height: 11px;
margin-left: 4px;
-webkit-mask-image: var(--psp-icon--sort-abs-asc--mask-image);
mask-image: var(--psp-icon--sort-abs-asc--mask-image);
}
.psp-header-sort-abs-col-desc:after {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
width: 14px;
height: 11px;
margin-left: 4px;
-webkit-mask-image: var(--psp-icon--sort-abs-col-desc--mask-image);
mask-image: var(--psp-icon--sort-abs-col-desc--mask-image);
}
.psp-header-sort-abs-col-asc:after {
background-repeat: no-repeat;
background-color: var(--psp--color);
content: "";
display: inline-block;
-webkit-mask-size: cover;
mask-size: cover;
width: 14px;
height: 11px;
margin-left: 4px;
-webkit-mask-image: var(--psp-icon--sort-abs-col-asc--mask-image);
mask-image: var(--psp-icon--sort-abs-col-asc--mask-image);
}
/* Shift-key affordance: while the user holds Shift on the host
* `<perspective-viewer>`, sort-arrow mask-image icons and tree
* expand/collapse carets recolor to advertise their Shift-modified actions
* (abs-sort and depth-level expand/collapse). Both `:host-context` (shadow
* render target) and the bare selector (light render target) are emitted to
* cover the runtime branch in datagrid.ts. */
:host-context(perspective-viewer.shift-active)
:is(
.psp-header-sort-asc,
.psp-header-sort-desc,
.psp-header-sort-col-asc,
.psp-header-sort-col-desc
):after,
perspective-viewer.shift-active
:is(
.psp-header-sort-asc,
.psp-header-sort-desc,
.psp-header-sort-col-asc,
.psp-header-sort-col-desc
):after {
background-color: var(
--shift-active--color,
var(--psp-datagrid--pos-cell--color, #1078d1)
);
}
:host-context(perspective-viewer.shift-active)
:is(.psp-tree-label-expand, .psp-tree-label-collapse):before,
perspective-viewer.shift-active
:is(.psp-tree-label-expand, .psp-tree-label-collapse):before {
color: var(
--shift-active--color,
var(--psp-datagrid--pos-cell--color, #1078d1)
);
}
tbody th:last-of-type {
border-right: 1px solid var(--psp-inactive--border-color, #8b868045);
overflow: hidden;
text-overflow: ellipsis;
}
tbody th:empty {
background-image: linear-gradient(
to right,
transparent 9px,
var(--psp-inactive--border-color, #eee) 10px,
transparent 11px
);
background-repeat: no-repeat;
min-width: 20px;
max-width: 20px;
pointer-events: none;
}
regular-table:not(.flat-group-rollup-mode) {
.psp-tree-label {
max-width: 0px;
min-width: 0px;
}
.psp-tree-label:before {
color: var(--psp--color);
font-family: var(--psp-button--font-family, inherit);
padding-right: 11px;
}
.psp-tree-label-expand:before {
content: var(--psp-label--tree-expand--content, "+");
}
.psp-tree-label-collapse:before {
content: var(--psp-label--tree-collapse--content, "-");
}
.psp-tree-label-expand,
.psp-tree-label-collapse {
cursor: pointer;
}
.psp-tree-label:hover:before {
color: var(--psp-active--color);
text-shadow: 0px 0px 5px var(--psp-active--color);
}
}
.psp-tree-leaf {
padding-left: 24px;
}
.psp-align-right {
text-align: right;
}
.psp-color-mode-bar {
padding: 0 2px;
}
.psp-color-mode-label-bar {
position: relative;
padding: 0 2px;
.psp-bar {
isolation: isolate;
position: unset;
}
.psp-bar:before {
color: transparent;
pointer-events: none;
content: var(--label);
display: inline-flex;
justify-content: flex-end;
align-items: center;
padding: 0 3px;
}
.psp-bar:after {
color: var(--psp-label-bar-bg);
content: var(--label);
mix-blend-mode: difference;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: inline-flex;
justify-content: flex-end;
align-items: center;
padding: 0 5px;
}
}
.psp-label-bar {
inset: 0;
pointer-events: none;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0px;
}
.psp-label-bar-fill {
position: absolute;
top: 10%;
height: 80%;
background: var(--psp-label-bar-color);
pointer-events: none;
}
.psp-label-bar-text {
position: relative;
color: var(--psp-label-bar-bg);
mix-blend-mode: difference;
pointer-events: none;
}
.psp-align-left {
text-align: left;
}
.psp-positive:not(:focus) {
color: var(--psp-datagrid--pos-cell--color);
}
.psp-negative:not(:focus) {
color: var(--psp-datagrid--neg-cell--color);
}
regular-table table tbody td {
min-width: 52px !important;
}
.psp-is-width-override .rt-column-resize,
.rt-column-resize:hover {
border: 1px dashed #999;
border-bottom-width: 0px;
border-left-width: 0px;
}
.boolean-editable {
cursor: pointer;
}
regular-table table {
font-size: 1em;
user-select: none;
color: inherit;
border-collapse: separate;
th {
font-weight: 400;
}
td,
th {
border-color: var(--psp-inactive--border-color, #8b868045);
height: var(--psp-datagrid--row--height, 23px);
}
.psp-header-group {
text-overflow: ellipsis;
}
th.psp-header-leaf {
border-bottom-width: 0px;
span {
height: var(--psp-datagrid--row--height, 23px);
min-height: var(--psp-datagrid--row--height, 23px);
}
}
td,
th.psp-tree-label,
th.psp-tree-label,
th.psp-tree-leaf,
tbody tr:first-child th {
border-style: solid;
border-width: 0px;
border-top-width: 1px;
}
tbody th:empty {
background-position: 0px -10px;
}
td.psp-select-region,
th.psp-select-region {
/* transition: background-color 0.5s, color 0.5s, border-color 0.5s; */
background-color: var(--psp--color) !important;
color: var(--psp--background-color) !important;
border-color: var(--psp--background-color) !important;
}
}
regular-table
tbody
tr:hover
td.psp-select-region:not(.psp-select-region-inactive),
regular-table
tbody
tr:hover
+ tr
td.psp-select-region:not(.psp-select-region-inactive) {
border-color: var(--psp--background-color) !important;
}
regular-table tbody tr:hover {
td.psp-select-region.psp-menu-open {
&:not(.psp-select-region-inactive) {
box-shadow:
inset -2px 0px 0px var(--psp--background-color),
inset 2px 0px 0px var(--psp--background-color);
}
}
}
:host-context(perspective-viewer[settings]) td.psp-select-region.psp-menu-open {
box-shadow:
inset -2px 0px 0px var(--psp--background-color),
inset 2px 0px 0px var(--psp--background-color);
}
/* This is a design/architecture bug. When the datagrid draws during a sidepanel */
/* collapse/uncollapse, the `settings` attribute is set to the _previous_ state */
/* as this collapse takes a while and we dont want the new state CSS to apply */
/* until the drawing is done. However, this causes the datagrid to draw its */
/* _first_ frame as if its in the wrong state, as it detects sidepanel state */
/* via HTML attribute checking. */
regular-table
#psp-column-edit-buttons
th:not(.rt-group-corner)
span:not(.rt-column-resize):before {
content: var(--psp-datagrid--column-edit-button--content, "Edit");
}
perspective-viewer:not([settings]),
:host-context(perspective-viewer:not([settings])) {
regular-table #psp-column-edit-buttons:after {
content: none;
}
}
/* Style the last row of the `<thead>` so that is has a bottom border. */
regular-table table thead tr:last-child:after {
width: 10000px;
box-sizing: border-box;
display: block;
height: var(--psp-datagrid--row--height, 23px);
content: " ";
border-bottom: 1px solid var(--psp-inactive--border-color);
}
/* Extend every row to the right edge of the page with a fake column. */
regular-table table tbody tr:after {
width: 10000px;
box-sizing: border-box;
display: block;
height: var(--psp-datagrid--row--height, 23px);
content: " ";
border-top: 1px solid transparent;
}
regular-table table tbody tr:not(:first-child):after {
border-top: 1px solid var(--psp-inactive--border-color);
}
regular-table table tbody tr:hover:not(:first-child):after {
border-top: 1px solid var(--psp-datagrid--hover--border-color);
}
regular-table table tbody tr:hover + tr:after {
border-top: 1px solid var(--psp-datagrid--hover--border-color);
}
@keyframes pulse_pos {
0% {
background-color: var(
--pulse--background-color-start,
rgba(0, 128, 255, 0.5)
);
}
100% {
background-color: var(
--pulse--background-color-end,
rgba(0, 128, 255, 0)
);
}
}
@keyframes pulse_pos2 {
0% {
background-color: var(
--pulse--background-color-start,
rgba(0, 128, 255, 0.5)
);
}
100% {
background-color: var(
--pulse--background-color-end,
rgba(0, 128, 255, 0)
);
}
}
@keyframes pulse_neg {
0% {
background-color: var(
--pulse--background-color-start,
rgba(255, 25, 0, 0.5)
);
}
100% {
background-color: var(
--pulse--background-color-end,
rgba(255, 25, 0, 0)
);
}
}
@keyframes pulse_neg2 {
0% {
background-color: var(
--pulse--background-color-start,
rgba(255, 25, 0, 0.5)
);
}
100% {
background-color: var(
--pulse--background-color-end,
rgba(255, 25, 0, 0)
);
}
}
@@ -0,0 +1,107 @@
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
* ┃ Copyright (c) 2017, the Perspective Authors. ┃
* ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
* ┃ This file is part of the Perspective library, distributed under the terms ┃
* ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
*/
regular-table {
tbody {
tr:hover
th.psp-tree-leaf:not(.psp-select-region):not(
.psp-select-region-inactive
),
tr:hover
th.psp-tree-label:not(.psp-select-region):not(
.psp-select-region-inactive
),
tr:hover td:not(.psp-select-region):not(.psp-select-region-inactive),
tr:hover:after {
border-color: var(
--psp-datagrid--hover--border-color,
#c5c9d080
) !important;
box-shadow: 0px 1px 0px
var(--psp-datagrid--hover--border-color, #c5c9d080);
&.psp-menu-open {
box-shadow:
inset -2px 0px 0px var(--psp--color),
inset 2px 0px 0px var(--psp--color),
0px 1px 0px
var(--psp-datagrid--hover--border-color, #c5c9d080);
}
}
tr:last-child:hover
td:not(.psp-select-region):not(
.psp-select-region-inactive
).psp-menu-open {
box-shadow:
inset -2px 0px 0px var(--psp--color),
inset 2px 0px 0px var(--psp--color),
inset 0px -2px 0px var(--psp--color),
0px 1px 0px var(--psp-datagrid--hover--border-color, #c5c9d080);
}
tr:hover
+ tr
th.psp-tree-leaf:not(.psp-select-region):not(
.psp-select-region-inactive
),
tr:hover
+ tr
th.psp-tree-label:not(.psp-select-region):not(
.psp-select-region-inactive
),
tr:hover
+ tr
td:not(.psp-select-region):not(.psp-select-region-inactive) {
border-top-color: transparent;
}
tr {
th:first-child:not(:empty),
th:first-child:empty + th:not(:empty),
th:first-child:empty ~ th:empty + th:not(:empty),
td:first-child {
border-left-width: 1px;
border-left-color: transparent;
}
th:last-child,
td:last-child {
border-right-width: 0px;
border-right-color: transparent;
}
}
tr:hover {
color: inherit;
th:first-child:not(:empty),
th:first-child:empty + th:not(:empty),
th:first-child:empty ~ th:empty + th:not(:empty),
td:first-child {
border-left-color: var(
--psp-datagrid--hover--border-color,
#c5c9d080
) !important;
}
th:last-child,
td:last-child {
border-right-color: var(
--psp-datagrid--hover--border-color,
#c5c9d080
) !important;
}
}
}
}
@@ -0,0 +1,35 @@
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
* ┃ Copyright (c) 2017, the Perspective Authors. ┃
* ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
* ┃ This file is part of the Perspective library, distributed under the terms ┃
* ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
*/
/* Scrollbar styling */
regular-table {
outline: none;
/* webkit (chrome, safari, etc) scrollbar styling */
&::-webkit-scrollbar,
&::-webkit-scrollbar-corner {
background-color: transparent;
height: 12px;
width: 12px;
}
&::-webkit-scrollbar-thumb {
background-clip: content-box;
background: var(--psp--color);
border: 5.5px solid var(--psp--background-color);
max-height: 50%;
max-width: 50%;
min-width: 10%;
min-height: 10%;
}
}
@@ -0,0 +1,83 @@
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
* ┃ Copyright (c) 2017, the Perspective Authors. ┃
* ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
* ┃ This file is part of the Perspective library, distributed under the terms ┃
* ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
*/
/* Handles sub-cell scrolling via CSS transform offset by variables set during */
/* rendering. */
perspective-viewer-datagrid:not(.sub-cell-scroll-disabled) regular-table table,
:host(:not(.sub-cell-scroll-disabled)) regular-table table {
tbody td,
thead th:not(.rt-group-corner) {
transform: translate(var(--regular-table--transform-x, 0px));
}
tbody {
transform: translate(0, var(--regular-table--transform-y, 0px));
}
tbody tr:first-child {
td,
th,
&:after {
clip-path: polygon(
0 var(--regular-table--clip-y, 0),
0 200%,
200% 200%,
200% var(--regular-table--clip-y, 0)
);
}
}
tbody tr td:first-of-type {
clip-path: polygon(
var(--regular-table--clip-x, 0) 0,
var(--regular-table--clip-x, 0) 200%,
200% 200%,
200% 0
);
}
tbody tr:first-child td:first-of-type {
clip-path: polygon(
var(--regular-table--clip-x, 0) var(--regular-table--clip-y, 0),
var(--regular-table--clip-x, 0) 200%,
200% 200%,
200% var(--regular-table--clip-y, 0)
);
}
thead tr:last-child:after {
transform: translate(var(--regular-table--transform-x, 0px));
}
tbody tr:not(:first-child):after {
transform: translate(var(--regular-table--transform-x, 0px));
}
tbody tr:hover:first-child:after {
clip-path: polygon(
0 var(--regular-table--clip-y, 0),
0 200%,
200% 200%,
200% var(--regular-table--clip-y, 0)
);
}
thead tr th.rt-group-corner + th:not(.rt-group-corner) {
clip-path: polygon(
var(--regular-table--clip-x, 0) 0,
var(--regular-table--clip-x, 0) 200%,
200% 200%,
200% 0
);
}
}
@@ -0,0 +1,219 @@
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
* ┃ Copyright (c) 2017, the Perspective Authors. ┃
* ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
* ┃ This file is part of the Perspective library, distributed under the terms ┃
* ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
*/
:host {
position: relative;
display: block;
height: 100%;
}
:host #container {
position: absolute;
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
:host #toolbar {
display: flex;
align-items: stretch;
height: 100%;
}
:host #toolbar .hover-target {
margin: 0;
display: inline-flex;
align-items: center;
cursor: pointer;
&:hover {
box-shadow:
-4px 0 0 var(--psp--color),
4px 0 0 var(--psp--color);
background-color: var(--psp--color);
}
}
:host #slot-container {
flex: 1;
position: relative;
}
#scroll_lock.lock-scroll:before {
-webkit-mask-image: var(--psp-toolbar-scroll-lock-active--content);
}
#scroll_lock:before {
-webkit-mask-image: var(--psp-toolbar-scroll-lock--content);
}
#select_mode:before {
content: "highlight_alt";
}
#edit_mode:before {
-webkit-mask-image: "";
}
#edit_mode[data-edit-mode="READ_ONLY"]:before {
-webkit-mask-image: var(--psp-toolbar-edit-mode-read-only--content);
}
#edit_mode[data-edit-mode="EDIT"]:before {
-webkit-mask-image: var(--psp-toolbar-edit-mode-edit--content);
}
:host(.aggregated) #toolbar #edit_mode[data-edit-mode="EDIT"]:before {
-webkit-mask-image: var(--psp-toolbar-edit-mode-read-only--content);
}
#edit_mode[data-edit-mode="SELECT_ROW"]:before {
-webkit-mask-image: var(--psp-toolbar-edit-mode-select-row--content);
}
#edit_mode[data-edit-mode="SELECT_COLUMN"]:before {
-webkit-mask-image: var(--psp-toolbar-edit-mode-select-column--content);
}
#edit_mode[data-edit-mode="SELECT_REGION"]:before {
-webkit-mask-image: var(--psp-toolbar-edit-mode-select-region--content);
}
#edit_mode[data-edit-mode="SELECT_ROW_TREE"]:before {
-webkit-mask-image: var(--psp-toolbar-edit-mode-select-row-tree--content);
}
/* #edit_mode span:before { */
/* content: var(--edit-mode-toggle--content, "N/A"); */
/* } */
#edit_mode[data-edit-mode="READ_ONLY"] span:before {
content: var(--psp-label--edit-mode-read-only--content, "Read Only");
}
#edit_mode[data-edit-mode="EDIT"] span:before {
content: var(--psp-label--edit-mode-edit--content, "Editable");
}
#edit_mode[data-edit-mode="SELECT_ROW"] span:before {
content: var(--psp-label--edit-mode-select-row--content, "Row Select");
}
#edit_mode[data-edit-mode="SELECT_COLUMN"] span:before {
content: var(
--psp-label--edit-mode-select-column--content,
"Column Select"
);
}
#edit_mode[data-edit-mode="SELECT_REGION"] span:before {
content: var(
--psp-label--edit-mode-select-region--content,
"Region Select"
);
}
#edit_mode[data-edit-mode="SELECT_ROW_TREE"] span:before {
content: var(
--psp-label--edit-mode-select-row-tree--content,
"Tree Select"
);
}
#scroll_lock span:before {
content: var(--psp-label--scroll-lock-toggle--content, "Free Scroll");
}
#scroll_lock.lock-scroll span:before {
content: var(--psp-label--scroll-lock-alt-toggle--content, "Align Scroll");
}
/* The icon. */
.button:before {
width: 21px;
height: 21px;
content: "";
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--psp--color);
}
.button.editable:before,
.button.lock-scroll:before {
color: inherit;
}
.button {
display: inline-flex;
justify-content: center;
align-items: center;
user-select: none;
padding: 0 5px;
border: 1px solid transparent;
border-radius: 3px;
border: 1px solid transparent;
box-sizing: border-box;
display: inline-flex;
font-size: var(--label--font-size, 0.75em);
height: 22px;
user-select: none;
white-space: nowrap;
width: 37px;
}
.button > span {
display: none;
margin: 0;
padding: 0;
}
.hover-target:focus-within .button,
.hover-target:hover .button {
position: relative;
background-color: var(--psp--color);
color: var(--psp--background-color);
opacity: 1;
display: flex;
align-items: center;
cursor: pointer;
}
.hover-target:focus-within .button:before,
.hover-target:hover .button:before {
background-color: var(--psp--background-color);
}
/* The label. */
.hover-target:focus-within .button > span,
.hover-target:hover .button > span {
display: block;
position: absolute;
top: calc(100% + 3px);
left: 50%;
translate: -50%;
margin: 0;
padding: 5px;
height: auto;
white-space: pre-wrap;
line-height: 1;
font-size: 9px;
background-color: var(--psp--color);
width: 35px;
text-align: center;
border-radius: 0 0 3px 3px;
}
@@ -0,0 +1,234 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { ColorRecord } from "./types.js";
/** 8-bit sRGB color as `[r, g, b]` with each channel in `[0, 255]`. */
export type RGB = [number, number, number];
/** HSL color as `[h, s, l]` with `h` in degrees `[0, 360)` and `s`, `l` in `[0, 1]`. */
export type HSL = [number, number, number];
const parse_cache = new Map<string, RGB>();
let parse_ctx: CanvasRenderingContext2D | null = null;
/** Parse a CSS hex color (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`). Returns `null` if `input` is not a hex literal. Alpha is ignored. */
function parse_hex(input: string): RGB | null {
const s = input.startsWith("#") ? input.slice(1) : input;
if (s.length === 3 || s.length === 4) {
const r = parseInt(s[0] + s[0], 16);
const g = parseInt(s[1] + s[1], 16);
const b = parseInt(s[2] + s[2], 16);
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
return [r, g, b];
}
} else if (s.length === 6 || s.length === 8) {
const r = parseInt(s.slice(0, 2), 16);
const g = parseInt(s.slice(2, 4), 16);
const b = parseInt(s.slice(4, 6), 16);
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
return [r, g, b];
}
}
return null;
}
/** Parse a CSS `rgb()` or `rgba()` functional color. Returns `null` if `input` does not match. Alpha is ignored. */
function parse_rgb_fn(input: string): RGB | null {
const m = input.match(
/^rgba?\(\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*[, ]\s*([\d.]+)/i,
);
if (!m) {
return null;
}
return [
Math.round(parseFloat(m[1])),
Math.round(parseFloat(m[2])),
Math.round(parseFloat(m[3])),
];
}
/** Fallback parser that defers to the browser by assigning `input` to a 2D canvas `fillStyle` and re-reading the normalized value. Handles named colors, `hsl()`, etc. Returns `[0, 0, 0]` if the value is invalid or no canvas context is available. */
function parse_via_canvas(input: string): RGB {
if (!parse_ctx) {
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
parse_ctx = canvas.getContext("2d");
}
if (!parse_ctx) {
return [0, 0, 0];
}
parse_ctx.fillStyle = "#000";
parse_ctx.fillStyle = input;
const normalized = parse_ctx.fillStyle as string;
return parse_hex(normalized) ?? parse_rgb_fn(normalized) ?? [0, 0, 0];
}
/** Parse any CSS color string into an `RGB` triple. Tries hex and `rgb()` fast paths, then falls back to a canvas-based parser for named colors, `hsl()`, etc. Results are memoized per input. */
export function parseColor(input: string): RGB {
const key = input.trim();
const cached = parse_cache.get(key);
if (cached) {
return cached;
}
const rgb = parse_hex(key) ?? parse_rgb_fn(key) ?? parse_via_canvas(key);
parse_cache.set(key, rgb);
return rgb;
}
/** Format a single channel as a clamped, zero-padded two-digit hex byte. */
function toHex(c: number): string {
const v = Math.max(0, Math.min(255, Math.round(c)));
return v.toString(16).padStart(2, "0");
}
/** Format an `RGB` triple as a `#rrggbb` hex string. Channels are clamped to `[0, 255]`. */
export function rgbToHex([r, g, b]: RGB): string {
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/** Convert sRGB to HSL. Output `h` is in degrees `[0, 360)`; `s` and `l` are in `[0, 1]`. */
export function rgbToHsl([r, g, b]: RGB): HSL {
const rn = r / 255,
gn = g / 255,
bn = b / 255;
const max = Math.max(rn, gn, bn);
const min = Math.min(rn, gn, bn);
const l = (max + min) / 2;
const d = max - min;
let h = 0;
let s = 0;
if (d !== 0) {
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === rn) {
h = ((gn - bn) / d + (gn < bn ? 6 : 0)) * 60;
} else if (max === gn) {
h = ((bn - rn) / d + 2) * 60;
} else {
h = ((rn - gn) / d + 4) * 60;
}
}
return [h, s, l];
}
/** Convert HSL to sRGB. `h` is wrapped into `[0, 360)`; `s` and `l` are expected in `[0, 1]`. Output channels are rounded to integers in `[0, 255]`. */
export function hslToRgb([h, s, l]: HSL): RGB {
const hn = (((h % 360) + 360) % 360) / 360;
if (s === 0) {
const v = Math.round(l * 255);
return [v, v, v];
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
const f = (t: number): number => {
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
};
return [
Math.round(f(hn + 1 / 3) * 255),
Math.round(f(hn) * 255),
Math.round(f(hn - 1 / 3) * 255),
];
}
/**
* Blend two `RGB` colors using LRGB (gamma-naive linear) interpolation,
* matching chroma-js's default `mix` mode. `f` is the weight of `b` in `[0, 1]`
* (0 returns `a`, 1 returns `b`).
*/
export function mixRgb(a: RGB, b: RGB, f = 0.5): RGB {
return [
Math.round(Math.sqrt(a[0] * a[0] * (1 - f) + b[0] * b[0] * f)),
Math.round(Math.sqrt(a[1] * a[1] * (1 - f) + b[1] * b[1] * f)),
Math.round(Math.sqrt(a[2] * a[2] * (1 - f) + b[2] * b[2] * f)),
];
}
/** 50/50 LRGB blend of CSS color `a` with `RGB`-ish triple `b`, returned as `#rrggbb`. */
export function blend(a: string, b: number[]): string {
return rgbToHex(mixRgb(parseColor(a), [b[0], b[1], b[2]], 0.5));
}
/** Composite a premultiplied-style `RGBA` cell color over `source` (default white) and return the resulting opaque `RGB`. Used to flatten heatmap cells against the background. */
export function rgbaToRgb(
[r, g, b, a]: [number, number, number, number],
source: [number, number, number] = [255, 255, 255],
): [number, number, number] {
function f(i: number, c: number): number {
return ((1 - a) * (source[i] / 255) + a * (c / 255)) * 255;
}
return [f(0, r), f(1, g), f(2, b)];
}
/** Pick a readable foreground (`#161616` or `#ffffff`) for the given background using a perceptual luminance threshold. */
export function infer_foreground_from_background([r, g, b]: [
number,
number,
number,
]): string {
// TODO Implement dark/light themes.
return Math.sqrt(r * r * 0.299 + g * g * 0.587 + b * b * 0.114) > 130
? "#161616"
: "#ffffff";
}
/** Build a CSS `linear-gradient` that fans `rgb` ±15° in hue, used as the negative-value swatch in column color pickers. */
function make_gradient(rgb: RGB): string {
const [h, s, l] = rgbToHsl(rgb);
const [r, g, b] = rgb;
const [r1, g1, b1] = hslToRgb([h - 15, s, l]);
const [r2, g2, b2] = hslToRgb([h + 15, s, l]);
return `linear-gradient(to right top,rgb(${r1},${g1},${b1}),rgb(${r},${g},${b}) 50%,rgb(${r2},${g2},${b2}))`;
}
/** Precompute the tuple of derived color strings (RGB channels, gradient, opaque/transparent rgba) cached on the model for a configured plugin color. */
export function make_color_record(color: string): ColorRecord {
const rgb = parseColor(color);
const _neg_grad = make_gradient(rgb);
return [
color,
rgb[0],
rgb[1],
rgb[2],
_neg_grad,
`rgba(${rgb[0]},${rgb[1]},${rgb[2]},1)`,
`rgba(${rgb[0]},${rgb[1]},${rgb[2]},0)`,
];
}
@@ -0,0 +1,269 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { PRIVATE_PLUGIN_SYMBOL } from "../model/index.js";
import { activate } from "../plugin/activate.js";
import { restore } from "../plugin/restore.js";
import { save } from "../plugin/save.js";
import { draw } from "../plugin/draw.js";
import column_config_schema, {
ColumnConfigSchema,
} from "../plugin/column_config_schema.js";
import datagridStyles from "../../../dist/css/perspective-viewer-datagrid.css";
import { format_raw } from "../data_listener/format_cell.js";
import { sourceColumn } from "@perspective-dev/viewer/src/ts/column-format.js";
import type { View, ViewWindow } from "@perspective-dev/client";
import type {
IPerspectiveViewerPlugin,
PluginStaticConfig,
} from "@perspective-dev/viewer";
import type {
DatagridModel,
DatagridToolbarElement,
EditMode,
DatagridPluginConfig,
ColumnsConfig,
} from "../types.js";
import { RegularTableElement } from "regular-table";
type RenderTarget = "shadow" | "light";
/**
* The custom element class for this plugin. The interface methods for this
*/
export class HTMLPerspectiveViewerDatagridPluginElement
extends HTMLElement
implements IPerspectiveViewerPlugin
{
private static _global_stylesheet_installed: boolean = false;
private static _sheet: CSSStyleSheet | undefined;
// Determines whether this datagrid renders in the light DOM. This will
// break style encapsulation and may cause inconsistent behavior.
static renderTarget: RenderTarget =
window.CSS?.supports &&
window.CSS?.supports("selector(:host-context(foo))")
? "shadow"
: "light";
regular_table: RegularTableElement;
model?: DatagridModel;
_toolbar?: DatagridToolbarElement;
_edit_button?: HTMLElement;
_scroll_lock?: HTMLElement;
_is_scroll_lock: boolean;
_edit_mode: EditMode;
_initialized?: boolean;
_reset_scroll_top?: boolean;
_reset_scroll_left?: boolean;
_reset_select?: boolean;
_reset_column_size?: boolean;
constructor() {
super();
this.regular_table = document.createElement(
"regular-table",
) as RegularTableElement;
this.regular_table.part = "regular-table";
this._is_scroll_lock = false;
this._edit_mode = "READ_ONLY";
const Elem = HTMLPerspectiveViewerDatagridPluginElement;
if (!Elem._sheet) {
Elem._sheet = new CSSStyleSheet();
Elem._sheet.replaceSync(datagridStyles);
}
if (Elem.renderTarget === "shadow") {
const shadow = this.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets.push(Elem._sheet);
} else if (
Elem.renderTarget === "light" &&
!Elem._global_stylesheet_installed
) {
Elem._global_stylesheet_installed = true;
document.adoptedStyleSheets.push(Elem._sheet);
}
}
connectedCallback(): void {
if (!this._toolbar) {
this._toolbar = document.createElement(
"perspective-viewer-datagrid-toolbar",
) as DatagridToolbarElement;
}
const parent = this.parentElement;
if (parent) {
parent.appendChild(this._toolbar);
}
}
disconnectedCallback(): void {
this._toolbar?.parentElement?.removeChild?.(this._toolbar);
}
async activate(view: View): Promise<void> {
return await activate.call(this, view);
}
get_static_config(): PluginStaticConfig {
return {
name: "Datagrid",
category: "Basic",
select_mode: "toggle",
config_column_names: ["Columns"],
group_rollup_modes: ["rollup", "flat", "total"],
// Higher priority than the chart plugins so the Datagrid is
// loaded by default.
priority: 1,
can_render_column_styles: true,
};
}
plugin_config_schema(): ColumnConfigSchema {
const fields = [];
fields.push({
kind: "Enum",
key: "edit_mode",
default: "READ_ONLY",
variants: [
{ value: "EDIT", label: "Edit" },
{ value: "READ_ONLY", label: "Read-only" },
{ value: "SELECT_ROW", label: "Row Select" },
{ value: "SELECT_COLUMN", label: "Column Select" },
{ value: "SELECT_REGION", label: "Region Select" },
{ value: "SELECT_ROW_TREE", label: "Tree Select" },
],
});
fields.push({
kind: "Bool",
key: "scroll_lock",
default: false,
});
return {
fields,
};
}
column_config_schema(
type: string,
group: string | undefined,
column_name: string,
current_value: Record<string, unknown> | null,
viewer_config?: { group_by?: string[]; group_rollup_mode?: string },
column_stats?: { abs_max: number },
): ColumnConfigSchema {
return column_config_schema.call(
this,
type as any,
group,
column_name,
current_value,
viewer_config,
column_stats,
);
}
async draw(view: View): Promise<void> {
return await draw.call(this, view);
}
async update(view: View): Promise<void> {
if (this.model === undefined) {
await this.draw(view);
} else if (this.model._config.split_by?.length > 0) {
const dimensions = await view.dimensions();
this.model._num_rows = dimensions.num_view_rows;
await this.regular_table.draw();
} else {
this.model._num_rows = await view.num_rows();
await this.regular_table.draw();
}
}
async render(view: View, viewport?: ViewWindow): Promise<string> {
const json = await view.to_columns(viewport as any);
const cols = await view.column_paths(viewport as any);
const nrows =
viewport?.end_row !== undefined &&
viewport?.end_row !== null &&
viewport?.start_row !== undefined &&
viewport?.start_row !== null
? viewport.end_row - viewport.start_row
: await view.num_rows();
let out = "";
for (let ridx = 0; ridx < nrows; ridx++) {
for (const col_name of cols) {
const col = (json as Record<string, unknown[]>)[col_name];
const type = this.model!._schema[col_name];
const pluginConfig = (this.regular_table as any)[
PRIVATE_PLUGIN_SYMBOL
] as ColumnsConfig | undefined;
const columnName = sourceColumn(col_name);
const formatter = format_raw(
type,
pluginConfig?.[columnName] || {},
);
if (formatter) {
out += formatter.format(col[ridx]) + "\t";
} else {
out += col[ridx] + "\t";
}
}
out += "\n";
}
return out.trim();
}
async resize(_view: View): Promise<void> {
if (!this.isConnected || this.offsetParent == null) {
return;
}
if (this._initialized) {
await this.regular_table.draw();
}
}
async clear(): Promise<void> {
this.regular_table.resetAutoSize();
this.regular_table.clear();
}
save(): any {
return save.call(this);
}
restore(token: DatagridPluginConfig, columns_config?: ColumnsConfig): void {
return restore.call(this, token, columns_config ?? {});
}
restyle() {}
delete(): void {
this.disconnectedCallback();
this._toolbar = undefined;
if ((this.regular_table as any).table_model) {
this.regular_table.resetAutoSize();
}
this.regular_table.clear();
}
}
@@ -0,0 +1,74 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
import TOOLBAR_STYLE from "../../../dist/css/perspective-viewer-datagrid-toolbar.css";
import { toggle_edit_mode, toggle_scroll_lock } from "../model/toolbar.js";
import type { DatagridPluginElement } from "../types.js";
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync(TOOLBAR_STYLE);
/**
* The custom element for this plugin's toolbar, a component which displays in
* the host `<perspective-viewer>`'s status bar when this plugin is active.
* In the case of Datagrid, this comprises "Editable" and "Scroll Lock" toggle
* buttons.
*/
export class HTMLPerspectiveViewerDatagridToolbarElement extends HTMLElement {
private _initialized: boolean = false;
connectedCallback(): void {
if (this._initialized) {
return;
}
this._initialized = true;
this.setAttribute("slot", "statusbar-extra");
this.attachShadow({ mode: "open" });
this.shadowRoot!.adoptedStyleSheets.push(stylesheet);
this.shadowRoot!.innerHTML = `
<div id="toolbar">
<span class="hover-target">
<span id="scroll_lock" class="button">
<span></span>
</span>
</span>
<span class="hover-target">
<span id="edit_mode" class="button" data-edit-mode="READ_ONLY">
<span></span>
</span>
</span>
</div>
`;
const viewer = this.parentElement as HTMLPerspectiveViewerElement;
const plugin = this.previousElementSibling as DatagridPluginElement;
plugin._scroll_lock = this.shadowRoot!.querySelector(
"#scroll_lock",
) as HTMLElement;
plugin._scroll_lock.addEventListener("click", () =>
toggle_scroll_lock.call(plugin),
);
plugin._edit_button = this.shadowRoot!.querySelector(
"#edit_mode",
) as HTMLElement;
plugin._edit_button.addEventListener("click", () => {
toggle_edit_mode.call(plugin);
plugin.regular_table.draw();
viewer.dispatchEvent(new Event("perspective-config-update"));
});
}
}
@@ -0,0 +1,103 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { FormatterCache, Formatter } from "./formatter_cache.js";
import type { DatagridModel, ColumnsConfig, ColumnConfig } from "../types.js";
import type { ColumnType } from "@perspective-dev/client";
const FORMAT_CACHE = new FormatterCache();
const MAX_BAR_WIDTH_PCT = 1;
export function format_raw(
type: ColumnType,
value: ColumnConfig,
): Formatter | false | undefined {
return FORMAT_CACHE.get(type, value);
}
/**
* Format a single cell's text content as the content of a `<td>` or `<th>`.
*/
export function format_cell(
this: DatagridModel,
title: string,
val: unknown,
plugins: ColumnsConfig = {},
use_table_schema = false,
): string | HTMLElement | null {
if (val === null) {
return null;
}
const type: ColumnType = ((use_table_schema && this._table_schema[title]) ||
this._schema[title] ||
"string") as ColumnType;
const plugin: ColumnConfig = plugins[title] || {};
const is_numeric = type === "integer" || type === "float";
if (
is_numeric &&
(plugin?.number_fg_mode === "bar" ||
plugin?.number_fg_mode === "label-bar")
) {
const a = Math.max(
0,
Math.min(
MAX_BAR_WIDTH_PCT,
Math.abs((val as number) / plugin.fg_gradient!) *
MAX_BAR_WIDTH_PCT,
),
);
const anchor = (val as number) >= 0 ? "" : "justify-self:flex-end;";
const pct = (a * 100).toFixed(2);
if (plugin.number_fg_mode === "bar") {
const div = this._div_factory.get();
div.className = "psp-bar";
div.setAttribute(
"style",
`${anchor}width:${pct}%;height:80%;top:10%;pointer-events:none;background:var(--psp-label-bar-color)`,
);
return div;
} else {
const formatter = FORMAT_CACHE.get(type, plugin);
const label = formatter ? formatter.format(val) : (val as string);
const div = this._div_factory.get();
div.className = "psp-bar";
div.setAttribute(
"style",
`--label:"${label}";${anchor}width:${pct}%;height:80%;top:10%;pointer-events:none;background:var(--psp-label-bar-color)`,
);
return div;
}
} else if (plugin?.format === "link" && type === "string") {
const anchor = document.createElement("a");
anchor.setAttribute("href", val as string);
anchor.setAttribute("target", "_blank");
anchor.textContent = val as string;
return anchor;
} else if (plugin?.format === "bold" && type === "string") {
const bold = document.createElement("b");
bold.textContent = val as string;
return bold;
} else if (plugin?.format === "italics" && type === "string") {
const italic = document.createElement("i");
italic.textContent = val as string;
return italic;
} else {
const formatter = FORMAT_CACHE.get(type, plugin);
return formatter ? formatter.format(val) : (val as string);
}
}
@@ -0,0 +1,98 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { PRIVATE_PLUGIN_SYMBOL } from "../types.js";
import { format_cell } from "./format_cell.js";
import type { DatagridModel, RegularTable, ColumnsConfig } from "../types.js";
type RowHeaderCell = string | HTMLElement | { toString(): string };
/**
* Format a single cell of the `group_by` tree header for __ROW_PATH__ data.
*/
export function* format_tree_header_row_path(
this: DatagridModel,
paths: unknown[][] = [],
row_headers: string[],
regularTable: RegularTable,
): Generator<RowHeaderCell[]> {
const plugins: ColumnsConfig =
(regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {};
for (const path of paths) {
const fullPath: unknown[] = ["TOTAL", ...path];
const last = fullPath[fullPath.length - 1];
let newPath: RowHeaderCell[] = fullPath
.slice(0, fullPath.length - 1)
.fill("") as string[];
const formatted = format_cell.call(
this,
row_headers[newPath.length - 1],
last,
plugins,
true,
);
if (formatted instanceof HTMLElement) {
newPath = newPath.concat(formatted);
} else {
newPath = newPath.concat({ toString: () => formatted as string });
}
newPath.length = row_headers.length + 1;
yield newPath;
}
}
export function* format_flat_header_row_path(
this: DatagridModel,
paths: unknown[][] = [],
row_headers: string[],
regularTable: RegularTable,
): Generator<RowHeaderCell[]> {
const plugins: ColumnsConfig =
(regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {};
for (const path of paths) {
yield path.map((part, i) =>
format_cell.call(this, row_headers[i], part, plugins, true),
) as RowHeaderCell[];
}
}
/**
* Format a single cell of the `group_by` tree header.
*/
export function* format_tree_header(
this: DatagridModel,
paths: unknown[][] = [],
row_headers: string[],
regularTable: RegularTable,
): Generator<unknown[]> {
const plugins: ColumnsConfig =
(regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {};
for (const path of paths) {
const new_path: unknown[] = [""];
for (const idx in path) {
new_path.push(
format_cell.call(
this,
row_headers[idx],
path[idx],
plugins,
true,
),
);
}
yield path;
}
}
@@ -0,0 +1,104 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { ColumnType } from "@perspective-dev/client";
import {
createDateFormatter,
createDatetimeFormatter,
createNumberFormatter,
} from "@perspective-dev/viewer/src/ts/column-format.js";
import type { ColumnConfig } from "../types.js";
export interface Formatter {
format(val: unknown): string;
}
class BooleanFormatter implements Formatter {
format(val: unknown): string {
return val ? "true" : "false";
}
}
// PluginConfig is a subset of ColumnConfig with the formatting properties
type PluginConfig = Pick<ColumnConfig, "date_format" | "number_format">;
export class FormatterCache {
private _formatters: Map<string, Formatter | false>;
constructor() {
this._formatters = new Map();
}
private create_datetime_formatter(
_type: ColumnType,
plugin: PluginConfig,
): Intl.DateTimeFormat {
return createDatetimeFormatter(plugin.date_format);
}
private create_date_formatter(
_type: ColumnType,
plugin: PluginConfig,
): Intl.DateTimeFormat {
return createDateFormatter(plugin.date_format);
}
private create_number_formatter(
type: ColumnType,
plugin: PluginConfig,
): Intl.NumberFormat {
return createNumberFormatter(type, plugin.number_format);
}
private create_boolean_formatter(
_type: ColumnType,
_plugin: PluginConfig,
): Formatter {
return new BooleanFormatter();
}
get(type: ColumnType, plugin: PluginConfig): Formatter | false | undefined {
const formatter_key = [
type,
...Object.values(plugin.date_format ?? {}),
...Object.values(plugin.number_format ?? {}),
].join("-");
if (!this._formatters.has(formatter_key)) {
if (type === "date") {
this._formatters.set(
formatter_key,
this.create_date_formatter(type, plugin),
);
} else if (type === "datetime") {
this._formatters.set(
formatter_key,
this.create_datetime_formatter(type, plugin),
);
} else if (type === "integer" || type === "float") {
this._formatters.set(
formatter_key,
this.create_number_formatter(type, plugin),
);
} else if (type === "boolean") {
this._formatters.set(
formatter_key,
this.create_boolean_formatter(type, plugin),
);
} else {
this._formatters.set(formatter_key, false);
}
}
return this._formatters.get(formatter_key);
}
}
@@ -0,0 +1,264 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { PRIVATE_PLUGIN_SYMBOL } from "../types.js";
import { isMetaColumn } from "../model/meta_columns.js";
import { format_cell } from "./format_cell.js";
import {
format_flat_header_row_path,
format_tree_header,
format_tree_header_row_path,
} from "./format_tree_header.js";
import type { DatagridModel, RegularTable, Schema } from "../types.js";
import type { CellScalar, DataResponse } from "regular-table/dist/esm/types.js";
import type { ViewConfig, ViewWindow } from "@perspective-dev/client";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
interface ColumnData {
__ROW_PATH__?: unknown[][];
__ID__?: unknown[][];
[key: string]: unknown[] | undefined;
}
/**
* Creates a new DataListener, suitable for passing to `regular-table`'s
* `.setDataListener()` method. This should be called as a method on the
* plugin.
*
* @returns A data listener for the plugin.
*/
export function createDataListener(
viewer: HTMLPerspectiveViewerElement,
): (
regularTable: RegularTable,
x0: number,
y0: number,
x1: number,
y1: number,
) => Promise<DataResponse> {
let last_meta: unknown[][] | undefined;
let last_column_paths: string[] | undefined;
let last_ids: unknown[][] | undefined;
let last_reverse_ids: Map<string, number> | undefined;
let last_reverse_columns: Map<string, number> | undefined;
return async function dataListener(
this: DatagridModel,
regularTable: RegularTable,
x0: number,
y0: number,
x1: number,
y1: number,
): Promise<DataResponse> {
let columns: ColumnData = {};
const new_window: ViewWindow = {
start_row: y0,
start_col: x0,
end_row: y1,
end_col: x1,
id: true,
};
let num_columns: number | null = null;
if (x1 - x0 > 0 && y1 - y0 > 0) {
this._is_old_viewport =
this._last_window?.start_row === y0 &&
this._last_window?.end_row === y1 &&
this._last_window?.start_col === x0 &&
this._last_window?.end_col === x1;
const [x, z] = await Promise.all([
this._view.to_columns_string(new_window as any),
this._view.num_columns(),
]);
num_columns = z;
columns = JSON.parse(x as string) as ColumnData;
const y = Object.keys(columns);
// `isMetaColumn` covers `__ROW_PATH__`, `__ID__`,
// `__GROUPING_ID__`, and the per-level `__ROW_PATH_<n>__`
// columns the DuckDB virtual server emits inline alongside
// the JSON sidecar. Exact-match against `__ROW_PATH__` /
// `__ID__` (the previous filter) misses the per-level
// form and the virtual server's columns leak into the
// visible grid.
const new_col_paths = y.filter((x) => !isMetaColumn(x));
let changed_cols = false;
for (let i = 0; i < new_col_paths.length; i++) {
if (
this._column_paths[new_window.start_col! + i] !==
new_col_paths[i]
) {
changed_cols = true;
this._column_paths[new_window.start_col! + i] =
new_col_paths[i];
}
}
if (changed_cols) {
const [a, b] = await Promise.all([
this._view.schema(),
this._view.expression_schema(),
]);
this._schema = { ...(a as Schema), ...(b as Schema) };
for (let i = 0; i < new_col_paths.length; i++) {
const column_path_parts = new_col_paths[i].split("|");
const column =
column_path_parts[this._config.split_by.length];
this._is_editable[i + new_window.start_col!] =
!!this._table_schema[column];
this._column_types[i + new_window.start_col!] =
this._schema[column];
}
}
this._last_window = new_window;
this._ids =
columns.__ID__ ||
Array(y1 - y0)
.fill(null)
.map((_, index) => [index + y0]);
this._reverse_columns = this._column_paths
.slice(x0, x1)
.reduce((acc, x, i) => {
acc.set(x, i);
return acc;
}, new Map<string, number>());
this._reverse_ids = this._ids?.reduce((acc, x, i) => {
acc.set(x?.join("|"), i);
return acc;
}, new Map<string, number>());
} else {
this._div_factory.clear();
num_columns = await this._view.num_columns();
}
const data: (string | HTMLElement)[][] = [];
const metadata: unknown[][] = [];
const column_headers: string[][] = [];
const column_paths: string[] = [];
const is_settings_open = viewer.hasAttribute("settings");
for (
let ipath = x0;
ipath < Math.min(x1, this._column_paths.length);
++ipath
) {
const path = this._column_paths[ipath];
const path_parts = path.split("|");
const column = columns[path] || new Array(y1 - y0).fill(null);
const agg_depth = Math.min(
(regularTable as any)[PRIVATE_PLUGIN_SYMBOL]?.[
path_parts[this._config.split_by.length]
]?.aggregate_depth || 0,
this._config.group_by.length,
);
data.push(
column.map((x, i) => {
if ((columns?.__ROW_PATH__?.[i]?.length ?? 0) < agg_depth) {
return "";
} else {
return format_cell.call(
this,
path_parts[this._config.split_by.length],
x,
(regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {},
) as string | HTMLElement;
}
}),
);
metadata.push(column as unknown[]);
if (is_settings_open) {
path_parts.push("");
}
column_headers.push(path_parts);
column_paths.push(path);
}
const is_dim_call = x1 - x0 > 0 && y1 - y0 > 0;
// Only update the last state if this is not a "phantom" call.
if (is_dim_call) {
this.last_column_paths = last_column_paths;
this.last_meta = last_meta;
this.last_ids = last_ids;
this.last_reverse_ids = last_reverse_ids;
this.last_reverse_columns = last_reverse_columns;
last_column_paths = column_paths;
last_meta = metadata;
last_ids = this._ids;
last_reverse_ids = this._reverse_ids;
last_reverse_columns = this._reverse_columns;
}
const is_row_path = columns.__ROW_PATH__ !== undefined;
const is_flat = this._config.group_rollup_mode === "flat";
const row_headers = Array.from(
(is_row_path
? is_flat
? format_flat_header_row_path
: format_tree_header_row_path
: format_tree_header
).call(
this,
columns.__ROW_PATH__,
this._config.group_by,
regularTable,
),
) as (string | HTMLElement)[][];
const num_row_headers = !is_dim_call
? row_header_depth(this._config)
: row_headers[0]?.length;
const result: DataResponse = {
num_column_headers:
this._config.split_by.length + (is_settings_open ? 2 : 1),
num_row_headers,
num_rows: this._num_rows,
num_columns,
row_headers: row_headers as CellScalar[][], // Add `HTMLElement`
column_headers,
data: data as CellScalar[][],
metadata,
column_header_merge_depth: Math.max(
0,
this._config.split_by.length,
),
};
return result;
};
}
function row_header_depth(config: ViewConfig) {
if (config.group_rollup_mode === "flat") {
return config.group_by.length;
} else if (config.group_rollup_mode === "total") {
return 0;
} else if (config.group_by.length === 0) {
return 0;
} else {
return config.group_by.length + 1;
}
}
@@ -0,0 +1,63 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import * as edit_click from "./click/edit_click.js";
import * as edit_keydown from "./keydown/edit_keydown.js";
import type { DatagridModel, SelectedPositionMap } from "../types.js";
import { isEditableMode } from "../types.js";
import { RegularTableElement } from "regular-table";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
export function createKeydownListener(
model: DatagridModel,
table: RegularTableElement,
viewer: HTMLPerspectiveViewerElement,
selected_position_map: SelectedPositionMap,
): EventListener {
return (event: Event): void => {
const keyEvent = event as KeyboardEvent;
if (model._edit_mode === "EDIT") {
if (!isEditableMode(model, viewer)) {
return;
}
edit_keydown.keydownListener(
model,
table,
viewer,
selected_position_map,
keyEvent,
);
} else {
console.debug(
`Mode ${model._edit_mode} for "keydown" event not yet implemented`,
);
}
};
}
export function createEditClickListener(
model: DatagridModel,
table: RegularTableElement,
viewer: HTMLPerspectiveViewerElement,
): EventListener {
return (event: Event): void => {
const mouseEvent = event as MouseEvent;
if (model._edit_mode === "EDIT") {
if (!isEditableMode(model, viewer)) {
return;
}
edit_click.clickListener(model, table, viewer, mouseEvent);
}
};
}
@@ -0,0 +1,80 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { CellMetadataBody } from "regular-table/dist/esm/types.js";
import {
type RegularTable,
type DatagridModel,
get_psp_type,
} from "../../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
export function write_cell(
table: RegularTable,
model: DatagridModel,
active_cell: HTMLElement,
): boolean {
const meta = table.getMeta(active_cell) as CellMetadataBody;
if (!meta) {
return false;
}
const type = model._schema[model._column_paths[meta.x!]];
let text: string | number | boolean | null = active_cell.textContent || "";
const id = model._ids[meta.y! - meta.y0][0];
if (type === "float" || type === "integer") {
const parsed = parseFloat(text.replace(/,/g, ""));
if (isNaN(parsed)) {
return false;
}
text = parsed;
} else if (type === "date" || type === "datetime") {
const parsed = Date.parse(text);
if (isNaN(parsed)) {
return false;
}
text = parsed;
} else if (type === "boolean") {
text = text === "true" ? false : text === "false" ? true : null;
}
const msg = {
__INDEX__: id,
[model._column_paths[meta.x]]: text,
};
model._table.update([msg], { port_id: model._edit_port, format: null });
return true;
}
export function clickListener(
model: DatagridModel,
table: RegularTable,
_viewer: HTMLPerspectiveViewerElement,
event: MouseEvent,
): void {
const meta = table.getMeta(event.target as HTMLElement);
if (meta?.type === "body" || meta?.type === "column_header") {
const is_editable2 = model._is_editable[meta.x];
const is_bool = get_psp_type(model, meta) === "boolean";
const is_null = (event.target as Element).classList.contains(
"psp-null",
);
if (is_editable2 && is_bool && !is_null) {
write_cell(table, model, event.target as HTMLElement);
}
}
}
@@ -0,0 +1,44 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { RegularTableElement } from "regular-table";
import getCellConfig from "../get_cell_config.js";
import type { DatagridModel, PerspectiveClickDetail } from "../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
export function createDispatchClickListener(
model: DatagridModel,
table: RegularTableElement,
viewer: HTMLPerspectiveViewerElement,
): EventListener {
return async (event: Event): Promise<void> => {
const mouseEvent = event as MouseEvent;
const meta = table.getMeta(mouseEvent.target as HTMLElement);
if (!meta || meta.type !== "body") {
return;
}
const { x, y } = meta;
const { row, column_names, config } = await getCellConfig(model, y, x);
viewer.dispatchEvent(
new CustomEvent<PerspectiveClickDetail>("perspective-click", {
bubbles: true,
composed: true,
detail: {
row,
column_names,
config,
},
}),
);
};
}
@@ -0,0 +1,48 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { RegularTable, DatagridModel } from "../types.js";
export async function expandCollapseHandler(
model: DatagridModel,
regularTable: RegularTable,
event: MouseEvent,
): Promise<void> {
const meta = regularTable.getMeta(event.target as HTMLElement);
if (!meta || meta.type !== "row_header") {
return;
}
const is_collapse = (event.target as Element).classList.contains(
"psp-tree-label-collapse",
);
if (event.shiftKey && is_collapse) {
model._view.set_depth(
(meta.row_header as unknown[]).filter((x) => x !== undefined)
.length - 2,
);
} else if (event.shiftKey) {
model._view.set_depth(
(meta.row_header as unknown[]).filter((x) => x !== undefined)
.length - 1,
);
} else if (is_collapse) {
model._view.collapse(meta.y);
} else {
model._view.expand(meta.y);
}
model._num_rows = await model._view.num_rows();
model._num_columns = await model._view.num_columns();
regularTable.draw();
}
@@ -0,0 +1,66 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { write_cell } from "./click/edit_click.js";
import type {
RegularTable,
DatagridModel,
SelectedPosition,
SelectedPositionMap,
} from "../types.js";
import { isEditableMode } from "../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
export function createFocusoutListener(
model: DatagridModel,
table: RegularTable,
viewer: HTMLPerspectiveViewerElement,
selected_position_map: SelectedPositionMap,
): EventListener {
return (event: Event): void => {
const focusEvent = event as FocusEvent;
if (isEditableMode(model, viewer) && selected_position_map.has(table)) {
const target = focusEvent.target as HTMLElement;
target.classList.remove("psp-error");
const selectedPosition = selected_position_map.get(table)!;
selected_position_map.delete(table);
if (selectedPosition.content !== target.textContent) {
if (!write_cell(table, model, target)) {
target.textContent = selectedPosition.content || "";
target.classList.add("psp-error");
target.focus();
}
}
}
};
}
export function createFocusinListener(
_model: DatagridModel,
table: RegularTable,
_viewer: HTMLPerspectiveViewerElement,
selected_position_map: SelectedPositionMap,
): EventListener {
return (event: Event): void => {
const focusEvent = event as FocusEvent;
const target = focusEvent.target as HTMLElement;
const meta = table.getMeta(target);
if (meta?.type === "body") {
const new_state: SelectedPosition = {
x: meta.x,
y: meta.y,
content: target.textContent || undefined,
};
selected_position_map.set(table, new_state);
}
};
}
@@ -0,0 +1,130 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { sortHandler } from "./sort.js";
import { expandCollapseHandler } from "./expand_collapse.js";
import type { RegularTable, DatagridModel } from "../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
export function createMousedownListener(
model: DatagridModel,
regularTable: RegularTable,
viewer: HTMLPerspectiveViewerElement,
): EventListener {
return async (event: Event): Promise<void> => {
const mouseEvent = event as MouseEvent;
if (mouseEvent.which !== 1) {
return;
}
let target = mouseEvent.target as HTMLElement | null;
if (target?.tagName === "A") {
return;
}
while (target && target.tagName !== "TD" && target.tagName !== "TH") {
target = target.parentElement;
if (!target || !regularTable.contains(target)) {
return;
}
}
if (!target) {
return;
}
if (target.classList.contains("psp-tree-label")) {
if (model._edit_mode !== "SELECT_ROW_TREE") {
expandCollapseHandler(model, regularTable, mouseEvent);
}
return;
}
if (target.classList.contains("psp-menu-enabled")) {
const meta = regularTable.getMeta(target);
const column_name =
meta?.column_header?.[model._config.split_by.length];
await viewer.toggleColumnSettings(`${column_name}`);
} else if (target.classList.contains("psp-sort-enabled")) {
sortHandler(model, regularTable, viewer, mouseEvent, target);
}
};
}
export function createDblclickListener(
model: DatagridModel,
regularTable: RegularTable,
viewer: HTMLPerspectiveViewerElement,
): EventListener {
return async (event: Event): Promise<void> => {
const mouseEvent = event as MouseEvent;
if (mouseEvent.which !== 1) {
return;
}
let target = mouseEvent.target as HTMLElement | null;
if (target?.tagName === "A") {
return;
}
while (target && target.tagName !== "TD" && target.tagName !== "TH") {
target = target.parentElement;
if (!target || !regularTable.contains(target)) {
return;
}
}
if (!target) {
return;
}
if (target.classList.contains("psp-tree-label")) {
if (model._edit_mode === "SELECT_ROW_TREE") {
expandCollapseHandler(model, regularTable, mouseEvent);
}
}
};
}
export function createClickListener(regularTable: RegularTable): EventListener {
return (event: Event): void => {
const mouseEvent = event as MouseEvent;
if (mouseEvent.which !== 1) {
return;
}
let target = mouseEvent.target as HTMLElement | null;
while (target && target.tagName !== "TD" && target.tagName !== "TH") {
target = target.parentElement;
if (!target || !regularTable.contains(target)) {
return;
}
}
if (!target) {
return;
}
if (
target.classList.contains("psp-tree-label") &&
mouseEvent.offsetX < 26
) {
mouseEvent.stopImmediatePropagation();
} else if (
target.classList.contains("psp-header-leaf") &&
!target.classList.contains("psp-header-corner")
) {
mouseEvent.stopImmediatePropagation();
}
};
}
@@ -0,0 +1,219 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { focusSelectedCell } from "../../style_handlers/focus.js";
import type {
RegularTable,
DatagridModel,
SelectedPositionMap,
} from "../../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
type AsyncMoveFunction = (
model: DatagridModel,
table: RegularTable,
selected_position_map: SelectedPositionMap,
active_cell: HTMLElement,
dx: number,
dy: number,
) => Promise<void | undefined>;
function lock(body: AsyncMoveFunction): AsyncMoveFunction {
let lockPromise: Promise<void> | undefined;
return async function (
model: DatagridModel,
table: RegularTable,
selected_position_map: SelectedPositionMap,
active_cell: HTMLElement,
dx: number,
dy: number,
): Promise<void | undefined> {
if (lockPromise) {
await lockPromise;
return;
}
let resolve: () => void;
lockPromise = new Promise((x) => (resolve = x));
const result = await body(
model,
table,
selected_position_map,
active_cell,
dx,
dy,
);
lockPromise = undefined;
resolve!();
return result;
};
}
interface ContentEditableElement extends HTMLElement {
isContentEditable: boolean;
selectionStart?: number;
}
function getPos(elem: ContentEditableElement): number {
if (elem.isContentEditable) {
const _range = (elem.getRootNode() as Document)
.getSelection()
?.getRangeAt(0);
if (!_range) {
return 0;
}
const range = _range.cloneRange();
range.selectNodeContents(elem);
range.setEnd(_range.endContainer, _range.endOffset);
return range.toString().length;
} else {
return elem.selectionStart || 0;
}
}
const moveSelection = lock(async function (
model: DatagridModel,
table: RegularTable,
selected_position_map: SelectedPositionMap,
active_cell: HTMLElement,
dx: number,
dy: number,
): Promise<void> {
const meta = table.getMeta(active_cell);
if (!meta || meta.type !== "body") {
return;
}
const num_columns = model._column_paths.length;
const num_rows = model._num_rows;
const selected_position = selected_position_map.get(table);
if (!selected_position) {
return;
}
if (meta.x + dx < num_columns && 0 <= meta.x + dx) {
selected_position.x = meta.x + dx;
}
if (meta.y + dy < num_rows && 0 <= meta.y + dy) {
selected_position.y = meta.y + dy;
}
const xmin = Math.max(meta.x0 - 10, 0);
const xmax = Math.min(meta.x0 + 10, num_columns);
const ymin = Math.max(meta.y0 - 5, 0);
const ymax = Math.min(meta.y0 + 10, num_rows);
let x = meta.x0 + dx,
y = meta.y0 + dy;
while (
!focusSelectedCell(table, selected_position_map) &&
x >= xmin &&
x < xmax &&
y >= ymin &&
y < ymax
) {
await table.scrollToCell(x, y);
selected_position_map.set(table, selected_position);
x += dx;
y += dy;
}
});
function isLastCell(
model: DatagridModel,
table: RegularTable,
target: HTMLElement,
): boolean {
const meta = table.getMeta(target);
return meta?.type === "body" && meta.y === model._num_rows - 1;
}
export function keydownListener(
model: DatagridModel,
table: RegularTable,
_viewer: HTMLPerspectiveViewerElement,
selected_position_map: SelectedPositionMap,
event: KeyboardEvent,
): void {
const target = (table.getRootNode() as Document)
.activeElement as HTMLElement;
(event.target as HTMLElement).classList.remove("psp-error");
switch (event.key) {
case "Enter":
event.preventDefault();
if (isLastCell(model, table, target)) {
target.blur();
selected_position_map.delete(table);
} else if (event.shiftKey) {
moveSelection(
model,
table,
selected_position_map,
target,
0,
-1,
);
} else {
moveSelection(
model,
table,
selected_position_map,
target,
0,
1,
);
}
break;
case "ArrowLeft":
if (getPos(target as ContentEditableElement) === 0) {
event.preventDefault();
moveSelection(
model,
table,
selected_position_map,
target,
-1,
0,
);
}
break;
case "ArrowUp":
event.preventDefault();
moveSelection(model, table, selected_position_map, target, 0, -1);
break;
case "ArrowRight":
if (
getPos(target as ContentEditableElement) ===
(target.textContent?.length || 0)
) {
event.preventDefault();
moveSelection(
model,
table,
selected_position_map,
target,
1,
0,
);
}
break;
case "ArrowDown":
event.preventDefault();
moveSelection(model, table, selected_position_map, target, 0, 1);
break;
default:
}
}
@@ -0,0 +1,461 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { RegularTableElement } from "regular-table";
import type {
DatagridPluginElement,
EditMode,
SelectionArea,
} from "../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
import type { ViewWindow } from "@perspective-dev/client";
import type { CellMetadataBody } from "regular-table/dist/esm/types.js";
const MOUSE_SELECTED_AREA_CLASS = "mouse-selected-area";
export type OnSelectCallback = (
area: SelectionArea,
isDeselect: boolean,
) => void;
interface AddAreaMouseSelectionOptions {
className?: string;
selected?: SelectionArea[];
onSelect?: OnSelectCallback;
}
export const addAreaMouseSelection = (
datagrid: DatagridPluginElement,
table: RegularTableElement,
{
className = MOUSE_SELECTED_AREA_CLASS,
selected = [],
onSelect,
}: AddAreaMouseSelectionOptions = {},
): RegularTableElement => {
datagrid.model!._selection_state = {
selected_areas: selected,
dirty: true,
};
table.addEventListener(
"mousedown",
getMousedownListener(datagrid, table, className),
);
table.addEventListener(
"mouseover",
getMouseoverListener(datagrid, table, className),
);
table.addEventListener(
"mouseup",
getMouseupListener(datagrid, table, className, onSelect),
);
table.addStyleListener(() =>
applyMouseAreaSelections(datagrid, table, className),
);
return table;
};
function isSingleClickMode(mode: EditMode): boolean {
return mode === "SELECT_ROW_TREE";
}
const getMousedownListener =
(
datagrid: DatagridPluginElement,
table: RegularTableElement,
className: string,
) =>
(event: Event): void => {
const mouseEvent = event as MouseEvent;
if (
mouseEvent.button === 0 &&
isSelectionMode(datagrid.model!._edit_mode)
) {
if (isSingleClickMode(datagrid.model!._edit_mode)) {
return;
}
datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES = {};
const meta = table.getMeta(mouseEvent.target as HTMLElement);
if (
meta?.type === "body" &&
meta.x !== undefined &&
meta.y !== undefined
) {
datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES =
{
x: meta.x,
y: meta.y,
};
datagrid.model!._selection_state.old_selected_areas =
datagrid.model!._selection_state.selected_areas;
datagrid.model!._selection_state.selected_areas = [];
const start: SelectionArea = {
x0: meta.x,
x1: meta.x,
y0: meta.y,
y1: meta.y,
};
datagrid.model!._selection_state.potential_selection = start;
applyMouseAreaSelections(
datagrid,
table,
className,
datagrid.model!._selection_state.selected_areas.concat([
start,
]),
);
return;
}
}
datagrid.model!._selection_state.selected_areas = [];
};
const getMouseoverListener =
(
datagrid: DatagridPluginElement,
table: RegularTableElement,
className: string,
) =>
(event: Event): void => {
const mouseEvent = event as MouseEvent;
const mode = datagrid.model!._edit_mode;
if (isSelectionMode(mode) && !isSingleClickMode(mode)) {
if (
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES &&
datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES
.x !== undefined
) {
const meta = table.getMeta(mouseEvent.target as HTMLElement);
if (
meta?.type === "body" &&
meta.x !== undefined &&
meta.y !== undefined
) {
const potentialSelection: SelectionArea = {
x0: Math.min(
meta.x,
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES.x!,
),
x1: Math.max(
meta.x,
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES.x!,
),
y0: Math.min(
meta.y,
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES.y!,
),
y1: Math.max(
meta.y,
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES.y!,
),
};
datagrid.model!._selection_state.potential_selection =
potentialSelection;
applyMouseAreaSelections(
datagrid,
table,
className,
datagrid.model!._selection_state.selected_areas.concat([
potentialSelection,
]),
);
}
}
}
};
const getMouseupListener =
(
datagrid: DatagridPluginElement,
table: RegularTableElement,
className: string,
onSelect?: OnSelectCallback,
) =>
(event: Event): void => {
const mouseEvent = event as MouseEvent;
const mode = datagrid.model!._edit_mode;
if (isSelectionMode(mode)) {
const meta = table.getMeta(mouseEvent.target as HTMLElement);
if (!meta) {
return;
}
// For single-click modes (SELECT_ROW_TREE), handle toggle
if (isSingleClickMode(mode)) {
if (
(meta.type === "body" || meta.type === "row_header") &&
meta.y !== undefined &&
meta.y >= 0
) {
const existing =
datagrid.model!._selection_state.selected_areas;
const isSameRow =
existing.length > 0 && existing[0].y0 === meta.y;
if (isSameRow) {
// Deselect
datagrid.model!._selection_state.selected_areas = [];
datagrid.model!._selection_state.dirty = true;
applyMouseAreaSelections(
datagrid,
table,
className,
[],
);
onSelect?.(existing[0], true);
} else {
// Select new row
const area: SelectionArea = {
x0: 0,
x1: 0,
y0: meta.y,
y1: meta.y,
};
datagrid.model!._selection_state.selected_areas = [
area,
];
datagrid.model!._selection_state.dirty = true;
applyMouseAreaSelections(datagrid, table, className);
onSelect?.(area, false);
}
}
datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES =
{};
datagrid.model!._selection_state.potential_selection =
undefined;
return;
}
// Drag-based modes (SELECT_ROW, SELECT_COLUMN, SELECT_REGION)
if (
(datagrid.model!._selection_state.old_selected_areas?.length ??
0) > 0
) {
const selected =
datagrid.model!._selection_state.old_selected_areas![0];
if (
selected.x0 === selected.x1 &&
selected.y0 === selected.y1 &&
meta?.type === "body" &&
selected.x0 === meta.x &&
selected.y0 === meta.y
) {
datagrid.model!._selection_state.selected_areas = [];
datagrid.model!._selection_state.old_selected_areas = [];
datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES =
{};
datagrid.model!._selection_state.potential_selection =
undefined;
applyMouseAreaSelections(datagrid, table, className, []);
return;
}
}
datagrid.model!._selection_state.old_selected_areas = [];
if (
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES &&
datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES
.x !== undefined &&
meta?.type === "body" &&
meta.x !== undefined &&
meta.y !== undefined
) {
const selection: SelectionArea = {
x0: Math.min(
meta.x,
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES.x!,
),
x1: Math.max(
meta.x,
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES.x!,
),
y0: Math.min(
meta.y,
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES.y!,
),
y1: Math.max(
meta.y,
datagrid.model!._selection_state
.CURRENT_MOUSEDOWN_COORDINATES.y!,
),
};
datagrid.model!._selection_state.selected_areas.push(selection);
applyMouseAreaSelections(datagrid, table, className);
}
datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES = {};
datagrid.model!._selection_state.potential_selection = undefined;
}
};
function modeIncludesColumns(mode: EditMode): boolean {
return mode === "SELECT_COLUMN" || mode === "SELECT_REGION";
}
function modeIncludesRows(mode: EditMode): boolean {
return (
mode === "SELECT_ROW" ||
mode === "SELECT_REGION" ||
mode === "SELECT_ROW_TREE"
);
}
function set_psp_selection(
viewer: HTMLPerspectiveViewerElement,
datagrid: DatagridPluginElement,
{ x0, x1, y0, y1 }: SelectionArea,
): void {
const viewport: ViewWindow = {};
const mode = datagrid.model!._edit_mode;
if (x0 !== undefined && modeIncludesColumns(mode)) {
viewport.start_col = x0;
}
if (x1 !== undefined && modeIncludesColumns(mode)) {
viewport.end_col = x1 + 1;
}
if (y0 !== undefined && modeIncludesRows(mode)) {
viewport.start_row = y0;
}
if (y1 !== undefined && modeIncludesRows(mode)) {
viewport.end_row = y1 + 1;
}
viewer.setSelection(viewport);
}
type CellPredicate = (meta: CellMetadataBody, area: SelectionArea) => boolean;
const SELECTION_PREDICATES: Record<string, CellPredicate> = {
SELECT_REGION: (m, a) =>
a.x0 <= m.x && m.x <= a.x1 && a.y0 <= m.y && m.y <= a.y1,
SELECT_ROW: (m, a) => a.y0 <= m.y && m.y <= a.y1,
SELECT_ROW_TREE: (m, a) => a.y0 <= m.y && m.y <= a.y1,
SELECT_COLUMN: (m, a) => a.x0 <= m.x && m.x <= a.x1,
};
function isSelectionMode(mode: EditMode): boolean {
return (
mode === "SELECT_REGION" ||
mode === "SELECT_ROW" ||
mode === "SELECT_COLUMN" ||
mode === "SELECT_ROW_TREE"
);
}
export const applyMouseAreaSelections = (
datagrid: DatagridPluginElement,
table: RegularTableElement,
className: string,
selected?: SelectionArea[],
): void => {
const mode = datagrid.model!._edit_mode;
if (isSelectionMode(mode)) {
selected = datagrid.model!._selection_state.selected_areas.slice(0);
if (datagrid.model!._selection_state.potential_selection) {
selected.push(datagrid.model!._selection_state.potential_selection);
}
if (selected.length > 0) {
set_psp_selection(
datagrid.parentElement as HTMLPerspectiveViewerElement,
datagrid,
selected[0],
);
// SELECT_ROW_TREE styling is handled entirely by the
// identity-based system in body.ts, which styles both td
// and th uniformly in a single draw pass.
if (!isSingleClickMode(mode)) {
applyMouseAreaSelection(datagrid, table, selected, className);
}
} else {
(
datagrid.parentElement as HTMLPerspectiveViewerElement
).setSelection();
const tds = table.querySelectorAll("tbody td");
for (const td of tds) {
td.classList.remove(className);
}
}
} else if (datagrid.model!._selection_state.dirty) {
datagrid.model!._selection_state.dirty = false;
const cells = table.querySelectorAll("tbody td, tbody th");
for (const cell of cells) {
cell.classList.remove(className);
}
}
};
const applyMouseAreaSelection = (
datagrid: DatagridPluginElement,
table: RegularTableElement,
selected: SelectionArea[],
className: string,
): void => {
const predicate = SELECTION_PREDICATES[datagrid.model!._edit_mode];
if (!predicate || selected.length === 0) {
return;
}
const tds = table.querySelectorAll("tbody td");
for (const td of tds) {
const meta = table.getMeta(td as HTMLElement);
if (!meta || meta.type !== "body") {
continue;
}
let rendered = false;
for (const area of selected) {
if (
area.x0 !== undefined &&
area.y0 !== undefined &&
area.x1 !== undefined &&
area.y1 !== undefined &&
predicate(meta, area)
) {
rendered = true;
datagrid.model!._selection_state.dirty = true;
td.classList.add(className);
}
}
if (!rendered) {
td.classList.remove(className);
}
}
};
@@ -0,0 +1,116 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { RegularTableElement } from "regular-table";
import type { DatagridModel, SortRotationOrder, SortTerm } from "../types.js";
import type { SortDir } from "@perspective-dev/client";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
const ROW_SORT_ORDER: SortRotationOrder = {
desc: "asc",
asc: undefined,
"desc abs": "asc abs",
"asc abs": undefined,
};
const ROW_COL_SORT_ORDER: SortRotationOrder = {
desc: "asc",
asc: undefined,
"desc abs": "asc abs",
"asc abs": undefined,
};
export async function sortHandler(
model: DatagridModel,
regularTable: RegularTableElement,
viewer: HTMLPerspectiveViewerElement,
event: MouseEvent,
target: HTMLElement,
): Promise<void> {
const meta = regularTable.getMeta(target);
if (!meta?.column_header) {
return;
}
const column_name = meta.column_header[model._config.split_by.length];
const sort_method =
event.ctrlKey ||
(event as MouseEvent & { metaKey?: boolean }).metaKey ||
event.altKey
? append_sort
: override_sort;
const abs = event.shiftKey;
const sort = sort_method(model, `${column_name}`, abs);
await viewer.restore({ sort });
}
export function append_sort(
model: DatagridModel,
column_name: string,
abs: boolean,
): SortTerm[] {
const sort: SortTerm[] = [];
let found = false;
for (const sort_term of model._config.sort) {
const [_column_name, _sort_dir] = sort_term;
if (_column_name === column_name) {
found = true;
const term = create_sort(model, column_name, _sort_dir, abs);
if (term) {
sort.push(term);
}
} else {
sort.push(sort_term);
}
}
if (!found) {
sort.push([column_name, abs ? "desc abs" : "desc"]);
}
return sort;
}
export function override_sort(
model: DatagridModel,
column_name: string,
abs: boolean,
): SortTerm[] {
for (const [_column_name, _sort_dir] of model._config.sort) {
if (_column_name === column_name) {
const sort = create_sort(model, column_name, _sort_dir, abs);
return sort ? [sort] : [];
}
}
return [[column_name, abs ? "desc abs" : "desc"]];
}
export function create_sort(
model: DatagridModel,
column_name: string,
sort_dir: SortDir | undefined,
_abs: boolean,
): SortTerm | undefined {
const is_col_sortable = model._config.split_by.length > 0;
const order = is_col_sortable ? ROW_COL_SORT_ORDER : ROW_SORT_ORDER;
const inc_sort_dir: SortDir | undefined = sort_dir
? order[sort_dir]
: "desc";
if (inc_sort_dir) {
return [column_name, inc_sort_dir];
}
return undefined;
}
@@ -0,0 +1,76 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { View, ViewConfig, Filter, Scalar } from "@perspective-dev/client";
import type { CellConfigResult } from "./types.js";
import { isMetaColumn } from "./model/meta_columns.js";
interface ModelWithViewAndConfig {
_view: View;
_config: ViewConfig;
}
interface RowData {
__ROW_PATH__?: unknown[];
[key: string]: unknown;
}
export default async function getCellConfig(
{ _view, _config }: ModelWithViewAndConfig,
row_idx: number,
col_idx: number,
): Promise<CellConfigResult> {
const group_by = _config.group_by;
const split_by = _config.split_by;
const start_row = row_idx >= 0 ? row_idx : 0;
const end_row = start_row + 1;
const r = (await _view.to_json({ start_row, end_row })) as RowData[];
const row_paths = r.map((x) => x.__ROW_PATH__);
const group_by_values = (row_paths[0] || []) as Scalar[];
const row_filters = group_by
.map((pivot, index): Filter | undefined => {
const pivot_value = group_by_values[index];
return pivot_value ? [pivot, "==", pivot_value] : undefined;
})
.filter((x): x is Filter => x !== undefined);
// Filter out *all* meta columns before indexing into the row's
// keys — the DuckDB virtual server's JSON output now includes
// per-level `__ROW_PATH_<n>__` columns alongside the
// `__ROW_PATH__` sidecar, so the previous `+1` skip (which
// assumed exactly one leading meta column) lands on a meta key
// when group_by has multiple levels.
const user_keys = Object.keys(r[0]).filter((k) => !isMetaColumn(k));
const column_paths = user_keys[col_idx];
const result: CellConfigResult = {
row: r[0] as Record<string, unknown>,
column_names: [],
config: { filter: [] },
};
let column_filters: Filter[] = [];
if (column_paths) {
const split_by_values = column_paths.split("|");
result.column_names = [split_by_values[split_by.length]];
column_filters = split_by
.map((pivot, index): Filter | undefined => {
const pivot_value = split_by_values[index];
return pivot_value ? [pivot, "==", pivot_value] : undefined;
})
.filter((x): x is Filter => x !== undefined)
.filter(([, , value]) => !isMetaColumn(value as string));
}
const filter = _config.filter.concat(row_filters).concat(column_filters);
result.config = { filter };
return result;
}
+49
View File
@@ -0,0 +1,49 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import "regular-table";
import { HTMLPerspectiveViewerDatagridPluginElement } from "./custom_elements/datagrid.js";
import { HTMLPerspectiveViewerDatagridToolbarElement } from "./custom_elements/toolbar.js";
/******************************************************************************
*
* Main
*
*/
interface PerspectiveViewerConstructor {
registerPlugin(tagName: string): void;
}
async function _register_element(): Promise<void> {
customElements.define(
"perspective-viewer-datagrid-toolbar",
HTMLPerspectiveViewerDatagridToolbarElement,
);
customElements.define(
"perspective-viewer-datagrid",
HTMLPerspectiveViewerDatagridPluginElement,
);
await customElements.whenDefined("perspective-viewer");
const PerspectiveViewer = customElements.get(
"perspective-viewer",
) as unknown as PerspectiveViewerConstructor;
PerspectiveViewer.registerPlugin("perspective-viewer-datagrid");
}
_register_element();
export { PRIVATE_PLUGIN_SYMBOL } from "./types.js";
export { HTMLPerspectiveViewerDatagridPluginElement };
export { HTMLPerspectiveViewerDatagridToolbarElement };
@@ -0,0 +1,119 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { ColumnOverrides, DatagridPluginElement } from "../types.js";
interface RegularTableWithOverrides {
restoreColumnSizes(overrides: Record<number, number | undefined>): void;
saveColumnSizes(): Record<number, number | undefined>;
}
interface DatagridPluginWithCache extends DatagridPluginElement {
_cached_column_sizes?: ColumnOverrides;
}
/**
* Restore a saved column width override token.
*
* @param old_sizes An object previously returned by a call to
* `save_column_size_overrides()`
* @param cache A flag indicating whether this value should
* be cached so a future `resetAutoSize()` call does not clear it.
*/
export function restore_column_size_overrides(
this: DatagridPluginWithCache,
old_sizes: ColumnOverrides,
cache = false,
): void {
if (!this._initialized) {
return;
}
if (cache) {
this._cached_column_sizes = old_sizes;
}
const regular_table = this.regular_table as RegularTableWithOverrides;
const overrides: Record<number, number | undefined> = {
...regular_table.saveColumnSizes(),
};
const { group_by } = this.model!._config;
const tree_header_offset = group_by?.length > 0 ? group_by.length + 1 : 0;
for (const key of Object.keys(old_sizes)) {
if (key === "__ROW_PATH__") {
overrides[tree_header_offset - 1] = old_sizes[key] as
| number
| undefined;
} else {
const index = this.model!._column_paths.indexOf(key);
// Skip keys that don't resolve to a known column — e.g. on the
// first draw after `activate`, `_column_paths` has not yet been
// populated by the data listener, so we leave any existing
// `regular-table` widths untouched rather than clobbering them
// with garbage indices.
if (index === -1) {
continue;
}
overrides[index + tree_header_offset] = old_sizes[key] as
| number
| undefined;
}
}
regular_table.restoreColumnSizes(overrides);
}
/**
* Extract the current user-overriden column widths from
* `regular-table`. This function depends on the internal
* implementation of `regular-table` and may break!
*
* @returns An Object-as-dictionary keyed by column_path string, and
* valued by the column's user-overridden pixel width.
*/
export function save_column_size_overrides(
this: DatagridPluginWithCache,
): ColumnOverrides {
if (!this._initialized) {
return {};
}
if (this._cached_column_sizes) {
const x = this._cached_column_sizes;
this._cached_column_sizes = undefined;
return x;
}
const overrides = (
this.regular_table as RegularTableWithOverrides
).saveColumnSizes();
const { group_by } = this.model!._config;
const tree_header_offset = group_by?.length > 0 ? group_by.length + 1 : 0;
const old_sizes: ColumnOverrides = {};
for (const key of Object.keys(overrides)) {
const numKey = Number(key);
if (overrides[numKey] !== undefined) {
const index = numKey - tree_header_offset;
if (index > -1) {
old_sizes[this.model!._column_paths[index]] = overrides[numKey];
} else if (index === -1) {
old_sizes["__ROW_PATH__"] = overrides[numKey];
}
}
}
return old_sizes;
}
@@ -0,0 +1,257 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { createDataListener } from "../data_listener/index.js";
import { blend, make_color_record, parseColor } from "../color_utils.js";
import type {
ColumnType,
Table,
View,
ViewConfig,
} from "@perspective-dev/client";
import {
type DatagridModel,
type DatagridPluginElement,
type RegularTable,
type Schema,
type ElemFactory,
type EditMode,
} from "../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
function arraysChanged<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) {
return true;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return true;
}
}
return false;
}
function nestedArraysChanged<T>(a: T[][], b: T[][]): boolean {
if (a.length !== b.length) {
return true;
}
for (let i = 0; i < a.length; i++) {
if (a[i].length !== b[i].length) {
return true;
}
for (let j = 0; j < a[i].length; j++) {
if (a[i][j] !== b[i][j]) {
return true;
}
}
}
return false;
}
function get_rule(regular: HTMLElement, tag: string, def: string): string {
const color = window.getComputedStyle(regular).getPropertyValue(tag).trim();
if (color.length > 0) {
return color;
} else {
return def;
}
}
class ElemFactoryImpl implements ElemFactory {
private _name: string;
private _elements: HTMLElement[];
private _index: number;
constructor(name: string) {
this._name = name;
this._elements = [];
this._index = 0;
}
clear(): void {
this._index = 0;
}
get(): HTMLElement {
if (!this._elements[this._index]) {
this._elements[this._index] = document.createElement(this._name);
}
const elem = this._elements[this._index];
this._index += 1;
return elem;
}
}
export async function createModel(
this: DatagridPluginElement,
regular: RegularTable,
table: Table,
view: View,
theme: string,
extend: Partial<DatagridModel> = {},
): Promise<DatagridModel> {
const config = (await view.get_config()) as ViewConfig;
if (this?.model?._config) {
const old = this.model._config;
const group_by_changed = arraysChanged(old.group_by, config.group_by);
const type_changed =
(old.group_by.length === 0 || config.group_by.length === 0) &&
group_by_changed;
const split_by_changed = arraysChanged(old.split_by, config.split_by);
const columns_changed = arraysChanged(old.columns, config.columns);
const filter_changed = nestedArraysChanged(
old.filter as unknown[][],
config.filter as unknown[][],
);
const sort_changed = nestedArraysChanged(
old.sort as unknown[][],
config.sort as unknown[][],
);
const group_rollup_mode_changed =
old.group_rollup_mode !== config.group_rollup_mode;
const theme_changed = this.model._theme !== theme;
this._reset_scroll_top = group_by_changed;
this._reset_scroll_left = split_by_changed;
this._reset_select =
group_by_changed ||
split_by_changed ||
filter_changed ||
sort_changed ||
columns_changed;
this._reset_column_size =
group_rollup_mode_changed ||
split_by_changed ||
group_by_changed ||
columns_changed ||
theme_changed ||
type_changed;
}
const [table_schema, num_rows, schema, expression_schema, _edit_port] =
await Promise.all([
table.schema(),
view.num_rows(),
view.schema(),
view.expression_schema(),
(this.parentElement as HTMLPerspectiveViewerElement).getEditPort(),
]);
const _plugin_background = parseColor(
get_rule(regular, "--psp--background-color", "#FFFFFF"),
);
const _pos_fg_color = make_color_record(
get_rule(regular, "--psp-datagrid--pos-cell--color", "#338DCD"),
);
const _neg_fg_color = make_color_record(
get_rule(regular, "--psp-datagrid--neg-cell--color", "#FF5942"),
);
const _pos_bg_color = make_color_record(
blend(_pos_fg_color[0], _plugin_background),
);
const _neg_bg_color = make_color_record(
blend(_neg_fg_color[0], _plugin_background),
);
const _color = make_color_record(
get_rule(regular, "--psp-active--color", "#ff0000"),
);
const _schema: Schema = {
...(schema as Schema),
...(expression_schema as Schema),
};
const _table_schema: Schema = {
...(table_schema as Schema),
...(expression_schema as Schema),
};
const _column_paths: string[] = [];
const _is_editable: boolean[] = [];
const _column_types: ColumnType[] = [];
let _edit_mode: EditMode = this._edit_mode || "READ_ONLY";
if (
_edit_mode === "SELECT_ROW_TREE" &&
(config.group_by.length === 0 || config.group_rollup_mode === "flat")
) {
_edit_mode = "READ_ONLY";
this._edit_mode = _edit_mode;
}
this._edit_button!.dataset.editMode = _edit_mode;
const model: DatagridModel = Object.assign(extend, {
_edit_port,
_view: view,
_table: table,
_table_schema,
_config: config,
_num_rows: num_rows,
_schema,
_ids: [],
_plugin_background,
_color,
_pos_fg_color,
_neg_fg_color,
_pos_bg_color,
_neg_bg_color,
_column_paths,
_column_types,
_theme: theme,
_is_editable,
_edit_mode,
_selection_state: {
selected_areas: [],
dirty: false,
},
_row_header_types: config.group_by.map((column_path) => {
return _table_schema[column_path];
}),
_series_color_map: new Map<string, string>(),
_series_color_seed: new Map<string, number>(),
// get_psp_type,
_div_factory: extend._div_factory || new ElemFactoryImpl("div"),
}) as DatagridModel;
regular.setDataListener(
createDataListener(
this.parentElement as HTMLPerspectiveViewerElement,
).bind(model, regular) as any,
{
virtual_mode: (window
.getComputedStyle(regular)
.getPropertyValue("--datagrid-virtual-mode")
?.trim() || "both") as
| "both"
| "horizontal"
| "vertical"
| "none",
},
);
return model;
}
@@ -0,0 +1,19 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
export { PRIVATE_PLUGIN_SYMBOL } from "../types.js";
export { createModel } from "./create.js";
export { toggle_edit_mode, toggle_scroll_lock } from "./toolbar.js";
export {
save_column_size_overrides,
restore_column_size_overrides,
} from "./column_overrides.js";
@@ -0,0 +1,47 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
/**
* Single-source-of-truth predicate for "this column is metadata, hide
* it from the user-visible grid." Five distinct names show up across
* the perspective protocol; older datagrid code filtered them by
* exact-match in three places, which only worked when the wire shape
* was the JSON-sidecar form (`__ROW_PATH__` array-of-arrays).
*
* The DuckDB virtual-server backend additionally surfaces per-level
* `__ROW_PATH_<n>__` columns directly in `to_columns_string` /
* `to_json` output (a side effect of keeping them in the frozen Arrow
* batch so that `to_arrow` consumers — viewer-charts via
* `with_typed_arrays` — see them inline, matching native
* `perspective-server`'s `to_arrow` behavior). Without this helper,
* those per-level columns slip through the legacy exact-match filters
* and render as user columns to the right of the grid.
*
* Match list:
* - `__ROW_PATH__` — JSON sidecar; used by the tree header.
* - `__ROW_PATH_<n>__` — per-level columns from the virtual server's
* inline-arrow shape.
* - `__ID__` — per-row identity column.
* - `__GROUPING_ID__` — internal SQL-rollup discriminator. The
* virtual server strips it server-side, but
* we cover it defensively in case a future
* backend leaks it.
*
* User columns named with leading/trailing double-underscores (e.g.
* `__user_col__`) are *not* matched — the regex requires the exact
* stems above.
*/
const META_COLUMN_RE = /^__(?:ROW_PATH(?:_\d+)?|ID|GROUPING_ID)__$/;
export function isMetaColumn(name: string): boolean {
return META_COLUMN_RE.test(name);
}
@@ -0,0 +1,89 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
import type {
DatagridModel,
DatagridPluginElement,
EditMode,
} from "../types.js";
export const EDIT_MODES: readonly EditMode[] = [
"READ_ONLY",
"EDIT",
"SELECT_ROW",
"SELECT_COLUMN",
"SELECT_REGION",
"SELECT_ROW_TREE",
] as const;
function isSelectRowTreeAvailable(model?: DatagridModel): boolean {
if (!model) {
return false;
}
return (
model._config.group_by.length > 0 &&
model._config.group_rollup_mode !== "flat"
);
}
export function toggle_edit_mode(
this: DatagridPluginElement,
mode?: EditMode,
): void {
if (typeof mode === "undefined") {
let idx = EDIT_MODES.indexOf(this._edit_mode);
do {
idx = (idx + 1) % EDIT_MODES.length;
} while (
EDIT_MODES[idx] === "SELECT_ROW_TREE" &&
!isSelectRowTreeAvailable(this.model)
);
mode = EDIT_MODES[idx];
}
(this.parentElement as HTMLPerspectiveViewerElement)?.setSelection?.();
this._edit_mode = mode;
if (this.model) {
this.model._edit_mode = mode;
this.model._tree_selection_id = undefined;
this.model._selection_state = {
selected_areas: [],
dirty: true,
};
}
(this.parentElement as HTMLPerspectiveViewerElement)?.restore?.({
plugin_config: { edit_mode: mode },
});
if (this._edit_button !== undefined) {
this._edit_button.dataset.editMode = mode;
}
}
export function toggle_scroll_lock(
this: DatagridPluginElement,
force?: boolean,
): void {
if (typeof force === "undefined") {
force = !this._is_scroll_lock;
}
this._is_scroll_lock = force;
this.classList.toggle("sub-cell-scroll-disabled", force);
if (this._scroll_lock !== undefined) {
this._scroll_lock.classList.toggle("lock-scroll", force);
}
}
@@ -0,0 +1,265 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { style_selected_column } from "../style_handlers/column_header.js";
import {
createMousedownListener,
createClickListener,
createDblclickListener,
} from "../event_handlers/header_click.js";
import {
createFocusinListener,
createFocusoutListener,
} from "../event_handlers/focus.js";
import {
createKeydownListener,
createEditClickListener,
} from "../event_handlers/click.js";
import { createModel } from "../model/create.js";
import { createDispatchClickListener } from "../event_handlers/dispatch_click.js";
import {
addAreaMouseSelection,
type OnSelectCallback,
} from "../event_handlers/select_region.js";
import { createConsolidatedStyleListener } from "../style_handlers/consolidated.js";
import getCellConfig from "../get_cell_config.js";
import type { View } from "@perspective-dev/client";
import {
type DatagridPluginElement,
type SelectedPositionMap,
type SelectionArea,
PerspectiveSelectDetail,
} from "../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
interface ToggleColumnSettingsEvent extends CustomEvent {
detail: {
column_name: string | null;
open: boolean;
};
}
/**
* Lazy initialize this plugin with various listeners.
*/
export async function activate(
this: DatagridPluginElement,
view: View,
): Promise<void> {
const viewer = this.parentElement as HTMLPerspectiveViewerElement;
const table = await viewer.getTable();
if (!this._initialized) {
this.innerHTML = "";
if (this.shadowRoot) {
this.shadowRoot.appendChild(this.regular_table);
} else {
this.appendChild(this.regular_table);
}
this.model = await createModel.call(
this,
this.regular_table,
table,
view,
viewer.getAttribute("theme")!,
);
if (!this.model) {
return;
}
const model = this.model;
const regularTable = this.regular_table;
const onSelect: OnSelectCallback = async (
area: SelectionArea,
isDeselect: boolean,
) => {
if (model._edit_mode !== "SELECT_ROW_TREE") {
return;
}
// Store the selected row identity on the model so it persists
// even when the selected row scrolls out of the viewport.
if (isDeselect) {
model._tree_selection_id = undefined;
} else {
const idx = area.y0 - (model._last_window?.start_row ?? 0);
if (idx >= 0 && idx < model._ids.length) {
model._tree_selection_id = model._ids[idx];
}
}
const { row, column_names, config } = await getCellConfig(
model,
area.y0,
0,
);
let detail: PerspectiveSelectDetail;
if (isDeselect) {
if ((model._last_insert_configs?.length || 0) > 0) {
detail = new PerspectiveSelectDetail(
false,
row,
[],
model._last_insert_configs ?? [],
[],
);
} else {
throw new Error("Suprious deselect");
}
model._last_insert_configs = undefined;
} else {
detail = new PerspectiveSelectDetail(
true,
row,
column_names,
model._last_insert_configs ?? [],
[config],
);
model._last_insert_configs = [config];
}
await regularTable.draw({ preserve_width: true });
viewer.dispatchEvent(
new CustomEvent<PerspectiveSelectDetail>(
"perspective-global-filter",
{
bubbles: true,
composed: true,
detail,
},
),
);
};
addAreaMouseSelection(this, this.regular_table, {
className: "psp-select-region",
onSelect,
});
// Create shared state map for focus tracking
const selected_position_map: SelectedPositionMap = new WeakMap();
this.regular_table.addStyleListener(
createConsolidatedStyleListener(
this,
this.model,
this.regular_table,
viewer,
selected_position_map,
),
);
this.regular_table.addEventListener(
"click",
createClickListener(this.regular_table),
);
// User event click
this.regular_table.addEventListener(
"click",
createDispatchClickListener(this.model, this.regular_table, viewer),
);
// tree collapse, expand, edit button headers
this.regular_table.addEventListener(
"mousedown",
createMousedownListener(this.model, this.regular_table, viewer),
);
this.regular_table.addEventListener(
"dblclick",
createDblclickListener(this.model, this.regular_table, viewer),
);
// Editing event handlers
this.regular_table.addEventListener(
"click",
createEditClickListener(this.model, this.regular_table, viewer),
);
this.regular_table.addEventListener(
"focusin",
createFocusinListener(
this.model,
this.regular_table,
viewer,
selected_position_map,
),
);
this.regular_table.addEventListener(
"focusout",
createFocusoutListener(
this.model,
this.regular_table,
viewer,
selected_position_map,
),
);
this.regular_table.addEventListener(
"keydown",
createKeydownListener(
this.model,
this.regular_table,
viewer,
selected_position_map,
),
);
// viewer event listeners
viewer.addEventListener(
"perspective-toggle-column-settings",
(event: Event) => {
const toggleEvent = event as ToggleColumnSettingsEvent;
if (this.isConnected) {
style_selected_column(
this.model!,
this.regular_table,
viewer,
toggleEvent.detail.column_name ?? undefined,
);
if (!toggleEvent.detail.open) {
this.model!._column_settings_selected_column =
undefined;
return;
}
this.model!._column_settings_selected_column =
toggleEvent.detail.column_name ?? undefined;
}
},
);
this._initialized = true;
} else {
await createModel.call(
this,
this.regular_table,
table,
view,
viewer.getAttribute("theme")!,
this.model,
);
}
}
@@ -0,0 +1,187 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { ColumnType } from "@perspective-dev/client";
import type { DatagridPluginElement } from "../types.js";
interface ViewerConfigLike {
group_by?: string[];
group_rollup_mode?: string;
}
type ControlSpec = Record<string, unknown> & { kind: string };
export interface ColumnConfigSchema {
fields: ControlSpec[];
}
/**
* Plugin schema for the Datagrid column-settings sidebar. Returns the
* controls the viewer should render in the Style tab for a given column.
*
* Each entry in `fields` is a `ControlSpec` discriminated by `kind`.
* Composite kinds (`NumberStyle`, `DatetimeFormat`, `StringFormat`,
* `NumberFormat`, `AggregateDepth`) own a fixed key namespace and
* carry only their `default`. Primitive kinds (`Enum`, `Bool`, `Color`,
* etc.) carry their own `key` (storage) and `label` (UI) inline.
*
* Aggregate Depth is plugin-owned — surfaced only inside the Datagrid
* because rollup-mode pivots are a Datagrid concern. Emitted only when
* the active view has a non-empty `group_by` and rollup mode is `Rollup`.
*/
interface ColumnStats {
abs_max?: number;
}
export default function column_config_schema(
this: DatagridPluginElement,
type: ColumnType,
_group: string | undefined,
_column_name: string,
current_value: Record<string, unknown> | null,
viewer_config?: ViewerConfigLike,
column_stats?: ColumnStats,
): ColumnConfigSchema {
const fields: ControlSpec[] = [];
if (type === "integer" || type === "float") {
const pos_fg = this.model!._pos_fg_color[0];
const neg_fg = this.model!._neg_fg_color[0];
const pos_bg = this.model!._pos_bg_color[0];
const neg_bg = this.model!._neg_bg_color[0];
fields.push({
kind: "Enum",
key: "number_fg_mode",
default: "color",
variants: [
{ value: "disabled", label: "Disabled" },
{ value: "color", label: "Color" },
{ value: "bar", label: "Bar" },
{ value: "label-bar", label: "Gradient" },
],
});
const fg_mode = (current_value?.number_fg_mode as string) ?? "color";
if (fg_mode !== "disabled") {
fields.push({
kind: "ColorRange",
key_pos: "pos_fg_color",
key_neg: "neg_fg_color",
default_pos: pos_fg,
default_neg: neg_fg,
is_gradient: false,
});
}
if (fg_mode === "bar" || fg_mode === "label-bar") {
fields.push({
kind: "Number",
key: "fg_gradient",
default: column_stats?.abs_max ?? 0,
include: true,
});
}
fields.push({
kind: "Enum",
key: "number_bg_mode",
default: "disabled",
variants: [
{ value: "disabled", label: "Disabled" },
{ value: "color", label: "Color" },
{ value: "gradient", label: "Gradient" },
{ value: "pulse", label: "Pulse" },
],
});
const bg_mode = (current_value?.number_bg_mode as string) ?? "disabled";
if (bg_mode !== "disabled") {
fields.push({
kind: "ColorRange",
key_pos: "pos_bg_color",
key_neg: "neg_bg_color",
default_pos: pos_bg,
default_neg: neg_bg,
is_gradient: bg_mode === "gradient" || bg_mode === "pulse",
});
}
if (bg_mode === "gradient") {
fields.push({
kind: "Number",
key: "bg_gradient",
include: true,
default: column_stats?.abs_max ?? 0,
});
}
fields.push({ kind: "NumberFormat" });
} else if (type === "date" || type === "datetime") {
fields.push({ kind: "DatetimeFormat" });
fields.push({
kind: "Enum",
key: "datetime_color_mode",
default: "none",
variants: [
{ value: "none", label: "None" },
{ value: "foreground", label: "Foreground" },
{ value: "background", label: "Background" },
],
});
const dt_mode =
(current_value?.datetime_color_mode as string) ?? "none";
if (dt_mode !== "none") {
fields.push({
kind: "Color",
key: "color",
default: this.model!._color[0],
});
}
} else if (type === "string") {
fields.push({ kind: "StringFormat" });
fields.push({
kind: "Enum",
key: "string_color_mode",
default: "none",
variants: [
{ value: "none", label: "None" },
{ value: "foreground", label: "Foreground" },
{ value: "background", label: "Background" },
{ value: "series", label: "Series" },
],
});
const str_mode = (current_value?.string_color_mode as string) ?? "none";
if (str_mode !== "none") {
fields.push({
kind: "Color",
key: "color",
default: this.model!._color[0],
});
}
}
const group_by = viewer_config?.group_by ?? [];
const is_rollup =
(viewer_config?.group_rollup_mode ?? "rollup") === "rollup";
if (group_by.length > 0 && is_rollup) {
fields.push({ kind: "AggregateDepth" });
}
return { fields };
}
@@ -0,0 +1,71 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import {
restore_column_size_overrides,
save_column_size_overrides,
} from "../model/column_overrides.js";
import type { View } from "@perspective-dev/client";
import type { DatagridPluginElement } from "../types.js";
interface DatagridPluginWithActivate extends DatagridPluginElement {
activate(view: View): Promise<void>;
}
export async function draw(
this: DatagridPluginWithActivate,
view: View,
): Promise<void> {
if (this.parentElement) {
await this.activate(view);
}
if (!this.isConnected || this.offsetParent == null || !this.model) {
return;
}
const old_sizes = save_column_size_overrides.call(this);
const drawPromise = this.regular_table.draw({
invalid_columns: true,
} as any);
if (this._reset_scroll_top) {
this.regular_table.scrollTop = 0;
this._reset_scroll_top = false;
}
if (this._reset_scroll_left) {
this.regular_table.scrollLeft = 0;
this._reset_scroll_left = false;
}
if (this._reset_select) {
this.regular_table.dispatchEvent(
new CustomEvent("psp-deselect-all", { bubbles: false }),
);
this._reset_select = false;
}
if (this._reset_column_size) {
this.regular_table.resetAutoSize();
this._reset_column_size = false;
}
restore_column_size_overrides.call(this, old_sizes);
await drawPromise;
this._toolbar?.classList.toggle(
"aggregated",
this.model._config.group_by.length > 0 ||
this.model._config.split_by.length > 0,
);
}
@@ -0,0 +1,114 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { restore_column_size_overrides } from "../model/column_overrides.js";
import {
EDIT_MODES,
toggle_edit_mode,
toggle_scroll_lock,
} from "../model/toolbar.js";
import { PRIVATE_PLUGIN_SYMBOL } from "../model/index.js";
import { make_color_record } from "../color_utils.js";
import type {
DatagridPluginElement,
ColumnOverrides,
EditMode,
ColumnsConfig,
ColorRecord,
} from "../types.js";
interface RestoreToken {
columns?: Record<string, { column_size_override?: number }>;
edit_mode?: EditMode;
scroll_lock?: boolean;
}
// interface ColumnConfigWithColors {
// pos_fg_color?: string;
// neg_fg_color?: string;
// pos_bg_color?: string;
// neg_bg_color?: string;
// color?: string;
// [key: string]: unknown;
// }
interface StylesConfig {
pos_fg_color?: ColorRecord;
neg_fg_color?: ColorRecord;
pos_bg_color?: ColorRecord;
neg_bg_color?: ColorRecord;
color?: ColorRecord;
[key: string]: unknown;
}
export function restore(
this: DatagridPluginElement,
token: RestoreToken,
columns: ColumnsConfig,
): void {
token = JSON.parse(JSON.stringify(token));
columns = JSON.parse(JSON.stringify(columns));
const overrides: ColumnOverrides = {};
if (token.columns) {
for (const [col, value] of Object.entries(token.columns)) {
if (value.column_size_override !== undefined) {
overrides[col] = value.column_size_override;
delete value["column_size_override"];
}
}
}
const styles: Record<string, StylesConfig> = {};
if (columns) {
for (const [col_name, controls] of Object.entries(columns)) {
styles[col_name] = {
...controls,
pos_fg_color: controls.pos_fg_color
? make_color_record(controls.pos_fg_color)
: undefined,
neg_fg_color: controls.neg_fg_color
? make_color_record(controls.neg_fg_color)
: undefined,
pos_bg_color: controls.pos_bg_color
? make_color_record(controls.pos_bg_color)
: undefined,
neg_bg_color: controls.neg_bg_color
? make_color_record(controls.neg_bg_color)
: undefined,
color: controls.color
? make_color_record(controls.color)
: undefined,
};
}
}
if ("edit_mode" in token) {
if (EDIT_MODES.indexOf(token.edit_mode!) !== -1) {
toggle_edit_mode.call(this, token.edit_mode);
} else {
console.error("Unknown edit mode " + token.edit_mode);
}
} else {
toggle_edit_mode.call(this, "READ_ONLY");
}
if ("scroll_lock" in token) {
toggle_scroll_lock.call(this, token.scroll_lock);
} else {
toggle_scroll_lock.call(this, false);
}
const datagrid = this.regular_table;
restore_column_size_overrides.call(this, overrides, true);
(datagrid as any)[PRIVATE_PLUGIN_SYMBOL] = styles as ColumnsConfig;
}
@@ -0,0 +1,42 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { save_column_size_overrides } from "../model/column_overrides.js";
import type { DatagridPluginElement, DatagridPluginConfig } from "../types.js";
export function save(
this: DatagridPluginElement,
): DatagridPluginConfig | Record<string, never> {
if (this.regular_table) {
const token: DatagridPluginConfig = {
columns: {},
scroll_lock: !!this._is_scroll_lock,
edit_mode: this._edit_mode,
};
const column_size_overrides = save_column_size_overrides.call(this);
for (const col of Object.keys(column_size_overrides || {})) {
if (!token.columns?.[col]) {
token.columns = token.columns || {};
token.columns[col] = {};
}
token.columns[col].column_size_override =
column_size_overrides[col];
}
return JSON.parse(JSON.stringify(token));
}
return {};
}
@@ -0,0 +1,227 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { RegularTableElement } from "regular-table";
import {
type DatagridModel,
type ColumnsConfig,
get_psp_type,
} from "../types.js";
import { cell_style_numeric } from "./table_cell/numeric.js";
import { cell_style_string } from "./table_cell/string.js";
import { cell_style_datetime } from "./table_cell/datetime.js";
import { cell_style_boolean } from "./table_cell/boolean.js";
import { cell_style_row_header } from "./table_cell/row_header.js";
import { CollectedCell } from "./types.js";
/**
* Apply styles to all body cells in a single pass.
*/
export function applyBodyCellStyles(
model: DatagridModel,
cells: CollectedCell[],
plugins: ColumnsConfig,
isSettingsOpen: boolean,
isSelectable: boolean,
isEditable: boolean,
regularTable: RegularTableElement,
): void {
const selectedId = isSelectable ? model._tree_selection_id : undefined;
regularTable.classList.toggle(
"flat-group-rollup-mode",
model._config.group_rollup_mode === "flat",
);
for (const { element: td, metadata, isHeader } of cells) {
const column_name =
metadata.column_header?.[model._config.split_by.length];
const type = get_psp_type(model, metadata);
const plugin = column_name
? plugins[column_name.toString()]
: undefined;
const is_numeric = type === "integer" || type === "float";
// Calculate aggregate depth visibility
// @ts-ignore
metadata._is_hidden_by_aggregate_depth =
model._config.group_rollup_mode === "rollup" &&
((x?: number) =>
x === 0 || x === undefined
? false
: x - 1 <
Math.min(
model._config.group_by.length,
plugin?.aggregate_depth || 0,
))(
(metadata.row_header as unknown[] | undefined)?.filter(
(x) => x !== undefined,
)?.length,
);
// Apply type-specific cell styling
if (is_numeric) {
cell_style_numeric(
model,
plugin as any,
td,
metadata as any,
isSettingsOpen,
);
} else if (type === "boolean") {
cell_style_boolean(model, plugin, td, metadata as any);
} else if (type === "string") {
cell_style_string(model, plugin as any, td, metadata as any);
} else if (type === "date" || type === "datetime") {
cell_style_datetime(model, plugin as any, td, metadata);
} else {
td.style.backgroundColor = "";
td.style.color = "";
}
// Apply common cell classes
td.classList.toggle(
"psp-bool-type",
type === "boolean" && metadata.user !== null,
);
td.classList.toggle("psp-null", metadata.value === null);
td.classList.toggle("psp-align-right", !isHeader && is_numeric);
td.classList.toggle("psp-align-left", isHeader || !is_numeric);
if (model._column_settings_selected_column) {
td.classList.toggle(
"psp-menu-open",
column_name === model._column_settings_selected_column,
);
} else {
td.classList.toggle("psp-menu-open", false);
}
td.classList.toggle(
"psp-color-mode-bar",
plugin?.number_fg_mode === "bar" && is_numeric,
);
td.classList.toggle(
"psp-color-mode-label-bar",
plugin?.number_fg_mode === "label-bar" && is_numeric,
);
// Apply row header styling
if (isHeader) {
cell_style_row_header(model, regularTable, td, metadata as any);
}
// Set data attributes
const tr = td.parentElement as HTMLElement;
if (tr) {
tr.dataset.y = String(metadata.y);
}
if (
metadata.type !== "row_header" ||
metadata.row_header_x ===
(metadata.row_header as unknown[]).length - 1 ||
(metadata.row_header as unknown[])[metadata.row_header_x + 1] ===
undefined
) {
td.dataset.y = String(metadata.y);
if (metadata.type !== "row_header") {
td.dataset.x = String(metadata.x);
} else {
delete td.dataset.x;
}
} else {
delete td.dataset.y;
delete td.dataset.x;
}
// Apply tree selection styling (SELECT_ROW_TREE).
// psp-select-region-inactive is exclusively a tree-selection class,
// so always clean it up. psp-select-region is shared with the
// coordinate-based selection modes, so only touch it when in
// SELECT_ROW_TREE mode (isSelectable).
td.classList.toggle("psp-select-region-inactive", false);
if (isSelectable) {
if (!selectedId) {
td.classList.toggle("psp-select-region", false);
} else {
const id = model._ids[(metadata.y ?? 0) - (metadata.y0 ?? 0)];
const key_match = selectedId.reduce<boolean>(
(agg, x, i) => agg && x === id[i],
true,
);
const isExact = id.length === selectedId.length && key_match;
const isSub = id.length !== selectedId.length && key_match;
if (isHeader) {
if (
metadata.type === "row_header" &&
metadata.row_header_x !== undefined &&
!!id[metadata.row_header_x]
) {
td.classList.toggle("psp-select-region", false);
} else {
td.classList.toggle("psp-select-region", isExact);
td.classList.toggle(
"psp-select-region-inactive",
isSub,
);
}
} else {
td.classList.toggle("psp-select-region", isExact);
td.classList.toggle("psp-select-region-inactive", isSub);
}
}
// } else if (
// model._edit_mode === "READ_ONLY" ||
// model._edit_mode === "EDIT"
// ) {
// td.classList.toggle("psp-select-region", false);
}
// Apply editable styling (if editable)
if (!isHeader && metadata.type === "body") {
if (isEditable && model._is_editable[metadata.x]) {
const col_name =
metadata.column_header?.[model._config.split_by.length];
const col_name_str = col_name?.toString();
if (
col_name_str &&
type === "string" &&
plugins[col_name_str]?.format === "link"
) {
td.toggleAttribute("contenteditable", false);
td.classList.toggle("boolean-editable", false);
} else if (type === "boolean") {
td.toggleAttribute("contenteditable", false);
td.classList.toggle(
"boolean-editable",
(metadata as { user?: unknown }).user !== null,
);
} else {
if (isEditable !== td.hasAttribute("contenteditable")) {
td.toggleAttribute("contenteditable", isEditable);
}
td.classList.toggle("boolean-editable", false);
}
} else {
td.toggleAttribute("contenteditable", false);
td.classList.toggle("boolean-editable", false);
}
}
}
}
@@ -0,0 +1,196 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { RegularTableElement } from "regular-table";
import { get_psp_type, type DatagridModel } from "../types.js";
import { CollectedHeaderRow } from "./types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
/**
* Apply selected column styling in response to column settings toggle events.
* This is called directly (not as a style listener) when the user opens/closes
* the column settings panel.
*/
export function style_selected_column(
model: DatagridModel,
regularTable: RegularTableElement,
viewer: HTMLPerspectiveViewerElement,
selectedColumn: string | undefined,
): void {
const group_header_trs = Array.from(
regularTable.children[0].children[0].children,
) as HTMLTableRowElement[];
const len = group_header_trs.length;
const settings_open = viewer.hasAttribute("settings");
if (len <= 1) {
group_header_trs[0]?.removeAttribute("id");
} else {
group_header_trs.forEach((tr, i) => {
const offset = settings_open ? 1 : 0;
const id =
i === len - (offset + 1)
? "psp-column-titles"
: i === len - offset
? "psp-column-edit-buttons"
: null;
id ? tr.setAttribute("id", id) : tr.removeAttribute("id");
});
}
viewer.classList.toggle("psp-menu-open", !!selectedColumn);
if (settings_open && len >= 2) {
const titles = Array.from(
group_header_trs[len - 2].children,
) as HTMLElement[];
const editBtns = Array.from(
group_header_trs[len - 1].children,
) as HTMLElement[];
if (titles && editBtns) {
group_header_trs.slice(0, len - 2).forEach((tr) => {
Array.from(tr.children).forEach((th) => {
th.classList.toggle("psp-menu-open", false);
});
});
for (let i = 0; i < titles.length; i++) {
const title = titles[i];
const editBtn = editBtns[i];
const open = title.textContent === selectedColumn;
title.classList.toggle("psp-menu-open", open);
editBtn.classList.toggle("psp-menu-open", open);
if (model._config.columns.length > 1) {
for (const r of regularTable.querySelectorAll("td")) {
const meta = regularTable.getMeta(r);
if (!meta?.column_header) {
continue;
}
const isOpen =
meta.column_header[
meta.column_header.length - 2
] === selectedColumn;
r.classList.toggle("psp-menu-open", isOpen);
}
}
}
}
}
}
/**
* Style a single column header row.
*/
export function styleColumnHeaderRow(
model: DatagridModel,
headerRow: CollectedHeaderRow,
regularTable: RegularTableElement,
is_menu_row: boolean,
): void {
const header_depth =
model._config.group_by.length -
(model._config.group_rollup_mode === "flat" ? 1 : 0);
const selectedColumn = model._column_settings_selected_column;
for (const { element: td, metadata } of headerRow.cells) {
if (
!metadata ||
metadata.type === "body" ||
metadata.type === "row_header"
) {
continue;
}
const column_name =
metadata.column_header?.[model._config.split_by.length];
const sort = model._config.sort.find((x) => x[0] === column_name);
const is_corner = typeof metadata.x === "undefined";
const needs_border =
(metadata.type === "corner" &&
metadata.row_header_x === header_depth) ||
(!is_corner &&
(metadata.x + 1) % model._config.columns.length === 0);
td.classList.toggle("psp-header-border", needs_border);
td.classList.toggle("psp-header-group", false);
td.classList.toggle("psp-header-leaf", true);
td.classList.toggle("psp-is-top", false);
td.classList.toggle("psp-header-corner", is_corner);
td.classList.toggle(
"psp-header-sort-asc",
!is_menu_row && !!sort && sort[1] === "asc",
);
td.classList.toggle(
"psp-header-sort-desc",
!is_menu_row && !!sort && sort[1] === "desc",
);
td.classList.toggle(
"psp-header-sort-col-asc",
!is_menu_row && !!sort && sort[1] === "col asc",
);
td.classList.toggle(
"psp-header-sort-col-desc",
!is_menu_row && !!sort && sort[1] === "col desc",
);
td.classList.toggle(
"psp-header-sort-abs-asc",
!is_menu_row && !!sort && sort[1] === "asc abs",
);
td.classList.toggle(
"psp-header-sort-abs-desc",
!is_menu_row && !!sort && sort[1] === "desc abs",
);
td.classList.toggle(
"psp-header-sort-abs-col-asc",
!is_menu_row && !!sort && sort[1] === "col asc abs",
);
td.classList.toggle(
"psp-header-sort-abs-col-desc",
!is_menu_row && !!sort && sort[1] === "col desc abs",
);
const type = get_psp_type(model, metadata);
const is_numeric = type === "integer" || type === "float";
const is_string = type === "string";
const is_date = type === "date";
const is_datetime = type === "datetime";
td.classList.toggle("psp-align-right", is_numeric);
td.classList.toggle("psp-align-left", !is_numeric);
td.classList.toggle(
"psp-menu-enabled",
(is_string || is_numeric || is_date || is_datetime) &&
!is_corner &&
metadata.column_header_y === model._config.split_by.length + 1,
);
td.classList.toggle(
"psp-sort-enabled",
(is_string || is_numeric || is_date || is_datetime) &&
!is_corner &&
metadata.column_header_y === model._config.split_by.length,
);
td.classList.toggle(
"psp-is-width-override",
regularTable.saveColumnSizes()[metadata.size_key!] !== undefined,
);
// Apply menu-open for selected column
if (model._config.columns.length > 1 && selectedColumn) {
const isOpen =
metadata.column_header?.[metadata.column_header.length - 2] ===
selectedColumn;
td.classList.toggle("psp-menu-open", isOpen);
}
}
}
@@ -0,0 +1,118 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { RegularTableElement } from "regular-table";
import { PRIVATE_PLUGIN_SYMBOL } from "../model/index.js";
import type {
DatagridModel,
ColumnsConfig,
DatagridPluginElement,
SelectedPositionMap,
} from "../types.js";
import { isEditableMode } from "../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
import { applyFocusStyle } from "./focus.js";
import { applyColumnHeaderStyles } from "./editable.js";
import { applyGroupHeaderStyles } from "./group_header.js";
import { applyBodyCellStyles } from "./body.js";
import { CellMetadata } from "regular-table/dist/esm/types.js";
import { CollectedCell, CollectedHeaderRow } from "./types.js";
/**
* Consolidated style listener that handles all cell styling in a single pass.
* This eliminates redundant DOM traversals and reduces layout thrashing by:
* 1. Collecting all cell metadata in a read phase
* 2. Applying all styles in a write phase
*/
export function createConsolidatedStyleListener(
datagrid: DatagridPluginElement,
model: DatagridModel,
regularTable: RegularTableElement,
viewer: HTMLPerspectiveViewerElement,
selectedPositionMap: SelectedPositionMap,
): () => void {
return function consolidatedStyleListener(): void {
const plugins: ColumnsConfig =
(regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {};
const isSettingsOpen = viewer.hasAttribute("settings");
const isSelectable = model._edit_mode === "SELECT_ROW_TREE";
const isEditable = isEditableMode(model, viewer);
const isEditableAllowed = isEditableMode(model, viewer, true);
// Toggle edit mode class on datagrid
datagrid.classList.toggle("edit-mode-allowed", isEditableAllowed);
const bodyCells: CollectedCell[] = [];
const groupHeaderRows: CollectedHeaderRow[] = [];
const tbody = regularTable.children[0]?.children[1];
if (tbody) {
for (const tr of tbody.children) {
for (const cell of tr.children) {
const metadata = regularTable.getMeta(
cell as HTMLElement,
) as CellMetadata | undefined;
if (
metadata &&
(metadata.type === "body" ||
metadata.type === "row_header")
) {
const isHeader = cell.tagName === "TH";
bodyCells.push({
element: cell as HTMLElement,
metadata,
isHeader,
});
}
}
}
}
// Collect header rows (thead)
const thead = regularTable.children[0]?.children[0];
if (thead) {
for (const tr of thead.children) {
const rowData: CollectedHeaderRow = {
row: tr as HTMLTableRowElement,
cells: [],
};
for (const cell of tr.children) {
const metadata = regularTable.getMeta(
cell as HTMLElement,
) as CellMetadata | undefined;
rowData.cells.push({
element: cell as HTMLTableCellElement,
metadata,
});
}
groupHeaderRows.push(rowData);
}
}
applyBodyCellStyles(
model,
bodyCells,
plugins,
isSettingsOpen,
isSelectable,
isEditable,
regularTable,
);
applyGroupHeaderStyles(model, groupHeaderRows, regularTable);
applyColumnHeaderStyles(model, groupHeaderRows, regularTable, viewer);
applyFocusStyle(model, bodyCells, regularTable, selectedPositionMap);
};
}
@@ -0,0 +1,100 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { RegularTableElement } from "regular-table";
import type { DatagridModel } from "../types.js";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
import { styleColumnHeaderRow } from "./column_header.js";
import { CollectedHeaderRow } from "./types.js";
/**
* Apply styles to column header rows.
*/
export function applyColumnHeaderStyles(
model: DatagridModel,
headerRows: CollectedHeaderRow[],
regularTable: RegularTableElement,
viewer: HTMLPerspectiveViewerElement,
): void {
if (headerRows.length === 0) {
return;
}
// Style selected column for settings panel
const selectedColumn = model._column_settings_selected_column;
const len = headerRows.length;
const settings_open = viewer.hasAttribute("settings");
// Set row IDs
if (len <= 1) {
headerRows[0]?.row.removeAttribute("id");
} else {
headerRows.forEach(({ row }, i) => {
const offset = settings_open ? 1 : 0;
const id =
i === len - (offset + 1)
? "psp-column-titles"
: i === len - offset
? "psp-column-edit-buttons"
: null;
id ? row.setAttribute("id", id) : row.removeAttribute("id");
});
}
viewer.classList.toggle("psp-menu-open", !!selectedColumn);
// Style column titles and edit buttons when settings open
if (settings_open && len >= 2) {
const titlesRow = headerRows[len - 2];
const editBtnsRow = headerRows[len - 1];
if (titlesRow && editBtnsRow) {
// Clear menu-open from other rows
headerRows.slice(0, len - 2).forEach(({ cells }) => {
cells.forEach(({ element }) => {
element.classList.toggle("psp-menu-open", false);
});
});
for (let i = 0; i < titlesRow.cells.length; i++) {
const title = titlesRow.cells[i]?.element;
const editBtn = editBtnsRow.cells[i]?.element;
if (!title || !editBtn) {
continue;
}
const open = title.textContent === selectedColumn;
title.classList.toggle("psp-menu-open", open);
editBtn.classList.toggle("psp-menu-open", open);
}
}
}
// Style the actual column header rows
const colHeadersIndex = model._config.split_by.length;
if (colHeadersIndex < headerRows.length) {
const colHeaders = headerRows[colHeadersIndex];
if (colHeaders) {
styleColumnHeaderRow(model, colHeaders, regularTable, false);
}
}
const menuHeadersIndex = model._config.split_by.length + 1;
if (menuHeadersIndex < headerRows.length) {
const menuHeaders = headerRows[menuHeadersIndex];
if (menuHeaders) {
styleColumnHeaderRow(model, menuHeaders, regularTable, true);
}
}
}
@@ -0,0 +1,102 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { RegularTableElement } from "regular-table";
import type { DatagridModel, SelectedPositionMap } from "../types.js";
import { CollectedCell } from "./types.js";
/**
* Apply focus style to the selected cell.
* Optimized to use collected cells instead of querySelectorAll.
*/
export function applyFocusStyle(
_model: DatagridModel,
cells: CollectedCell[],
regularTable: RegularTableElement,
selectedPositionMap: SelectedPositionMap,
): void {
const selected_position = selectedPositionMap.get(regularTable);
const host = regularTable.getRootNode() as Document;
if (selected_position) {
for (const { element: td, metadata } of cells) {
if (
metadata.type === "body" &&
metadata.x === selected_position.x &&
metadata.y === selected_position.y
) {
if (host.activeElement !== td) {
td.focus({ preventScroll: true });
}
return;
}
}
// If we didn't find the cell to focus, blur current
if (
document.activeElement !== document.body &&
regularTable.contains(host.activeElement)
) {
(host.activeElement as HTMLElement).blur();
}
}
}
/**
* Standalone function to focus the selected cell.
* This collects cells from the table and tries to focus the selected position.
* Returns true if focus was successful, false otherwise.
*
* Used by edit_keydown.ts for keyboard navigation.
*/
export function focusSelectedCell(
regularTable: RegularTableElement,
selectedPositionMap: SelectedPositionMap,
): boolean {
const selected_position = selectedPositionMap.get(regularTable);
if (!selected_position) {
return false;
}
const host = regularTable.getRootNode() as Document;
const tbody = regularTable.children[0]?.children[1];
if (tbody) {
for (const tr of tbody.children) {
for (const cell of tr.children) {
const metadata = regularTable.getMeta(cell as HTMLElement);
if (
metadata?.type === "body" &&
metadata.x === selected_position.x &&
metadata.y === selected_position.y
) {
if (host.activeElement !== cell) {
(cell as HTMLElement).focus({ preventScroll: true });
}
return true;
}
}
}
}
// If we didn't find the cell to focus, blur current
if (
document.activeElement !== document.body &&
regularTable.contains(host.activeElement)
) {
(host.activeElement as HTMLElement).blur();
}
return false;
}
@@ -0,0 +1,88 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { RegularTableElement } from "regular-table";
import type { DatagridModel } from "../types.js";
import { CollectedHeaderRow } from "./types.js";
/**
* Apply styles to group header rows.
*/
export function applyGroupHeaderStyles(
model: DatagridModel,
headerRows: CollectedHeaderRow[],
regularTable: RegularTableElement,
): void {
const header_depth = model._config.group_by.length;
const m: boolean[][] = [];
let marked = new Set<number>();
for (let y = 0; y < headerRows.length; y++) {
const { cells } = headerRows[y];
const tops = new Set<number>();
for (let x = 0; x < cells.length; x++) {
const { element: td, metadata } = cells[x];
if (!metadata) {
continue;
}
td.style.backgroundColor = "";
const needs_border =
(metadata.type === "corner" &&
header_depth > 0 &&
metadata.row_header_x === header_depth) ||
(metadata.type === "column_header" && metadata.x >= 0);
td.classList.toggle("psp-align-right", false);
td.classList.toggle("psp-align-left", false);
td.classList.toggle("psp-header-group", true);
td.classList.toggle("psp-header-leaf", false);
td.classList.toggle("psp-header-border", needs_border);
td.classList.toggle(
"psp-header-group-corner",
metadata.type === "corner",
);
td.classList.toggle("psp-color-mode-bar", false);
td.classList.toggle("psp-color-mode-label-bar", false);
td.classList.toggle("psp-header-sort-asc", false);
td.classList.toggle("psp-header-sort-desc", false);
td.classList.toggle("psp-header-sort-col-asc", false);
td.classList.toggle("psp-header-sort-col-desc", false);
td.classList.toggle("psp-sort-enabled", false);
// Calculate spanning for psp-is-top
let xx = x;
for (; m[y] && m[y][xx]; ++xx) {}
tops.add(xx);
const cell = td;
for (let tx = xx; tx < xx + cell.colSpan; ++tx) {
for (let ty = y; ty < y + cell.rowSpan; ++ty) {
if (!m[ty]) {
m[ty] = [];
}
m[ty][tx] = true;
}
}
cell.classList.toggle("psp-is-top", y === 0 || !marked.has(xx));
}
marked = tops;
}
}
@@ -0,0 +1,36 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { CellMetadata } from "regular-table/dist/esm/types.js";
import type { DatagridModel, ColumnConfig, ColorRecord } from "../../types.js";
export function cell_style_boolean(
model: DatagridModel,
_plugin: ColumnConfig | undefined,
td: HTMLElement,
metadata: CellMetadata,
): void {
// @ts-ignore
if (metadata._is_hidden_by_aggregate_depth) {
td.style.backgroundColor = "";
td.style.color = "";
} else {
const [hex]: ColorRecord | [string, number, number, number, string] =
metadata.user === true
? model._pos_fg_color
: metadata.user === false
? model._neg_fg_color
: ["", 0, 0, 0, ""];
td.style.backgroundColor = "";
td.style.color = hex;
}
}
@@ -0,0 +1,75 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { CellMetadataBody } from "regular-table/dist/esm/types.js";
import type { DatagridModel, ColorRecord } from "../../types.js";
export function style_cell_flash(
model: DatagridModel,
metadata: CellMetadataBody,
td: HTMLElement,
[, , , , , pos_s, pos_e]: ColorRecord,
[, , , , , neg_s, neg_e]: ColorRecord,
is_settings_open: boolean,
): void {
const id = model._ids?.[metadata.dy ?? 0]?.join("|");
const metadata_path = (
is_settings_open
? (metadata.column_header ?? []).slice(0, -1)
: (metadata.column_header ?? [])
).join("|");
if (
model.last_reverse_columns?.has(metadata_path) &&
model.last_reverse_ids?.has(id)
) {
const row_idx = model.last_reverse_ids?.get(id);
const col_idx = model.last_reverse_columns.get(metadata_path);
if (!model._is_old_viewport) {
td.style.animation = "";
} else if (
col_idx !== undefined &&
row_idx !== undefined &&
(model.last_meta?.[col_idx]?.[row_idx] as number | undefined) !==
undefined &&
(model.last_meta![col_idx]![row_idx] as number) >
((metadata.user ?? 0) as number)
) {
td.style.setProperty("--pulse--background-color-start", neg_s);
td.style.setProperty("--pulse--background-color-end", neg_e);
if (td.style.animationName === "pulse_neg") {
td.style.animation = "pulse_neg2 0.5s linear";
} else {
td.style.animation = "pulse_neg 0.5s linear";
}
} else if (
col_idx !== undefined &&
row_idx !== undefined &&
(model.last_meta?.[col_idx]?.[row_idx] as number | undefined) !==
undefined &&
(model.last_meta![col_idx]![row_idx] as number) <
((metadata.user ?? 0) as number)
) {
td.style.setProperty("--pulse--background-color-start", pos_s);
td.style.setProperty("--pulse--background-color-end", pos_e);
if (td.style.animationName === "pulse_pos") {
td.style.animation = "pulse_pos2 0.5s linear";
} else {
td.style.animation = "pulse_pos 0.5s linear";
}
} else if (row_idx !== metadata.dy) {
td.style.animation = "";
}
} else {
td.style.animation = "";
}
}
@@ -0,0 +1,64 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { CellMetadata } from "regular-table/dist/esm/types.js";
import {
rgbaToRgb,
infer_foreground_from_background,
} from "../../color_utils.js";
import type { DatagridModel, ColumnConfig, ColorRecord } from "../../types.js";
interface PluginWithColor extends Omit<ColumnConfig, "color"> {
color?: ColorRecord;
}
export function cell_style_datetime(
model: DatagridModel,
plugin: PluginWithColor,
td: HTMLElement,
metadata: CellMetadata,
): void {
const colorRecord: ColorRecord = (() => {
if (plugin?.color !== undefined) {
return plugin.color;
} else {
return model._color;
}
})();
const [hex, r, g, b] = colorRecord;
// @ts-ignore
if (metadata._is_hidden_by_aggregate_depth) {
td.style.backgroundColor = "";
td.style.color = "";
} else if (
plugin?.datetime_color_mode === "foreground" &&
metadata.user !== null
) {
td.style.color = hex;
td.style.backgroundColor = "";
} else if (
plugin?.datetime_color_mode === "background" &&
metadata.user !== null
) {
const source = model._plugin_background as [number, number, number];
const foreground = infer_foreground_from_background(
rgbaToRgb([r, g, b, 1], source),
);
td.style.color = foreground;
td.style.backgroundColor = hex;
} else {
td.style.backgroundColor = "";
td.style.color = "";
}
}
@@ -0,0 +1,185 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { style_cell_flash } from "./cell_flash.js";
import {
rgbaToRgb,
infer_foreground_from_background,
} from "../../color_utils.js";
import type { DatagridModel, ColumnConfig, ColorRecord } from "../../types.js";
interface CellMetaWithExtras {
_is_hidden_by_aggregate_depth?: boolean;
user?: number;
dy: number;
column_header?: string[];
}
interface PluginWithColors
extends Omit<
ColumnConfig,
"pos_fg_color" | "neg_fg_color" | "pos_bg_color" | "neg_bg_color"
> {
pos_bg_color?: ColorRecord;
neg_bg_color?: ColorRecord;
pos_fg_color?: ColorRecord;
neg_fg_color?: ColorRecord;
}
export function cell_style_numeric(
model: DatagridModel,
plugin: PluginWithColors | undefined,
td: HTMLElement,
metadata: CellMetaWithExtras,
is_settings_open: boolean,
): void {
const is_positive = (metadata.user ?? 0) > 0;
const is_negative = (metadata.user ?? 0) < 0;
let pos_bg_color: ColorRecord;
if (plugin?.pos_bg_color !== undefined) {
pos_bg_color = plugin.pos_bg_color;
} else {
pos_bg_color = model._pos_bg_color;
}
let neg_bg_color: ColorRecord;
if (plugin?.neg_bg_color !== undefined) {
neg_bg_color = plugin.neg_bg_color;
} else {
neg_bg_color = model._neg_bg_color;
}
const bg_tuple: ColorRecord = is_positive
? pos_bg_color
: is_negative
? neg_bg_color
: [
"",
model._plugin_background[0],
model._plugin_background[1],
model._plugin_background[2],
"",
"",
"",
];
{
const [hex, r, g, b] = bg_tuple;
td.style.position = "";
if (metadata._is_hidden_by_aggregate_depth) {
td.style.animation = "";
td.style.backgroundColor = "";
} else if (plugin?.number_bg_mode === "color") {
td.style.animation = "";
td.style.backgroundColor = hex;
} else if (plugin?.number_bg_mode === "gradient") {
const a = Math.max(
0,
Math.min(
1,
Math.abs((metadata.user ?? 0) / (plugin.bg_gradient ?? 1)),
),
);
const source = model._plugin_background as [number, number, number];
const foreground = infer_foreground_from_background(
rgbaToRgb([r, g, b, a], source),
);
td.style.animation = "";
td.style.color = foreground;
td.style.backgroundColor = `rgba(${r},${g},${b},${a})`;
} else if (plugin?.number_bg_mode === "pulse") {
style_cell_flash(
model,
metadata as any,
td,
pos_bg_color,
neg_bg_color,
is_settings_open,
);
td.style.backgroundColor = "";
} else if (
plugin?.number_bg_mode === "disabled" ||
!plugin?.number_bg_mode
) {
td.style.animation = "";
td.style.backgroundColor = "";
} else {
td.style.animation = "";
td.style.backgroundColor = "";
}
}
const fg_tuple: ColorRecord = (() => {
if (plugin?.pos_fg_color !== undefined) {
return is_positive
? plugin.pos_fg_color
: is_negative
? plugin.neg_fg_color!
: [
"",
model._plugin_background[0],
model._plugin_background[1],
model._plugin_background[2],
"",
"",
"",
];
} else {
return is_positive
? model._pos_fg_color
: is_negative
? model._neg_fg_color
: [
"",
model._plugin_background[0],
model._plugin_background[1],
model._plugin_background[2],
"",
"",
"",
];
}
})();
const [hex, , , , gradhex] = fg_tuple;
if (metadata._is_hidden_by_aggregate_depth) {
td.style.backgroundColor = "";
td.style.color = "";
} else if (plugin?.number_fg_mode === "disabled") {
if (plugin?.number_bg_mode === "color") {
const source = model._plugin_background as [number, number, number];
const foreground = infer_foreground_from_background(
rgbaToRgb([bg_tuple[1], bg_tuple[2], bg_tuple[3], 1], source),
);
td.style.color = foreground;
} else if (plugin?.number_bg_mode === "gradient") {
// Color already set above
} else {
td.style.color = "";
}
} else if (plugin?.number_fg_mode === "bar") {
td.style.color = "";
td.style.position = "relative";
td.style.setProperty("--psp-label-bar-color", gradhex);
td.style.setProperty("--psp-label-bar-bg", hex);
} else if (plugin?.number_fg_mode === "label-bar") {
td.style.color = "";
td.style.setProperty("--psp-label-bar-color", gradhex);
td.style.setProperty("--psp-label-bar-bg", hex);
} else if (plugin?.number_fg_mode === "color" || !plugin?.number_fg_mode) {
td.style.color = hex;
}
}
@@ -0,0 +1,54 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import {
CellMetadata,
CellMetadataRowHeader,
} from "regular-table/dist/esm/types.js";
import type { DatagridModel } from "../../types.js";
import { RegularTableElement } from "regular-table";
export function cell_style_row_header(
model: DatagridModel,
regularTable: RegularTableElement,
td: HTMLElement,
metadata: CellMetadataRowHeader,
): void {
const is_not_empty =
metadata.value !== undefined &&
metadata.value !== null &&
metadata.value?.toString()?.trim().length > 0;
const is_leaf =
(metadata.row_header_x ?? 0) >= model._config.group_by.length;
const next = regularTable.getMeta({
dx: 0,
dy: (metadata.y ?? 0) - (metadata.y0 ?? 0) + 1,
} as CellMetadata);
const is_collapse =
next &&
next.row_header &&
typeof next.row_header[(metadata.row_header_x ?? 0) + 1] !==
"undefined";
td.classList.toggle("psp-tree-label", is_not_empty && !is_leaf);
td.classList.toggle(
"psp-tree-label-expand",
is_not_empty && !is_leaf && !is_collapse,
);
td.classList.toggle(
"psp-tree-label-collapse",
is_not_empty && !is_leaf && is_collapse,
);
td.classList.toggle("psp-tree-leaf", is_not_empty && is_leaf);
}
@@ -0,0 +1,104 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import {
hslToRgb,
infer_foreground_from_background,
parseColor,
rgbaToRgb,
rgbToHex,
rgbToHsl,
} from "../../color_utils.js";
import type { DatagridModel, ColumnConfig, ColorRecord } from "../../types.js";
interface CellMetaWithExtras {
_is_hidden_by_aggregate_depth?: boolean;
user?: string | null;
column_header?: string[];
}
interface PluginWithColor extends Omit<ColumnConfig, "color"> {
color?: ColorRecord;
}
export function cell_style_string(
model: DatagridModel,
plugin: PluginWithColor | undefined,
td: HTMLElement,
metadata: CellMetaWithExtras,
): void {
const column_name = metadata.column_header?.[model._config.split_by.length];
const colorRecord: ColorRecord = (() => {
if (plugin?.color !== undefined) {
return plugin.color;
} else {
return model._color;
}
})();
const [hex, r, g, b] = colorRecord;
if (metadata._is_hidden_by_aggregate_depth) {
td.style.backgroundColor = "";
td.style.color = "";
} else if (
plugin?.string_color_mode === "foreground" &&
metadata.user !== null
) {
td.style.color = hex;
td.style.backgroundColor = "";
if (plugin?.format === "link" && td.children[0]) {
(td.children[0] as HTMLElement).style.color = hex;
}
} else if (
plugin?.string_color_mode === "background" &&
metadata.user !== null
) {
const source = model._plugin_background as [number, number, number];
const foreground = infer_foreground_from_background(
rgbaToRgb([r, g, b, 1], source),
);
td.style.color = foreground;
td.style.backgroundColor = hex;
} else if (
plugin?.string_color_mode === "series" &&
metadata.user !== null &&
column_name
) {
if (!model._series_color_map.has(column_name)) {
model._series_color_map.set(column_name, new Map());
model._series_color_seed.set(column_name, 0);
}
const series_map = model._series_color_map.get(column_name)!;
if (metadata.user && !series_map.has(metadata.user)) {
const seed = model._series_color_seed.get(column_name) ?? 0;
series_map.set(metadata.user, seed);
model._series_color_seed.set(column_name, seed + 1);
}
const color_seed = series_map.get(metadata.user!) ?? 0;
const [h, s, l] = rgbToHsl(parseColor(hex));
const rotated = hslToRgb([h + ((color_seed * 150) % 360), s, l]);
const [r2, g2, b2] = rotated;
const hex2 = rgbToHex(rotated);
const source = model._plugin_background as [number, number, number];
const foreground = infer_foreground_from_background(
rgbaToRgb([r2, g2, b2, 1], source),
);
td.style.color = foreground;
td.style.backgroundColor = hex2;
} else {
td.style.backgroundColor = "";
td.style.color = "";
}
}
@@ -0,0 +1,31 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import {
CellMetadata,
CellMetadataBody,
CellMetadataRowHeader,
} from "regular-table/dist/esm/types.js";
export interface CollectedCell {
element: HTMLElement;
metadata: CellMetadataRowHeader | CellMetadataBody;
isHeader: boolean;
}
export interface CollectedHeaderRow {
row: HTMLTableRowElement;
cells: Array<{
element: HTMLTableCellElement;
metadata: CellMetadata | undefined;
}>;
}
+297
View File
@@ -0,0 +1,297 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type {
View,
Table,
ViewConfig,
ColumnType,
SortDir,
ViewWindow,
ViewConfigUpdate,
} from "@perspective-dev/client";
import type {
HTMLPerspectiveViewerElement,
ViewerConfig,
} from "@perspective-dev/viewer";
import type { RegularTableElement } from "regular-table";
import type { CellMetadata, DataResponse } from "regular-table/dist/esm/types";
// Re-export types from regular-table for use throughout the codebase
export type { RegularTableElement as RegularTable };
export function get_psp_type(
model: DatagridModel,
metadata: CellMetadata,
): ColumnType {
if (
metadata.type === "body" ||
metadata.type === "column_header" ||
metadata.type === "corner"
) {
return model._column_types[metadata.x];
} else {
return model._row_header_types[(metadata.row_header_x ?? 0) - 1];
}
}
// Edit mode for the datagrid
export type EditMode =
| "READ_ONLY"
| "EDIT"
| "SELECT_COLUMN"
| "SELECT_ROW"
| "SELECT_REGION"
| "SELECT_ROW_TREE";
// Color record for styling - tuple returned by make_color_record
export type ColorRecord = [
string, // hex color
number, // red
number, // green
number, // blue
string, // gradient
string, // rgba solid
string, // rgba transparent
];
export type SortTerm = [string, SortDir];
// Selection state for mouse-based region selection
export interface SelectionArea {
x0: number;
x1: number;
y0: number;
y1: number;
}
export interface SelectionState {
selected_areas: SelectionArea[];
dirty: boolean;
CURRENT_MOUSEDOWN_COORDINATES?: { x?: number; y?: number };
old_selected_areas?: SelectionArea[];
potential_selection?: SelectionArea;
}
// Position tracking for cell focus
export interface SelectedPosition {
x: number;
y: number;
content?: string;
}
// Column configuration values from viewer
export interface ColumnConfig {
color?: string;
pos_fg_color?: string;
neg_fg_color?: string;
pos_bg_color?: string;
neg_bg_color?: string;
fg_gradient?: number;
bg_gradient?: number;
number_fg_mode?: string;
number_bg_mode?: string;
string_color_mode?: string;
datetime_color_mode?: string;
fixed?: number;
aggregate_depth?: number;
column_size_override?: number;
format?: string;
date_format?: DateFormatConfig;
number_format?: NumberFormatConfig;
}
// Date format configuration for column styling
export interface DateFormatConfig {
format?: "custom" | string;
timeZone?: string;
dateStyle?: "short" | "medium" | "long" | "full" | "disabled";
timeStyle?: "short" | "medium" | "long" | "full" | "disabled";
second?: "numeric" | "2-digit" | "disabled";
minute?: "numeric" | "2-digit" | "disabled";
hour?: "numeric" | "2-digit" | "disabled";
day?: "numeric" | "2-digit" | "disabled";
weekday?: "narrow" | "short" | "long" | "disabled";
month?: "numeric" | "2-digit" | "narrow" | "short" | "long" | "disabled";
year?: "numeric" | "2-digit" | "disabled";
hour12?: boolean;
fractionalSecondDigits?: 1 | 2 | 3;
}
// Number format configuration for column styling
export interface NumberFormatConfig {
style?: "decimal" | "currency" | "percent" | "unit";
minimumFractionDigits?: number;
maximumFractionDigits?: number;
minimumIntegerDigits?: number;
minimumSignificantDigits?: number;
maximumSignificantDigits?: number;
currency?: string;
currencyDisplay?: "code" | "symbol" | "narrowSymbol" | "name";
notation?: "standard" | "scientific" | "engineering" | "compact";
compactDisplay?: "short" | "long";
useGrouping?: boolean;
}
export type ColumnsConfig = Record<string, ColumnConfig>;
// Plugin save state
export interface DatagridPluginConfig {
columns?: ColumnsConfig;
editable?: boolean;
scroll_lock?: boolean;
edit_mode?: EditMode;
column_size_override?: Record<string, number>;
}
// Element factory for reusing DOM elements
export interface ElemFactory {
clear(): void;
get(): HTMLElement;
}
export type Schema = Record<string, ColumnType>;
// Model object stored on regular-table
export interface DatagridModel {
_edit_port: number;
_view: View;
_table: Table;
_table_schema: Schema;
_config: ViewerConfig;
_num_rows: number;
_num_columns?: number;
_schema: Schema;
_theme: string;
_ids: unknown[][];
_plugin_background: number[];
_color: ColorRecord;
_pos_fg_color: ColorRecord;
_neg_fg_color: ColorRecord;
_pos_bg_color: ColorRecord;
_neg_bg_color: ColorRecord;
_column_paths: string[];
_column_types: ColumnType[];
_is_editable: boolean[];
_edit_mode: EditMode;
_tree_selection_id?: unknown[];
_last_insert_configs?: ViewConfigUpdate[];
_selection_state: SelectionState;
_row_header_types: ColumnType[];
_series_color_map: Map<string, Map<string, number>>;
_series_color_seed: Map<string, number>;
_div_factory: ElemFactory;
_last_window?: ViewWindow;
_is_old_viewport?: boolean;
_reverse_columns?: Map<string, number>;
_reverse_ids?: Map<string, number>;
last_column_paths?: string[];
last_meta?: unknown[][];
last_ids?: unknown[][];
last_reverse_ids?: Map<string, number>;
last_reverse_columns?: Map<string, number>;
get_psp_type(metadata: CellMetadata): ColumnType;
_column_settings_selected_column?: string;
}
// Symbol for private plugin data on regular-table
export const PRIVATE_PLUGIN_SYMBOL: unique symbol = Symbol(
"Perspective Column Config",
);
// Data listener function type
export type DataListener = (
regularTable: RegularTableElement,
x0: number,
y0: number,
x1: number,
y1: number,
) => Promise<DataResponse>;
// Style listener function type
export type StyleListener = () => void;
// Toolbar element interface
export interface DatagridToolbarElement extends HTMLElement {
setEditButton(button: HTMLElement): void;
setScrollLockButton(button: HTMLElement): void;
}
// Column override for persisting column sizes
export type ColumnOverrides = Record<string, number | undefined>;
// Formatter cache types
export interface FormatterCacheEntry {
format(value: unknown): string;
}
export type FormatterCache = Map<string, FormatterCacheEntry>;
// Cell config result from getCellConfig
export interface CellConfigResult {
row: Record<string, unknown>;
column_names: string[];
config: ViewConfigUpdate;
}
// Custom event detail types
export interface PerspectiveClickDetail {
row: Record<string, unknown>;
column_names: string[];
config: Partial<ViewConfig>;
}
export { PerspectiveSelectDetail } from "@perspective-dev/viewer/src/ts/extensions.js";
// Mouse event with handled flag
export interface HandledMouseEvent extends MouseEvent {
handled?: boolean;
}
// Sort order mappings
export type SortRotationOrder = Record<string, SortDir | undefined>;
// Datagrid plugin element interface for toolbar
export interface DatagridPluginElement extends HTMLElement {
regular_table: RegularTableElement;
model?: DatagridModel;
_toolbar?: DatagridToolbarElement;
_edit_button?: HTMLElement;
_scroll_lock?: HTMLElement;
_is_scroll_lock: boolean;
_edit_mode: EditMode;
_initialized?: boolean;
_reset_scroll_top?: boolean;
_reset_scroll_left?: boolean;
_reset_select?: boolean;
_reset_column_size?: boolean;
}
// Map types for selected positions
export type SelectedPositionMap = WeakMap<
RegularTableElement,
SelectedPosition
>;
// Centralized editable mode check - used by style handlers and event handlers
export function isEditableMode(
model: DatagridModel,
viewer: HTMLPerspectiveViewerElement,
allowed: boolean = false,
): boolean {
const has_pivots =
model._config.group_by.length === 0 &&
model._config.split_by.length === 0;
const plugin = viewer.children[0] as DatagridPluginElement | undefined;
const editable = allowed || plugin?._edit_mode === "EDIT";
return has_pivots && editable;
}