Support multi-line parameters on /db/-/execute-write

Refs https://github.com/simonw/datasette/issues/2742#issuecomment-4536317049

Each paramater input now has an expand/collapse button toggle to turn into a textarea.

If you paste text that includes at least one newline it toggles automatically.
This commit is contained in:
Simon Willison 2026-05-25 11:35:09 -07:00
commit 66bbbbc947
2 changed files with 94 additions and 1 deletions

View file

@ -74,6 +74,25 @@
color: #b00020;
font-weight: 700;
}
form.sql .execute-write-parameter-row textarea[data-parameter-control] {
border: 1px solid #ccc;
border-radius: 3px;
box-sizing: content-box;
display: inline-block;
font-family: Helvetica, sans-serif;
font-size: 1em;
min-height: 7rem;
padding: 9px 4px;
vertical-align: top;
width: 60%;
}
form.sql.core button.execute-write-parameter-toggle[type=button] {
font-size: 0.72rem;
height: 1.8rem;
line-height: 1;
margin-left: 0.35rem;
padding: 0.25rem 0.45rem;
}
</style>
{% endblock %}
@ -118,7 +137,7 @@
{% if parameter_names %}
<h2>Parameters</h2>
{% for parameter in parameter_names %}
<p><label for="qp{{ loop.index }}">{{ parameter }}</label> <input type="text" id="qp{{ loop.index }}" name="{{ parameter }}" value="{{ parameter_values.get(parameter, "") }}"></p>
<p class="execute-write-parameter-row"><label for="qp{{ loop.index }}">{{ parameter }}</label> <input type="text" id="qp{{ loop.index }}" name="{{ parameter }}" value="{{ parameter_values.get(parameter, "") }}" data-parameter-control> <button type="button" class="execute-write-parameter-toggle" data-parameter-toggle aria-controls="qp{{ loop.index }}" aria-expanded="false">Expand</button></p>
{% endfor %}
{% endif %}
@ -164,6 +183,79 @@ if (executeWriteSqlInput && !executeWriteSqlInput.value) {
{% include "_codemirror_foot.html" %}
<script>
window.addEventListener("DOMContentLoaded", () => {
function replaceParameterControl(control, button, expand, value, selectionStart) {
const replacement = document.createElement(expand ? "textarea" : "input");
replacement.id = control.id;
replacement.name = control.name;
replacement.value = value === undefined ? control.value : value;
replacement.setAttribute("data-parameter-control", "");
if (expand) {
replacement.rows = 5;
button.textContent = "Collapse";
button.setAttribute("aria-expanded", "true");
} else {
replacement.type = "text";
button.textContent = "Expand";
button.setAttribute("aria-expanded", "false");
}
control.replaceWith(replacement);
replacement.focus();
if (selectionStart !== undefined && replacement.setSelectionRange) {
replacement.setSelectionRange(selectionStart, selectionStart);
}
}
document.querySelectorAll("[data-parameter-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const control = document.getElementById(button.getAttribute("aria-controls"));
if (!control) {
return;
}
const expanded = control.tagName.toLowerCase() === "textarea";
replaceParameterControl(control, button, !expanded);
});
});
document
.querySelector("form.sql.core")
.addEventListener("paste", (event) => {
const control = event.target;
if (
!(control instanceof HTMLInputElement) ||
!control.matches("[data-parameter-control]")
) {
return;
}
const pasted = event.clipboardData ? event.clipboardData.getData("text") : "";
if (!/[\r\n]/.test(pasted)) {
return;
}
const button = document.querySelector(
`[data-parameter-toggle][aria-controls="${control.id}"]`
);
if (!button) {
return;
}
event.preventDefault();
const selectionStart = control.selectionStart ?? control.value.length;
const selectionEnd = control.selectionEnd ?? selectionStart;
const value =
control.value.slice(0, selectionStart) +
pasted +
control.value.slice(selectionEnd);
replaceParameterControl(
control,
button,
true,
value,
selectionStart + pasted.length
);
});
});
</script>
{% if write_template_tables %}
<script>
window.addEventListener("DOMContentLoaded", () => {

View file

@ -719,6 +719,7 @@ async def test_execute_write_get_prepopulates_without_executing():
assert 'data-sql-template="insert"' in response.text
assert 'data-sql-template="update"' in response.text
assert 'data-sql-template="delete"' in response.text
assert 'addEventListener("paste"' in response.text
assert '<table class="execute-write-analysis">' in response.text
assert '<th scope="col">Required permission</th>' in response.text
assert "<td><code>insert</code></td>" in response.text