@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 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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 || "&nbsp;", 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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}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||"&nbsp;",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
+ }