@matdata/yasgui 4.6.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/CHANGELOG.md +175 -0
- package/build/ts/src/PersistentConfig.d.ts +49 -0
- package/build/ts/src/Tab.d.ts +105 -0
- package/build/ts/src/TabContextMenu.d.ts +29 -0
- package/build/ts/src/TabElements.d.ts +45 -0
- package/build/ts/src/TabSettingsModal.d.ts +28 -0
- package/build/ts/src/defaults.d.ts +3 -0
- package/build/ts/src/endpointSelect.d.ts +44 -0
- package/build/ts/src/index.d.ts +104 -0
- package/build/ts/src/linkUtils.d.ts +43 -0
- package/build/yasgui.html +24 -0
- package/build/yasgui.min.css +2 -0
- package/build/yasgui.min.css.map +1 -0
- package/build/yasgui.min.js +3 -0
- package/build/yasgui.min.js.LICENSE.txt +59 -0
- package/build/yasgui.min.js.map +1 -0
- package/package.json +48 -0
- package/src/PersistentConfig.ts +159 -0
- package/src/Tab.ts +775 -0
- package/src/TabContextMenu.scss +42 -0
- package/src/TabContextMenu.ts +143 -0
- package/src/TabElements.scss +134 -0
- package/src/TabElements.ts +336 -0
- package/src/TabSettingsModal.scss +226 -0
- package/src/TabSettingsModal.ts +424 -0
- package/src/defaults.ts +64 -0
- package/src/endpointSelect.scss +122 -0
- package/src/endpointSelect.ts +339 -0
- package/src/index.scss +97 -0
- package/src/index.ts +373 -0
- package/src/linkUtils.ts +234 -0
- package/src/tab.scss +61 -0
- package/static/yasgui.bootstrap.css +36 -0
- package/static/yasgui.polyfill.min.js +4 -0
- package/typings-custom/@tarekraafat/autocomplete.js/index.d.ts +48 -0
- package/typings-custom/main.d.ts +1 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
.yasgui {
|
|
2
|
+
.autocomplete {
|
|
3
|
+
padding: 3px 6px;
|
|
4
|
+
margin: 4px 0px;
|
|
5
|
+
border: 2px solid #ccc;
|
|
6
|
+
width: 100%;
|
|
7
|
+
&:hover {
|
|
8
|
+
border-color: #bbb;
|
|
9
|
+
}
|
|
10
|
+
&:focus {
|
|
11
|
+
border-color: #337ab7;
|
|
12
|
+
background: none;
|
|
13
|
+
outline: none;
|
|
14
|
+
}
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
transition: border-color ease-in 200ms;
|
|
17
|
+
}
|
|
18
|
+
.autocompleteWrapper {
|
|
19
|
+
width: 100%;
|
|
20
|
+
max-width: 700px;
|
|
21
|
+
margin-left: 10px;
|
|
22
|
+
position: relative;
|
|
23
|
+
}
|
|
24
|
+
.autocompleteList {
|
|
25
|
+
position: absolute;
|
|
26
|
+
max-height: 300px;
|
|
27
|
+
overflow-y: auto;
|
|
28
|
+
z-index: 6;
|
|
29
|
+
margin: 0;
|
|
30
|
+
margin-top: -4px;
|
|
31
|
+
padding: 0;
|
|
32
|
+
list-style: none;
|
|
33
|
+
background: white;
|
|
34
|
+
border: 1px solid #aaa;
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
left: 0;
|
|
37
|
+
right: 0;
|
|
38
|
+
&:hover {
|
|
39
|
+
.autoComplete_result.autoComplete_selected:not(:hover) {
|
|
40
|
+
background: unset;
|
|
41
|
+
.removeItem {
|
|
42
|
+
visibility: hidden;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
.autoComplete_result {
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
padding: 5px 10px;
|
|
49
|
+
margin: 0;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
display: flex;
|
|
52
|
+
transition: background visibility ease-in 200ms;
|
|
53
|
+
b {
|
|
54
|
+
color: #1f49a3;
|
|
55
|
+
}
|
|
56
|
+
.autoComplete_highlighted {
|
|
57
|
+
font-weight: bold;
|
|
58
|
+
}
|
|
59
|
+
&.autoComplete_selected {
|
|
60
|
+
background: #ccc;
|
|
61
|
+
.removeItem {
|
|
62
|
+
visibility: visible;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
&:hover {
|
|
66
|
+
background: #ccc;
|
|
67
|
+
.removeItem {
|
|
68
|
+
visibility: visible;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
.noResults {
|
|
73
|
+
padding: 5px 10px;
|
|
74
|
+
margin: 0;
|
|
75
|
+
}
|
|
76
|
+
.removeItem {
|
|
77
|
+
color: #000;
|
|
78
|
+
font-size: 15px;
|
|
79
|
+
text-shadow: 0 1px 0 #fff;
|
|
80
|
+
opacity: 0.5;
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
text-align: end;
|
|
83
|
+
margin-left: auto; // Make sure x always appears at the same place
|
|
84
|
+
visibility: hidden;
|
|
85
|
+
background: none;
|
|
86
|
+
border: none;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
margin-right: -10px;
|
|
89
|
+
padding-right: 20px;
|
|
90
|
+
&:hover {
|
|
91
|
+
opacity: 0.8;
|
|
92
|
+
color: #1f49a3;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&:empty {
|
|
97
|
+
display: none;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.clearEndpointBtn {
|
|
102
|
+
border: 1px solid #d1d1d1;
|
|
103
|
+
background-color: #d1d1d1;
|
|
104
|
+
color: #505050;
|
|
105
|
+
border-radius: 3px;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
|
|
108
|
+
padding: 4px 8px;
|
|
109
|
+
margin: 4px;
|
|
110
|
+
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
|
|
115
|
+
opacity: 1;
|
|
116
|
+
transition: opacity ease-in 200ms;
|
|
117
|
+
|
|
118
|
+
&:hover {
|
|
119
|
+
opacity: 0.8;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import Autocomplete from "@tarekraafat/autocomplete.js";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import { pick } from "lodash-es";
|
|
4
|
+
import { addClass } from "@matdata/yasgui-utils";
|
|
5
|
+
require("./endpointSelect.scss");
|
|
6
|
+
import parse from "autosuggest-highlight/parse";
|
|
7
|
+
import DOMPurify from "dompurify";
|
|
8
|
+
|
|
9
|
+
//Export this here instead of from our custom-types folder of autocomplete-js
|
|
10
|
+
//as this interface is exported via the yasgui config. The custom typings are
|
|
11
|
+
//not exported as part of the yasgui typings, so we'd get typing errors.
|
|
12
|
+
//instead, include this interface in the yasgui typings itself by defining it here
|
|
13
|
+
interface AutocompleteItem<T> {
|
|
14
|
+
index: number; //index of suggestion in array of suggestions
|
|
15
|
+
value: T; //suggestion object
|
|
16
|
+
key: keyof T; //key that matches search string. In our case, always 'all'
|
|
17
|
+
match: string; //suggestion value of the key above
|
|
18
|
+
}
|
|
19
|
+
export interface CatalogueItem {
|
|
20
|
+
endpoint: string;
|
|
21
|
+
type?: "history";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RenderedCatalogueItem<T> {
|
|
25
|
+
matches: { [k in keyof T]?: ReturnType<typeof parse> } & { endpoint?: ReturnType<typeof parse> };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { sanitize } = DOMPurify;
|
|
29
|
+
|
|
30
|
+
function listElementIsFullyVissible(el: HTMLLIElement) {
|
|
31
|
+
const { top, bottom } = el.getBoundingClientRect();
|
|
32
|
+
// Check if bottom of the element is off the page
|
|
33
|
+
if (bottom < 0) return false;
|
|
34
|
+
// Check its within the document viewport
|
|
35
|
+
if (top > document.documentElement.clientHeight) return false;
|
|
36
|
+
|
|
37
|
+
const ulRect = (el.parentNode as HTMLUListElement).getBoundingClientRect();
|
|
38
|
+
if (bottom <= ulRect.bottom === false) return false;
|
|
39
|
+
// Check if the element is out of view due to a container scrolling
|
|
40
|
+
if (top <= ulRect.top) return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function splitSearchString(searchString: string): string[] {
|
|
45
|
+
return searchString.match(/\S+/g) || [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EndpointSelectConfig<T = CatalogueItem> {
|
|
49
|
+
// Omit endpoint since it will be used as default, this makes sure elements don't appear twice in the list
|
|
50
|
+
keys: (keyof T)[];
|
|
51
|
+
getData: () => T[];
|
|
52
|
+
renderItem: (data: AutocompleteItem<T> & RenderedCatalogueItem<T>, source: HTMLElement) => void;
|
|
53
|
+
}
|
|
54
|
+
export interface EndpointSelect {
|
|
55
|
+
on(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
56
|
+
on(event: "remove", listener: (endpoint: string, history: string[]) => void): this;
|
|
57
|
+
emit(event: "remove", endpoint: string, history: string[]): boolean;
|
|
58
|
+
on(event: "select", listener: (endpoint: string, history: string[]) => void): this;
|
|
59
|
+
emit(event: "select", endpoint: string, history: string[]): boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class EndpointSelect extends EventEmitter {
|
|
63
|
+
private container: HTMLDivElement;
|
|
64
|
+
private options: EndpointSelectConfig;
|
|
65
|
+
private value: string;
|
|
66
|
+
private history: CatalogueItem[];
|
|
67
|
+
private inputField!: HTMLInputElement;
|
|
68
|
+
constructor(initialValue: string, container: HTMLDivElement, options: EndpointSelectConfig, history: string[]) {
|
|
69
|
+
super();
|
|
70
|
+
this.container = container;
|
|
71
|
+
this.options = options;
|
|
72
|
+
this.value = initialValue;
|
|
73
|
+
this.history = history.map((endpoint) => {
|
|
74
|
+
return { endpoint: sanitize(endpoint), type: "history" };
|
|
75
|
+
});
|
|
76
|
+
// Add endpoint if not defined
|
|
77
|
+
if (this.options.keys.indexOf("endpoint") <= 0) this.options.keys.push("endpoint");
|
|
78
|
+
this.draw();
|
|
79
|
+
}
|
|
80
|
+
private draw() {
|
|
81
|
+
// Create container, we don't need to interact with it anymore
|
|
82
|
+
const autocompleteWrapper = document.createElement("div");
|
|
83
|
+
addClass(autocompleteWrapper, "autocompleteWrapper");
|
|
84
|
+
this.container.appendChild(autocompleteWrapper);
|
|
85
|
+
|
|
86
|
+
// Create field
|
|
87
|
+
this.inputField = document.createElement("input");
|
|
88
|
+
addClass(this.inputField, "autocomplete");
|
|
89
|
+
this.inputField.value = this.value;
|
|
90
|
+
autocompleteWrapper.appendChild(this.inputField);
|
|
91
|
+
|
|
92
|
+
// Create clear button
|
|
93
|
+
const clearBtn = document.createElement("button");
|
|
94
|
+
clearBtn.title = "Clear endpoint";
|
|
95
|
+
addClass(clearBtn, "clearEndpointBtn");
|
|
96
|
+
clearBtn.innerText = "✖";
|
|
97
|
+
clearBtn.addEventListener("click", () => {
|
|
98
|
+
this.inputField.value = "";
|
|
99
|
+
this.inputField.focus();
|
|
100
|
+
});
|
|
101
|
+
this.container.appendChild(clearBtn);
|
|
102
|
+
|
|
103
|
+
// Init autocomplete library
|
|
104
|
+
new Autocomplete<CatalogueItem>({
|
|
105
|
+
placeholder: "Search or add an endpoint",
|
|
106
|
+
highlight: false,
|
|
107
|
+
maxResults: 100,
|
|
108
|
+
trigger: {
|
|
109
|
+
event: ["input", "focusin"],
|
|
110
|
+
//we always want to show the autocomplete, even if no query is set
|
|
111
|
+
//in that case, we'd just show the full list
|
|
112
|
+
condition: () => true,
|
|
113
|
+
},
|
|
114
|
+
// threshold: -1,
|
|
115
|
+
searchEngine: (query, record) => {
|
|
116
|
+
if (!query || query.trim().length === 0) {
|
|
117
|
+
//show everything when we've got an empty search string
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return splitSearchString(query).every((m) => record.indexOf(m) >= 0);
|
|
121
|
+
},
|
|
122
|
+
data: {
|
|
123
|
+
src: async () => {
|
|
124
|
+
return [...this.history, ...this.options.getData()].map((item) => ({
|
|
125
|
+
...item,
|
|
126
|
+
all: Object.values(pick(item, ["endpoint", ...this.options.keys])).join(" "),
|
|
127
|
+
}));
|
|
128
|
+
},
|
|
129
|
+
key: ["all" as any], // All is something we add as a workaround for getting multiple results of the library
|
|
130
|
+
cache: false,
|
|
131
|
+
},
|
|
132
|
+
// Use a selector coming from the container, this is to make sure we grab our own autocomplete element
|
|
133
|
+
selector: () => this.inputField,
|
|
134
|
+
resultsList: {
|
|
135
|
+
render: true,
|
|
136
|
+
destination: this.inputField,
|
|
137
|
+
container: (element) => {
|
|
138
|
+
// Remove id, there can be multiple yasgui's active on one page, we can't delete since the library will then add the default
|
|
139
|
+
element.id = "";
|
|
140
|
+
addClass(element, "autocompleteList");
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
resultItem: {
|
|
144
|
+
content: (data, source) => {
|
|
145
|
+
const endpoint = sanitize(data.value.endpoint);
|
|
146
|
+
|
|
147
|
+
// Custom handling of items with history, these are able to be removed
|
|
148
|
+
if (data.value.type && data.value.type === "history") {
|
|
149
|
+
// Add a container to make folding work correctly
|
|
150
|
+
const resultsContainer = document.createElement("div");
|
|
151
|
+
// Match is highlighted text
|
|
152
|
+
resultsContainer.innerHTML = parse(endpoint, createHighlights(endpoint, this.inputField.value)).reduce(
|
|
153
|
+
(current, object) => (object.highlight ? current + object.text.bold() : current + object.text),
|
|
154
|
+
"",
|
|
155
|
+
);
|
|
156
|
+
source.append(resultsContainer);
|
|
157
|
+
|
|
158
|
+
// Remove button
|
|
159
|
+
const removeBtn = document.createElement("button");
|
|
160
|
+
removeBtn.textContent = "✖";
|
|
161
|
+
addClass(removeBtn, "removeItem");
|
|
162
|
+
removeBtn.addEventListener("mousedown", (event) => {
|
|
163
|
+
this.history = this.history.filter((item) => item.endpoint !== endpoint);
|
|
164
|
+
this.emit(
|
|
165
|
+
"remove",
|
|
166
|
+
this.value,
|
|
167
|
+
this.history.map((value) => value.endpoint),
|
|
168
|
+
);
|
|
169
|
+
source.remove();
|
|
170
|
+
event.stopPropagation();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
source.appendChild(removeBtn);
|
|
174
|
+
} else {
|
|
175
|
+
// Add our own field highlighting
|
|
176
|
+
const matches: RenderedCatalogueItem<CatalogueItem> = { matches: {} };
|
|
177
|
+
for (const key of [...this.options.keys]) {
|
|
178
|
+
const val = data.value[key];
|
|
179
|
+
if (val) {
|
|
180
|
+
matches.matches[key] = parse(val, createHighlights(val, this.inputField.value));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.options.renderItem({ ...data, ...matches }, source);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
element: "li",
|
|
187
|
+
},
|
|
188
|
+
onSelection: (feedback) => {
|
|
189
|
+
const item = feedback.selection.value;
|
|
190
|
+
this.value = item.endpoint;
|
|
191
|
+
this.inputField.value = this.value;
|
|
192
|
+
this.emit(
|
|
193
|
+
"select",
|
|
194
|
+
this.value,
|
|
195
|
+
this.history.map((value) => value.endpoint),
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
noResults: () => {
|
|
199
|
+
const container = this.container.querySelector(".autocompleteList");
|
|
200
|
+
if (container) {
|
|
201
|
+
const noResults = document.createElement("div");
|
|
202
|
+
addClass(noResults, "noResults");
|
|
203
|
+
noResults.innerText = 'Press "enter" to add this endpoint';
|
|
204
|
+
container.appendChild(noResults);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
// New data handler
|
|
209
|
+
this.inputField.addEventListener("keyup", (event) => {
|
|
210
|
+
const target = <HTMLInputElement>event.target;
|
|
211
|
+
// Enter
|
|
212
|
+
if (event.keyCode === 13) {
|
|
213
|
+
if (this.value === target.value) {
|
|
214
|
+
//we have typed exactly the same value the one from the suggestion list
|
|
215
|
+
//So, just close the suggestion list
|
|
216
|
+
this.clearListSuggestionList();
|
|
217
|
+
this.inputField.blur();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!target.value || !target.value.trim()) {
|
|
221
|
+
this.clearListSuggestionList();
|
|
222
|
+
this.inputField.blur();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
this.options.getData().find((i) => i.endpoint === this.inputField.value) ||
|
|
227
|
+
this.history.find((item) => item.endpoint === this.inputField.value)
|
|
228
|
+
) {
|
|
229
|
+
//the value you typed is already in our catalogue or in our history
|
|
230
|
+
this.value = target.value;
|
|
231
|
+
this.clearListSuggestionList();
|
|
232
|
+
this.emit(
|
|
233
|
+
"select",
|
|
234
|
+
this.value,
|
|
235
|
+
this.history.map((h) => h.endpoint),
|
|
236
|
+
);
|
|
237
|
+
this.inputField.blur();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.value = target.value;
|
|
241
|
+
this.history.push({ endpoint: target.value, type: "history" });
|
|
242
|
+
this.emit(
|
|
243
|
+
"select",
|
|
244
|
+
this.value,
|
|
245
|
+
this.history.map((value) => value.endpoint),
|
|
246
|
+
);
|
|
247
|
+
this.clearListSuggestionList();
|
|
248
|
+
this.inputField.blur();
|
|
249
|
+
}
|
|
250
|
+
// Blur and set value on enter
|
|
251
|
+
if (event.keyCode === 27) {
|
|
252
|
+
this.inputField.blur();
|
|
253
|
+
this.inputField.value = this.value;
|
|
254
|
+
this.clearListSuggestionList();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Stop moving caret around when hitting up and down keys
|
|
258
|
+
if (event.keyCode === 38 || event.keyCode === 40) {
|
|
259
|
+
event.stopPropagation();
|
|
260
|
+
const selected: HTMLLIElement | null = this.container.querySelector(
|
|
261
|
+
".autocompleteList .autoComplete_result.autoComplete_selected",
|
|
262
|
+
);
|
|
263
|
+
if (selected && !listElementIsFullyVissible(selected)) {
|
|
264
|
+
selected.scrollIntoView(false);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
this.inputField.addEventListener("blur", (event) => {
|
|
269
|
+
const target = <HTMLInputElement>event.target;
|
|
270
|
+
// Tabbing blur event
|
|
271
|
+
if (target.className === this.inputField.className && event.relatedTarget) {
|
|
272
|
+
this.clearListSuggestionList();
|
|
273
|
+
this.inputField.value = this.value;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
// Improvised clickAway handler, Blur will fire before select or click events, causing interactive suggestion to no longer work
|
|
277
|
+
document.addEventListener("mousedown", (event) => {
|
|
278
|
+
if (event.button !== 2) {
|
|
279
|
+
const target = <HTMLElement>event.target;
|
|
280
|
+
if (
|
|
281
|
+
target.className === "removeItem" ||
|
|
282
|
+
target.className === "autoComplete_result" ||
|
|
283
|
+
target.className === "autocomplete"
|
|
284
|
+
)
|
|
285
|
+
return;
|
|
286
|
+
this.clearListSuggestionList();
|
|
287
|
+
this.inputField.value = this.value;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
private clearListSuggestionList = () => {
|
|
292
|
+
const autocompleteList = this.container.querySelector(".autocompleteList");
|
|
293
|
+
if (autocompleteList) autocompleteList.innerHTML = "";
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
public setEndpoint(endpoint: string, endpointHistory?: string[]) {
|
|
297
|
+
this.value = endpoint;
|
|
298
|
+
if (endpointHistory) {
|
|
299
|
+
this.history = endpointHistory.map((endpoint) => {
|
|
300
|
+
return { endpoint, type: "history" };
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Force focus when the endpoint is open
|
|
304
|
+
if (this.inputField === document.activeElement) {
|
|
305
|
+
this.inputField.focus();
|
|
306
|
+
} else {
|
|
307
|
+
// Only set when the user is not using the widget at this time
|
|
308
|
+
this.inputField.value = endpoint;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
public destroy() {
|
|
312
|
+
this.removeAllListeners();
|
|
313
|
+
this.inputField.remove();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function createHighlights(text: string, query: string) {
|
|
318
|
+
return splitSearchString(query)
|
|
319
|
+
.reduce((result: Array<[number, number]>, word: string) => {
|
|
320
|
+
if (!word.length) return result;
|
|
321
|
+
const wordLen = word.length;
|
|
322
|
+
// const regex = new RegExp(escapeRegexCharacters(word), 'i');
|
|
323
|
+
// const { index = -1 } = text.match(regex);
|
|
324
|
+
const index = text.indexOf(word);
|
|
325
|
+
if (index > -1) {
|
|
326
|
+
result.push([index, index + wordLen]);
|
|
327
|
+
|
|
328
|
+
// Replace what we just found with spaces so we don't find it again.
|
|
329
|
+
text = text.slice(0, index) + new Array(wordLen + 1).join(" ") + text.slice(index + wordLen);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
}, [])
|
|
334
|
+
.sort((match1: [number, number], match2: [number, number]) => {
|
|
335
|
+
return match1[0] - match2[0];
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export default EndpointSelect;
|
package/src/index.scss
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
.yasgui {
|
|
2
|
+
a {
|
|
3
|
+
color: #337ab7;
|
|
4
|
+
text-decoration: none;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Keep tabs visible when yasqe is in fullscreen
|
|
8
|
+
&:has(.yasqe.fullscreen) .tabsList {
|
|
9
|
+
position: fixed;
|
|
10
|
+
top: 0;
|
|
11
|
+
left: 0;
|
|
12
|
+
right: 0;
|
|
13
|
+
z-index: 9999;
|
|
14
|
+
background: white;
|
|
15
|
+
border-bottom: 1px solid #ddd;
|
|
16
|
+
padding: 5px;
|
|
17
|
+
margin: 0;
|
|
18
|
+
|
|
19
|
+
.tab {
|
|
20
|
+
span {
|
|
21
|
+
display: inline !important;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
a {
|
|
25
|
+
display: flex !important;
|
|
26
|
+
align-items: center !important;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//css taken from https://www.muicss.com/docs/v1/css-js/forms
|
|
32
|
+
$focusColor: #337ab7;
|
|
33
|
+
$font-size: 15px;
|
|
34
|
+
$font-color: rgba(0, 0, 0, 0.87);
|
|
35
|
+
$border-color: rgba(0, 0, 0, 0.26);
|
|
36
|
+
$label-font-size: 12px;
|
|
37
|
+
$label-font-color: rgba(0, 0, 0, 0.54);
|
|
38
|
+
$label-line-height: 15px;
|
|
39
|
+
.yasgui_textfield {
|
|
40
|
+
display: block;
|
|
41
|
+
padding-top: $font-size * 1.25;
|
|
42
|
+
// margin-bottom: $mui-form-group-margin-bottom;
|
|
43
|
+
position: relative;
|
|
44
|
+
|
|
45
|
+
> label {
|
|
46
|
+
// Positioning
|
|
47
|
+
position: absolute;
|
|
48
|
+
top: 0;
|
|
49
|
+
|
|
50
|
+
// Display
|
|
51
|
+
display: block;
|
|
52
|
+
width: 100%;
|
|
53
|
+
|
|
54
|
+
// Other
|
|
55
|
+
color: $label-font-color;
|
|
56
|
+
font-size: $label-font-size;
|
|
57
|
+
font-weight: 400;
|
|
58
|
+
line-height: $label-line-height;
|
|
59
|
+
overflow-x: hidden;
|
|
60
|
+
text-overflow: ellipsis;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
& > input,
|
|
65
|
+
& > textarea {
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
display: block;
|
|
68
|
+
color: $font-color;
|
|
69
|
+
border: none;
|
|
70
|
+
border-bottom: 1px solid $border-color;
|
|
71
|
+
outline: none;
|
|
72
|
+
width: 100%;
|
|
73
|
+
padding: 0;
|
|
74
|
+
box-shadow: none;
|
|
75
|
+
border-radius: 0px;
|
|
76
|
+
|
|
77
|
+
// Typography
|
|
78
|
+
font-size: $font-size;
|
|
79
|
+
font-family: inherit;
|
|
80
|
+
line-height: inherit;
|
|
81
|
+
|
|
82
|
+
// Bugfix for firefox-android
|
|
83
|
+
background-image: none;
|
|
84
|
+
|
|
85
|
+
&:focus {
|
|
86
|
+
border-color: $focusColor;
|
|
87
|
+
border-width: 2px;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
> input,
|
|
91
|
+
> textarea {
|
|
92
|
+
&:focus ~ label {
|
|
93
|
+
color: $focusColor;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|