pivotkit-element 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +396 -2
- package/dist/THIRD_PARTY_LICENSES.txt +547 -0
- package/dist/chunks/chunk-3J3RQPK7.js +8 -0
- package/dist/chunks/chunk-75T75RQU.js +6 -0
- package/dist/chunks/chunk-7HSMEVX7.js +1 -0
- package/dist/chunks/chunk-KQ7VYRM6.js +1 -0
- package/dist/chunks/chunk-LXY26OUE.js +2 -0
- package/dist/chunks/chunk-UTPKLCNX.js +1 -0
- package/dist/chunks/echarts-LRMTE3RI.js +18 -0
- package/dist/chunks/export-7NQ77OOT.js +6 -0
- package/dist/chunks/link-EYT7DYWT.js +1 -0
- package/dist/chunks/menus-CB4DJWWT.js +1 -0
- package/dist/element.js +1 -0
- package/dist/index.js +1 -0
- package/package.json +72 -4
- package/types/element.d.ts +428 -0
- package/types/events.d.ts +34 -0
- package/types/global.d.ts +33 -0
- package/types/index.d.ts +22 -0
- package/types/jsx.d.ts +25 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `pivotkit-element` are documented in this file. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-07
|
|
10
|
+
|
|
11
|
+
First release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- The attribute `field-list` (`"open"`, `"collapsed"`): the field list collapsed to a rail, as
|
|
16
|
+
pivotkit-react's `fieldList`.
|
|
17
|
+
- Templates as pivotkit-react's: `templates` entries with `scope` and `file`, the attributes
|
|
18
|
+
`template-store` (`"browser"`: "My views" in localStorage, no event for its saves and deletes) and
|
|
19
|
+
`can-publish`, the event `pivot-template-publish` (`{ name, view, respondWith }`), and `scope` in
|
|
20
|
+
the details of `pivot-template-save` and `pivot-template-delete`.
|
|
21
|
+
- `pivot-export` also for every copy and the drill-down's exports: kinds `copy-table`, `copy-json`,
|
|
22
|
+
`drill-copy`, `drill-json`, `drill-csv`, `drill-xlsx`; a copy has `text`, a drill-down export
|
|
23
|
+
`rows: { shown, count }`; `preventDefault()` keeps a copy from the clipboard as it keeps a file
|
|
24
|
+
from the download. The Export menu loads with the menus (first load 151.0 KiB measured, budget 152
|
|
25
|
+
unchanged); the menus' budget is 17.5 KiB (16.4 measured), the export's 11.5 (10.6 with the rows
|
|
26
|
+
export, TSV and JSON); the size check follows the chunks a chunk loads (the export is loaded from
|
|
27
|
+
the menus).
|
|
28
|
+
- Attributes `share-link`, `persist-view`, `loading`, `field-info` and `zone-info`, and the property
|
|
29
|
+
`chartOptions`, as pivotkit-react's props; `pivot-export` also for a downloaded view (`kind`
|
|
30
|
+
`"json"`). pivotkit-core's link module is a chunk of its own, loaded on the first link; the size
|
|
31
|
+
budget of the first load is 152 KiB (150.7 measured with the chart display settings, sharing, the
|
|
32
|
+
skeleton, the menu changes, the value conditions and locale number entry).
|
|
33
|
+
- Attributes `tips` ("on", "off" or a comma-separated list of tip ids) and `storage-key`, the property
|
|
34
|
+
`dismissedTips` and the event `pivot-tip-dismiss` (`{ id, dismissed }`), as pivotkit-react's tips
|
|
35
|
+
props; `fieldDefaults` takes `label`, `description`, `href` and `type`.
|
|
36
|
+
- The attribute `grow` (pivotkit-react's `grow`: past the height or inside it).
|
|
37
|
+
- The menus' texts load with the menus (pivotkit-react's `menu-labels.js`): the first load stays
|
|
38
|
+
within its 152 KiB budget (151.0 measured with the automatic chart type, the field list's keys, the
|
|
39
|
+
touch targets and the settings' explanations loaded with the menus); the menus' budget is 15 KiB
|
|
40
|
+
(13.8 measured).
|
|
41
|
+
- The attribute `schema-url` (pivotkit-react's `schemaUrl`: the `$schema` of the view files; `""` for none).
|
|
42
|
+
- Attributes `setting-info` (the "i" next to the settings, as pivotkit-react's `settingInfo`),
|
|
43
|
+
`source-id` and `source-name` (the data shown, for the shared view files and the check before a
|
|
44
|
+
view for other data loads), the property `sources` and the event `pivot-source-switch`
|
|
45
|
+
(`{ source, view, name }`: "Switch to … and load"). Full screen works on the report inside the
|
|
46
|
+
shadow root.
|
|
47
|
+
|
|
48
|
+
- `<pivot-report>`: pivotkit-react's `<PivotReport>` as a custom element in an open shadow root, with
|
|
49
|
+
its own React 18.3.1, ReactDOM, pivotkit-react, pivotkit-core and @dnd-kit in the bundle; ECharts
|
|
50
|
+
(bar, line, pie and heatmap only) and the XLSX/CSV export are chunks loaded on first use.
|
|
51
|
+
- Properties `data`, `dataKey`, `view`, `defaultView`, `templates`, `labels`, `fieldDefaults`, `limits`;
|
|
52
|
+
reflected attributes `locale` (else the element's `lang`), `theme`, `height`, `time-zone`,
|
|
53
|
+
`file-name`, `drill-down`, `drill-down-limit`, `save-templates`; JSON attributes `view` and
|
|
54
|
+
`default-view`. Controlled and uncontrolled as in pivotkit-react; a different `defaultView` starts
|
|
55
|
+
the report again on it, the view the report last reported does not. `height="100%"` fills the
|
|
56
|
+
element.
|
|
57
|
+
- Events `pivot-view-change`, `pivot-cell-click`, `pivot-template-save` and `pivot-template-delete`
|
|
58
|
+
(answered with `detail.respondWith(promise)`, a console warning when nothing answered), and
|
|
59
|
+
`pivot-export` (cancelable: the page keeps the file); all bubble and are composed.
|
|
60
|
+
- Styles in the shadow root as one shared constructed stylesheet (a `<style>` element where there is
|
|
61
|
+
none); the `--pk-*` tokens are declared on `:host`, so the page themes the element with rules on it
|
|
62
|
+
and nothing else crosses; `data-pk-theme` on the element tells the theme shown.
|
|
63
|
+
- Lifecycle: properties set before the definition are taken over, a disconnected element unmounts its
|
|
64
|
+
React root (a move in the same task keeps it), a reconnected one mounts again on its last view.
|
|
65
|
+
- `define(name)` for another tag name; loading the package twice does not throw and warns with both
|
|
66
|
+
versions (`PivotReportElement.version`). Imports in Node without defining anything.
|
|
67
|
+
- Type declarations: the element, `PivotReportEventMap`, `HTMLElementTagNameMap` and the event map of
|
|
68
|
+
the DOM; `pivotkit-element/jsx` declares the element in React's JSX.
|
|
69
|
+
- `dist/THIRD_PARTY_LICENSES.txt` with the license texts of the bundled packages; a size budget per
|
|
70
|
+
load group (`npm run size`).
|
|
71
|
+
- `fieldDefaults[field].drillFormat` as in pivotkit-react: the format of a field's values in the
|
|
72
|
+
drill-down.
|
|
73
|
+
- Attribute and property `label`: the name of the report's region landmark, one per report on a
|
|
74
|
+
page.
|
|
75
|
+
- Everything of pivotkit-core and pivotkit-react 0.1.0 in the bundle, among it number ranges with
|
|
76
|
+
"Starting at", custom sort orders (`fieldDefaults[field].order`, reversed too, also for saved views),
|
|
77
|
+
unclickable empty cells with a text for screen readers, row labels that wrap on a phone and are
|
|
78
|
+
shown whole in the status line of a long table, one header for a single value, year ranges without
|
|
79
|
+
thousands separators, CSV percentages and calculations as the table shows them, heatmaps without
|
|
80
|
+
grey empty cells.
|
|
81
|
+
|
|
82
|
+
[Unreleased]: https://github.com/eizedev/pivotkit/compare/pivotkit-element@0.1.0...HEAD
|
|
83
|
+
[0.1.0]: https://github.com/eizedev/pivotkit/releases/tag/pivotkit-element@0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 eizedev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,397 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pivotkit-element
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Documentation: [pivotkit.eize.dev](https://pivotkit.eize.dev/reference/element/) - getting started, every attribute, property and event, theming, security and a [live demo](https://pivotkit.eize.dev/demo/).
|
|
4
|
+
|
|
5
|
+
What pivotkit does, in short: [overview](https://pivotkit.eize.dev/overview/); every feature, also the small behaviours: [feature list](https://pivotkit.eize.dev/features/).
|
|
6
|
+
|
|
7
|
+
The pivot report builder of [pivotkit-react](https://github.com/eizedev/pivotkit/tree/main/packages/pivotkit-react#readme) as one web component,
|
|
8
|
+
`<pivot-report>`, for pages without React: Angular apps, PHP and other server-rendered pages, plain
|
|
9
|
+
HTML. It brings its own React 18.3.1 and ReactDOM, pivotkit-react, [pivotkit-core](https://github.com/eizedev/pivotkit/tree/main/packages/pivotkit-core#readme)
|
|
10
|
+
and @dnd-kit, and renders into an open shadow root. Everything the React component does (field list,
|
|
11
|
+
drag and drop, table and chart, filters, drill-down, undo, saved and shared views, export) works the same; the
|
|
12
|
+
[pivotkit-react README](https://github.com/eizedev/pivotkit/tree/main/packages/pivotkit-react#what-the-user-can-do) describes it.
|
|
13
|
+
|
|
14
|
+
Version 0.1.0: `npm install pivotkit-element`, or serve its `dist/` directory as below. Try it in the
|
|
15
|
+
[example](https://github.com/eizedev/pivotkit/tree/main/examples/element).
|
|
16
|
+
|
|
17
|
+
## Use
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<script type="module" src="/assets/pivotkit-element/dist/index.js"></script>
|
|
21
|
+
|
|
22
|
+
<pivot-report
|
|
23
|
+
locale="de-DE"
|
|
24
|
+
height="640"
|
|
25
|
+
default-view='{"rows":["Region"],"values":[{"field":"Sales","agg":"sum"}]}'
|
|
26
|
+
></pivot-report>
|
|
27
|
+
|
|
28
|
+
<script type="module" src="/assets/report.js"></script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
// report.js
|
|
33
|
+
const report = document.querySelector("pivot-report");
|
|
34
|
+
report.data = rows; // an array of objects, property only
|
|
35
|
+
report.templates = [{ name: "By region", view: { rows: ["Region"] } }];
|
|
36
|
+
report.addEventListener("pivot-view-change", (e) => localStorage.setItem("view", JSON.stringify(e.detail.view)));
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
With a bundler: `npm install pivotkit-element`, then `import "pivotkit-element";` once (it defines
|
|
40
|
+
`<pivot-report>`). `dist/` is ES modules; the files in `dist/chunks/` are loaded by relative URL
|
|
41
|
+
from the entry, so serve the whole `dist/` directory, not `index.js` alone.
|
|
42
|
+
|
|
43
|
+
From another origin (a CDN), module scripts are fetched with CORS: the server must send
|
|
44
|
+
`Access-Control-Allow-Origin` for `index.js` and every chunk. Subresource Integrity covers only the
|
|
45
|
+
`<script integrity>` of the entry; the chunks it imports later cannot carry a hash, so pin a version in
|
|
46
|
+
the URL (or host `dist/` yourself) instead of relying on SRI. The bundle targets ES2022 in Chrome 111,
|
|
47
|
+
Firefox 113 and Safari 16.4 or later (`target` in pivotkit-react's `build.js`); older browsers are not
|
|
48
|
+
supported.
|
|
49
|
+
|
|
50
|
+
| Entry | What |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `pivotkit-element` (`dist/index.js`) | Defines `<pivot-report>`; exports `PivotReportElement`, `define`, `TAG`, `EVENTS` |
|
|
53
|
+
| `pivotkit-element/element` (`dist/element.js`) | The same exports, defines nothing: `define("my-report")` registers another tag name |
|
|
54
|
+
|
|
55
|
+
`define(name)` does nothing when the name is defined already, so a page that loads the package twice
|
|
56
|
+
(two scripts, two copies) throws no error; the first definition stays, and the second copy says so in
|
|
57
|
+
a console warning with both versions (`PivotReportElement.version`). Without `customElements`
|
|
58
|
+
(Node, a server render) importing the package defines nothing and touches no DOM.
|
|
59
|
+
|
|
60
|
+
## Properties and attributes
|
|
61
|
+
|
|
62
|
+
| Property | Attribute | Type | Default | What |
|
|
63
|
+
| --- | --- | --- | --- | --- |
|
|
64
|
+
| `data` | | `object[]` | `[]` | The rows: an array of objects. Encoded once per array, or once per `dataKey`. pivotkit-core takes rows only; columnar data (`{ columns, rows }`) is not supported, map it to objects first. |
|
|
65
|
+
| `dataKey` | | `string \| number` | | The identity of `data`: a new array with the same key is not encoded again. |
|
|
66
|
+
| `view` | `view` (JSON) | view | | Controlled: the view to show, see below. |
|
|
67
|
+
| `defaultView` | `default-view` (JSON) | view | empty | Uncontrolled: the start view, see below. |
|
|
68
|
+
| `templates` | | `{ name?, view?, file?, scope?, deletable? }[]` | `[]` | The templates of the Views menu in two groups by `scope`, "Shared views" and "My views" (without `scope` a `deletable` entry is the user's own); a `view` or a view file in `file`; the first three are also offered in the empty state. |
|
|
69
|
+
| `saveTemplates` | `save-templates` (boolean) | `boolean` | `false` | Offers "Save current view"; a save is a `pivot-template-save` event. It wins over `template-store`. |
|
|
70
|
+
| `templateStore` | `template-store` | `"browser"` | | Keeps "My views" in `localStorage` under `pivotkit:views:<storage-key>`, per data (`source-id`): only this browser and device, lost when the site's data is cleared; at most 100 views. No event for its saves and deletes. |
|
|
71
|
+
| `canPublish` | `can-publish` (boolean) | `boolean` | `false` | Offers "Publish as shared view" on the user's views; a publish is a `pivot-template-publish` event. The element only shows what the page allows: the server must check who may save, publish or delete. |
|
|
72
|
+
| `fieldDefaults` | | `{ [field]: { agg?, format?, drillFormat?, order?, label?, description?, href?, type? } }` | | The aggregator and format a field gets in Values (another aggregator in the value's menu keeps the format while the result is in the field's unit, not for a count); `drillFormat` (else `format`) for its values in the drill-down (`"text"`: an identifier as it is); `order`: the field's custom order of items on Rows or Columns (also in a view without a sort entry for it), offered as "Custom order" and "Custom order, reversed" in its menu; `label`, `description`, `href`, `type`: its display name, its description behind an "i" with "Learn more" (https), its column type (pivotkit-core's `fieldDefaultsFromSchema` reads them from an OpenAPI document). |
|
|
73
|
+
| `labels` | | partial labels | | Texts merged over the language's labels (the keys are in pivotkit-react's `LABELS.en`). |
|
|
74
|
+
| `limits` | | `Partial<Limits>` | | pivotkit-core's limits; a layout or data over them shows a message in place of the result. |
|
|
75
|
+
| `locale` | `locale` | `string` | `"en-US"` | Numbers, dates, sorting and the language of the labels (`de-*` is German). Without it the element's own `lang` attribute is used; the page's language is not. |
|
|
76
|
+
| `theme` | `theme` | `"auto" \| "light" \| "dark"` | `"auto"` | `auto` follows the nearest `data-theme="light\|dark"` on the element or above it (`<html>`), then `prefers-color-scheme`, and changes with them. |
|
|
77
|
+
| `height` | `height` | `number \| string` | `640` | Pixels (`height="480"`) or a CSS length; `"100%"` fills the element, which then needs a height from the page (`pivot-report { height: 70vh }`). |
|
|
78
|
+
| `timeZone` | `time-zone` | `string` | `"UTC"` | The zone `data` is encoded in; see pivotkit-core. |
|
|
79
|
+
| `fileName` | `file-name` | `string` | `"pivot"` | Name of exported files, without the extension; made safe. |
|
|
80
|
+
| `label` | `label` | `string` | `labels.report` | The name of the report's region landmark; give each report on a page its own (`label="Tickets"`). |
|
|
81
|
+
| `drillDown` | `drill-down` | `boolean` | `true` | The panel with the rows behind a clicked value; `drill-down="false"` turns it off. |
|
|
82
|
+
| `drillDownLimit` | `drill-down-limit` | `number` | `1000` | Rows passed to the panel and to `pivot-cell-click`; the panel's exports hold these rows and say when there are more ("1,000 of 4,212 rows"). |
|
|
83
|
+
| `tips` | `tips` | `string` | `"on"` | The tips above the result: `"on"`, `"off"` (none, no switch in Options) or the tip ids allowed, comma-separated. |
|
|
84
|
+
| `storageKey` | `storage-key` | `string` | | Remembers dismissed tips in `localStorage` under `pivotkit:tips:<key>`; one key per element on a page. With `template-store="browser"` the user's own views are kept under `pivotkit:views:<key>` too. |
|
|
85
|
+
| `dismissedTips` | | `string[]` | | Dismissed tip ids from the page's own storage (from `pivot-tip-dismiss`; `"*"`: all off; `"-explanations"`: "Show explanations" off; `"open:<section>"`, `"closed:<section>"`: a menu section opened or folded). |
|
|
86
|
+
| `fieldInfo`, `zoneInfo`, `settingInfo` | `field-info`, `zone-info`, `setting-info` | `"on" \| "off"` | | Decide the "i" of the field descriptions, of the zones and of the settings that need explaining, one kind each; unset, the user's one switch "Show explanations" in Options, Help decides. |
|
|
87
|
+
| `sourceId`, `sourceName` | `source-id`, `source-name` | `string` | | The data shown: it goes into the shared [view files](https://pivotkit.eize.dev/reference/view-file/) and links, and a view made for another source asks before it loads. |
|
|
88
|
+
| `sources` | | `{ id, name? }[]` | | The other data the page can show: a view made for one of them offers "Switch to “name” and load" (`pivot-source-switch`). |
|
|
89
|
+
| `chartOptions` | | `object` | | Plain JSON merged over the chart's ECharts option; it wins over the view's chart settings. |
|
|
90
|
+
| `shareLink` | `share-link` | `boolean \| string` | off | "Copy link" and shared links: the attribute alone for the page's address without its query, a value as the base URL. |
|
|
91
|
+
| `persistView` | `persist-view` | `"session"` | | Keeps the view for the tab (`sessionStorage`, per `storage-key`). |
|
|
92
|
+
| `loading` | `loading` (boolean) | `boolean` | `false` | The page is loading the data: the report shows its skeleton. |
|
|
93
|
+
| `fieldList` | `field-list` | `"open" \| "collapsed"` | the user's choice | The field list of a wide report open or collapsed to a rail, over the user's stored choice. |
|
|
94
|
+
|
|
95
|
+
Properties with an attribute reflect it, as native elements do: `report.theme = "dark"` sets
|
|
96
|
+
`theme="dark"`, `null` removes it. `view` and `default-view` are parsed as JSON objects (a text that
|
|
97
|
+
is not one is ignored with a console warning); setting the property does not write the attribute.
|
|
98
|
+
Data, templates, sources, labels, field defaults, limits and dismissed tips are properties only. A view is user input either
|
|
99
|
+
way: pivotkit-core normalizes it against the data and it cannot run code.
|
|
100
|
+
|
|
101
|
+
The element sets `data-pk-theme="light|dark"` on itself, the theme the report shows. It is an
|
|
102
|
+
output: read it or style with it (below), do not set it.
|
|
103
|
+
|
|
104
|
+
Properties set before the element is defined (a framework that renders before the script has
|
|
105
|
+
loaded) are taken over when it is.
|
|
106
|
+
|
|
107
|
+
`data` is compared by identity, as in React: rows changed in place and the same array set again
|
|
108
|
+
are not encoded again. Set a new array (`report.data = [...rows]`), or a new `dataKey`.
|
|
109
|
+
|
|
110
|
+
On a phone the report's zones sit above the table, so the default 640 px leaves little room for it.
|
|
111
|
+
Give the element more height on narrow screens, for example `height="100%"` and
|
|
112
|
+
`@media (max-width: 639px) { pivot-report { height: calc(100svh - 24px); } }`, as the
|
|
113
|
+
[example](https://github.com/eizedev/pivotkit/tree/main/examples/element) does: the page scrolls to the report, the table scrolls inside it.
|
|
114
|
+
|
|
115
|
+
### Controlled and uncontrolled
|
|
116
|
+
|
|
117
|
+
As with `<PivotReport>`:
|
|
118
|
+
|
|
119
|
+
- Uncontrolled (no `view`): the report starts on `defaultView` and keeps the view itself; every change
|
|
120
|
+
is a `pivot-view-change` event. Setting a different `defaultView` (compared by value) starts the
|
|
121
|
+
report again on it, with a new undo history and Reset pointing there; the same view again changes
|
|
122
|
+
nothing, and so does the view the report last reported: a page that stores every
|
|
123
|
+
`pivot-view-change` and binds it as `defaultView` (`[defaultView]="stored"`) keeps the report and
|
|
124
|
+
its undo history. Feed the view back unchanged and before the next change: an older view (from a
|
|
125
|
+
debounced or asynchronous store) counts as a new start view and starts the report again. That way a framework that sets properties after the element is in the page
|
|
126
|
+
(React 18, Angular) still gets its start view.
|
|
127
|
+
- Controlled (`view` set): the report shows that view. A change is a `pivot-view-change` event, and the
|
|
128
|
+
report shows it only when the page sets `view = event.detail.view`; a page that does not set it keeps
|
|
129
|
+
the report on its view. A view the page sets on its own becomes one undo step. `view = undefined`
|
|
130
|
+
makes it uncontrolled again; switching back and forth is not meant for normal use.
|
|
131
|
+
|
|
132
|
+
Undo and redo always stay inside the element. When the element is removed from the page, its React
|
|
133
|
+
root is unmounted (in a microtask: a move in the same task, `parent.append(el)` say, keeps the report
|
|
134
|
+
and its history); connected again, it mounts a new report that starts on the last view it showed, with
|
|
135
|
+
a new history.
|
|
136
|
+
|
|
137
|
+
## Events
|
|
138
|
+
|
|
139
|
+
Every event is a `CustomEvent` that bubbles and is composed: a listener on the element, on a parent
|
|
140
|
+
or on the document gets it, with `event.target` the element. So does every other script on the page:
|
|
141
|
+
the details carry the view and, for `pivot-cell-click`, the page's own row objects behind the cell.
|
|
142
|
+
A page that shows data some of its scripts must not read keeps those scripts off the page, as it
|
|
143
|
+
would for the data itself.
|
|
144
|
+
|
|
145
|
+
| Event | `detail` | What |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `pivot-view-change` | `{ view }` | Every change of the view, normalized. |
|
|
148
|
+
| `pivot-cell-click` | `{ row, col, value, valueIndex, text, count, rows }` | A click (or Enter) on a value or a chart point: key paths, the value, the number of source rows and the first `drillDownLimit` of them (the page's own row objects). |
|
|
149
|
+
| `pivot-template-save` | `{ name, view, scope, respondWith }` | "Save current view" (with `save-templates`); `scope` is `"user"`. |
|
|
150
|
+
| `pivot-template-delete` | `{ name, scope, respondWith }` | Delete of a template marked `deletable`, after the user confirmed in the menu; `scope` is its group. Not sent for a view of the browser store. |
|
|
151
|
+
| `pivot-template-publish` | `{ name, view, respondWith }` | "Publish as shared view" on one of the user's views (with `can-publish`), after the user confirmed. |
|
|
152
|
+
| `pivot-export` | `{ kind, fileName, blob, text, rows }` | Every exported file before its download and every copy before it goes on the clipboard: `"xlsx"`, `"csv"`, `"png"`, `"json"` for the view file of "Download view"; the copies `"copy-table"` and `"copy-json"` (Export, Copy table and Copy data as JSON); the drill-down panel's `"drill-copy"`, `"drill-json"`, `"drill-csv"` and `"drill-xlsx"`. A copy also has its `text` and the `fileName` it would have as a file (`pivot.tsv`); a drill-down export has `rows: { shown, count }`. Cancelable: `preventDefault()` skips the download or the copy, the page keeps the blob (to upload it, say). |
|
|
153
|
+
| `pivot-tip-dismiss` | `{ id, dismissed }` | A tip dismissed (`id`), all tips off (`"*"`) or back on (`null`), with every dismissed id, for a page that stores them and sets `dismissedTips`. |
|
|
154
|
+
| `pivot-source-switch` | `{ source, view, name }` | "Switch to “name” and load" for a view made for one of `sources`: show that data and set `defaultView` to `view`. |
|
|
155
|
+
|
|
156
|
+
The template events wait for the page's answer: call `event.detail.respondWith(promise)` in the
|
|
157
|
+
listener itself (later throws `InvalidStateError`). The report shows "Saved" or "Published" (or removes the entry)
|
|
158
|
+
when the promise resolves, and an error with its message when it rejects (a rejection without a
|
|
159
|
+
message shows "The server could not save the view.", `labels.toastSaveRejected`). Without `respondWith` (no
|
|
160
|
+
listener, or `respondWith` after an `await`) the action counts as done at once and the element logs a
|
|
161
|
+
console warning, since the user is told "saved", "deleted" or "published" either way. The page then sets
|
|
162
|
+
`templates` to the new list.
|
|
163
|
+
|
|
164
|
+
When a listener of `pivot-export` calls `preventDefault()`, the report shows no "Exported" or
|
|
165
|
+
"Copied" note: the page that kept the file or the text says itself what became of it.
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
report.saveTemplates = true;
|
|
169
|
+
report.addEventListener("pivot-template-save", (e) => {
|
|
170
|
+
e.detail.respondWith(
|
|
171
|
+
fetch("/api/templates", { method: "POST", body: JSON.stringify({ name: e.detail.name, view: e.detail.view }) })
|
|
172
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(r.statusText))))
|
|
173
|
+
.then((list) => (report.templates = list))
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Theming and styles
|
|
179
|
+
|
|
180
|
+
The report's stylesheet lives in the shadow root: no CSS of the page reaches into the report and none
|
|
181
|
+
of the report's reaches out. What does cross is what crosses every shadow root: inherited properties
|
|
182
|
+
(the font family and size the report inherits through the element, `color-scheme`) and the `--pk-*`
|
|
183
|
+
custom properties set **on the element**. Every token is declared on the element's `:host`, so a
|
|
184
|
+
`--pk-*` set higher up the page (on `body`) does not reach the report; a rule of the page on the
|
|
185
|
+
element wins over the defaults:
|
|
186
|
+
|
|
187
|
+
```css
|
|
188
|
+
pivot-report {
|
|
189
|
+
--pk-accent: #0a7d5a;
|
|
190
|
+
--pk-radius: 4px;
|
|
191
|
+
--pk-font: "Inter", sans-serif;
|
|
192
|
+
}
|
|
193
|
+
pivot-report[data-pk-theme="dark"] {
|
|
194
|
+
--pk-surface: #101418;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The tokens and their defaults are listed in the
|
|
199
|
+
[pivotkit-react README](https://github.com/eizedev/pivotkit/tree/main/packages/pivotkit-react#theming). There are no `::part()` names; the
|
|
200
|
+
tokens are the styling interface.
|
|
201
|
+
|
|
202
|
+
The styles reach the shadow root as one constructed stylesheet shared by every element of the
|
|
203
|
+
document (`adoptedStyleSheets`). Where a browser has none, or refuses one, the element adds a
|
|
204
|
+
`<style>` element to its shadow root instead (a browser test covers it). An element moved into
|
|
205
|
+
another document (an iframe) after it was created keeps what it had; that case is not tested.
|
|
206
|
+
|
|
207
|
+
Menus and popovers are `position: fixed`. Inside an ancestor with a `transform`, a `filter` or
|
|
208
|
+
`contain` (router animations, dialogs, carousels) fixed elements are placed relative to that
|
|
209
|
+
ancestor; the report measures its origin and places them at their button all the same (browser test
|
|
210
|
+
with a transformed wrapper). The narrow layout's bottom sheets, the field drawer and the drill-down
|
|
211
|
+
cover that ancestor, not the window, in such a case. The drag preview (also `position: fixed`) is not
|
|
212
|
+
corrected there and is not tested; a scaled ancestor (`scale()`) is not corrected either.
|
|
213
|
+
|
|
214
|
+
## Content-Security-Policy
|
|
215
|
+
|
|
216
|
+
Measured in the browser tests (`tests/browser/element.spec.js`) in Chromium, Firefox and WebKit of
|
|
217
|
+
Playwright 1.64.1, with this policy in a `<meta>` element:
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- The report renders and is styled: the constructed stylesheet is not blocked by `style-src 'self'`,
|
|
224
|
+
and the inline styles React sets (through `element.style`, the CSSOM) are not blocked either.
|
|
225
|
+
- The chart works: ECharts loads from its chunk and draws on a canvas; nothing needs `'unsafe-eval'`.
|
|
226
|
+
Its tooltip shows on hover without a violation: pivotkit-react builds it as DOM nodes with styles
|
|
227
|
+
set through the CSSOM and the data's texts as `textContent`, instead of ECharts' default HTML string
|
|
228
|
+
with inline `style` attributes (which `style-src-attr` would refuse).
|
|
229
|
+
- The XLSX and PNG exports download (a blob URL on an `<a download>`; no `fetch` of `data:`); CSV
|
|
230
|
+
takes the same path as XLSX.
|
|
231
|
+
- No violation is reported (`securitypolicyviolation`) and nothing is logged; the test checks that the
|
|
232
|
+
policy is in force by adding an inline `<style>`, which is refused.
|
|
233
|
+
- The `<style>` fallback is tested on its own page (constructable stylesheets removed), without CSP.
|
|
234
|
+
|
|
235
|
+
Not covered: the `<style>` fallback (browsers without constructable stylesheets) needs
|
|
236
|
+
`style-src 'unsafe-inline'`, since the element has no nonce option; only the `<meta>` form of the
|
|
237
|
+
policy was measured, not an HTTP header; `script-src` must allow the origin that serves `dist/` and
|
|
238
|
+
its chunks. A page script that sets the properties must be a file (or carry a nonce) under such a policy.
|
|
239
|
+
|
|
240
|
+
## Bundle size
|
|
241
|
+
|
|
242
|
+
What a page loads, measured with `npm run size -w pivotkit-element` on one Mac (esbuild 0.28.2,
|
|
243
|
+
gzip level 9 per file, sizes add up per group; approximate, they change with every dependency
|
|
244
|
+
update). CI runs the same command; it fails when a group is over `SIZE_BUDGET_KIB` in `build.js`
|
|
245
|
+
(a few per cent above these numbers):
|
|
246
|
+
|
|
247
|
+
| Loaded | Files | Size | gzip |
|
|
248
|
+
| --- | ---: | ---: | ---: |
|
|
249
|
+
| First (`index.js` and its static imports: React, ReactDOM, pivotkit-react, pivotkit-core, @dnd-kit, styles) | 6 | 464 KiB | 151 KiB |
|
|
250
|
+
| On the first chart (ECharts with bar, line, pie (also the donut), heatmap, the components they use, the title among them, and the canvas renderer) | 1 | 595 KiB | 201 KiB |
|
|
251
|
+
| On the first export or copy (pivotkit-core's export, and fflate as a chunk it shares with the link; the Export menu and the drill-down's start loading it when they open) | 2 | 24 KiB | 11 KiB |
|
|
252
|
+
| On the first shared link, Copy link or loaded link (pivotkit-core's link with fflate) | 2 | 13 KiB | 6 KiB |
|
|
253
|
+
| Right after the first render (the menus' panels, the Export menu, the drill-down's panel and the texts only they show) | 1 | 56 KiB | 16 KiB |
|
|
254
|
+
|
|
255
|
+
ECharts is bundled, not left to the page: a page without a bundler or an import map would have no
|
|
256
|
+
way to provide it, and loading it on first use keeps it out of the first load. Only the four chart
|
|
257
|
+
types the report draws are in the chunk: pivotkit-react's `echarts.js` imports them by name
|
|
258
|
+
(`test/build.test.js` checks the chunk against it). A page that already has ECharts loads it twice; one that needs a single
|
|
259
|
+
copy uses [pivotkit-react](https://github.com/eizedev/pivotkit/tree/main/packages/pivotkit-react#readme) with its own React instead.
|
|
260
|
+
|
|
261
|
+
## React on the page
|
|
262
|
+
|
|
263
|
+
The element's React is private to the bundle: it sets no global, and a page with a React of its own
|
|
264
|
+
(another version, or the same) is not affected, nor is the element by it. The browser tests run a
|
|
265
|
+
page with its own React 18 and, in the React 19 job, React 19, that renders `<pivot-report>` in its
|
|
266
|
+
tree and renders again every 200 ms. Each element has its own React root; the elements of a page share
|
|
267
|
+
one copy of the bundle. A React page can use pivotkit-react directly and save the second React.
|
|
268
|
+
|
|
269
|
+
React 18 passes only attributes to a custom element: set `data` and the other objects in a ref
|
|
270
|
+
(`useEffect(() => { ref.current.data = rows; }, [rows])`) and add listeners there. React 19 sets a prop
|
|
271
|
+
as a property when the element has one, and listens for a custom event with `on` and the event's
|
|
272
|
+
exact name (react.dev, "Custom HTML elements"):
|
|
273
|
+
|
|
274
|
+
```jsx
|
|
275
|
+
// React 19
|
|
276
|
+
<pivot-report data={rows} defaultView={start} theme="auto" onpivot-view-change={(e) => save(e.detail.view)} />
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The test page uses the ref in the React 18 job and these props in the React 19 job, and checks that
|
|
280
|
+
`data` arrives as a property. For TypeScript, `import type {} from "pivotkit-element/jsx";` once
|
|
281
|
+
declares `<pivot-report>` with its properties and `on<event>` props in React's JSX (needs
|
|
282
|
+
`@types/react` 18.3 or later; the types describe React 19's behaviour).
|
|
283
|
+
|
|
284
|
+
## Angular
|
|
285
|
+
|
|
286
|
+
Not built or tested in this repository; the element is a standard custom element, so Angular's
|
|
287
|
+
property and event bindings apply:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
|
291
|
+
import "pivotkit-element";
|
|
292
|
+
import type { PivotView } from "pivotkit-element";
|
|
293
|
+
|
|
294
|
+
@Component({
|
|
295
|
+
selector: "app-ticket-report",
|
|
296
|
+
standalone: true,
|
|
297
|
+
// <pivot-report> is no Angular component: the schema lets the template use it
|
|
298
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
299
|
+
template: `
|
|
300
|
+
<pivot-report
|
|
301
|
+
[data]="rows"
|
|
302
|
+
[defaultView]="start"
|
|
303
|
+
[templates]="templates"
|
|
304
|
+
locale="de-DE"
|
|
305
|
+
height="640"
|
|
306
|
+
(pivot-view-change)="onView($event)"
|
|
307
|
+
></pivot-report>
|
|
308
|
+
`
|
|
309
|
+
})
|
|
310
|
+
export class TicketReportComponent {
|
|
311
|
+
rows: object[] = [];
|
|
312
|
+
templates = [];
|
|
313
|
+
start: PivotView = { rows: ["Team"], values: [{ field: "Ticket", agg: "count" }] };
|
|
314
|
+
|
|
315
|
+
onView(event: Event) {
|
|
316
|
+
const { view } = (event as CustomEvent<{ view: PivotView }>).detail;
|
|
317
|
+
// store the view
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`[data]="rows"` sets the property (Angular binds properties of an element with the schema); a new
|
|
323
|
+
array is a new data set. For the controlled form bind `[view]="view"` and set `this.view` in the
|
|
324
|
+
handler. Binding the stored view back as `[defaultView]` is fine: the element ignores the view it
|
|
325
|
+
reported itself.
|
|
326
|
+
|
|
327
|
+
Change detection: the element runs React's scheduler, ECharts' pointer listeners and animation
|
|
328
|
+
frames in whatever zone created it. Inside Angular's zone (zone.js) each of them may trigger change
|
|
329
|
+
detection of the app. Not measured here; if it shows, create and feed the element outside the zone
|
|
330
|
+
and come back in for the app's own state:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
constructor(private zone: NgZone, private host: ElementRef<HTMLElement>) {}
|
|
334
|
+
|
|
335
|
+
ngAfterViewInit() {
|
|
336
|
+
this.zone.runOutsideAngular(() => {
|
|
337
|
+
const report = document.createElement("pivot-report");
|
|
338
|
+
report.data = this.rows;
|
|
339
|
+
report.addEventListener("pivot-view-change", (e) => this.zone.run(() => this.onView(e)));
|
|
340
|
+
this.host.nativeElement.append(report);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## PHP and server-rendered pages
|
|
346
|
+
|
|
347
|
+
The server writes the rows (and templates) as JSON into a `<script type="application/json">`, which
|
|
348
|
+
the browser does not run, and a module script hands them to the element:
|
|
349
|
+
|
|
350
|
+
```php
|
|
351
|
+
<script type="application/json" id="report-rows">
|
|
352
|
+
<?= json_encode($rows, JSON_HEX_TAG | JSON_HEX_AMP | JSON_THROW_ON_ERROR) ?>
|
|
353
|
+
</script>
|
|
354
|
+
<pivot-report id="report" locale="de-DE" file-name="tickets"></pivot-report>
|
|
355
|
+
<script type="module" src="/assets/pivotkit-element/dist/index.js"></script>
|
|
356
|
+
<script type="module" src="/assets/report.js"></script>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
// report.js
|
|
361
|
+
document.getElementById("report").data = JSON.parse(document.getElementById("report-rows").textContent);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
`JSON_HEX_TAG` writes `<` and `>` as `\u003C` and `\u003E`, so a value containing `</script>`
|
|
365
|
+
cannot end the block. A start view as JSON fits in the `default-view` attribute:
|
|
366
|
+
`default-view='<?= htmlspecialchars(json_encode($view), ENT_QUOTES) ?>'` (`ENT_QUOTES` also encodes
|
|
367
|
+
`'`, which PHP before 8.1 leaves alone by default and the attribute's quotes need). Data never goes
|
|
368
|
+
into an attribute.
|
|
369
|
+
|
|
370
|
+
## Types
|
|
371
|
+
|
|
372
|
+
`types/` holds the declarations: `PivotReportElement` with its properties, the event map
|
|
373
|
+
(`PivotReportEventMap`), and the DOM's maps, so `document.createElement("pivot-report")` is a
|
|
374
|
+
`PivotReportElement` and `addEventListener("pivot-view-change", (e) => e.detail.view)` knows the
|
|
375
|
+
detail, on the element and on the document. The view type is a structural `PivotView`; the full one is
|
|
376
|
+
pivotkit-core's `View`. `pivotkit-element/jsx` (types only) adds the element to React's JSX, see
|
|
377
|
+
above.
|
|
378
|
+
|
|
379
|
+
## Development
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
npm run build -w pivotkit-element # pivotkit-react's dist, then dist/ of this package (esbuild)
|
|
383
|
+
npm run size -w pivotkit-element # the same, with the size of what a page loads, checked against the budget
|
|
384
|
+
npm test -w pivotkit-element # build, then node --test: attributes, events, stylesheet, bundle, Node import
|
|
385
|
+
npm run types -w pivotkit-element # tsc: types/ from the JSDoc, plus src/global.d.ts and src/jsx.d.ts
|
|
386
|
+
npm run test:browser # Playwright in Chromium, Firefox and WebKit, also of the element pages
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`build.js` bundles pivotkit-react as it ships (its `dist/`), with `process.env.NODE_ENV` set to
|
|
390
|
+
`production` and the package's version as `PivotReportElement.version`, and writes
|
|
391
|
+
`dist/THIRD_PARTY_LICENSES.txt` with the license texts of every bundled package, once per name and
|
|
392
|
+
version (React, ReactDOM, scheduler and @dnd-kit: MIT; ECharts: Apache-2.0 with its NOTICE; zrender:
|
|
393
|
+
BSD-3-Clause; tslib: 0BSD; fflate: MIT). The browser test pages are in `tests/browser/pages/element/`.
|
|
394
|
+
|
|
395
|
+
## License
|
|
396
|
+
|
|
397
|
+
[MIT](https://github.com/eizedev/pivotkit/blob/main/LICENSE); the bundled packages under their own licenses, see `dist/THIRD_PARTY_LICENSES.txt`.
|