@neovici/cosmoz-autocomplete 5.3.2 → 6.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-autocomplete",
3
- "version": "5.3.2",
3
+ "version": "6.0.0",
4
4
  "description": "A autocomplete input web component",
5
5
  "keywords": [
6
6
  "lit-html",
@@ -21,14 +21,14 @@
21
21
  "test": "test"
22
22
  },
23
23
  "scripts": {
24
- "lint": "eslint --cache --ext .js .",
25
- "lint-tsc": "tsc",
24
+ "lint": "tsc && eslint --cache .",
25
+ "build": "tsc -p tsconfig.build.json",
26
26
  "start": "wds",
27
27
  "test": "wtr --coverage",
28
28
  "test:watch": "wtr --watch",
29
+ "prepare": "husky install",
29
30
  "storybook:build": "build-storybook",
30
- "storybook:deploy": "storybook-to-ghpages",
31
- "prepare": "husky install"
31
+ "storybook:deploy": "storybook-to-ghpages"
32
32
  },
33
33
  "release": {
34
34
  "plugins": [
@@ -46,8 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "files": [
49
- "*.js",
50
- "lib/**/*.js"
49
+ "dist/"
51
50
  ],
52
51
  "commitlint": {
53
52
  "extends": [
@@ -61,14 +60,19 @@
61
60
  ]
62
61
  }
63
62
  },
63
+ "exports": {
64
+ ".": "./dist/index.js",
65
+ "./autocomplete": "./dist/autocomplete/index.js",
66
+ "./listbox": "./dist/listbox/index.js",
67
+ "./item-renderer": "./dist/listbox/item-renderer.js"
68
+ },
64
69
  "dependencies": {
65
70
  "@lit-labs/virtualizer": "^1.0.0",
66
71
  "@neovici/cosmoz-dropdown": "^3.0.0",
67
72
  "@neovici/cosmoz-input": "^3.0.0",
68
73
  "@neovici/cosmoz-utils": "^5.0.0",
69
74
  "haunted": "^5.0.0",
70
- "lit-html": "^2.0.0",
71
- "position.js": "^0.3.0"
75
+ "lit-html": "^2.0.0"
72
76
  },
73
77
  "devDependencies": {
74
78
  "@commitlint/cli": "^17.0.0",
@@ -1,21 +0,0 @@
1
- import { component, useCallback } from 'haunted';
2
- import { Autocomplete, observedAttributes } from './lib/autocomplete';
3
-
4
- const Standalone = host => {
5
- const { onChange, onText, ...props } = host;
6
-
7
- return Autocomplete.call(host, {
8
- ...props,
9
- onChange: useCallback((value, ...args) => {
10
- host.value = value;
11
- onChange?.(value, ...args);
12
- }, [onChange]),
13
- onText: useCallback(text => {
14
- host.text = text;
15
- onText?.(text);
16
- }, [onText])
17
- });
18
- };
19
-
20
- customElements.define('cosmoz-autocomplete-ui', component(Autocomplete, { observedAttributes }));
21
- customElements.define('cosmoz-autocomplete', component(Standalone, { observedAttributes }));
package/cosmoz-listbox.js DELETED
@@ -1,145 +0,0 @@
1
- import { html, component, useEffect, useRef, useMemo } from 'haunted';
2
- import { ref } from 'lit-html/directives/ref.js';
3
- import {
4
- virtualize,
5
- virtualizerRef,
6
- } from '@lit-labs/virtualizer/virtualize.js';
7
- import { tagged as css } from '@neovici/cosmoz-utils';
8
- import { portal } from '@neovici/cosmoz-utils/directives/portal';
9
- import { spreadProps } from '@neovici/cosmoz-utils/directives/spread-props';
10
- import { props } from '@neovici/cosmoz-utils/object';
11
- import { useListbox, properties } from './lib/use-listbox';
12
-
13
- const svg =
14
- /* eslint-disable quotes */
15
- "data:image/svg+xml,%3Csvg width='11' height='8' viewBox='0 0 11 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath " +
16
- "d='M9.5 1L5.20039 7.04766L1.66348 3.46152' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E",
17
- /* eslint-enable quotes */
18
- styles = ({ index, height, itemHeight }) => css`
19
- :host {
20
- position: fixed;
21
- min-height: ${itemHeight}px;
22
- z-index: 1000;
23
- height: ${height}px;
24
- font-family: var(--paper-font-subhead_-_font-family, initial);
25
- background: #fff;
26
- min-width: 72px;
27
- box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14),
28
- 0 1px 8px 0 rgba(0, 0, 0, 0.12), 0 3px 3px -2px rgba(0, 0, 0, 0.4);
29
- }
30
- .items {
31
- position: relative;
32
- overflow-y: auto;
33
- contain: layout paint !important;
34
- height: 100%;
35
- }
36
- .item {
37
- font-size: 14px;
38
- line-height: ${itemHeight}px;
39
- height: ${itemHeight}px;
40
- padding: 0 20px;
41
- box-sizing: border-box;
42
- width: 100%;
43
- cursor: pointer;
44
- text-overflow: ellipsis;
45
- white-space: nowrap;
46
- transition: background 0.2s;
47
- color: var(--cosmoz-listbox-color, #101010);
48
- }
49
-
50
- .item[data-index='${index || '0'}'] {
51
- background: var(
52
- --cosmoz-listbox-active-color,
53
- var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1))
54
- );
55
- }
56
- .sizer {
57
- position: relative;
58
- visibility: hidden;
59
- opacity: 0;
60
- pointer-events: none;
61
- z-index: -1;
62
- height: 0;
63
- width: auto;
64
- padding: 0 20px;
65
- overflow: hidden;
66
- max-width: inherit;
67
- font-size: 14px;
68
- }
69
-
70
- :host(:not([multi])) .item[aria-selected] {
71
- background: var(--cosmoz-listbox-single-selection-color, #dadada);
72
- }
73
-
74
- :host([multi]) .item {
75
- padding-left: 0;
76
- }
77
- :host([multi]) .item::before {
78
- content: '';
79
- font-size: 0;
80
- padding: 7.5px;
81
- margin: 0 8px;
82
- background: #fff;
83
- border: 1px solid #d6d6d5;
84
- border-radius: 4px;
85
- vertical-align: top;
86
- }
87
- :host([multi]) .item[aria-selected]::before {
88
- border-color: #5881f6;
89
- /* prettier-ignore */
90
- background: url("${svg}") #5881f6 no-repeat 50%;
91
- }
92
- :host([multi]) .sizer {
93
- padding-left: 33px;
94
- }
95
- .swatch {
96
- width: 18px;
97
- height: 18px;
98
- display: inline-block;
99
- box-sizing: border-box;
100
- vertical-align: middle;
101
- border-radius: 50%;
102
- border: 2px solid #eee;
103
- }
104
- `,
105
- Listbox = (host) => {
106
- const listRef = useRef(undefined);
107
- const { position, items, renderItem, height, itemHeight } =
108
- useListbox(host);
109
-
110
- useEffect(() => {
111
- if (!position.scroll) return;
112
- const vl = listRef.current[virtualizerRef];
113
- if (!vl._layout) return;
114
- vl.element(position.index)?.scrollIntoView({ block: 'nearest' });
115
- }, [position]);
116
-
117
- const layout = useMemo(
118
- () => ({ _itemSize: { height: itemHeight - 0.00001 } }),
119
- [itemHeight]
120
- );
121
-
122
- return html` <style>
123
- ${styles({ ...position, height, itemHeight })}
124
- </style>
125
- <div class="items" ${ref((el) => (listRef.current = el))}>
126
- <div virtualizer-sizer></div>
127
- ${virtualize({
128
- items,
129
- renderItem,
130
- scroller: true,
131
- layout,
132
- })}
133
- </div>`;
134
- };
135
-
136
- customElements.define('cosmoz-listbox', component(Listbox));
137
-
138
- export const listbox = ({ multi, ...thru }) =>
139
- portal(
140
- html`<cosmoz-listbox
141
- part="listbox"
142
- ?multi=${multi}
143
- ...=${spreadProps(props(properties)(thru))}
144
- ></cosmoz-listbox>`
145
- );
@@ -1,108 +0,0 @@
1
- import { tagged as css } from '@neovici/cosmoz-utils';
2
-
3
- export default css`
4
- :host {
5
- display: block;
6
- position: relative;
7
- }
8
- .chip {
9
- border-radius: var(--cosmoz-autocomplete-chip-border-radius, 500px);
10
- background: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
11
- margin: 1px 4px 1px 0;
12
- white-space: nowrap;
13
- overflow: hidden;
14
- display: flex;
15
- flex-direction: row;
16
- align-items: center;
17
- flex: 0.00001 1 min-content;
18
- max-width: 285px;
19
- max-width: 28ch;
20
- }
21
- .chip[data-one] {
22
- position: absolute;
23
- left: 0;
24
- top: 1px;
25
- bottom: 1px;
26
- right: 0;
27
- margin: 0;
28
- z-index: 2;
29
- pointer-events: auto;
30
- max-width: initial;
31
- }
32
- .chip-text {
33
- color: var(--cosmoz-autocomplete-chip-color, #424242);
34
- margin: var(--cosmoz-autocomplete-chip-text-margin, 0 10px);
35
- font-size: var(--cosmoz-autocomplete-chip-text-font-size, 12px);
36
- line-height: var(--cosmoz-autocomplete-chip-text-line-height, 22px);
37
- overflow: hidden;
38
- text-overflow: ellipsis;
39
- flex: 1;
40
- }
41
- .chip-clear {
42
- margin: 2px 4px 2px -6px;
43
- background-color: var(--cosmoz-autocomplete-chip-clear-bg-color, #81899b);
44
- border-radius: 500px;
45
- cursor: pointer;
46
- width: 16px;
47
- height: 16px;
48
- stroke: var(
49
- --cosmoz-autocomplete-chip-clear-stroke,
50
- var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb)
51
- );
52
- display: block;
53
- }
54
-
55
- .chip-clear svg,
56
- .badge-clear svg {
57
- display: block;
58
- transform: translate(3.5px, 3.5px);
59
- }
60
-
61
- .badge {
62
- background: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
63
- color: var(--cosmoz-autocomplete-chip-color, #424242);
64
- font-weight: 500;
65
- font-size: 12px;
66
- line-height: 22px;
67
- padding: 0 4px 0 6px;
68
- gap: 4px;
69
- border-radius: 90px;
70
- text-align: center;
71
- box-sizing: border-box;
72
- /* fallback for unsupported min-content FF, Safari */
73
- flex: 0.01 2 30px;
74
- flex: 0 2 min-content;
75
- display: flex;
76
- align-items: center;
77
- }
78
- .badge-clear {
79
- background-color: var(--cosmoz-autocomplete-chip-clear-bg-color, #81899b);
80
- border-radius: 500px;
81
- cursor: pointer;
82
- width: 16px;
83
- height: 16px;
84
- stroke: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
85
- display: block;
86
- }
87
-
88
- slot {
89
- display: contents !important;
90
- }
91
-
92
- @keyframes rotating {
93
- 100% {
94
- transform: rotate(360deg);
95
- }
96
- }
97
- .spinner {
98
- border-radius: 50%;
99
- width: 22px;
100
- height: 22px;
101
- border: 2px solid rgba(0, 0, 0, 0.1);
102
- border-top: 2px solid #5f5a92;
103
- animation: rotating 1.2s infinite cubic-bezier(0.785, 0.135, 0.15, 0.86);
104
- box-sizing: border-box;
105
- margin-top: -3px;
106
- flex: none;
107
- }
108
- `;
@@ -1,190 +0,0 @@
1
- import { html, nothing } from 'lit-html'; // eslint-disable-line object-curly-newline
2
- import { live } from 'lit-html/directives/live.js';
3
- import { until } from 'lit-html/directives/until.js';
4
- import { when } from 'lit-html/directives/when.js';
5
- import { useCallback } from 'haunted';
6
- import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
7
- import { array } from '@neovici/cosmoz-utils/array';
8
- import '@neovici/cosmoz-input';
9
- import { useAutocomplete } from './use-autocomplete';
10
- import { listbox } from '../cosmoz-listbox';
11
- import style from './autocomplete.css';
12
-
13
- const blank = () => nothing,
14
- clear = html`
15
- <svg
16
- width="9"
17
- height="8"
18
- viewBox="0 0 9 8"
19
- fill="none"
20
- xmlns="http://www.w3.org/2000/svg"
21
- >
22
- <line
23
- x1="7.53033"
24
- y1="0.994808"
25
- x2="1.16637"
26
- y2="7.35877"
27
- stroke-width="1.5"
28
- />
29
- <line
30
- x1="7.46967"
31
- y1="7.35882"
32
- x2="1.10571"
33
- y2="0.99486"
34
- stroke-width="1.5"
35
- />
36
- </svg>
37
- `,
38
- renderChip = (text, onClear, disabled, isOne) => html`
39
- <div
40
- class="chip"
41
- part="chip"
42
- slot="suffix"
43
- title=${text}
44
- ?data-one=${isOne}
45
- >
46
- <span class="chip-text" part="chip-text">${text}</span>
47
- ${when(
48
- !disabled,
49
- () =>
50
- html` <span class="chip-clear" part="chip-clear" @click=${onClear}>
51
- ${clear}
52
- </span>`
53
- )}
54
- </div>
55
- `,
56
- renderBadge = ({ value, onDeselect }) => {
57
- const len = value?.length;
58
- return when(
59
- len > 0,
60
- () => html`<div class="badge" slot="suffix" part="badge">
61
- ${len}
62
- <span
63
- class="badge-clear"
64
- part="chip-clear"
65
- @click=${() => onDeselect(value)}
66
- >${clear}</span
67
- >
68
- </div>`
69
- );
70
- },
71
- renderSelection = (value, isOne, onDeselect, textual, disabled) => {
72
- if (isOne || value.length === 1) {
73
- const val = value[0];
74
- return when(val, () =>
75
- renderChip(textual(val), () => onDeselect(val), disabled, isOne)
76
- );
77
- }
78
- return renderBadge({ value, onDeselect });
79
- },
80
- inputParts = ['input', 'label', 'line', 'error']
81
- .map((part) => `${part}: input-${part}`)
82
- .join(),
83
- autocomplete = (props) => {
84
- const {
85
- invalid,
86
- errorMessage,
87
- label,
88
- placeholder,
89
- disabled,
90
- noLabelFloat,
91
- alwaysFloatLabel,
92
- textual,
93
- text,
94
- onText,
95
- onFocus,
96
- onClick,
97
- onDeselect,
98
- value,
99
- limit,
100
- showSingle,
101
- items$,
102
- values$,
103
- } = props,
104
- host = useHost(),
105
- isOne = limit == 1, // eslint-disable-line eqeqeq
106
- isSingle = isOne && array(value)?.[0] != null,
107
- anchor = useCallback(
108
- () => host.shadowRoot.querySelector('#input'),
109
- [host, value]
110
- ),
111
- suggestions = until(
112
- items$.then((items) =>
113
- when((!isSingle || showSingle) && items.length, () =>
114
- listbox({
115
- ...props,
116
- anchor,
117
- items,
118
- multi: !isOne,
119
- })
120
- )
121
- )
122
- );
123
-
124
- return html` <style>
125
- ${style}
126
- </style>
127
- <cosmoz-input
128
- id="input"
129
- part="input"
130
- .label=${label}
131
- .placeholder=${isSingle ? undefined : placeholder}
132
- ?no-label-float=${noLabelFloat}
133
- ?always-float-label=${isSingle || alwaysFloatLabel}
134
- ?readonly=${isSingle}
135
- ?disabled=${disabled}
136
- ?invalid=${until(
137
- values$.then(
138
- () => invalid,
139
- () => true
140
- ),
141
- invalid
142
- )}
143
- .errorMessage=${until(
144
- values$.then(
145
- () => errorMessage,
146
- (e) => e.message
147
- ),
148
- errorMessage
149
- )}
150
- .value=${live(text)}
151
- @value-changed=${onText}
152
- @focusin=${onFocus}
153
- @focusout=${onFocus}
154
- @click=${onClick}
155
- autocomplete="off"
156
- exportparts=${inputParts}
157
- >
158
- <slot name="prefix" slot="prefix"></slot>
159
- <slot name="suffix" slot="suffix"></slot>
160
- ${renderSelection(array(value), isOne, onDeselect, textual, disabled)}
161
- ${until(
162
- values$.then(blank, blank),
163
- html`<div slot="suffix" class="spinner"></div>`
164
- )}
165
- </cosmoz-input>
166
-
167
- ${suggestions}`;
168
- },
169
- Autocomplete = (props) =>
170
- autocomplete({
171
- ...props,
172
- ...useAutocomplete(props),
173
- }),
174
- observedAttributes = [
175
- 'disabled',
176
- 'invalid',
177
- 'no-label-float',
178
- 'always-float-label',
179
- 'text-property',
180
- 'limit',
181
- 'show-single',
182
- 'keep-query',
183
- 'keep-opened',
184
- 'hide-empty',
185
- 'default-index',
186
- 'item-height',
187
- 'item-limit',
188
- ];
189
-
190
- export { autocomplete, Autocomplete, observedAttributes };
@@ -1,26 +0,0 @@
1
- import { html } from 'haunted';
2
- import { mark, identity } from './utils';
3
- export const itemRenderer =
4
- (render = identity) =>
5
- (item, i, { highlight, select, textual = identity, query, isSelected }) => {
6
- const text = textual(item),
7
- content = mark(text, query),
8
- rendered = render(content, item, i);
9
-
10
- return html` <div
11
- class="item"
12
- role="option"
13
- part="option"
14
- ?aria-selected=${isSelected(item)}
15
- data-index=${i}
16
- @mouseenter=${() => highlight(i)}
17
- @click=${() => select(item)}
18
- @mousedown=${(e) => e.preventDefault()}
19
- title=${text}
20
- >
21
- ${rendered}
22
- </div>
23
- <div class="sizer" virtualizer-sizer>${rendered}</div>`;
24
- };
25
-
26
- export default itemRenderer();
@@ -1,37 +0,0 @@
1
- /**
2
- * Returns all ancestor ShadowRoot elements
3
- * @param {HTMLElement} el The element
4
- * @param {HTMLElement} limit The limit to stop searching
5
- * @returns {ShadowRoot[]} Array of ancestor roots
6
- */
7
- const ancestorRoots = (el, limit = document.body) => {
8
- const roots = [];
9
- let ancestor = el;
10
- while (ancestor && ancestor !== limit) {
11
- if (ancestor.assignedSlot) {
12
- ancestor = ancestor.assignedSlot;
13
- continue;
14
- }
15
- if (ancestor instanceof ShadowRoot) {
16
- roots.push(ancestor);
17
- ancestor = ancestor.host;
18
- continue;
19
- }
20
- ancestor = ancestor.parentNode;
21
- }
22
- return roots;
23
- },
24
- /**
25
- * Listen to a scroll event in a ancestor root
26
- * @param {HTMLElement} el The element
27
- * @param {function} handler The event handler
28
- * @returns {function} Function to cleanup the event listeners
29
- */
30
- onScrolled = (el, handler) => {
31
- const roots = ancestorRoots(el);
32
- roots.forEach(r => r.addEventListener('scroll', handler, true));
33
- return () => {
34
- roots.forEach(r => r.removeEventListener('scroll', handler, true));
35
- };
36
- };
37
- export { onScrolled };
@@ -1,102 +0,0 @@
1
- import {
2
- useCallback, useMemo
3
- } from 'haunted';
4
- import {
5
- without, search, strProp, prop, array
6
- } from './utils';
7
- import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
8
- import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
9
- import { useFocus } from '@neovici/cosmoz-dropdown/src/use-focus.js';
10
- import { useKeys } from './use-keys';
11
-
12
- const
13
- EMPTY = [],
14
- EMPTY$ = Promise.resolve(EMPTY),
15
- notify = (host, name, detail) => host.dispatchEvent(new CustomEvent(name, { detail })),
16
- useNotify = (host, fn, name) => useCallback(val => {
17
- fn?.(val);
18
- notify(host, name, val);
19
- }, [fn]),
20
- useAutocomplete = ({
21
- value,
22
- text,
23
- onChange: _onChange,
24
- onText: _onText,
25
- onSelect,
26
- limit,
27
- source,
28
- textProperty,
29
- valueProperty,
30
- external,
31
- hideEmpty,
32
- keepQuery,
33
- keepOpened,
34
- ...thru
35
- }) => {
36
- const
37
- textual = useMemo(() => strProp(textProperty), [textProperty]),
38
- { active, onFocus, setClosed } = useFocus(thru),
39
- empty = !text,
40
- query = useMemo(() => text?.trim().toLowerCase(), [text]),
41
- host = useHost(),
42
- onText = useNotify(host, _onText, 'text'),
43
- onChange = useCallback(val => {
44
- _onChange?.(val, () => setClosed(true));
45
- notify(host, 'value', val);
46
- }, [_onChange]),
47
- source$ = useMemo(() => Promise.resolve(typeof source === 'function'
48
- ? source({ query, active })
49
- : source
50
- ), [source, active, query]),
51
- values$ = useMemo(() => source$.then(source => [...array(value), ...without(value, prop(valueProperty))(source)]), [source$, value, valueProperty]);
52
-
53
- useKeys({
54
- active,
55
- empty,
56
- limit,
57
- value,
58
- hideEmpty,
59
- onChange,
60
- onText
61
- });
62
-
63
- const meta = useMeta({ onText, onChange, value, limit, keepQuery, keepOpened, setClosed, onSelect });
64
-
65
- return {
66
- query,
67
- textual,
68
- values$,
69
- items$: useMemo(() => {
70
- if (!active || hideEmpty && empty) {
71
- return EMPTY$;
72
- }
73
- return query && !external
74
- ? values$.then(values => search(values, query, textual))
75
- : values$;
76
- }, [values$, active, query, textual, external, hideEmpty, empty]),
77
- onClick: useCallback(() => setClosed(false), []),
78
- onFocus,
79
- onText: useCallback(e => {
80
- onText(e.target.value);
81
- setClosed(false);
82
- }, [onText, text, setClosed]),
83
- onSelect: useCallback(newVal => {
84
- const { onSelect} = meta;
85
- if(onSelect) {
86
- return onSelect(newVal, meta);
87
- }
88
- const { onChange, onText, limit, value: val, keepQuery, keepOpened, setClosed } = meta;
89
- if (!keepQuery) {
90
- onText('');
91
- }
92
- if (!keepOpened) {
93
- setClosed(true);
94
- }
95
- const value = array(val);
96
- onChange((value.includes(newVal) ? without(newVal)(value) : [...value, newVal]).slice(-limit));
97
- }, [meta]),
98
- onDeselect: useCallback(val => meta.onChange(without(val)(meta.value)), [meta])
99
- };
100
- };
101
-
102
- export { useAutocomplete };
@@ -1,35 +0,0 @@
1
- import { useEffect } from 'haunted';
2
- import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
3
-
4
- export const useKeyboard = handlers => {
5
- const listeners = useMeta(handlers);
6
-
7
- useEffect(() => {
8
- const handler = e => {
9
- if (e.ctrlKey && e.altKey || e.defaultPrevented) {
10
- return;
11
- }
12
- switch (e.key) {
13
- case 'Up':
14
- case 'ArrowUp':
15
- e.preventDefault();
16
- listeners.onUp();
17
- break;
18
- case 'Down':
19
- case 'ArrowDown':
20
- e.preventDefault();
21
- listeners.onDown();
22
- break;
23
- case 'Enter':
24
- e.preventDefault();
25
- listeners.onEnter();
26
- break;
27
- default:
28
- break;
29
- }
30
- };
31
-
32
- document.addEventListener('keydown', handler, true);
33
- return () => document.removeEventListener('keydown', handler, true);
34
- }, [listeners]);
35
- };
package/lib/use-keys.js DELETED
@@ -1,39 +0,0 @@
1
- import { useEffect } from 'haunted';
2
- import { array } from './utils';
3
- import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
4
-
5
- const useKeys = ({
6
- active,
7
- empty,
8
- ...info
9
- }) => {
10
- const enabled = active && empty,
11
- meta = useMeta(info);
12
- useEffect(() => {
13
- if (!enabled) {
14
- return;
15
- }
16
- const handler = e => {
17
- if (e.defaultPrevented) {
18
- return;
19
- }
20
- const { key } = e,
21
- values = array(meta.value),
22
- isOne = meta.limit == 1; //eslint-disable-line eqeqeq
23
-
24
- if (values.length > 0 && (key === 'Backspace' || isOne && key.length === 1)) {
25
- return meta.onChange(values.slice(0, -1));
26
- } else if (meta.hideEmpty && values.length < 1 && ['Up', 'ArrowUp', 'Down', 'ArrowDown'].includes(key)) {
27
- meta.onText(' ');
28
- e.preventDefault();
29
- }
30
- };
31
-
32
- document.addEventListener('keydown', handler, true);
33
- return () => document.removeEventListener('keydown', handler, true);
34
-
35
- }, [enabled, meta]);
36
- };
37
-
38
- export { useKeys };
39
-
@@ -1,123 +0,0 @@
1
- import { useCallback, useEffect, useState, useMemo } from 'haunted';
2
- import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
3
- import { usePosition } from '@neovici/cosmoz-dropdown/src/use-position.js';
4
- import { useKeyboard } from './use-keyboard';
5
- import defaultItemRenderer from './item-renderer';
6
- import { byValue } from './utils';
7
-
8
- const useItems = ({ items, onSelect, defaultIndex = 0 }) => {
9
- const [position, setPosition] = useState({
10
- index: defaultIndex,
11
- }),
12
- { index } = position,
13
- { length } = items;
14
-
15
- useEffect(() => {
16
- setPosition({ index: defaultIndex, scroll: true });
17
- }, [items, defaultIndex]);
18
-
19
- useKeyboard({
20
- onUp: useCallback(
21
- () =>
22
- setPosition(({ index }) => ({
23
- index: index > 0 ? index - 1 : length - 1,
24
- scroll: true,
25
- })),
26
- [length]
27
- ),
28
- onDown: useCallback(
29
- () =>
30
- setPosition(({ index }) => ({
31
- index: index < length - 1 ? index + 1 : 0,
32
- scroll: true,
33
- })),
34
- [length]
35
- ),
36
- onEnter: useCallback(
37
- () => index > -1 && index < length && onSelect?.(items[index], index),
38
- [index, items, onSelect]
39
- ),
40
- });
41
-
42
- return {
43
- position,
44
- highlight: useCallback((index) => setPosition({ index }), []),
45
- select: useCallback((item) => onSelect?.(item), [onSelect]),
46
- };
47
- },
48
- useRenderItem = ({ itemRenderer, ...meta }) => {
49
- const info = useMeta(meta);
50
- return useCallback(
51
- (item, i) => (itemRenderer || defaultItemRenderer)(item, i, info),
52
- [info, itemRenderer]
53
- );
54
- },
55
- useListbox = (host) => {
56
- const {
57
- value,
58
- valueProperty,
59
- items: _items,
60
- onSelect,
61
- defaultIndex,
62
- query,
63
- textual,
64
- itemRenderer,
65
- itemHeight = 40,
66
- itemLimit = 5,
67
- anchor,
68
- confinement,
69
- placement,
70
- } = host,
71
- isSelected = useMemo(
72
- () => byValue(value, valueProperty),
73
- [value, valueProperty]
74
- ),
75
- items = useMemo(() => _items.slice(), [_items, isSelected]),
76
- { position, highlight, select } = useItems({
77
- items,
78
- onSelect,
79
- defaultIndex: isNaN(defaultIndex) ? undefined : Number(defaultIndex),
80
- });
81
-
82
- usePosition({
83
- anchor,
84
- host,
85
- confinement,
86
- placement,
87
- limit: true,
88
- });
89
-
90
- return {
91
- position,
92
- items,
93
- height: Math.min(itemLimit, items.length) * itemHeight,
94
- highlight,
95
- select,
96
- itemHeight,
97
- renderItem: useRenderItem({
98
- itemRenderer,
99
- highlight,
100
- select,
101
- textual,
102
- query,
103
- isSelected,
104
- }),
105
- };
106
- },
107
- properties = [
108
- 'query',
109
- 'items',
110
- 'onSelect',
111
- 'textual',
112
- 'anchor',
113
- 'confinement',
114
- 'placement',
115
- 'itemHeight',
116
- 'itemLimit',
117
- 'itemRenderer',
118
- 'defaultIndex',
119
- 'value',
120
- 'styles',
121
- ];
122
-
123
- export { useListbox, useRenderItem, properties };
package/lib/utils.js DELETED
@@ -1,62 +0,0 @@
1
- import { html } from 'lit-html';
2
-
3
- import { identity } from '@neovici/cosmoz-utils/function';
4
- import { array, without } from '@neovici/cosmoz-utils/array';
5
- import { prop, strProp } from '@neovici/cosmoz-utils/object';
6
-
7
- const mark = (text, query) => {
8
- if (!query || !text) {
9
- return text;
10
- }
11
- const i = text.toLowerCase().indexOf(query.toLowerCase());
12
- if (i < 0) {
13
- return text;
14
- }
15
- const end = i + query.length;
16
- return [
17
- text.slice(0, i),
18
- html`<mark>${ text.slice(i, end) }</mark>`,
19
- text.slice(end)
20
- ];
21
- },
22
- search = (source, query, textual) => {
23
- const matches = [];
24
- for (const item of source) {
25
- const index = textual(item).toLowerCase().indexOf(query);
26
- if (index < 0) {
27
- continue;
28
- }
29
- matches.push({
30
- item,
31
- index
32
- });
33
- }
34
- return matches
35
- .sort((a, b) => {
36
- return a.index - b.index;
37
- })
38
- .map(({ item }) => item);
39
- },
40
- scrollIntoViewIfNeeded = (el, parent) => {
41
- if (el == null) {
42
- return;
43
- }
44
- if (el.scrollIntoViewIfNeeded) {
45
- el.scrollIntoViewIfNeeded();
46
- return;
47
- }
48
- const ebox = el.getBoundingClientRect(),
49
- pbox = parent.getBoundingClientRect();
50
- if (ebox.top >= pbox.top && ebox.top + ebox.height < pbox.top + pbox.height) {
51
- return;
52
- }
53
- el.scrollIntoView();
54
- },
55
- byValue = (value, valueProperty) => {
56
- const valuation = prop(valueProperty);
57
- return item => array(value).find(value => valuation(value) === valuation(item));
58
- };
59
-
60
- export {
61
- array, identity, mark, prop, search, without, strProp, scrollIntoViewIfNeeded, byValue
62
- };