algolia-experiences 1.0.0 → 1.0.1
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 +1 -1
- package/src/fake-configuration.ts +157 -4
- package/src/get-information.ts +29 -0
- package/src/render.tsx +250 -0
- package/src/setup-instantsearch.ts +4 -223
- package/src/types.ts +72 -22
- package/src/util.ts +9 -0
- package/src/widgets.ts +0 -4
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ const CONFIGURATION_OBJECT: Record<string, Configuration> = {
|
|
|
17
17
|
type: 'ais.refinementList',
|
|
18
18
|
parameters: {
|
|
19
19
|
attribute: 'brand',
|
|
20
|
+
header: 'Brand',
|
|
20
21
|
},
|
|
21
22
|
},
|
|
22
23
|
],
|
|
@@ -26,7 +27,7 @@ const CONFIGURATION_OBJECT: Record<string, Configuration> = {
|
|
|
26
27
|
parameters: {},
|
|
27
28
|
children: [
|
|
28
29
|
{
|
|
29
|
-
type: '
|
|
30
|
+
type: 'span',
|
|
30
31
|
parameters: {
|
|
31
32
|
text: [
|
|
32
33
|
{ type: 'string', value: 'cols: ' },
|
|
@@ -50,7 +51,7 @@ const CONFIGURATION_OBJECT: Record<string, Configuration> = {
|
|
|
50
51
|
parameters: {},
|
|
51
52
|
children: [
|
|
52
53
|
{
|
|
53
|
-
type: '
|
|
54
|
+
type: 'span',
|
|
54
55
|
parameters: {
|
|
55
56
|
text: [
|
|
56
57
|
{ type: 'string', value: 'one: ' },
|
|
@@ -77,7 +78,7 @@ const CONFIGURATION_OBJECT: Record<string, Configuration> = {
|
|
|
77
78
|
parameters: {},
|
|
78
79
|
children: [
|
|
79
80
|
{
|
|
80
|
-
type: '
|
|
81
|
+
type: 'span',
|
|
81
82
|
parameters: {
|
|
82
83
|
text: [
|
|
83
84
|
{ type: 'string', value: 'other: ' },
|
|
@@ -98,7 +99,7 @@ const CONFIGURATION_OBJECT: Record<string, Configuration> = {
|
|
|
98
99
|
parameters: { limit: 4 },
|
|
99
100
|
children: [
|
|
100
101
|
{
|
|
101
|
-
type: '
|
|
102
|
+
type: 'span',
|
|
102
103
|
parameters: {
|
|
103
104
|
text: [
|
|
104
105
|
{ type: 'string', value: 'trending: ' },
|
|
@@ -117,6 +118,158 @@ const CONFIGURATION_OBJECT: Record<string, Configuration> = {
|
|
|
117
118
|
},
|
|
118
119
|
],
|
|
119
120
|
},
|
|
121
|
+
'category:audio': {
|
|
122
|
+
id: 'category:audio',
|
|
123
|
+
indexName: 'instant_search',
|
|
124
|
+
children: [
|
|
125
|
+
{
|
|
126
|
+
type: 'ais.configure',
|
|
127
|
+
parameters: {
|
|
128
|
+
hitsPerPage: 9,
|
|
129
|
+
filters: 'categories:"Audio"',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'columns',
|
|
134
|
+
children: [
|
|
135
|
+
[
|
|
136
|
+
{
|
|
137
|
+
type: 'ais.refinementList',
|
|
138
|
+
parameters: {
|
|
139
|
+
attribute: 'brand',
|
|
140
|
+
header: 'brand',
|
|
141
|
+
collapsed: true,
|
|
142
|
+
searchable: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'ais.refinementList',
|
|
147
|
+
parameters: {
|
|
148
|
+
attribute: 'type',
|
|
149
|
+
header: 'type',
|
|
150
|
+
collapsed: true,
|
|
151
|
+
searchable: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'ais.rangeInput',
|
|
156
|
+
parameters: {
|
|
157
|
+
attribute: 'price',
|
|
158
|
+
header: 'price',
|
|
159
|
+
collapsed: true,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'ais.toggleRefinement',
|
|
164
|
+
parameters: {
|
|
165
|
+
attribute: 'free_shipping',
|
|
166
|
+
header: 'free shipping',
|
|
167
|
+
collapsed: true,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
{
|
|
173
|
+
type: 'ais.hits',
|
|
174
|
+
parameters: {},
|
|
175
|
+
children: [
|
|
176
|
+
{
|
|
177
|
+
type: 'link',
|
|
178
|
+
parameters: { href: [{ type: 'attribute', path: ['url'] }] },
|
|
179
|
+
children: [
|
|
180
|
+
{
|
|
181
|
+
type: 'image',
|
|
182
|
+
parameters: {
|
|
183
|
+
src: [{ type: 'attribute', path: ['image'] }],
|
|
184
|
+
alt: [{ type: 'string', value: '' }],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: 'div',
|
|
189
|
+
parameters: {
|
|
190
|
+
class: [{ type: 'string', value: '__flex' }],
|
|
191
|
+
},
|
|
192
|
+
children: [
|
|
193
|
+
{
|
|
194
|
+
type: 'span',
|
|
195
|
+
parameters: {
|
|
196
|
+
text: [{ type: 'attribute', path: ['name'] }],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
type: 'span',
|
|
201
|
+
parameters: {
|
|
202
|
+
class: [{ type: 'string', value: '__bold' }],
|
|
203
|
+
text: [
|
|
204
|
+
{ type: 'string', value: '$' },
|
|
205
|
+
{ type: 'attribute', path: ['price'] },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
type: 'ais.pagination',
|
|
217
|
+
parameters: {
|
|
218
|
+
padding: 2,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: 'ais.trendingItems',
|
|
223
|
+
parameters: {
|
|
224
|
+
limit: 4,
|
|
225
|
+
facetName: 'categories',
|
|
226
|
+
facetValue: 'Audio',
|
|
227
|
+
},
|
|
228
|
+
children: [
|
|
229
|
+
{
|
|
230
|
+
type: 'link',
|
|
231
|
+
parameters: { href: [{ type: 'attribute', path: ['url'] }] },
|
|
232
|
+
children: [
|
|
233
|
+
{
|
|
234
|
+
type: 'image',
|
|
235
|
+
parameters: {
|
|
236
|
+
src: [{ type: 'attribute', path: ['image'] }],
|
|
237
|
+
alt: [{ type: 'string', value: '' }],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
type: 'div',
|
|
242
|
+
parameters: {
|
|
243
|
+
class: [{ type: 'string', value: '__flex' }],
|
|
244
|
+
},
|
|
245
|
+
children: [
|
|
246
|
+
{
|
|
247
|
+
type: 'span',
|
|
248
|
+
parameters: {
|
|
249
|
+
text: [{ type: 'attribute', path: ['name'] }],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: 'span',
|
|
254
|
+
parameters: {
|
|
255
|
+
class: [{ type: 'string', value: '__bold' }],
|
|
256
|
+
text: [
|
|
257
|
+
{ type: 'string', value: '$' },
|
|
258
|
+
{ type: 'attribute', path: ['price'] },
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
120
273
|
};
|
|
121
274
|
const CONFIGURATION_MAP = new Map<string, Configuration>(
|
|
122
275
|
Object.entries(CONFIGURATION_OBJECT)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function getSettings(): { appId: string; apiKey: string } {
|
|
2
|
+
const metaConfiguration = document.querySelector<HTMLMetaElement>(
|
|
3
|
+
'meta[name="instantsearch-configuration"]'
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
if (!metaConfiguration || !metaConfiguration.content) {
|
|
7
|
+
throw new Error('No meta tag found');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { appId, apiKey } = JSON.parse(metaConfiguration.content);
|
|
11
|
+
|
|
12
|
+
if (!appId || !apiKey) {
|
|
13
|
+
throw new Error('Missing appId or apiKey in the meta tag');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { appId, apiKey };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getElements() {
|
|
20
|
+
const elements = new Map<string, HTMLElement>();
|
|
21
|
+
document
|
|
22
|
+
.querySelectorAll<HTMLElement>('[data-instantsearch-id]')
|
|
23
|
+
.forEach((element) => {
|
|
24
|
+
const id = element.dataset.instantsearchId!;
|
|
25
|
+
elements.set(id, element);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return elements;
|
|
29
|
+
}
|
package/src/render.tsx
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { getPropertyByPath } from 'instantsearch.js/es/lib/utils';
|
|
3
|
+
import { index, panel } from 'instantsearch.js/es/widgets';
|
|
4
|
+
import { h, Fragment } from 'preact';
|
|
5
|
+
|
|
6
|
+
import { error } from './util';
|
|
7
|
+
import { widgets } from './widgets';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
Child,
|
|
11
|
+
Configuration,
|
|
12
|
+
PanelWidget,
|
|
13
|
+
PanelWidgetTypes,
|
|
14
|
+
TemplateAttribute,
|
|
15
|
+
TemplateChild,
|
|
16
|
+
TemplateText,
|
|
17
|
+
TemplateWidgetTypes,
|
|
18
|
+
} from './types';
|
|
19
|
+
import type { Widget } from 'instantsearch.js';
|
|
20
|
+
import type { ComponentChildren, JSX } from 'preact';
|
|
21
|
+
|
|
22
|
+
export function injectStyles() {
|
|
23
|
+
const style = document.createElement('style');
|
|
24
|
+
// @TODO: decide if this should be for all columns or only a specific type
|
|
25
|
+
style.textContent = `
|
|
26
|
+
.ais-Columns {
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-columns: minmax(min-content, 200px) 1fr;
|
|
29
|
+
gap: 1em;
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
document.head.appendChild(style);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function configToIndex(
|
|
36
|
+
config: Configuration,
|
|
37
|
+
elements: Map<string, HTMLElement>
|
|
38
|
+
) {
|
|
39
|
+
const container = elements.get(config.id);
|
|
40
|
+
if (!container) {
|
|
41
|
+
error(`Element with id ${config.id} not found`);
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return [
|
|
46
|
+
index({
|
|
47
|
+
indexName: config.indexName,
|
|
48
|
+
indexId: config.id,
|
|
49
|
+
}).addWidgets(
|
|
50
|
+
config.children.flatMap((child) => childToWidget(child, container))
|
|
51
|
+
),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const hitWidgets = new Set<TemplateWidgetTypes>([
|
|
56
|
+
'ais.hits',
|
|
57
|
+
'ais.infiniteHits',
|
|
58
|
+
'ais.frequentlyBoughtTogether',
|
|
59
|
+
'ais.lookingSimilar',
|
|
60
|
+
'ais.relatedProducts',
|
|
61
|
+
'ais.trendingItems',
|
|
62
|
+
]);
|
|
63
|
+
function isTemplateWidget(
|
|
64
|
+
child: Child
|
|
65
|
+
): child is Child & { children: TemplateChild[] } {
|
|
66
|
+
return hitWidgets.has(child.type as any);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const panelWidgets = new Set<PanelWidgetTypes>([
|
|
70
|
+
'ais.refinementList',
|
|
71
|
+
'ais.menu',
|
|
72
|
+
'ais.hierarchicalMenu',
|
|
73
|
+
'ais.breadcrumb',
|
|
74
|
+
'ais.numericMenu',
|
|
75
|
+
'ais.rangeInput',
|
|
76
|
+
'ais.rangeSlider',
|
|
77
|
+
'ais.ratingMenu',
|
|
78
|
+
'ais.toggleRefinement',
|
|
79
|
+
]);
|
|
80
|
+
function isPanelWidget(child: Child): child is PanelWidget {
|
|
81
|
+
return panelWidgets.has(child.type as any);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tagNames = new Map<string, string>(
|
|
85
|
+
Object.entries({
|
|
86
|
+
paragraph: 'p',
|
|
87
|
+
span: 'span',
|
|
88
|
+
h2: 'h2',
|
|
89
|
+
div: 'div',
|
|
90
|
+
link: 'a',
|
|
91
|
+
image: 'img',
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
function renderText(text: TemplateText[number], hit: any, components: any) {
|
|
96
|
+
if (text.type === 'string') {
|
|
97
|
+
return text.value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (text.type === 'attribute') {
|
|
101
|
+
return getPropertyByPath(hit, text.path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (text.type === 'highlight') {
|
|
105
|
+
return components.Highlight({
|
|
106
|
+
hit,
|
|
107
|
+
attribute: text.path,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (text.type === 'snippet') {
|
|
112
|
+
return components.Snippet({
|
|
113
|
+
hit,
|
|
114
|
+
attribute: text.path,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderAttribute(text: TemplateAttribute[number], hit: any) {
|
|
122
|
+
if (text.type === 'string') {
|
|
123
|
+
return text.value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (text.type === 'attribute') {
|
|
127
|
+
return getPropertyByPath(hit, text.path);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function childToWidget(child: Child, container: HTMLElement): Widget[] {
|
|
134
|
+
const widgetContainer = container.appendChild(document.createElement('div'));
|
|
135
|
+
|
|
136
|
+
if (child.type === 'columns') {
|
|
137
|
+
widgetContainer.classList.add('ais-Columns');
|
|
138
|
+
|
|
139
|
+
return child.children
|
|
140
|
+
.map((column) => {
|
|
141
|
+
const columnContainer = widgetContainer.appendChild(
|
|
142
|
+
Object.assign(document.createElement('div'), {
|
|
143
|
+
className: 'ais-Column',
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
return column.map((ch) => childToWidget(ch, columnContainer));
|
|
147
|
+
})
|
|
148
|
+
.flat(2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (child.type === 'ais.configure') {
|
|
152
|
+
return [widgets[child.type]({ ...child.parameters })];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (isTemplateWidget(child)) {
|
|
156
|
+
// type cast is needed here because the spread adding `container` and `templates` loses the type discriminant
|
|
157
|
+
const parameters = child.parameters as Parameters<
|
|
158
|
+
typeof widgets['ais.hits']
|
|
159
|
+
>[0];
|
|
160
|
+
const widget = widgets[child.type] as typeof widgets['ais.hits'];
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
widget({
|
|
164
|
+
...parameters,
|
|
165
|
+
container: widgetContainer,
|
|
166
|
+
templates: {
|
|
167
|
+
item: (hit: any, { components }) => {
|
|
168
|
+
if (!child.children.length) {
|
|
169
|
+
return <code> no item template given</code>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderChild(ch: TemplateChild) {
|
|
173
|
+
const Tag = tagNames.get(ch.type) as keyof JSX.IntrinsicElements;
|
|
174
|
+
if (!Tag) {
|
|
175
|
+
return <Fragment></Fragment>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let children: ComponentChildren = null;
|
|
179
|
+
if ('text' in ch.parameters) {
|
|
180
|
+
children = ch.parameters.text.map((text) =>
|
|
181
|
+
renderText(text, hit, components)
|
|
182
|
+
);
|
|
183
|
+
} else if ('children' in ch) {
|
|
184
|
+
children = ch.children.map(renderChild);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const attributes = Object.fromEntries(
|
|
188
|
+
Object.entries(ch.parameters)
|
|
189
|
+
.filter(
|
|
190
|
+
(tuple): tuple is [string, TemplateAttribute] =>
|
|
191
|
+
tuple[0] !== 'text'
|
|
192
|
+
)
|
|
193
|
+
.map(([key, value]) => [
|
|
194
|
+
key,
|
|
195
|
+
value.map((item) => renderAttribute(item, hit)).join(''),
|
|
196
|
+
])
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return <Tag {...attributes}>{children}</Tag>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return child.children.map(renderChild);
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isPanelWidget(child)) {
|
|
210
|
+
// type cast is needed here because the spread adding `container` loses the type discriminant
|
|
211
|
+
const {
|
|
212
|
+
header,
|
|
213
|
+
collapsed: defaultCollapsed,
|
|
214
|
+
...parameters
|
|
215
|
+
} = child.parameters as Parameters<
|
|
216
|
+
typeof widgets['ais.refinementList']
|
|
217
|
+
>[0] & { header: string; collapsed: boolean };
|
|
218
|
+
const widget = widgets[child.type] as typeof widgets['ais.refinementList'];
|
|
219
|
+
return [
|
|
220
|
+
panel<typeof widgets['ais.refinementList']>({
|
|
221
|
+
templates: {
|
|
222
|
+
header,
|
|
223
|
+
collapseButtonText: ({ collapsed }) => (
|
|
224
|
+
// @TODO: put this style in a stylesheet
|
|
225
|
+
<span style="cursor: pointer">{collapsed ? '+' : '-'}</span>
|
|
226
|
+
),
|
|
227
|
+
},
|
|
228
|
+
collapsed:
|
|
229
|
+
typeof defaultCollapsed === 'undefined'
|
|
230
|
+
? undefined
|
|
231
|
+
: () => defaultCollapsed,
|
|
232
|
+
})(widget)({
|
|
233
|
+
...parameters,
|
|
234
|
+
container: widgetContainer,
|
|
235
|
+
}),
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// type cast is needed here because the spread adding `container` loses the type discriminant
|
|
240
|
+
const parameters = child.parameters as Parameters<
|
|
241
|
+
typeof widgets['ais.pagination']
|
|
242
|
+
>[0];
|
|
243
|
+
const widget = widgets[child.type] as typeof widgets['ais.pagination'];
|
|
244
|
+
return [
|
|
245
|
+
widget({
|
|
246
|
+
...parameters,
|
|
247
|
+
container: widgetContainer,
|
|
248
|
+
}),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
+
/** @jsx h */
|
|
1
2
|
import algoliasearch from 'algoliasearch/lite';
|
|
2
3
|
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
|
|
3
|
-
import { getPropertyByPath } from 'instantsearch.js/es/lib/utils';
|
|
4
4
|
|
|
5
5
|
import { fakeFetchConfiguration } from './fake-configuration';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
Child,
|
|
10
|
-
Configuration,
|
|
11
|
-
TemplateChild,
|
|
12
|
-
TemplateText,
|
|
13
|
-
} from './types';
|
|
14
|
-
import type { Widget } from 'instantsearch.js';
|
|
15
|
-
|
|
16
|
-
// @TODO: hook up to some way it can be set runtime, maybe query params
|
|
17
|
-
const VERBOSE = true;
|
|
6
|
+
import { getElements, getSettings } from './get-information';
|
|
7
|
+
import { configToIndex, injectStyles } from './render';
|
|
8
|
+
import { error } from './util';
|
|
18
9
|
|
|
19
10
|
declare global {
|
|
20
11
|
interface Window {
|
|
@@ -47,213 +38,3 @@ export function setupInstantSearch() {
|
|
|
47
38
|
error((err as Error).message);
|
|
48
39
|
}
|
|
49
40
|
}
|
|
50
|
-
|
|
51
|
-
function injectStyles() {
|
|
52
|
-
const style = document.createElement('style');
|
|
53
|
-
style.textContent = `
|
|
54
|
-
.ais-Columns {
|
|
55
|
-
display: grid;
|
|
56
|
-
grid-template-columns: minmax(min-content, 200px) 1fr;
|
|
57
|
-
}
|
|
58
|
-
`;
|
|
59
|
-
document.head.appendChild(style);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function getSettings(): { appId: string; apiKey: string } {
|
|
63
|
-
const metaConfiguration = document.querySelector<HTMLMetaElement>(
|
|
64
|
-
'meta[name="instantsearch-configuration"]'
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
if (!metaConfiguration || !metaConfiguration.content) {
|
|
68
|
-
throw new Error('No meta tag found');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const { appId, apiKey } = JSON.parse(metaConfiguration.content);
|
|
72
|
-
|
|
73
|
-
if (!appId || !apiKey) {
|
|
74
|
-
throw new Error('Missing appId or apiKey in the meta tag');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { appId, apiKey };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function getElements() {
|
|
81
|
-
const elements = new Map<string, HTMLElement>();
|
|
82
|
-
document
|
|
83
|
-
.querySelectorAll<HTMLElement>('[data-instantsearch-id]')
|
|
84
|
-
.forEach((element) => {
|
|
85
|
-
const id = element.dataset.instantsearchId!;
|
|
86
|
-
elements.set(id, element);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
return elements;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function configToIndex(
|
|
93
|
-
config: Configuration,
|
|
94
|
-
elements: Map<string, HTMLElement>
|
|
95
|
-
) {
|
|
96
|
-
const container = elements.get(config.id);
|
|
97
|
-
if (!container) {
|
|
98
|
-
error(`Element with id ${config.id} not found`);
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return [
|
|
103
|
-
widgets['ais.index']({
|
|
104
|
-
indexName: config.indexName,
|
|
105
|
-
indexId: config.id,
|
|
106
|
-
}).addWidgets(
|
|
107
|
-
config.children.flatMap((child) => childToWidget(child, container))
|
|
108
|
-
),
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const hitWidgets = new Set([
|
|
113
|
-
'ais.hits',
|
|
114
|
-
'ais.infiniteHits',
|
|
115
|
-
'ais.frequentlyBoughtTogether',
|
|
116
|
-
'ais.lookingSimilar',
|
|
117
|
-
'ais.relatedProducts',
|
|
118
|
-
'ais.trendingItems',
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
function isTemplateWidget(
|
|
122
|
-
child: Child
|
|
123
|
-
): child is Child & { children: TemplateChild[] } {
|
|
124
|
-
return hitWidgets.has(child.type);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const textChildrenObject = {
|
|
128
|
-
paragraph: 'p',
|
|
129
|
-
div: 'div',
|
|
130
|
-
span: 'span',
|
|
131
|
-
h2: 'h2',
|
|
132
|
-
};
|
|
133
|
-
const textChildren = new Map(Object.entries(textChildrenObject));
|
|
134
|
-
|
|
135
|
-
type TextChildType = keyof typeof textChildrenObject;
|
|
136
|
-
function isTextChild(child: TemplateChild): child is TemplateChild & {
|
|
137
|
-
type: TextChildType;
|
|
138
|
-
} {
|
|
139
|
-
return textChildren.has(child.type as any);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function renderText(text: TemplateText[number], hit: any, components: any) {
|
|
143
|
-
if (text.type === 'string') {
|
|
144
|
-
return text.value;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (text.type === 'attribute') {
|
|
148
|
-
return getPropertyByPath(hit, text.path);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (text.type === 'highlight') {
|
|
152
|
-
return components.Highlight({
|
|
153
|
-
hit,
|
|
154
|
-
attribute: text.path,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (text.type === 'snippet') {
|
|
159
|
-
return components.Snippet({
|
|
160
|
-
hit,
|
|
161
|
-
attribute: text.path,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function renderAttribute(text: TemplateText[number], hit: any) {
|
|
169
|
-
if (text.type === 'string') {
|
|
170
|
-
return text.value;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (text.type === 'attribute') {
|
|
174
|
-
return getPropertyByPath(hit, text.path);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function childToWidget(child: Child, container: HTMLElement): Widget[] {
|
|
181
|
-
const widgetContainer = container.appendChild(document.createElement('div'));
|
|
182
|
-
|
|
183
|
-
if (child.type === 'columns') {
|
|
184
|
-
widgetContainer.classList.add('ais-Columns');
|
|
185
|
-
|
|
186
|
-
return child.children
|
|
187
|
-
.map((column) => column.map((ch) => childToWidget(ch, widgetContainer)))
|
|
188
|
-
.flat(2);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (child.type === 'ais.configure') {
|
|
192
|
-
return [widgets[child.type]({ ...child.parameters })];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (isTemplateWidget(child)) {
|
|
196
|
-
// type cast is needed here because the spread adding `container` and `templates` loses the type discriminant
|
|
197
|
-
const parameters = child.parameters as Parameters<
|
|
198
|
-
typeof widgets['ais.hits']
|
|
199
|
-
>[0];
|
|
200
|
-
const widget = widgets[child.type] as typeof widgets['ais.hits'];
|
|
201
|
-
|
|
202
|
-
return [
|
|
203
|
-
widget({
|
|
204
|
-
...parameters,
|
|
205
|
-
container: widgetContainer,
|
|
206
|
-
templates: {
|
|
207
|
-
item: (hit: any, { html, components }) => {
|
|
208
|
-
if (!child.children.length) {
|
|
209
|
-
return html`<code> no item template given</code>`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return child.children.map((ch) => {
|
|
213
|
-
if (isTextChild(ch)) {
|
|
214
|
-
const Tag = textChildren.get(ch.type)!;
|
|
215
|
-
return html`<${Tag}>
|
|
216
|
-
${ch.parameters.text.map((text) =>
|
|
217
|
-
renderText(text, hit, components)
|
|
218
|
-
)}
|
|
219
|
-
</${Tag}>`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (ch.type === 'image') {
|
|
223
|
-
return html`<img
|
|
224
|
-
src="${ch.parameters.src
|
|
225
|
-
.map((src) => renderAttribute(src, hit))
|
|
226
|
-
.join('')}"
|
|
227
|
-
alt="${ch.parameters.alt
|
|
228
|
-
.map((alt) => renderAttribute(alt, hit))
|
|
229
|
-
.join('')}"
|
|
230
|
-
/>`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return html``;
|
|
234
|
-
});
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
}),
|
|
238
|
-
];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// type cast is needed here because the spread adding `container` loses the type discriminant
|
|
242
|
-
const parameters = child.parameters as Parameters<
|
|
243
|
-
typeof widgets['ais.pagination']
|
|
244
|
-
>[0];
|
|
245
|
-
const widget = widgets[child.type] as typeof widgets['ais.pagination'];
|
|
246
|
-
return [
|
|
247
|
-
widget({
|
|
248
|
-
...parameters,
|
|
249
|
-
container: widgetContainer,
|
|
250
|
-
}),
|
|
251
|
-
];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function error(message: string) {
|
|
255
|
-
if (VERBOSE) {
|
|
256
|
-
// eslint-disable-next-line no-console
|
|
257
|
-
console.error(`[InstantSearch] ${message}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
package/src/types.ts
CHANGED
|
@@ -5,42 +5,92 @@ type Attribute = { type: 'attribute'; path: string[] };
|
|
|
5
5
|
type Highlight = { type: 'highlight' | 'snippet'; path: string[] };
|
|
6
6
|
export type TemplateText = Array<Attribute | StaticString | Highlight>;
|
|
7
7
|
export type TemplateAttribute = Array<Attribute | StaticString>;
|
|
8
|
+
type RegularParameters = {
|
|
9
|
+
class?: TemplateAttribute;
|
|
10
|
+
};
|
|
8
11
|
export type TemplateChild =
|
|
9
12
|
| {
|
|
10
|
-
type: 'paragraph' | '
|
|
13
|
+
type: 'paragraph' | 'span' | 'h2';
|
|
11
14
|
parameters: {
|
|
12
15
|
text: TemplateText;
|
|
13
|
-
};
|
|
16
|
+
} & RegularParameters;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
type: 'div';
|
|
20
|
+
parameters: RegularParameters;
|
|
21
|
+
children: TemplateChild[];
|
|
14
22
|
}
|
|
15
23
|
| {
|
|
16
24
|
type: 'image';
|
|
17
25
|
parameters: {
|
|
18
26
|
src: TemplateAttribute;
|
|
19
27
|
alt: TemplateAttribute;
|
|
20
|
-
};
|
|
28
|
+
} & RegularParameters;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
type: 'link';
|
|
32
|
+
parameters: {
|
|
33
|
+
href: TemplateAttribute;
|
|
34
|
+
} & RegularParameters;
|
|
35
|
+
children: TemplateChild[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type TemplateWidgetTypes =
|
|
39
|
+
| 'ais.hits'
|
|
40
|
+
| 'ais.infiniteHits'
|
|
41
|
+
| 'ais.frequentlyBoughtTogether'
|
|
42
|
+
| 'ais.lookingSimilar'
|
|
43
|
+
| 'ais.relatedProducts'
|
|
44
|
+
| 'ais.trendingItems';
|
|
45
|
+
|
|
46
|
+
export type TemplateWidget<
|
|
47
|
+
TKeys extends TemplateWidgetTypes = TemplateWidgetTypes
|
|
48
|
+
> = {
|
|
49
|
+
[key in TKeys]: {
|
|
50
|
+
type: key;
|
|
51
|
+
parameters: Omit<
|
|
52
|
+
Parameters<typeof widgets[key]>[0],
|
|
53
|
+
'container' | 'templates'
|
|
54
|
+
>;
|
|
55
|
+
children: TemplateChild[];
|
|
56
|
+
};
|
|
57
|
+
}[TKeys];
|
|
58
|
+
|
|
59
|
+
export type PanelWidgetTypes =
|
|
60
|
+
| 'ais.refinementList'
|
|
61
|
+
| 'ais.menu'
|
|
62
|
+
| 'ais.hierarchicalMenu'
|
|
63
|
+
| 'ais.breadcrumb'
|
|
64
|
+
| 'ais.numericMenu'
|
|
65
|
+
| 'ais.rangeInput'
|
|
66
|
+
| 'ais.rangeSlider'
|
|
67
|
+
| 'ais.ratingMenu'
|
|
68
|
+
| 'ais.toggleRefinement';
|
|
69
|
+
export type PanelWidget<TKeys extends PanelWidgetTypes = PanelWidgetTypes> = {
|
|
70
|
+
[key in TKeys]: {
|
|
71
|
+
type: key;
|
|
72
|
+
parameters: Omit<Parameters<typeof widgets[key]>[0], 'container'> & {
|
|
73
|
+
header?: string;
|
|
74
|
+
collapsed?: boolean;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}[TKeys];
|
|
78
|
+
|
|
79
|
+
type RegularWidget<TKeys extends keyof typeof widgets = keyof typeof widgets> =
|
|
80
|
+
{
|
|
81
|
+
[key in TKeys]: {
|
|
82
|
+
type: key;
|
|
83
|
+
parameters: Omit<Parameters<typeof widgets[key]>[0], 'container'>;
|
|
21
84
|
};
|
|
85
|
+
}[TKeys];
|
|
22
86
|
|
|
23
87
|
export type Child =
|
|
24
88
|
| {
|
|
25
|
-
[key in keyof typeof widgets]: key extends
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
| 'ais.relatedProducts'
|
|
31
|
-
| 'ais.trendingItems'
|
|
32
|
-
? {
|
|
33
|
-
type: key;
|
|
34
|
-
parameters: Omit<
|
|
35
|
-
Parameters<typeof widgets[key]>[0],
|
|
36
|
-
'container' | 'templates'
|
|
37
|
-
>;
|
|
38
|
-
children: TemplateChild[];
|
|
39
|
-
}
|
|
40
|
-
: {
|
|
41
|
-
type: key;
|
|
42
|
-
parameters: Omit<Parameters<typeof widgets[key]>[0], 'container'>;
|
|
43
|
-
};
|
|
89
|
+
[key in keyof typeof widgets]: key extends TemplateWidgetTypes
|
|
90
|
+
? TemplateWidget<key>
|
|
91
|
+
: key extends PanelWidgetTypes
|
|
92
|
+
? PanelWidget<key>
|
|
93
|
+
: RegularWidget<key>;
|
|
44
94
|
}[keyof typeof widgets]
|
|
45
95
|
| {
|
|
46
96
|
type: 'columns';
|
package/src/util.ts
ADDED
package/src/widgets.ts
CHANGED
|
@@ -7,13 +7,11 @@ import {
|
|
|
7
7
|
hierarchicalMenu,
|
|
8
8
|
hits,
|
|
9
9
|
hitsPerPage,
|
|
10
|
-
index,
|
|
11
10
|
infiniteHits,
|
|
12
11
|
lookingSimilar,
|
|
13
12
|
menu,
|
|
14
13
|
numericMenu,
|
|
15
14
|
pagination,
|
|
16
|
-
panel,
|
|
17
15
|
rangeInput,
|
|
18
16
|
rangeSlider,
|
|
19
17
|
ratingMenu,
|
|
@@ -35,13 +33,11 @@ export const widgets = {
|
|
|
35
33
|
'ais.hierarchicalMenu': hierarchicalMenu,
|
|
36
34
|
'ais.hits': hits,
|
|
37
35
|
'ais.hitsPerPage': hitsPerPage,
|
|
38
|
-
'ais.index': index,
|
|
39
36
|
'ais.infiniteHits': infiniteHits,
|
|
40
37
|
'ais.lookingSimilar': lookingSimilar,
|
|
41
38
|
'ais.menu': menu,
|
|
42
39
|
'ais.numericMenu': numericMenu,
|
|
43
40
|
'ais.pagination': pagination,
|
|
44
|
-
'ais.panel': panel,
|
|
45
41
|
'ais.rangeInput': rangeInput,
|
|
46
42
|
'ais.rangeSlider': rangeSlider,
|
|
47
43
|
'ais.ratingMenu': ratingMenu,
|