@pro6pp/infer-core 0.0.2-beta.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/README.md +39 -0
- package/dist/index.d.mts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +228 -0
- package/dist/index.mjs +202 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @pro6pp/infer-core
|
|
2
|
+
|
|
3
|
+
The headless logic engine behind the Pro6PP Infer SDKs.
|
|
4
|
+
Use this package if you are building a custom integration for a framework, or if you need to run Infer in a non-standard environment.
|
|
5
|
+
|
|
6
|
+
> **Note:** If you are using React, use [`@pro6pp/infer-react`](../react) instead.
|
|
7
|
+
> If you are using plain HTML/JS, use [`@pro6pp/infer-js`](../js).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @pro6pp/infer-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
The core logic is exposed via the `InferCore` class. It manages the API requests, state and parses input.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { InferCore } from '@pro6pp/infer-core';
|
|
21
|
+
|
|
22
|
+
const core = new InferCore({
|
|
23
|
+
authKey: 'YOUR_AUTH_KEY',
|
|
24
|
+
country: 'NL',
|
|
25
|
+
onStateChange: (state) => {
|
|
26
|
+
console.log('Current Suggestions:', state.suggestions);
|
|
27
|
+
console.log('Is Loading:', state.isLoading);
|
|
28
|
+
},
|
|
29
|
+
onSelect: (result) => {
|
|
30
|
+
console.log('User selected:', result);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// feed it input events, e.g. via an <input>
|
|
35
|
+
core.handleInput('Amsterdam');
|
|
36
|
+
|
|
37
|
+
// handle selections when user clicks a suggestion in your dropdown
|
|
38
|
+
core.selectItem(suggestionObject);
|
|
39
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported ISO 3166-1 alpha-2 country codes.
|
|
3
|
+
*/
|
|
4
|
+
type CountryCode = 'NL' | 'DE';
|
|
5
|
+
/**
|
|
6
|
+
* The current step in the address inference process.
|
|
7
|
+
*/
|
|
8
|
+
type Stage = 'empty' | 'mixed' | 'street' | 'city' | 'postcode' | 'house_number' | 'house_number_first' | 'addition' | 'direct' | 'final';
|
|
9
|
+
/**
|
|
10
|
+
* The standardized address object returned upon selection.
|
|
11
|
+
*/
|
|
12
|
+
interface AddressValue {
|
|
13
|
+
street: string;
|
|
14
|
+
city: string;
|
|
15
|
+
street_number?: string | number;
|
|
16
|
+
house_number?: string | number;
|
|
17
|
+
postcode?: string;
|
|
18
|
+
postcode_full?: string;
|
|
19
|
+
addition?: string;
|
|
20
|
+
/** Allow for extra fields if API expands. */
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A single item in the dropdown list.
|
|
25
|
+
*/
|
|
26
|
+
interface InferResult {
|
|
27
|
+
/** The text to display in the UI (e.g. "Amsterdam"). */
|
|
28
|
+
label: string;
|
|
29
|
+
/** The actual address data. Only present if this result completes an address. */
|
|
30
|
+
value?: AddressValue;
|
|
31
|
+
/** Helper text. */
|
|
32
|
+
subtitle?: string | null;
|
|
33
|
+
/** Number of underlying results (optional). */
|
|
34
|
+
count?: number | string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The complete state returned by the `useInfer` hook.
|
|
38
|
+
*/
|
|
39
|
+
interface InferState {
|
|
40
|
+
/** The current value of the input field. */
|
|
41
|
+
query: string;
|
|
42
|
+
/** The current inference stage. */
|
|
43
|
+
stage: Stage | null;
|
|
44
|
+
/** List of cities to display, specific to `mixed` mode. */
|
|
45
|
+
cities: InferResult[];
|
|
46
|
+
/** List of streets to display, specific to `mixed` mode. */
|
|
47
|
+
streets: InferResult[];
|
|
48
|
+
/** General suggestions to display. */
|
|
49
|
+
suggestions: InferResult[];
|
|
50
|
+
/** True if a full, valid address has been selected. */
|
|
51
|
+
isValid: boolean;
|
|
52
|
+
/** True if the last network request failed. */
|
|
53
|
+
isError: boolean;
|
|
54
|
+
/** True if a network request is currently active. */
|
|
55
|
+
isLoading: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Custom fetch implementation, compatible with `window.fetch`.
|
|
59
|
+
* Useful for server-side usage or testing.
|
|
60
|
+
*/
|
|
61
|
+
type Fetcher = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
62
|
+
/**
|
|
63
|
+
* Configuration for Infer Core.
|
|
64
|
+
*/
|
|
65
|
+
interface InferConfig {
|
|
66
|
+
/**
|
|
67
|
+
* Pro6PP Authorization Key.
|
|
68
|
+
*/
|
|
69
|
+
authKey: string;
|
|
70
|
+
/**
|
|
71
|
+
* Country to search addresses in.
|
|
72
|
+
*/
|
|
73
|
+
country: CountryCode;
|
|
74
|
+
/**
|
|
75
|
+
* Base URL for the Pro6PP API.
|
|
76
|
+
* Useful for proxying requests through your own backend.
|
|
77
|
+
* @default 'https://api.pro6pp.nl/v2'
|
|
78
|
+
*/
|
|
79
|
+
apiUrl?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Custom fetch implementation.
|
|
82
|
+
* Useful for server-side usage or testing.
|
|
83
|
+
*/
|
|
84
|
+
fetcher?: Fetcher;
|
|
85
|
+
/**
|
|
86
|
+
* Maximum number of results to return.
|
|
87
|
+
* @default 1000
|
|
88
|
+
*/
|
|
89
|
+
limit?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Callback fired when the internal state changes.
|
|
92
|
+
*/
|
|
93
|
+
onStateChange?: (state: InferState) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Callback fired when a user selects a full valid address.
|
|
96
|
+
*/
|
|
97
|
+
onSelect?: (selection: AddressValue | string | null) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
declare const INITIAL_STATE: InferState;
|
|
101
|
+
declare class InferCore {
|
|
102
|
+
private country;
|
|
103
|
+
private authKey;
|
|
104
|
+
private apiUrl;
|
|
105
|
+
private limit;
|
|
106
|
+
private fetcher;
|
|
107
|
+
private onStateChange;
|
|
108
|
+
private onSelect;
|
|
109
|
+
state: InferState;
|
|
110
|
+
private abortController;
|
|
111
|
+
private debouncedFetch;
|
|
112
|
+
constructor(config: InferConfig);
|
|
113
|
+
handleInput(value: string): void;
|
|
114
|
+
handleKeyDown(event: KeyboardEvent | React.KeyboardEvent<HTMLInputElement>): void;
|
|
115
|
+
selectItem(item: InferResult | string): void;
|
|
116
|
+
private shouldAutoInsertComma;
|
|
117
|
+
private finishSelection;
|
|
118
|
+
private processSelection;
|
|
119
|
+
private executeFetch;
|
|
120
|
+
private mapResponseToState;
|
|
121
|
+
private updateQueryAndFetch;
|
|
122
|
+
private replaceLastSegment;
|
|
123
|
+
private getQueryPrefix;
|
|
124
|
+
private getCurrentFragment;
|
|
125
|
+
private resetState;
|
|
126
|
+
private updateState;
|
|
127
|
+
private debounce;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { type AddressValue, type CountryCode, type Fetcher, INITIAL_STATE, type InferConfig, InferCore, type InferResult, type InferState, type Stage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported ISO 3166-1 alpha-2 country codes.
|
|
3
|
+
*/
|
|
4
|
+
type CountryCode = 'NL' | 'DE';
|
|
5
|
+
/**
|
|
6
|
+
* The current step in the address inference process.
|
|
7
|
+
*/
|
|
8
|
+
type Stage = 'empty' | 'mixed' | 'street' | 'city' | 'postcode' | 'house_number' | 'house_number_first' | 'addition' | 'direct' | 'final';
|
|
9
|
+
/**
|
|
10
|
+
* The standardized address object returned upon selection.
|
|
11
|
+
*/
|
|
12
|
+
interface AddressValue {
|
|
13
|
+
street: string;
|
|
14
|
+
city: string;
|
|
15
|
+
street_number?: string | number;
|
|
16
|
+
house_number?: string | number;
|
|
17
|
+
postcode?: string;
|
|
18
|
+
postcode_full?: string;
|
|
19
|
+
addition?: string;
|
|
20
|
+
/** Allow for extra fields if API expands. */
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A single item in the dropdown list.
|
|
25
|
+
*/
|
|
26
|
+
interface InferResult {
|
|
27
|
+
/** The text to display in the UI (e.g. "Amsterdam"). */
|
|
28
|
+
label: string;
|
|
29
|
+
/** The actual address data. Only present if this result completes an address. */
|
|
30
|
+
value?: AddressValue;
|
|
31
|
+
/** Helper text. */
|
|
32
|
+
subtitle?: string | null;
|
|
33
|
+
/** Number of underlying results (optional). */
|
|
34
|
+
count?: number | string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The complete state returned by the `useInfer` hook.
|
|
38
|
+
*/
|
|
39
|
+
interface InferState {
|
|
40
|
+
/** The current value of the input field. */
|
|
41
|
+
query: string;
|
|
42
|
+
/** The current inference stage. */
|
|
43
|
+
stage: Stage | null;
|
|
44
|
+
/** List of cities to display, specific to `mixed` mode. */
|
|
45
|
+
cities: InferResult[];
|
|
46
|
+
/** List of streets to display, specific to `mixed` mode. */
|
|
47
|
+
streets: InferResult[];
|
|
48
|
+
/** General suggestions to display. */
|
|
49
|
+
suggestions: InferResult[];
|
|
50
|
+
/** True if a full, valid address has been selected. */
|
|
51
|
+
isValid: boolean;
|
|
52
|
+
/** True if the last network request failed. */
|
|
53
|
+
isError: boolean;
|
|
54
|
+
/** True if a network request is currently active. */
|
|
55
|
+
isLoading: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Custom fetch implementation, compatible with `window.fetch`.
|
|
59
|
+
* Useful for server-side usage or testing.
|
|
60
|
+
*/
|
|
61
|
+
type Fetcher = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
62
|
+
/**
|
|
63
|
+
* Configuration for Infer Core.
|
|
64
|
+
*/
|
|
65
|
+
interface InferConfig {
|
|
66
|
+
/**
|
|
67
|
+
* Pro6PP Authorization Key.
|
|
68
|
+
*/
|
|
69
|
+
authKey: string;
|
|
70
|
+
/**
|
|
71
|
+
* Country to search addresses in.
|
|
72
|
+
*/
|
|
73
|
+
country: CountryCode;
|
|
74
|
+
/**
|
|
75
|
+
* Base URL for the Pro6PP API.
|
|
76
|
+
* Useful for proxying requests through your own backend.
|
|
77
|
+
* @default 'https://api.pro6pp.nl/v2'
|
|
78
|
+
*/
|
|
79
|
+
apiUrl?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Custom fetch implementation.
|
|
82
|
+
* Useful for server-side usage or testing.
|
|
83
|
+
*/
|
|
84
|
+
fetcher?: Fetcher;
|
|
85
|
+
/**
|
|
86
|
+
* Maximum number of results to return.
|
|
87
|
+
* @default 1000
|
|
88
|
+
*/
|
|
89
|
+
limit?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Callback fired when the internal state changes.
|
|
92
|
+
*/
|
|
93
|
+
onStateChange?: (state: InferState) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Callback fired when a user selects a full valid address.
|
|
96
|
+
*/
|
|
97
|
+
onSelect?: (selection: AddressValue | string | null) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
declare const INITIAL_STATE: InferState;
|
|
101
|
+
declare class InferCore {
|
|
102
|
+
private country;
|
|
103
|
+
private authKey;
|
|
104
|
+
private apiUrl;
|
|
105
|
+
private limit;
|
|
106
|
+
private fetcher;
|
|
107
|
+
private onStateChange;
|
|
108
|
+
private onSelect;
|
|
109
|
+
state: InferState;
|
|
110
|
+
private abortController;
|
|
111
|
+
private debouncedFetch;
|
|
112
|
+
constructor(config: InferConfig);
|
|
113
|
+
handleInput(value: string): void;
|
|
114
|
+
handleKeyDown(event: KeyboardEvent | React.KeyboardEvent<HTMLInputElement>): void;
|
|
115
|
+
selectItem(item: InferResult | string): void;
|
|
116
|
+
private shouldAutoInsertComma;
|
|
117
|
+
private finishSelection;
|
|
118
|
+
private processSelection;
|
|
119
|
+
private executeFetch;
|
|
120
|
+
private mapResponseToState;
|
|
121
|
+
private updateQueryAndFetch;
|
|
122
|
+
private replaceLastSegment;
|
|
123
|
+
private getQueryPrefix;
|
|
124
|
+
private getCurrentFragment;
|
|
125
|
+
private resetState;
|
|
126
|
+
private updateState;
|
|
127
|
+
private debounce;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { type AddressValue, type CountryCode, type Fetcher, INITIAL_STATE, type InferConfig, InferCore, type InferResult, type InferState, type Stage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
var index_exports = {};
|
|
24
|
+
__export(index_exports, {
|
|
25
|
+
INITIAL_STATE: () => INITIAL_STATE,
|
|
26
|
+
InferCore: () => InferCore
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/core.ts
|
|
31
|
+
var DEFAULTS = {
|
|
32
|
+
API_URL: "https://api.pro6pp.nl/v2",
|
|
33
|
+
LIMIT: 1e3,
|
|
34
|
+
DEBOUNCE_MS: 300
|
|
35
|
+
};
|
|
36
|
+
var PATTERNS = {
|
|
37
|
+
DIGITS_1_3: /^[0-9]{1,3}$/
|
|
38
|
+
};
|
|
39
|
+
var INITIAL_STATE = {
|
|
40
|
+
query: "",
|
|
41
|
+
stage: null,
|
|
42
|
+
cities: [],
|
|
43
|
+
streets: [],
|
|
44
|
+
suggestions: [],
|
|
45
|
+
isValid: false,
|
|
46
|
+
isError: false,
|
|
47
|
+
isLoading: false
|
|
48
|
+
};
|
|
49
|
+
var InferCore = class {
|
|
50
|
+
constructor(config) {
|
|
51
|
+
__publicField(this, "country");
|
|
52
|
+
__publicField(this, "authKey");
|
|
53
|
+
__publicField(this, "apiUrl");
|
|
54
|
+
__publicField(this, "limit");
|
|
55
|
+
__publicField(this, "fetcher");
|
|
56
|
+
__publicField(this, "onStateChange");
|
|
57
|
+
__publicField(this, "onSelect");
|
|
58
|
+
__publicField(this, "state");
|
|
59
|
+
__publicField(this, "abortController", null);
|
|
60
|
+
__publicField(this, "debouncedFetch");
|
|
61
|
+
this.country = config.country;
|
|
62
|
+
this.authKey = config.authKey;
|
|
63
|
+
this.apiUrl = config.apiUrl || DEFAULTS.API_URL;
|
|
64
|
+
this.limit = config.limit || DEFAULTS.LIMIT;
|
|
65
|
+
this.fetcher = config.fetcher || ((url, init) => fetch(url, init));
|
|
66
|
+
this.onStateChange = config.onStateChange || (() => {
|
|
67
|
+
});
|
|
68
|
+
this.onSelect = config.onSelect || (() => {
|
|
69
|
+
});
|
|
70
|
+
this.state = { ...INITIAL_STATE };
|
|
71
|
+
this.debouncedFetch = this.debounce(
|
|
72
|
+
(val) => this.executeFetch(val),
|
|
73
|
+
DEFAULTS.DEBOUNCE_MS
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
handleInput(value) {
|
|
77
|
+
this.updateState({
|
|
78
|
+
query: value,
|
|
79
|
+
isValid: false,
|
|
80
|
+
isLoading: !!value.trim()
|
|
81
|
+
});
|
|
82
|
+
if (this.state.stage === "final") {
|
|
83
|
+
this.onSelect(null);
|
|
84
|
+
}
|
|
85
|
+
this.debouncedFetch(value);
|
|
86
|
+
}
|
|
87
|
+
handleKeyDown(event) {
|
|
88
|
+
const target = event.target;
|
|
89
|
+
const val = target.value;
|
|
90
|
+
if (event.key === " " && this.shouldAutoInsertComma(val)) {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
const next = `${val.trim()}, `;
|
|
93
|
+
this.updateQueryAndFetch(next);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
selectItem(item) {
|
|
97
|
+
const label = typeof item === "string" ? item : item.label;
|
|
98
|
+
const value = typeof item !== "string" ? item.value : void 0;
|
|
99
|
+
const subtitle = typeof item !== "string" ? item.subtitle : null;
|
|
100
|
+
if (this.state.stage === "final") {
|
|
101
|
+
this.finishSelection(label, value);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.processSelection(label, subtitle);
|
|
105
|
+
}
|
|
106
|
+
shouldAutoInsertComma(currentVal) {
|
|
107
|
+
const isStartOfSegmentAndNumeric = !currentVal.includes(",") && PATTERNS.DIGITS_1_3.test(currentVal.trim());
|
|
108
|
+
if (isStartOfSegmentAndNumeric) return true;
|
|
109
|
+
if (this.state.stage === "house_number") {
|
|
110
|
+
const currentFragment = this.getCurrentFragment(currentVal);
|
|
111
|
+
return PATTERNS.DIGITS_1_3.test(currentFragment);
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
finishSelection(label, value) {
|
|
116
|
+
this.updateState({ query: label, suggestions: [], cities: [], streets: [], isValid: true });
|
|
117
|
+
this.onSelect(value || label);
|
|
118
|
+
}
|
|
119
|
+
processSelection(label, subtitle) {
|
|
120
|
+
const { stage, query } = this.state;
|
|
121
|
+
let nextQuery = query;
|
|
122
|
+
const isContextualSelection = subtitle && (stage === "city" || stage === "street" || stage === "mixed");
|
|
123
|
+
if (isContextualSelection) {
|
|
124
|
+
if (stage === "city") {
|
|
125
|
+
nextQuery = `${subtitle}, ${label}, `;
|
|
126
|
+
} else {
|
|
127
|
+
const prefix = this.getQueryPrefix(query);
|
|
128
|
+
nextQuery = prefix ? `${prefix} ${label}, ${subtitle}, ` : `${label}, ${subtitle}, `;
|
|
129
|
+
}
|
|
130
|
+
this.updateQueryAndFetch(nextQuery);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (stage === "direct" || stage === "addition") {
|
|
134
|
+
this.finishSelection(label);
|
|
135
|
+
this.handleInput(label);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const hasComma = query.includes(",");
|
|
139
|
+
const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "house_number_first");
|
|
140
|
+
if (isFirstSegment) {
|
|
141
|
+
nextQuery = `${label}, `;
|
|
142
|
+
} else {
|
|
143
|
+
nextQuery = this.replaceLastSegment(query, label);
|
|
144
|
+
if (stage !== "house_number") {
|
|
145
|
+
nextQuery += ", ";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.updateQueryAndFetch(nextQuery);
|
|
149
|
+
}
|
|
150
|
+
executeFetch(val) {
|
|
151
|
+
const text = (val || "").toString();
|
|
152
|
+
if (!text.trim()) {
|
|
153
|
+
this.abortController?.abort();
|
|
154
|
+
this.resetState();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.updateState({ isError: false });
|
|
158
|
+
if (this.abortController) this.abortController.abort();
|
|
159
|
+
this.abortController = new AbortController();
|
|
160
|
+
const url = new URL(`${this.apiUrl}/infer/${this.country.toLowerCase()}`);
|
|
161
|
+
const params = {
|
|
162
|
+
authKey: this.authKey,
|
|
163
|
+
query: text,
|
|
164
|
+
limit: this.limit.toString()
|
|
165
|
+
};
|
|
166
|
+
url.search = new URLSearchParams(params).toString();
|
|
167
|
+
this.fetcher(url.toString(), { signal: this.abortController.signal }).then((res) => {
|
|
168
|
+
if (!res.ok) throw new Error("Network error");
|
|
169
|
+
return res.json();
|
|
170
|
+
}).then((data) => this.mapResponseToState(data)).catch((e) => {
|
|
171
|
+
if (e.name !== "AbortError") {
|
|
172
|
+
this.updateState({ isError: true, isLoading: false });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
mapResponseToState(data) {
|
|
177
|
+
const newState = {
|
|
178
|
+
stage: data.stage,
|
|
179
|
+
isLoading: false
|
|
180
|
+
};
|
|
181
|
+
if (data.stage === "mixed") {
|
|
182
|
+
newState.cities = data.cities || [];
|
|
183
|
+
newState.streets = data.streets || [];
|
|
184
|
+
newState.suggestions = [];
|
|
185
|
+
} else {
|
|
186
|
+
newState.suggestions = data.suggestions || [];
|
|
187
|
+
newState.cities = [];
|
|
188
|
+
newState.streets = [];
|
|
189
|
+
}
|
|
190
|
+
newState.isValid = data.stage === "final";
|
|
191
|
+
this.updateState(newState);
|
|
192
|
+
}
|
|
193
|
+
updateQueryAndFetch(nextQuery) {
|
|
194
|
+
this.updateState({ query: nextQuery, suggestions: [], cities: [], streets: [] });
|
|
195
|
+
this.handleInput(nextQuery);
|
|
196
|
+
}
|
|
197
|
+
replaceLastSegment(fullText, newSegment) {
|
|
198
|
+
const lastCommaIndex = fullText.lastIndexOf(",");
|
|
199
|
+
if (lastCommaIndex === -1) return newSegment;
|
|
200
|
+
return `${fullText.slice(0, lastCommaIndex + 1)} ${newSegment}`.trim();
|
|
201
|
+
}
|
|
202
|
+
getQueryPrefix(q) {
|
|
203
|
+
const lastComma = q.lastIndexOf(",");
|
|
204
|
+
return lastComma === -1 ? "" : q.slice(0, lastComma + 1).trimEnd();
|
|
205
|
+
}
|
|
206
|
+
getCurrentFragment(q) {
|
|
207
|
+
return (q.split(",").slice(-1)[0] ?? "").trim();
|
|
208
|
+
}
|
|
209
|
+
resetState() {
|
|
210
|
+
this.updateState({ ...INITIAL_STATE, query: this.state.query });
|
|
211
|
+
}
|
|
212
|
+
updateState(updates) {
|
|
213
|
+
this.state = { ...this.state, ...updates };
|
|
214
|
+
this.onStateChange(this.state);
|
|
215
|
+
}
|
|
216
|
+
debounce(func, wait) {
|
|
217
|
+
let timeout;
|
|
218
|
+
return (...args) => {
|
|
219
|
+
if (timeout) clearTimeout(timeout);
|
|
220
|
+
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
225
|
+
0 && (module.exports = {
|
|
226
|
+
INITIAL_STATE,
|
|
227
|
+
InferCore
|
|
228
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/core.ts
|
|
6
|
+
var DEFAULTS = {
|
|
7
|
+
API_URL: "https://api.pro6pp.nl/v2",
|
|
8
|
+
LIMIT: 1e3,
|
|
9
|
+
DEBOUNCE_MS: 300
|
|
10
|
+
};
|
|
11
|
+
var PATTERNS = {
|
|
12
|
+
DIGITS_1_3: /^[0-9]{1,3}$/
|
|
13
|
+
};
|
|
14
|
+
var INITIAL_STATE = {
|
|
15
|
+
query: "",
|
|
16
|
+
stage: null,
|
|
17
|
+
cities: [],
|
|
18
|
+
streets: [],
|
|
19
|
+
suggestions: [],
|
|
20
|
+
isValid: false,
|
|
21
|
+
isError: false,
|
|
22
|
+
isLoading: false
|
|
23
|
+
};
|
|
24
|
+
var InferCore = class {
|
|
25
|
+
constructor(config) {
|
|
26
|
+
__publicField(this, "country");
|
|
27
|
+
__publicField(this, "authKey");
|
|
28
|
+
__publicField(this, "apiUrl");
|
|
29
|
+
__publicField(this, "limit");
|
|
30
|
+
__publicField(this, "fetcher");
|
|
31
|
+
__publicField(this, "onStateChange");
|
|
32
|
+
__publicField(this, "onSelect");
|
|
33
|
+
__publicField(this, "state");
|
|
34
|
+
__publicField(this, "abortController", null);
|
|
35
|
+
__publicField(this, "debouncedFetch");
|
|
36
|
+
this.country = config.country;
|
|
37
|
+
this.authKey = config.authKey;
|
|
38
|
+
this.apiUrl = config.apiUrl || DEFAULTS.API_URL;
|
|
39
|
+
this.limit = config.limit || DEFAULTS.LIMIT;
|
|
40
|
+
this.fetcher = config.fetcher || ((url, init) => fetch(url, init));
|
|
41
|
+
this.onStateChange = config.onStateChange || (() => {
|
|
42
|
+
});
|
|
43
|
+
this.onSelect = config.onSelect || (() => {
|
|
44
|
+
});
|
|
45
|
+
this.state = { ...INITIAL_STATE };
|
|
46
|
+
this.debouncedFetch = this.debounce(
|
|
47
|
+
(val) => this.executeFetch(val),
|
|
48
|
+
DEFAULTS.DEBOUNCE_MS
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
handleInput(value) {
|
|
52
|
+
this.updateState({
|
|
53
|
+
query: value,
|
|
54
|
+
isValid: false,
|
|
55
|
+
isLoading: !!value.trim()
|
|
56
|
+
});
|
|
57
|
+
if (this.state.stage === "final") {
|
|
58
|
+
this.onSelect(null);
|
|
59
|
+
}
|
|
60
|
+
this.debouncedFetch(value);
|
|
61
|
+
}
|
|
62
|
+
handleKeyDown(event) {
|
|
63
|
+
const target = event.target;
|
|
64
|
+
const val = target.value;
|
|
65
|
+
if (event.key === " " && this.shouldAutoInsertComma(val)) {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
const next = `${val.trim()}, `;
|
|
68
|
+
this.updateQueryAndFetch(next);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
selectItem(item) {
|
|
72
|
+
const label = typeof item === "string" ? item : item.label;
|
|
73
|
+
const value = typeof item !== "string" ? item.value : void 0;
|
|
74
|
+
const subtitle = typeof item !== "string" ? item.subtitle : null;
|
|
75
|
+
if (this.state.stage === "final") {
|
|
76
|
+
this.finishSelection(label, value);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.processSelection(label, subtitle);
|
|
80
|
+
}
|
|
81
|
+
shouldAutoInsertComma(currentVal) {
|
|
82
|
+
const isStartOfSegmentAndNumeric = !currentVal.includes(",") && PATTERNS.DIGITS_1_3.test(currentVal.trim());
|
|
83
|
+
if (isStartOfSegmentAndNumeric) return true;
|
|
84
|
+
if (this.state.stage === "house_number") {
|
|
85
|
+
const currentFragment = this.getCurrentFragment(currentVal);
|
|
86
|
+
return PATTERNS.DIGITS_1_3.test(currentFragment);
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
finishSelection(label, value) {
|
|
91
|
+
this.updateState({ query: label, suggestions: [], cities: [], streets: [], isValid: true });
|
|
92
|
+
this.onSelect(value || label);
|
|
93
|
+
}
|
|
94
|
+
processSelection(label, subtitle) {
|
|
95
|
+
const { stage, query } = this.state;
|
|
96
|
+
let nextQuery = query;
|
|
97
|
+
const isContextualSelection = subtitle && (stage === "city" || stage === "street" || stage === "mixed");
|
|
98
|
+
if (isContextualSelection) {
|
|
99
|
+
if (stage === "city") {
|
|
100
|
+
nextQuery = `${subtitle}, ${label}, `;
|
|
101
|
+
} else {
|
|
102
|
+
const prefix = this.getQueryPrefix(query);
|
|
103
|
+
nextQuery = prefix ? `${prefix} ${label}, ${subtitle}, ` : `${label}, ${subtitle}, `;
|
|
104
|
+
}
|
|
105
|
+
this.updateQueryAndFetch(nextQuery);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (stage === "direct" || stage === "addition") {
|
|
109
|
+
this.finishSelection(label);
|
|
110
|
+
this.handleInput(label);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const hasComma = query.includes(",");
|
|
114
|
+
const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "house_number_first");
|
|
115
|
+
if (isFirstSegment) {
|
|
116
|
+
nextQuery = `${label}, `;
|
|
117
|
+
} else {
|
|
118
|
+
nextQuery = this.replaceLastSegment(query, label);
|
|
119
|
+
if (stage !== "house_number") {
|
|
120
|
+
nextQuery += ", ";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.updateQueryAndFetch(nextQuery);
|
|
124
|
+
}
|
|
125
|
+
executeFetch(val) {
|
|
126
|
+
const text = (val || "").toString();
|
|
127
|
+
if (!text.trim()) {
|
|
128
|
+
this.abortController?.abort();
|
|
129
|
+
this.resetState();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.updateState({ isError: false });
|
|
133
|
+
if (this.abortController) this.abortController.abort();
|
|
134
|
+
this.abortController = new AbortController();
|
|
135
|
+
const url = new URL(`${this.apiUrl}/infer/${this.country.toLowerCase()}`);
|
|
136
|
+
const params = {
|
|
137
|
+
authKey: this.authKey,
|
|
138
|
+
query: text,
|
|
139
|
+
limit: this.limit.toString()
|
|
140
|
+
};
|
|
141
|
+
url.search = new URLSearchParams(params).toString();
|
|
142
|
+
this.fetcher(url.toString(), { signal: this.abortController.signal }).then((res) => {
|
|
143
|
+
if (!res.ok) throw new Error("Network error");
|
|
144
|
+
return res.json();
|
|
145
|
+
}).then((data) => this.mapResponseToState(data)).catch((e) => {
|
|
146
|
+
if (e.name !== "AbortError") {
|
|
147
|
+
this.updateState({ isError: true, isLoading: false });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
mapResponseToState(data) {
|
|
152
|
+
const newState = {
|
|
153
|
+
stage: data.stage,
|
|
154
|
+
isLoading: false
|
|
155
|
+
};
|
|
156
|
+
if (data.stage === "mixed") {
|
|
157
|
+
newState.cities = data.cities || [];
|
|
158
|
+
newState.streets = data.streets || [];
|
|
159
|
+
newState.suggestions = [];
|
|
160
|
+
} else {
|
|
161
|
+
newState.suggestions = data.suggestions || [];
|
|
162
|
+
newState.cities = [];
|
|
163
|
+
newState.streets = [];
|
|
164
|
+
}
|
|
165
|
+
newState.isValid = data.stage === "final";
|
|
166
|
+
this.updateState(newState);
|
|
167
|
+
}
|
|
168
|
+
updateQueryAndFetch(nextQuery) {
|
|
169
|
+
this.updateState({ query: nextQuery, suggestions: [], cities: [], streets: [] });
|
|
170
|
+
this.handleInput(nextQuery);
|
|
171
|
+
}
|
|
172
|
+
replaceLastSegment(fullText, newSegment) {
|
|
173
|
+
const lastCommaIndex = fullText.lastIndexOf(",");
|
|
174
|
+
if (lastCommaIndex === -1) return newSegment;
|
|
175
|
+
return `${fullText.slice(0, lastCommaIndex + 1)} ${newSegment}`.trim();
|
|
176
|
+
}
|
|
177
|
+
getQueryPrefix(q) {
|
|
178
|
+
const lastComma = q.lastIndexOf(",");
|
|
179
|
+
return lastComma === -1 ? "" : q.slice(0, lastComma + 1).trimEnd();
|
|
180
|
+
}
|
|
181
|
+
getCurrentFragment(q) {
|
|
182
|
+
return (q.split(",").slice(-1)[0] ?? "").trim();
|
|
183
|
+
}
|
|
184
|
+
resetState() {
|
|
185
|
+
this.updateState({ ...INITIAL_STATE, query: this.state.query });
|
|
186
|
+
}
|
|
187
|
+
updateState(updates) {
|
|
188
|
+
this.state = { ...this.state, ...updates };
|
|
189
|
+
this.onStateChange(this.state);
|
|
190
|
+
}
|
|
191
|
+
debounce(func, wait) {
|
|
192
|
+
let timeout;
|
|
193
|
+
return (...args) => {
|
|
194
|
+
if (timeout) clearTimeout(timeout);
|
|
195
|
+
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
export {
|
|
200
|
+
INITIAL_STATE,
|
|
201
|
+
InferCore
|
|
202
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pro6pp/infer-core",
|
|
3
|
+
"version": "0.0.2-beta.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
20
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
21
|
+
"type-check": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.0.0",
|
|
25
|
+
"typescript": "^5.0.0"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://gitlab.d-centralize.nl/dc/pro6pp/pro6pp-infer-sdk"
|
|
34
|
+
}
|
|
35
|
+
}
|