chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user