mirror of
https://github.com/simonw/datasette.git
synced 2026-07-15 20:14:38 +02:00
Compare commits
7 commits
main
...
claude/mod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32be92fa24 |
||
|
|
14397ac4c1 |
||
|
|
7d19e949dd |
||
|
|
256ce15184 |
||
|
|
967c1298ea |
||
|
|
5fd27d9347 |
||
|
|
0693f2f099 |
13 changed files with 1291 additions and 1257 deletions
|
|
@ -938,78 +938,16 @@ p.zero-results {
|
|||
display: none;
|
||||
}
|
||||
|
||||
@keyframes datasette-modal-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes datasette-modal-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/*
|
||||
* Modal dialogs are rendered by the <datasette-modal> web component
|
||||
* (datasette-modal.js), which provides the shared frame, header, footer
|
||||
* and button styles. The per-dialog rules below only override sizing
|
||||
* (via --datasette-modal-* custom properties) and style the content
|
||||
* that is specific to each dialog.
|
||||
*/
|
||||
dialog.mobile-column-actions-dialog {
|
||||
--ink: #0f0f0f;
|
||||
--paper: #eef6ff;
|
||||
--muted: #6b6b6b;
|
||||
--rule: #d8e6f5;
|
||||
--accent: #1a56db;
|
||||
--card: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
max-width: 95vw;
|
||||
max-height: min(640px, calc(100vh - 32px));
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
dialog.mobile-column-actions-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
dialog.mobile-column-actions-dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-meta {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
background: var(--paper);
|
||||
padding: 3px 9px;
|
||||
border-radius: 20px;
|
||||
--datasette-modal-width: min(420px, calc(100vw - 32px));
|
||||
--datasette-modal-max-height: min(640px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .list-wrap {
|
||||
|
|
@ -1142,102 +1080,9 @@ dialog.mobile-column-actions-dialog::backdrop {
|
|||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .footer-info {
|
||||
flex: 1;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .btn-ghost:hover {
|
||||
background: var(--rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
dialog.set-column-type-dialog {
|
||||
--ink: #0f0f0f;
|
||||
--paper: #eef6ff;
|
||||
--muted: #6b6b6b;
|
||||
--rule: #d8e6f5;
|
||||
--accent: #1a56db;
|
||||
--card: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
max-width: 95vw;
|
||||
max-height: min(720px, calc(100vh - 32px));
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
dialog.set-column-type-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
dialog.set-column-type-dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
.set-column-type-dialog .modal-header {
|
||||
padding: 20px 24px 12px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.set-column-type-dialog .modal-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.set-column-type-dialog .modal-meta {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
background: var(--paper);
|
||||
padding: 3px 9px;
|
||||
border-radius: 20px;
|
||||
--datasette-modal-width: min(520px, calc(100vw - 32px));
|
||||
--datasette-modal-max-height: min(720px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
.set-column-type-status,
|
||||
|
|
@ -1302,60 +1147,6 @@ dialog.set-column-type-dialog::backdrop {
|
|||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.set-column-type-dialog .modal-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.set-column-type-dialog .footer-info {
|
||||
flex: 1;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.set-column-type-dialog .btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.set-column-type-dialog .btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.set-column-type-dialog .btn-ghost:hover {
|
||||
background: var(--rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.set-column-type-dialog .btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.set-column-type-dialog .btn-primary:hover {
|
||||
background: #1949b8;
|
||||
}
|
||||
|
||||
.set-column-type-dialog .btn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.row-mutation-status {
|
||||
margin: 0 0 0.75rem;
|
||||
padding: 8px 10px;
|
||||
|
|
@ -1389,57 +1180,7 @@ button.table-insert-row svg {
|
|||
}
|
||||
|
||||
dialog.row-delete-dialog {
|
||||
--ink: #0f0f0f;
|
||||
--paper: #eef6ff;
|
||||
--muted: #6b6b6b;
|
||||
--rule: #d8e6f5;
|
||||
--accent: #1a56db;
|
||||
--card: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: min(440px, calc(100vw - 32px));
|
||||
max-width: 95vw;
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
dialog.row-delete-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
dialog.row-delete-dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
.row-delete-dialog .modal-header {
|
||||
padding: 20px 24px 12px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row-delete-dialog .modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
--datasette-modal-width: min(440px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.row-delete-message,
|
||||
|
|
@ -1470,106 +1211,12 @@ dialog.row-delete-dialog::backdrop {
|
|||
}
|
||||
|
||||
.row-delete-dialog .modal-footer {
|
||||
padding: 18px 20px 14px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.row-delete-dialog .btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.row-delete-dialog .btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.row-delete-dialog .btn-ghost:hover {
|
||||
background: var(--rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.row-delete-dialog .btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.row-delete-dialog .btn-primary:hover {
|
||||
background: #1949b8;
|
||||
}
|
||||
|
||||
.row-delete-dialog .btn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
dialog.row-edit-dialog {
|
||||
--ink: #0f0f0f;
|
||||
--paper: #eef6ff;
|
||||
--muted: #6b6b6b;
|
||||
--rule: #d8e6f5;
|
||||
--accent: #1a56db;
|
||||
--card: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: min(720px, calc(100vw - 32px));
|
||||
max-width: 95vw;
|
||||
max-height: min(780px, calc(100vh - 32px));
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
dialog.row-edit-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
dialog.row-edit-dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
.row-edit-dialog .modal-header {
|
||||
padding: 20px 24px 12px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row-edit-dialog .modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
--datasette-modal-width: min(720px, calc(100vw - 32px));
|
||||
--datasette-modal-max-height: min(780px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
.row-edit-dialog .modal-title .row-dialog-action,
|
||||
|
|
@ -1865,105 +1512,9 @@ datasette-autocomplete input[type="text"],
|
|||
max-width: 46rem;
|
||||
}
|
||||
|
||||
.row-edit-dialog .modal-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.row-edit-dialog .btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.row-edit-dialog .btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.row-edit-dialog .btn-ghost:hover {
|
||||
background: var(--rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.row-edit-dialog .btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.row-edit-dialog .btn-primary:hover {
|
||||
background: #1949b8;
|
||||
}
|
||||
|
||||
.row-edit-dialog .btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
dialog.table-create-dialog {
|
||||
--ink: #0f0f0f;
|
||||
--paper: #eef6ff;
|
||||
--muted: #6b6b6b;
|
||||
--rule: #d8e6f5;
|
||||
--accent: #1a56db;
|
||||
--card: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: min(980px, calc(100vw - 32px));
|
||||
max-width: 95vw;
|
||||
max-height: min(780px, calc(100vh - 32px));
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
dialog.table-create-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
dialog.table-create-dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
.table-create-dialog .modal-header {
|
||||
padding: 20px 24px 12px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.table-create-dialog .modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
--datasette-modal-width: min(980px, calc(100vw - 32px));
|
||||
--datasette-modal-max-height: min(780px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
.table-create-form {
|
||||
|
|
@ -2288,50 +1839,6 @@ select.table-create-input {
|
|||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.table-create-dialog .modal-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.table-create-dialog .btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.table-create-dialog .btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.table-create-dialog .btn-ghost:hover {
|
||||
background: var(--rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.table-create-dialog .btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-create-dialog .btn-primary:hover {
|
||||
background: #1949b8;
|
||||
}
|
||||
|
||||
.table-create-dialog .btn:disabled,
|
||||
.table-create-add-column:disabled,
|
||||
.table-create-icon-button:disabled {
|
||||
opacity: 0.55;
|
||||
|
|
@ -2339,56 +1846,8 @@ select.table-create-input {
|
|||
}
|
||||
|
||||
dialog.table-alter-dialog {
|
||||
--ink: #0f0f0f;
|
||||
--paper: #eef6ff;
|
||||
--muted: #6b6b6b;
|
||||
--rule: #d8e6f5;
|
||||
--accent: #1a56db;
|
||||
--card: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: min(980px, calc(100vw - 32px));
|
||||
max-width: 95vw;
|
||||
max-height: min(780px, calc(100vh - 32px));
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
dialog.table-alter-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
dialog.table-alter-dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
.table-alter-dialog .modal-header {
|
||||
padding: 20px 24px 12px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.table-alter-dialog .modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
--datasette-modal-width: min(980px, calc(100vw - 32px));
|
||||
--datasette-modal-max-height: min(780px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
.table-alter-form {
|
||||
|
|
@ -2443,8 +1902,7 @@ dialog.table-alter-dialog::backdrop {
|
|||
max-width: 24rem;
|
||||
}
|
||||
|
||||
.table-alter-fields[hidden],
|
||||
.table-alter-dialog .modal-footer [hidden] {
|
||||
.table-alter-fields[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -2746,72 +2204,6 @@ select.table-alter-input {
|
|||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.table-alter-dialog .modal-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn-ghost:hover {
|
||||
background: var(--rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn-danger {
|
||||
background: #b91c1c;
|
||||
color: #fff;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn-danger:hover {
|
||||
background: #991b1b;
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn-danger:disabled,
|
||||
.table-alter-dialog .btn-danger:disabled:hover {
|
||||
background: #d98c8c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn-primary:hover {
|
||||
background: #1949b8;
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn-primary:disabled,
|
||||
.table-alter-dialog .btn-primary:disabled:hover {
|
||||
background: #a0aec0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-alter-dialog .btn:disabled,
|
||||
.table-alter-add-column:disabled,
|
||||
.table-alter-icon-button:disabled {
|
||||
opacity: 0.55;
|
||||
|
|
@ -2937,17 +2329,11 @@ select.table-alter-input {
|
|||
width: 13px;
|
||||
}
|
||||
|
||||
/*
|
||||
* <datasette-modal> already provides the small-screen dialog frame and
|
||||
* header/footer padding. These rules only adjust dialog-specific content.
|
||||
*/
|
||||
@media (max-width: 640px) {
|
||||
dialog.mobile-column-actions-dialog {
|
||||
width: 95vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-header {
|
||||
padding: 16px 18px 14px;
|
||||
}
|
||||
|
||||
.mobile-column-top-actions {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
|
|
@ -2970,13 +2356,6 @@ select.table-alter-input {
|
|||
padding-right: 18px;
|
||||
}
|
||||
|
||||
dialog.set-column-type-dialog {
|
||||
width: 95vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.set-column-type-dialog .modal-header,
|
||||
.set-column-type-status,
|
||||
.set-column-type-empty,
|
||||
.set-column-type-error,
|
||||
|
|
@ -2985,31 +2364,12 @@ select.table-alter-input {
|
|||
padding-right: 18px;
|
||||
}
|
||||
|
||||
dialog.row-delete-dialog {
|
||||
width: 95vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.row-delete-dialog .modal-header,
|
||||
.row-delete-message,
|
||||
.row-delete-error {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.row-delete-dialog .modal-footer {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
dialog.row-edit-dialog {
|
||||
width: 95vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.row-edit-dialog .modal-header,
|
||||
.row-edit-summary,
|
||||
.row-edit-loading,
|
||||
.row-edit-fields {
|
||||
|
|
@ -3031,18 +2391,6 @@ select.table-alter-input {
|
|||
padding-top: 0;
|
||||
}
|
||||
|
||||
.row-edit-dialog .modal-footer {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
dialog.table-create-dialog {
|
||||
width: 95vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.table-create-dialog .modal-header,
|
||||
.table-create-fields {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
|
|
@ -3112,11 +2460,6 @@ select.table-alter-input {
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-create-dialog .modal-footer {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.row-inline-action {
|
||||
min-height: 30px;
|
||||
min-width: 30px;
|
||||
|
|
|
|||
|
|
@ -41,76 +41,22 @@ class ColumnChooser extends HTMLElement {
|
|||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
dialog {
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: min(640px, calc(100vh - 32px));
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: slideIn var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
/* Frame styles come from the shared <datasette-modal> component */
|
||||
datasette-modal {
|
||||
--datasette-modal-width: min(420px, 95vw);
|
||||
--datasette-modal-max-height: min(640px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
datasette-modal > dialog {
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
datasette-modal > dialog[open] {
|
||||
height: min(640px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: fadeIn var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-meta {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
background: var(--paper);
|
||||
padding: 3px 9px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.list-toolbar {
|
||||
padding: 6px 24px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
|
|
@ -299,48 +245,6 @@ class ColumnChooser extends HTMLElement {
|
|||
50% { transform: translateX(-50%) scale(1.5); opacity: 0.07; }
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
flex: 1;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover { background: #1448c0; }
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
.btn-ghost:hover { background: var(--rule); color: var(--ink); }
|
||||
|
||||
.list-wrap::-webkit-scrollbar { width: 5px; }
|
||||
.list-wrap::-webkit-scrollbar-track { background: transparent; }
|
||||
.list-wrap::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 99px; }
|
||||
|
|
@ -348,11 +252,7 @@ class ColumnChooser extends HTMLElement {
|
|||
input, textarea { -webkit-user-select: auto; user-select: auto; }
|
||||
</style>
|
||||
|
||||
<dialog aria-labelledby="modalTitle">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="modalTitle">Choose columns</span>
|
||||
<span class="modal-meta" id="selectedCount"></span>
|
||||
</div>
|
||||
<datasette-modal modal-title="Choose columns">
|
||||
<div class="list-toolbar">
|
||||
<button id="selectAllBtn">Select all</button>
|
||||
<button id="deselectAllBtn">Deselect all</button>
|
||||
|
|
@ -367,11 +267,11 @@ class ColumnChooser extends HTMLElement {
|
|||
<button class="btn btn-ghost" id="cancelBtn">Cancel</button>
|
||||
<button class="btn btn-primary" id="applyBtn">Apply</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</datasette-modal>
|
||||
`;
|
||||
|
||||
// DOM refs
|
||||
this._dialog = this.shadowRoot.querySelector("dialog");
|
||||
this._modal = this.shadowRoot.querySelector("datasette-modal");
|
||||
this._listWrap = this.shadowRoot.getElementById("listWrap");
|
||||
this._dragList = this.shadowRoot.getElementById("dragList");
|
||||
this._pulseTop = this.shadowRoot.getElementById("pulseTop");
|
||||
|
|
@ -380,20 +280,16 @@ class ColumnChooser extends HTMLElement {
|
|||
this._deselectAllBtn = this.shadowRoot.getElementById("deselectAllBtn");
|
||||
this._cancelBtn = this.shadowRoot.getElementById("cancelBtn");
|
||||
this._applyBtn = this.shadowRoot.getElementById("applyBtn");
|
||||
this._countEl = this.shadowRoot.getElementById("selectedCount");
|
||||
this._footerEl = this.shadowRoot.getElementById("footerInfo");
|
||||
|
||||
// Event listeners
|
||||
// Event listeners - dismissal (backdrop click, Escape) is handled
|
||||
// by the <datasette-modal> component
|
||||
this._selectAllBtn.addEventListener("click", () => this._selectAll());
|
||||
this._deselectAllBtn.addEventListener("click", () => this._deselectAll());
|
||||
this._cancelBtn.addEventListener("click", () => this._close());
|
||||
this._applyBtn.addEventListener("click", () => this._apply());
|
||||
this._dialog.addEventListener("click", (e) => {
|
||||
if (e.target === this._dialog) this._close();
|
||||
});
|
||||
this._dialog.addEventListener("cancel", (e) => {
|
||||
e.preventDefault();
|
||||
this._close();
|
||||
this._modal.addEventListener("datasette-modal-close", () => {
|
||||
this._restoreSavedState();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -405,6 +301,10 @@ class ColumnChooser extends HTMLElement {
|
|||
* @param {function(string[]): void} opts.onApply - Called with the selected columns in order when Apply is clicked.
|
||||
*/
|
||||
open({ columns, selected = [], onApply }) {
|
||||
if (!this._modal.dialog) {
|
||||
// datasette-modal.js is missing or <dialog> is unsupported
|
||||
return;
|
||||
}
|
||||
this._items = [...columns];
|
||||
this._checked = new Set(selected);
|
||||
this._onApply = onApply || null;
|
||||
|
|
@ -414,17 +314,20 @@ class ColumnChooser extends HTMLElement {
|
|||
this._savedChecked = new Set(this._checked);
|
||||
|
||||
this._render();
|
||||
this._dialog.showModal();
|
||||
this._modal.showModal();
|
||||
}
|
||||
|
||||
// ── Internal methods ──
|
||||
|
||||
_close() {
|
||||
_restoreSavedState() {
|
||||
this._items = this._savedItems ? [...this._savedItems] : this._items;
|
||||
this._checked = this._savedChecked
|
||||
? new Set(this._savedChecked)
|
||||
: this._checked;
|
||||
this._dialog.close();
|
||||
}
|
||||
|
||||
_close() {
|
||||
this._modal.close();
|
||||
}
|
||||
|
||||
_selectAll() {
|
||||
|
|
@ -445,7 +348,7 @@ class ColumnChooser extends HTMLElement {
|
|||
|
||||
_apply() {
|
||||
const selected = this._items.filter((col) => this._checked.has(col));
|
||||
this._dialog.close();
|
||||
this._modal.close();
|
||||
if (this._onApply) {
|
||||
this._onApply(selected);
|
||||
}
|
||||
|
|
@ -493,7 +396,7 @@ class ColumnChooser extends HTMLElement {
|
|||
|
||||
_updateCounts() {
|
||||
const n = this._checked.size;
|
||||
this._countEl.textContent = `${n} of ${this._items.length} selected`;
|
||||
this._modal.setMeta(`${n} of ${this._items.length} selected`);
|
||||
this._footerEl.textContent = `${this._items.length} columns`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -215,6 +215,21 @@ const datasetteManager = {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a <datasette-modal> element, append it to the document and
|
||||
* return it. A convenience wrapper for window.DatasetteModal.create() -
|
||||
* see the "Modal dialogs" section of the JavaScript plugins
|
||||
* documentation for the supported options.
|
||||
*
|
||||
* Returns null in browsers without <dialog> support.
|
||||
*/
|
||||
createModal: (options) => {
|
||||
if (!window.DatasetteModal || !window.DatasetteModal.supported) {
|
||||
return null;
|
||||
}
|
||||
return window.DatasetteModal.create(options);
|
||||
},
|
||||
|
||||
/** Selectors for document (DOM) elements. Store identifier instead of immediate references in case they haven't loaded when Manager starts. */
|
||||
selectors: DOM_SELECTORS,
|
||||
|
||||
|
|
|
|||
584
datasette/static/datasette-modal.js
Normal file
584
datasette/static/datasette-modal.js
Normal file
|
|
@ -0,0 +1,584 @@
|
|||
/**
|
||||
* <datasette-modal> is Datasette's shared modal dialog Web Component.
|
||||
*
|
||||
* This element, and the DatasetteModal class exposed as
|
||||
* window.DatasetteModal, are part of Datasette's public JavaScript API
|
||||
* for plugins. See the "Modal dialogs" section of the JavaScript
|
||||
* plugins documentation.
|
||||
*
|
||||
* The component wraps a native <dialog> element and provides:
|
||||
*
|
||||
* - The standard Datasette modal frame: sizing, rounded corners,
|
||||
* backdrop, animations, and optional .modal-header scaffolding
|
||||
* - Close-on-backdrop-click and Escape key handling
|
||||
* - Declarative cancel buttons: clicking any element inside the modal
|
||||
* with a `data-modal-cancel` attribute calls requestClose("cancel")
|
||||
* - A `busy` property that blocks user-initiated dismissal while an
|
||||
* operation is in flight
|
||||
* - A `closeGuard` hook for "discard unsaved changes?" style prompts
|
||||
* - Focus restoration to the triggering element on close
|
||||
* - `datasette-modal-open` and `datasette-modal-close` events
|
||||
*
|
||||
* Markup structure once connected:
|
||||
*
|
||||
* <datasette-modal modal-title="Example">
|
||||
* <dialog id class aria-labelledby>
|
||||
* <div class="modal-header">
|
||||
* <span class="modal-title">Example</span>
|
||||
* <span class="modal-meta" hidden></span>
|
||||
* </div>
|
||||
* ...consumer content, typically ending in a .modal-footer...
|
||||
* </dialog>
|
||||
* </datasette-modal>
|
||||
*
|
||||
* The component uses light DOM so page CSS and plugins can style the
|
||||
* dialog contents. The shared frame styles are distributed via a
|
||||
* stylesheet that the component adopts into whatever document or
|
||||
* shadow root it is connected to, which means the component also
|
||||
* works inside the shadow DOM of other web components.
|
||||
*/
|
||||
(function () {
|
||||
var FRAME_CSS = `
|
||||
datasette-modal {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@keyframes datasette-modal-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes datasette-modal-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
datasette-modal > dialog {
|
||||
--ink: #0f0f0f;
|
||||
--paper: #eef6ff;
|
||||
--muted: #6b6b6b;
|
||||
--rule: #d8e6f5;
|
||||
--accent: #1a56db;
|
||||
--card: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--datasette-modal-border-radius, var(--modal-border-radius, 0.75rem));
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: var(--datasette-modal-width, min(520px, calc(100vw - 32px)));
|
||||
max-width: 95vw;
|
||||
max-height: var(--datasette-modal-max-height, min(720px, calc(100vh - 32px)));
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
datasette-modal > dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
datasette-modal > dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
datasette-modal .modal-header {
|
||||
padding: 20px 24px 14px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
datasette-modal .modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
datasette-modal .modal-meta {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
background: var(--paper);
|
||||
padding: 3px 9px;
|
||||
border-radius: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
datasette-modal .modal-meta[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
datasette-modal .modal-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
datasette-modal .modal-footer [hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
datasette-modal .footer-info {
|
||||
flex: 1;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
datasette-modal .btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
datasette-modal .btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
datasette-modal .btn-ghost:hover {
|
||||
background: var(--rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
datasette-modal .btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
datasette-modal .btn-primary:hover {
|
||||
background: #1949b8;
|
||||
}
|
||||
|
||||
datasette-modal .btn-primary:disabled,
|
||||
datasette-modal .btn-primary:disabled:hover {
|
||||
background: #a0aec0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
datasette-modal .btn-danger {
|
||||
background: #b91c1c;
|
||||
color: #fff;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
datasette-modal .btn-danger:hover {
|
||||
background: #991b1b;
|
||||
}
|
||||
|
||||
datasette-modal .btn-danger:disabled,
|
||||
datasette-modal .btn-danger:disabled:hover {
|
||||
background: #d98c8c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
datasette-modal .btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
datasette-modal > dialog {
|
||||
width: var(--datasette-modal-small-screen-width, 95vw);
|
||||
max-height: var(--datasette-modal-small-screen-max-height, 85vh);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
datasette-modal .modal-header {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
datasette-modal .modal-footer {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
var sharedFrameSheet = null;
|
||||
var styledRoots = new WeakSet();
|
||||
var titleIdCounter = 0;
|
||||
|
||||
/* Make the shared frame styles available in a document or shadow root */
|
||||
function adoptFrameStyles(rootNode) {
|
||||
if (!rootNode || styledRoots.has(rootNode)) {
|
||||
return;
|
||||
}
|
||||
styledRoots.add(rootNode);
|
||||
if (
|
||||
typeof CSSStyleSheet !== "undefined" &&
|
||||
"adoptedStyleSheets" in rootNode
|
||||
) {
|
||||
try {
|
||||
if (!sharedFrameSheet) {
|
||||
sharedFrameSheet = new CSSStyleSheet();
|
||||
sharedFrameSheet.replaceSync(FRAME_CSS);
|
||||
}
|
||||
rootNode.adoptedStyleSheets =
|
||||
rootNode.adoptedStyleSheets.concat(sharedFrameSheet);
|
||||
return;
|
||||
} catch (_error) {
|
||||
// Fall back to a <style> element below
|
||||
}
|
||||
}
|
||||
var style = document.createElement("style");
|
||||
style.setAttribute("data-datasette-modal", "");
|
||||
style.textContent = FRAME_CSS;
|
||||
(rootNode.head || rootNode).appendChild(style);
|
||||
}
|
||||
|
||||
class DatasetteModal extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ["modal-title", "modal-meta"];
|
||||
}
|
||||
|
||||
/** True if the browser supports everything the component needs */
|
||||
static get supported() {
|
||||
return typeof window.HTMLDialogElement !== "undefined";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a <datasette-modal>, append it to the document (or
|
||||
* options.parent) and return it. Returns null in browsers without
|
||||
* <dialog> support.
|
||||
*
|
||||
* Options:
|
||||
* - id: id attribute for the inner <dialog>
|
||||
* - className: class attribute for the inner <dialog>
|
||||
* - title: text for the standard header title (omit for no header)
|
||||
* - meta: text for the header meta chip
|
||||
* - titleId: id for the title element (defaults to "<id>-title")
|
||||
* - labelledBy: aria-labelledby override for the dialog
|
||||
* - describedBy: aria-describedby for the dialog
|
||||
* - content: HTML string or DOM node placed after the header
|
||||
* - parent: element to append to (defaults to document.body)
|
||||
*/
|
||||
static create(options) {
|
||||
options = options || {};
|
||||
if (!DatasetteModal.supported) {
|
||||
return null;
|
||||
}
|
||||
var modal = document.createElement("datasette-modal");
|
||||
if (options.id) {
|
||||
modal.setAttribute("dialog-id", options.id);
|
||||
}
|
||||
if (options.className) {
|
||||
modal.setAttribute("dialog-class", options.className);
|
||||
}
|
||||
if (options.title !== undefined && options.title !== null) {
|
||||
modal.setAttribute("modal-title", options.title);
|
||||
}
|
||||
if (options.meta !== undefined && options.meta !== null) {
|
||||
modal.setAttribute("modal-meta", options.meta);
|
||||
}
|
||||
if (options.titleId) {
|
||||
modal.setAttribute("title-id", options.titleId);
|
||||
}
|
||||
if (options.labelledBy) {
|
||||
modal.setAttribute("labelled-by", options.labelledBy);
|
||||
}
|
||||
if (options.describedBy) {
|
||||
modal.setAttribute("described-by", options.describedBy);
|
||||
}
|
||||
if (options.content !== undefined && options.content !== null) {
|
||||
if (typeof options.content === "string") {
|
||||
modal.innerHTML = options.content;
|
||||
} else {
|
||||
modal.appendChild(options.content);
|
||||
}
|
||||
}
|
||||
(options.parent || document.body).appendChild(modal);
|
||||
return modal;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._dialog = null;
|
||||
this._titleElement = null;
|
||||
this._metaElement = null;
|
||||
this._trigger = null;
|
||||
this._restoreFocus = true;
|
||||
this._busy = false;
|
||||
|
||||
/**
|
||||
* Optional function called with a reason string ("escape",
|
||||
* "backdrop" or the reason passed to requestClose()) when the
|
||||
* user tries to dismiss the modal. Return false to keep the
|
||||
* modal open. Not called for direct close() calls.
|
||||
*/
|
||||
this.closeGuard = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
adoptFrameStyles(this.getRootNode());
|
||||
this._build();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name) {
|
||||
if (!this._dialog) {
|
||||
return;
|
||||
}
|
||||
if (name === "modal-title" && this._titleElement) {
|
||||
this._titleElement.textContent = this.getAttribute("modal-title") || "";
|
||||
}
|
||||
if (name === "modal-meta" && this._metaElement) {
|
||||
this._syncMeta();
|
||||
}
|
||||
}
|
||||
|
||||
/** The underlying HTMLDialogElement, or null if unsupported */
|
||||
get dialog() {
|
||||
return this._dialog;
|
||||
}
|
||||
|
||||
/** True if the modal is currently open */
|
||||
get open() {
|
||||
return !!(this._dialog && this._dialog.open);
|
||||
}
|
||||
|
||||
/** The .modal-title element, or null if there is no header */
|
||||
get titleElement() {
|
||||
return this._titleElement;
|
||||
}
|
||||
|
||||
/** The .modal-meta element, or null if there is no header */
|
||||
get metaElement() {
|
||||
return this._metaElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* While true, Escape, backdrop clicks and requestClose() will not
|
||||
* close the modal. Use this while a save or delete is in flight.
|
||||
*/
|
||||
get busy() {
|
||||
return this._busy;
|
||||
}
|
||||
|
||||
set busy(value) {
|
||||
this._busy = !!value;
|
||||
this.toggleAttribute("busy", this._busy);
|
||||
}
|
||||
|
||||
/** Set the header title text */
|
||||
setTitle(text) {
|
||||
this.setAttribute("modal-title", text == null ? "" : text);
|
||||
}
|
||||
|
||||
/** Set the header meta chip text - blank hides the chip */
|
||||
setMeta(text) {
|
||||
this.setAttribute("modal-meta", text == null ? "" : text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the modal. Records options.trigger (defaults to the
|
||||
* currently focused element) so focus can be restored on close.
|
||||
*/
|
||||
showModal(options) {
|
||||
options = options || {};
|
||||
if (!this.isConnected) {
|
||||
document.body.appendChild(this);
|
||||
}
|
||||
if (!this._dialog) {
|
||||
return;
|
||||
}
|
||||
if (options.trigger !== undefined) {
|
||||
this._trigger = options.trigger;
|
||||
} else if (!this._dialog.open) {
|
||||
var active = document.activeElement;
|
||||
this._trigger = active && active !== document.body ? active : null;
|
||||
}
|
||||
this._restoreFocus = true;
|
||||
if (!this._dialog.open) {
|
||||
this._dialog.showModal();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("datasette-modal-open", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal unconditionally, skipping busy and closeGuard.
|
||||
* Pass {restoreFocus: false} to leave focus where it is.
|
||||
*/
|
||||
close(options) {
|
||||
options = options || {};
|
||||
if (!this._dialog) {
|
||||
return;
|
||||
}
|
||||
this._restoreFocus = options.restoreFocus !== false;
|
||||
if (this._dialog.open) {
|
||||
this._dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the modal to close on the user's behalf. Does nothing while
|
||||
* busy, and consults closeGuard if one is set. Returns true if the
|
||||
* modal was closed.
|
||||
*/
|
||||
requestClose(reason) {
|
||||
if (!this._dialog || !this._dialog.open || this._busy) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typeof this.closeGuard === "function" &&
|
||||
!this.closeGuard(reason || "dismiss")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
this.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
_build() {
|
||||
if (this._dialog || !DatasetteModal.supported) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = document.createElement("dialog");
|
||||
var dialogId = this.getAttribute("dialog-id");
|
||||
if (dialogId) {
|
||||
dialog.id = dialogId;
|
||||
}
|
||||
var dialogClass = this.getAttribute("dialog-class");
|
||||
if (dialogClass) {
|
||||
dialog.className = dialogClass;
|
||||
}
|
||||
|
||||
// Move any existing light DOM content into the dialog
|
||||
var content = document.createDocumentFragment();
|
||||
while (this.firstChild) {
|
||||
content.appendChild(this.firstChild);
|
||||
}
|
||||
|
||||
if (this.hasAttribute("modal-title")) {
|
||||
var header = document.createElement("div");
|
||||
header.className = "modal-header";
|
||||
|
||||
this._titleElement = document.createElement("span");
|
||||
this._titleElement.className = "modal-title";
|
||||
this._titleElement.id =
|
||||
this.getAttribute("title-id") ||
|
||||
(dialogId
|
||||
? dialogId + "-title"
|
||||
: "datasette-modal-title-" + ++titleIdCounter);
|
||||
this._titleElement.textContent = this.getAttribute("modal-title");
|
||||
|
||||
this._metaElement = document.createElement("span");
|
||||
this._metaElement.className = "modal-meta";
|
||||
|
||||
header.appendChild(this._titleElement);
|
||||
header.appendChild(this._metaElement);
|
||||
dialog.appendChild(header);
|
||||
this._syncMeta();
|
||||
}
|
||||
|
||||
var labelledBy =
|
||||
this.getAttribute("labelled-by") ||
|
||||
(this._titleElement ? this._titleElement.id : null);
|
||||
if (labelledBy) {
|
||||
dialog.setAttribute("aria-labelledby", labelledBy);
|
||||
}
|
||||
var describedBy = this.getAttribute("described-by");
|
||||
if (describedBy) {
|
||||
dialog.setAttribute("aria-describedby", describedBy);
|
||||
}
|
||||
|
||||
dialog.appendChild(content);
|
||||
this.appendChild(dialog);
|
||||
this._dialog = dialog;
|
||||
|
||||
dialog.addEventListener("click", (ev) => {
|
||||
if (ev.target === dialog) {
|
||||
this.requestClose("backdrop");
|
||||
return;
|
||||
}
|
||||
// Declarative cancel buttons: any element inside the modal
|
||||
// with a data-modal-cancel attribute requests a close
|
||||
var cancelTrigger =
|
||||
ev.target.closest && ev.target.closest("[data-modal-cancel]");
|
||||
if (cancelTrigger && dialog.contains(cancelTrigger)) {
|
||||
this.requestClose("cancel");
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("keydown", (ev) => {
|
||||
if (ev.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this.requestClose("escape");
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", (ev) => {
|
||||
ev.preventDefault();
|
||||
this.requestClose("escape");
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", () => {
|
||||
var restoreFocus = this._restoreFocus;
|
||||
this._restoreFocus = true;
|
||||
if (
|
||||
restoreFocus &&
|
||||
this._trigger &&
|
||||
this._trigger.isConnected &&
|
||||
typeof this._trigger.focus === "function"
|
||||
) {
|
||||
this._trigger.focus();
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("datasette-modal-close", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_syncMeta() {
|
||||
var meta = this.getAttribute("modal-meta") || "";
|
||||
this._metaElement.textContent = meta;
|
||||
this._metaElement.hidden = meta === "";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("datasette-modal", DatasetteModal);
|
||||
window.DatasetteModal = DatasetteModal;
|
||||
})();
|
||||
|
|
@ -864,6 +864,7 @@ function showTableCreateDialogError(state, message) {
|
|||
|
||||
function setTableCreateDialogSaving(state, isSaving) {
|
||||
state.isSaving = isSaving;
|
||||
state.modal.busy = isSaving;
|
||||
state.cancelButton.disabled = isSaving;
|
||||
state.saveButton.disabled = isSaving;
|
||||
state.addColumnButton.disabled = isSaving;
|
||||
|
|
@ -1492,8 +1493,7 @@ async function saveTableCreateDialog(state) {
|
|||
var tableUrl =
|
||||
responseData.table_url ||
|
||||
fallbackTableUrl(responseData.table || payload.table);
|
||||
state.shouldRestoreFocus = false;
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
if (tableUrl) {
|
||||
location.href = tableUrl;
|
||||
} else {
|
||||
|
|
@ -1522,8 +1522,7 @@ function closeTableCreateDialogIfConfirmed(state) {
|
|||
if (!confirmDiscardTableCreateChanges(state)) {
|
||||
return false;
|
||||
}
|
||||
state.shouldRestoreFocus = true;
|
||||
state.dialog.close();
|
||||
state.modal.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1531,18 +1530,16 @@ function ensureTableCreateDialog(manager) {
|
|||
if (tableCreateDialogState) {
|
||||
return tableCreateDialogState;
|
||||
}
|
||||
if (!window.HTMLDialogElement) {
|
||||
if (!window.DatasetteModal || !window.DatasetteModal.supported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var dialog = document.createElement("dialog");
|
||||
dialog.id = TABLE_CREATE_DIALOG_ID;
|
||||
dialog.className = "table-create-dialog";
|
||||
dialog.setAttribute("aria-labelledby", "table-create-title");
|
||||
dialog.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="table-create-title">Create table</span>
|
||||
</div>
|
||||
var modal = window.DatasetteModal.create({
|
||||
id: TABLE_CREATE_DIALOG_ID,
|
||||
className: "table-create-dialog",
|
||||
title: "Create table",
|
||||
titleId: "table-create-title",
|
||||
content: `
|
||||
<form class="table-create-form" method="post" novalidate>
|
||||
<p class="table-create-error" id="table-create-error" role="alert" tabindex="-1" hidden></p>
|
||||
<div class="table-create-fields">
|
||||
|
|
@ -1562,17 +1559,18 @@ function ensureTableCreateDialog(manager) {
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost table-create-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-ghost table-create-cancel" data-modal-cancel>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary table-create-save">Create table</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
`,
|
||||
});
|
||||
var dialog = modal.dialog;
|
||||
|
||||
tableCreateDialogState = {
|
||||
modal: modal,
|
||||
dialog: dialog,
|
||||
form: dialog.querySelector(".table-create-form"),
|
||||
title: dialog.querySelector(".modal-title"),
|
||||
error: dialog.querySelector(".table-create-error"),
|
||||
fields: dialog.querySelector(".table-create-fields"),
|
||||
tableName: dialog.querySelector(".table-create-table-name"),
|
||||
|
|
@ -1580,8 +1578,6 @@ function ensureTableCreateDialog(manager) {
|
|||
addColumnButton: dialog.querySelector(".table-create-add-column"),
|
||||
cancelButton: dialog.querySelector(".table-create-cancel"),
|
||||
saveButton: dialog.querySelector(".table-create-save"),
|
||||
currentButton: null,
|
||||
shouldRestoreFocus: true,
|
||||
isSaving: false,
|
||||
initialSignature: "",
|
||||
nextColumnIndex: 0,
|
||||
|
|
@ -1605,44 +1601,19 @@ function ensureTableCreateDialog(manager) {
|
|||
row.querySelector(".table-create-column-name").focus();
|
||||
});
|
||||
|
||||
tableCreateDialogState.cancelButton.addEventListener("click", function () {
|
||||
closeTableCreateDialogIfConfirmed(tableCreateDialogState);
|
||||
});
|
||||
|
||||
tableCreateDialogState.tableName.addEventListener("input", function () {
|
||||
clearTableCreateDialogError(tableCreateDialogState);
|
||||
});
|
||||
|
||||
dialog.addEventListener("click", function (ev) {
|
||||
if (ev.target === dialog) {
|
||||
closeTableCreateDialogIfConfirmed(tableCreateDialogState);
|
||||
}
|
||||
});
|
||||
modal.closeGuard = function () {
|
||||
var state = tableCreateDialogState;
|
||||
return !state.isSaving && confirmDiscardTableCreateChanges(state);
|
||||
};
|
||||
|
||||
dialog.addEventListener("keydown", function (ev) {
|
||||
if (ev.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
closeTableCreateDialogIfConfirmed(tableCreateDialogState);
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", function (ev) {
|
||||
ev.preventDefault();
|
||||
closeTableCreateDialogIfConfirmed(tableCreateDialogState);
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", function () {
|
||||
modal.addEventListener("datasette-modal-close", function () {
|
||||
var state = tableCreateDialogState;
|
||||
clearTableCreateDialogError(state);
|
||||
setTableCreateDialogSaving(state, false);
|
||||
if (
|
||||
state.shouldRestoreFocus &&
|
||||
state.currentButton &&
|
||||
document.contains(state.currentButton)
|
||||
) {
|
||||
state.currentButton.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return tableCreateDialogState;
|
||||
|
|
@ -1663,22 +1634,19 @@ function openTableCreateDialog(button, manager) {
|
|||
menu.open = false;
|
||||
}
|
||||
state.manager = manager;
|
||||
state.currentButton = button;
|
||||
state.shouldRestoreFocus = true;
|
||||
state.title.textContent = "Create a table in " + data.databaseName;
|
||||
state.modal.setTitle("Create a table in " + data.databaseName);
|
||||
clearTableCreateDialogError(state);
|
||||
resetTableCreateDialog(state);
|
||||
loadTableCreateForeignKeyTargets(state);
|
||||
if (!state.dialog.open) {
|
||||
state.dialog.showModal();
|
||||
}
|
||||
state.modal.showModal({ trigger: button });
|
||||
state.tableName.focus();
|
||||
}
|
||||
|
||||
function initTableCreateActions(manager) {
|
||||
if (
|
||||
!window.fetch ||
|
||||
!window.HTMLDialogElement ||
|
||||
!window.DatasetteModal ||
|
||||
!window.DatasetteModal.supported ||
|
||||
!databaseCreateTableData()
|
||||
) {
|
||||
return;
|
||||
|
|
@ -1697,6 +1665,7 @@ function initTableCreateActions(manager) {
|
|||
|
||||
function setRowDeleteDialogBusy(state, isBusy) {
|
||||
state.isBusy = isBusy;
|
||||
state.modal.busy = isBusy;
|
||||
state.confirmButton.disabled = isBusy;
|
||||
state.cancelButton.disabled = isBusy;
|
||||
state.confirmButton.textContent = isBusy ? "Deleting..." : "Delete row";
|
||||
|
|
@ -1943,6 +1912,7 @@ function showTableAlterDialogError(state, message) {
|
|||
|
||||
function setTableAlterDialogSaving(state, isSaving) {
|
||||
state.isSaving = isSaving;
|
||||
state.modal.busy = isSaving;
|
||||
state.cancelButton.disabled = isSaving;
|
||||
state.addColumnButton.disabled = isSaving;
|
||||
state.backButton.disabled = isSaving;
|
||||
|
|
@ -3078,8 +3048,7 @@ async function applyTableAlterChanges(state, result) {
|
|||
result.columnTypeAssignments || [],
|
||||
tableUrl,
|
||||
);
|
||||
state.shouldRestoreFocus = false;
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
if (tableAlterResultRenamesTable(result) && tableUrl) {
|
||||
window.location.href = tableUrl;
|
||||
} else {
|
||||
|
|
@ -3140,8 +3109,7 @@ async function dropTableFromAlterDialog(state) {
|
|||
if (!response.ok || (responseData && responseData.ok === false)) {
|
||||
throw rowMutationRequestError(response, responseData);
|
||||
}
|
||||
state.shouldRestoreFocus = false;
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
window.location.href = tableAlterDatabaseUrl() || "/";
|
||||
} catch (error) {
|
||||
setTableAlterDialogSaving(state, false);
|
||||
|
|
@ -3184,8 +3152,7 @@ function closeTableAlterDialogIfConfirmed(state) {
|
|||
if (!confirmDiscardTableAlterChanges(state)) {
|
||||
return false;
|
||||
}
|
||||
state.shouldRestoreFocus = true;
|
||||
state.dialog.close();
|
||||
state.modal.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -3193,8 +3160,7 @@ function closeTableAlterDialog(state) {
|
|||
if (!state || state.isSaving) {
|
||||
return false;
|
||||
}
|
||||
state.shouldRestoreFocus = true;
|
||||
state.dialog.close();
|
||||
state.modal.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -3202,18 +3168,16 @@ function ensureTableAlterDialog(manager) {
|
|||
if (tableAlterDialogState) {
|
||||
return tableAlterDialogState;
|
||||
}
|
||||
if (!window.HTMLDialogElement) {
|
||||
if (!window.DatasetteModal || !window.DatasetteModal.supported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var dialog = document.createElement("dialog");
|
||||
dialog.id = TABLE_ALTER_DIALOG_ID;
|
||||
dialog.className = "table-alter-dialog";
|
||||
dialog.setAttribute("aria-labelledby", "table-alter-title");
|
||||
dialog.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="table-alter-title">Alter table</span>
|
||||
</div>
|
||||
var modal = window.DatasetteModal.create({
|
||||
id: TABLE_ALTER_DIALOG_ID,
|
||||
className: "table-alter-dialog",
|
||||
title: "Alter table",
|
||||
titleId: "table-alter-title",
|
||||
content: `
|
||||
<form class="table-alter-form" method="post" novalidate>
|
||||
<p class="table-alter-error" id="table-alter-error" role="alert" tabindex="-1" hidden></p>
|
||||
<div class="table-alter-fields">
|
||||
|
|
@ -3243,13 +3207,14 @@ function ensureTableAlterDialog(manager) {
|
|||
<button type="submit" class="btn btn-primary table-alter-save">Review changes</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
`,
|
||||
});
|
||||
var dialog = modal.dialog;
|
||||
|
||||
tableAlterDialogState = {
|
||||
modal: modal,
|
||||
dialog: dialog,
|
||||
form: dialog.querySelector(".table-alter-form"),
|
||||
title: dialog.querySelector(".modal-title"),
|
||||
error: dialog.querySelector(".table-alter-error"),
|
||||
fields: dialog.querySelector(".table-alter-fields"),
|
||||
tableOptions: dialog.querySelector(".table-alter-table-options"),
|
||||
|
|
@ -3261,8 +3226,6 @@ function ensureTableAlterDialog(manager) {
|
|||
dropButton: dialog.querySelector(".table-alter-drop"),
|
||||
cancelButton: dialog.querySelector(".table-alter-cancel"),
|
||||
saveButton: dialog.querySelector(".table-alter-save"),
|
||||
currentButton: null,
|
||||
shouldRestoreFocus: true,
|
||||
isSaving: false,
|
||||
initialSignature: "",
|
||||
originalTableName: "",
|
||||
|
|
@ -3325,36 +3288,15 @@ function ensureTableAlterDialog(manager) {
|
|||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("click", function (ev) {
|
||||
if (ev.target === dialog) {
|
||||
closeTableAlterDialogIfConfirmed(tableAlterDialogState);
|
||||
}
|
||||
});
|
||||
modal.closeGuard = function () {
|
||||
var state = tableAlterDialogState;
|
||||
return !state.isSaving && confirmDiscardTableAlterChanges(state);
|
||||
};
|
||||
|
||||
dialog.addEventListener("keydown", function (ev) {
|
||||
if (ev.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
closeTableAlterDialogIfConfirmed(tableAlterDialogState);
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", function (ev) {
|
||||
ev.preventDefault();
|
||||
closeTableAlterDialogIfConfirmed(tableAlterDialogState);
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", function () {
|
||||
modal.addEventListener("datasette-modal-close", function () {
|
||||
var state = tableAlterDialogState;
|
||||
clearTableAlterDialogError(state);
|
||||
setTableAlterDialogSaving(state, false);
|
||||
if (
|
||||
state.shouldRestoreFocus &&
|
||||
state.currentButton &&
|
||||
document.contains(state.currentButton)
|
||||
) {
|
||||
state.currentButton.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return tableAlterDialogState;
|
||||
|
|
@ -3375,9 +3317,7 @@ function openTableAlterDialog(button, manager) {
|
|||
menu.open = false;
|
||||
}
|
||||
state.manager = manager;
|
||||
state.currentButton = button;
|
||||
state.shouldRestoreFocus = true;
|
||||
state.title.textContent = "Alter table " + data.tableName;
|
||||
state.modal.setTitle("Alter table " + data.tableName);
|
||||
clearTableAlterDialogError(state);
|
||||
resetTableAlterDialog(state, data);
|
||||
loadSchemaDialogForeignKeyTargets(
|
||||
|
|
@ -3386,9 +3326,7 @@ function openTableAlterDialog(button, manager) {
|
|||
tableAlterForeignKeyTargetsUrl(),
|
||||
{ filterByType: false },
|
||||
);
|
||||
if (!state.dialog.open) {
|
||||
state.dialog.showModal();
|
||||
}
|
||||
state.modal.showModal({ trigger: button });
|
||||
var firstName = state.columnList.querySelector(".table-alter-column-name");
|
||||
if (firstName) {
|
||||
firstName.focus();
|
||||
|
|
@ -3396,7 +3334,12 @@ function openTableAlterDialog(button, manager) {
|
|||
}
|
||||
|
||||
function initTableAlterActions(manager) {
|
||||
if (!window.fetch || !window.HTMLDialogElement || !tableAlterData()) {
|
||||
if (
|
||||
!window.fetch ||
|
||||
!window.DatasetteModal ||
|
||||
!window.DatasetteModal.supported ||
|
||||
!tableAlterData()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener("click", function (ev) {
|
||||
|
|
@ -3679,31 +3622,31 @@ function ensureRowDeleteDialog(manager) {
|
|||
if (rowDeleteDialogState) {
|
||||
return rowDeleteDialogState;
|
||||
}
|
||||
if (!window.HTMLDialogElement) {
|
||||
if (!window.DatasetteModal || !window.DatasetteModal.supported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var dialog = document.createElement("dialog");
|
||||
dialog.id = ROW_DELETE_DIALOG_ID;
|
||||
dialog.className = "row-delete-dialog";
|
||||
dialog.setAttribute("aria-labelledby", "row-delete-title");
|
||||
dialog.setAttribute("aria-describedby", "row-delete-message");
|
||||
dialog.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="row-delete-title">Delete row</span>
|
||||
</div>
|
||||
var modal = window.DatasetteModal.create({
|
||||
id: ROW_DELETE_DIALOG_ID,
|
||||
className: "row-delete-dialog",
|
||||
title: "Delete row",
|
||||
titleId: "row-delete-title",
|
||||
describedBy: "row-delete-message",
|
||||
content: `
|
||||
<p class="row-delete-message" id="row-delete-message">Delete row <span class="row-delete-id"></span>?</p>
|
||||
<p class="row-delete-error" role="alert" hidden></p>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost row-delete-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-ghost row-delete-cancel" data-modal-cancel>Cancel</button>
|
||||
<button type="button" class="btn btn-primary row-delete-confirm">Delete row</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
`,
|
||||
});
|
||||
var dialog = modal.dialog;
|
||||
|
||||
rowDeleteDialogState = {
|
||||
modal: modal,
|
||||
dialog: dialog,
|
||||
title: dialog.querySelector(".modal-title"),
|
||||
title: modal.titleElement,
|
||||
message: dialog.querySelector(".row-delete-message"),
|
||||
rowId: dialog.querySelector(".row-delete-id"),
|
||||
error: dialog.querySelector(".row-delete-error"),
|
||||
|
|
@ -3714,23 +3657,8 @@ function ensureRowDeleteDialog(manager) {
|
|||
currentPkPath: null,
|
||||
manager: manager,
|
||||
isBusy: false,
|
||||
shouldRestoreFocus: true,
|
||||
};
|
||||
|
||||
rowDeleteDialogState.cancelButton.addEventListener("click", function () {
|
||||
if (!rowDeleteDialogState.isBusy) {
|
||||
rowDeleteDialogState.shouldRestoreFocus = true;
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("click", function (ev) {
|
||||
if (ev.target === dialog && !rowDeleteDialogState.isBusy) {
|
||||
rowDeleteDialogState.shouldRestoreFocus = true;
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("keydown", function (ev) {
|
||||
if (
|
||||
ev.key === "Enter" &&
|
||||
|
|
@ -3740,39 +3668,13 @@ function ensureRowDeleteDialog(manager) {
|
|||
if (!rowDeleteDialogState.isBusy) {
|
||||
rowDeleteDialogState.confirmButton.click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
if (rowDeleteDialogState.isBusy) {
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
rowDeleteDialogState.shouldRestoreFocus = true;
|
||||
dialog.close();
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", function (ev) {
|
||||
if (rowDeleteDialogState.isBusy) {
|
||||
ev.preventDefault();
|
||||
} else {
|
||||
rowDeleteDialogState.shouldRestoreFocus = true;
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", function () {
|
||||
modal.addEventListener("datasette-modal-close", function () {
|
||||
var state = rowDeleteDialogState;
|
||||
clearRowDeleteDialogError(state);
|
||||
setRowDeleteDialogBusy(state, false);
|
||||
if (
|
||||
state.shouldRestoreFocus &&
|
||||
state.currentButton &&
|
||||
document.contains(state.currentButton)
|
||||
) {
|
||||
state.currentButton.focus();
|
||||
}
|
||||
});
|
||||
|
||||
rowDeleteDialogState.confirmButton.addEventListener(
|
||||
|
|
@ -3799,8 +3701,7 @@ function ensureRowDeleteDialog(manager) {
|
|||
throw rowMutationRequestError(response, data);
|
||||
}
|
||||
if (data && data.redirect) {
|
||||
state.shouldRestoreFocus = false;
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
|
|
@ -3812,8 +3713,7 @@ function ensureRowDeleteDialog(manager) {
|
|||
var statusMessage = state.currentPkPath
|
||||
? "Deleted row " + state.currentPkPath + "."
|
||||
: "Deleted row.";
|
||||
state.shouldRestoreFocus = false;
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
state.currentRow.remove();
|
||||
showRowMutationStatus(state.manager, statusMessage, false);
|
||||
if (focusTarget && document.contains(focusTarget)) {
|
||||
|
|
@ -3842,11 +3742,9 @@ function openRowDeleteDialog(button, manager) {
|
|||
}
|
||||
|
||||
state.manager = manager;
|
||||
state.currentButton = button;
|
||||
state.currentRow = row;
|
||||
state.currentDeleteUrl = rowDeleteUrl(row);
|
||||
state.currentPkPath = rowDisplayLabel(row);
|
||||
state.shouldRestoreFocus = true;
|
||||
|
||||
clearRowDeleteDialogError(state);
|
||||
setRowDeleteDialogBusy(state, false);
|
||||
|
|
@ -3858,14 +3756,16 @@ function openRowDeleteDialog(button, manager) {
|
|||
);
|
||||
state.rowId.textContent = state.currentPkPath || "this row";
|
||||
|
||||
if (!state.dialog.open) {
|
||||
state.dialog.showModal();
|
||||
}
|
||||
state.modal.showModal({ trigger: button });
|
||||
state.confirmButton.focus();
|
||||
}
|
||||
|
||||
function initRowDeleteActions(manager) {
|
||||
if (!window.fetch || !window.HTMLDialogElement) {
|
||||
if (
|
||||
!window.fetch ||
|
||||
!window.DatasetteModal ||
|
||||
!window.DatasetteModal.supported
|
||||
) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener("click", function (ev) {
|
||||
|
|
@ -4659,8 +4559,7 @@ function closeRowEditDialogIfConfirmed(state) {
|
|||
if (!confirmDiscardRowEditChanges(state)) {
|
||||
return false;
|
||||
}
|
||||
state.shouldRestoreFocus = true;
|
||||
state.dialog.close();
|
||||
state.modal.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -4673,8 +4572,7 @@ function scheduleCloseRowEditDialogIfConfirmed(state) {
|
|||
return false;
|
||||
}
|
||||
if (!rowEditDialogHasChanges(state)) {
|
||||
state.shouldRestoreFocus = true;
|
||||
state.dialog.close();
|
||||
state.modal.close();
|
||||
return true;
|
||||
}
|
||||
state.isClosePending = true;
|
||||
|
|
@ -4777,9 +4675,8 @@ async function saveRowEditDialog(state) {
|
|||
}
|
||||
var formValues = collectRowFormValues(state);
|
||||
if (state.mode === "edit" && !Object.keys(formValues).length) {
|
||||
state.shouldRestoreFocus = true;
|
||||
hideRowMutationStatus();
|
||||
state.dialog.close();
|
||||
state.modal.close();
|
||||
return;
|
||||
}
|
||||
var payload =
|
||||
|
|
@ -4812,9 +4709,8 @@ async function saveRowEditDialog(state) {
|
|||
insertedRowData,
|
||||
insertData.primaryKeys || [],
|
||||
);
|
||||
state.shouldRestoreFocus = false;
|
||||
if (!insertedRowId) {
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
var missingIdStatus = showRowMutationStatus(
|
||||
state.manager,
|
||||
"Inserted row. Refresh the page to see it.",
|
||||
|
|
@ -4830,7 +4726,7 @@ async function saveRowEditDialog(state) {
|
|||
try {
|
||||
insertedRow = await fetchUpdatedRowElement(state);
|
||||
} catch (_error) {
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
var refreshFailedStatus = showRowMutationStatus(
|
||||
state.manager,
|
||||
"Inserted row, but could not refresh the table row. Refresh the page to see it.",
|
||||
|
|
@ -4845,7 +4741,7 @@ async function saveRowEditDialog(state) {
|
|||
rowTitleLabel(insertedRow),
|
||||
);
|
||||
var addedRow = addInsertedRowToPage(insertedRow);
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
showRowMutationStatus(state.manager, insertedStatusMessage, false);
|
||||
if (addedRow) {
|
||||
var insertedFocusTarget =
|
||||
|
|
@ -4854,7 +4750,7 @@ async function saveRowEditDialog(state) {
|
|||
insertedFocusTarget.focus();
|
||||
}
|
||||
} else {
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
var filteredStatus = showRowMutationStatus(
|
||||
state.manager,
|
||||
"Inserted row. It does not match the current filters.",
|
||||
|
|
@ -4866,8 +4762,7 @@ async function saveRowEditDialog(state) {
|
|||
}
|
||||
|
||||
if (isRowPage()) {
|
||||
state.shouldRestoreFocus = false;
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
|
@ -4903,8 +4798,7 @@ async function saveRowEditDialog(state) {
|
|||
);
|
||||
}
|
||||
|
||||
state.shouldRestoreFocus = false;
|
||||
state.dialog.close();
|
||||
state.modal.close({ restoreFocus: false });
|
||||
if (focusTarget && document.contains(focusTarget)) {
|
||||
focusTarget.focus();
|
||||
}
|
||||
|
|
@ -5023,18 +4917,16 @@ function ensureRowEditDialog(manager) {
|
|||
if (rowEditDialogState) {
|
||||
return rowEditDialogState;
|
||||
}
|
||||
if (!window.HTMLDialogElement) {
|
||||
if (!window.DatasetteModal || !window.DatasetteModal.supported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var dialog = document.createElement("dialog");
|
||||
dialog.id = ROW_EDIT_DIALOG_ID;
|
||||
dialog.className = "row-edit-dialog";
|
||||
dialog.setAttribute("aria-labelledby", "row-edit-title");
|
||||
dialog.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="row-edit-title">Edit row</span>
|
||||
</div>
|
||||
var modal = window.DatasetteModal.create({
|
||||
id: ROW_EDIT_DIALOG_ID,
|
||||
className: "row-edit-dialog",
|
||||
title: "Edit row",
|
||||
titleId: "row-edit-title",
|
||||
content: `
|
||||
<form class="row-edit-form" method="post">
|
||||
<p class="row-edit-summary" id="row-edit-summary" hidden></p>
|
||||
<p class="row-edit-loading" role="status" aria-live="polite">Loading row...</p>
|
||||
|
|
@ -5045,20 +4937,21 @@ function ensureRowEditDialog(manager) {
|
|||
<button type="submit" class="btn btn-primary row-edit-save" disabled>Save</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
`,
|
||||
});
|
||||
var dialog = modal.dialog;
|
||||
|
||||
rowEditDialogState = {
|
||||
modal: modal,
|
||||
dialog: dialog,
|
||||
form: dialog.querySelector(".row-edit-form"),
|
||||
title: dialog.querySelector(".modal-title"),
|
||||
title: modal.titleElement,
|
||||
summary: dialog.querySelector(".row-edit-summary"),
|
||||
loading: dialog.querySelector(".row-edit-loading"),
|
||||
error: dialog.querySelector(".row-edit-error"),
|
||||
fields: dialog.querySelector(".row-edit-fields"),
|
||||
cancelButton: dialog.querySelector(".row-edit-cancel"),
|
||||
saveButton: dialog.querySelector(".row-edit-save"),
|
||||
currentButton: null,
|
||||
currentRow: null,
|
||||
currentRowId: null,
|
||||
currentPkPath: null,
|
||||
|
|
@ -5072,7 +4965,6 @@ function ensureRowEditDialog(manager) {
|
|||
isSaving: false,
|
||||
isClosePending: false,
|
||||
hasLoaded: false,
|
||||
shouldRestoreFocus: true,
|
||||
};
|
||||
|
||||
rowEditDialogState.form.addEventListener("submit", function (ev) {
|
||||
|
|
@ -5082,31 +4974,21 @@ function ensureRowEditDialog(manager) {
|
|||
|
||||
rowEditDialogState.cancelButton.addEventListener("click", function () {
|
||||
if (!rowEditDialogState.isSaving) {
|
||||
rowEditDialogState.shouldRestoreFocus = true;
|
||||
dialog.close();
|
||||
modal.close();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("click", function (ev) {
|
||||
if (ev.target === dialog) {
|
||||
closeRowEditDialogIfConfirmed(rowEditDialogState);
|
||||
modal.closeGuard = function (reason) {
|
||||
var state = rowEditDialogState;
|
||||
if (reason === "escape") {
|
||||
// scheduleClose... closes the dialog itself if the user confirms
|
||||
scheduleCloseRowEditDialogIfConfirmed(state);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return !state.isSaving && confirmDiscardRowEditChanges(state);
|
||||
};
|
||||
|
||||
dialog.addEventListener("keydown", function (ev) {
|
||||
if (ev.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
scheduleCloseRowEditDialogIfConfirmed(rowEditDialogState);
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", function (ev) {
|
||||
ev.preventDefault();
|
||||
scheduleCloseRowEditDialogIfConfirmed(rowEditDialogState);
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", function () {
|
||||
modal.addEventListener("datasette-modal-close", function () {
|
||||
var state = rowEditDialogState;
|
||||
state.loadId += 1;
|
||||
state.isClosePending = false;
|
||||
|
|
@ -5115,13 +4997,6 @@ function ensureRowEditDialog(manager) {
|
|||
destroyRowEditFields(state);
|
||||
setRowEditDialogLoading(state, false);
|
||||
setRowEditDialogSaving(state, false);
|
||||
if (
|
||||
state.shouldRestoreFocus &&
|
||||
state.currentButton &&
|
||||
document.contains(state.currentButton)
|
||||
) {
|
||||
state.currentButton.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return rowEditDialogState;
|
||||
|
|
@ -5139,7 +5014,6 @@ async function openRowEditDialog(button, manager) {
|
|||
|
||||
state.manager = manager;
|
||||
state.mode = "edit";
|
||||
state.currentButton = button;
|
||||
state.currentRow = row;
|
||||
state.currentRowId = row.getAttribute("data-row") || "";
|
||||
state.currentPkPath = rowDisplayLabel(row);
|
||||
|
|
@ -5154,7 +5028,6 @@ async function openRowEditDialog(button, manager) {
|
|||
} else {
|
||||
state.form.removeAttribute("action");
|
||||
}
|
||||
state.shouldRestoreFocus = true;
|
||||
state.hasLoaded = false;
|
||||
state.loadId += 1;
|
||||
var loadId = state.loadId;
|
||||
|
|
@ -5172,9 +5045,7 @@ async function openRowEditDialog(button, manager) {
|
|||
state.summary.hidden = true;
|
||||
state.summary.textContent = "";
|
||||
|
||||
if (!state.dialog.open) {
|
||||
state.dialog.showModal();
|
||||
}
|
||||
state.modal.showModal({ trigger: button });
|
||||
state.cancelButton.focus();
|
||||
|
||||
try {
|
||||
|
|
@ -5214,14 +5085,12 @@ function openRowInsertDialog(button, manager) {
|
|||
|
||||
state.manager = manager;
|
||||
state.mode = "insert";
|
||||
state.currentButton = button;
|
||||
state.currentRow = null;
|
||||
state.currentRowId = null;
|
||||
state.currentPkPath = null;
|
||||
state.currentInsertUrl = tableInsertUrl();
|
||||
state.currentUpdateUrl = null;
|
||||
state.currentFragmentUrl = null;
|
||||
state.shouldRestoreFocus = true;
|
||||
state.hasLoaded = false;
|
||||
state.loadId += 1;
|
||||
|
||||
|
|
@ -5247,14 +5116,16 @@ function openRowInsertDialog(button, manager) {
|
|||
state.summary.hidden = true;
|
||||
state.summary.textContent = "";
|
||||
|
||||
if (!state.dialog.open) {
|
||||
state.dialog.showModal();
|
||||
}
|
||||
state.modal.showModal({ trigger: button });
|
||||
renderRowInsertFields(state, insertData);
|
||||
}
|
||||
|
||||
function initRowEditActions(manager) {
|
||||
if (!window.fetch || !window.HTMLDialogElement) {
|
||||
if (
|
||||
!window.fetch ||
|
||||
!window.DatasetteModal ||
|
||||
!window.DatasetteModal.supported
|
||||
) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener("click", function (ev) {
|
||||
|
|
@ -5268,7 +5139,12 @@ function initRowEditActions(manager) {
|
|||
}
|
||||
|
||||
function initRowInsertActions(manager) {
|
||||
if (!window.fetch || !window.HTMLDialogElement || !tableInsertData()) {
|
||||
if (
|
||||
!window.fetch ||
|
||||
!window.DatasetteModal ||
|
||||
!window.DatasetteModal.supported ||
|
||||
!tableInsertData()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener("click", function (ev) {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ function initMobileColumnActions(manager) {
|
|||
|
||||
if (
|
||||
!window.URLSearchParams ||
|
||||
!window.HTMLDialogElement ||
|
||||
!window.DatasetteModal ||
|
||||
!window.DatasetteModal.supported ||
|
||||
!manager.columnActions
|
||||
) {
|
||||
triggerButton.style.display = "none";
|
||||
|
|
@ -66,32 +67,28 @@ function initMobileColumnActions(manager) {
|
|||
return;
|
||||
}
|
||||
|
||||
var dialog = document.createElement("dialog");
|
||||
dialog.className = "mobile-column-actions-dialog";
|
||||
dialog.id = MOBILE_COLUMN_DIALOG_ID;
|
||||
dialog.setAttribute("aria-labelledby", MOBILE_COLUMN_DIALOG_TITLE_ID);
|
||||
dialog.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="${MOBILE_COLUMN_DIALOG_TITLE_ID}">Column actions</span>
|
||||
<span class="modal-meta"></span>
|
||||
</div>
|
||||
var modal = window.DatasetteModal.create({
|
||||
id: MOBILE_COLUMN_DIALOG_ID,
|
||||
className: "mobile-column-actions-dialog",
|
||||
title: "Column actions",
|
||||
titleId: MOBILE_COLUMN_DIALOG_TITLE_ID,
|
||||
content: `
|
||||
<div class="list-wrap mobile-column-list"></div>
|
||||
<div class="modal-footer">
|
||||
<span class="footer-info">Tap a column to reveal actions.</span>
|
||||
<button type="button" class="btn btn-ghost mobile-column-actions-done">Done</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
`,
|
||||
});
|
||||
var dialog = modal.dialog;
|
||||
|
||||
triggerButton.setAttribute("aria-haspopup", "dialog");
|
||||
triggerButton.setAttribute("aria-controls", MOBILE_COLUMN_DIALOG_ID);
|
||||
triggerButton.setAttribute("aria-expanded", "false");
|
||||
|
||||
var countEl = dialog.querySelector(".modal-meta");
|
||||
var listWrap = dialog.querySelector(".mobile-column-list");
|
||||
var doneButton = dialog.querySelector(".mobile-column-actions-done");
|
||||
var expandedSectionId = null;
|
||||
var shouldRestoreFocus = true;
|
||||
|
||||
function updateExpandedSection() {
|
||||
Array.from(dialog.querySelectorAll(".col-header")).forEach((button) => {
|
||||
|
|
@ -129,12 +126,12 @@ function initMobileColumnActions(manager) {
|
|||
|
||||
function closeDialog(options) {
|
||||
options = options || {};
|
||||
shouldRestoreFocus = options.restoreFocus !== false;
|
||||
if (dialog.open) {
|
||||
dialog.close();
|
||||
var restoreFocus = options.restoreFocus !== false;
|
||||
if (modal.open) {
|
||||
modal.close({ restoreFocus: restoreFocus });
|
||||
} else {
|
||||
triggerButton.setAttribute("aria-expanded", "false");
|
||||
if (shouldRestoreFocus) {
|
||||
if (restoreFocus) {
|
||||
triggerButton.focus();
|
||||
}
|
||||
}
|
||||
|
|
@ -156,9 +153,7 @@ function initMobileColumnActions(manager) {
|
|||
expandedSectionId = null;
|
||||
}
|
||||
|
||||
countEl.textContent = `${headers.length} column${
|
||||
headers.length === 1 ? "" : "s"
|
||||
}`;
|
||||
modal.setMeta(`${headers.length} column${headers.length === 1 ? "" : "s"}`);
|
||||
listWrap.innerHTML = "";
|
||||
|
||||
if (manager.columnActions.shouldShowShowAllColumns()) {
|
||||
|
|
@ -265,9 +260,7 @@ function initMobileColumnActions(manager) {
|
|||
if (!renderDialog()) {
|
||||
return;
|
||||
}
|
||||
if (!dialog.open) {
|
||||
dialog.showModal();
|
||||
}
|
||||
modal.showModal({ trigger: triggerButton });
|
||||
triggerButton.setAttribute("aria-expanded", "true");
|
||||
var focusTarget =
|
||||
dialog.querySelector(".mobile-column-top-action") ||
|
||||
|
|
@ -277,7 +270,7 @@ function initMobileColumnActions(manager) {
|
|||
}
|
||||
|
||||
triggerButton.addEventListener("click", function () {
|
||||
if (dialog.open) {
|
||||
if (modal.open) {
|
||||
closeDialog();
|
||||
} else {
|
||||
openDialog();
|
||||
|
|
@ -288,26 +281,12 @@ function initMobileColumnActions(manager) {
|
|||
closeDialog();
|
||||
});
|
||||
|
||||
dialog.addEventListener("click", function (ev) {
|
||||
if (ev.target === dialog) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", function (ev) {
|
||||
ev.preventDefault();
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", function () {
|
||||
modal.addEventListener("datasette-modal-close", function () {
|
||||
triggerButton.setAttribute("aria-expanded", "false");
|
||||
if (shouldRestoreFocus) {
|
||||
triggerButton.focus();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.innerWidth > MOBILE_COLUMN_BREAKPOINT && dialog.open) {
|
||||
if (window.innerWidth > MOBILE_COLUMN_BREAKPOINT && modal.open) {
|
||||
closeDialog({ restoreFocus: false });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ class NavigationSearch extends HTMLElement {
|
|||
this.renderedMatches = [];
|
||||
this.debounceTimer = null;
|
||||
this.restoreFocusTarget = null;
|
||||
this.shouldRestoreFocus = true;
|
||||
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
|
|
@ -29,38 +28,10 @@ class NavigationSearch extends HTMLElement {
|
|||
display: contents;
|
||||
}
|
||||
|
||||
dialog {
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
max-width: 90vw;
|
||||
width: 600px;
|
||||
max-height: 80vh;
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: slideIn var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: fadeIn var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
/* Frame styles come from the shared <datasette-modal> component */
|
||||
datasette-modal {
|
||||
--datasette-modal-width: min(600px, 90vw);
|
||||
--datasette-modal-max-height: 80vh;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
|
|
@ -255,12 +226,6 @@ class NavigationSearch extends HTMLElement {
|
|||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 640px) {
|
||||
dialog {
|
||||
width: 95vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
|
@ -280,7 +245,7 @@ class NavigationSearch extends HTMLElement {
|
|||
}
|
||||
</style>
|
||||
|
||||
<dialog aria-modal="true" aria-labelledby="${this.titleId}">
|
||||
<datasette-modal labelled-by="${this.titleId}">
|
||||
<div class="search-container">
|
||||
<h2 id="${this.titleId}" class="visually-hidden">Jump to</h2>
|
||||
<p id="${this.instructionsId}" class="visually-hidden">Type to search. Use up and down arrow keys to move through results, Enter to select a result, and Escape to close this menu.</p>
|
||||
|
|
@ -309,12 +274,12 @@ class NavigationSearch extends HTMLElement {
|
|||
<span><kbd>Esc</kbd> Close</span>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</datasette-modal>
|
||||
`;
|
||||
this._modal = this.shadowRoot.querySelector("datasette-modal");
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const dialog = this.shadowRoot.querySelector("dialog");
|
||||
const input = this.shadowRoot.querySelector(".search-input");
|
||||
const closeButton = this.shadowRoot.querySelector(".close-search");
|
||||
const resultsContainer =
|
||||
|
|
@ -322,7 +287,7 @@ class NavigationSearch extends HTMLElement {
|
|||
|
||||
// Global keyboard listener for "/"
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "/" && !this.isInputFocused() && !dialog.open) {
|
||||
if (e.key === "/" && !this.isInputFocused() && !this._modal.open) {
|
||||
e.preventDefault();
|
||||
this.openMenu();
|
||||
}
|
||||
|
|
@ -380,19 +345,8 @@ class NavigationSearch extends HTMLElement {
|
|||
}
|
||||
});
|
||||
|
||||
// Close on backdrop click
|
||||
dialog.addEventListener("click", (e) => {
|
||||
if (e.target === dialog) {
|
||||
this.closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", (e) => {
|
||||
e.preventDefault();
|
||||
this.closeMenu();
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", () => {
|
||||
// Backdrop clicks and the Escape key are handled by <datasette-modal>
|
||||
this._modal.addEventListener("datasette-modal-close", () => {
|
||||
this.onMenuClosed();
|
||||
});
|
||||
|
||||
|
|
@ -465,18 +419,17 @@ class NavigationSearch extends HTMLElement {
|
|||
}
|
||||
|
||||
updateComboboxState() {
|
||||
const dialog = this.shadowRoot.querySelector("dialog");
|
||||
const isOpen = this._modal.open;
|
||||
const input = this.shadowRoot.querySelector(".search-input");
|
||||
const matches = this.renderedMatches || [];
|
||||
this.setElementAttribute(
|
||||
input,
|
||||
"aria-expanded",
|
||||
dialog && dialog.open && matches.length > 0 ? "true" : "false",
|
||||
isOpen && matches.length > 0 ? "true" : "false",
|
||||
);
|
||||
|
||||
if (
|
||||
dialog &&
|
||||
dialog.open &&
|
||||
isOpen &&
|
||||
this.selectedIndex >= 0 &&
|
||||
this.selectedIndex < matches.length
|
||||
) {
|
||||
|
|
@ -854,14 +807,14 @@ class NavigationSearch extends HTMLElement {
|
|||
}
|
||||
|
||||
openMenu(trigger) {
|
||||
const dialog = this.shadowRoot.querySelector("dialog");
|
||||
if (!this._modal.dialog) {
|
||||
// datasette-modal.js is missing or <dialog> is unsupported
|
||||
return;
|
||||
}
|
||||
const input = this.shadowRoot.querySelector(".search-input");
|
||||
|
||||
this.restoreFocusTarget = this.focusRestoreTarget(trigger);
|
||||
this.shouldRestoreFocus = true;
|
||||
if (!dialog.open) {
|
||||
dialog.showModal();
|
||||
}
|
||||
this._modal.showModal({ trigger: this.restoreFocusTarget });
|
||||
this.setNavigationTriggersExpanded(true);
|
||||
input.value = "";
|
||||
input.focus();
|
||||
|
|
@ -874,10 +827,8 @@ class NavigationSearch extends HTMLElement {
|
|||
}
|
||||
|
||||
closeMenu(options = {}) {
|
||||
const dialog = this.shadowRoot.querySelector("dialog");
|
||||
this.shouldRestoreFocus = options.restoreFocus !== false;
|
||||
if (dialog.open) {
|
||||
dialog.close();
|
||||
if (this._modal.open) {
|
||||
this._modal.close({ restoreFocus: options.restoreFocus !== false });
|
||||
} else {
|
||||
this.onMenuClosed();
|
||||
}
|
||||
|
|
@ -889,13 +840,6 @@ class NavigationSearch extends HTMLElement {
|
|||
this.removeElementAttribute(input, "aria-activedescendant");
|
||||
this.setNavigationTriggersExpanded(false);
|
||||
this.setStatus("");
|
||||
if (
|
||||
this.shouldRestoreFocus &&
|
||||
this.restoreFocusTarget &&
|
||||
typeof this.restoreFocusTarget.focus === "function"
|
||||
) {
|
||||
this.restoreFocusTarget.focus();
|
||||
}
|
||||
this.restoreFocusTarget = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,12 @@ function getSetColumnTypeConfig(column) {
|
|||
}
|
||||
|
||||
function canSetColumnType() {
|
||||
return !!(getSetColumnTypeData() && window.HTMLDialogElement && window.fetch);
|
||||
return !!(
|
||||
getSetColumnTypeData() &&
|
||||
window.DatasetteModal &&
|
||||
window.DatasetteModal.supported &&
|
||||
window.fetch
|
||||
);
|
||||
}
|
||||
|
||||
function setColumnTypeActionLabel(column) {
|
||||
|
|
@ -157,6 +162,7 @@ function createSetColumnTypeOption(value, name, description, checked) {
|
|||
|
||||
function setSetColumnTypeDialogBusy(state, isBusy) {
|
||||
state.isBusy = isBusy;
|
||||
state.modal.busy = isBusy;
|
||||
state.saveButton.disabled = isBusy;
|
||||
state.cancelButton.disabled = isBusy;
|
||||
Array.from(
|
||||
|
|
@ -181,33 +187,31 @@ function ensureSetColumnTypeDialog() {
|
|||
if (setColumnTypeDialogState) {
|
||||
return setColumnTypeDialogState;
|
||||
}
|
||||
if (!window.HTMLDialogElement) {
|
||||
if (!window.DatasetteModal || !window.DatasetteModal.supported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var dialog = document.createElement("dialog");
|
||||
dialog.id = SET_COLUMN_TYPE_DIALOG_ID;
|
||||
dialog.className = "set-column-type-dialog";
|
||||
dialog.setAttribute("aria-labelledby", "set-column-type-title");
|
||||
dialog.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="set-column-type-title">Set custom type</span>
|
||||
<span class="modal-meta"></span>
|
||||
</div>
|
||||
var modal = window.DatasetteModal.create({
|
||||
id: SET_COLUMN_TYPE_DIALOG_ID,
|
||||
className: "set-column-type-dialog",
|
||||
title: "Set custom type",
|
||||
titleId: "set-column-type-title",
|
||||
content: `
|
||||
<p class="set-column-type-status"></p>
|
||||
<p class="set-column-type-error" hidden></p>
|
||||
<div class="set-column-type-options"></div>
|
||||
<div class="modal-footer">
|
||||
<span class="footer-info"></span>
|
||||
<button type="button" class="btn btn-ghost set-column-type-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-ghost set-column-type-cancel" data-modal-cancel>Cancel</button>
|
||||
<button type="button" class="btn btn-primary set-column-type-save">Save</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
`,
|
||||
});
|
||||
var dialog = modal.dialog;
|
||||
|
||||
setColumnTypeDialogState = {
|
||||
modal: modal,
|
||||
dialog: dialog,
|
||||
meta: dialog.querySelector(".modal-meta"),
|
||||
status: dialog.querySelector(".set-column-type-status"),
|
||||
error: dialog.querySelector(".set-column-type-error"),
|
||||
optionsWrap: dialog.querySelector(".set-column-type-options"),
|
||||
|
|
@ -219,72 +223,57 @@ function ensureSetColumnTypeDialog() {
|
|||
isBusy: false,
|
||||
};
|
||||
|
||||
setColumnTypeDialogState.cancelButton.addEventListener("click", function () {
|
||||
if (!setColumnTypeDialogState.isBusy) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("click", function (ev) {
|
||||
if (ev.target === dialog && !setColumnTypeDialogState.isBusy) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", function (ev) {
|
||||
if (setColumnTypeDialogState.isBusy) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", function () {
|
||||
modal.addEventListener("datasette-modal-close", function () {
|
||||
clearSetColumnTypeDialogError(setColumnTypeDialogState);
|
||||
setSetColumnTypeDialogBusy(setColumnTypeDialogState, false);
|
||||
});
|
||||
|
||||
setColumnTypeDialogState.saveButton.addEventListener("click", async function () {
|
||||
var state = setColumnTypeDialogState;
|
||||
var selected = state.dialog.querySelector(
|
||||
'input[name="set-column-type-choice"]:checked',
|
||||
);
|
||||
var selectedType = selected ? selected.value : "";
|
||||
var currentType = state.currentConfig.current
|
||||
? state.currentConfig.current.type
|
||||
: "";
|
||||
setColumnTypeDialogState.saveButton.addEventListener(
|
||||
"click",
|
||||
async function () {
|
||||
var state = setColumnTypeDialogState;
|
||||
var selected = state.dialog.querySelector(
|
||||
'input[name="set-column-type-choice"]:checked',
|
||||
);
|
||||
var selectedType = selected ? selected.value : "";
|
||||
var currentType = state.currentConfig.current
|
||||
? state.currentConfig.current.type
|
||||
: "";
|
||||
|
||||
if (selectedType === currentType) {
|
||||
state.dialog.close();
|
||||
return;
|
||||
}
|
||||
|
||||
clearSetColumnTypeDialogError(state);
|
||||
setSetColumnTypeDialogBusy(state, true);
|
||||
|
||||
var payload = {
|
||||
column: state.currentColumn,
|
||||
column_type: selectedType ? { type: selectedType } : null,
|
||||
};
|
||||
|
||||
try {
|
||||
var response = await fetch(getSetColumnTypeData().path, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
var data = await response.json();
|
||||
if (!response.ok || data.ok === false) {
|
||||
var message = (data.errors || ["Request failed"]).join(" ");
|
||||
throw new Error(message);
|
||||
if (selectedType === currentType) {
|
||||
state.modal.close();
|
||||
return;
|
||||
}
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
setSetColumnTypeDialogBusy(state, false);
|
||||
showSetColumnTypeDialogError(state, error.message || "Request failed");
|
||||
}
|
||||
});
|
||||
|
||||
clearSetColumnTypeDialogError(state);
|
||||
setSetColumnTypeDialogBusy(state, true);
|
||||
|
||||
var payload = {
|
||||
column: state.currentColumn,
|
||||
column_type: selectedType ? { type: selectedType } : null,
|
||||
};
|
||||
|
||||
try {
|
||||
var response = await fetch(getSetColumnTypeData().path, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
var data = await response.json();
|
||||
if (!response.ok || data.ok === false) {
|
||||
var message = (data.errors || ["Request failed"]).join(" ");
|
||||
throw new Error(message);
|
||||
}
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
setSetColumnTypeDialogBusy(state, false);
|
||||
showSetColumnTypeDialogError(state, error.message || "Request failed");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return setColumnTypeDialogState;
|
||||
}
|
||||
|
|
@ -306,7 +295,7 @@ function openSetColumnTypeDialog(th) {
|
|||
state.currentColumn = column;
|
||||
state.currentConfig = columnConfig;
|
||||
state.status.textContent = `Column: ${column}`;
|
||||
state.meta.textContent = getColumnTypeText(th) || "Type unavailable";
|
||||
state.modal.setMeta(getColumnTypeText(th) || "Type unavailable");
|
||||
state.footerInfo.textContent = columnConfig.current
|
||||
? `Current custom type: ${columnConfig.current.type}`
|
||||
: "No custom type set.";
|
||||
|
|
@ -341,9 +330,7 @@ function openSetColumnTypeDialog(th) {
|
|||
state.optionsWrap.appendChild(emptyState);
|
||||
}
|
||||
|
||||
if (!state.dialog.open) {
|
||||
state.dialog.showModal();
|
||||
}
|
||||
state.modal.showModal();
|
||||
var selectedOption = state.dialog.querySelector(
|
||||
'input[name="set-column-type-choice"]:checked',
|
||||
);
|
||||
|
|
@ -367,9 +354,10 @@ function shouldShowShowAllColumns() {
|
|||
|
||||
function hasMultipleVisibleColumns(manager) {
|
||||
return (
|
||||
Array.from(document.querySelectorAll(manager.selectors.tableHeaders)).filter(
|
||||
(th) => th.dataset.column && th.dataset.isLinkColumn !== "1",
|
||||
).length > 1
|
||||
Array.from(
|
||||
document.querySelectorAll(manager.selectors.tableHeaders),
|
||||
).filter((th) => th.dataset.column && th.dataset.isLinkColumn !== "1")
|
||||
.length > 1
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -649,10 +637,12 @@ function filterRowNumberFromName(name) {
|
|||
}
|
||||
|
||||
function nextFilterRowNumber(manager) {
|
||||
return filterRowsWithControls(manager).reduce((max, row) => {
|
||||
var column = row.querySelector("select");
|
||||
return Math.max(max, filterRowNumberFromName(column && column.name));
|
||||
}, 0) + 1;
|
||||
return (
|
||||
filterRowsWithControls(manager).reduce((max, row) => {
|
||||
var column = row.querySelector("select");
|
||||
return Math.max(max, filterRowNumberFromName(column && column.name));
|
||||
}, 0) + 1
|
||||
);
|
||||
}
|
||||
|
||||
function setFilterRowNumber(row, number) {
|
||||
|
|
@ -679,9 +669,11 @@ function updateFilterRowButtons(manager) {
|
|||
if (addButton) {
|
||||
addButton.hidden = index !== rows.length - 1 || !column.value;
|
||||
}
|
||||
var visibleButtonCount = [removeButton, addButton].filter(function (button) {
|
||||
return button && !button.hidden;
|
||||
}).length;
|
||||
var visibleButtonCount = [removeButton, addButton].filter(
|
||||
function (button) {
|
||||
return button && !button.hidden;
|
||||
},
|
||||
).length;
|
||||
row.classList.toggle(
|
||||
"filter-controls-row-has-buttons",
|
||||
visibleButtonCount > 0,
|
||||
|
|
@ -703,7 +695,9 @@ function cloneFilterRow(row) {
|
|||
clone.querySelector(".filter-op select").name = "_filter_op";
|
||||
clone.querySelector("input.filter-value").name = "_filter_value";
|
||||
resetFilterRow(clone);
|
||||
clone.querySelectorAll(".filter-row-icon").forEach((button) => button.remove());
|
||||
clone
|
||||
.querySelectorAll(".filter-row-icon")
|
||||
.forEach((button) => button.remove());
|
||||
return clone;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
{% endfor %}
|
||||
<script>window.datasetteVersion = '{{ datasette_version }}';</script>
|
||||
<script src="{{ static('datasette-manager.js') }}" defer></script>
|
||||
<script src="{{ static('datasette-modal.js') }}" defer></script>
|
||||
{% for url in extra_js_urls %}
|
||||
<script {% if url.module %}type="module" {% endif %}src="{{ url.url }}"{% if url.get("sri") %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}></script>
|
||||
{% endfor %}
|
||||
|
|
|
|||
BIN
docs/datasette-modal-example.png
Normal file
BIN
docs/datasette-modal-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
144
docs/generate-datasette-modal-example.sh
Executable file
144
docs/generate-datasette-modal-example.sh
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
#!/bin/bash
|
||||
# Recreate docs/datasette-modal-example.png
|
||||
#
|
||||
# Takes a screenshot of the <datasette-modal> example plugin from the
|
||||
# "Modal dialogs" section of docs/javascript_plugins.rst, running against
|
||||
# a temporary Datasette instance that this script starts and stops.
|
||||
#
|
||||
# Requirements:
|
||||
# - datasette importable by $PYTHON (e.g. "pip install -e ." in this repo)
|
||||
# - uv, for "uvx shot-scraper" and the Pillow-based PNG quantization step
|
||||
# (shot-scraper needs a Playwright browser: "uvx shot-scraper install")
|
||||
#
|
||||
# Environment variable overrides:
|
||||
# PYTHON Python command (default: python3)
|
||||
# SHOT_SCRAPER shot-scraper command (default: uvx shot-scraper)
|
||||
# PORT port for the temporary server (default: 8574)
|
||||
set -euo pipefail
|
||||
|
||||
read -r -a PYTHON_CMD <<< "${PYTHON:-python3}"
|
||||
read -r -a SHOT_SCRAPER_CMD <<< "${SHOT_SCRAPER:-uvx shot-scraper}"
|
||||
PORT="${PORT:-8574}"
|
||||
|
||||
docs_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||
output="$docs_dir/datasette-modal-example.png"
|
||||
|
||||
tmp_dir=$(mktemp -d)
|
||||
server_pid=""
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$server_pid" ]; then
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# A small demo database for the page shown behind the dialog
|
||||
"${PYTHON_CMD[@]}" - "$tmp_dir/demo.db" <<'EOF'
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
conn = sqlite3.connect(sys.argv[1])
|
||||
conn.executescript(
|
||||
"""
|
||||
create table plugins (id integer primary key, name text, description text);
|
||||
insert into plugins (name, description) values
|
||||
('datasette-cluster-map', 'Renders a map of geographic data'),
|
||||
('datasette-vega', 'Visualize data with Vega charts'),
|
||||
('datasette-graphql', 'GraphQL endpoint for Datasette');
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
EOF
|
||||
|
||||
# The example plugin code from the "Modal dialogs" section of
|
||||
# docs/javascript_plugins.rst, injected via extra_body_script()
|
||||
mkdir "$tmp_dir/plugins"
|
||||
cat > "$tmp_dir/plugins/modal_example.py" <<'EOF'
|
||||
from datasette import hookimpl
|
||||
|
||||
|
||||
@hookimpl
|
||||
def extra_body_script():
|
||||
return {
|
||||
"script": """
|
||||
document.addEventListener("datasette_init", function (event) {
|
||||
const manager = event.detail;
|
||||
const modal = manager.createModal({
|
||||
id: "my-plugin-dialog",
|
||||
className: "my-plugin-dialog",
|
||||
title: "My plugin",
|
||||
content: `
|
||||
<p style="padding: 16px 24px">Hello from a plugin!</p>
|
||||
<div class="modal-footer">
|
||||
<span class="footer-info"></span>
|
||||
<button type="button" class="btn btn-ghost" data-modal-cancel>Cancel</button>
|
||||
<button type="button" class="btn btn-primary my-plugin-save">Save</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
if (!modal) {
|
||||
return; // Browser does not support <dialog>
|
||||
}
|
||||
// Open it later, for example from a button click:
|
||||
// modal.showModal({trigger: button});
|
||||
});
|
||||
"""
|
||||
}
|
||||
EOF
|
||||
|
||||
"${PYTHON_CMD[@]}" -m datasette "$tmp_dir/demo.db" \
|
||||
--plugins-dir "$tmp_dir/plugins" --port "$PORT" &
|
||||
server_pid=$!
|
||||
|
||||
# Wait for the server to start responding
|
||||
for _ in $(seq 1 50); do
|
||||
if curl -s -o /dev/null "http://127.0.0.1:$PORT/"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
# Open the modal with animations disabled, blur the auto-focused Cancel
|
||||
# button so no focus ring appears, then take a retina viewport shot
|
||||
"${SHOT_SCRAPER_CMD[@]}" shot "http://127.0.0.1:$PORT/demo/plugins" \
|
||||
--javascript '
|
||||
new Promise((resolve) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent =
|
||||
"dialog, dialog::backdrop { animation: none !important; }";
|
||||
document.head.appendChild(style);
|
||||
const modal = document
|
||||
.getElementById("my-plugin-dialog")
|
||||
.closest("datasette-modal");
|
||||
modal.showModal();
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
setTimeout(resolve, 500);
|
||||
})' \
|
||||
--width 760 --height 460 --retina \
|
||||
--output "$tmp_dir/shot.png" --silent
|
||||
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
wait "$server_pid" 2>/dev/null || true
|
||||
server_pid=""
|
||||
|
||||
# Quantize to an 8-bit palette PNG to roughly halve the file size
|
||||
cat > "$tmp_dir/quantize.py" <<'EOF'
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open(sys.argv[1]).convert("RGB")
|
||||
img.quantize(
|
||||
colors=256,
|
||||
method=Image.Quantize.MEDIANCUT,
|
||||
dither=Image.Dither.FLOYDSTEINBERG,
|
||||
).save(sys.argv[2], optimize=True)
|
||||
EOF
|
||||
uv run --no-project --with pillow python \
|
||||
"$tmp_dir/quantize.py" "$tmp_dir/shot.png" "$output"
|
||||
|
||||
echo "Wrote $output"
|
||||
|
|
@ -49,9 +49,165 @@ The ``datasetteManager`` object
|
|||
``makeColumnField(context)``
|
||||
Calls the ``makeColumnField()`` hook on registered plugins, returning the first custom insert/edit field control that matches the provided field context. This is used internally by Datasette's row insert and edit dialogs.
|
||||
|
||||
``createModal(options)``
|
||||
Creates a :ref:`\<datasette-modal\> <javascript_plugins_datasette_modal>` element, appends it to the page and returns it. Returns ``null`` in browsers without ``<dialog>`` support.
|
||||
|
||||
``selectors`` - object
|
||||
An object providing named aliases to useful CSS selectors, :ref:`listed below <javascript_datasette_manager_selectors>`
|
||||
|
||||
.. _javascript_plugins_datasette_modal:
|
||||
|
||||
Modal dialogs: the datasette-modal element
|
||||
------------------------------------------
|
||||
|
||||
Datasette provides a ``<datasette-modal>`` Web Component that renders a modal dialog in the same visual style as Datasette's own dialogs - the create/alter table dialogs, the insert/edit/delete row dialogs, the column chooser, the mobile column actions sheet and the ``/`` jump menu are all built on it.
|
||||
|
||||
The element, and the ``DatasetteModal`` class exposed as ``window.DatasetteModal``, are a stable public API for plugins. Plugins that need a modal dialog should use this component rather than building their own, so their dialogs automatically match Datasette's styling, keyboard handling and accessibility behavior.
|
||||
|
||||
The component wraps a native ``<dialog>`` element and provides:
|
||||
|
||||
- Datasette's standard modal frame: sizing, rounded corners, backdrop, open/close animations and an optional header with a title and a "meta" chip
|
||||
- Close on backdrop click and on the ``Escape`` key
|
||||
- A ``busy`` property that blocks the user from dismissing the dialog while a save or delete is in flight
|
||||
- A ``closeGuard`` hook for "Discard unsaved changes?" style confirmation prompts
|
||||
- Focus restoration to the triggering element when the dialog closes
|
||||
- ``datasette-modal-open`` and ``datasette-modal-close`` events
|
||||
|
||||
Creating a modal
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The simplest way to create a modal from a plugin is ``datasetteManager.createModal()`` (or the equivalent ``window.DatasetteModal.create()``), which creates the element, appends it to ``document.body`` and returns it:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
document.addEventListener("datasette_init", function (event) {
|
||||
const manager = event.detail;
|
||||
const modal = manager.createModal({
|
||||
id: "my-plugin-dialog",
|
||||
className: "my-plugin-dialog",
|
||||
title: "My plugin",
|
||||
content: `
|
||||
<p style="padding: 16px 24px">Hello from a plugin!</p>
|
||||
<div class="modal-footer">
|
||||
<span class="footer-info"></span>
|
||||
<button type="button" class="btn btn-ghost" data-modal-cancel>Cancel</button>
|
||||
<button type="button" class="btn btn-primary my-plugin-save">Save</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
if (!modal) {
|
||||
return; // Browser does not support <dialog>
|
||||
}
|
||||
// Open it later, for example from a button click:
|
||||
// modal.showModal({trigger: button});
|
||||
});
|
||||
|
||||
The ``data-modal-cancel`` attribute on the Cancel button is a declarative shortcut: clicking any element inside the modal that carries this attribute calls ``modal.requestClose("cancel")``, so Cancel buttons need no JavaScript wiring. Like other dismissals it is blocked while the modal is ``busy`` and consults ``closeGuard``, both described below.
|
||||
|
||||
Calling ``modal.showModal()`` then displays the dialog:
|
||||
|
||||
.. Regenerate this screenshot with docs/generate-datasette-modal-example.sh
|
||||
|
||||
.. image:: datasette-modal-example.png
|
||||
:alt: A modal dialog titled "My plugin" containing the text "Hello from a plugin!" and Cancel and Save buttons, shown over a dimmed and blurred Datasette table page.
|
||||
:width: 550px
|
||||
|
||||
``createModal(options)`` / ``DatasetteModal.create(options)`` accepts:
|
||||
|
||||
``id`` - string, optional
|
||||
``id`` attribute for the inner ``<dialog>`` element.
|
||||
|
||||
``className`` - string, optional
|
||||
``class`` attribute for the inner ``<dialog>``, useful for scoping custom CSS.
|
||||
|
||||
``title`` - string, optional
|
||||
Text for the standard header title. If omitted no header is created and the modal content fills the whole dialog.
|
||||
|
||||
``meta`` - string, optional
|
||||
Text for the small "meta" chip shown on the right of the header, for example ``"3 of 12 selected"``.
|
||||
|
||||
``titleId`` - string, optional
|
||||
``id`` for the generated title element. Defaults to ``"<id>-title"``.
|
||||
|
||||
``labelledBy`` / ``describedBy`` - strings, optional
|
||||
Explicit ``aria-labelledby`` / ``aria-describedby`` values for the dialog. By default the dialog is labelled by the generated title element.
|
||||
|
||||
``content`` - string or DOM node, optional
|
||||
Content placed inside the dialog, after the header. By convention this ends with a ``<div class="modal-footer">`` containing an optional ``<span class="footer-info">`` and buttons using the ``btn`` classes shown above.
|
||||
|
||||
``parent`` - element, optional
|
||||
Element to append the modal to. Defaults to ``document.body``.
|
||||
|
||||
The element can also be used declaratively in a template - light DOM children become the dialog content:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<datasette-modal dialog-id="my-dialog" modal-title="My dialog">
|
||||
<p>Dialog content</p>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" data-modal-cancel>Cancel</button>
|
||||
<button type="button" class="btn btn-primary">OK</button>
|
||||
</div>
|
||||
</datasette-modal>
|
||||
|
||||
The ``dialog-id``, ``dialog-class``, ``modal-title``, ``modal-meta``, ``title-id``, ``labelled-by`` and ``described-by`` attributes correspond to the options above. ``modal-title`` and ``modal-meta`` can be updated at any time and the header will update to match.
|
||||
|
||||
Properties, methods and events
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``DatasetteModal.supported`` - boolean, static
|
||||
True if the browser supports everything the component needs. ``createModal()`` returns ``null`` when this is false.
|
||||
|
||||
``modal.dialog`` - ``HTMLDialogElement``
|
||||
The underlying native dialog element. Query this for elements inside the modal.
|
||||
|
||||
``modal.open`` - boolean
|
||||
True while the modal is open.
|
||||
|
||||
``modal.showModal(options)``
|
||||
Opens the modal. ``options.trigger`` is the element that focus should return to when the modal closes - it defaults to the element that was focused when ``showModal()`` was called.
|
||||
|
||||
``modal.close(options)``
|
||||
Closes the modal unconditionally, skipping ``busy`` and ``closeGuard``. Pass ``{restoreFocus: false}`` to leave focus where it is, for example when the page is about to navigate.
|
||||
|
||||
``modal.requestClose(reason)``
|
||||
Asks the modal to close on the user's behalf, respecting ``busy`` and ``closeGuard``. Returns true if the modal closed. Backdrop clicks and the ``Escape`` key call this internally with reasons ``"backdrop"`` and ``"escape"``, and clicking an element with a ``data-modal-cancel`` attribute calls it with ``"cancel"``. Cancel buttons can either carry that attribute or call this method directly.
|
||||
|
||||
``modal.busy`` - boolean
|
||||
While true, ``Escape``, backdrop clicks and ``requestClose()`` will not close the modal. Set this while an operation is in flight. A ``busy`` attribute is reflected on the element for CSS.
|
||||
|
||||
``modal.closeGuard`` - function or null
|
||||
If set, called with the reason string whenever the user tries to dismiss the modal. Return false to keep the modal open - for example after a ``confirm("Discard unsaved changes?")`` returns false. Not consulted by direct ``close()`` calls.
|
||||
|
||||
``modal.setTitle(text)`` / ``modal.setMeta(text)``
|
||||
Update the header title and meta chip. Setting the meta text to ``""`` hides the chip. ``modal.titleElement`` and ``modal.metaElement`` expose the underlying elements for richer markup.
|
||||
|
||||
The element dispatches two bubbling events:
|
||||
|
||||
``datasette-modal-open``
|
||||
Fired when the modal opens.
|
||||
|
||||
``datasette-modal-close``
|
||||
Fired when the modal closes, however that happened. Use this to reset dialog state.
|
||||
|
||||
Styling
|
||||
~~~~~~~
|
||||
|
||||
Modal content uses light DOM, so page-level CSS and plugin CSS can style it directly. The component provides the frame plus styles for these conventional class names inside it: ``.modal-header``, ``.modal-title``, ``.modal-meta``, ``.modal-footer``, ``.footer-info`` and the button classes ``.btn``, ``.btn-primary``, ``.btn-ghost`` and ``.btn-danger``.
|
||||
|
||||
Sizing can be customized with CSS custom properties on the dialog:
|
||||
|
||||
.. code-block:: css
|
||||
|
||||
dialog.my-plugin-dialog {
|
||||
--datasette-modal-width: min(700px, calc(100vw - 32px));
|
||||
--datasette-modal-max-height: min(600px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
The dialog frame also respects the page-wide theme properties ``--modal-border-radius``, ``--modal-shadow``, ``--modal-backdrop-bg``, ``--modal-backdrop-blur`` and ``--modal-animation-duration``.
|
||||
|
||||
``<datasette-modal>`` also works inside the shadow DOM of other Web Components - Datasette's own ``<column-chooser>`` and ``<navigation-search>`` components use it this way. The shared frame styles are automatically adopted into whichever document or shadow root the element is connected to.
|
||||
|
||||
.. _javascript_plugin_objects:
|
||||
|
||||
JavaScript plugin objects
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def find_free_port():
|
|||
return sock.getsockname()[1]
|
||||
|
||||
|
||||
def wait_for_server(process, url, timeout=10):
|
||||
def wait_for_server(process, url, timeout=45):
|
||||
deadline = time.monotonic() + timeout
|
||||
last_error = None
|
||||
while time.monotonic() < deadline:
|
||||
|
|
@ -890,3 +890,98 @@ def test_delete_row_flow_removes_row(page, datasette_server):
|
|||
page.locator(".row-mutation-status", has_text="Deleted row 1").wait_for()
|
||||
page.locator('tr[data-row="1"]').wait_for(state="detached")
|
||||
assert project_rows(datasette_server, id=1) == []
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_column_chooser_dialog(page, datasette_server):
|
||||
page.goto(f"{datasette_server}data/projects")
|
||||
page.locator('th[data-column="title"] svg.dropdown-menu-icon').click()
|
||||
page.get_by_role("link", name="Choose columns").click()
|
||||
|
||||
dialog = page.locator("column-chooser dialog")
|
||||
dialog.wait_for(state="visible")
|
||||
assert page.locator("column-chooser .modal-title").inner_text() == "Choose columns"
|
||||
assert "selected" in page.locator("column-chooser .modal-meta").inner_text()
|
||||
|
||||
notes_item = page.locator("column-chooser .drag-item", has_text="notes")
|
||||
notes_item.locator('input[type="checkbox"]').uncheck()
|
||||
page.locator("column-chooser #applyBtn").click()
|
||||
|
||||
page.wait_for_url(lambda url: "_col=" in url)
|
||||
assert "_col=title" in page.url
|
||||
assert "_col=notes" not in page.url
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_column_chooser_dialog_escape_discards_changes(page, datasette_server):
|
||||
page.goto(f"{datasette_server}data/projects")
|
||||
page.locator('th[data-column="title"] svg.dropdown-menu-icon').click()
|
||||
page.get_by_role("link", name="Choose columns").click()
|
||||
|
||||
dialog = page.locator("column-chooser dialog")
|
||||
dialog.wait_for(state="visible")
|
||||
notes_item = page.locator("column-chooser .drag-item", has_text="notes")
|
||||
notes_item.locator('input[type="checkbox"]').uncheck()
|
||||
page.keyboard.press("Escape")
|
||||
dialog.wait_for(state="hidden")
|
||||
|
||||
# Re-opening should show the original selection again
|
||||
page.locator('th[data-column="title"] svg.dropdown-menu-icon').click()
|
||||
page.get_by_role("link", name="Choose columns").click()
|
||||
dialog.wait_for(state="visible")
|
||||
notes_item = page.locator("column-chooser .drag-item", has_text="notes")
|
||||
assert notes_item.locator('input[type="checkbox"]').is_checked()
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_mobile_column_actions_dialog(page, datasette_server):
|
||||
# Deferred import so collecting this module works without playwright
|
||||
from playwright.sync_api import expect
|
||||
|
||||
page.set_viewport_size({"width": 400, "height": 800})
|
||||
page.goto(f"{datasette_server}data/projects")
|
||||
trigger = page.locator("button.column-actions-mobile")
|
||||
trigger.click()
|
||||
|
||||
dialog = page.locator("#mobile-column-actions-dialog")
|
||||
dialog.wait_for(state="visible")
|
||||
assert dialog.locator(".modal-title").inner_text() == "Column actions"
|
||||
assert "columns" in dialog.locator(".modal-meta").inner_text()
|
||||
assert trigger.get_attribute("aria-expanded") == "true"
|
||||
|
||||
section = dialog.locator(".mobile-column-section", has_text="title").first
|
||||
section.locator(".col-header").click()
|
||||
section.locator(".col-actions a", has_text="Sort ascending").wait_for(
|
||||
state="visible"
|
||||
)
|
||||
|
||||
dialog.locator(".mobile-column-actions-done").click()
|
||||
dialog.wait_for(state="hidden")
|
||||
# aria-expanded resets from the dialog close event, which fires in a
|
||||
# queued task after the dialog is already hidden - so poll for it
|
||||
expect(trigger).to_have_attribute("aria-expanded", "false")
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_set_column_type_dialog(page, datasette_server):
|
||||
page.goto(f"{datasette_server}data/projects")
|
||||
page.locator('th[data-column="title"] svg.dropdown-menu-icon').click()
|
||||
page.get_by_role("link", name="Set custom type").click()
|
||||
|
||||
dialog = page.locator("#set-column-type-dialog")
|
||||
dialog.wait_for(state="visible")
|
||||
assert dialog.locator(".modal-title").inner_text() == "Set custom type"
|
||||
assert "TEXT" in dialog.locator(".modal-meta").inner_text()
|
||||
option_names = dialog.locator(".set-column-type-option-name").all_inner_texts()
|
||||
assert "asset" in option_names
|
||||
|
||||
# The declarative data-modal-cancel attribute closes the dialog
|
||||
dialog.locator("[data-modal-cancel]").click()
|
||||
dialog.wait_for(state="hidden")
|
||||
|
||||
# Escape also closes the dialog via the shared datasette-modal component
|
||||
page.locator('th[data-column="title"] svg.dropdown-menu-icon').click()
|
||||
page.get_by_role("link", name="Set custom type").click()
|
||||
dialog.wait_for(state="visible")
|
||||
page.keyboard.press("Escape")
|
||||
dialog.wait_for(state="hidden")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue