@spectric/ui 0.0.4
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/.gitlab-ci.yml +28 -0
- package/.nvmrc +1 -0
- package/.storybook/analyze.sh +4 -0
- package/.storybook/main.ts +55 -0
- package/.storybook/preview.ts +42 -0
- package/.vscode/extensions.json +5 -0
- package/.vscode/settings.json +41 -0
- package/README.MD +50 -0
- package/html-include.png +0 -0
- package/package.json +33 -0
- package/src/classes/BitArray.ts +48 -0
- package/src/classes/DisposibleElement.ts +108 -0
- package/src/components/Banner.ts +102 -0
- package/src/components/Bitdisplay.ts +383 -0
- package/src/components/Button.ts +121 -0
- package/src/components/Header.ts +125 -0
- package/src/components/Page.ts +157 -0
- package/src/components/Panel.ts +56 -0
- package/src/components/ThemeProvider.ts +251 -0
- package/src/components/button.css.ts +160 -0
- package/src/components/configurations/classifications.ts +194 -0
- package/src/components/dialog/dialog.css.ts +50 -0
- package/src/components/dialog/dialog.ts +163 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/header.css.ts +38 -0
- package/src/components/index.ts +10 -0
- package/src/components/input.css +75 -0
- package/src/components/input.ts +312 -0
- package/src/components/page.css.ts +158 -0
- package/src/components/panel.css.ts +44 -0
- package/src/components/query_bar/QueryBar.css +48 -0
- package/src/components/query_bar/QueryBar.ts +378 -0
- package/src/components/query_bar/index.ts +2 -0
- package/src/components/query_bar/querylanguage/kuery/ast/_generated_/kuery.js +3186 -0
- package/src/components/query_bar/querylanguage/kuery/ast/ast.ts +113 -0
- package/src/components/query_bar/querylanguage/kuery/ast/index.ts +31 -0
- package/src/components/query_bar/querylanguage/kuery/ast/kuery.peg +417 -0
- package/src/components/query_bar/querylanguage/kuery/functions/and.ts +55 -0
- package/src/components/query_bar/querylanguage/kuery/functions/exists.ts +62 -0
- package/src/components/query_bar/querylanguage/kuery/functions/index.ts +47 -0
- package/src/components/query_bar/querylanguage/kuery/functions/is.ts +211 -0
- package/src/components/query_bar/querylanguage/kuery/functions/nested.ts +63 -0
- package/src/components/query_bar/querylanguage/kuery/functions/not.ts +53 -0
- package/src/components/query_bar/querylanguage/kuery/functions/or.ts +56 -0
- package/src/components/query_bar/querylanguage/kuery/functions/range.ts +163 -0
- package/src/components/query_bar/querylanguage/kuery/functions/utils/get_fields.ts +49 -0
- package/src/components/query_bar/querylanguage/kuery/functions/utils/get_full_field_name_node.ts +87 -0
- package/src/components/query_bar/querylanguage/kuery/index.ts +38 -0
- package/src/components/query_bar/querylanguage/kuery/kuery_syntax_error.ts +76 -0
- package/src/components/query_bar/querylanguage/kuery/node_types/function.ts +75 -0
- package/src/components/query_bar/querylanguage/kuery/node_types/index.ts +46 -0
- package/src/components/query_bar/querylanguage/kuery/node_types/literal.ts +42 -0
- package/src/components/query_bar/querylanguage/kuery/node_types/named_arg.ts +47 -0
- package/src/components/query_bar/querylanguage/kuery/node_types/types.ts +108 -0
- package/src/components/query_bar/querylanguage/kuery/node_types/wildcard.ts +80 -0
- package/src/components/query_bar/querylanguage/kuery/types.ts +52 -0
- package/src/components/query_bar/querylanguage/outputTypes/toCQL.ts +122 -0
- package/src/components/query_bar/querylanguage/outputTypes/toMongo.ts +103 -0
- package/src/components/query_bar/querylanguage/utils.ts +35 -0
- package/src/components/query_bar/types.ts +59 -0
- package/src/components/splitview/index.ts +1 -0
- package/src/components/splitview/splitview.css.ts +66 -0
- package/src/components/splitview/splitview.ts +183 -0
- package/src/components/types.ts +35 -0
- package/src/index.ts +1 -0
- package/src/stories/Banner.stories.ts +46 -0
- package/src/stories/BitDisplay.stories.ts +68 -0
- package/src/stories/Button.stories.ts +138 -0
- package/src/stories/Header.stories.ts +55 -0
- package/src/stories/Page.stories.ts +108 -0
- package/src/stories/QueryBar.stories.ts +63 -0
- package/src/stories/Splitview.stories.ts +52 -0
- package/src/stories/fixtures/Bits.ts +15 -0
- package/src/stories/fixtures/ExampleContent.ts +102 -0
- package/src/stories/fixtures/data.ts +30 -0
- package/src/stories/fixtures/lorumipsum.ts +19 -0
- package/src/stories/input.stories.ts +77 -0
- package/src/stories/tsconfig.json +35 -0
- package/src/utils/debounce.ts +18 -0
- package/src/utils/spread.ts +71 -0
- package/src/vite-env.d.ts +1 -0
- package/test/__init__.py +9 -0
- package/test/elastic.py +9 -0
- package/test/interface.py +16 -0
- package/tsconfig.json +29 -0
- package/vite.config.js +34 -0
- package/vue-example.png +0 -0
- package/vue-include.png +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
|
|
2
|
+
import { FieldTypes, KueryNode } from '../..';
|
|
3
|
+
import { wildcardSymbol } from '../kuery/node_types/wildcard';
|
|
4
|
+
|
|
5
|
+
export const KQL_WILDCARD_SYMBOL = wildcardSymbol;
|
|
6
|
+
export const KQL_NODE_TYPE_WILDCARD = 'wildcard';
|
|
7
|
+
export type FunctionName = 'is' | 'and' | 'or' | 'not' | 'range' | 'exists' | 'nested';
|
|
8
|
+
const and = (node: KueryNode, fields?: FieldTypes[]) => {
|
|
9
|
+
const children = node.arguments || [];
|
|
10
|
+
return (
|
|
11
|
+
'(' +
|
|
12
|
+
children
|
|
13
|
+
.map((child: KueryNode) => {
|
|
14
|
+
return toCql(child, fields);
|
|
15
|
+
})
|
|
16
|
+
.join(' AND ') +
|
|
17
|
+
')'
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
const is = (node: KueryNode, fields?: FieldTypes[]) => {
|
|
21
|
+
var {
|
|
22
|
+
arguments: [fieldNameArg, valueArg, isValue],
|
|
23
|
+
} = node;
|
|
24
|
+
let operator = '=';
|
|
25
|
+
if (valueArg.type === 'wildcard') {
|
|
26
|
+
operator = '';
|
|
27
|
+
}
|
|
28
|
+
let value = toCql(valueArg);
|
|
29
|
+
isValue = isValue.value || typeof value === "string";
|
|
30
|
+
if (valueArg.type === 'literal' && isValue) {
|
|
31
|
+
value = `'${value}'`; //should be quoted
|
|
32
|
+
}
|
|
33
|
+
if (valueArg.type === 'literal' && !isValue) {
|
|
34
|
+
value = `${value}`; //shouldn't be quoted
|
|
35
|
+
}
|
|
36
|
+
let fieldName = toCql(fieldNameArg);
|
|
37
|
+
if (fieldName == null) {
|
|
38
|
+
//this isn't possible if we don't have the names of all the fields.
|
|
39
|
+
//if we have all the fields we can do a list of or statements (field ILIKE "%value%")
|
|
40
|
+
if (fields && isValue) {
|
|
41
|
+
return `(${fields.filter(f => f.type === "string").map(field => `${field.name} ILIKE '%${valueArg.value}%'`).join(" OR ")})`
|
|
42
|
+
}
|
|
43
|
+
if (fields && !isValue && (valueArg.value === true || valueArg.value === false)) {
|
|
44
|
+
return `(${fields.filter(f => f.type === "boolean").map(field => `${field.name}=${valueArg.value}`).join(" OR ")})`
|
|
45
|
+
}
|
|
46
|
+
return ""
|
|
47
|
+
}
|
|
48
|
+
return fieldName + operator + value;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const or = (node: KueryNode, fields?: FieldTypes[]) => {
|
|
52
|
+
const children = node.arguments || [];
|
|
53
|
+
return (
|
|
54
|
+
'(' +
|
|
55
|
+
children
|
|
56
|
+
.map((child: KueryNode) => {
|
|
57
|
+
return toCql(child, fields);
|
|
58
|
+
})
|
|
59
|
+
.join(' OR ') +
|
|
60
|
+
')'
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
const not = (node: KueryNode, fields?: FieldTypes[]) => {
|
|
64
|
+
const [argument] = node.arguments;
|
|
65
|
+
return 'NOT (' + toCql(argument, fields) + ')';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const AST_TO_CQL = {
|
|
69
|
+
gt: '>',
|
|
70
|
+
lt: '<',
|
|
71
|
+
gte: '>=',
|
|
72
|
+
lte: '<=',
|
|
73
|
+
};
|
|
74
|
+
const range = (node: KueryNode,) => {
|
|
75
|
+
|
|
76
|
+
const [fieldNameArg, operator] = node.arguments;
|
|
77
|
+
let valueArg = operator.value
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
const opsign = AST_TO_CQL[operator.name];
|
|
80
|
+
let value = toCql(valueArg);
|
|
81
|
+
if (valueArg.type === 'literal') {
|
|
82
|
+
value = `${value}`;
|
|
83
|
+
}
|
|
84
|
+
return `${fieldNameArg.value} ${opsign} ${value}`;
|
|
85
|
+
};
|
|
86
|
+
const exists = (node: KueryNode) => {
|
|
87
|
+
const [fieldNameArg] = node.arguments;
|
|
88
|
+
return `${fieldNameArg.value} IS NOT NULL`;
|
|
89
|
+
};
|
|
90
|
+
const nested = (node: KueryNode) => {
|
|
91
|
+
//nested types don't exist in CQL
|
|
92
|
+
console.warn("Nested types dont exist in CQL", node)
|
|
93
|
+
return ""
|
|
94
|
+
}
|
|
95
|
+
export const functions = {
|
|
96
|
+
is,
|
|
97
|
+
and,
|
|
98
|
+
or,
|
|
99
|
+
not,
|
|
100
|
+
range,
|
|
101
|
+
exists,
|
|
102
|
+
nested
|
|
103
|
+
};
|
|
104
|
+
const nodeTypes = {
|
|
105
|
+
function: (node: KueryNode, fields?: FieldTypes[]) => {
|
|
106
|
+
// @ts-ignore
|
|
107
|
+
return functions[node.function as FunctionName](node, fields);
|
|
108
|
+
},
|
|
109
|
+
literal: (node: KueryNode) => {
|
|
110
|
+
return node.value;
|
|
111
|
+
},
|
|
112
|
+
wildcard: (node: KueryNode) => {
|
|
113
|
+
const { value } = node;
|
|
114
|
+
return ` LIKE '${value.split(KQL_WILDCARD_SYMBOL).join('%')}'`;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const toCql = (node: KueryNode, fields?: FieldTypes[]): string => {
|
|
119
|
+
//@ts-ignore
|
|
120
|
+
const nodeType = nodeTypes[node.type] as unknown as any;
|
|
121
|
+
return nodeType(node, fields);
|
|
122
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* eslint-disable @kbn/eslint/require-license-header */
|
|
2
|
+
import { FieldTypes, KueryNode } from '../..';
|
|
3
|
+
import { wildcardSymbol } from '../kuery/node_types/wildcard';
|
|
4
|
+
|
|
5
|
+
export const KQL_WILDCARD_SYMBOL = wildcardSymbol;
|
|
6
|
+
export const KQL_NODE_TYPE_WILDCARD = 'wildcard';
|
|
7
|
+
export type FunctionName = 'is' | 'and' | 'or' | 'not' | 'range' | 'exists' | 'nested';
|
|
8
|
+
const and = (node: KueryNode) => {
|
|
9
|
+
const children = node.arguments || [];
|
|
10
|
+
let query: any = { "$and": children.map((c: KueryNode) => toMongo(c)) }
|
|
11
|
+
|
|
12
|
+
return query;
|
|
13
|
+
};
|
|
14
|
+
const is = (node: KueryNode) => {
|
|
15
|
+
const {
|
|
16
|
+
arguments: [fieldNameArg, valueArg],
|
|
17
|
+
} = node;
|
|
18
|
+
|
|
19
|
+
let value = toMongo(valueArg);
|
|
20
|
+
const isExistsQuery = valueArg.type === 'wildcard' && (valueArg.value as any) === '@kuery-wildcard@';
|
|
21
|
+
if (isExistsQuery) {
|
|
22
|
+
return exists(node)
|
|
23
|
+
}
|
|
24
|
+
let query: any = {}
|
|
25
|
+
query[toMongo(fieldNameArg)] = { "$eq": value }
|
|
26
|
+
return query;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const or = (node: KueryNode) => {
|
|
30
|
+
const children = node.arguments || [];
|
|
31
|
+
return {
|
|
32
|
+
"$or": children
|
|
33
|
+
.map((child: KueryNode) => {
|
|
34
|
+
return toMongo(child);
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
};
|
|
39
|
+
const not = (node: KueryNode) => {
|
|
40
|
+
const [fieldNameArg] = node.arguments;
|
|
41
|
+
let query: any = {};
|
|
42
|
+
query = { "$ne": toMongo(fieldNameArg) }
|
|
43
|
+
return query;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const AST_TO_CQL = {
|
|
47
|
+
gt: '$gt',
|
|
48
|
+
lt: '$lt',
|
|
49
|
+
gte: '$gte',
|
|
50
|
+
lte: '$lte',
|
|
51
|
+
};
|
|
52
|
+
const range = (node: KueryNode) => {
|
|
53
|
+
const [fieldNameArg, operator] = node.arguments;
|
|
54
|
+
let valueArg = operator.value
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
const opsign = AST_TO_CQL[operator.name];
|
|
57
|
+
let value = toMongo(valueArg);
|
|
58
|
+
|
|
59
|
+
let query: any = {}
|
|
60
|
+
query[fieldNameArg.value] = {}
|
|
61
|
+
query[fieldNameArg.value][opsign] = value
|
|
62
|
+
return query
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
};
|
|
66
|
+
const exists = (node: KueryNode) => {
|
|
67
|
+
const [fieldNameArg] = node.arguments;
|
|
68
|
+
return { [toMongo(fieldNameArg)]: { $ne: null } }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const nested = (node: KueryNode) => {
|
|
72
|
+
|
|
73
|
+
console.warn("TODO Implement nested search", node)
|
|
74
|
+
return ""
|
|
75
|
+
}
|
|
76
|
+
export const functions = {
|
|
77
|
+
is,
|
|
78
|
+
and,
|
|
79
|
+
or,
|
|
80
|
+
not,
|
|
81
|
+
range,
|
|
82
|
+
exists,
|
|
83
|
+
nested
|
|
84
|
+
};
|
|
85
|
+
const nodeTypes = {
|
|
86
|
+
function: (node: KueryNode) => {
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
return functions[node.function as FunctionName](node);
|
|
89
|
+
},
|
|
90
|
+
literal: (node: KueryNode) => {
|
|
91
|
+
return node.value;
|
|
92
|
+
},
|
|
93
|
+
wildcard: (node: KueryNode) => {
|
|
94
|
+
const { value } = node;
|
|
95
|
+
return `/${value.split(KQL_WILDCARD_SYMBOL).join('.*')}/`;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const toMongo = (node: KueryNode, fields?: FieldTypes[]): string => {
|
|
100
|
+
//@ts-ignore
|
|
101
|
+
const nodeType = nodeTypes[node.type] as unknown as any;
|
|
102
|
+
return nodeType(node, fields);
|
|
103
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
*
|
|
4
|
+
* The OpenSearch Contributors require contributions made to
|
|
5
|
+
* this file be licensed under the Apache-2.0 license or a
|
|
6
|
+
* compatible open source license.
|
|
7
|
+
*
|
|
8
|
+
* Any modifications Copyright OpenSearch Contributors. See
|
|
9
|
+
* GitHub history for details.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
14
|
+
* license agreements. See the NOTICE file distributed with
|
|
15
|
+
* this work for additional information regarding copyright
|
|
16
|
+
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
17
|
+
* the Apache License, Version 2.0 (the "License"); you may
|
|
18
|
+
* not use this file except in compliance with the License.
|
|
19
|
+
* You may obtain a copy of the License at
|
|
20
|
+
*
|
|
21
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
22
|
+
*
|
|
23
|
+
* Unless required by applicable law or agreed to in writing,
|
|
24
|
+
* software distributed under the License is distributed on an
|
|
25
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
26
|
+
* KIND, either express or implied. See the License for the
|
|
27
|
+
* specific language governing permissions and limitations
|
|
28
|
+
* under the License.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export function getTimeZoneFromSettings(dateFormatTZ: string) {
|
|
33
|
+
|
|
34
|
+
return dateFormatTZ;//always use zulu
|
|
35
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NodeTypes } from './querylanguage/kuery/node_types';
|
|
2
|
+
|
|
3
|
+
export interface KueryNode {
|
|
4
|
+
type: keyof NodeTypes;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type DslQuery = any;
|
|
9
|
+
|
|
10
|
+
export interface KueryParseOptions {
|
|
11
|
+
helpers: {
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
};
|
|
14
|
+
startRule: string;
|
|
15
|
+
allowLeadingWildcards: boolean;
|
|
16
|
+
errorOnLuceneSyntax: boolean;
|
|
17
|
+
cursorSymbol?: string;
|
|
18
|
+
parseCursor?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type IIndexPattern = { fields: IFieldType[] }
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
24
|
+
export interface JsonArray extends Array<JsonValue> { }
|
|
25
|
+
export interface JsonObject {
|
|
26
|
+
[key: string]: JsonValue;
|
|
27
|
+
}
|
|
28
|
+
export type JsonValue = null | boolean | number | string | JsonObject | JsonArray;
|
|
29
|
+
export { nodeTypes } from './querylanguage/kuery/node_types';
|
|
30
|
+
|
|
31
|
+
export interface IFieldSubType {
|
|
32
|
+
multi?: { parent: string };
|
|
33
|
+
nested?: { path: string };
|
|
34
|
+
}
|
|
35
|
+
export interface IFieldType {
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
script?: string;
|
|
39
|
+
lang?: string;
|
|
40
|
+
count?: number;
|
|
41
|
+
// esTypes might be undefined on old index patterns that have not been refreshed since we added
|
|
42
|
+
// this prop. It is also undefined on scripted fields.
|
|
43
|
+
esTypes?: string[];
|
|
44
|
+
aggregatable?: boolean;
|
|
45
|
+
filterable?: boolean;
|
|
46
|
+
searchable?: boolean;
|
|
47
|
+
sortable?: boolean;
|
|
48
|
+
visualizable?: boolean;
|
|
49
|
+
readFromDocValues?: boolean;
|
|
50
|
+
scripted?: boolean;
|
|
51
|
+
subType?: IFieldSubType;
|
|
52
|
+
displayName?: string;
|
|
53
|
+
format?: any;
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
export interface LatLon {
|
|
57
|
+
lat: number;
|
|
58
|
+
lon: number;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./splitview"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { css } from "lit";
|
|
2
|
+
|
|
3
|
+
export const style = css`
|
|
4
|
+
:host {
|
|
5
|
+
display: block;
|
|
6
|
+
height: 100%;
|
|
7
|
+
width: 100%;
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
.split-view {
|
|
11
|
+
display: flex;
|
|
12
|
+
height: 100%;
|
|
13
|
+
width: 100%;
|
|
14
|
+
position:relative;
|
|
15
|
+
}
|
|
16
|
+
.split-view.active{
|
|
17
|
+
cursor:ew-resize;
|
|
18
|
+
}
|
|
19
|
+
.split-view.active.vertical{
|
|
20
|
+
cursor:ns-resize;
|
|
21
|
+
}
|
|
22
|
+
.split-view.vertical {
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
}
|
|
25
|
+
.panel {
|
|
26
|
+
flex: 1;
|
|
27
|
+
overflow: auto;
|
|
28
|
+
}
|
|
29
|
+
.splitter {
|
|
30
|
+
border-radius: var(--spectric-border-radius, .4em);
|
|
31
|
+
background-color: var(--spectric-input-color,#ccc);
|
|
32
|
+
user-select: none;
|
|
33
|
+
transition: background-color 100ms linear;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.splitter.invisible {
|
|
37
|
+
background-color: #00000000;
|
|
38
|
+
}
|
|
39
|
+
.splitter:hover {
|
|
40
|
+
background-color: var(--spectric-button-primary, #1ea7fd);
|
|
41
|
+
transition: background-color 500ms linear;
|
|
42
|
+
}
|
|
43
|
+
.splitter.active {
|
|
44
|
+
background-color: var(--spectric-button-primary, #1ea7fd);
|
|
45
|
+
transition: background-color 500ms linear;
|
|
46
|
+
}
|
|
47
|
+
.split-view > .error-display{
|
|
48
|
+
display:none;
|
|
49
|
+
position:absolute;
|
|
50
|
+
top:0;
|
|
51
|
+
right:0
|
|
52
|
+
}
|
|
53
|
+
.split-view.error > .error-display{
|
|
54
|
+
display:inline-block;
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
.split-view.vertical .splitter {
|
|
58
|
+
cursor: ns-resize;
|
|
59
|
+
height: 5px;
|
|
60
|
+
width: 100%;
|
|
61
|
+
}
|
|
62
|
+
.split-view:not(.vertical) .splitter {
|
|
63
|
+
width: 5px;
|
|
64
|
+
cursor: ew-resize;
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { html, PropertyValues, } from 'lit-element';
|
|
2
|
+
import { DisposableElement } from '../../classes/DisposibleElement';
|
|
3
|
+
import { customElement, property, query, queryAsync, state } from 'lit/decorators.js';
|
|
4
|
+
|
|
5
|
+
import { style } from './splitview.css';
|
|
6
|
+
import { HTMLElementTagWithEvents, ReactElementWithPropsAndEvents } from '../types';
|
|
7
|
+
import { debounceAnimation } from '../../utils/debounce';
|
|
8
|
+
|
|
9
|
+
export enum Orientations {
|
|
10
|
+
horizontal = "horizontal",
|
|
11
|
+
vertical = "vertical"
|
|
12
|
+
}
|
|
13
|
+
export interface SplitViewProps {
|
|
14
|
+
/** Controls the orientation of the splitter handle */
|
|
15
|
+
orientation: `${Orientations}`;
|
|
16
|
+
/** the percentage to split the view default: 50*/
|
|
17
|
+
percentage?: number
|
|
18
|
+
/** Should the splitter handle be invisible? */
|
|
19
|
+
invisible?: boolean
|
|
20
|
+
/** Clamps the minimum split percentage default: 10 */
|
|
21
|
+
min: number
|
|
22
|
+
/** Clamps the maximum split percentage default: 90 */
|
|
23
|
+
max: number
|
|
24
|
+
/** save and load split state to localstorage splitter must have an id attribute default: true */
|
|
25
|
+
useSavedState?: boolean
|
|
26
|
+
}
|
|
27
|
+
export const ElementTag = 'spectric-splitview'
|
|
28
|
+
/**
|
|
29
|
+
* Split view will take a container and split it horizontally or vertically. This element can only have two children.
|
|
30
|
+
* If you supply the **id** attribute on the split view element it will save and load the state keeping the user defined position
|
|
31
|
+
* @slot - Element can only take 2 HTMLElements.
|
|
32
|
+
*/
|
|
33
|
+
@customElement(ElementTag)
|
|
34
|
+
export class SplitView extends DisposableElement implements SplitViewProps {
|
|
35
|
+
|
|
36
|
+
@property({ type: String }) orientation = 'horizontal' as Orientations;
|
|
37
|
+
|
|
38
|
+
@property({ type: Number, reflect: true }) percentage: SplitViewProps['percentage'] = 50;
|
|
39
|
+
|
|
40
|
+
@property({ type: Boolean }) invisible: SplitViewProps['invisible'] = false;
|
|
41
|
+
static styles = style
|
|
42
|
+
|
|
43
|
+
@property({ type: Number, reflect: true }) min: SplitViewProps['min'] = 10;
|
|
44
|
+
@property({ type: Number, reflect: true }) max: SplitViewProps['max'] = 90;
|
|
45
|
+
@property({ type: Boolean, reflect: true }) useSavedState: SplitViewProps['useSavedState'] = true;
|
|
46
|
+
|
|
47
|
+
@state()
|
|
48
|
+
private isDragging: boolean = false;
|
|
49
|
+
@state()
|
|
50
|
+
private _error: string | false = false;
|
|
51
|
+
@queryAsync(".splitter")
|
|
52
|
+
private _splitter!: Promise<HTMLElement>
|
|
53
|
+
|
|
54
|
+
@query("slot[name='panel1']")
|
|
55
|
+
private _panel1!: HTMLSlotElement
|
|
56
|
+
@query("slot[name='panel2']")
|
|
57
|
+
private _panel2!: HTMLSlotElement
|
|
58
|
+
constructor() {
|
|
59
|
+
super()
|
|
60
|
+
let id = this.getAttribute("id");
|
|
61
|
+
if (id && this.useSavedState) {
|
|
62
|
+
let savedValue = localStorage.getItem(`splitview-${id}`)
|
|
63
|
+
if (savedValue) {
|
|
64
|
+
this.percentage = parseFloat(savedValue)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.addDisposableListener(this._splitter, "mousedown", () => {
|
|
68
|
+
this.isDragging = true;
|
|
69
|
+
})
|
|
70
|
+
/**
|
|
71
|
+
* On double click set percentage to 50%
|
|
72
|
+
*/
|
|
73
|
+
this.addDisposableListener(this._splitter, "dblclick", () => {
|
|
74
|
+
this.percentage = 50
|
|
75
|
+
this._emitChange()
|
|
76
|
+
})
|
|
77
|
+
this.addDisposableListener(document.body, "mousemove", this._onMouseMove)
|
|
78
|
+
this.addDisposableListener(document.body, 'mouseup', () => {
|
|
79
|
+
this.isDragging = false;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
84
|
+
if (changedProperties.has("percentage")) {
|
|
85
|
+
this.percentage = Math.min(Math.max(Number(this.percentage), this.min), this.max)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
_emitChange = () => {
|
|
89
|
+
let { percentage, orientation, invisible, min, max, useSavedState } = this
|
|
90
|
+
let id = this.getAttribute("id");
|
|
91
|
+
if (id && this.useSavedState) {
|
|
92
|
+
localStorage.setItem(`splitview-${id}`, String(percentage))
|
|
93
|
+
}
|
|
94
|
+
/** CustomEvent\<SplitViewProps\> Fired every time there is a change to the split percentage */
|
|
95
|
+
this.dispatchEvent(new CustomEvent<SplitViewProps>("change", { detail: { orientation, percentage, invisible, min, max, useSavedState } }))
|
|
96
|
+
}
|
|
97
|
+
_onMouseMove = debounceAnimation((e: MouseEvent) => {
|
|
98
|
+
if (this.isDragging) {
|
|
99
|
+
const rect = this.getBoundingClientRect();
|
|
100
|
+
let offset: number;
|
|
101
|
+
let percentage: number;
|
|
102
|
+
if (this.orientation === 'horizontal') {
|
|
103
|
+
offset = e.clientX - rect.left;
|
|
104
|
+
percentage = (offset / rect.width) * 100;
|
|
105
|
+
} else {
|
|
106
|
+
offset = e.clientY - rect.top;
|
|
107
|
+
percentage = (offset / rect.height) * 100;
|
|
108
|
+
}
|
|
109
|
+
//clamp to min/mix of the split
|
|
110
|
+
percentage = Math.min(Math.max(percentage, this.min), this.max)
|
|
111
|
+
this.percentage = percentage
|
|
112
|
+
this._emitChange()
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
private _assignSlot = (e: Event) => {
|
|
116
|
+
if (!e.target) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
let slot = e.target as HTMLSlotElement;
|
|
120
|
+
let nodes = slot.assignedNodes().filter(n => n instanceof HTMLElement);
|
|
121
|
+
if (nodes.length > 2) {
|
|
122
|
+
console.log("cannot assign more than 2 elements to a split view")
|
|
123
|
+
}
|
|
124
|
+
nodes.forEach(element => {
|
|
125
|
+
if (this._panel1.assignedNodes().length === 0) {
|
|
126
|
+
element.slot = "panel1"
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
if (this._panel2.assignedNodes().length === 0) {
|
|
130
|
+
element.slot = "panel2"
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
this._error = "Too many element assigned to split view splitter can only have a maximum of 2 children"
|
|
134
|
+
console.warn("Too many element assigned to split view splitter can only have a maximum of 2 children")
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
render() {
|
|
139
|
+
return html`
|
|
140
|
+
<div class="split-view ${this.orientation} ${this._error ? "error" : ""} ${this.isDragging ? "active" : ""}" style="--split-percentage: ${this.percentage}%;">
|
|
141
|
+
<span class="error-display">${this._error}</span>
|
|
142
|
+
<div class="panel" style="${this.orientation === 'horizontal' ? 'flex: var(--split-percentage, 50%)' : 'max-height: var(--split-percentage, 50%)'}">
|
|
143
|
+
<slot name="panel1"></slot>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="splitter ${this.invisible ? "invisible" : ""} ${this.isDragging ? "active" : ""}"></div>
|
|
146
|
+
<div class="panel" style="${this.orientation === 'horizontal' ? 'flex: calc(100% - var(--split-percentage, 50%))' : 'max-height: calc(100% - var(--split-percentage, 50%))'}">
|
|
147
|
+
<slot name="panel2"></slot>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<slot @slotchange=${this._assignSlot} style="display:none"></slot>
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface SplitViewEvents {
|
|
156
|
+
'change': (event: CustomEvent<SplitViewProps>) => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
declare global {
|
|
161
|
+
interface HTMLElementTagNameMap {
|
|
162
|
+
[ElementTag]: HTMLElementTagWithEvents<SplitView, SplitViewEvents>
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
namespace JSX {
|
|
166
|
+
interface IntrinsicElements {
|
|
167
|
+
/**
|
|
168
|
+
* @see {@link SplitView}
|
|
169
|
+
*/
|
|
170
|
+
[ElementTag]: ReactElementWithPropsAndEvents<SplitView, SplitViewProps, SplitViewEvents>;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
namespace React {
|
|
174
|
+
namespace JSX {
|
|
175
|
+
interface IntrinsicElements {
|
|
176
|
+
/**
|
|
177
|
+
* @see {@link SplitView}
|
|
178
|
+
*/
|
|
179
|
+
[ElementTag]: ReactElementWithPropsAndEvents<SplitView, SplitViewProps, SplitViewEvents>
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type HTMLElementTagWithEvents<ElementClass, EventsMap> = Omit<{
|
|
2
|
+
[P in keyof ElementClass]: ElementClass[P];
|
|
3
|
+
}, "addEventListener" | "removeEventListener"> & WithEvents<EventsMap>;
|
|
4
|
+
|
|
5
|
+
type WithEvents<EventsMap> = {
|
|
6
|
+
addEventListener<K extends keyof Omit<HTMLElementEventMap, keyof EventsMap>>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
7
|
+
addEventListener<K extends keyof EventsMap>(type: K, listener: EventsMap[K], options?: boolean | AddEventListenerOptions): void;
|
|
8
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
9
|
+
removeEventListener<K extends keyof Omit<HTMLElementEventMap, keyof EventsMap>>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
10
|
+
removeEventListener<K extends keyof EventsMap>(type: K, listener: EventsMap[K], options?: boolean | AddEventListenerOptions): void;
|
|
11
|
+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//export type ReactElementWithPropsAndEvents<Class,Props,Events> = React.DetailedHTMLProps<Props & React.DOMAttributes<Class>&ReactEventMap<Events>, Class>
|
|
15
|
+
export type ReactElementWithPropsAndEvents<Class, Props, Events = {}> = React.DetailedHTMLProps<Props & React.DOMAttributes<Class> & OverloadReactSyntheticEvents<Class, Events> & ReactEventMap<Events>, Class>
|
|
16
|
+
|
|
17
|
+
// If our Event name happens to be a built in dom event like click
|
|
18
|
+
// the element.addEventListener("click") will not get the
|
|
19
|
+
// specified event in the event map because
|
|
20
|
+
// react internally creates a synthetic event for particular types
|
|
21
|
+
// so we need to extract the those events and return a react synthetic event for those specific types
|
|
22
|
+
type OverloadReactSyntheticEvents<Class, Events> = Pick<ReactSyntheticEvents<Class, Events>, ExtractOverlappingEvents<Class, Events>>
|
|
23
|
+
type ExtractOverlappingEvents<Class, Events> = Extract<React.DOMAttributes<Class>, keyof ReactSyntheticEvents<Class, Events>>
|
|
24
|
+
|
|
25
|
+
//Map the event names that are handled by react to syntheticEvents
|
|
26
|
+
export type ReactSyntheticEvents<Class, Events> = {
|
|
27
|
+
[Key in keyof Events as `on${Capitalize<string & Key>}`]: (e: React.SyntheticEvent<Class, Events[Key]>) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
//Map native event to the React on<eventtype> property F
|
|
32
|
+
// FIXME we need to Omit the keys that where mapped to synthetic events (the inverse of the OverloadReactSyntheticEvents)
|
|
33
|
+
export type ReactEventMap<Events> = Partial<{
|
|
34
|
+
[Key in keyof Events as `on${string & Key}`]: Events[Key];
|
|
35
|
+
}>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./components"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/web-components';
|
|
3
|
+
|
|
4
|
+
import type { BannerProps, BannerSlots } from '../components/Banner';
|
|
5
|
+
import '../components/Banner';
|
|
6
|
+
import { html } from 'lit';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'UI/Banner',
|
|
10
|
+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
component: "spectric-banner",
|
|
13
|
+
render: (args) => {
|
|
14
|
+
console.log(args)
|
|
15
|
+
return html`<spectric-banner ?dismissable=${args.dismissable} .headerStyle=${args.headerStyle}>${args.text}</spectric-banner>
|
|
16
|
+
`
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
text: "Banner with default style"
|
|
20
|
+
}
|
|
21
|
+
} satisfies Meta<BannerProps & BannerSlots>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<BannerProps & BannerSlots>;
|
|
25
|
+
|
|
26
|
+
export const ClassificationBanner: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
text: "Unclassified",
|
|
29
|
+
headerStyle: {
|
|
30
|
+
backgroundColor: "green",
|
|
31
|
+
color: "white"
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const DismissableBanner: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
text: "Can be dismissed",
|
|
39
|
+
dismissable: true,
|
|
40
|
+
headerStyle: {
|
|
41
|
+
backgroundColor: "cornflowerblue",
|
|
42
|
+
color: "white"
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|