@zwayam/sql-query-editor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/sql-query-editor.js +639 -0
- package/dist/sql-query-editor.umd.cjs +153 -0
- package/package.json +41 -0
- package/sql-query-editor.d.ts +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# SQL Query Editor
|
|
2
|
+
|
|
3
|
+
A framework-agnostic SQL editor built as a single native [Web Component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components). Syntax highlighting, schema-aware autocomplete, and Shadow DOM style isolation — with zero runtime dependencies (~5 KB gzipped).
|
|
4
|
+
|
|
5
|
+
Because it's a Web Component rather than a React/Angular/Vue-specific library, it registers once as a real HTML element (`<sql-query-editor>`) and can be dropped into any of them, or into plain HTML with no framework at all.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Features](#features)
|
|
10
|
+
- [Install](#install)
|
|
11
|
+
- [Quick start](#quick-start)
|
|
12
|
+
- [Public API](#public-api)
|
|
13
|
+
- [Events](#events)
|
|
14
|
+
- [Theming](#theming)
|
|
15
|
+
- [TypeScript](#typescript)
|
|
16
|
+
- [Framework integration](#framework-integration)
|
|
17
|
+
- [Vanilla JS](#vanilla-js)
|
|
18
|
+
- [Angular](#angular)
|
|
19
|
+
- [React](#react)
|
|
20
|
+
- [Vue](#vue)
|
|
21
|
+
- [Server-side rendering (SSR)](#server-side-rendering-ssr)
|
|
22
|
+
- [Known limitations](#known-limitations)
|
|
23
|
+
- [Development](#development)
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- SQL syntax highlighting (keywords, functions, strings, numbers, operators, comments)
|
|
28
|
+
- Context-aware autocomplete: keywords/functions everywhere, tables after `FROM`/`JOIN`, columns after `WHERE`/`ON`
|
|
29
|
+
- Table/column autocomplete from a schema you supply
|
|
30
|
+
- `query` / `schema` work as both HTML attributes and JS properties, kept in sync live
|
|
31
|
+
- `query-change` and `query-submit` (Ctrl/Cmd+Enter) events
|
|
32
|
+
- Shadow DOM isolation — the editor's internal styles never leak into your page, and your page's styles never leak in
|
|
33
|
+
- Bundled TypeScript declarations
|
|
34
|
+
- Zero dependencies
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @zwayam/sql-query-editor
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
Register the element once, anywhere early in your app (an entry file, root module, or root component):
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import "@zwayam/sql-query-editor";
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then use the tag anywhere in your markup:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<sql-query-editor
|
|
54
|
+
id="editor"
|
|
55
|
+
query="SELECT * FROM users"
|
|
56
|
+
schema='{"users": ["id", "name", "email"]}'
|
|
57
|
+
></sql-query-editor>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const editor = document.getElementById("editor");
|
|
62
|
+
|
|
63
|
+
editor.addEventListener("query-change", (event) => {
|
|
64
|
+
console.log("current SQL:", event.detail.query);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
editor.addEventListener("query-submit", (event) => {
|
|
68
|
+
// fires on Ctrl+Enter / Cmd+Enter
|
|
69
|
+
runQuery(event.detail.query);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`query` and `schema` are also plain JS properties, useful when you're building the value programmatically instead of writing it as a literal attribute:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
editor.query = "SELECT * FROM orders";
|
|
77
|
+
editor.schema = { orders: ["id", "user_id", "amount"] };
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Public API
|
|
81
|
+
|
|
82
|
+
### Attributes / properties
|
|
83
|
+
|
|
84
|
+
| Name | Type | Notes |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `query` | `string` | The current SQL text. Settable as an attribute (`query="..."`) or a property (`el.query = "..."`). Changing either updates the other — and updates the editor live, even after it's already rendered. |
|
|
87
|
+
| `schema` | `object` | Tables/columns used by autocomplete. As an attribute it must be a JSON string (`schema='{"users":["id"]}'`); as a property it's a plain object (`el.schema = { users: ["id"] }`). Accepts `{ table: [columns] }` or `{ table: { columns: [...] } }`. |
|
|
88
|
+
|
|
89
|
+
### Methods
|
|
90
|
+
|
|
91
|
+
| Method | Returns | Description |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `getQuery()` | `string` | Returns the current SQL text. Equivalent to reading `.query`. |
|
|
94
|
+
| `setQuery(query)` | `void` | Sets the SQL text and re-highlights it. Equivalent to writing `.query`. |
|
|
95
|
+
| `setSchema(schema)` | `void` | Sets the autocomplete schema. Equivalent to writing `.schema`. |
|
|
96
|
+
| `clear()` | `void` | Empties the editor. |
|
|
97
|
+
| `focus()` | `void` | Focuses the editor's text input. |
|
|
98
|
+
|
|
99
|
+
The property/attribute forms and the method forms are interchangeable — use whichever reads more naturally for your framework's binding syntax (see [Framework integration](#framework-integration)).
|
|
100
|
+
|
|
101
|
+
## Events
|
|
102
|
+
|
|
103
|
+
Both events are standard `CustomEvent`s dispatched on the element itself, so they bubble and can be listened for with `addEventListener` (or your framework's native event-binding syntax) anywhere in the DOM tree above the element.
|
|
104
|
+
|
|
105
|
+
| Event | Fires when | `event.detail` |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `query-change` | The text changes (every keystroke, or a completed autocomplete suggestion) | `{ query: string }` |
|
|
108
|
+
| `query-submit` | The user presses Ctrl+Enter (or Cmd+Enter on macOS) | `{ query: string }` |
|
|
109
|
+
|
|
110
|
+
## Theming
|
|
111
|
+
|
|
112
|
+
The editor exposes its colors as CSS custom properties on the host element, so you can restyle it from outside without piercing the Shadow DOM:
|
|
113
|
+
|
|
114
|
+
```css
|
|
115
|
+
sql-query-editor {
|
|
116
|
+
--sql-editor-border: #444;
|
|
117
|
+
--sql-editor-background: #1e1e1e;
|
|
118
|
+
--sql-editor-text: #ffffff;
|
|
119
|
+
--sql-editor-keyword: #569cd6;
|
|
120
|
+
--sql-editor-function: #dcdcaa;
|
|
121
|
+
--sql-editor-string: #ce9178;
|
|
122
|
+
--sql-editor-number: #b5cea8;
|
|
123
|
+
--sql-editor-operator: #d4d4d4;
|
|
124
|
+
--sql-editor-comment: #6a9955;
|
|
125
|
+
--sql-editor-active: #2a2d2e;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| Variable | Controls | Default |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `--sql-editor-border` | Editor border | `#d0d5dd` |
|
|
132
|
+
| `--sql-editor-background` | Editor background | `#ffffff` |
|
|
133
|
+
| `--sql-editor-text` | Base text color | `#1f2937` |
|
|
134
|
+
| `--sql-editor-keyword` | Keywords (`SELECT`, `WHERE`, ...) | `#0066cc` |
|
|
135
|
+
| `--sql-editor-function` | Functions (`COUNT`, `SUM`, ...) | `#8e44ad` |
|
|
136
|
+
| `--sql-editor-string` | String literals | `#008000` |
|
|
137
|
+
| `--sql-editor-number` | Numeric literals | `#d35400` |
|
|
138
|
+
| `--sql-editor-operator` | Operators (`=`, `>`, ...) | `#c0392b` |
|
|
139
|
+
| `--sql-editor-comment` | `--` line comments | `#777777` |
|
|
140
|
+
| `--sql-editor-active` | Highlighted autocomplete suggestion | `#e8f0fe` |
|
|
141
|
+
|
|
142
|
+
## TypeScript
|
|
143
|
+
|
|
144
|
+
Type declarations are bundled with the package — no `@types/*` install needed. Importing the package gives you a typed `SqlQueryEditor` class and augments the global `HTMLElementTagNameMap`, so plain DOM calls are typed automatically in any framework:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import "@zwayam/sql-query-editor";
|
|
148
|
+
|
|
149
|
+
const editor = document.querySelector("sql-query-editor"); // typed as SqlQueryEditor
|
|
150
|
+
editor.setSchema({ users: ["id", "name"] });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If you use JSX (React/Preact) and want `<sql-query-editor>` recognized as a valid intrinsic element with typed props, add this once in your own project (not needed for Angular or Vue templates, which don't type-check tag names this way):
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// e.g. src/sql-query-editor.d.ts
|
|
157
|
+
import type { SqlQueryEditor } from "@zwayam/sql-query-editor";
|
|
158
|
+
|
|
159
|
+
declare global {
|
|
160
|
+
namespace JSX {
|
|
161
|
+
interface IntrinsicElements {
|
|
162
|
+
"sql-query-editor": React.DetailedHTMLProps<
|
|
163
|
+
React.HTMLAttributes<SqlQueryEditor>,
|
|
164
|
+
SqlQueryEditor
|
|
165
|
+
> & { query?: string };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Framework integration
|
|
172
|
+
|
|
173
|
+
### Vanilla JS
|
|
174
|
+
|
|
175
|
+
No wrapper needed — use it exactly as shown in [Quick start](#quick-start).
|
|
176
|
+
|
|
177
|
+
### Angular
|
|
178
|
+
|
|
179
|
+
Angular's template compiler rejects unknown tags by default, so tell it to accept custom elements once, either on a standalone component or an `NgModule`:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
|
183
|
+
import "@zwayam/sql-query-editor";
|
|
184
|
+
|
|
185
|
+
@Component({
|
|
186
|
+
selector: "app-query-page",
|
|
187
|
+
standalone: true,
|
|
188
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
189
|
+
template: `
|
|
190
|
+
<sql-query-editor
|
|
191
|
+
[query]="sql"
|
|
192
|
+
[schema]="schema"
|
|
193
|
+
(query-change)="sql = $event.detail.query"
|
|
194
|
+
(query-submit)="runQuery($event.detail.query)"
|
|
195
|
+
></sql-query-editor>
|
|
196
|
+
`
|
|
197
|
+
})
|
|
198
|
+
export class QueryPageComponent {
|
|
199
|
+
sql = "SELECT * FROM users";
|
|
200
|
+
|
|
201
|
+
schema = {
|
|
202
|
+
users: ["id", "name", "email"]
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
runQuery(query: string) {
|
|
206
|
+
// send query to your backend
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
For an `NgModule`-based app, add the same `schemas: [CUSTOM_ELEMENTS_SCHEMA]` to the `@NgModule` decorator instead, and put `import "@zwayam/sql-query-editor"` in `main.ts`.
|
|
212
|
+
|
|
213
|
+
`[query]` and `[schema]` are Angular property bindings — they assign straight to the element's `query`/`schema` properties, so no `ViewChild` or imperative lifecycle code is required. Note that Angular's `[(x)]` two-way binding shorthand expects an `xChange` event name, and this component's event is `query-change` (not `queryChange`), so two-way sync is written explicitly as `[query]="sql" (query-change)="sql = $event.detail.query"` rather than `[(query)]="sql"`.
|
|
214
|
+
|
|
215
|
+
### React
|
|
216
|
+
|
|
217
|
+
JSX has no way to bind a custom DOM event (`query-change`) to a prop, so React needs a small `ref`-based wrapper — write it once, then use `<SqlEditor>` like any other component:
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
import { useEffect, useRef } from "react";
|
|
221
|
+
import "@zwayam/sql-query-editor";
|
|
222
|
+
import type { SqlQueryEditor, SqlSchema } from "@zwayam/sql-query-editor";
|
|
223
|
+
|
|
224
|
+
interface SqlEditorProps {
|
|
225
|
+
query: string;
|
|
226
|
+
schema: SqlSchema;
|
|
227
|
+
onQueryChange: (query: string) => void;
|
|
228
|
+
onQuerySubmit?: (query: string) => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function SqlEditor({ query, schema, onQueryChange, onQuerySubmit }: SqlEditorProps) {
|
|
232
|
+
const ref = useRef<SqlQueryEditor>(null);
|
|
233
|
+
|
|
234
|
+
// Only push `query` in when it actually differs, so typing doesn't fight
|
|
235
|
+
// the parent's re-render (which would otherwise reset the cursor position).
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (ref.current && ref.current.getQuery() !== query) {
|
|
238
|
+
ref.current.setQuery(query);
|
|
239
|
+
}
|
|
240
|
+
}, [query]);
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
ref.current?.setSchema(schema);
|
|
244
|
+
}, [schema]);
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
const el = ref.current;
|
|
248
|
+
if (!el) return;
|
|
249
|
+
|
|
250
|
+
const handleChange = (e: Event) => onQueryChange((e as CustomEvent).detail.query);
|
|
251
|
+
const handleSubmit = (e: Event) => onQuerySubmit?.((e as CustomEvent).detail.query);
|
|
252
|
+
|
|
253
|
+
el.addEventListener("query-change", handleChange);
|
|
254
|
+
el.addEventListener("query-submit", handleSubmit);
|
|
255
|
+
|
|
256
|
+
return () => {
|
|
257
|
+
el.removeEventListener("query-change", handleChange);
|
|
258
|
+
el.removeEventListener("query-submit", handleSubmit);
|
|
259
|
+
};
|
|
260
|
+
}, [onQueryChange, onQuerySubmit]);
|
|
261
|
+
|
|
262
|
+
return <sql-query-editor ref={ref} />;
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Usage:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
<SqlEditor
|
|
270
|
+
query={sql}
|
|
271
|
+
schema={{ users: ["id", "name"] }}
|
|
272
|
+
onQueryChange={setSql}
|
|
273
|
+
onQuerySubmit={runQuery}
|
|
274
|
+
/>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
This pattern works on every React version. (React 19 added some built-in property detection for custom elements, so passing `schema={...}` directly as a JSX prop may also work there — but the `ref` approach above is the version-agnostic, reliable one, and you still need it for the event listeners regardless.)
|
|
278
|
+
|
|
279
|
+
### Vue
|
|
280
|
+
|
|
281
|
+
Vue treats any tag containing a dash as a native custom element automatically in standard Vite/Vue CLI setups, binds `:prop` to matching DOM properties, and listens for custom events with `@event-name` — no wrapper needed:
|
|
282
|
+
|
|
283
|
+
```vue
|
|
284
|
+
<template>
|
|
285
|
+
<sql-query-editor
|
|
286
|
+
:query="sql"
|
|
287
|
+
:schema="schema"
|
|
288
|
+
@query-change="sql = $event.detail.query"
|
|
289
|
+
@query-submit="runQuery($event.detail.query)"
|
|
290
|
+
/>
|
|
291
|
+
</template>
|
|
292
|
+
|
|
293
|
+
<script setup>
|
|
294
|
+
import { ref } from "vue";
|
|
295
|
+
import "@zwayam/sql-query-editor";
|
|
296
|
+
|
|
297
|
+
const sql = ref("SELECT * FROM users");
|
|
298
|
+
const schema = ref({ users: ["id", "name"] });
|
|
299
|
+
|
|
300
|
+
function runQuery(query) {
|
|
301
|
+
// send query to your backend
|
|
302
|
+
}
|
|
303
|
+
</script>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
If your build ever warns about an unresolved component, tell the compiler to treat the tag as a custom element:
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
// vite.config.js
|
|
310
|
+
export default {
|
|
311
|
+
vue: {
|
|
312
|
+
template: {
|
|
313
|
+
compilerOptions: {
|
|
314
|
+
isCustomElement: (tag) => tag === "sql-query-editor"
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Server-side rendering (SSR)
|
|
322
|
+
|
|
323
|
+
The component registers itself via `customElements.define` and extends `HTMLElement`, neither of which exist in a Node.js SSR environment (Next.js, Nuxt, Angular Universal, etc.). Make sure it's only imported/rendered on the client:
|
|
324
|
+
|
|
325
|
+
- **Next.js** — import it inside `useEffect`, or use `next/dynamic` with `{ ssr: false }`, or mark the importing component `"use client"` and defer the import to the client bundle.
|
|
326
|
+
- **Nuxt** — wrap usage in `<ClientOnly>`.
|
|
327
|
+
- **Angular Universal** — guard the `import "@zwayam/sql-query-editor"` call with `isPlatformBrowser(this.platformId)`.
|
|
328
|
+
|
|
329
|
+
## Known limitations
|
|
330
|
+
|
|
331
|
+
- The editor's height is fixed at `350px` and isn't exposed as a CSS custom property or a `::part`. To resize it, wrap it in a container and reach into its Shadow DOM directly: `document.querySelector("sql-query-editor").shadowRoot.querySelector(".editor").style.height = "600px"`.
|
|
332
|
+
- Autocomplete context detection (which clause you're in) is a heuristic based on the nearest preceding keyword, not a full SQL parser — it's reliable for single, flat statements but can guess wrong inside subqueries.
|
|
333
|
+
- Quoted identifiers (MySQL backticks, Postgres double quotes, SQL Server brackets) and block comments (`/* ... */`) aren't tokenized/highlighted — only `'strings'` and `-- line comments`.
|
|
334
|
+
|
|
335
|
+
## Development
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
npm install
|
|
339
|
+
npm run dev # start the demo (demo/index.html) with live reload
|
|
340
|
+
npm run build # build dist/
|
|
341
|
+
npm run preview # preview the production build
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
MIT
|
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
const f = {
|
|
2
|
+
keywords: [
|
|
3
|
+
"SELECT",
|
|
4
|
+
"FROM",
|
|
5
|
+
"WHERE",
|
|
6
|
+
"AND",
|
|
7
|
+
"OR",
|
|
8
|
+
"NOT",
|
|
9
|
+
"NULL",
|
|
10
|
+
"IS",
|
|
11
|
+
"IN",
|
|
12
|
+
"BETWEEN",
|
|
13
|
+
"LIKE",
|
|
14
|
+
"AS",
|
|
15
|
+
"DISTINCT",
|
|
16
|
+
"ALL",
|
|
17
|
+
"TOP",
|
|
18
|
+
"LIMIT",
|
|
19
|
+
"OFFSET",
|
|
20
|
+
"ORDER",
|
|
21
|
+
"BY",
|
|
22
|
+
"ASC",
|
|
23
|
+
"DESC",
|
|
24
|
+
"GROUP",
|
|
25
|
+
"HAVING",
|
|
26
|
+
"JOIN",
|
|
27
|
+
"INNER",
|
|
28
|
+
"LEFT",
|
|
29
|
+
"RIGHT",
|
|
30
|
+
"FULL",
|
|
31
|
+
"OUTER",
|
|
32
|
+
"CROSS",
|
|
33
|
+
"ON",
|
|
34
|
+
"UNION",
|
|
35
|
+
"UNION ALL",
|
|
36
|
+
"INTERSECT",
|
|
37
|
+
"EXCEPT",
|
|
38
|
+
"INSERT",
|
|
39
|
+
"INTO",
|
|
40
|
+
"VALUES",
|
|
41
|
+
"UPDATE",
|
|
42
|
+
"SET",
|
|
43
|
+
"DELETE",
|
|
44
|
+
"CREATE",
|
|
45
|
+
"ALTER",
|
|
46
|
+
"DROP",
|
|
47
|
+
"TABLE",
|
|
48
|
+
"DATABASE",
|
|
49
|
+
"INDEX",
|
|
50
|
+
"VIEW",
|
|
51
|
+
"PRIMARY",
|
|
52
|
+
"KEY",
|
|
53
|
+
"FOREIGN",
|
|
54
|
+
"REFERENCES",
|
|
55
|
+
"CONSTRAINT",
|
|
56
|
+
"DEFAULT",
|
|
57
|
+
"UNIQUE",
|
|
58
|
+
"CASE",
|
|
59
|
+
"WHEN",
|
|
60
|
+
"THEN",
|
|
61
|
+
"ELSE",
|
|
62
|
+
"END",
|
|
63
|
+
"WITH",
|
|
64
|
+
"RECURSIVE",
|
|
65
|
+
"EXISTS",
|
|
66
|
+
"CAST",
|
|
67
|
+
"CONVERT",
|
|
68
|
+
"TRUE",
|
|
69
|
+
"FALSE"
|
|
70
|
+
],
|
|
71
|
+
functions: [
|
|
72
|
+
"COUNT",
|
|
73
|
+
"SUM",
|
|
74
|
+
"AVG",
|
|
75
|
+
"MIN",
|
|
76
|
+
"MAX",
|
|
77
|
+
"ROUND",
|
|
78
|
+
"CEIL",
|
|
79
|
+
"FLOOR",
|
|
80
|
+
"ABS",
|
|
81
|
+
"LENGTH",
|
|
82
|
+
"LOWER",
|
|
83
|
+
"UPPER",
|
|
84
|
+
"TRIM",
|
|
85
|
+
"SUBSTRING",
|
|
86
|
+
"CONCAT",
|
|
87
|
+
"REPLACE",
|
|
88
|
+
"NOW",
|
|
89
|
+
"CURRENT_DATE",
|
|
90
|
+
"CURRENT_TIMESTAMP",
|
|
91
|
+
"DATE",
|
|
92
|
+
"YEAR",
|
|
93
|
+
"MONTH",
|
|
94
|
+
"DAY"
|
|
95
|
+
],
|
|
96
|
+
operators: [
|
|
97
|
+
"=",
|
|
98
|
+
"!=",
|
|
99
|
+
"<>",
|
|
100
|
+
">",
|
|
101
|
+
"<",
|
|
102
|
+
">=",
|
|
103
|
+
"<=",
|
|
104
|
+
"+",
|
|
105
|
+
"-",
|
|
106
|
+
"*",
|
|
107
|
+
"/",
|
|
108
|
+
"%"
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
class y {
|
|
112
|
+
constructor(t = {}) {
|
|
113
|
+
this.setSchema(t);
|
|
114
|
+
}
|
|
115
|
+
setSchema(t = {}) {
|
|
116
|
+
this.schema = t || {};
|
|
117
|
+
}
|
|
118
|
+
getTables() {
|
|
119
|
+
return Object.keys(this.schema);
|
|
120
|
+
}
|
|
121
|
+
getColumns(t) {
|
|
122
|
+
const i = this.schema?.[t];
|
|
123
|
+
return Array.isArray(i) ? i : i && Array.isArray(i.columns) ? i.columns : [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function E(n, t) {
|
|
127
|
+
const i = [];
|
|
128
|
+
let e = 0;
|
|
129
|
+
for (; e < n.length; ) {
|
|
130
|
+
if (n[e] === "'") {
|
|
131
|
+
let o = e + 1;
|
|
132
|
+
for (; o < n.length; ) {
|
|
133
|
+
if (n[o] === "'" && n[o - 1] !== "\\") {
|
|
134
|
+
o++;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
o++;
|
|
138
|
+
}
|
|
139
|
+
i.push({
|
|
140
|
+
type: "string",
|
|
141
|
+
value: n.slice(e, o)
|
|
142
|
+
}), e = o;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (n[e] === "-" && n[e + 1] === "-") {
|
|
146
|
+
let o = n.indexOf(`
|
|
147
|
+
`, e);
|
|
148
|
+
o === -1 && (o = n.length), i.push({
|
|
149
|
+
type: "comment",
|
|
150
|
+
value: n.slice(e, o)
|
|
151
|
+
}), e = o;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const r = n.slice(e).match(/^\d+(?:\.\d+)?/);
|
|
155
|
+
if (r) {
|
|
156
|
+
i.push({
|
|
157
|
+
type: "number",
|
|
158
|
+
value: r[0]
|
|
159
|
+
}), e += r[0].length;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const c = n.slice(e).match(/^[A-Za-z_][A-Za-z0-9_]*/);
|
|
163
|
+
if (c) {
|
|
164
|
+
const o = c[0], h = o.toUpperCase();
|
|
165
|
+
let s = "identifier";
|
|
166
|
+
t.keywords.includes(h) ? s = "keyword" : t.functions.includes(h) && (s = "function"), i.push({
|
|
167
|
+
type: s,
|
|
168
|
+
value: o
|
|
169
|
+
}), e += o.length;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const a = n.slice(e).match(/^(<>|!=|>=|<=|=|>|<|\+|-|\*|\/|%)/);
|
|
173
|
+
if (a) {
|
|
174
|
+
i.push({
|
|
175
|
+
type: "operator",
|
|
176
|
+
value: a[0]
|
|
177
|
+
}), e += a[0].length;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
i.push({
|
|
181
|
+
type: "text",
|
|
182
|
+
value: n[e]
|
|
183
|
+
}), e++;
|
|
184
|
+
}
|
|
185
|
+
return i;
|
|
186
|
+
}
|
|
187
|
+
function b(n) {
|
|
188
|
+
return n.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
189
|
+
}
|
|
190
|
+
function S(n, t) {
|
|
191
|
+
return E(n, t).map((e) => {
|
|
192
|
+
const r = b(e.value);
|
|
193
|
+
switch (e.type) {
|
|
194
|
+
case "keyword":
|
|
195
|
+
return `<span class="sql-keyword">${r}</span>`;
|
|
196
|
+
case "function":
|
|
197
|
+
return `<span class="sql-function">${r}</span>`;
|
|
198
|
+
case "string":
|
|
199
|
+
return `<span class="sql-string">${r}</span>`;
|
|
200
|
+
case "number":
|
|
201
|
+
return `<span class="sql-number">${r}</span>`;
|
|
202
|
+
case "operator":
|
|
203
|
+
return `<span class="sql-operator">${r}</span>`;
|
|
204
|
+
case "comment":
|
|
205
|
+
return `<span class="sql-comment">${r}</span>`;
|
|
206
|
+
default:
|
|
207
|
+
return r;
|
|
208
|
+
}
|
|
209
|
+
}).join("");
|
|
210
|
+
}
|
|
211
|
+
class T {
|
|
212
|
+
constructor(t, i) {
|
|
213
|
+
this.dictionary = t, this.schemaDictionary = i;
|
|
214
|
+
}
|
|
215
|
+
getCurrentWord(t, i) {
|
|
216
|
+
const r = t.slice(0, i).match(/([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
217
|
+
return r ? r[1] : "";
|
|
218
|
+
}
|
|
219
|
+
getContext(t, i) {
|
|
220
|
+
const e = t.slice(0, i).toUpperCase(), r = this.getCurrentWord(
|
|
221
|
+
t,
|
|
222
|
+
i
|
|
223
|
+
), c = [
|
|
224
|
+
"ORDER BY",
|
|
225
|
+
"GROUP BY",
|
|
226
|
+
"LEFT JOIN",
|
|
227
|
+
"RIGHT JOIN",
|
|
228
|
+
"INNER JOIN",
|
|
229
|
+
"FULL JOIN",
|
|
230
|
+
"JOIN",
|
|
231
|
+
"WHERE",
|
|
232
|
+
"FROM",
|
|
233
|
+
"SELECT",
|
|
234
|
+
"ON"
|
|
235
|
+
];
|
|
236
|
+
let a = "keyword";
|
|
237
|
+
for (const o of c)
|
|
238
|
+
if (e.includes(o)) {
|
|
239
|
+
a = o;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
context: a,
|
|
244
|
+
currentWord: r
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
getSuggestions(t, i) {
|
|
248
|
+
const {
|
|
249
|
+
context: e,
|
|
250
|
+
currentWord: r
|
|
251
|
+
} = this.getContext(t, i), c = r.toLowerCase(), a = [], o = (s, l) => {
|
|
252
|
+
!s || !s.toLowerCase().startsWith(c) || a.push({
|
|
253
|
+
value: s,
|
|
254
|
+
type: l
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
e === "FROM" || e.includes("JOIN") ? (this.schemaDictionary.getTables().forEach((s) => o(s, "table")), this.dictionary.keywords.forEach((s) => {
|
|
258
|
+
["JOIN", "INNER", "LEFT", "RIGHT", "FULL", "CROSS"].includes(s) && o(s, "keyword");
|
|
259
|
+
})) : e === "WHERE" || e === "ON" ? (this.dictionary.keywords.filter(
|
|
260
|
+
(s) => ["AND", "OR", "NOT", "IN", "LIKE", "IS", "BETWEEN", "NULL"].includes(s)
|
|
261
|
+
).forEach((s) => o(s, "keyword")), this.dictionary.operators.forEach((s) => o(s, "operator")), this.schemaDictionary.getTables().forEach((s) => {
|
|
262
|
+
this.schemaDictionary.getColumns(s).forEach((l) => o(l, "column"));
|
|
263
|
+
})) : (this.dictionary.keywords.forEach((s) => o(s, "keyword")), this.dictionary.functions.forEach((s) => o(s, "function")), this.schemaDictionary.getTables().forEach((s) => o(s, "table")), this.schemaDictionary.getTables().forEach((s) => {
|
|
264
|
+
this.schemaDictionary.getColumns(s).forEach((l) => o(l, "column"));
|
|
265
|
+
}));
|
|
266
|
+
const h = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const s of a) {
|
|
268
|
+
const l = `${s.type}:${s.value.toLowerCase()}`;
|
|
269
|
+
h.has(l) || h.set(l, s);
|
|
270
|
+
}
|
|
271
|
+
return [...h.values()].sort((s, l) => {
|
|
272
|
+
const p = s.value.toLowerCase() === c, u = l.value.toLowerCase() === c;
|
|
273
|
+
return p !== u ? p ? -1 : 1 : s.value.length - l.value.length;
|
|
274
|
+
}).slice(0, 10);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
class x extends HTMLElement {
|
|
278
|
+
static get observedAttributes() {
|
|
279
|
+
return ["query", "schema"];
|
|
280
|
+
}
|
|
281
|
+
constructor() {
|
|
282
|
+
super(), this.attachShadow({ mode: "open" }), this.schemaDictionary = new y(), this.autocompleteEngine = new T(
|
|
283
|
+
f,
|
|
284
|
+
this.schemaDictionary
|
|
285
|
+
), this.suggestions = [], this.selectedIndex = 0, this.initialized = !1, this._pendingQuery = void 0;
|
|
286
|
+
}
|
|
287
|
+
connectedCallback() {
|
|
288
|
+
if (this.initialized)
|
|
289
|
+
return;
|
|
290
|
+
this.initialized = !0, this.render(), this.attachEvents();
|
|
291
|
+
const t = this._pendingQuery !== void 0 ? this._pendingQuery : this.getAttribute("query") || "";
|
|
292
|
+
this._pendingQuery = void 0, t ? this.setQuery(t) : this.updateHighlight();
|
|
293
|
+
}
|
|
294
|
+
attributeChangedCallback(t, i, e) {
|
|
295
|
+
if (i !== e) {
|
|
296
|
+
if (t === "query") {
|
|
297
|
+
this.query = e || "";
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (t === "schema") {
|
|
301
|
+
if (!e) {
|
|
302
|
+
this.setSchema({});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
this.setSchema(JSON.parse(e));
|
|
307
|
+
} catch (r) {
|
|
308
|
+
console.warn(
|
|
309
|
+
'sql-query-editor: could not parse "schema" attribute as JSON',
|
|
310
|
+
r
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
render() {
|
|
317
|
+
this.shadowRoot.innerHTML = `
|
|
318
|
+
<style>
|
|
319
|
+
:host {
|
|
320
|
+
display: block;
|
|
321
|
+
width: 100%;
|
|
322
|
+
--sql-editor-border: #d0d5dd;
|
|
323
|
+
--sql-editor-background: #ffffff;
|
|
324
|
+
--sql-editor-text: #1f2937;
|
|
325
|
+
--sql-editor-keyword: #0066cc;
|
|
326
|
+
--sql-editor-function: #8e44ad;
|
|
327
|
+
--sql-editor-string: #008000;
|
|
328
|
+
--sql-editor-number: #d35400;
|
|
329
|
+
--sql-editor-operator: #c0392b;
|
|
330
|
+
--sql-editor-comment: #777777;
|
|
331
|
+
--sql-editor-active: #e8f0fe;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.editor {
|
|
335
|
+
position: relative;
|
|
336
|
+
width: 100%;
|
|
337
|
+
height: 350px;
|
|
338
|
+
border: 1px solid var(--sql-editor-border);
|
|
339
|
+
border-radius: 8px;
|
|
340
|
+
overflow: hidden;
|
|
341
|
+
background: var(--sql-editor-background);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
textarea,
|
|
345
|
+
.highlight {
|
|
346
|
+
position: absolute;
|
|
347
|
+
inset: 0;
|
|
348
|
+
width: 100%;
|
|
349
|
+
height: 100%;
|
|
350
|
+
margin: 0;
|
|
351
|
+
padding: 16px;
|
|
352
|
+
border: 0;
|
|
353
|
+
outline: 0;
|
|
354
|
+
box-sizing: border-box;
|
|
355
|
+
font-family: Consolas, Monaco, "Courier New", monospace;
|
|
356
|
+
font-size: 14px;
|
|
357
|
+
line-height: 1.6;
|
|
358
|
+
letter-spacing: normal;
|
|
359
|
+
white-space: pre-wrap;
|
|
360
|
+
overflow: auto;
|
|
361
|
+
tab-size: 2;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.highlight {
|
|
365
|
+
z-index: 1;
|
|
366
|
+
pointer-events: none;
|
|
367
|
+
color: var(--sql-editor-text);
|
|
368
|
+
overflow: hidden;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
textarea {
|
|
372
|
+
z-index: 2;
|
|
373
|
+
resize: none;
|
|
374
|
+
background: transparent;
|
|
375
|
+
color: transparent;
|
|
376
|
+
caret-color: var(--sql-editor-text);
|
|
377
|
+
-webkit-text-fill-color: transparent;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
textarea::placeholder {
|
|
381
|
+
color: #98a2b3;
|
|
382
|
+
-webkit-text-fill-color: #98a2b3;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.sql-keyword {
|
|
386
|
+
color: var(--sql-editor-keyword);
|
|
387
|
+
font-weight: 700;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.sql-function {
|
|
391
|
+
color: var(--sql-editor-function);
|
|
392
|
+
font-weight: 700;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.sql-string {
|
|
396
|
+
color: var(--sql-editor-string);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.sql-number {
|
|
400
|
+
color: var(--sql-editor-number);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.sql-operator {
|
|
404
|
+
color: var(--sql-editor-operator);
|
|
405
|
+
font-weight: 700;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.sql-comment {
|
|
409
|
+
color: var(--sql-editor-comment);
|
|
410
|
+
font-style: italic;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.autocomplete {
|
|
414
|
+
position: absolute;
|
|
415
|
+
display: none;
|
|
416
|
+
z-index: 10;
|
|
417
|
+
width: 280px;
|
|
418
|
+
max-height: 220px;
|
|
419
|
+
overflow-y: auto;
|
|
420
|
+
background: #ffffff;
|
|
421
|
+
border: 1px solid var(--sql-editor-border);
|
|
422
|
+
border-radius: 6px;
|
|
423
|
+
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.14);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.suggestion {
|
|
427
|
+
display: flex;
|
|
428
|
+
align-items: center;
|
|
429
|
+
justify-content: space-between;
|
|
430
|
+
gap: 16px;
|
|
431
|
+
padding: 8px 12px;
|
|
432
|
+
cursor: pointer;
|
|
433
|
+
font-family: Consolas, Monaco, "Courier New", monospace;
|
|
434
|
+
font-size: 13px;
|
|
435
|
+
color: #1f2937;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.suggestion:hover,
|
|
439
|
+
.suggestion.active {
|
|
440
|
+
background: var(--sql-editor-active);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.suggestion-type {
|
|
444
|
+
color: #98a2b3;
|
|
445
|
+
font-size: 10px;
|
|
446
|
+
text-transform: uppercase;
|
|
447
|
+
}
|
|
448
|
+
</style>
|
|
449
|
+
|
|
450
|
+
<div class="editor">
|
|
451
|
+
<div class="highlight" aria-hidden="true"></div>
|
|
452
|
+
|
|
453
|
+
<textarea
|
|
454
|
+
spellcheck="false"
|
|
455
|
+
autocomplete="off"
|
|
456
|
+
autocorrect="off"
|
|
457
|
+
autocapitalize="off"
|
|
458
|
+
placeholder="Write SQL query..."
|
|
459
|
+
aria-label="SQL query editor"
|
|
460
|
+
></textarea>
|
|
461
|
+
|
|
462
|
+
<div
|
|
463
|
+
class="autocomplete"
|
|
464
|
+
role="listbox"
|
|
465
|
+
></div>
|
|
466
|
+
</div>
|
|
467
|
+
`, this.editor = this.shadowRoot.querySelector("textarea"), this.highlight = this.shadowRoot.querySelector(".highlight"), this.autocomplete = this.shadowRoot.querySelector(".autocomplete");
|
|
468
|
+
}
|
|
469
|
+
attachEvents() {
|
|
470
|
+
this.editor.addEventListener("input", () => {
|
|
471
|
+
this.updateHighlight(), this.updateAutocomplete(), this.emitQueryChange();
|
|
472
|
+
}), this.editor.addEventListener("click", () => {
|
|
473
|
+
this.updateAutocomplete();
|
|
474
|
+
}), this.editor.addEventListener("keyup", (t) => {
|
|
475
|
+
[
|
|
476
|
+
"ArrowUp",
|
|
477
|
+
"ArrowDown",
|
|
478
|
+
"Enter",
|
|
479
|
+
"Tab",
|
|
480
|
+
"Escape"
|
|
481
|
+
].includes(t.key) || this.updateAutocomplete();
|
|
482
|
+
}), this.editor.addEventListener("scroll", () => {
|
|
483
|
+
this.syncScroll(), this.isAutocompleteVisible() && this.positionAutocomplete();
|
|
484
|
+
}), this.editor.addEventListener("keydown", (t) => {
|
|
485
|
+
this.handleKeyboard(t);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
handleKeyboard(t) {
|
|
489
|
+
if ((t.ctrlKey || t.metaKey) && t.key === "Enter") {
|
|
490
|
+
t.preventDefault(), this.emitQuerySubmit();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (!(!this.isAutocompleteVisible() || !this.suggestions.length)) {
|
|
494
|
+
if (t.key === "ArrowDown") {
|
|
495
|
+
t.preventDefault(), this.selectedIndex = (this.selectedIndex + 1) % this.suggestions.length, this.renderSuggestions();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (t.key === "ArrowUp") {
|
|
499
|
+
t.preventDefault(), this.selectedIndex--, this.selectedIndex < 0 && (this.selectedIndex = this.suggestions.length - 1), this.renderSuggestions();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (t.key === "Enter" || t.key === "Tab") {
|
|
503
|
+
t.preventDefault(), this.applySuggestion(
|
|
504
|
+
this.selectedIndex
|
|
505
|
+
);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
t.key === "Escape" && (t.preventDefault(), this.hideAutocomplete());
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
updateHighlight() {
|
|
512
|
+
const t = S(
|
|
513
|
+
this.editor.value,
|
|
514
|
+
f
|
|
515
|
+
);
|
|
516
|
+
this.highlight.innerHTML = t || " ", this.syncScroll();
|
|
517
|
+
}
|
|
518
|
+
syncScroll() {
|
|
519
|
+
this.highlight.scrollTop = this.editor.scrollTop, this.highlight.scrollLeft = this.editor.scrollLeft;
|
|
520
|
+
}
|
|
521
|
+
updateAutocomplete() {
|
|
522
|
+
if (this.suggestions = this.autocompleteEngine.getSuggestions(
|
|
523
|
+
this.editor.value,
|
|
524
|
+
this.editor.selectionStart
|
|
525
|
+
), !this.suggestions.length) {
|
|
526
|
+
this.hideAutocomplete();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.selectedIndex = 0, this.renderSuggestions(), this.positionAutocomplete();
|
|
530
|
+
}
|
|
531
|
+
renderSuggestions() {
|
|
532
|
+
this.autocomplete.innerHTML = "", this.suggestions.forEach(
|
|
533
|
+
(t, i) => {
|
|
534
|
+
const e = document.createElement("div");
|
|
535
|
+
e.className = "suggestion", e.setAttribute(
|
|
536
|
+
"role",
|
|
537
|
+
"option"
|
|
538
|
+
), i === this.selectedIndex && e.classList.add("active");
|
|
539
|
+
const r = document.createElement("span");
|
|
540
|
+
r.textContent = t.value;
|
|
541
|
+
const c = document.createElement("span");
|
|
542
|
+
c.className = "suggestion-type", c.textContent = t.type, e.append(
|
|
543
|
+
r,
|
|
544
|
+
c
|
|
545
|
+
), e.addEventListener(
|
|
546
|
+
"mousedown",
|
|
547
|
+
(a) => {
|
|
548
|
+
a.preventDefault(), this.applySuggestion(i);
|
|
549
|
+
}
|
|
550
|
+
), this.autocomplete.appendChild(
|
|
551
|
+
e
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
), this.autocomplete.style.display = "block";
|
|
555
|
+
}
|
|
556
|
+
positionAutocomplete() {
|
|
557
|
+
const t = this.editor.selectionStart, e = this.editor.value.slice(0, t).split(`
|
|
558
|
+
`), r = e.length - 1, c = e[e.length - 1].length, a = getComputedStyle(this.editor), o = parseFloat(a.lineHeight) || 22, h = parseFloat(a.fontSize) || 14, s = parseFloat(a.paddingLeft) || 16, l = parseFloat(a.paddingTop) || 16, p = h * 0.6;
|
|
559
|
+
let u = s + c * p - this.editor.scrollLeft, d = l + (r + 1) * o - this.editor.scrollTop;
|
|
560
|
+
const g = this.editor.clientWidth - this.autocomplete.offsetWidth - 8;
|
|
561
|
+
u > g && (u = g), u = Math.max(4, u);
|
|
562
|
+
const m = this.editor.clientHeight - this.autocomplete.offsetHeight - 8;
|
|
563
|
+
d > m && (d = l + r * o - this.autocomplete.offsetHeight - 4 - this.editor.scrollTop), d = Math.max(4, d), this.autocomplete.style.left = `${u}px`, this.autocomplete.style.top = `${d}px`;
|
|
564
|
+
}
|
|
565
|
+
applySuggestion(t) {
|
|
566
|
+
const i = this.suggestions[t];
|
|
567
|
+
if (!i)
|
|
568
|
+
return;
|
|
569
|
+
const e = this.editor.selectionStart, c = this.editor.value.slice(0, e).match(
|
|
570
|
+
/([A-Za-z_][A-Za-z0-9_]*)$/
|
|
571
|
+
), a = c ? c[1] : "", o = e - a.length;
|
|
572
|
+
this.editor.setRangeText(
|
|
573
|
+
i.value,
|
|
574
|
+
o,
|
|
575
|
+
e,
|
|
576
|
+
"end"
|
|
577
|
+
), this.hideAutocomplete(), this.updateHighlight(), this.emitQueryChange(), this.editor.focus();
|
|
578
|
+
}
|
|
579
|
+
isAutocompleteVisible() {
|
|
580
|
+
return this.autocomplete.style.display === "block";
|
|
581
|
+
}
|
|
582
|
+
hideAutocomplete() {
|
|
583
|
+
this.autocomplete.style.display = "none", this.suggestions = [];
|
|
584
|
+
}
|
|
585
|
+
emitQueryChange() {
|
|
586
|
+
this.dispatchEvent(
|
|
587
|
+
new CustomEvent("query-change", {
|
|
588
|
+
detail: {
|
|
589
|
+
query: this.editor.value
|
|
590
|
+
}
|
|
591
|
+
})
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
emitQuerySubmit() {
|
|
595
|
+
this.dispatchEvent(
|
|
596
|
+
new CustomEvent("query-submit", {
|
|
597
|
+
detail: {
|
|
598
|
+
query: this.editor.value
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
getQuery() {
|
|
604
|
+
return this.editor.value;
|
|
605
|
+
}
|
|
606
|
+
setQuery(t = "") {
|
|
607
|
+
this.editor.value = String(t), this.updateHighlight(), this.hideAutocomplete();
|
|
608
|
+
}
|
|
609
|
+
clear() {
|
|
610
|
+
this.setQuery("");
|
|
611
|
+
}
|
|
612
|
+
focus() {
|
|
613
|
+
this.editor.focus();
|
|
614
|
+
}
|
|
615
|
+
setSchema(t = {}) {
|
|
616
|
+
this.schemaDictionary.setSchema(t), this.autocompleteEngine.schemaDictionary = this.schemaDictionary;
|
|
617
|
+
}
|
|
618
|
+
get query() {
|
|
619
|
+
return this.editor ? this.getQuery() : this._pendingQuery || "";
|
|
620
|
+
}
|
|
621
|
+
set query(t) {
|
|
622
|
+
const i = t == null ? "" : String(t);
|
|
623
|
+
if (!this.editor) {
|
|
624
|
+
this._pendingQuery = i;
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this.setQuery(i);
|
|
628
|
+
}
|
|
629
|
+
get schema() {
|
|
630
|
+
return this.schemaDictionary.schema;
|
|
631
|
+
}
|
|
632
|
+
set schema(t) {
|
|
633
|
+
this.setSchema(t || {});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
customElements.get("sql-query-editor") || customElements.define("sql-query-editor", x);
|
|
637
|
+
export {
|
|
638
|
+
x as SqlQueryEditor
|
|
639
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
(function(d,p){typeof exports=="object"&&typeof module<"u"?p(exports):typeof define=="function"&&define.amd?define(["exports"],p):(d=typeof globalThis<"u"?globalThis:d||self,p(d.SqlQueryEditor={}))})(this,(function(d){"use strict";const p={keywords:["SELECT","FROM","WHERE","AND","OR","NOT","NULL","IS","IN","BETWEEN","LIKE","AS","DISTINCT","ALL","TOP","LIMIT","OFFSET","ORDER","BY","ASC","DESC","GROUP","HAVING","JOIN","INNER","LEFT","RIGHT","FULL","OUTER","CROSS","ON","UNION","UNION ALL","INTERSECT","EXCEPT","INSERT","INTO","VALUES","UPDATE","SET","DELETE","CREATE","ALTER","DROP","TABLE","DATABASE","INDEX","VIEW","PRIMARY","KEY","FOREIGN","REFERENCES","CONSTRAINT","DEFAULT","UNIQUE","CASE","WHEN","THEN","ELSE","END","WITH","RECURSIVE","EXISTS","CAST","CONVERT","TRUE","FALSE"],functions:["COUNT","SUM","AVG","MIN","MAX","ROUND","CEIL","FLOOR","ABS","LENGTH","LOWER","UPPER","TRIM","SUBSTRING","CONCAT","REPLACE","NOW","CURRENT_DATE","CURRENT_TIMESTAMP","DATE","YEAR","MONTH","DAY"],operators:["=","!=","<>",">","<",">=","<=","+","-","*","/","%"]};class E{constructor(e={}){this.setSchema(e)}setSchema(e={}){this.schema=e||{}}getTables(){return Object.keys(this.schema)}getColumns(e){const s=this.schema?.[e];return Array.isArray(s)?s:s&&Array.isArray(s.columns)?s.columns:[]}}function b(n,e){const s=[];let t=0;for(;t<n.length;){if(n[t]==="'"){let o=t+1;for(;o<n.length;){if(n[o]==="'"&&n[o-1]!=="\\"){o++;break}o++}s.push({type:"string",value:n.slice(t,o)}),t=o;continue}if(n[t]==="-"&&n[t+1]==="-"){let o=n.indexOf(`
|
|
2
|
+
`,t);o===-1&&(o=n.length),s.push({type:"comment",value:n.slice(t,o)}),t=o;continue}const r=n.slice(t).match(/^\d+(?:\.\d+)?/);if(r){s.push({type:"number",value:r[0]}),t+=r[0].length;continue}const c=n.slice(t).match(/^[A-Za-z_][A-Za-z0-9_]*/);if(c){const o=c[0],h=o.toUpperCase();let i="identifier";e.keywords.includes(h)?i="keyword":e.functions.includes(h)&&(i="function"),s.push({type:i,value:o}),t+=o.length;continue}const a=n.slice(t).match(/^(<>|!=|>=|<=|=|>|<|\+|-|\*|\/|%)/);if(a){s.push({type:"operator",value:a[0]}),t+=a[0].length;continue}s.push({type:"text",value:n[t]}),t++}return s}function S(n){return n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function T(n,e){return b(n,e).map(t=>{const r=S(t.value);switch(t.type){case"keyword":return`<span class="sql-keyword">${r}</span>`;case"function":return`<span class="sql-function">${r}</span>`;case"string":return`<span class="sql-string">${r}</span>`;case"number":return`<span class="sql-number">${r}</span>`;case"operator":return`<span class="sql-operator">${r}</span>`;case"comment":return`<span class="sql-comment">${r}</span>`;default:return r}}).join("")}class x{constructor(e,s){this.dictionary=e,this.schemaDictionary=s}getCurrentWord(e,s){const r=e.slice(0,s).match(/([A-Za-z_][A-Za-z0-9_]*)$/);return r?r[1]:""}getContext(e,s){const t=e.slice(0,s).toUpperCase(),r=this.getCurrentWord(e,s),c=["ORDER BY","GROUP BY","LEFT JOIN","RIGHT JOIN","INNER JOIN","FULL JOIN","JOIN","WHERE","FROM","SELECT","ON"];let a="keyword";for(const o of c)if(t.includes(o)){a=o;break}return{context:a,currentWord:r}}getSuggestions(e,s){const{context:t,currentWord:r}=this.getContext(e,s),c=r.toLowerCase(),a=[],o=(i,l)=>{!i||!i.toLowerCase().startsWith(c)||a.push({value:i,type:l})};t==="FROM"||t.includes("JOIN")?(this.schemaDictionary.getTables().forEach(i=>o(i,"table")),this.dictionary.keywords.forEach(i=>{["JOIN","INNER","LEFT","RIGHT","FULL","CROSS"].includes(i)&&o(i,"keyword")})):t==="WHERE"||t==="ON"?(this.dictionary.keywords.filter(i=>["AND","OR","NOT","IN","LIKE","IS","BETWEEN","NULL"].includes(i)).forEach(i=>o(i,"keyword")),this.dictionary.operators.forEach(i=>o(i,"operator")),this.schemaDictionary.getTables().forEach(i=>{this.schemaDictionary.getColumns(i).forEach(l=>o(l,"column"))})):(this.dictionary.keywords.forEach(i=>o(i,"keyword")),this.dictionary.functions.forEach(i=>o(i,"function")),this.schemaDictionary.getTables().forEach(i=>o(i,"table")),this.schemaDictionary.getTables().forEach(i=>{this.schemaDictionary.getColumns(i).forEach(l=>o(l,"column"))}));const h=new Map;for(const i of a){const l=`${i.type}:${i.value.toLowerCase()}`;h.has(l)||h.set(l,i)}return[...h.values()].sort((i,l)=>{const f=i.value.toLowerCase()===c,u=l.value.toLowerCase()===c;return f!==u?f?-1:1:i.value.length-l.value.length}).slice(0,10)}}class m extends HTMLElement{static get observedAttributes(){return["query","schema"]}constructor(){super(),this.attachShadow({mode:"open"}),this.schemaDictionary=new E,this.autocompleteEngine=new x(p,this.schemaDictionary),this.suggestions=[],this.selectedIndex=0,this.initialized=!1,this._pendingQuery=void 0}connectedCallback(){if(this.initialized)return;this.initialized=!0,this.render(),this.attachEvents();const e=this._pendingQuery!==void 0?this._pendingQuery:this.getAttribute("query")||"";this._pendingQuery=void 0,e?this.setQuery(e):this.updateHighlight()}attributeChangedCallback(e,s,t){if(s!==t){if(e==="query"){this.query=t||"";return}if(e==="schema"){if(!t){this.setSchema({});return}try{this.setSchema(JSON.parse(t))}catch(r){console.warn('sql-query-editor: could not parse "schema" attribute as JSON',r)}}}}render(){this.shadowRoot.innerHTML=`
|
|
3
|
+
<style>
|
|
4
|
+
:host {
|
|
5
|
+
display: block;
|
|
6
|
+
width: 100%;
|
|
7
|
+
--sql-editor-border: #d0d5dd;
|
|
8
|
+
--sql-editor-background: #ffffff;
|
|
9
|
+
--sql-editor-text: #1f2937;
|
|
10
|
+
--sql-editor-keyword: #0066cc;
|
|
11
|
+
--sql-editor-function: #8e44ad;
|
|
12
|
+
--sql-editor-string: #008000;
|
|
13
|
+
--sql-editor-number: #d35400;
|
|
14
|
+
--sql-editor-operator: #c0392b;
|
|
15
|
+
--sql-editor-comment: #777777;
|
|
16
|
+
--sql-editor-active: #e8f0fe;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.editor {
|
|
20
|
+
position: relative;
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 350px;
|
|
23
|
+
border: 1px solid var(--sql-editor-border);
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
background: var(--sql-editor-background);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
textarea,
|
|
30
|
+
.highlight {
|
|
31
|
+
position: absolute;
|
|
32
|
+
inset: 0;
|
|
33
|
+
width: 100%;
|
|
34
|
+
height: 100%;
|
|
35
|
+
margin: 0;
|
|
36
|
+
padding: 16px;
|
|
37
|
+
border: 0;
|
|
38
|
+
outline: 0;
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
font-family: Consolas, Monaco, "Courier New", monospace;
|
|
41
|
+
font-size: 14px;
|
|
42
|
+
line-height: 1.6;
|
|
43
|
+
letter-spacing: normal;
|
|
44
|
+
white-space: pre-wrap;
|
|
45
|
+
overflow: auto;
|
|
46
|
+
tab-size: 2;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.highlight {
|
|
50
|
+
z-index: 1;
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
color: var(--sql-editor-text);
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
textarea {
|
|
57
|
+
z-index: 2;
|
|
58
|
+
resize: none;
|
|
59
|
+
background: transparent;
|
|
60
|
+
color: transparent;
|
|
61
|
+
caret-color: var(--sql-editor-text);
|
|
62
|
+
-webkit-text-fill-color: transparent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
textarea::placeholder {
|
|
66
|
+
color: #98a2b3;
|
|
67
|
+
-webkit-text-fill-color: #98a2b3;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.sql-keyword {
|
|
71
|
+
color: var(--sql-editor-keyword);
|
|
72
|
+
font-weight: 700;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.sql-function {
|
|
76
|
+
color: var(--sql-editor-function);
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.sql-string {
|
|
81
|
+
color: var(--sql-editor-string);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.sql-number {
|
|
85
|
+
color: var(--sql-editor-number);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.sql-operator {
|
|
89
|
+
color: var(--sql-editor-operator);
|
|
90
|
+
font-weight: 700;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.sql-comment {
|
|
94
|
+
color: var(--sql-editor-comment);
|
|
95
|
+
font-style: italic;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.autocomplete {
|
|
99
|
+
position: absolute;
|
|
100
|
+
display: none;
|
|
101
|
+
z-index: 10;
|
|
102
|
+
width: 280px;
|
|
103
|
+
max-height: 220px;
|
|
104
|
+
overflow-y: auto;
|
|
105
|
+
background: #ffffff;
|
|
106
|
+
border: 1px solid var(--sql-editor-border);
|
|
107
|
+
border-radius: 6px;
|
|
108
|
+
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.14);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.suggestion {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: space-between;
|
|
115
|
+
gap: 16px;
|
|
116
|
+
padding: 8px 12px;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
font-family: Consolas, Monaco, "Courier New", monospace;
|
|
119
|
+
font-size: 13px;
|
|
120
|
+
color: #1f2937;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.suggestion:hover,
|
|
124
|
+
.suggestion.active {
|
|
125
|
+
background: var(--sql-editor-active);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.suggestion-type {
|
|
129
|
+
color: #98a2b3;
|
|
130
|
+
font-size: 10px;
|
|
131
|
+
text-transform: uppercase;
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
134
|
+
|
|
135
|
+
<div class="editor">
|
|
136
|
+
<div class="highlight" aria-hidden="true"></div>
|
|
137
|
+
|
|
138
|
+
<textarea
|
|
139
|
+
spellcheck="false"
|
|
140
|
+
autocomplete="off"
|
|
141
|
+
autocorrect="off"
|
|
142
|
+
autocapitalize="off"
|
|
143
|
+
placeholder="Write SQL query..."
|
|
144
|
+
aria-label="SQL query editor"
|
|
145
|
+
></textarea>
|
|
146
|
+
|
|
147
|
+
<div
|
|
148
|
+
class="autocomplete"
|
|
149
|
+
role="listbox"
|
|
150
|
+
></div>
|
|
151
|
+
</div>
|
|
152
|
+
`,this.editor=this.shadowRoot.querySelector("textarea"),this.highlight=this.shadowRoot.querySelector(".highlight"),this.autocomplete=this.shadowRoot.querySelector(".autocomplete")}attachEvents(){this.editor.addEventListener("input",()=>{this.updateHighlight(),this.updateAutocomplete(),this.emitQueryChange()}),this.editor.addEventListener("click",()=>{this.updateAutocomplete()}),this.editor.addEventListener("keyup",e=>{["ArrowUp","ArrowDown","Enter","Tab","Escape"].includes(e.key)||this.updateAutocomplete()}),this.editor.addEventListener("scroll",()=>{this.syncScroll(),this.isAutocompleteVisible()&&this.positionAutocomplete()}),this.editor.addEventListener("keydown",e=>{this.handleKeyboard(e)})}handleKeyboard(e){if((e.ctrlKey||e.metaKey)&&e.key==="Enter"){e.preventDefault(),this.emitQuerySubmit();return}if(!(!this.isAutocompleteVisible()||!this.suggestions.length)){if(e.key==="ArrowDown"){e.preventDefault(),this.selectedIndex=(this.selectedIndex+1)%this.suggestions.length,this.renderSuggestions();return}if(e.key==="ArrowUp"){e.preventDefault(),this.selectedIndex--,this.selectedIndex<0&&(this.selectedIndex=this.suggestions.length-1),this.renderSuggestions();return}if(e.key==="Enter"||e.key==="Tab"){e.preventDefault(),this.applySuggestion(this.selectedIndex);return}e.key==="Escape"&&(e.preventDefault(),this.hideAutocomplete())}}updateHighlight(){const e=T(this.editor.value,p);this.highlight.innerHTML=e||" ",this.syncScroll()}syncScroll(){this.highlight.scrollTop=this.editor.scrollTop,this.highlight.scrollLeft=this.editor.scrollLeft}updateAutocomplete(){if(this.suggestions=this.autocompleteEngine.getSuggestions(this.editor.value,this.editor.selectionStart),!this.suggestions.length){this.hideAutocomplete();return}this.selectedIndex=0,this.renderSuggestions(),this.positionAutocomplete()}renderSuggestions(){this.autocomplete.innerHTML="",this.suggestions.forEach((e,s)=>{const t=document.createElement("div");t.className="suggestion",t.setAttribute("role","option"),s===this.selectedIndex&&t.classList.add("active");const r=document.createElement("span");r.textContent=e.value;const c=document.createElement("span");c.className="suggestion-type",c.textContent=e.type,t.append(r,c),t.addEventListener("mousedown",a=>{a.preventDefault(),this.applySuggestion(s)}),this.autocomplete.appendChild(t)}),this.autocomplete.style.display="block"}positionAutocomplete(){const e=this.editor.selectionStart,t=this.editor.value.slice(0,e).split(`
|
|
153
|
+
`),r=t.length-1,c=t[t.length-1].length,a=getComputedStyle(this.editor),o=parseFloat(a.lineHeight)||22,h=parseFloat(a.fontSize)||14,i=parseFloat(a.paddingLeft)||16,l=parseFloat(a.paddingTop)||16,f=h*.6;let u=i+c*f-this.editor.scrollLeft,g=l+(r+1)*o-this.editor.scrollTop;const y=this.editor.clientWidth-this.autocomplete.offsetWidth-8;u>y&&(u=y),u=Math.max(4,u);const A=this.editor.clientHeight-this.autocomplete.offsetHeight-8;g>A&&(g=l+r*o-this.autocomplete.offsetHeight-4-this.editor.scrollTop),g=Math.max(4,g),this.autocomplete.style.left=`${u}px`,this.autocomplete.style.top=`${g}px`}applySuggestion(e){const s=this.suggestions[e];if(!s)return;const t=this.editor.selectionStart,c=this.editor.value.slice(0,t).match(/([A-Za-z_][A-Za-z0-9_]*)$/),a=c?c[1]:"",o=t-a.length;this.editor.setRangeText(s.value,o,t,"end"),this.hideAutocomplete(),this.updateHighlight(),this.emitQueryChange(),this.editor.focus()}isAutocompleteVisible(){return this.autocomplete.style.display==="block"}hideAutocomplete(){this.autocomplete.style.display="none",this.suggestions=[]}emitQueryChange(){this.dispatchEvent(new CustomEvent("query-change",{detail:{query:this.editor.value}}))}emitQuerySubmit(){this.dispatchEvent(new CustomEvent("query-submit",{detail:{query:this.editor.value}}))}getQuery(){return this.editor.value}setQuery(e=""){this.editor.value=String(e),this.updateHighlight(),this.hideAutocomplete()}clear(){this.setQuery("")}focus(){this.editor.focus()}setSchema(e={}){this.schemaDictionary.setSchema(e),this.autocompleteEngine.schemaDictionary=this.schemaDictionary}get query(){return this.editor?this.getQuery():this._pendingQuery||""}set query(e){const s=e==null?"":String(e);if(!this.editor){this._pendingQuery=s;return}this.setQuery(s)}get schema(){return this.schemaDictionary.schema}set schema(e){this.setSchema(e||{})}}customElements.get("sql-query-editor")||customElements.define("sql-query-editor",m),d.SqlQueryEditor=m,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zwayam/sql-query-editor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Framework-agnostic SQL query editor Web Component with syntax highlighting and autocomplete.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/sql-query-editor.umd.cjs",
|
|
10
|
+
"module": "./dist/sql-query-editor.js",
|
|
11
|
+
"types": "./sql-query-editor.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./sql-query-editor.d.ts",
|
|
15
|
+
"import": "./dist/sql-query-editor.js",
|
|
16
|
+
"require": "./dist/sql-query-editor.umd.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"sql-query-editor.d.ts"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "vite",
|
|
25
|
+
"build": "vite build",
|
|
26
|
+
"preview": "vite preview"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"sql",
|
|
30
|
+
"query-editor",
|
|
31
|
+
"sql-editor",
|
|
32
|
+
"autocomplete",
|
|
33
|
+
"syntax-highlighting",
|
|
34
|
+
"web-component",
|
|
35
|
+
"framework-agnostic"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"vite": "^7.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type SqlSchema = Record<
|
|
2
|
+
string,
|
|
3
|
+
string[] | { columns: string[] }
|
|
4
|
+
>;
|
|
5
|
+
|
|
6
|
+
export interface QueryChangeEventDetail {
|
|
7
|
+
query: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export declare class SqlQueryEditor extends HTMLElement {
|
|
11
|
+
/** Current SQL text. Also settable/gettable as the `query` attribute. */
|
|
12
|
+
query: string;
|
|
13
|
+
|
|
14
|
+
/** Tables/columns used by autocomplete. Also settable as a JSON `schema` attribute. */
|
|
15
|
+
schema: SqlSchema;
|
|
16
|
+
|
|
17
|
+
getQuery(): string;
|
|
18
|
+
setQuery(query?: string): void;
|
|
19
|
+
clear(): void;
|
|
20
|
+
focus(): void;
|
|
21
|
+
setSchema(schema?: SqlSchema): void;
|
|
22
|
+
|
|
23
|
+
addEventListener<K extends "query-change" | "query-submit">(
|
|
24
|
+
type: K,
|
|
25
|
+
listener: (event: CustomEvent<QueryChangeEventDetail>) => void,
|
|
26
|
+
options?: boolean | AddEventListenerOptions
|
|
27
|
+
): void;
|
|
28
|
+
addEventListener(
|
|
29
|
+
type: string,
|
|
30
|
+
listener: EventListenerOrEventListenerObject,
|
|
31
|
+
options?: boolean | AddEventListenerOptions
|
|
32
|
+
): void;
|
|
33
|
+
|
|
34
|
+
removeEventListener<K extends "query-change" | "query-submit">(
|
|
35
|
+
type: K,
|
|
36
|
+
listener: (event: CustomEvent<QueryChangeEventDetail>) => void,
|
|
37
|
+
options?: boolean | EventListenerOptions
|
|
38
|
+
): void;
|
|
39
|
+
removeEventListener(
|
|
40
|
+
type: string,
|
|
41
|
+
listener: EventListenerOrEventListenerObject,
|
|
42
|
+
options?: boolean | EventListenerOptions
|
|
43
|
+
): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare global {
|
|
47
|
+
interface HTMLElementTagNameMap {
|
|
48
|
+
"sql-query-editor": SqlQueryEditor;
|
|
49
|
+
}
|
|
50
|
+
}
|