lucos_search_component 0.0.17 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5820,3 +5820,157 @@ class LucosSearchComponent extends HTMLSpanElement {
5820
5820
  }
5821
5821
  }
5822
5822
  customElements.define('lucos-search', LucosSearchComponent, { extends: "span" });
5823
+
5824
+ class LucosLangComponent extends HTMLSpanElement {
5825
+ static get observedAttributes() {
5826
+ return ['data-api-key'];
5827
+ }
5828
+ constructor() {
5829
+ super();
5830
+ const component = this;
5831
+ const shadow = component.attachShadow({mode: 'open'});
5832
+
5833
+ {
5834
+ const tomStyle = document.createElement('style');
5835
+ tomStyle.textContent = tomSelectStylesheet;
5836
+ shadow.appendChild(tomStyle);
5837
+ }
5838
+
5839
+ const mainStyle = document.createElement('style');
5840
+ mainStyle.textContent = `
5841
+ .lozenge {
5842
+ align-items: center;
5843
+ vertical-align: baseline;
5844
+ border-radius: 3px;
5845
+ background-repeat: repeat-x;
5846
+ border-style: solid;
5847
+ border-width: 1px;
5848
+ text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);
5849
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 1px rgba(255, 255, 255, 0.03);
5850
+
5851
+ /** Make the colour settings !important so they override the tom-select default style **/
5852
+ background-image: linear-gradient(to bottom, #ffffff63, #24232347) !important;
5853
+ background-color: var(--lozenge-background) !important;
5854
+ border-color: var(--lozenge-border) !important;
5855
+ color: var(--lozenge-text) !important;
5856
+ }
5857
+ .lozenge .remove {
5858
+ border-left-color: var(--lozenge-border) !important;
5859
+ }
5860
+ .lozenge {
5861
+ --lozenge-background: #8affe7;
5862
+ --lozenge-border: #068900;
5863
+ --lozenge-text: #000000;
5864
+ }
5865
+ .ts-dropdown {
5866
+ margin: 0;
5867
+ }
5868
+ .lozenge a {
5869
+ color: inherit;
5870
+ text-decoration: none;
5871
+ }
5872
+ .optgroup-header {
5873
+ text-transform: capitalize;
5874
+ }
5875
+ `;
5876
+ shadow.appendChild(mainStyle);
5877
+
5878
+
5879
+ const selector = component.querySelector("select");
5880
+ if (!selector) throw new Error("Can't find select element in lucos-lang");
5881
+ selector.setAttribute("multiple", "multiple");
5882
+ new TomSelect(selector, {
5883
+ valueField: 'code',
5884
+ labelField: 'label',
5885
+ searchField: ['code','label'],
5886
+ optgroupField: 'family',
5887
+ closeAfterSelect: true,
5888
+ plugins: {
5889
+ remove_button:{
5890
+ title:'Remove this language',
5891
+ },
5892
+ drag_drop: {},
5893
+ },
5894
+ onItemAdd: function() { // Workaround until https://github.com/orchidjs/tom-select/pull/945 is merged/released
5895
+ this.setTextboxValue('');
5896
+ this.refreshOptions();
5897
+ },
5898
+ // On startup, update any existing options with latest data from search
5899
+ onInitialize: async function() {
5900
+ const families = await component.getLanguageFamilies();
5901
+ //this.clearOptionGroups();
5902
+ families.forEach(family => {
5903
+ this.addOptionGroup(family.code, family);
5904
+ });
5905
+ const languages = await component.getLanguages();
5906
+ languages.forEach(language => {
5907
+ this.updateOption(language.code, language); // Updates any existing options which are selected with the correct label
5908
+ this.addOption(language); // Makes the option available for new selections
5909
+ });
5910
+ },
5911
+ onItemSelect: function (item) {
5912
+ // Tom-select prevents clicking on link in an item to work as normal, so force it here
5913
+ window.open(item.dataset.url, '_blank').focus();
5914
+ },
5915
+ render:{
5916
+ item: function(data, escape) {
5917
+ return `<div class="lozenge" data-url="${escape(data.url)}"><a href="${escape(data.url)}" target="_blank">${escape(data.label)}</a></div>`;
5918
+ },
5919
+ },
5920
+ });
5921
+ if (selector.nextElementSibling) {
5922
+ shadow.append(selector.nextElementSibling);
5923
+ }
5924
+ }
5925
+ async searchRequest(searchParams) {
5926
+ const key = this.getAttribute("data-api-key");
5927
+ if (!key) throw new Error("No `data-api-key` attribute set on `lucos-search` component");
5928
+ const response = await fetch("https://arachne.l42.eu/search?"+searchParams.toString(), {
5929
+ headers: { 'X-TYPESENSE-API-KEY': key },
5930
+ signal: AbortSignal.timeout(900),
5931
+ });
5932
+ const data = await response.json();
5933
+ if (!response.ok) {
5934
+ throw new Error(`Recieved ${response.status} error from search endpoint: ${data["message"]}`);
5935
+ }
5936
+ return data;
5937
+ }
5938
+ async getLanguages() {
5939
+ const searchParams = new URLSearchParams({
5940
+ filter_by: 'type:=Language',
5941
+ query_by: "pref_label",
5942
+ include_fields: "id,pref_label,lang_family",
5943
+ sort_by: "pref_label:asc",
5944
+ enable_highlight_v1: false,
5945
+ per_page: 200,
5946
+ });
5947
+ const data = await this.searchRequest(searchParams);
5948
+ return data.hits.map(result => {
5949
+ return {
5950
+ code: result.document.id.split("/").reverse()[1],
5951
+ label: result.document.pref_label,
5952
+ url: result.document.id,
5953
+ family: result.document.lang_family || 'qli', // If no language family is given, using `qli` code as language isolate
5954
+ }
5955
+ });
5956
+ }
5957
+ async getLanguageFamilies() {
5958
+ const searchParams = new URLSearchParams({
5959
+ filter_by: 'type:=Language Family',
5960
+ query_by: "pref_label",
5961
+ include_fields: "id,pref_label",
5962
+ sort_by: "pref_label:asc",
5963
+ enable_highlight_v1: false,
5964
+ per_page: 200,
5965
+ });
5966
+ const data = await this.searchRequest(searchParams);
5967
+ return data.hits.map(result => {
5968
+ return {
5969
+ code: result.document.id.split("/").pop() || 'qli',
5970
+ label: result.document.pref_label,
5971
+ url: result.document.id,
5972
+ }
5973
+ });
5974
+ }
5975
+ }
5976
+ customElements.define('lucos-lang', LucosLangComponent, { extends: "span" });
@@ -3,6 +3,7 @@
3
3
  <title>Hello World Search Component</title>
4
4
  </head>
5
5
  <body>
6
+ <h1>lucos-search</h1>
6
7
  <label for="search1">Everything:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}"><select id="search1"></select></span>
7
8
  <label for="search2">No Tracks:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-exclude_types="Track"><select id="search2"></select></span>
8
9
  <label for="search3">Only Cites and Rivers:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="City,River"><select id="search3"></select></span>
@@ -27,6 +28,13 @@
27
28
  <option value="https://eolas.l42.eu/metadata/place/316/" selected>https://eolas.l42.eu/metadata/place/316/</option>
28
29
  <option value="https://eolas.l42.eu/metadata/place/317/" selected>https://eolas.l42.eu/metadata/place/317/</option>
29
30
  </select></span>
31
+ <h1>lucos-lang</h1>
32
+ <label for="lang1">Blank:</label><span is="lucos-lang" data-api-key="${KEY_LUCOS_ARACHNE}"><select id="lang1"></select></span>
33
+ <label for="lang2">Load with existing:</label><span is="lucos-lang" data-api-key="${KEY_LUCOS_ARACHNE}"><select id="lang2" multiple>
34
+ <option id="ga" selected>ga</option>
35
+ <option id="emen" selected>emen</option>
36
+ <option id="art-x-simlish" selected>art-x-simlish</option>
37
+ </select></span>
30
38
  <script src="./built.js"></script>
31
39
  </body>
32
40
  </html>
package/index.js CHANGED
@@ -1,224 +1,2 @@
1
- import TomSelect from 'tom-select';
2
- import tomSelectStylesheet from 'tom-select/dist/css/tom-select.default.css';
3
-
4
- class LucosSearchComponent extends HTMLSpanElement {
5
- static get observedAttributes() {
6
- return ['data-api-key','data-types','data-exclude-types'];
7
- }
8
- constructor() {
9
- super();
10
- const component = this;
11
- const shadow = component.attachShadow({mode: 'open'});
12
-
13
- if (tomSelectStylesheet) {
14
- const tomStyle = document.createElement('style');
15
- tomStyle.textContent = tomSelectStylesheet;
16
- shadow.appendChild(tomStyle);
17
- }
18
-
19
- const mainStyle = document.createElement('style');
20
- mainStyle.textContent = `
21
- .lozenge {
22
- align-items: center;
23
- vertical-align: baseline;
24
- border-radius: 3px;
25
- background-repeat: repeat-x;
26
- border-style: solid;
27
- border-width: 1px;
28
- text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);
29
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 1px rgba(255, 255, 255, 0.03);
30
-
31
- /** Make the colour settings !important so they override the tom-select default style **/
32
- background-image: linear-gradient(to bottom, #ffffff63, #24232347) !important;
33
- background-color: var(--lozenge-background) !important;
34
- border-color: var(--lozenge-border) !important;
35
- color: var(--lozenge-text) !important;
36
- }
37
- .lozenge .remove {
38
- border-left-color: var(--lozenge-border) !important;
39
- }
40
-
41
- /* Default colour to greys, but override based on type */
42
- .lozenge {
43
- --lozenge-background: #555;
44
- --lozenge-border: #6d6d6d;
45
- --lozenge-text: #fff;
46
- }
47
- /* Items from lucos_eolas have many types. For now, count any type which isn't specified later as part of eolas. */
48
- .lozenge[data-type] {
49
- --lozenge-background: #6a00c2;
50
- --lozenge-border: #44265d;
51
- }
52
- .lozenge[data-type="Track"] {
53
- --lozenge-background: #000060;
54
- --lozenge-border: #000020;
55
- }
56
- .lozenge[data-type="Person"] {
57
- --lozenge-background: #044E00;
58
- --lozenge-border: #033100;
59
- }
60
- /** Aquatic Places **/
61
- .lozenge[data-type="Ocean"], .lozenge[data-type="Sea"], .lozenge[data-type="Sea Inlet"], .lozenge[data-type="River"], .lozenge[data-type="Lake"] {
62
- --lozenge-background: #0085fe;
63
- --lozenge-border: #0036b1;
64
- }
65
- /** Terrestrial Places **/
66
- .lozenge[data-type="Archipelago"], .lozenge[data-type="Area Of Outstanding Natural Beauty"], .lozenge[data-type="Continent"], .lozenge[data-type="Historical Site"], .lozenge[data-type="Island"], .lozenge[data-type="Mountain"] {
67
- --lozenge-background: #652c17;
68
- --lozenge-border: #321200;
69
- }
70
- /** Cosmic Places **/
71
- .lozenge[data-type="Galaxy"], .lozenge[data-type="Planetary System"], .lozenge[data-type="Star"], .lozenge[data-type="Planet"], .lozenge[data-type="Natural Satellite"] {
72
- --lozenge-background: #15163a;
73
- --lozenge-border: #000000;
74
- --lozenge-text: #feffe8;
75
- }
76
- /** Human Places **/
77
- .lozenge[data-type="Airport"], .lozenge[data-type="Autonomous Area"], .lozenge[data-type="City"], .lozenge[data-type="Country"], .lozenge[data-type="County"], .lozenge[data-type="Dependent Territory"], .lozenge[data-type="Historical Site"], .lozenge[data-type="Neighbourhood"], .lozenge[data-type="Province"], .lozenge[data-type="Region"], .lozenge[data-type="Road"], .lozenge[data-type="State"], .lozenge[data-type="Town"], .lozenge[data-type="Tribal Nation"], .lozenge[data-type="Village"] {
78
- --lozenge-background: #aed0db;
79
- --lozenge-border: #3f6674;
80
- --lozenge-text: #0c1a1b;
81
- }
82
- /** Supernatural Places **/
83
- .lozenge[data-type="Supernatural Realm"] {
84
- --lozenge-background: #f1ff5f;
85
- --lozenge-border: #674800;
86
- --lozenge-text: #352005;
87
- }
88
- .lozenge[data-type="Historical Event"] {
89
- --lozenge-background: #740909;
90
- --lozenge-border: #470202;
91
- }
92
- .lozenge[data-type="Number"] {
93
- --lozenge-background: #0000ff;
94
- --lozenge-border: #000083;
95
- }
96
- /** Temporal Types **/
97
- .lozenge[data-type="Calendar"], .lozenge[data-type="Festival"], .lozenge[data-type="Month of Year"], .lozenge[data-type="Day of Week"] {
98
- --lozenge-background: #fffc33;
99
- --lozenge-border: #7f7e00;
100
- --lozenge-text: #0f0f00;
101
- }
102
-
103
-
104
- .lozenge.active {
105
- --lozenge-border: #b00;
106
- }
107
- .type {
108
- margin: 0 3px;
109
- padding: 2px 6px;
110
- }
111
- .ts-dropdown {
112
- margin: 0;
113
- }
114
- .lozenge a {
115
- color: inherit;
116
- text-decoration: none;
117
- }
118
- `;
119
- shadow.appendChild(mainStyle);
120
-
121
-
122
- const selector = component.querySelector("select");
123
- if (!selector) throw new Error("Can't find select element in lucos-search");
124
- selector.setAttribute("multiple", "multiple");
125
- new TomSelect(selector, {
126
- valueField: 'id',
127
- labelField: 'pref_label',
128
- searchField: [],
129
- closeAfterSelect: true,
130
- highlight: false, // Will use typesense's hightlight (as it can consider other fields)
131
- load: async function(query, callback) {
132
- const queryParams = new URLSearchParams({
133
- q: query,
134
- });
135
- if (component.getAttribute("data-types")) {
136
- queryParams.set("filter_by",`type:[${component.getAttribute("data-types")}]`);
137
- } else if (component.getAttribute("data-exclude_types")) {
138
- queryParams.set("filter_by",`type:![${component.getAttribute("data-exclude_types")}]`);
139
- }
140
- const results = await component.searchRequest(queryParams);
141
- this.clearOptions();
142
- callback(results);
143
- },
144
- plugins: {
145
- remove_button:{
146
- title:'Remove this item',
147
- },
148
- drag_drop: {},
149
- },
150
- onItemAdd: function() { // Workaround until https://github.com/orchidjs/tom-select/pull/945 is merged/released
151
- this.setTextboxValue('');
152
- this.refreshOptions();
153
- },
154
- onFocus: function() {
155
- this.clearOptions();
156
- },
157
- // On startup, update any existing options with latest data from search
158
- onInitialize: async function() {
159
- const ids = Object.keys(this.options);
160
- if (ids.length < 1) return;
161
- const searchParams = new URLSearchParams({
162
- q: '*',
163
- filter_by: `id:[${ids.join(",")}]`,
164
- per_page: ids.length,
165
- });
166
- const results = await component.searchRequest(searchParams);
167
- results.forEach(result => {
168
- this.updateOption(result.id, result);
169
- });
170
- },
171
- onItemSelect: function (item) {
172
- // Tom-select prevents clicking on link in an item to work as normal, so force it here
173
- window.open(item.dataset.value, '_blank').focus();
174
- },
175
- render:{
176
- option: function(data, escape) {
177
- let label = escape(data.pref_label);
178
- let alt_label = "";
179
- if (data.highlight.pref_label) {
180
- label = data.highlight.pref_label.snippet;
181
- } else if (data.highlight.labels) {
182
- const matched_label = data.highlight.labels.find(l => l.matched_tokens.length > 0);
183
- if (matched_label) {
184
- alt_label = ` <span class="alt-label">(${matched_label.snippet})</span>`;
185
- }
186
- }
187
- label = label.replace(` (${data.type})`,""); // No need to include any type disambiguation in label, as types are always shown
188
- return `<div>${label}${alt_label}<span class="type lozenge" data-type="${escape(data.type)}">${escape(data.type)}</span></div>`;
189
- },
190
- item: function(data, escape) {
191
- return `<div class="lozenge" data-type="${escape(data.type)}"><a href="${data.id}" target="_blank">${escape(data.pref_label)}</a></div>`;
192
- },
193
- },
194
- });
195
- if (selector.nextElementSibling) {
196
- shadow.append(selector.nextElementSibling);
197
- }
198
- }
199
- async searchRequest(searchParams) {
200
- const key = this.getAttribute("data-api-key");
201
- if (!key) throw new Error("No `data-api-key` attribute set on `lucos-search` component");
202
- searchParams.set('query_by', "pref_label,labels,description,lyrics");
203
- searchParams.set('query_by_weights', "10,8,3,1");
204
- searchParams.set('sort_by', "_text_match:desc,pref_label:asc");
205
- searchParams.set('prioritize_num_matching_fields', false);
206
- searchParams.set('include_fields', "id,pref_label,type,labels");
207
- searchParams.set('enable_highlight_v1', false);
208
- searchParams.set('highlight_start_tag', '<span class="highlight">')
209
- searchParams.set('highlight_end_tag', '</span>');
210
- const response = await fetch("https://arachne.l42.eu/search?"+searchParams.toString(), {
211
- headers: { 'X-TYPESENSE-API-KEY': key },
212
- signal: AbortSignal.timeout(900),
213
- });
214
- const data = await response.json();
215
- if (!response.ok) {
216
- throw new Error(`Recieved ${response.status} error from search endpoint: ${data["message"]}`);
217
- }
218
- const results = data.hits.map(result => {
219
- return {...result, ...result.document}
220
- });
221
- return results;
222
- }
223
- }
224
- customElements.define('lucos-search', LucosSearchComponent, { extends: "span" });
1
+ import './web-components/lucos-search.js';
2
+ import './web-components/lucos-lang.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lucos_search_component",
3
- "version": "0.0.17",
4
- "description": "Web Component for searching lucOS data",
3
+ "version": "0.1.2",
4
+ "description": "Web Components for searching lucOS data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "scripts": {
@@ -0,0 +1,156 @@
1
+ import TomSelect from 'tom-select';
2
+ import tomSelectStylesheet from 'tom-select/dist/css/tom-select.default.css';
3
+
4
+ class LucosLangComponent extends HTMLSpanElement {
5
+ static get observedAttributes() {
6
+ return ['data-api-key'];
7
+ }
8
+ constructor() {
9
+ super();
10
+ const component = this;
11
+ const shadow = component.attachShadow({mode: 'open'});
12
+
13
+ if (tomSelectStylesheet) {
14
+ const tomStyle = document.createElement('style');
15
+ tomStyle.textContent = tomSelectStylesheet;
16
+ shadow.appendChild(tomStyle);
17
+ }
18
+
19
+ const mainStyle = document.createElement('style');
20
+ mainStyle.textContent = `
21
+ .lozenge {
22
+ align-items: center;
23
+ vertical-align: baseline;
24
+ border-radius: 3px;
25
+ background-repeat: repeat-x;
26
+ border-style: solid;
27
+ border-width: 1px;
28
+ text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);
29
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 1px rgba(255, 255, 255, 0.03);
30
+
31
+ /** Make the colour settings !important so they override the tom-select default style **/
32
+ background-image: linear-gradient(to bottom, #ffffff63, #24232347) !important;
33
+ background-color: var(--lozenge-background) !important;
34
+ border-color: var(--lozenge-border) !important;
35
+ color: var(--lozenge-text) !important;
36
+ }
37
+ .lozenge .remove {
38
+ border-left-color: var(--lozenge-border) !important;
39
+ }
40
+ .lozenge {
41
+ --lozenge-background: #8affe7;
42
+ --lozenge-border: #068900;
43
+ --lozenge-text: #000000;
44
+ }
45
+ .ts-dropdown {
46
+ margin: 0;
47
+ }
48
+ .lozenge a {
49
+ color: inherit;
50
+ text-decoration: none;
51
+ }
52
+ .optgroup-header {
53
+ text-transform: capitalize;
54
+ }
55
+ `;
56
+ shadow.appendChild(mainStyle);
57
+
58
+
59
+ const selector = component.querySelector("select");
60
+ if (!selector) throw new Error("Can't find select element in lucos-lang");
61
+ selector.setAttribute("multiple", "multiple");
62
+ new TomSelect(selector, {
63
+ valueField: 'code',
64
+ labelField: 'label',
65
+ searchField: ['code','label'],
66
+ optgroupField: 'family',
67
+ closeAfterSelect: true,
68
+ plugins: {
69
+ remove_button:{
70
+ title:'Remove this language',
71
+ },
72
+ drag_drop: {},
73
+ },
74
+ onItemAdd: function() { // Workaround until https://github.com/orchidjs/tom-select/pull/945 is merged/released
75
+ this.setTextboxValue('');
76
+ this.refreshOptions();
77
+ },
78
+ // On startup, update any existing options with latest data from search
79
+ onInitialize: async function() {
80
+ const families = await component.getLanguageFamilies();
81
+ //this.clearOptionGroups();
82
+ families.forEach(family => {
83
+ this.addOptionGroup(family.code, family);
84
+ });
85
+ const languages = await component.getLanguages();
86
+ languages.forEach(language => {
87
+ this.updateOption(language.code, language); // Updates any existing options which are selected with the correct label
88
+ this.addOption(language); // Makes the option available for new selections
89
+ });
90
+ },
91
+ onItemSelect: function (item) {
92
+ // Tom-select prevents clicking on link in an item to work as normal, so force it here
93
+ window.open(item.dataset.url, '_blank').focus();
94
+ },
95
+ render:{
96
+ item: function(data, escape) {
97
+ return `<div class="lozenge" data-url="${escape(data.url)}"><a href="${escape(data.url)}" target="_blank">${escape(data.label)}</a></div>`;
98
+ },
99
+ },
100
+ });
101
+ if (selector.nextElementSibling) {
102
+ shadow.append(selector.nextElementSibling);
103
+ }
104
+ }
105
+ async searchRequest(searchParams) {
106
+ const key = this.getAttribute("data-api-key");
107
+ if (!key) throw new Error("No `data-api-key` attribute set on `lucos-search` component");
108
+ const response = await fetch("https://arachne.l42.eu/search?"+searchParams.toString(), {
109
+ headers: { 'X-TYPESENSE-API-KEY': key },
110
+ signal: AbortSignal.timeout(900),
111
+ });
112
+ const data = await response.json();
113
+ if (!response.ok) {
114
+ throw new Error(`Recieved ${response.status} error from search endpoint: ${data["message"]}`);
115
+ }
116
+ return data;
117
+ }
118
+ async getLanguages() {
119
+ const searchParams = new URLSearchParams({
120
+ filter_by: 'type:=Language',
121
+ query_by: "pref_label",
122
+ include_fields: "id,pref_label,lang_family",
123
+ sort_by: "pref_label:asc",
124
+ enable_highlight_v1: false,
125
+ per_page: 200,
126
+ });
127
+ const data = await this.searchRequest(searchParams);
128
+ return data.hits.map(result => {
129
+ return {
130
+ code: result.document.id.split("/").reverse()[1],
131
+ label: result.document.pref_label,
132
+ url: result.document.id,
133
+ family: result.document.lang_family || 'qli', // If no language family is given, using `qli` code as language isolate
134
+ }
135
+ });
136
+ }
137
+ async getLanguageFamilies() {
138
+ const searchParams = new URLSearchParams({
139
+ filter_by: 'type:=Language Family',
140
+ query_by: "pref_label",
141
+ include_fields: "id,pref_label",
142
+ sort_by: "pref_label:asc",
143
+ enable_highlight_v1: false,
144
+ per_page: 200,
145
+ });
146
+ const data = await this.searchRequest(searchParams);
147
+ return data.hits.map(result => {
148
+ return {
149
+ code: result.document.id.split("/").pop() || 'qli',
150
+ label: result.document.pref_label,
151
+ url: result.document.id,
152
+ }
153
+ });
154
+ }
155
+ }
156
+ customElements.define('lucos-lang', LucosLangComponent, { extends: "span" });
@@ -0,0 +1,224 @@
1
+ import TomSelect from 'tom-select';
2
+ import tomSelectStylesheet from 'tom-select/dist/css/tom-select.default.css';
3
+
4
+ class LucosSearchComponent extends HTMLSpanElement {
5
+ static get observedAttributes() {
6
+ return ['data-api-key','data-types','data-exclude-types'];
7
+ }
8
+ constructor() {
9
+ super();
10
+ const component = this;
11
+ const shadow = component.attachShadow({mode: 'open'});
12
+
13
+ if (tomSelectStylesheet) {
14
+ const tomStyle = document.createElement('style');
15
+ tomStyle.textContent = tomSelectStylesheet;
16
+ shadow.appendChild(tomStyle);
17
+ }
18
+
19
+ const mainStyle = document.createElement('style');
20
+ mainStyle.textContent = `
21
+ .lozenge {
22
+ align-items: center;
23
+ vertical-align: baseline;
24
+ border-radius: 3px;
25
+ background-repeat: repeat-x;
26
+ border-style: solid;
27
+ border-width: 1px;
28
+ text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);
29
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 1px rgba(255, 255, 255, 0.03);
30
+
31
+ /** Make the colour settings !important so they override the tom-select default style **/
32
+ background-image: linear-gradient(to bottom, #ffffff63, #24232347) !important;
33
+ background-color: var(--lozenge-background) !important;
34
+ border-color: var(--lozenge-border) !important;
35
+ color: var(--lozenge-text) !important;
36
+ }
37
+ .lozenge .remove {
38
+ border-left-color: var(--lozenge-border) !important;
39
+ }
40
+
41
+ /* Default colour to greys, but override based on type */
42
+ .lozenge {
43
+ --lozenge-background: #555;
44
+ --lozenge-border: #6d6d6d;
45
+ --lozenge-text: #fff;
46
+ }
47
+ /* Items from lucos_eolas have many types. For now, count any type which isn't specified later as part of eolas. */
48
+ .lozenge[data-type] {
49
+ --lozenge-background: #6a00c2;
50
+ --lozenge-border: #44265d;
51
+ }
52
+ .lozenge[data-type="Track"] {
53
+ --lozenge-background: #000060;
54
+ --lozenge-border: #000020;
55
+ }
56
+ .lozenge[data-type="Person"] {
57
+ --lozenge-background: #044E00;
58
+ --lozenge-border: #033100;
59
+ }
60
+ /** Aquatic Places **/
61
+ .lozenge[data-type="Ocean"], .lozenge[data-type="Sea"], .lozenge[data-type="Sea Inlet"], .lozenge[data-type="River"], .lozenge[data-type="Lake"] {
62
+ --lozenge-background: #0085fe;
63
+ --lozenge-border: #0036b1;
64
+ }
65
+ /** Terrestrial Places **/
66
+ .lozenge[data-type="Archipelago"], .lozenge[data-type="Area Of Outstanding Natural Beauty"], .lozenge[data-type="Continent"], .lozenge[data-type="Historical Site"], .lozenge[data-type="Island"], .lozenge[data-type="Mountain"] {
67
+ --lozenge-background: #652c17;
68
+ --lozenge-border: #321200;
69
+ }
70
+ /** Cosmic Places **/
71
+ .lozenge[data-type="Galaxy"], .lozenge[data-type="Planetary System"], .lozenge[data-type="Star"], .lozenge[data-type="Planet"], .lozenge[data-type="Natural Satellite"] {
72
+ --lozenge-background: #15163a;
73
+ --lozenge-border: #000000;
74
+ --lozenge-text: #feffe8;
75
+ }
76
+ /** Human Places **/
77
+ .lozenge[data-type="Airport"], .lozenge[data-type="Autonomous Area"], .lozenge[data-type="City"], .lozenge[data-type="Country"], .lozenge[data-type="County"], .lozenge[data-type="Dependent Territory"], .lozenge[data-type="Historical Site"], .lozenge[data-type="Neighbourhood"], .lozenge[data-type="Province"], .lozenge[data-type="Region"], .lozenge[data-type="Road"], .lozenge[data-type="State"], .lozenge[data-type="Town"], .lozenge[data-type="Tribal Nation"], .lozenge[data-type="Village"] {
78
+ --lozenge-background: #aed0db;
79
+ --lozenge-border: #3f6674;
80
+ --lozenge-text: #0c1a1b;
81
+ }
82
+ /** Supernatural Places **/
83
+ .lozenge[data-type="Supernatural Realm"] {
84
+ --lozenge-background: #f1ff5f;
85
+ --lozenge-border: #674800;
86
+ --lozenge-text: #352005;
87
+ }
88
+ .lozenge[data-type="Historical Event"] {
89
+ --lozenge-background: #740909;
90
+ --lozenge-border: #470202;
91
+ }
92
+ .lozenge[data-type="Number"] {
93
+ --lozenge-background: #0000ff;
94
+ --lozenge-border: #000083;
95
+ }
96
+ /** Temporal Types **/
97
+ .lozenge[data-type="Calendar"], .lozenge[data-type="Festival"], .lozenge[data-type="Month of Year"], .lozenge[data-type="Day of Week"] {
98
+ --lozenge-background: #fffc33;
99
+ --lozenge-border: #7f7e00;
100
+ --lozenge-text: #0f0f00;
101
+ }
102
+
103
+
104
+ .lozenge.active {
105
+ --lozenge-border: #b00;
106
+ }
107
+ .type {
108
+ margin: 0 3px;
109
+ padding: 2px 6px;
110
+ }
111
+ .ts-dropdown {
112
+ margin: 0;
113
+ }
114
+ .lozenge a {
115
+ color: inherit;
116
+ text-decoration: none;
117
+ }
118
+ `;
119
+ shadow.appendChild(mainStyle);
120
+
121
+
122
+ const selector = component.querySelector("select");
123
+ if (!selector) throw new Error("Can't find select element in lucos-search");
124
+ selector.setAttribute("multiple", "multiple");
125
+ new TomSelect(selector, {
126
+ valueField: 'id',
127
+ labelField: 'pref_label',
128
+ searchField: [],
129
+ closeAfterSelect: true,
130
+ highlight: false, // Will use typesense's hightlight (as it can consider other fields)
131
+ load: async function(query, callback) {
132
+ const queryParams = new URLSearchParams({
133
+ q: query,
134
+ });
135
+ if (component.getAttribute("data-types")) {
136
+ queryParams.set("filter_by",`type:[${component.getAttribute("data-types")}]`);
137
+ } else if (component.getAttribute("data-exclude_types")) {
138
+ queryParams.set("filter_by",`type:![${component.getAttribute("data-exclude_types")}]`);
139
+ }
140
+ const results = await component.searchRequest(queryParams);
141
+ this.clearOptions();
142
+ callback(results);
143
+ },
144
+ plugins: {
145
+ remove_button:{
146
+ title:'Remove this item',
147
+ },
148
+ drag_drop: {},
149
+ },
150
+ onItemAdd: function() { // Workaround until https://github.com/orchidjs/tom-select/pull/945 is merged/released
151
+ this.setTextboxValue('');
152
+ this.refreshOptions();
153
+ },
154
+ onFocus: function() {
155
+ this.clearOptions();
156
+ },
157
+ // On startup, update any existing options with latest data from search
158
+ onInitialize: async function() {
159
+ const ids = Object.keys(this.options);
160
+ if (ids.length < 1) return;
161
+ const searchParams = new URLSearchParams({
162
+ q: '*',
163
+ filter_by: `id:[${ids.join(",")}]`,
164
+ per_page: ids.length,
165
+ });
166
+ const results = await component.searchRequest(searchParams);
167
+ results.forEach(result => {
168
+ this.updateOption(result.id, result);
169
+ });
170
+ },
171
+ onItemSelect: function (item) {
172
+ // Tom-select prevents clicking on link in an item to work as normal, so force it here
173
+ window.open(item.dataset.value, '_blank').focus();
174
+ },
175
+ render:{
176
+ option: function(data, escape) {
177
+ let label = escape(data.pref_label);
178
+ let alt_label = "";
179
+ if (data.highlight.pref_label) {
180
+ label = data.highlight.pref_label.snippet;
181
+ } else if (data.highlight.labels) {
182
+ const matched_label = data.highlight.labels.find(l => l.matched_tokens.length > 0);
183
+ if (matched_label) {
184
+ alt_label = ` <span class="alt-label">(${matched_label.snippet})</span>`;
185
+ }
186
+ }
187
+ label = label.replace(` (${data.type})`,""); // No need to include any type disambiguation in label, as types are always shown
188
+ return `<div>${label}${alt_label}<span class="type lozenge" data-type="${escape(data.type)}">${escape(data.type)}</span></div>`;
189
+ },
190
+ item: function(data, escape) {
191
+ return `<div class="lozenge" data-type="${escape(data.type)}"><a href="${data.id}" target="_blank">${escape(data.pref_label)}</a></div>`;
192
+ },
193
+ },
194
+ });
195
+ if (selector.nextElementSibling) {
196
+ shadow.append(selector.nextElementSibling);
197
+ }
198
+ }
199
+ async searchRequest(searchParams) {
200
+ const key = this.getAttribute("data-api-key");
201
+ if (!key) throw new Error("No `data-api-key` attribute set on `lucos-search` component");
202
+ searchParams.set('query_by', "pref_label,labels,description,lyrics");
203
+ searchParams.set('query_by_weights', "10,8,3,1");
204
+ searchParams.set('sort_by', "_text_match:desc,pref_label:asc");
205
+ searchParams.set('prioritize_num_matching_fields', false);
206
+ searchParams.set('include_fields', "id,pref_label,type,labels");
207
+ searchParams.set('enable_highlight_v1', false);
208
+ searchParams.set('highlight_start_tag', '<span class="highlight">')
209
+ searchParams.set('highlight_end_tag', '</span>');
210
+ const response = await fetch("https://arachne.l42.eu/search?"+searchParams.toString(), {
211
+ headers: { 'X-TYPESENSE-API-KEY': key },
212
+ signal: AbortSignal.timeout(900),
213
+ });
214
+ const data = await response.json();
215
+ if (!response.ok) {
216
+ throw new Error(`Recieved ${response.status} error from search endpoint: ${data["message"]}`);
217
+ }
218
+ const results = data.hits.map(result => {
219
+ return {...result, ...result.document}
220
+ });
221
+ return results;
222
+ }
223
+ }
224
+ customElements.define('lucos-search', LucosSearchComponent, { extends: "span" });