mirror of
https://github.com/simonw/datasette.git
synced 2026-07-13 02:54:44 +02:00
Use <datasette-sql-editor> on core SQL pages
The five SQL pages now render the element wrapping a plain <textarea name=sql> fallback that keeps working without JavaScript (the element adopts and removes it on mount). The element also gained a parser-timing guard: when its definition loads from <head> the browser connects it at the start tag before its children are parsed, so mounting defers to DOMContentLoaded in that case. window.editor back-compat preserved; cm global deprecation deferred. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
866b2d34e1
commit
4ec0f94ed8
14 changed files with 86 additions and 42 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -352,7 +352,10 @@ const sqlEditorTheme = EditorView.theme({
|
|||
// default-table unqualified-column default table for autocomplete
|
||||
// readonly boolean; mounts the editor read-only
|
||||
// autofocus boolean; focuses the editor once mounted
|
||||
// The initial document is the element's trimmed textContent (cleared on mount).
|
||||
// The initial document is, in priority order: a programmatically-set value; the
|
||||
// value of a light-DOM <textarea> first child (a progressive-enhancement form
|
||||
// field that keeps working with JS disabled - it is adopted then removed); or
|
||||
// the element's trimmed textContent. The light DOM is cleared on mount.
|
||||
//
|
||||
// Properties:
|
||||
// value get/set the document (set is host-tagged: no "input" event)
|
||||
|
|
@ -393,8 +396,50 @@ export class DatasetteSqlEditorElement extends HTMLElement {
|
|||
connectedCallback() {
|
||||
if (this._handle) return; // already mounted (e.g. move within the DOM)
|
||||
|
||||
// Parser-timing guard. When this element's definition loads BEFORE the parser
|
||||
// reaches the element (e.g. a non-deferred <script> in <head>, which is how
|
||||
// Datasette's own pages load the bundle), the browser upgrades and connects
|
||||
// the element at its start tag - before its light-DOM children (the initial
|
||||
// document / fallback <textarea>) have been parsed. Reading them now would
|
||||
// yield an empty document. Detect that case - still parsing, nothing set
|
||||
// programmatically, no children yet - and defer mounting to DOMContentLoaded,
|
||||
// by which point the element's subtree is fully parsed. The listener is
|
||||
// registered as the parser sees this element (before any later inline
|
||||
// script's DOMContentLoaded handler), so consumers reading .view on
|
||||
// DOMContentLoaded still observe a mounted editor.
|
||||
if (
|
||||
this.ownerDocument.readyState === "loading" &&
|
||||
this._pendingDoc == null &&
|
||||
!this.firstChild
|
||||
) {
|
||||
this.ownerDocument.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => this.connectedCallback(),
|
||||
{ once: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Progressive-enhancement fallback: a light-DOM <textarea> first child is a
|
||||
// real form field that keeps working with JavaScript disabled. When present,
|
||||
// adopt its value as the initial document and remove it, so it does not also
|
||||
// submit a duplicate field alongside the value the element contributes via
|
||||
// setFormValue.
|
||||
const firstEl = this.firstElementChild;
|
||||
const fallbackTextarea =
|
||||
firstEl && firstEl.tagName === "TEXTAREA" ? firstEl : null;
|
||||
let fallbackDoc = null;
|
||||
if (fallbackTextarea) {
|
||||
fallbackDoc = fallbackTextarea.value;
|
||||
fallbackTextarea.remove();
|
||||
}
|
||||
|
||||
const initialDoc =
|
||||
this._pendingDoc != null ? this._pendingDoc : this.textContent.trim();
|
||||
this._pendingDoc != null
|
||||
? this._pendingDoc
|
||||
: fallbackDoc != null
|
||||
? fallbackDoc
|
||||
: this.textContent.trim();
|
||||
this._initialDoc = initialDoc;
|
||||
this._pendingDoc = null;
|
||||
// Clear the light-DOM text so it doesn't render behind the editor.
|
||||
|
|
|
|||
|
|
@ -8,28 +8,21 @@
|
|||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const sqlFormat = document.querySelector("button#sql-format");
|
||||
const readOnly = document.querySelector("pre#sql-query");
|
||||
const sqlInput = document.querySelector("textarea#sql-editor");
|
||||
const editorElement = document.querySelector("datasette-sql-editor#sql-editor");
|
||||
if (sqlFormat && !readOnly) {
|
||||
sqlFormat.hidden = false;
|
||||
}
|
||||
if (sqlInput) {
|
||||
var editor = (window.editor = cm.editorFromTextArea(sqlInput, {
|
||||
schema,
|
||||
{% if default_table is defined and default_table %}
|
||||
defaultTable: {{ default_table|tojson }},
|
||||
{% endif %}
|
||||
}));
|
||||
if (editorElement) {
|
||||
// Rich lang-sql schema inlined server-side (see const schema above): drives
|
||||
// first-paint autocomplete with no extra HTTP request and no flash of
|
||||
// no-completions. The default table is carried on the element attribute.
|
||||
if (Object.keys(schema).length) {
|
||||
editorElement.schema = schema;
|
||||
}
|
||||
// Back-compat: plugins and inline scripts reach the raw EditorView here.
|
||||
window.editor = editorElement.view;
|
||||
if (sqlFormat) {
|
||||
sqlFormat.addEventListener("click", (ev) => {
|
||||
const formatted = sqlFormatter.format(editor.state.doc.toString());
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert: formatted,
|
||||
},
|
||||
});
|
||||
});
|
||||
sqlFormat.addEventListener("click", () => editorElement.format());
|
||||
}
|
||||
}
|
||||
if (sqlFormat && readOnly) {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ form.sql .query-create-sql {
|
|||
max-width: 52rem;
|
||||
}
|
||||
.query-create-sql .cm-editor,
|
||||
form.sql .query-create-sql textarea#sql-editor {
|
||||
form.sql .query-create-sql textarea[name="sql"] {
|
||||
grid-column: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ form.sql .query-create-sql textarea#sql-editor {
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
.query-create-sql .cm-editor,
|
||||
form.sql .query-create-sql textarea#sql-editor {
|
||||
form.sql .query-create-sql textarea[name="sql"] {
|
||||
grid-column: 1;
|
||||
}
|
||||
.query-create-options,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ window.datasetteSqlParameters = (() => {
|
|||
if (window.editor) {
|
||||
return window.editor.state.doc.toString();
|
||||
}
|
||||
const sqlInput = form.querySelector("textarea#sql-editor, input[name=sql]");
|
||||
const sqlInput = form.querySelector(
|
||||
"#sql-editor, textarea[name=sql], input[name=sql]"
|
||||
);
|
||||
return sqlInput ? sqlInput.value : "";
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +203,7 @@ window.datasetteSqlParameters = (() => {
|
|||
editorElement.addEventListener("input", callback);
|
||||
}
|
||||
if (!window.editor) {
|
||||
const sqlInput = form.querySelector("textarea#sql-editor");
|
||||
const sqlInput = form.querySelector("#sql-editor");
|
||||
if (sqlInput) {
|
||||
sqlInput.addEventListener("input", callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
form.sql .sql-editor {
|
||||
max-width: 52rem;
|
||||
}
|
||||
form.sql .sql-editor textarea#sql-editor {
|
||||
form.sql .sql-editor textarea[name="sql"] {
|
||||
width: 100%;
|
||||
}
|
||||
form.sql .sql-parameters-section {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
{% if allow_execute_sql %}
|
||||
<form class="sql core" action="{{ urls.database(database) }}/-/query" method="get" data-parameters-url="{{ urls.database(database) }}/-/query/parameters">
|
||||
<h3>Custom SQL query</h3>
|
||||
<p class="sql-editor"><textarea id="sql-editor" name="sql">{% if tables %}select * from {{ tables[0].name|escape_sqlite }}{% else %}select sqlite_version(){% endif %}</textarea></p>
|
||||
<p class="sql-editor"><datasette-sql-editor id="sql-editor" name="sql"{% if default_table is defined and default_table %} default-table="{{ default_table }}"{% endif %}><textarea name="sql">{% if tables %}select * from {{ tables[0].name|escape_sqlite }}{% else %}select sqlite_version(){% endif %}</textarea></datasette-sql-editor></p>
|
||||
{% set parameter_names = [] %}
|
||||
{% set parameter_values = {} %}
|
||||
{% set sql_parameters_allow_expand = false %}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ form.sql.core input[data-execute-write-submit]:disabled {
|
|||
<p class="message-warning execute-write-template-unavailable">There are no tables that you can currently edit.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="sql-editor{% if not sql %} sql-editor-min-lines{% endif %}"><textarea id="sql-editor" name="sql"{% if sql %} style="height: {{ sql.split("\n")|length + 2 }}em"{% endif %}>{{ sql }}</textarea></p>
|
||||
<p class="sql-editor{% if not sql %} sql-editor-min-lines{% endif %}"><datasette-sql-editor id="sql-editor" name="sql"{% if default_table is defined and default_table %} default-table="{{ default_table }}"{% endif %}><textarea name="sql"{% if sql %} style="height: {{ sql.split("\n")|length + 2 }}em"{% endif %}>{{ sql }}</textarea></datasette-sql-editor></p>
|
||||
|
||||
{% set sql_parameters_section_id = "execute-write-parameters-section" %}
|
||||
{% set sql_parameters_allow_expand = true %}
|
||||
|
|
@ -175,7 +175,7 @@ form.sql.core input[data-execute-write-submit]:disabled {
|
|||
|
||||
<script>
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const executeWriteSqlInput = document.querySelector("textarea#sql-editor");
|
||||
const executeWriteSqlInput = document.querySelector("#sql-editor");
|
||||
const form = document.querySelector("form.sql.core");
|
||||
const analysisSection = document.querySelector("#execute-write-analysis-section");
|
||||
const submitButton = form
|
||||
|
|
@ -261,7 +261,7 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const tableSelect = document.querySelector("#execute-write-template-table");
|
||||
const templateButtons = document.querySelectorAll("[data-sql-template]");
|
||||
const sqlInput = document.querySelector("textarea#sql-editor");
|
||||
const sqlInput = document.querySelector("#sql-editor");
|
||||
|
||||
function dataKey(operation) {
|
||||
return `template${operation.charAt(0).toUpperCase()}${operation.slice(1)}Sql`;
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@
|
|||
{% endif %}
|
||||
{% if not hide_sql %}
|
||||
{% if editable and allow_execute_sql %}
|
||||
<p class="sql-editor"><textarea id="sql-editor" name="sql"{% if query and query.sql %} style="height: {{ query.sql.split("\n")|length + 2 }}em"{% endif %}
|
||||
>{% if query and query.sql %}{{ query.sql }}{% elif tables %}select * from {{ tables[0].name|escape_sqlite }}{% endif %}</textarea></p>
|
||||
<p class="sql-editor"><datasette-sql-editor id="sql-editor" name="sql"{% if default_table is defined and default_table %} default-table="{{ default_table }}"{% endif %}><textarea name="sql"{% if query and query.sql %} style="height: {{ query.sql.split("\n")|length + 2 }}em"{% endif %}
|
||||
>{% if query and query.sql %}{{ query.sql }}{% elif tables %}select * from {{ tables[0].name|escape_sqlite }}{% endif %}</textarea></datasette-sql-editor></p>
|
||||
{% else %}
|
||||
<pre id="sql-query">{% if query %}{{ query.sql }}{% endif %}</pre>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<p class="query-create-field"><label for="query-description">Description</label> <textarea id="query-description" name="description" rows="3">{{ description or "" }}</textarea></p>
|
||||
</div>
|
||||
|
||||
<p class="query-create-sql sql-editor"><textarea id="sql-editor" name="sql"{% if sql %} style="height: {{ sql.split("\n")|length + 2 }}em"{% endif %}>{{ sql }}</textarea></p>
|
||||
<p class="query-create-sql sql-editor"><datasette-sql-editor id="sql-editor" name="sql"{% if default_table is defined and default_table %} default-table="{{ default_table }}"{% endif %}><textarea name="sql"{% if sql %} style="height: {{ sql.split("\n")|length + 2 }}em"{% endif %}>{{ sql }}</textarea></datasette-sql-editor></p>
|
||||
|
||||
<p class="query-create-options">
|
||||
<span class="query-create-analysis-note" data-query-create-analysis-note aria-live="polite">{% if analysis_error %}This query cannot be saved until the SQL is valid.{% elif not has_sql %}Enter SQL to analyze this query.{% elif analysis_is_write %}This query updates data in the database.{% else %}This is a read-only query.{% endif %}</span>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<p class="query-create-field"><label for="query-description">Description</label> <textarea id="query-description" name="description" rows="3">{{ description or "" }}</textarea></p>
|
||||
</div>
|
||||
|
||||
<p class="query-create-sql sql-editor"><textarea id="sql-editor" name="sql"{% if sql %} style="height: {{ sql.split("\n")|length + 2 }}em"{% endif %}>{{ sql }}</textarea></p>
|
||||
<p class="query-create-sql sql-editor"><datasette-sql-editor id="sql-editor" name="sql"{% if default_table is defined and default_table %} default-table="{{ default_table }}"{% endif %}><textarea name="sql"{% if sql %} style="height: {{ sql.split("\n")|length + 2 }}em"{% endif %}>{{ sql }}</textarea></datasette-sql-editor></p>
|
||||
|
||||
<p class="query-create-options">
|
||||
<span class="query-create-analysis-note" data-query-create-analysis-note aria-live="polite">{% if analysis_error %}This query cannot be saved until the SQL is valid.{% elif not has_sql %}Enter SQL to analyze this query.{% elif analysis_is_write %}This query updates data in the database.{% else %}This is a read-only query.{% endif %}</span>
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ async def test_query_page_with_no_sql(ds_client):
|
|||
# https://github.com/simonw/datasette/issues/2743
|
||||
response = await ds_client.get("/fixtures/-/query")
|
||||
assert response.status_code == 200
|
||||
assert '<textarea id="sql-editor" name="sql"' in response.text
|
||||
assert '<datasette-sql-editor id="sql-editor" name="sql"' in response.text
|
||||
assert 'class="rows-and-columns"' not in response.text
|
||||
|
||||
|
||||
|
|
@ -306,16 +306,16 @@ async def test_query_page_default_table_from_table_scoped_link(ds_client):
|
|||
href = soup.find("span", string="View and edit SQL").find_parent("a")["href"]
|
||||
response = await ds_client.get(href, follow_redirects=True)
|
||||
assert response.status_code == 200
|
||||
assert 'defaultTable: "facetable"' in response.text
|
||||
assert 'default-table="facetable"' in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_page_no_default_table_without_table_scope(ds_client):
|
||||
# The plain database query page (no focal table) should not set
|
||||
# defaultTable at all.
|
||||
# default-table at all.
|
||||
response = await ds_client.get("/fixtures/-/query?sql=select+1")
|
||||
assert response.status_code == 200
|
||||
assert "defaultTable" not in response.text
|
||||
assert "default-table=" not in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -328,7 +328,7 @@ async def test_query_page_ignores_invalid_table_param(ds_client):
|
|||
"/fixtures/-/query?sql=select+1&_table=not_a_real_table"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "defaultTable" not in response.text
|
||||
assert "default-table=" not in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -923,7 +923,8 @@ async def test_query_error(ds_client):
|
|||
response = await ds_client.get("/fixtures/-/query?sql=select+*+from+notatable")
|
||||
html = response.text
|
||||
assert '<p class="message-error">no such table: notatable</p>' in html
|
||||
assert '<textarea id="sql-editor" name="sql" style="height: 3em' in html
|
||||
assert '<datasette-sql-editor id="sql-editor" name="sql"' in html
|
||||
assert '<textarea name="sql" style="height: 3em' in html
|
||||
assert ">select * from notatable</textarea>" in html
|
||||
assert "0 results" not in html
|
||||
|
||||
|
|
|
|||
|
|
@ -1888,7 +1888,10 @@ async def test_execute_write_get_prepopulates_without_executing():
|
|||
actor={"id": "root"},
|
||||
)
|
||||
assert '<p class="sql-editor sql-editor-min-lines">' in empty_response.text
|
||||
assert '<textarea id="sql-editor" name="sql"></textarea>' in empty_response.text
|
||||
assert (
|
||||
'<datasette-sql-editor id="sql-editor" name="sql"><textarea name="sql"></textarea></datasette-sql-editor>'
|
||||
in empty_response.text
|
||||
)
|
||||
assert "min-height: calc(5.6em + 8px);" in empty_response.text
|
||||
assert 'executeWriteSqlInput.value = "\\n\\n\\n";' not in empty_response.text
|
||||
assert "Enter writable SQL before executing." in empty_response.text
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue