@spectric/ui 0.0.10 → 0.0.11

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/dist/style.css CHANGED
@@ -1 +1 @@
1
- spectric-input{--input-color: var(--spectric-input-color, #f4f4f4);--border-radius: var(--spectric-border-radius, .4em);--input-bottom: var(--spectric-input-bottom, var(--spectric-button-primary, #a8a8a8));--input-bottom-focused: var(--primary, #1ea7fd);--text-on-color: var(--spectric-text-on-color, #ffffff);--text-on-color-disabled: var(--spectric-text-on-color-disabled, #8d8d8d);--text-placeholder: rgba(22, 22, 22, .4);--text-primary: var(--spectric-text-primary, #161616);--text-secondary: var(--spectric-text-secondary, #525252)}spectric-input .inputWrapper{color:var(--text-secondary)}spectric-input .inputWrapper input{box-sizing:border-box;margin:0;vertical-align:baseline;font-size:.875rem;font-weight:400;line-height:1.28572;letter-spacing:.16px;outline:transparent solid 2px;outline-offset:-2px;border:none;padding:0 1rem;background-color:var(--input-color);color:var(--text-primary, #161616);font-family:inherit;inline-size:100%;block-size:2.5rem}spectric-input .inputWrapper .inputContainer:active:after,spectric-input .inputContainer:focus-within:after{border-bottom-color:var(--input-bottom-focused);width:calc(100% - 5px);transition:width .4s ease-in-out}spectric-input .inputWrapper input:read-only{background-color:transparent;border-bottom-color:var(--border-disabled)}spectric-input .inputContainer{position:relative;border-radius:var(--border-radius);overflow:hidden}spectric-input .inputContainer:after{content:"";width:0px;transition:background-color .4s cubic-bezier(.2,0,.38,.9),border-bottom-color .4s cubic-bezier(.2,0,.38,.9);border-bottom-color:var(--input-bottom);border-bottom-style:solid;border-bottom-width:1px;position:absolute;left:2.5px;bottom:0}spectric-input #helper-text{height:18px}spectric-input[variant=password] spectric-button{position:absolute;right:4px;bottom:3px}spectric-input .checkbox{display:flex;justify-self:center}spectric-query{font-family:monospace}spectric-query .autocomplete{color:var(--spectric-text-primary, #161616);border-radius:0em 0em var(--spectric-border-radius, .4em) var(--spectric-border-radius, .4em);background-color:var(--spectric-background, #ffffff);border:1px solid var(--spectric-background-hover, rgba(141, 141, 141, .12));max-height:300px;border-top:0px;margin:-18px 0 0;position:fixed;top:anchor(bottom);justify-self:anchor-center;text-align:center}spectric-query .autocomplete .optiontype{float:left;max-width:10px}spectric-query .autocomplete .label{position:absolute;right:0}spectric-query .autocomplete .option.active,spectric-query .autocomplete .option:hover{background-color:var(--spectric-background-hover, rgba(141, 141, 141, .12));border-bottom:1px solid var(--primary, #1ea7fd)}spectric-query .autocomplete .option{border-bottom:1px solid transparent;padding:8px}.query-bar-date-quick-select{display:flex;justify-content:space-evenly}spectric-pagination .spectric-pagination-container{display:flex;justify-content:space-between;align-items:center}spectric-pagination .spectric-pagination-text{flex-grow:1;text-align:center}spectric-table{display:flex;flex-direction:column}spectric-table tr{text-align:center}spectric-table-body tr:hover{background-color:color-mix(in srgb,var(--spectric-primary, #1ea7fd),transparent 70%)}spectric-table-header{display:table-header-group;font-weight:700}spectric-table div[role=table]{display:table}spectric-table-body{display:table-row-group}spectric-table-cell{display:contents;vertical-align:middle}spectric-table-cell td{position:relative}spectric-table td:hover:has(.filterable){border:1px solid var(--spectric-primary, #1ea7fd)}spectric-table td{border:1px solid transparent}spectric-table-cell .table-cell-actions{position:absolute;display:flex;width:100%;flex-direction:row-reverse;visibility:hidden;top:-10px}spectric-table-cell td:hover .table-cell-actions{visibility:unset}.spectric-tooltip-portal{position:fixed;z-index:9999;pointer-events:none;--spectric-tooltip-background: color-mix(in srgb,var(--spectric-background-inverse,#f4f4f4) 100%,var(--spectric-primary,#1ea7fd) 90%) }.spectric-tooltip-portal .tooltip-container{display:flex;justify-content:center;align-items:center}.spectric-tooltip-portal.top .tooltip-container{flex-direction:column-reverse}.spectric-tooltip-portal.bottom .tooltip-container{flex-direction:column}.spectric-tooltip-portal.left .tooltip-container{flex-direction:row-reverse}.spectric-tooltip-portal .tooltip-content{background:var(--spectric-tooltip-background);border-radius:var(--spectric-border-radius,.4em);box-shadow:0 0 .01em .01em color-mix(in srgb,var(--spectric-background-hover,rgba(141, 141, 141, .12)) 90%,var(--spectric-text-on-color,#ffffff) 90%);padding:.2em;color:var(--spectric-text-on-color,#ffffff)}.spectric-tooltip-portal .tooltip-caret{background:var(--spectric-tooltip-background)}.spectric-tooltip-portal.top .tooltip-caret,.spectric-tooltip-portal.bottom .tooltip-caret{inline-size:.75rem;block-size:.374rem}.spectric-tooltip-portal.left .tooltip-caret,.spectric-tooltip-portal.right .tooltip-caret{inline-size:.375rem;block-size:.75rem}.spectric-tooltip-portal.top .tooltip-caret{clip-path:polygon(0 0,50% 100%,100% 0)}.spectric-tooltip-portal.bottom .tooltip-caret{clip-path:polygon(0 100%,50% 0,100% 100%)}.spectric-tooltip-portal.left .tooltip-caret{clip-path:polygon(0 0,100% 50%,0 100%)}.spectric-tooltip-portal.right .tooltip-caret{clip-path:polygon(0 50%,100% 0,100% 100%)}
1
+ spectric-input{--input-color: var(--spectric-input-color, #f4f4f4);--border-radius: var(--spectric-border-radius, .4em);--input-bottom: var(--spectric-input-bottom, var(--spectric-button-primary, #a8a8a8));--input-bottom-focused: var(--primary, #1ea7fd);--text-on-color: var(--spectric-text-on-color, #ffffff);--text-on-color-disabled: var(--spectric-text-on-color-disabled, #8d8d8d);--text-placeholder: rgba(22, 22, 22, .4);--text-primary: var(--spectric-text-primary, #161616);--text-secondary: var(--spectric-text-secondary, #525252)}spectric-input .inputWrapper{color:var(--text-secondary)}spectric-input .inputWrapper input{box-sizing:border-box;margin:0;vertical-align:baseline;font-size:.875rem;font-weight:400;line-height:1.28572;letter-spacing:.16px;outline:transparent solid 2px;outline-offset:-2px;border:none;padding:0 1rem;background-color:var(--input-color);color:var(--text-primary, #161616);font-family:inherit;inline-size:100%;block-size:2.5rem}spectric-input .inputWrapper .inputContainer:active:after,spectric-input .inputContainer:focus-within:after{border-bottom-color:var(--input-bottom-focused);width:calc(100% - 5px);transition:width .4s ease-in-out}spectric-input .inputWrapper input:read-only{background-color:transparent;border-bottom-color:var(--border-disabled)}spectric-input .inputContainer{position:relative;border-radius:var(--border-radius);overflow:hidden}spectric-input .inputContainer:after{content:"";width:0px;transition:background-color .4s cubic-bezier(.2,0,.38,.9),border-bottom-color .4s cubic-bezier(.2,0,.38,.9);border-bottom-color:var(--input-bottom);border-bottom-style:solid;border-bottom-width:1px;position:absolute;left:2.5px;bottom:0}spectric-input #helper-text{height:18px}spectric-input[variant=password] spectric-button{position:absolute;right:4px;bottom:3px}spectric-input .checkbox{display:flex;justify-self:center}spectric-query{font-family:monospace}spectric-query .autocomplete{color:var(--spectric-text-primary, #161616);border-radius:0em 0em var(--spectric-border-radius, .4em) var(--spectric-border-radius, .4em);background-color:var(--spectric-background, #ffffff);border:1px solid var(--spectric-background-hover, rgba(141, 141, 141, .12));max-height:300px;border-top:0px;margin:-18px 0 0;position:fixed;top:anchor(bottom);justify-self:anchor-center;text-align:center}spectric-query .autocomplete .optiontype{float:left;max-width:10px}spectric-query .autocomplete .label{position:absolute;right:0}spectric-query .autocomplete .option.active,spectric-query .autocomplete .option:hover{background-color:var(--spectric-background-hover, rgba(141, 141, 141, .12));border-bottom:1px solid var(--primary, #1ea7fd)}spectric-query .autocomplete .option{border-bottom:1px solid transparent;padding:8px}.query-bar-date-quick-select{display:flex;justify-content:space-evenly}spectric-pagination .spectric-pagination-container{display:flex;justify-content:space-between;align-items:center}spectric-pagination .spectric-pagination-text{flex-grow:1;text-align:center}spectric-table{display:flex;flex-direction:column;overflow:hidden}spectric-table .table-wrapper{overflow:auto;flex-grow:1}spectric-table tr{text-align:center}spectric-table-body tr:nth-child(odd){background-color:color-mix(in srgb,var(--spectric-primary, #1ea7fd),transparent 90%)}spectric-table-body tr:hover{background-color:color-mix(in srgb,var(--spectric-primary, #1ea7fd),transparent 70%)}spectric-table-header{display:table-header-group;font-weight:700;position:sticky;top:0;left:0;z-index:1;background:var(--spectric-background, #ffffff)}spectric-table-header td{vertical-align:middle}spectric-table-header .header-contents{position:relative}spectric-table-header .header-contents .sort-direction{position:absolute;right:0}spectric-table-header .header-contents.sortable{cursor:pointer}spectric-table-header .header-contents.sortable:hover .sort-direction.none:before{content:"⮁"}spectric-table div[role=table]{display:table;min-width:100%}spectric-table-body{display:table-row-group}spectric-table-cell{display:contents;vertical-align:middle}spectric-table-cell td{position:relative}spectric-table td:hover:has(.filterable){border:1px solid var(--spectric-primary, #1ea7fd)}spectric-table td{border:1px solid transparent}spectric-table-cell .table-cell-actions{position:absolute;display:flex;width:100%;flex-direction:row-reverse;visibility:hidden;top:-10px}spectric-table-cell td:hover .table-cell-actions{visibility:unset}spectric-table .table-checkbox-single spectric-button{--button-border-radius: 50%}spectric-input.table-checkbox-single[checked] spectric-button{--text-on-color: transparent;border-radius:50%;position:relative}spectric-input.table-checkbox-single[checked] spectric-button:before{position:absolute;content:" ";height:50%;width:50%;left:25%;top:25%;border-radius:50%;z-index:1;box-shadow:0 0 0 4px var(--input-color)}.spectric-tooltip-portal{position:fixed;z-index:9999;pointer-events:none;--spectric-tooltip-background: color-mix(in srgb,var(--spectric-background-inverse,#f4f4f4) 100%,var(--spectric-primary,#1ea7fd) 90%) }.spectric-tooltip-portal .tooltip-container{display:flex;justify-content:center;align-items:center}.spectric-tooltip-portal.top .tooltip-container{flex-direction:column-reverse}.spectric-tooltip-portal.bottom .tooltip-container{flex-direction:column}.spectric-tooltip-portal.left .tooltip-container{flex-direction:row-reverse}.spectric-tooltip-portal .tooltip-content{background:var(--spectric-tooltip-background);border-radius:var(--spectric-border-radius,.4em);box-shadow:0 0 .01em .01em color-mix(in srgb,var(--spectric-background-hover,rgba(141, 141, 141, .12)) 90%,var(--spectric-text-on-color,#ffffff) 90%);padding:.2em;color:var(--spectric-text-on-color,#ffffff)}.spectric-tooltip-portal .tooltip-caret{background:var(--spectric-tooltip-background)}.spectric-tooltip-portal.top .tooltip-caret,.spectric-tooltip-portal.bottom .tooltip-caret{inline-size:.75rem;block-size:.374rem}.spectric-tooltip-portal.left .tooltip-caret,.spectric-tooltip-portal.right .tooltip-caret{inline-size:.375rem;block-size:.75rem}.spectric-tooltip-portal.top .tooltip-caret{clip-path:polygon(0 0,50% 100%,100% 0)}.spectric-tooltip-portal.bottom .tooltip-caret{clip-path:polygon(0 100%,50% 0,100% 100%)}.spectric-tooltip-portal.left .tooltip-caret{clip-path:polygon(0 0,100% 50%,0 100%)}.spectric-tooltip-portal.right .tooltip-caret{clip-path:polygon(0 50%,100% 0,100% 100%)}
@@ -0,0 +1 @@
1
+ export declare function once(func: Function): (...args: any[]) => any;
@@ -3,7 +3,7 @@ import { AsyncDirective } from 'lit/async-directive.js';
3
3
  /**
4
4
  * Usage:
5
5
  * import { html, render } from 'lit';
6
- * import { spreadProps } from '@open-wc/lit-helpers';
6
+ * import { spreadProps } from '@spectric/ui';
7
7
  *
8
8
  * render(
9
9
  * html`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectric/ui",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.es.js",
6
6
  "module": "./dist/index.es.js",
@@ -303,7 +303,6 @@ export class SpectricInput extends LitElement implements InputProps {
303
303
  <spectric-button @click=${() => {
304
304
  this.checked = !this.checked;
305
305
  this.value = Boolean(this.checked);
306
- console.log(this.checked, this.value)
307
306
  this.dispatchEvent(new Event("change", { bubbles: true }))
308
307
  }} icon size=${this.size || "xxsmall"} variant=${this.checked ? "primary" : "secondary"}>${this.checked ? '✓' : "\u00A0"}</spectric-button>
309
308
  ${this.invalid || this.helperText ? html`<spectric-tooltip text=${this.invalid || this.helperText}></spectric-tooltip>` : null}
@@ -8,12 +8,12 @@ import { ButtonSizesTypes } from '../Button';
8
8
  export type { PaginationProps, PaginationChangeProps, PaginationEvents }
9
9
 
10
10
  interface PaginationChangeProps {
11
- page: number;
12
- pageSize: number;
11
+ page?: number;
12
+ pageSize?: number;
13
13
  }
14
14
 
15
15
  interface PaginationProps extends PaginationChangeProps {
16
- size: ButtonSizesTypes
16
+ size?: ButtonSizesTypes
17
17
  totalItems?: number
18
18
  pageSizeOptions?: number[]
19
19
  }
@@ -93,8 +93,8 @@ export class PaginationElement extends LitElement implements PaginationProps {
93
93
  ${pageText}
94
94
  </div>
95
95
  <div>
96
- <spectric-button size=${this.size} ?disabled=${this.page === 1} @click=${this._handlePageDown} icon><</spectric-button>
97
- <spectric-button size=${this.size} ?disabled=${nextPageDisabled} @click=${this._handlePageUp} icon>></spectric-button>
96
+ <spectric-button size=${this.size} ?disabled=${this.page === 1} @click=${this._handlePageDown} icon>🠈</spectric-button>
97
+ <spectric-button size=${this.size} ?disabled=${nextPageDisabled} @click=${this._handlePageUp} icon>🠊</spectric-button>
98
98
  </div>
99
99
  </div>
100
100
  `: null}
@@ -115,7 +115,7 @@ declare global {
115
115
  namespace JSX {
116
116
  interface IntrinsicElements {
117
117
  /**
118
- * @see {@link DialogElement}
118
+ * @see {@link PaginationElement}
119
119
  */
120
120
  [PaginationElementTag]: ReactElementWithPropsAndEvents<PaginationElement, PaginationProps, PaginationEvents>;
121
121
  }
@@ -124,7 +124,7 @@ declare global {
124
124
  namespace JSX {
125
125
  interface IntrinsicElements {
126
126
  /**
127
- * @see {@link DialogElement}
127
+ * @see {@link PaginationElement}
128
128
  */
129
129
  [PaginationElementTag]: ReactElementWithPropsAndEvents<PaginationElement, PaginationProps, PaginationEvents>
130
130
  }
@@ -133,32 +133,34 @@ export class SpectricQuery extends LitElement implements IQueryProps {
133
133
  protected _input!: SpectricInput;
134
134
  _parseQuery = (e: InputEvent | undefined = undefined) => {
135
135
  let ast;
136
- if (e && e?.currentTarget && (e.currentTarget as HTMLInputElement).value) {
136
+ if (e && e?.currentTarget) {
137
137
  this.value = (e.currentTarget as HTMLInputElement).value
138
138
  }
139
- if (this.value == "") {
140
- return
139
+ if (this.value) {
140
+ try {
141
+ if (e && e.data == "(") {
142
+ //Auto close parentheses or parsing and suggestions fail
143
+ this.value = this.value + " )"
144
+ this._input.setSelectionRange(this.value.length - 2, this.value.length - 2)
145
+ }
146
+ let value = this.value;
147
+ if (this._input.selectionStart !== null) {
148
+ value = value.substring(0, this._input.selectionStart) + "@kuery-cursor@" + value.substring(this._input.selectionStart)
149
+ }
150
+ //FIXME: make auto complete work well.
151
+ let suggestions = kuery.parse(value, { parseCursor: true, cursorSymbol: "@kuery-cursor@", allowLeadingWildcards: false }) as unknown as Suggestion;
152
+ this.autoComplete(suggestions)
153
+ } catch (error: any) {
154
+ // this.completions = []
155
+ // this._input.invalid = true;
156
+ // let [expect, _, arrow] = e.message.split("\n")
157
+ // this._input.invalidText = html`&#160;&#160;${arrow} ${expect}`;
158
+ return
159
+ }
141
160
  }
142
161
  try {
143
- if (e && e.data == "(") {
144
- //Auto close parentheses or parsing and suggestions fail
145
- this.value = this.value + " )"
146
- this._input.setSelectionRange(this.value.length - 2, this.value.length - 2)
147
- }
148
- let value = this.value;
149
- if (this._input.selectionStart !== null) {
150
- value = value.substring(0, this._input.selectionStart) + "@kuery-cursor@" + value.substring(this._input.selectionStart)
151
- }
152
- //FIXME: make auto complete work well.
153
- let suggestions = kuery.parse(value, { parseCursor: true, cursorSymbol: "@kuery-cursor@", allowLeadingWildcards: false }) as unknown as Suggestion;
154
- this.autoComplete(suggestions)
155
-
156
162
  ast = kuery.parse(this.value, { allowLeadingWildcards: false });
157
- } catch (e: any) {
158
- // this.completions = []
159
- // this._input.invalid = true;
160
- // let [expect, _, arrow] = e.message.split("\n")
161
- // this._input.invalidText = html`&#160;&#160;${arrow} ${expect}`;
163
+ } catch (error: any) {
162
164
  return
163
165
  }
164
166
  let output
@@ -288,6 +290,8 @@ export class SpectricQuery extends LitElement implements IQueryProps {
288
290
  } else {
289
291
  this.value = prefix + this.value.substring(completion.end)
290
292
  }
293
+ //Important if the underlying inputs value hasn't been set the setSelectionRange will fail to set a range greater than the inputs current value
294
+ this._input.value = this.value
291
295
  this._input.setSelectionRange(insertIndex, insertIndex)
292
296
  if (completion.onSelect) {
293
297
  let value = await completion.onSelect()
@@ -299,6 +303,7 @@ export class SpectricQuery extends LitElement implements IQueryProps {
299
303
  this.completionIndex = 0;
300
304
  this.completions = []
301
305
  this._parseQuery()
306
+ this._input.focus()
302
307
  }
303
308
  _handleArrows = (e: KeyboardEvent) => {
304
309
  if (e.key === "Escape") {
@@ -43,7 +43,7 @@ export class TableCellElement<T> extends LitElement implements CellProps<T> {
43
43
  _handleFilterOut = () => {
44
44
  let value = undefined;
45
45
  if (this.column.key && typeof this.row === "object") {
46
- value = getValue(this.row as Record<any, any>, this.column.key)
46
+ value = rowGetValue(this.row as Record<any, any>, this.column.key)
47
47
  }
48
48
  this.dispatchEvent(new CustomEvent<FilterEvent<T>>("filter", {
49
49
  composed: true,
@@ -59,7 +59,7 @@ export class TableCellElement<T> extends LitElement implements CellProps<T> {
59
59
  _handleFilterFor = () => {
60
60
  let value = undefined;
61
61
  if (this.column.key && typeof this.row === "object") {
62
- value = getValue(this.row as Record<any, any>, this.column.key)
62
+ value = rowGetValue(this.row as Record<any, any>, this.column.key)
63
63
  }
64
64
  this.dispatchEvent(new CustomEvent<FilterEvent<T>>("filter", {
65
65
  composed: true,
@@ -77,8 +77,7 @@ export class TableCellElement<T> extends LitElement implements CellProps<T> {
77
77
  if (this.column.render) {
78
78
  rendered = this.column.render(this.row, this.table)
79
79
  } else if (this.column.key && typeof this.row === 'object') {
80
-
81
- rendered = getValue(this.row as any, this.column.key)
80
+ rendered = rowGetValue(this.row as any, this.column.key)
82
81
  } else {
83
82
  rendered = html`error`
84
83
  }
@@ -134,11 +133,11 @@ declare global {
134
133
  }
135
134
 
136
135
 
137
- const getValue = (context: Record<string, any>, key: string) => {
136
+ export const rowGetValue = (context: Record<string, any>, key: string) => {
138
137
  let path = key.split(".")
139
138
  let value = context[path[0]]
140
139
  if (path.length > 1) {
141
- value = getValue(value, path.slice(1).join("."))
140
+ value = rowGetValue(value, path.slice(1).join("."))
142
141
  }
143
142
  return value
144
143
  }
@@ -4,7 +4,7 @@ import { customElement, property, } from 'lit/decorators.js';
4
4
  import { HTMLElementTagWithEvents, ReactElementWithPropsAndEvents } from '../types';
5
5
  import "./table.css"
6
6
  export const TableHeaderElementTag = "spectric-table-header"
7
- import { ColumnSettings } from './table';
7
+ import { ColumnSettings, TableSortDirection } from './table';
8
8
 
9
9
 
10
10
  interface HeaderProps<T> {
@@ -22,6 +22,20 @@ export class TableHeaderElement<T> extends LitElement implements HeaderProps<T>
22
22
  protected createRenderRoot(): HTMLElement | DocumentFragment {
23
23
  return this
24
24
  }
25
+ _handleSortChange = (column: ColumnSettings<T>) => {
26
+ column = JSON.parse(JSON.stringify(column))// Clone the column to not mutate the original object
27
+ if (!column.sortable) {
28
+ return
29
+ }
30
+ if (column.sortDirection === TableSortDirection.none || column.sortDirection === undefined) {
31
+ column.sortDirection = TableSortDirection.ascending
32
+ } else if (column.sortDirection === TableSortDirection.ascending) {
33
+ column.sortDirection = TableSortDirection.decending
34
+ } else if (column.sortDirection === TableSortDirection.decending) {
35
+ column.sortDirection = TableSortDirection.none
36
+ }
37
+ this.dispatchEvent(new CustomEvent<ColumnSettings<T>>("sortChange", { detail: column }))
38
+ }
25
39
  protected render(): unknown {
26
40
 
27
41
  return html`
@@ -31,7 +45,12 @@ export class TableHeaderElement<T> extends LitElement implements HeaderProps<T>
31
45
  if (column.filterable) {
32
46
  //classes.push("filterable")
33
47
  }
34
- return html`<td><div class=${classes.join(" ")}>${column.title || column.key}</div></td>`
48
+ if (column.sortable) {
49
+ classes.push("sortable")
50
+ }
51
+ let sortDirection = column.sortDirection === TableSortDirection.ascending ? `🠉` : column.sortDirection == TableSortDirection.decending ? `🠋` : ``
52
+ let sortClass = column.sortDirection || TableSortDirection.none
53
+ return html`<td @click=${() => this._handleSortChange(column)}><div class=${classes.join(" ")}>${column.title || column.key} <span class="sort-direction ${sortClass}">${sortDirection}</span></div></td>`
35
54
  })}
36
55
  </tr>
37
56
  `
@@ -39,7 +58,7 @@ export class TableHeaderElement<T> extends LitElement implements HeaderProps<T>
39
58
  }
40
59
 
41
60
  interface TableHeaderEvents {
42
- 'sort': (event: CustomEvent<ColumnSettings<any>>) => void; //TODO sort events
61
+ 'sortChange': (event: CustomEvent<ColumnSettings<any>>) => void; //TODO sort events
43
62
  }
44
63
 
45
64
  declare global {
@@ -0,0 +1,34 @@
1
+ import { once } from "../../utils/once";
2
+ import { rowGetValue } from "./cell";
3
+ import { ColumnSettings, TableSortDirection } from "./table";
4
+
5
+ /**
6
+ * Creates a chain of sort functions so you can perform multi column sorting.
7
+ */
8
+ export const createSortChain = <T>(sorts: ColumnSettings<T>[]) => {
9
+ return sorts.map(({ key, sortDirection, compareFn }) => {
10
+ return (a: T, b: T) => {
11
+ if (!key) {
12
+ return undefined
13
+ }
14
+ let v1 = rowGetValue(a as Record<string, any>, key)
15
+ let v2 = rowGetValue(b as Record<string, any>, key)
16
+ if (compareFn) {
17
+ return compareFn(a, b)
18
+ }
19
+ if (typeof v1 === "number" || typeof v1 === "bigint" || typeof v1 === "boolean") {
20
+ //@ts-ignore
21
+ let sort: number = v1 - v2 as unknown as number
22
+ if (sortDirection === TableSortDirection.decending) {
23
+ sort = sort * -1
24
+ }
25
+ return sort
26
+ } else if (typeof v1 === "string") {
27
+ return v1.localeCompare(v2) * (sortDirection === TableSortDirection.decending ? -1 : 1)
28
+ }
29
+ once(() => { console.error(`Unable to sort type ${typeof v1}`) })
30
+ return undefined
31
+
32
+ }
33
+ })
34
+ }
@@ -1,20 +1,53 @@
1
1
  spectric-table{
2
2
  display: flex;
3
3
  flex-direction: column;
4
+ overflow: hidden;
5
+ }
6
+ spectric-table .table-wrapper{
7
+ overflow: auto;
8
+ flex-grow: 1;
4
9
  }
5
10
  spectric-table tr{
6
11
  text-align: center;
7
12
  }
8
13
 
14
+ spectric-table-body tr:nth-child(odd){
15
+ background-color: color-mix(in srgb, var(--spectric-primary, #1ea7fd), transparent 90%);
16
+ }
9
17
  spectric-table-body tr:hover{
10
18
  background-color: color-mix(in srgb, var(--spectric-primary, #1ea7fd), transparent 70%)
11
19
  }
12
20
  spectric-table-header{
13
21
  display: table-header-group;
14
22
  font-weight: bold;
23
+ position: sticky;
24
+ top: 0px;
25
+ left: 0px;
26
+ z-index: 1;
27
+ background: var(--spectric-background, #ffffff);
28
+ }
29
+ spectric-table-header td {
30
+ vertical-align: middle;
31
+ }
32
+
33
+ spectric-table-header .header-contents {
34
+ position: relative;
35
+ }
36
+
37
+ spectric-table-header .header-contents .sort-direction {
38
+ position: absolute;
39
+ right: 0;
40
+ }
41
+ spectric-table-header .header-contents.sortable {
42
+ cursor: pointer;
43
+ }
44
+ spectric-table-header .header-contents.sortable:hover .sort-direction.none::before
45
+ {
46
+ content: "\2B81";
15
47
  }
16
48
  spectric-table div[role="table"]{
17
49
  display: table;
50
+ min-width: 100%;
18
51
  }
19
52
  spectric-table-body {
20
53
  display: table-row-group;
@@ -43,4 +76,23 @@ spectric-table-cell .table-cell-actions{
43
76
  }
44
77
  spectric-table-cell td:hover .table-cell-actions{
45
78
  visibility: unset;
79
+ }
80
+ spectric-table .table-checkbox-single spectric-button{
81
+ --button-border-radius: 50%;
82
+ }
83
+ spectric-input.table-checkbox-single[checked] spectric-button{
84
+ --text-on-color: transparent;
85
+ border-radius: 50%;
86
+ position: relative;
87
+ }
88
+ spectric-input.table-checkbox-single[checked] spectric-button::before {
89
+ position: absolute;
90
+ content: " ";
91
+ height: 50%;
92
+ width: 50%;
93
+ left: 25%;
94
+ top: 25%;
95
+ border-radius: 50%;
96
+ z-index: 1;
97
+ box-shadow: 0px 0px 0px 4px var(--input-color);
46
98
  }
@@ -9,26 +9,57 @@ export const TableElementTag = "spectric-table"
9
9
  import { spreadProps } from '../../utils/spread';
10
10
  import { PaginationChangeProps, PaginationProps } from '../pagination';
11
11
  import { FilterEvent } from './cell';
12
+ import { createSortChain } from './sorting';
12
13
  export type { TableProps, TableEvents }
13
14
 
14
15
  export type DomRenderable = HTMLElement | TemplateResult | string | number | null
16
+ export enum TableSelectOptions {
17
+ multi = "multi",
18
+ single = "single",
19
+ none = "none"
20
+ }
15
21
 
22
+ export enum TableSortOption {
23
+ multi = "multi",
24
+ single = "single",
25
+ }
26
+ export type TableSortOptionTypes = `${TableSortOption}`
27
+ export enum TableSortDirection {
28
+ ascending = "ascending",
29
+ decending = "decending",
30
+ none = "none"
31
+ }
32
+ export type TableSortDirectionTypes = `${TableSortDirection}`
16
33
  export type ColumnSettings<T> = {
17
34
  width?: number
18
35
  whiteSpace?: "nowrap";
19
36
  hidden?: boolean
20
37
  sortable?: boolean
38
+ sortDirection?: TableSortDirectionTypes
21
39
  filterable?: boolean
22
40
  title?: DomRenderable
41
+ /**
42
+ * Key to used for getting data from an object for a cell
43
+ */
23
44
  key?: string
45
+ /**
46
+ * Render function to render a table cell for displaying custom html
47
+ */
24
48
  render?: (row: T, table: TableElement<T>) => DomRenderable
49
+ /**
50
+ * Custom comparator function for sorting
51
+ */
52
+ compareFn?: ((a: T, b: T) => number) | undefined
25
53
  }
26
- type TableSelectOptions = "multi" | "single"
27
- interface TableProps<T> {
54
+ export type TableSelectOptionsTypes = `${TableSelectOptions}`
55
+ export interface TableDataOptions<T> {
28
56
  pagination?: PaginationProps
29
57
  columns: ColumnSettings<T>[]
58
+ }
59
+ interface TableProps<T> extends TableDataOptions<T> {
30
60
  data: T[]
31
- select?: TableSelectOptions
61
+ select: TableSelectOptionsTypes
62
+ sort?: TableSortOptionTypes
32
63
  }
33
64
 
34
65
  type DomEvent<T> = Event & {
@@ -37,30 +68,84 @@ type DomEvent<T> = Event & {
37
68
  /**
38
69
  * React example
39
70
  * <iframe width="100%" height="400px" src="https://stackblitz.com/edit/react-ts-2ue7azag?ctl=1&embed=1&file=App.tsx&hideExplorer=1&hideNavigation=1"/>
40
- *
71
+ *
41
72
  */
42
73
  @customElement(TableElementTag)
43
74
  export class TableElement<T> extends LitElement implements TableProps<T> {
44
75
  @property({ type: Array, attribute: false })
45
76
  data: T[] = [];
46
77
  @property({ type: Object, attribute: false })
47
- pagination?: PaginationProps | undefined;
78
+ pagination?: PaginationProps;
48
79
  @property({ attribute: false })
49
80
  columns: ColumnSettings<T>[] = [];
50
- @property({ type: String, reflect: false })
51
- select?: TableSelectOptions;
81
+ @property({ type: String, reflect: true })
82
+ select: TableSelectOptionsTypes = TableSelectOptions.none;
83
+ @property({ type: String, reflect: true })
84
+ sort: TableSortOptionTypes = TableSortOption.single;
85
+
86
+ static getDefaultDataSorterAndPaginatior<T>(data: T[]) {
87
+ return (props: TableDataOptions<T>) => {
88
+ let sorts = props.columns.filter(column => column.sortable && column.sortDirection && column.sortDirection !== TableSortDirection.none)
89
+ let rows = [...data] // Need to copy the array to prevent mutating the ordering of the original data
90
+ if (sorts.length) {
91
+ let sortChain = createSortChain(sorts)
92
+ rows.sort((a, b) => {
93
+ for (let sort of sortChain) {
94
+ let result = sort(a, b)
95
+ if (result) {
96
+ return result
97
+ }
98
+ }
99
+ return 0
100
+ })
101
+ }
102
+
103
+ if (!props.pagination) {
104
+ return rows
105
+ }
106
+ let { page, pageSize } = props.pagination
107
+ if (!page || !pageSize) {
108
+ return rows
109
+ }
110
+ return !props.pagination ? rows : rows.slice((page - 1) * pageSize, (page) * pageSize)
111
+ }
112
+ }
113
+
52
114
  private _handlePaginationChange = (e: CustomEvent<PaginationChangeProps>) => {
53
115
  e.preventDefault()
54
116
  e.stopPropagation()
55
117
  if (this.pagination) {
56
- this.pagination.size
57
- this.pagination = { ...this.pagination, ...e.detail }
118
+ let pagination = { ...this.pagination, ...e.detail }
119
+
120
+ let { totalItems, page, pageSize } = pagination
121
+ if (totalItems && page && pageSize && ((page - 1) * pageSize) > totalItems) {
122
+ pagination.page = 1
123
+ }
124
+ this.pagination = pagination
58
125
  }
59
126
  this._emitChange()
60
127
  };
128
+ private _handleSortChange = (e: CustomEvent<ColumnSettings<T>>) => {
129
+ let columnSetting = e.detail
130
+ let column = this.columns.find(col => col.key == columnSetting.key)
131
+ if (!column) {
132
+ console.warn("Unable to find sort column")
133
+ return
134
+ }
135
+ if (this.sort == TableSortOption.single) {
136
+ //Single column sort so we reset the sort direction for all columns
137
+ this.columns.forEach(col => {
138
+ col.sortDirection = TableSortDirection.none
139
+ })
140
+ }
141
+ column.sortDirection = columnSetting.sortDirection;
142
+ this.columns = [...this.columns]
143
+ this._emitChange()
144
+ }
145
+
61
146
  private _emitChange = () => {
62
- let { pagination } = this
63
- this.dispatchEvent(new CustomEvent<{ pagination?: PaginationChangeProps }>("change", { detail: { pagination } }))
147
+ let { pagination, columns } = this
148
+ this.dispatchEvent(new CustomEvent<TableDataOptions<T>>("change", { detail: { pagination, columns } }))
64
149
  }
65
150
  //@ts-ignore
66
151
  private __DO_NOT_USE_filter = () => {
@@ -68,7 +153,7 @@ export class TableElement<T> extends LitElement implements TableProps<T> {
68
153
  this.dispatchEvent(new CustomEvent<FilterEvent<T>>("filter"))
69
154
  }
70
155
  @state()
71
- selected: T[] = [];
156
+ private selected: T[] = [];
72
157
  protected createRenderRoot(): HTMLElement | DocumentFragment {
73
158
  return this
74
159
  }
@@ -79,27 +164,27 @@ export class TableElement<T> extends LitElement implements TableProps<T> {
79
164
  } else {
80
165
  this.selected = []
81
166
  }
82
- this.dispatchEvent(new CustomEvent("select", { detail: this.selected }))
167
+ this.dispatchEvent(new CustomEvent("selected", { detail: this.selected }))
83
168
  }
84
169
  protected render(): unknown {
85
170
  let columns = this.columns.filter(column => !column.hidden)
86
- if (this.select) {
171
+ if (this.select !== TableSelectOptions.none) {
87
172
  columns.unshift({
88
173
  title: this.select === "multi" ? html`<spectric-input variant="checkbox" @change=${this._handleSelectAllChange} .helperText=${"Select All"}></spectric-input>` : null,
89
174
  render: (row) => {
90
- return html`<spectric-input variant="checkbox" .checked=${this.selected.includes(row)} @change=${(e: DomEvent<HTMLInputElement>) => {
175
+ return html`<spectric-input variant="checkbox" class="table-checkbox-${this.select}" .checked=${this.selected.includes(row)} @change=${(e: DomEvent<HTMLInputElement>) => {
91
176
  e.stopPropagation()
92
177
  if (this.select === "single") {
93
178
  this.selected = []
94
179
  }
95
180
  if (e.target.checked) {
96
181
  this.selected.push(row)
97
- this.dispatchEvent(new CustomEvent("select", { detail: this.selected }))
182
+ this.dispatchEvent(new CustomEvent("selected", { detail: this.selected }))
98
183
  } else {
99
184
  let index = this.selected.findIndex(value => value === row)
100
185
  if (index !== -1) {
101
186
  this.selected.splice(index, 1)
102
- this.dispatchEvent(new CustomEvent("select", { detail: this.selected }))
187
+ this.dispatchEvent(new CustomEvent("selected", { detail: this.selected }))
103
188
  }
104
189
  }
105
190
  }}></spectric-input>`
@@ -108,9 +193,11 @@ export class TableElement<T> extends LitElement implements TableProps<T> {
108
193
  }
109
194
 
110
195
  return html`
111
- <div role="table">
112
- <spectric-table-header .columns=${columns}></spectric-table-header>
113
- <spectric-table-body .columns=${columns} .data=${this.data} .table=${this}></spectric-table-body>
196
+ <div class="table-wrapper">
197
+ <div role="table">
198
+ <spectric-table-header .columns=${columns} @sortChange=${this._handleSortChange}></spectric-table-header>
199
+ <spectric-table-body .columns=${columns} .data=${this.data} .table=${this}></spectric-table-body>
200
+ </div>
114
201
  </div>
115
202
  ${this.pagination ? html`<spectric-pagination ${spreadProps(this.pagination)} @change=${this._handlePaginationChange}></spectric-pagination>` : null}
116
203
  `;
@@ -130,7 +217,7 @@ declare global {
130
217
  namespace JSX {
131
218
  interface IntrinsicElements {
132
219
  /**
133
- * @see {@link DialogElement}
220
+ * @see {@link TableElement}
134
221
  */
135
222
  [TableElementTag]: ReactElementWithPropsAndEvents<TableElement<any>, TableProps<any>, TableEvents>;
136
223
  }
@@ -139,7 +226,7 @@ declare global {
139
226
  namespace JSX {
140
227
  interface IntrinsicElements {
141
228
  /**
142
- * @see {@link DialogElement}
229
+ * @see {@link TableElement}
143
230
  */
144
231
  [TableElementTag]: ReactElementWithPropsAndEvents<TableElement<any>, TableProps<any>, TableEvents>
145
232
  }
@@ -210,7 +210,7 @@ declare global {
210
210
  namespace JSX {
211
211
  interface IntrinsicElements {
212
212
  /**
213
- * @see {@link DialogElement}
213
+ * @see {@link TooltipElement}
214
214
  */
215
215
  [TooltipElementTag]: ReactElementWithPropsAndEvents<TooltipElement, TooltipProps, TooltipEvents>;
216
216
  }
@@ -219,7 +219,7 @@ declare global {
219
219
  namespace JSX {
220
220
  interface IntrinsicElements {
221
221
  /**
222
- * @see {@link DialogElement}
222
+ * @see {@link TooltipElement}
223
223
  */
224
224
  [TooltipElementTag]: ReactElementWithPropsAndEvents<TooltipElement, TooltipProps, TooltipEvents>
225
225
  }