var ROW_DELETE_DIALOG_ID = "row-delete-dialog"; var rowDeleteDialogState = null; var ROW_EDIT_DIALOG_ID = "row-edit-dialog"; var rowEditDialogState = null; var TABLE_CREATE_DIALOG_ID = "table-create-dialog"; var tableCreateDialogState = null; var TABLE_ALTER_DIALOG_ID = "table-alter-dialog"; var tableAlterDialogState = null; function ensureRowMutationStatus(manager) { var status = document.querySelector(".row-mutation-status"); if (status) { return status; } status = document.createElement("p"); status.className = "row-mutation-status"; status.hidden = true; status.setAttribute("role", "status"); status.setAttribute("aria-live", "polite"); status.setAttribute("tabindex", "-1"); var tableWrapper = document.querySelector(manager.selectors.tableWrapper); if (tableWrapper && tableWrapper.parentNode) { tableWrapper.parentNode.insertBefore(status, tableWrapper); } else { document.body.appendChild(status); } return status; } function showRowMutationStatus(manager, message, isError) { var status = ensureRowMutationStatus(manager); status.hidden = false; status.classList.toggle("row-mutation-status-error", !!isError); status.textContent = message; return status; } function hideRowMutationStatus() { var status = document.querySelector(".row-mutation-status"); if (!status) { return; } status.hidden = true; status.classList.remove("row-mutation-status-error"); status.textContent = ""; } function databaseCreateTableData() { return ( window._datasetteDatabaseData && window._datasetteDatabaseData.createTable ); } function tableCreateColumnTypes() { var data = databaseCreateTableData() || {}; return data.columnTypes && data.columnTypes.length ? data.columnTypes : ["text", "integer", "float", "blob"]; } var SQLITE_COLUMN_TYPE_LABELS = { float: "floating point number", real: "floating point number", blob: "blob - binary data", }; function sqliteColumnTypeLabel(type) { return SQLITE_COLUMN_TYPE_LABELS[type] || type; } function populateSqliteColumnTypeSelect(select, type, options) { options.forEach(function (option) { var optionElement = document.createElement("option"); optionElement.value = option; optionElement.textContent = sqliteColumnTypeLabel(option); select.appendChild(optionElement); }); select.value = options.indexOf(type) === -1 ? options[0] : type; } function updateSelectPlaceholder(select, placeholderClass) { select.classList.toggle(placeholderClass, !select.value); } function createCustomColumnTypeSelect(options, className, placeholderClass) { var select = document.createElement("select"); select.className = className; select.setAttribute("aria-label", "Custom column type"); var blankOption = document.createElement("option"); blankOption.value = ""; blankOption.textContent = "- custom type -"; select.appendChild(blankOption); options.forEach(function (option) { var optionElement = document.createElement("option"); optionElement.value = option.name; optionElement.textContent = option.description ? option.description + " (" + option.name + ")" : option.name; select.appendChild(optionElement); }); updateSelectPlaceholder(select, placeholderClass); return select; } var COLUMN_MOVE_ICONS = { top: '', up: '', down: '', bottom: '', remove: '', }; function createSchemaDialogIconButton(prefix, modifier, ariaLabel, title, svg) { var button = document.createElement("button"); button.type = "button"; button.className = prefix + "-icon-button " + prefix + "-" + modifier; button.setAttribute("aria-label", ariaLabel); button.title = title; button.dataset.defaultTitle = title; button.innerHTML = svg; return button; } function createSchemaDialogMoveControls(prefix) { var moveControls = document.createElement("div"); moveControls.className = prefix + "-move-controls"; var moveTopButton = createSchemaDialogIconButton( prefix, "move-top", "Move column to top", "Move column to top", COLUMN_MOVE_ICONS.top, ); var moveUpButton = createSchemaDialogIconButton( prefix, "move-up", "Move column up", "Move column up", COLUMN_MOVE_ICONS.up, ); var moveDownButton = createSchemaDialogIconButton( prefix, "move-down", "Move column down", "Move column down", COLUMN_MOVE_ICONS.down, ); var moveBottomButton = createSchemaDialogIconButton( prefix, "move-bottom", "Move column to bottom", "Move column to bottom", COLUMN_MOVE_ICONS.bottom, ); moveControls.appendChild(moveTopButton); moveControls.appendChild(moveUpButton); moveControls.appendChild(moveDownButton); moveControls.appendChild(moveBottomButton); return { controls: moveControls, topButton: moveTopButton, upButton: moveUpButton, downButton: moveDownButton, bottomButton: moveBottomButton, }; } function createSchemaDialogMoreOptionsButton(prefix, details) { var expandButton = document.createElement("button"); expandButton.type = "button"; expandButton.className = prefix + "-more-options"; expandButton.setAttribute("aria-label", "Toggle column settings"); expandButton.setAttribute("aria-controls", details.id); expandButton.setAttribute("aria-expanded", details.hidden ? "false" : "true"); updateSchemaDialogMoreOptionsButton(expandButton); return expandButton; } function updateSchemaDialogMoreOptionsButton(button) { var isExpanded = button.getAttribute("aria-expanded") === "true"; button.textContent = isExpanded ? "v Hide options" : "> Advanced options"; button.title = isExpanded ? "Hide column settings" : "Show column settings"; } function toggleSchemaDialogMoreOptions(button, details) { var isExpanded = button.getAttribute("aria-expanded") === "true"; details.hidden = isExpanded; button.setAttribute("aria-expanded", isExpanded ? "false" : "true"); updateSchemaDialogMoreOptionsButton(button); } function schemaDialogRows(state, prefix) { return Array.prototype.slice.call( state.columnList.querySelectorAll("." + prefix + "-column-row"), ); } function schemaDialogRowIsPrimaryKey(row, prefix) { var input = row && row.querySelector("." + prefix + "-primary-key-input"); return !!(input && input.checked); } function schemaDialogFirstNonPrimaryRow(state, prefix) { var rows = schemaDialogRows(state, prefix); for (var i = 0; i < rows.length; i += 1) { if (!schemaDialogRowIsPrimaryKey(rows[i], prefix)) { return rows[i]; } } return null; } function updateSchemaDialogMoveButtons(state, prefix) { if (!state || !state.columnList) { return; } var firstNonPrimary = schemaDialogFirstNonPrimaryRow(state, prefix); var rows = schemaDialogRows(state, prefix); var hasPrimaryKeys = rows.some(function (row) { return schemaDialogRowIsPrimaryKey(row, prefix); }); var primaryKeyMoveTitle = "Primary key columns are always listed first"; rows.forEach(function (row) { var isPrimaryKey = schemaDialogRowIsPrimaryKey(row, prefix); var previous = row.previousElementSibling; var next = row.nextElementSibling; row .querySelectorAll("." + prefix + "-move-controls button") .forEach(function (button) { button.title = button.dataset.defaultTitle || button.title; button.disabled = state.isSaving || isPrimaryKey; if (isPrimaryKey) { button.title = primaryKeyMoveTitle; } }); if (!isPrimaryKey) { var topButton = row.querySelector("." + prefix + "-move-top"); var upButton = row.querySelector("." + prefix + "-move-up"); var downButton = row.querySelector("." + prefix + "-move-down"); var bottomButton = row.querySelector("." + prefix + "-move-bottom"); topButton.disabled = state.isSaving || !firstNonPrimary || row === firstNonPrimary; upButton.disabled = state.isSaving || !previous || schemaDialogRowIsPrimaryKey(previous, prefix); downButton.disabled = state.isSaving || !next; bottomButton.disabled = state.isSaving || !next; if (hasPrimaryKeys && row === firstNonPrimary) { topButton.title = primaryKeyMoveTitle; upButton.title = primaryKeyMoveTitle; } } }); } function normalizeSchemaDialogPrimaryKeyRows(state, prefix) { var rows = schemaDialogRows(state, prefix); rows .filter(function (row) { return schemaDialogRowIsPrimaryKey(row, prefix); }) .concat( rows.filter(function (row) { return !schemaDialogRowIsPrimaryKey(row, prefix); }), ) .forEach(function (row) { state.columnList.appendChild(row); }); } function tableCreateCustomColumnTypes() { var data = databaseCreateTableData() || {}; return data.customColumnTypes || []; } function tableCreateCustomColumnType(name) { var options = tableCreateCustomColumnTypes(); for (var i = 0; i < options.length; i += 1) { if (options[i].name === name) { return options[i]; } } return null; } function tableCreateCustomTypeAppliesToSqliteType(option, sqliteType) { return ( option && option.sqliteTypes && option.sqliteTypes.indexOf(sqliteType) !== -1 ); } function tableCreateDialogRows(state) { return schemaDialogRows(state, "table-create"); } function tableCreateRowIsPrimaryKey(row) { return schemaDialogRowIsPrimaryKey(row, "table-create"); } function tableCreateFirstNonPrimaryRow(state) { return schemaDialogFirstNonPrimaryRow(state, "table-create"); } function updateTableCreateMoveButtons(state) { updateSchemaDialogMoveButtons(state, "table-create"); } function tableCreateTypeAffinity(type) { if (type === "float") { return "real"; } return type; } function foreignKeyTypesCompatible(sourceAffinity, targetAffinity) { if (sourceAffinity === targetAffinity) { return true; } var numericAffinities = ["integer", "real", "numeric"]; if (sourceAffinity === "numeric") { return numericAffinities.indexOf(targetAffinity) !== -1; } if (targetAffinity === "numeric") { return numericAffinities.indexOf(sourceAffinity) !== -1; } return false; } function tableCreateForeignKeyTargetKey(target) { return target.fk_table + "\u001f" + target.fk_column; } function tableCreateForeignKeyTargetLabel(target) { return ( target.fk_table + "." + target.fk_column + " (" + sqliteColumnTypeLabel(target.type) + ")" ); } function tableCreateForeignKeyTargetsUrl() { var data = databaseCreateTableData() || {}; if (data.foreignKeyTargetsPath) { return data.foreignKeyTargetsPath; } if (!data.path) { return null; } return data.path.replace(/\/-\/create$/, "/-/foreign-key-targets"); } function populateTableCreateForeignKeySelect(select, state, sourceType) { var previousKey = select.value || select.dataset.selectedKey || ""; select.textContent = ""; var blankOption = document.createElement("option"); blankOption.value = ""; blankOption.textContent = "- no foreign key -"; select.appendChild(blankOption); if (state.foreignKeyTargetsLoading) { var loadingOption = document.createElement("option"); loadingOption.value = ""; loadingOption.disabled = true; loadingOption.textContent = "Loading foreign keys..."; select.appendChild(loadingOption); } else if (state.foreignKeyTargetsError) { var errorOption = document.createElement("option"); errorOption.value = ""; errorOption.disabled = true; errorOption.textContent = "Could not load foreign keys"; select.appendChild(errorOption); } else { var sourceAffinity = tableCreateTypeAffinity(sourceType); (state.foreignKeyTargets || []).forEach(function (target) { if (!foreignKeyTypesCompatible(sourceAffinity, target.type)) { return; } var optionElement = document.createElement("option"); optionElement.value = tableCreateForeignKeyTargetKey(target); optionElement.dataset.fkTable = target.fk_table; optionElement.dataset.fkColumn = target.fk_column; optionElement.textContent = tableCreateForeignKeyTargetLabel(target); select.appendChild(optionElement); }); } select.value = previousKey; if (select.value !== previousKey) { select.value = ""; } select.dataset.selectedKey = select.value; select.disabled = state.isSaving || select.options.length <= 1; updateSelectPlaceholder(select, "table-create-input-placeholder"); } function syncTableCreateForeignKeyOptions(row, state) { var typeSelect = row.querySelector(".table-create-column-type"); var foreignKeySelect = row.querySelector(".table-create-foreign-key-target"); if (!typeSelect || !foreignKeySelect) { return; } populateTableCreateForeignKeySelect( foreignKeySelect, state, typeSelect.value, ); } function syncTableCreateCustomTypeAndForeignKey(row, state, isFirstColumn) { var customTypeSelect = row.querySelector(".table-create-custom-column-type"); var foreignKeySelect = row.querySelector(".table-create-foreign-key-target"); if (!foreignKeySelect) { return; } var hasCustomType = customTypeSelect && !!customTypeSelect.value; var hasForeignKey = !!foreignKeySelect.value; if (customTypeSelect && hasForeignKey) { customTypeSelect.value = ""; updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); hasCustomType = false; } if (isFirstColumn || hasCustomType) { foreignKeySelect.value = ""; foreignKeySelect.dataset.selectedKey = ""; updateTableCreateCustomColumnTypePlaceholder(foreignKeySelect); hasForeignKey = false; } if (customTypeSelect) { customTypeSelect.disabled = state.isSaving; } foreignKeySelect.disabled = state.isSaving || isFirstColumn || foreignKeySelect.options.length <= 1; } function refreshTableCreateForeignKeyControls(state) { tableCreateDialogRows(state).forEach(function (row, index) { if (index > 0) { syncTableCreateForeignKeyOptions(row, state); } syncTableCreateCustomTypeAndForeignKey(row, state, index === 0); }); } function updateTableCreateColumnRules(state) { tableCreateDialogRows(state).forEach(function (row, index) { var isFirstColumn = index === 0; var pkLabel = row.querySelector(".table-create-primary-key"); var pkInput = row.querySelector(".table-create-primary-key-input"); var foreignKeyField = row.querySelector(".table-create-foreign-key-field"); var foreignKeySelect = row.querySelector(".table-create-foreign-key-target"); if (pkLabel && pkInput) { pkLabel.hidden = !isFirstColumn; if (!isFirstColumn) { pkInput.checked = false; } } if (foreignKeyField && foreignKeySelect) { foreignKeyField.hidden = isFirstColumn; if (isFirstColumn) { foreignKeySelect.value = ""; foreignKeySelect.dataset.selectedKey = ""; foreignKeySelect.disabled = true; updateTableCreateCustomColumnTypePlaceholder(foreignKeySelect); } else { syncTableCreateForeignKeyOptions(row, state); } syncTableCreateCustomTypeAndForeignKey(row, state, isFirstColumn); } }); updateTableCreateMoveButtons(state); } async function loadTableCreateForeignKeyTargets(state) { var url = tableCreateForeignKeyTargetsUrl(); if (!url || !window.fetch) { state.foreignKeyTargets = []; state.foreignKeyTargetsLoading = false; refreshTableCreateForeignKeyControls(state); return; } state.foreignKeyTargets = []; state.foreignKeyTargetsError = null; state.foreignKeyTargetsLoading = true; refreshTableCreateForeignKeyControls(state); try { var response = await fetch(url, { headers: { Accept: "application/json", }, }); var data = await response.json(); if (!response.ok || data.ok === false) { throw rowMutationRequestError(response, data); } state.foreignKeyTargets = data.targets || []; } catch (error) { state.foreignKeyTargets = []; state.foreignKeyTargetsError = error; } finally { state.foreignKeyTargetsLoading = false; refreshTableCreateForeignKeyControls(state); } } function tableCreateDialogSignature(state) { if (!state || !state.form) { return ""; } return JSON.stringify({ table: state.tableName.value, columns: tableCreateDialogRows(state).map(function (row) { return { name: row.querySelector(".table-create-column-name").value, type: row.querySelector(".table-create-column-type").value, customType: ( row.querySelector(".table-create-custom-column-type") || { value: "", } ).value || "", pk: row.querySelector(".table-create-primary-key-input").checked, foreignKey: ( row.querySelector(".table-create-foreign-key-target") || { value: "", } ).value || "", }; }), }); } function tableCreateDialogHasChanges(state) { return ( !!state && !state.isSaving && tableCreateDialogSignature(state) !== state.initialSignature ); } function clearTableCreateDialogError(state) { state.error.hidden = true; state.error.textContent = ""; state.dialog.removeAttribute("aria-describedby"); } function showTableCreateDialogError(state, message) { state.error.hidden = false; state.error.textContent = message; state.dialog.setAttribute("aria-describedby", "table-create-error"); state.error.focus(); } function setTableCreateDialogSaving(state, isSaving) { state.isSaving = isSaving; state.cancelButton.disabled = isSaving; state.saveButton.disabled = isSaving; state.addColumnButton.disabled = isSaving; state.saveButton.textContent = isSaving ? "Creating..." : "Create table"; state.columnList .querySelectorAll("input, select, button") .forEach(function (control) { control.disabled = isSaving; }); if (!isSaving) { updateTableCreateColumnRules(state); } updateTableCreateMoveButtons(state); } function tableCreateSelectTypeValue(select, type) { var options = tableCreateColumnTypes(); populateSqliteColumnTypeSelect(select, type, options); } function updateTableCreateCustomColumnTypePlaceholder(select) { updateSelectPlaceholder(select, "table-create-input-placeholder"); } function createTableCustomColumnTypeSelect() { var options = tableCreateCustomColumnTypes(); return createCustomColumnTypeSelect( options, "table-create-input table-create-custom-column-type", "table-create-input-placeholder", ); } function syncTableCreateCustomTypeForSqliteType(row) { var typeSelect = row.querySelector(".table-create-column-type"); var customTypeSelect = row.querySelector(".table-create-custom-column-type"); if (!typeSelect || !customTypeSelect || !customTypeSelect.value) { return; } var option = tableCreateCustomColumnType(customTypeSelect.value); if (!tableCreateCustomTypeAppliesToSqliteType(option, typeSelect.value)) { customTypeSelect.value = ""; updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); } } function createTableColumnRow(state, column) { var index = state.nextColumnIndex; state.nextColumnIndex += 1; var row = document.createElement("div"); row.className = "table-create-column-row"; var main = document.createElement("div"); main.className = "table-create-column-main"; var details = document.createElement("div"); details.className = "table-create-column-details"; details.id = "table-create-column-details-" + index; details.hidden = !(column && column.expanded); var expandButton = createSchemaDialogMoreOptionsButton( "table-create", details, ); var nameId = "table-create-column-name-" + index; var nameLabel = document.createElement("label"); nameLabel.className = "table-create-column-label"; nameLabel.setAttribute("for", nameId); nameLabel.textContent = "Column"; var nameInput = document.createElement("input"); nameInput.id = nameId; nameInput.className = "table-create-input table-create-column-name"; nameInput.type = "text"; nameInput.required = true; nameInput.autocomplete = "off"; nameInput.value = column && column.name ? column.name : ""; var typeSelect = document.createElement("select"); typeSelect.className = "table-create-input table-create-column-type"; typeSelect.setAttribute("aria-label", "Column type"); tableCreateSelectTypeValue(typeSelect, column && column.type); var customTypeSelect = null; var customTypeField = null; if (tableCreateCustomColumnTypes().length) { var customTypeId = "table-create-column-custom-type-" + index; customTypeField = document.createElement("div"); customTypeField.className = "table-create-detail-field table-create-custom-type-field"; var customTypeLabel = document.createElement("label"); customTypeLabel.className = "table-create-detail-label"; customTypeLabel.setAttribute("for", customTypeId); customTypeLabel.textContent = "Custom type"; var customTypeHelpId = "table-create-column-custom-type-help-" + index; var customTypeHelp = document.createElement("p"); customTypeHelp.id = customTypeHelpId; customTypeHelp.className = "table-create-detail-help"; customTypeHelp.textContent = "Controls how Datasette displays and edits this column"; customTypeSelect = createTableCustomColumnTypeSelect(); customTypeSelect.id = customTypeId; customTypeSelect.setAttribute("aria-describedby", customTypeHelpId); customTypeSelect.value = column && column.customType ? column.customType : ""; updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); customTypeField.appendChild(customTypeLabel); customTypeField.appendChild(customTypeHelp); customTypeField.appendChild(customTypeSelect); } var pkLabel = document.createElement("label"); pkLabel.className = "table-create-detail-check table-create-primary-key"; var pkInput = document.createElement("input"); pkInput.type = "checkbox"; pkInput.className = "table-create-primary-key-input"; pkInput.checked = !!(column && column.primaryKey); var pkText = document.createElement("span"); var pkStrong = document.createElement("strong"); pkStrong.textContent = "Primary key"; pkText.appendChild(pkStrong); pkText.appendChild( document.createTextNode(" This ID uniquely identifies the record"), ); pkLabel.appendChild(pkInput); pkLabel.appendChild(pkText); var foreignKeyId = "table-create-column-foreign-key-" + index; var foreignKeyHelpId = "table-create-column-foreign-key-help-" + index; var foreignKeyField = document.createElement("div"); foreignKeyField.className = "table-create-detail-field table-create-foreign-key-field"; var foreignKeyLabel = document.createElement("label"); foreignKeyLabel.className = "table-create-detail-label"; foreignKeyLabel.setAttribute("for", foreignKeyId); foreignKeyLabel.textContent = "Foreign key"; var foreignKeyHelp = document.createElement("p"); foreignKeyHelp.id = foreignKeyHelpId; foreignKeyHelp.className = "table-create-detail-help"; foreignKeyHelp.textContent = "Link this column to another table."; var foreignKeySelect = document.createElement("select"); foreignKeySelect.id = foreignKeyId; foreignKeySelect.className = "table-create-input table-create-foreign-key-target"; foreignKeySelect.setAttribute("aria-label", "Foreign key target"); foreignKeySelect.setAttribute("aria-describedby", foreignKeyHelpId); foreignKeyField.appendChild(foreignKeyLabel); foreignKeyField.appendChild(foreignKeyHelp); foreignKeyField.appendChild(foreignKeySelect); var moveControls = createSchemaDialogMoveControls("table-create"); var removeButton = createSchemaDialogIconButton( "table-create", "remove-column", "Remove column", "Remove column", COLUMN_MOVE_ICONS.remove, ); main.appendChild(nameLabel); main.appendChild(nameInput); main.appendChild(typeSelect); main.appendChild(moveControls.controls); main.appendChild(removeButton); main.appendChild(expandButton); if (customTypeField) { details.appendChild(customTypeField); } details.appendChild(foreignKeyField); details.appendChild(pkLabel); row.appendChild(main); row.appendChild(details); removeButton.addEventListener("click", function () { if (state.isSaving) { return; } row.remove(); clearTableCreateDialogError(state); updateTableCreateColumnRules(state); var nextInput = state.columnList.querySelector(".table-create-column-name"); if (nextInput) { nextInput.focus(); } else { state.addColumnButton.focus(); } }); nameInput.addEventListener("input", function () { clearTableCreateDialogError(state); }); typeSelect.addEventListener("change", function () { clearTableCreateDialogError(state); syncTableCreateCustomTypeForSqliteType(row); syncTableCreateForeignKeyOptions(row, state); syncTableCreateCustomTypeAndForeignKey( row, state, row === state.columnList.firstElementChild, ); }); if (customTypeSelect) { customTypeSelect.addEventListener("change", function () { clearTableCreateDialogError(state); updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); if (customTypeSelect.value) { foreignKeySelect.value = ""; foreignKeySelect.dataset.selectedKey = ""; } var option = tableCreateCustomColumnType(customTypeSelect.value); if ( option && option.fixedSqliteType && tableCreateColumnTypes().indexOf(option.fixedSqliteType) !== -1 ) { typeSelect.value = option.fixedSqliteType; syncTableCreateForeignKeyOptions(row, state); } syncTableCreateCustomTypeAndForeignKey( row, state, row === state.columnList.firstElementChild, ); }); } pkInput.addEventListener("change", function () { clearTableCreateDialogError(state); updateTableCreateColumnRules(state); }); foreignKeySelect.addEventListener("change", function () { foreignKeySelect.dataset.selectedKey = foreignKeySelect.value; clearTableCreateDialogError(state); updateTableCreateCustomColumnTypePlaceholder(foreignKeySelect); if (customTypeSelect && foreignKeySelect.value) { customTypeSelect.value = ""; updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); } syncTableCreateCustomTypeAndForeignKey( row, state, row === state.columnList.firstElementChild, ); }); expandButton.addEventListener("click", function () { toggleSchemaDialogMoreOptions(expandButton, details); }); moveControls.topButton.addEventListener("click", function () { var first = tableCreateFirstNonPrimaryRow(state); if ( state.isSaving || tableCreateRowIsPrimaryKey(row) || !first || first === row ) { return; } state.columnList.insertBefore(row, first); clearTableCreateDialogError(state); updateTableCreateColumnRules(state); row.querySelector(".table-create-column-name").focus(); }); moveControls.upButton.addEventListener("click", function () { var previous = row.previousElementSibling; if ( state.isSaving || tableCreateRowIsPrimaryKey(row) || !previous || tableCreateRowIsPrimaryKey(previous) ) { return; } state.columnList.insertBefore(row, previous); clearTableCreateDialogError(state); updateTableCreateColumnRules(state); row.querySelector(".table-create-column-name").focus(); }); moveControls.downButton.addEventListener("click", function () { var next = row.nextElementSibling; if (state.isSaving || tableCreateRowIsPrimaryKey(row) || !next) { return; } state.columnList.insertBefore(next, row); clearTableCreateDialogError(state); updateTableCreateColumnRules(state); row.querySelector(".table-create-column-name").focus(); }); moveControls.bottomButton.addEventListener("click", function () { var last = state.columnList.lastElementChild; if ( state.isSaving || tableCreateRowIsPrimaryKey(row) || !last || last === row ) { return; } state.columnList.appendChild(row); clearTableCreateDialogError(state); updateTableCreateColumnRules(state); row.querySelector(".table-create-column-name").focus(); }); return row; } function addTableCreateColumn(state, column) { var row = createTableColumnRow(state, column || { type: "text" }); state.columnList.appendChild(row); updateTableCreateColumnRules(state); return row; } function resetTableCreateDialog(state) { state.nextColumnIndex = 0; state.tableName.value = ""; state.columnList.textContent = ""; addTableCreateColumn(state, { name: "id", type: "integer", primaryKey: true, }); addTableCreateColumn(state, { name: "", type: "text", primaryKey: false, }); updateTableCreateColumnRules(state); state.initialSignature = tableCreateDialogSignature(state); } function collectTableCreatePayload(state) { var payload = { table: state.tableName.value.trim(), columns: [], }; var primaryKeys = []; tableCreateDialogRows(state).forEach(function (row, index) { var name = row.querySelector(".table-create-column-name").value.trim(); var type = row.querySelector(".table-create-column-type").value; var column = { name: name, type: type }; var foreignKeySelect = row.querySelector(".table-create-foreign-key-target"); var foreignKeyOption = foreignKeySelect && foreignKeySelect.selectedOptions ? foreignKeySelect.selectedOptions[0] : null; if ( index > 0 && foreignKeyOption && foreignKeyOption.dataset.fkTable && foreignKeyOption.dataset.fkColumn ) { column.fk_table = foreignKeyOption.dataset.fkTable; column.fk_column = foreignKeyOption.dataset.fkColumn; } payload.columns.push(column); if ( index === 0 && row.querySelector(".table-create-primary-key-input").checked ) { primaryKeys.push(name); } }); if (primaryKeys.length === 1) { payload.pk = primaryKeys[0]; } else if (primaryKeys.length > 1) { payload.pks = primaryKeys; } return payload; } function collectTableCreateColumnTypeAssignments(state) { var assignments = []; tableCreateDialogRows(state).forEach(function (row) { var customTypeSelect = row.querySelector( ".table-create-custom-column-type", ); if (!customTypeSelect || !customTypeSelect.value) { return; } assignments.push({ column: row.querySelector(".table-create-column-name").value.trim(), columnType: customTypeSelect.value, sqliteType: row.querySelector(".table-create-column-type").value, }); }); return assignments; } function validateTableCreatePayload(payload) { if (!payload.table) { return "Table name is required."; } if (payload.table.indexOf("\n") !== -1) { return "Table name cannot contain newlines."; } if (/^sqlite_/i.test(payload.table)) { return "Table name cannot start with sqlite_."; } if (!payload.columns.length) { return "At least one column is required."; } var seen = {}; var supportedTypes = tableCreateColumnTypes(); for (var i = 0; i < payload.columns.length; i += 1) { var column = payload.columns[i]; if (!column.name) { return "Column name is required."; } if (column.name.indexOf("\n") !== -1) { return "Column names cannot contain newlines."; } var columnKey = column.name.toLowerCase(); if (seen[columnKey]) { return "Duplicate column name: " + column.name; } seen[columnKey] = true; if (supportedTypes.indexOf(column.type) === -1) { return "Unsupported column type: " + column.type; } } return null; } function validateTableCreateColumnTypeAssignments(assignments) { for (var i = 0; i < assignments.length; i += 1) { var assignment = assignments[i]; var option = tableCreateCustomColumnType(assignment.columnType); if (!option) { return "Unknown custom column type: " + assignment.columnType; } if ( !tableCreateCustomTypeAppliesToSqliteType(option, assignment.sqliteType) ) { return ( "Custom type " + assignment.columnType + " cannot be used with SQLite type " + assignment.sqliteType + "." ); } } return null; } function fallbackTableUrl(tableName) { var data = databaseCreateTableData() || {}; if (!data.path) { return null; } return data.path.replace(/\/-\/create$/, "/" + encodeURIComponent(tableName)); } function tableCreateSetColumnTypeUrl(responseData, payload) { var tableUrl = responseData.table_url || fallbackTableUrl(responseData.table || payload.table); if (!tableUrl) { return null; } var url = new URL(tableUrl, location.href); url.hash = ""; url.search = ""; url.pathname = url.pathname.replace(/\/$/, "") + "/-/set-column-type"; return url.toString(); } async function assignTableCreateColumnTypes( responseData, payload, assignments, ) { if (!assignments.length) { return; } var url = tableCreateSetColumnTypeUrl(responseData, payload); if (!url) { throw new Error("Could not find the set column type URL."); } for (var i = 0; i < assignments.length; i += 1) { var assignment = assignments[i]; var response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ column: assignment.column, column_type: { type: assignment.columnType, }, }), }); var data = null; try { data = await response.json(); } catch (_error) { data = null; } if (!response.ok || (data && data.ok === false)) { var error = rowMutationRequestError(response, data); throw new Error( "Created table, but could not set custom type for " + assignment.column + ": " + error.message, ); } } } async function saveTableCreateDialog(state) { if (state.isSaving) { return; } var data = databaseCreateTableData(); if (!data || !data.path) { showTableCreateDialogError(state, "Could not find the create table URL."); return; } clearTableCreateDialogError(state); var payload = collectTableCreatePayload(state); var columnTypeAssignments = collectTableCreateColumnTypeAssignments(state); var validationError = validateTableCreatePayload(payload); if (validationError) { showTableCreateDialogError(state, validationError); return; } var columnTypeValidationError = validateTableCreateColumnTypeAssignments( columnTypeAssignments, ); if (columnTypeValidationError) { showTableCreateDialogError(state, columnTypeValidationError); return; } setTableCreateDialogSaving(state, true); try { var response = await fetch(data.path, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(payload), }); var responseData = null; try { responseData = await response.json(); } catch (_error) { responseData = null; } if (!response.ok || (responseData && responseData.ok === false)) { throw rowMutationRequestError(response, responseData); } await assignTableCreateColumnTypes( responseData, payload, columnTypeAssignments, ); var tableUrl = responseData.table_url || fallbackTableUrl(responseData.table || payload.table); state.shouldRestoreFocus = false; state.dialog.close(); if (tableUrl) { location.href = tableUrl; } else { location.reload(); } } catch (error) { setTableCreateDialogSaving(state, false); showTableCreateDialogError( state, error.message || "Could not create table", ); } } function confirmDiscardTableCreateChanges(state) { if (!tableCreateDialogHasChanges(state)) { return true; } return window.confirm("Discard this new table?"); } function closeTableCreateDialogIfConfirmed(state) { if (!state || state.isSaving) { return false; } if (!confirmDiscardTableCreateChanges(state)) { return false; } state.shouldRestoreFocus = true; state.dialog.close(); return true; } function ensureTableCreateDialog(manager) { if (tableCreateDialogState) { return tableCreateDialogState; } if (!window.HTMLDialogElement) { 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 = `
`; document.body.appendChild(dialog); tableCreateDialogState = { 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"), columnList: dialog.querySelector(".table-create-column-list"), 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, foreignKeyTargets: [], foreignKeyTargetsError: null, foreignKeyTargetsLoading: false, manager: manager, }; tableCreateDialogState.form.addEventListener("submit", function (ev) { ev.preventDefault(); saveTableCreateDialog(tableCreateDialogState); }); tableCreateDialogState.addColumnButton.addEventListener("click", function () { if (tableCreateDialogState.isSaving) { return; } var row = addTableCreateColumn(tableCreateDialogState, { type: "text" }); clearTableCreateDialogError(tableCreateDialogState); 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); } }); 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 () { var state = tableCreateDialogState; clearTableCreateDialogError(state); setTableCreateDialogSaving(state, false); if ( state.shouldRestoreFocus && state.currentButton && document.contains(state.currentButton) ) { state.currentButton.focus(); } }); return tableCreateDialogState; } function openTableCreateDialog(button, manager) { var data = databaseCreateTableData(); if (!data) { return; } var state = ensureTableCreateDialog(manager); if (!state) { return; } var menu = button.closest("details"); if (menu) { menu.open = false; } state.manager = manager; state.currentButton = button; state.shouldRestoreFocus = true; state.title.textContent = "Create a table in " + data.databaseName; clearTableCreateDialogError(state); resetTableCreateDialog(state); loadTableCreateForeignKeyTargets(state); if (!state.dialog.open) { state.dialog.showModal(); } state.tableName.focus(); } function initTableCreateActions(manager) { if ( !window.fetch || !window.HTMLDialogElement || !databaseCreateTableData() ) { return; } document.addEventListener("click", function (ev) { var button = ev.target.closest( 'button[data-database-action="create-table"]', ); if (!button) { return; } ev.preventDefault(); openTableCreateDialog(button, manager); }); } function setRowDeleteDialogBusy(state, isBusy) { state.isBusy = isBusy; state.confirmButton.disabled = isBusy; state.cancelButton.disabled = isBusy; state.confirmButton.textContent = isBusy ? "Deleting..." : "Delete row"; } function clearRowDeleteDialogError(state) { state.error.hidden = true; state.error.textContent = ""; } function showRowDeleteDialogError(state, message) { state.error.hidden = false; state.error.textContent = message; } function rowMutationRequestError(response, data) { if (data && data.errors) { return new Error(data.errors.join(" ")); } if (data && data.error) { return new Error(data.error); } if (data && data.title) { return new Error(data.title); } return new Error("Request failed with HTTP " + response.status); } function tildeDecode(value) { if (!value) { return ""; } var placeholder = "__datasette_percent_placeholder__"; try { return decodeURIComponent( value.replace(/%/g, placeholder).replace(/~/g, "%").replace(/\+/g, " "), ).replace(new RegExp(placeholder, "g"), "%"); } catch (_error) { return value; } } function tildeEncode(value) { var bytes = new TextEncoder().encode(String(value)); var encoded = ""; bytes.forEach(function (byte) { var isSafe = (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || (byte >= 48 && byte <= 57) || byte === 95 || byte === 45; if (isSafe) { encoded += String.fromCharCode(byte); } else if (byte === 32) { encoded += "+"; } else { encoded += "~" + byte.toString(16).toUpperCase().padStart(2, "0"); } }); return encoded; } function rowDisplayLabel(row) { return tildeDecode(row.getAttribute("data-row") || ""); } function rowTitleLabel(row) { return row.getAttribute("data-row-label") || ""; } function insertedRowStatusMessage(rowId, rowLabel) { var message = "Inserted row " + rowId; if (rowLabel && rowLabel !== rowId) { message += " (" + rowLabel + ")"; } return message + "."; } function tableBaseUrl() { var tableUrl = window._datasetteTableData && window._datasetteTableData.tableUrl; var url = new URL(tableUrl || location.href, location.href); url.hash = ""; url.search = ""; return url; } function tablePageData() { return window._datasetteTableData || {}; } function tableInsertData() { return tablePageData().insertRow; } function tableAlterData() { return tablePageData().alterTable; } function tableAlterColumnTypes() { var data = tableAlterData() || {}; return data.columnTypes && data.columnTypes.length ? data.columnTypes : ["text", "integer", "float", "blob"]; } function tableAlterDefaultExpressions() { var data = tableAlterData() || {}; return data.defaultExpressions || []; } function tableAlterCustomColumnTypes() { var data = tableAlterData() || {}; return data.customColumnTypes || []; } function tableAlterCustomColumnType(name) { var options = tableAlterCustomColumnTypes(); for (var i = 0; i < options.length; i += 1) { if (options[i].name === name) { return options[i]; } } return null; } function tableAlterCustomTypeAppliesToSqliteType(option, sqliteType) { return ( option && option.sqliteTypes && option.sqliteTypes.indexOf(sqliteType) !== -1 ); } function tableAlterDialogRows(state) { return schemaDialogRows(state, "table-alter"); } function tableAlterRowSignature(row) { return { existing: row.dataset.existing === "1", originalName: row.dataset.originalName || "", name: row.querySelector(".table-alter-column-name").value, type: row.querySelector(".table-alter-column-type").value, customType: ( row.querySelector(".table-alter-custom-column-type") || { value: "", } ).value || "", notNull: row.querySelector(".table-alter-not-null-input").checked, defaultValue: row.querySelector(".table-alter-default").value, defaultExpr: row.querySelector(".table-alter-default-expr").value, pk: row.querySelector(".table-alter-primary-key-input").checked, }; } function tableAlterDialogSignature(state) { if (!state || !state.form) { return ""; } return JSON.stringify({ columns: tableAlterDialogRows(state).map(tableAlterRowSignature), deletedColumns: state.deletedColumns.slice(), }); } function tableAlterDialogHasChanges(state) { return ( !!state && !state.isSaving && tableAlterDialogSignature(state) !== state.initialSignature ); } function updateTableAlterSaveButtonState(state) { if (!state || !state.saveButton) { return; } state.saveButton.disabled = state.isSaving || (state.mode !== "review" && tableAlterDialogSignature(state) === state.initialSignature); } function tableAlterRowIsPrimaryKey(row) { return schemaDialogRowIsPrimaryKey(row, "table-alter"); } function tableAlterFirstNonPrimaryRow(state) { return schemaDialogFirstNonPrimaryRow(state, "table-alter"); } function updateTableAlterMoveButtons(state) { updateSchemaDialogMoveButtons(state, "table-alter"); } function normalizeTableAlterPrimaryKeyRows(state) { normalizeSchemaDialogPrimaryKeyRows(state, "table-alter"); } function clearTableAlterDialogError(state) { state.error.hidden = true; state.error.textContent = ""; state.dialog.removeAttribute("aria-describedby"); } function showTableAlterDialogError(state, message) { state.error.hidden = false; state.error.textContent = message; state.dialog.setAttribute("aria-describedby", "table-alter-error"); state.error.focus(); } function setTableAlterDialogSaving(state, isSaving) { state.isSaving = isSaving; state.cancelButton.disabled = isSaving; state.addColumnButton.disabled = isSaving; state.backButton.disabled = isSaving; state.dropButton.disabled = isSaving; state.saveButton.textContent = isSaving ? state.mode === "review" ? "Applying..." : "Preparing..." : tableAlterSaveButtonText(state); state.columnList .querySelectorAll("input, select, button") .forEach(function (control) { control.disabled = isSaving; }); if (!isSaving) { state.columnList .querySelectorAll(".table-alter-default-expr") .forEach(function (select) { syncTableAlterDefaultControls( select.closest(".table-alter-column-row"), ); }); } updateTableAlterMoveButtons(state); updateTableAlterSaveButtonState(state); } function tableAlterSaveButtonText(state) { return state && state.mode === "review" ? "Apply changes" : "Review changes"; } function tableAlterSelectTypeValue(select, type) { var options = tableAlterColumnTypes(); populateSqliteColumnTypeSelect(select, type, options); } function updateTableAlterCustomColumnTypePlaceholder(select) { updateSelectPlaceholder(select, "table-alter-input-placeholder"); } function createTableAlterCustomColumnTypeSelect() { var options = tableAlterCustomColumnTypes(); return createCustomColumnTypeSelect( options, "table-alter-input table-alter-custom-column-type", "table-alter-input-placeholder", ); } function syncTableAlterCustomTypeForSqliteType(row) { var typeSelect = row.querySelector(".table-alter-column-type"); var customTypeSelect = row.querySelector(".table-alter-custom-column-type"); if (!typeSelect || !customTypeSelect || !customTypeSelect.value) { return; } var option = tableAlterCustomColumnType(customTypeSelect.value); if (!tableAlterCustomTypeAppliesToSqliteType(option, typeSelect.value)) { customTypeSelect.value = ""; updateTableAlterCustomColumnTypePlaceholder(customTypeSelect); } } function tableAlterSelectDefaultExprValue(select, value) { var blankOption = document.createElement("option"); blankOption.value = ""; blankOption.textContent = "- default expr -"; select.appendChild(blankOption); tableAlterDefaultExpressions().forEach(function (option) { var optionElement = document.createElement("option"); optionElement.value = option; optionElement.textContent = option.replace(/_/g, " "); select.appendChild(optionElement); }); select.value = value || ""; updateTableAlterDefaultExprPlaceholder(select); } function updateTableAlterDefaultExprPlaceholder(select) { updateSelectPlaceholder(select, "table-alter-input-placeholder"); } function syncTableAlterDefaultControls(row) { if (!row) { return; } var defaultInput = row.querySelector(".table-alter-default"); var defaultExprSelect = row.querySelector(".table-alter-default-expr"); if (!defaultInput || !defaultExprSelect) { return; } defaultInput.disabled = !!defaultExprSelect.value; updateTableAlterDefaultExprPlaceholder(defaultExprSelect); } function createTableAlterColumnRow(state, column) { var index = state.nextColumnIndex; state.nextColumnIndex += 1; var existing = !!(column && column.existing); var originalName = existing ? column.name || "" : ""; var originalCustomType = existing && column.column_type ? column.column_type.type || "" : ""; var originalDefault = existing && column.has_default && column.default !== null ? String(column.default) : ""; var row = document.createElement("div"); row.className = "table-alter-column-row"; row.dataset.existing = existing ? "1" : "0"; row.dataset.originalName = originalName; row.dataset.originalType = existing ? column.type || "text" : ""; row.dataset.originalNotNull = existing && column.notnull ? "1" : "0"; row.dataset.originalHasDefault = existing && column.has_default ? "1" : "0"; row.dataset.originalDefault = originalDefault; row.dataset.originalPk = existing && column.is_pk ? "1" : "0"; row.dataset.originalCustomType = originalCustomType; var main = document.createElement("div"); main.className = "table-alter-column-main"; var details = document.createElement("div"); details.className = "table-alter-column-details"; details.id = "table-alter-column-details-" + index; details.hidden = !(column && column.expanded); var expandButton = createSchemaDialogMoreOptionsButton( "table-alter", details, ); var nameId = "table-alter-column-name-" + index; var nameLabel = document.createElement("label"); nameLabel.className = "table-alter-column-label"; nameLabel.setAttribute("for", nameId); nameLabel.textContent = "Column"; var nameInput = document.createElement("input"); nameInput.id = nameId; nameInput.className = "table-alter-input table-alter-column-name"; nameInput.type = "text"; nameInput.required = true; nameInput.autocomplete = "off"; nameInput.value = column && column.name ? column.name : ""; var typeSelect = document.createElement("select"); typeSelect.className = "table-alter-input table-alter-column-type"; typeSelect.setAttribute("aria-label", "Column type"); tableAlterSelectTypeValue(typeSelect, column && column.type); var customTypeSelect = null; var customTypeField = null; if (tableAlterCustomColumnTypes().length) { var customTypeId = "table-alter-column-custom-type-" + index; customTypeField = document.createElement("div"); customTypeField.className = "table-alter-detail-field table-alter-custom-type-field"; var customTypeLabel = document.createElement("label"); customTypeLabel.className = "table-alter-detail-label"; customTypeLabel.setAttribute("for", customTypeId); customTypeLabel.textContent = "Custom type"; var customTypeHelpId = "table-alter-column-custom-type-help-" + index; var customTypeHelp = document.createElement("p"); customTypeHelp.id = customTypeHelpId; customTypeHelp.className = "table-alter-detail-help"; customTypeHelp.textContent = "Controls how Datasette displays and edits this column"; customTypeSelect = createTableAlterCustomColumnTypeSelect(); customTypeSelect.id = customTypeId; customTypeSelect.setAttribute("aria-describedby", customTypeHelpId); customTypeSelect.value = originalCustomType; updateTableAlterCustomColumnTypePlaceholder(customTypeSelect); customTypeField.appendChild(customTypeLabel); customTypeField.appendChild(customTypeHelp); customTypeField.appendChild(customTypeSelect); } var notNullLabel = document.createElement("label"); notNullLabel.className = "table-alter-detail-check table-alter-not-null"; var notNullInput = document.createElement("input"); notNullInput.type = "checkbox"; notNullInput.className = "table-alter-not-null-input"; notNullInput.checked = !!(column && column.notnull); var notNullText = document.createElement("span"); var notNullStrong = document.createElement("strong"); notNullStrong.textContent = "Not null"; notNullText.appendChild(notNullStrong); notNullText.appendChild( document.createTextNode(" This value cannot be left unset"), ); notNullLabel.appendChild(notNullInput); notNullLabel.appendChild(notNullText); var defaultId = "table-alter-column-default-" + index; var defaultField = document.createElement("div"); defaultField.className = "table-alter-detail-field"; var defaultLabel = document.createElement("label"); defaultLabel.className = "table-alter-detail-label"; defaultLabel.setAttribute("for", defaultId); defaultLabel.textContent = "Default value"; var defaultInput = document.createElement("input"); defaultInput.id = defaultId; defaultInput.className = "table-alter-input table-alter-default"; defaultInput.type = "text"; defaultInput.autocomplete = "off"; defaultInput.placeholder = "default"; defaultInput.setAttribute("aria-label", "Default value"); defaultInput.value = originalDefault; defaultField.appendChild(defaultLabel); defaultField.appendChild(defaultInput); var defaultExprId = "table-alter-column-default-expr-" + index; var defaultExprField = document.createElement("div"); defaultExprField.className = "table-alter-detail-field"; var defaultExprLabel = document.createElement("label"); defaultExprLabel.className = "table-alter-detail-label"; defaultExprLabel.setAttribute("for", defaultExprId); defaultExprLabel.textContent = "or default to a specific time"; var defaultExprSelect = document.createElement("select"); defaultExprSelect.id = defaultExprId; defaultExprSelect.className = "table-alter-input table-alter-default-expr"; defaultExprSelect.setAttribute("aria-label", "or default to a specific time"); tableAlterSelectDefaultExprValue(defaultExprSelect, ""); defaultExprField.appendChild(defaultExprLabel); defaultExprField.appendChild(defaultExprSelect); var pkLabel = document.createElement("label"); pkLabel.className = "table-alter-detail-check table-alter-primary-key"; var pkInput = document.createElement("input"); pkInput.type = "checkbox"; pkInput.className = "table-alter-primary-key-input"; pkInput.checked = !!(column && column.is_pk); var pkText = document.createElement("span"); var pkStrong = document.createElement("strong"); pkStrong.textContent = "Primary key"; pkText.appendChild(pkStrong); pkText.appendChild( document.createTextNode(" This ID uniquely identifies the record"), ); pkLabel.appendChild(pkInput); pkLabel.appendChild(pkText); var moveControls = createSchemaDialogMoveControls("table-alter"); var removeButton = createSchemaDialogIconButton( "table-alter", "remove-column", existing ? "Drop column" : "Remove column", existing ? "Drop column" : "Remove column", COLUMN_MOVE_ICONS.remove, ); main.appendChild(nameLabel); main.appendChild(nameInput); main.appendChild(typeSelect); main.appendChild(moveControls.controls); main.appendChild(removeButton); main.appendChild(expandButton); if (customTypeField) { details.appendChild(customTypeField); } details.appendChild(notNullLabel); details.appendChild(defaultField); details.appendChild(defaultExprField); details.appendChild(pkLabel); row.appendChild(main); row.appendChild(details); var controls = [ nameInput, typeSelect, notNullInput, defaultInput, defaultExprSelect, pkInput, ]; if (customTypeSelect) { controls.push(customTypeSelect); } controls.forEach(function (control) { control.addEventListener("input", function () { clearTableAlterDialogError(state); updateTableAlterSaveButtonState(state); }); control.addEventListener("change", function () { clearTableAlterDialogError(state); updateTableAlterSaveButtonState(state); }); }); defaultInput.addEventListener("input", function () { if (defaultInput.value) { defaultExprSelect.value = ""; syncTableAlterDefaultControls(row); } updateTableAlterSaveButtonState(state); }); defaultExprSelect.addEventListener("change", function () { if (defaultExprSelect.value) { defaultInput.value = ""; } syncTableAlterDefaultControls(row); updateTableAlterSaveButtonState(state); }); pkInput.addEventListener("change", function () { normalizeTableAlterPrimaryKeyRows(state); updateTableAlterMoveButtons(state); updateTableAlterSaveButtonState(state); }); expandButton.addEventListener("click", function () { toggleSchemaDialogMoreOptions(expandButton, details); }); typeSelect.addEventListener("change", function () { syncTableAlterCustomTypeForSqliteType(row); updateTableAlterSaveButtonState(state); }); if (customTypeSelect) { customTypeSelect.addEventListener("change", function () { updateTableAlterCustomColumnTypePlaceholder(customTypeSelect); var option = tableAlterCustomColumnType(customTypeSelect.value); if ( option && option.fixedSqliteType && tableAlterColumnTypes().indexOf(option.fixedSqliteType) !== -1 ) { typeSelect.value = option.fixedSqliteType; } updateTableAlterSaveButtonState(state); }); } moveControls.topButton.addEventListener("click", function () { var first = tableAlterFirstNonPrimaryRow(state); if ( state.isSaving || tableAlterRowIsPrimaryKey(row) || !first || first === row ) { return; } state.columnList.insertBefore(row, first); clearTableAlterDialogError(state); updateTableAlterMoveButtons(state); updateTableAlterSaveButtonState(state); row.querySelector(".table-alter-column-name").focus(); }); moveControls.upButton.addEventListener("click", function () { var previous = row.previousElementSibling; if ( state.isSaving || tableAlterRowIsPrimaryKey(row) || !previous || tableAlterRowIsPrimaryKey(previous) ) { return; } state.columnList.insertBefore(row, previous); clearTableAlterDialogError(state); updateTableAlterMoveButtons(state); updateTableAlterSaveButtonState(state); row.querySelector(".table-alter-column-name").focus(); }); moveControls.downButton.addEventListener("click", function () { var next = row.nextElementSibling; if (state.isSaving || tableAlterRowIsPrimaryKey(row) || !next) { return; } state.columnList.insertBefore(next, row); clearTableAlterDialogError(state); updateTableAlterMoveButtons(state); updateTableAlterSaveButtonState(state); row.querySelector(".table-alter-column-name").focus(); }); moveControls.bottomButton.addEventListener("click", function () { var last = state.columnList.lastElementChild; if ( state.isSaving || tableAlterRowIsPrimaryKey(row) || !last || last === row ) { return; } state.columnList.appendChild(row); clearTableAlterDialogError(state); updateTableAlterMoveButtons(state); updateTableAlterSaveButtonState(state); row.querySelector(".table-alter-column-name").focus(); }); removeButton.addEventListener("click", function () { if (state.isSaving) { return; } if (row.dataset.existing === "1") { state.deletedColumns.push(row.dataset.originalName); } row.remove(); clearTableAlterDialogError(state); updateTableAlterMoveButtons(state); updateTableAlterSaveButtonState(state); var nextInput = state.columnList.querySelector(".table-alter-column-name"); if (nextInput) { nextInput.focus(); } else { state.addColumnButton.focus(); } }); syncTableAlterDefaultControls(row); return row; } function addTableAlterColumn(state, column) { var row = createTableAlterColumnRow(state, column || { type: "text" }); state.columnList.appendChild(row); return row; } function resetTableAlterDialog(state, data) { state.nextColumnIndex = 0; state.deletedColumns = []; state.originalPrimaryKeys = (data.primaryKeys || []).slice(); state.originalColumnNames = (data.columns || []).map(function (column) { return column.name; }); state.columnList.textContent = ""; (data.columns || []).forEach(function (column) { addTableAlterColumn( state, Object.assign({}, column, { existing: true, }), ); }); normalizeTableAlterPrimaryKeyRows(state); state.initialSignature = tableAlterDialogSignature(state); showTableAlterEditor(state); } function collectTableAlterRows(state) { return tableAlterDialogRows(state).map(function (row) { var signature = tableAlterRowSignature(row); signature.originalType = row.dataset.originalType || ""; signature.originalNotNull = row.dataset.originalNotNull === "1"; signature.originalHasDefault = row.dataset.originalHasDefault === "1"; signature.originalDefault = row.dataset.originalDefault || ""; signature.originalPk = row.dataset.originalPk === "1"; signature.originalCustomType = row.dataset.originalCustomType || ""; return signature; }); } function validateTableAlterRows(state, rows) { if (!rows.length) { return "At least one column is required."; } var seen = {}; var supportedTypes = tableAlterColumnTypes(); for (var i = 0; i < rows.length; i += 1) { var row = rows[i]; var name = row.name.trim(); if (!name) { return "Column name is required."; } if (name.indexOf("\n") !== -1) { return "Column names cannot contain newlines."; } var columnKey = name.toLowerCase(); if (seen[columnKey]) { return "Duplicate column name: " + name; } seen[columnKey] = true; if (supportedTypes.indexOf(row.type) === -1) { return "Unsupported column type: " + row.type; } if (row.customType) { var option = tableAlterCustomColumnType(row.customType); if (!option) { return "Unknown custom column type: " + row.customType; } if (!tableAlterCustomTypeAppliesToSqliteType(option, row.type)) { return ( "Custom type " + row.customType + " cannot be used with SQLite type " + row.type + "." ); } } if (row.defaultValue && row.defaultExpr) { return "Use either a default value or a default expression."; } if (!row.existing && row.notNull && !row.defaultValue && !row.defaultExpr) { return "New NOT NULL columns need a default or default expression."; } } var pkColumns = rows.filter(function (row) { return row.pk; }); if (state.originalPrimaryKeys.length && !pkColumns.length) { return "At least one primary key column is required."; } return null; } function collectTableAlterColumnTypeAssignments(rows) { var assignments = []; if (!tableAlterCustomColumnTypes().length) { return assignments; } rows.forEach(function (row) { var renamed = row.existing && row.name.trim() !== row.originalName; if (row.customType === row.originalCustomType && !renamed) { return; } if (!row.customType && !row.originalCustomType) { return; } assignments.push({ column: row.name.trim(), columnType: row.customType || null, sqliteType: row.type, }); }); return assignments; } function tableAlterPkIdentityColumns(rows) { return rows .filter(function (row) { return row.pk; }) .map(function (row) { return row.existing ? row.originalName : row.name.trim(); }); } function tableAlterPkChanged(state, rows) { return ( JSON.stringify(tableAlterPkIdentityColumns(rows)) !== JSON.stringify(state.originalPrimaryKeys) ); } function tableAlterNaturalColumnOrder(state, rows) { var existingRowsByOriginalName = {}; var newRows = []; rows.forEach(function (row) { if (row.existing) { existingRowsByOriginalName[row.originalName] = row; } else { newRows.push(row); } }); var naturalOrder = []; state.originalColumnNames.forEach(function (originalName) { var row = existingRowsByOriginalName[originalName]; if (row) { naturalOrder.push(row.name.trim()); } }); newRows.forEach(function (row) { naturalOrder.push(row.name.trim()); }); return naturalOrder; } function tableAlterColumnsReordered(state, rows) { var finalOrder = rows.map(function (row) { return row.name.trim(); }); return ( JSON.stringify(finalOrder) !== JSON.stringify(tableAlterNaturalColumnOrder(state, rows)) ); } function collectTableAlterPayload(state) { var rows = collectTableAlterRows(state); var validationError = validateTableAlterRows(state, rows); if (validationError) { return { error: validationError }; } var operations = []; var columnTypeAssignments = collectTableAlterColumnTypeAssignments(rows); rows.forEach(function (row) { var name = row.name.trim(); if (!row.existing) { var addArgs = { name: name, type: row.type, not_null: row.notNull, }; if (row.defaultExpr) { addArgs.default_expr = row.defaultExpr; } else if (row.defaultValue || row.notNull) { addArgs.default = row.defaultValue; } operations.push({ op: "add_column", args: addArgs }); return; } var originalName = row.originalName; if (name !== originalName) { operations.push({ op: "rename_column", args: { name: originalName, to: name }, }); } var alterArgs = { name: originalName }; if (row.type !== row.originalType) { alterArgs.type = row.type; } if (row.notNull !== row.originalNotNull) { alterArgs.not_null = row.notNull; } if (row.defaultExpr) { alterArgs.default_expr = row.defaultExpr; } else if (row.originalHasDefault) { if (row.defaultValue !== row.originalDefault) { alterArgs.default = row.defaultValue === "" ? null : row.defaultValue; } } else if (row.defaultValue) { alterArgs.default = row.defaultValue; } if (Object.keys(alterArgs).length > 1) { operations.push({ op: "alter_column", args: alterArgs }); } }); state.deletedColumns.forEach(function (name) { operations.push({ op: "drop_column", args: { name: name } }); }); var pkColumns = rows .filter(function (row) { return row.pk; }) .map(function (row) { return row.name.trim(); }); if (tableAlterPkChanged(state, rows)) { operations.push({ op: "set_primary_key", args: { columns: pkColumns } }); } if (tableAlterColumnsReordered(state, rows)) { operations.push({ op: "reorder_columns", args: { columns: rows.map(function (row) { return row.name.trim(); }), }, }); } if (!operations.length && !columnTypeAssignments.length) { return { error: "No changes to apply." }; } return { payload: operations.length ? { operations: operations } : null, columnTypeAssignments: columnTypeAssignments, }; } function tableAlterQuotedName(name) { return '"' + name + '"'; } function tableAlterReadableDefaultExpression(value) { return value ? value.replace(/_/g, " ") : ""; } function tableAlterReadableValue(value) { if (value === null) { return "NULL"; } return '"' + String(value) + '"'; } function tableAlterOperationSummary(operation) { var args = operation.args || {}; if (operation.op === "add_column") { var addDetails = ["as " + args.type]; if (args.not_null) { addDetails.push("with values required"); } if (args.default_expr) { addDetails.push( "defaulting to " + tableAlterReadableDefaultExpression(args.default_expr), ); } else if (Object.prototype.hasOwnProperty.call(args, "default")) { addDetails.push( "with default value " + tableAlterReadableValue(args.default), ); } return { text: "Add column " + tableAlterQuotedName(args.name) + " " + addDetails.join(", ") + ".", damaging: false, }; } if (operation.op === "rename_column") { return { text: "Rename column " + tableAlterQuotedName(args.name) + " to " + tableAlterQuotedName(args.to) + ".", damaging: false, }; } if (operation.op === "alter_column") { var changes = []; if (args.type) { changes.push("set type to " + args.type); } if (Object.prototype.hasOwnProperty.call(args, "not_null")) { changes.push( args.not_null ? "not null (require values)" : "allow unset values", ); } if (args.default_expr) { changes.push( "default to " + tableAlterReadableDefaultExpression(args.default_expr), ); } else if (Object.prototype.hasOwnProperty.call(args, "default")) { changes.push( args.default === null ? "remove the default value" : "set default value to " + tableAlterReadableValue(args.default), ); } return { text: "Change column " + tableAlterQuotedName(args.name) + ": " + changes.join(", ") + ".", damaging: false, }; } if (operation.op === "drop_column") { return { text: "Drop column " + tableAlterQuotedName(args.name) + ".", damaging: true, }; } if (operation.op === "set_primary_key") { return { text: "Set primary key to " + (args.columns || []).map(tableAlterQuotedName).join(", ") + ".", damaging: false, }; } if (operation.op === "reorder_columns") { return { text: "Set column order to " + (args.columns || []).map(tableAlterQuotedName).join(", ") + ".", damaging: false, }; } return { text: "Run " + operation.op + ".", damaging: false, }; } function tableAlterColumnTypeAssignmentSummary(assignment) { return { text: assignment.columnType ? "Set custom type for column " + tableAlterQuotedName(assignment.column) + " to " + assignment.columnType + "." : "Remove custom type from column " + tableAlterQuotedName(assignment.column) + ".", damaging: false, }; } function tableAlterReviewItems(result) { var items = []; var operations = result.payload ? result.payload.operations || [] : []; operations.forEach(function (operation) { items.push(tableAlterOperationSummary(operation)); }); (result.columnTypeAssignments || []).forEach(function (assignment) { items.push(tableAlterColumnTypeAssignmentSummary(assignment)); }); return items; } function tableAlterReviewHasDamagingItems(items) { return items.some(function (item) { return item.damaging; }); } function appendTableAlterReviewText(element, text) { text.split(/("[^"]+")/g).forEach(function (part) { if (!part) { return; } if (part.charAt(0) === '"' && part.charAt(part.length - 1) === '"') { var name = document.createElement("code"); name.className = "table-alter-review-name"; name.textContent = part.slice(1, -1); element.appendChild(name); } else { element.appendChild(document.createTextNode(part)); } }); } function tableAlterSetColumnTypeUrl() { var data = tableAlterData(); if (!data || !data.path) { return null; } var url = new URL(data.path, location.href); url.pathname = url.pathname.replace(/\/-\/alter\/?$/, "/-/set-column-type"); return url.toString(); } async function assignTableAlterColumnTypes(assignments) { if (!assignments.length) { return; } var url = tableAlterSetColumnTypeUrl(); if (!url) { throw new Error("Could not find the set column type URL."); } for (var i = 0; i < assignments.length; i += 1) { var assignment = assignments[i]; var response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ column: assignment.column, column_type: assignment.columnType ? { type: assignment.columnType, } : null, }), }); var data = null; try { data = await response.json(); } catch (_error) { data = null; } if (!response.ok || (data && data.ok === false)) { var error = rowMutationRequestError(response, data); throw new Error( "Saved schema changes, but could not set custom type for " + assignment.column + ": " + error.message, ); } } } function showTableAlterEditor(state) { state.mode = "edit"; state.reviewResult = null; state.dialog.classList.remove("table-alter-reviewing"); state.fields.hidden = false; state.review.hidden = true; state.review.textContent = ""; state.backButton.hidden = true; var data = tableAlterData(); state.dropButton.hidden = !(data && data.dropPath); state.saveButton.textContent = tableAlterSaveButtonText(state); updateTableAlterMoveButtons(state); updateTableAlterSaveButtonState(state); } function showTableAlterReview(state, result) { var items = tableAlterReviewItems(result); state.mode = "review"; state.reviewResult = result; state.dialog.classList.add("table-alter-reviewing"); state.fields.hidden = true; state.review.hidden = false; state.review.textContent = ""; state.backButton.hidden = false; state.dropButton.hidden = true; state.saveButton.textContent = tableAlterSaveButtonText(state); updateTableAlterSaveButtonState(state); var heading = document.createElement("h3"); heading.className = "table-alter-review-title"; heading.tabIndex = -1; heading.textContent = "Review changes"; state.review.appendChild(heading); var intro = document.createElement("p"); intro.className = "table-alter-review-intro"; intro.textContent = "These changes will be applied to the table."; state.review.appendChild(intro); if (tableAlterReviewHasDamagingItems(items)) { var warning = document.createElement("p"); warning.className = "table-alter-review-warning"; warning.setAttribute("role", "alert"); warning.textContent = "Warning: data in dropped columns will be permanently lost."; state.review.appendChild(warning); } var list = document.createElement("ol"); list.className = "table-alter-review-list"; items.forEach(function (item) { var listItem = document.createElement("li"); appendTableAlterReviewText(listItem, item.text); if (item.damaging) { listItem.className = "table-alter-review-damaging"; } list.appendChild(listItem); }); state.review.appendChild(list); heading.focus(); } async function applyTableAlterChanges(state, result) { if (state.isSaving) { return; } if (!result) { showTableAlterDialogError(state, "Could not find the reviewed changes."); return; } var data = tableAlterData(); if (!data || !data.path) { showTableAlterDialogError(state, "Could not find the alter table URL."); return; } clearTableAlterDialogError(state); if (result.error) { showTableAlterDialogError(state, result.error); return; } setTableAlterDialogSaving(state, true); try { if (result.payload) { var response = await fetch(data.path, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(result.payload), }); var responseData = null; try { responseData = await response.json(); } catch (_error) { responseData = null; } if (!response.ok || (responseData && responseData.ok === false)) { throw rowMutationRequestError(response, responseData); } } await assignTableAlterColumnTypes(result.columnTypeAssignments || []); state.shouldRestoreFocus = false; state.dialog.close(); location.reload(); } catch (error) { setTableAlterDialogSaving(state, false); showTableAlterDialogError(state, error.message || "Could not alter table"); } } function tableAlterDatabaseUrl() { var data = tableAlterData(); if (!data || !data.path) { return null; } var url = new URL(data.path, location.href); url.pathname = url.pathname.replace(/\/[^/]+\/-\/alter\/?$/, ""); url.search = ""; url.hash = ""; return url.toString(); } async function dropTableFromAlterDialog(state) { if (state.isSaving) { return; } var data = tableAlterData(); if (!data || !data.dropPath) { return; } if ( !window.confirm( 'Permanently delete the table "' + data.tableName + '"? This will delete all of its data and cannot be undone.', ) ) { return; } clearTableAlterDialogError(state); setTableAlterDialogSaving(state, true); try { var response = await fetch(data.dropPath, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ confirm: true }), }); var responseData = null; try { responseData = await response.json(); } catch (_error) { responseData = null; } if (!response.ok || (responseData && responseData.ok === false)) { throw rowMutationRequestError(response, responseData); } state.shouldRestoreFocus = false; state.dialog.close(); window.location.href = tableAlterDatabaseUrl() || "/"; } catch (error) { setTableAlterDialogSaving(state, false); showTableAlterDialogError(state, error.message || "Could not drop table"); } } async function saveTableAlterDialog(state) { if (state.isSaving) { return; } if (state.mode === "review") { if (!state.reviewResult) { showTableAlterDialogError(state, "Could not find the reviewed changes."); return; } await applyTableAlterChanges(state, state.reviewResult); return; } clearTableAlterDialogError(state); var result = collectTableAlterPayload(state); if (result.error) { showTableAlterDialogError(state, result.error); return; } showTableAlterReview(state, result); } function confirmDiscardTableAlterChanges(state) { if (!tableAlterDialogHasChanges(state)) { return true; } return window.confirm("Discard table changes?"); } function closeTableAlterDialogIfConfirmed(state) { if (!state || state.isSaving) { return false; } if (!confirmDiscardTableAlterChanges(state)) { return false; } state.shouldRestoreFocus = true; state.dialog.close(); return true; } function closeTableAlterDialog(state) { if (!state || state.isSaving) { return false; } state.shouldRestoreFocus = true; state.dialog.close(); return true; } function ensureTableAlterDialog(manager) { if (tableAlterDialogState) { return tableAlterDialogState; } if (!window.HTMLDialogElement) { 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 = `
`; document.body.appendChild(dialog); tableAlterDialogState = { 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"), review: dialog.querySelector(".table-alter-review"), columnList: dialog.querySelector(".table-alter-column-list"), addColumnButton: dialog.querySelector(".table-alter-add-column"), backButton: dialog.querySelector(".table-alter-back"), 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: "", nextColumnIndex: 0, deletedColumns: [], originalColumnNames: [], originalPrimaryKeys: [], mode: "edit", reviewResult: null, manager: manager, }; tableAlterDialogState.form.addEventListener("submit", function (ev) { ev.preventDefault(); saveTableAlterDialog(tableAlterDialogState); }); tableAlterDialogState.addColumnButton.addEventListener("click", function () { if (tableAlterDialogState.isSaving) { return; } var row = addTableAlterColumn(tableAlterDialogState, { type: "text", existing: false, expanded: true, }); clearTableAlterDialogError(tableAlterDialogState); updateTableAlterMoveButtons(tableAlterDialogState); updateTableAlterSaveButtonState(tableAlterDialogState); row.querySelector(".table-alter-column-name").focus(); }); tableAlterDialogState.cancelButton.addEventListener("click", function () { closeTableAlterDialog(tableAlterDialogState); }); tableAlterDialogState.dropButton.addEventListener("click", function () { dropTableFromAlterDialog(tableAlterDialogState); }); tableAlterDialogState.backButton.addEventListener("click", function () { if (tableAlterDialogState.isSaving) { return; } clearTableAlterDialogError(tableAlterDialogState); showTableAlterEditor(tableAlterDialogState); var firstName = tableAlterDialogState.columnList.querySelector( ".table-alter-column-name", ); if (firstName) { firstName.focus(); } }); dialog.addEventListener("click", function (ev) { if (ev.target === dialog) { closeTableAlterDialogIfConfirmed(tableAlterDialogState); } }); 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 () { var state = tableAlterDialogState; clearTableAlterDialogError(state); setTableAlterDialogSaving(state, false); if ( state.shouldRestoreFocus && state.currentButton && document.contains(state.currentButton) ) { state.currentButton.focus(); } }); return tableAlterDialogState; } function openTableAlterDialog(button, manager) { var data = tableAlterData(); if (!data) { return; } var state = ensureTableAlterDialog(manager); if (!state) { return; } var menu = button.closest("details"); if (menu) { menu.open = false; } state.manager = manager; state.currentButton = button; state.shouldRestoreFocus = true; state.title.textContent = "Alter table " + data.tableName; clearTableAlterDialogError(state); resetTableAlterDialog(state, data); if (!state.dialog.open) { state.dialog.showModal(); } var firstName = state.columnList.querySelector(".table-alter-column-name"); if (firstName) { firstName.focus(); } } function initTableAlterActions(manager) { if (!window.fetch || !window.HTMLDialogElement || !tableAlterData()) { return; } document.addEventListener("click", function (ev) { var button = ev.target.closest('button[data-table-action="alter-table"]'); if (!button) { return; } ev.preventDefault(); openTableAlterDialog(button, manager); }); } function tableForeignKeys() { return tablePageData().foreignKeys || {}; } function isRowPage() { return document.body && document.body.classList.contains("row"); } function rowElementForActionButton(button) { return ( button.closest("[data-row]") || (button.getAttribute("data-row") ? button : null) ); } function foreignKeyAutocompleteUrl(column) { return tableForeignKeys()[column] || null; } function autocompleteRowPk(row) { var pks = (row && row.pks) || {}; var keys = Object.keys(pks); if (keys.length !== 1) { return null; } return pks[keys[0]]; } function foreignKeyRowUrl(autocompleteUrl, pk) { var url = new URL(autocompleteUrl, location.href); if (!/\/-\/autocomplete\/?$/.test(url.pathname)) { return null; } url.pathname = url.pathname.replace(/\/-\/autocomplete\/?$/, "") + "/" + tildeEncode(pk); url.search = ""; url.hash = ""; return url.toString(); } function foreignKeyLabelText(row) { var pk = autocompleteRowPk(row); var label = row && row.label; if ( label !== null && typeof label !== "undefined" && String(label) !== String(pk) ) { return String(label); } return "View row"; } function rowEditMetaTextWithoutCurrentValue(meta) { return (meta.dataset.baseMeta || "") .split(" · ") .filter(function (part) { return part !== "Current value: NULL"; }) .join(" · "); } function updateRowEditForeignKeySeparator(meta) { var separator = meta.querySelector(".row-edit-fk-separator"); if (!separator) { return; } var baseMeta = meta.querySelector(".row-edit-base-meta"); var hasBaseMeta = !!(baseMeta && baseMeta.textContent); separator.textContent = hasBaseMeta ? " · " : ""; separator.hidden = !hasBaseMeta; } function updateRowEditFieldMetaHidden(meta) { var baseMeta = meta.querySelector(".row-edit-base-meta"); var hasBaseMeta = !!(baseMeta && baseMeta.textContent); var foreignKeyLinkWrap = meta.querySelector(".row-edit-fk-link-wrap"); var hasForeignKeyLink = foreignKeyLinkWrap && !foreignKeyLinkWrap.hidden; meta.hidden = meta.dataset.reserveSpace !== "1" && !hasBaseMeta && !hasForeignKeyLink; } function setRowEditBaseMetaText(meta, text) { var baseMeta = meta.querySelector(".row-edit-base-meta"); if (!baseMeta) { return; } baseMeta.textContent = text || ""; updateRowEditForeignKeySeparator(meta); updateRowEditFieldMetaHidden(meta); } function setForeignKeyMetaLink(meta, autocompleteUrl, row) { var wrap = meta.querySelector(".row-edit-fk-link-wrap"); if (!wrap) { return; } var pkSpan = wrap.querySelector(".row-edit-fk-pk"); var link = wrap.querySelector("a"); var pk = autocompleteRowPk(row); var url = pk === null || typeof pk === "undefined" ? null : foreignKeyRowUrl(autocompleteUrl, pk); if (!url) { wrap.hidden = true; pkSpan.textContent = ""; link.removeAttribute("href"); link.textContent = ""; link.removeAttribute("aria-label"); setRowEditBaseMetaText(meta, meta.dataset.baseMeta || ""); updateRowEditFieldMetaHidden(meta); return; } setRowEditBaseMetaText(meta, rowEditMetaTextWithoutCurrentValue(meta)); var pkText = String(pk); var linkText = foreignKeyLabelText(row); pkSpan.textContent = pkText; link.href = url; link.textContent = linkText; link.setAttribute( "aria-label", "Open referenced row " + pkText + " " + linkText + " in a new tab", ); wrap.hidden = false; updateRowEditFieldMetaHidden(meta); } async function resolveForeignKeyMetaLink(control, autocompleteUrl, meta) { var value = control.value.trim(); if (!value) { setForeignKeyMetaLink(meta, autocompleteUrl, null); return; } var url = new URL(autocompleteUrl, location.href); url.searchParams.set("q", value); try { var response = await fetch(url.toString(), { headers: { Accept: "application/json", }, }); if (!response.ok) { throw new Error("HTTP " + response.status); } var data = await response.json(); if (control.value.trim() !== value) { return; } var rows = (data && data.rows) || []; var row = rows.find(function (candidate) { var pk = autocompleteRowPk(candidate); return pk !== null && typeof pk !== "undefined" && String(pk) === value; }); setForeignKeyMetaLink(meta, autocompleteUrl, row || null); } catch (_error) { if (control.value.trim() === value) { setForeignKeyMetaLink(meta, autocompleteUrl, null); } } } function tableInsertUrl() { var data = tableInsertData(); if (data && data.path) { return new URL(data.path, location.href).toString(); } var url = tableBaseUrl(); url.pathname = url.pathname.replace(/\/$/, "") + "/-/insert"; return url.toString(); } function rowResourceUrl(row) { var rowId = row.getAttribute("data-row"); if (!rowId) { return null; } var url = tableBaseUrl(); url.pathname = url.pathname.replace(/\/$/, "") + "/" + rowId; return url; } function rowJsonUrl(row) { var url = rowResourceUrl(row); if (!url) { return ""; } url.pathname = url.pathname + ".json"; url.searchParams.set("_extra", "columns,column_types"); return url.toString(); } function rowDeleteUrl(row) { var url = rowResourceUrl(row); if (!url) { return ""; } url.pathname = url.pathname.replace(/\/$/, "") + "/-/delete"; if (isRowPage()) { url.searchParams.set("_redirect_to_table", "1"); } return url.toString(); } function rowUpdateUrl(row) { var url = rowResourceUrl(row); if (!url) { return ""; } url.pathname = url.pathname.replace(/\/$/, "") + "/-/update"; if (isRowPage()) { url.searchParams.set("_message", "1"); } return url.toString(); } function rowFragmentUrl(row) { var rowId = row.getAttribute("data-row"); return rowFragmentUrlById(rowId); } function rowFragmentUrlById(rowId) { if (!rowId) { return ""; } var url = tableBaseUrl(); url.search = new URL(location.href).search; url.pathname = url.pathname.replace(/\/$/, "") + "/-/fragment"; url.searchParams.delete("_next"); url.searchParams.set("_row", rowId); url.searchParams.set("_nocount", "1"); url.searchParams.set("_nofacet", "1"); url.searchParams.set("_nosuggest", "1"); return url.toString(); } function nextRowActionFocusTarget(row, action) { var selector = 'button[data-row-action="' + action + '"]:not([disabled])'; var sibling = row.nextElementSibling; while (sibling) { var nextButton = sibling.querySelector(selector); if (nextButton) { return nextButton; } sibling = sibling.nextElementSibling; } sibling = row.previousElementSibling; while (sibling) { var previousButton = sibling.querySelector(selector); if (previousButton) { return previousButton; } sibling = sibling.previousElementSibling; } return null; } function nextRowDeleteFocusTarget(row, manager) { return ( nextRowActionFocusTarget(row, "delete") || ensureRowMutationStatus(manager) ); } function ensureRowDeleteDialog(manager) { if (rowDeleteDialogState) { return rowDeleteDialogState; } if (!window.HTMLDialogElement) { 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 = `

Delete row ?

`; document.body.appendChild(dialog); rowDeleteDialogState = { dialog: dialog, title: dialog.querySelector(".modal-title"), message: dialog.querySelector(".row-delete-message"), rowId: dialog.querySelector(".row-delete-id"), error: dialog.querySelector(".row-delete-error"), cancelButton: dialog.querySelector(".row-delete-cancel"), confirmButton: dialog.querySelector(".row-delete-confirm"), currentRow: null, currentDeleteUrl: null, 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" && document.activeElement === rowDeleteDialogState.confirmButton ) { ev.preventDefault(); 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 () { var state = rowDeleteDialogState; clearRowDeleteDialogError(state); setRowDeleteDialogBusy(state, false); if ( state.shouldRestoreFocus && state.currentButton && document.contains(state.currentButton) ) { state.currentButton.focus(); } }); rowDeleteDialogState.confirmButton.addEventListener( "click", async function () { var state = rowDeleteDialogState; clearRowDeleteDialogError(state); setRowDeleteDialogBusy(state, true); try { var response = await fetch(state.currentDeleteUrl, { method: "POST", headers: { Accept: "application/json", }, }); var data = null; try { data = await response.json(); } catch (_error) { data = null; } if (!response.ok || (data && data.ok === false)) { throw rowMutationRequestError(response, data); } if (data && data.redirect) { state.shouldRestoreFocus = false; state.dialog.close(); location.href = data.redirect; return; } var focusTarget = nextRowDeleteFocusTarget( state.currentRow, state.manager, ); var statusMessage = state.currentPkPath ? "Deleted row " + state.currentPkPath + "." : "Deleted row."; state.shouldRestoreFocus = false; state.dialog.close(); state.currentRow.remove(); showRowMutationStatus(state.manager, statusMessage, false); if (focusTarget && document.contains(focusTarget)) { focusTarget.focus(); } else { ensureRowMutationStatus(state.manager).focus(); } } catch (error) { setRowDeleteDialogBusy(state, false); showRowDeleteDialogError(state, error.message || "Delete failed"); } }, ); return rowDeleteDialogState; } function openRowDeleteDialog(button, manager) { var row = rowElementForActionButton(button); if (!row || !row.getAttribute("data-row")) { return; } var state = ensureRowDeleteDialog(manager); if (!state) { return; } 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); setRowDialogTitle( state.title, "Delete row", state.currentPkPath || "this row", rowTitleLabel(row), ); state.rowId.textContent = state.currentPkPath || "this row"; if (!state.dialog.open) { state.dialog.showModal(); } state.confirmButton.focus(); } function initRowDeleteActions(manager) { if (!window.fetch || !window.HTMLDialogElement) { return; } document.addEventListener("click", function (ev) { var button = ev.target.closest('button[data-row-action="delete"]'); if (!button) { return; } ev.preventDefault(); openRowDeleteDialog(button, manager); }); } function valueToEditText(value) { if (value === null || typeof value === "undefined") { return ""; } if (typeof value === "object") { return JSON.stringify(value, null, 2); } return String(value); } function shouldUseTextarea(value, columnType) { if (columnType && columnType.type === "textarea") { return true; } if (value && typeof value === "object") { return true; } var text = valueToEditText(value); return text.length > 80 || /[\r\n]/.test(text); } function rowEditValueKind(value) { if (value === null || typeof value === "undefined") { return "null"; } if (typeof value === "number") { return "number"; } if (typeof value === "boolean") { return "boolean"; } return "string"; } function rowEditControlElement(control, autocompleteUrl) { if (!autocompleteUrl || control.nodeName !== "INPUT") { return control; } var autocomplete = document.createElement("datasette-autocomplete"); autocomplete.setAttribute("src", autocompleteUrl); autocomplete.setAttribute("suggest-on-focus", ""); autocomplete.appendChild(control); return autocomplete; } function columnTypeForContext(columnType) { if (!columnType) { return null; } return { type: columnType.type, config: columnType.config || {}, }; } function defaultExpressionForContext(expression) { if (expression === null || typeof expression === "undefined") { return null; } return expression; } function columnFormControlContext(column, isPk, columnType, options) { var pageData = tablePageData(); var defaultExpression = defaultExpressionForContext( options.defaultExpression, ); return { mode: options.mode || "edit", database: pageData.database || null, table: pageData.table || (tableInsertData() && tableInsertData().tableName) || null, tableUrl: pageData.tableUrl || null, column: column, columnType: columnTypeForContext(columnType), sqliteType: options.sqliteType || null, notNull: !!options.notnull, isPk: !!isPk, defaultExpression: defaultExpression, form: options.form || null, dialog: options.dialog || null, }; } function makeColumnField(manager, context) { if (!manager || !manager.makeColumnField) { return null; } return manager.makeColumnField(context); } function createColumnFieldApi(options) { var control = options.control; var context = options.context; var field = { context: context, id: options.id, labelId: options.labelId, descriptionId: options.descriptionId, root: null, form: options.form || null, dialog: options.dialog || null, input: control, control: control, meta: options.meta || null, validationMessageElement: null, getValue: function () { return valueFromRowEditControl(control); }, setValue: function (value) { if ( value !== null && typeof value !== "undefined" && typeof value === "object" ) { throw new TypeError( "field.setValue() accepts strings, numbers, booleans or null; serialize objects before setting the field value", ); } field.stopUsingSqliteDefault(); control.value = valueToEditText(value); control.dataset.currentValueKind = rowEditValueKind(value); }, getInitialValue: function () { return initialValueFromRowEditControl(control); }, hasChanged: function () { return rowEditControlHasChanged(control); }, clearValue: function () { field.setValue(null); }, isUsingSqliteDefault: function () { return control.dataset.useSqliteDefault === "1"; }, useSqliteDefault: function () { if ( context.defaultExpression === null || typeof context.defaultExpression === "undefined" ) { return; } control.dataset.useSqliteDefault = "1"; control.disabled = true; control.value = ""; control.dataset.currentValueKind = "null"; field.syncSqliteDefaultUi(); }, stopUsingSqliteDefault: function () { if (control.dataset.useSqliteDefault !== "1") { return; } control.dataset.useSqliteDefault = "0"; control.disabled = false; field.syncSqliteDefaultUi(); }, syncSqliteDefaultUi: function () {}, markClean: function () { markRowEditControlClean(control); }, setValidity: function (message) { message = message || ""; control.setCustomValidity(message); if (message) { control.setAttribute("aria-invalid", "true"); } else { control.removeAttribute("aria-invalid"); } var validationMessage = ensureColumnFieldValidationMessage(field); if (validationMessage) { validationMessage.textContent = message; validationMessage.hidden = !message; } }, clearValidity: function () { field.setValidity(""); }, }; field.markClean(); return field; } function ensureColumnFieldValidationMessage(field) { if (field.validationMessageElement) { return field.validationMessageElement; } if (!field.meta) { return null; } var validationMessage = document.createElement("span"); validationMessage.id = field.id + "-validation-error"; validationMessage.className = "row-edit-field-validation-error"; validationMessage.hidden = true; validationMessage.setAttribute("role", "alert"); field.meta.appendChild(validationMessage); field.validationMessageElement = validationMessage; return validationMessage; } function renderColumnField(pluginControl, fieldApi) { if (!pluginControl || !pluginControl.render) { return null; } var pluginWrap = document.createElement("div"); pluginWrap.className = "row-edit-plugin-control"; pluginWrap.dataset.pluginName = pluginControl.pluginName || ""; pluginWrap.dataset.column = fieldApi.context.column; if (fieldApi.context.columnType && fieldApi.context.columnType.type) { pluginWrap.dataset.columnType = fieldApi.context.columnType.type; } fieldApi.root = pluginWrap; try { var rendered = pluginControl.render(fieldApi); if (rendered && rendered.nodeType) { pluginWrap.appendChild(rendered); } } catch (error) { console.error("Error rendering column form control", error); return null; } pluginWrap._datasetteColumnField = pluginControl; pluginWrap._datasetteColumnFormField = fieldApi; return pluginWrap; } function validateJsonColumnField(field) { var value = field.input.value; if (value.trim() === "") { field.clearValidity(); return true; } try { JSON.parse(value); field.clearValidity(); return true; } catch (error) { field.setValidity( "Invalid JSON" + (error && error.message ? ": " + error.message : ""), ); return false; } } function registerBuiltinColumnFieldPlugins(manager) { if (!manager || !manager.registerPlugin) { return; } manager.registerPlugin("datasette-json-column", { version: "1.0", makeColumnField: function (context) { if (!context.columnType || context.columnType.type !== "json") { return; } return { useTextarea: true, render: function (field) { field.input.addEventListener("input", function () { validateJsonColumnField(field); }); field.input.addEventListener("change", function () { validateJsonColumnField(field); }); validateJsonColumnField(field); return field.input; }, focus: function (field) { field.input.focus(); }, }; }, }); } function focusRowEditPluginControl(field) { var pluginWrap = field.querySelector(".row-edit-plugin-control"); if (!pluginWrap) { return false; } var pluginControl = pluginWrap._datasetteColumnField; var fieldApi = pluginWrap._datasetteColumnFormField; if (pluginControl && pluginControl.focus) { try { pluginControl.focus(fieldApi); return true; } catch (error) { console.error("Error focusing column form control", error); } } return false; } function focusFirstRowEditControl(state, options) { options = options || {}; var fields = state.fields.querySelectorAll(".row-edit-field"); for (var i = 0; i < fields.length; i += 1) { var field = fields[i]; var control = field.querySelector(".row-edit-input"); if (!control) { continue; } if (options.skipReadonly && (control.readOnly || control.disabled)) { continue; } if (focusRowEditPluginControl(field)) { return true; } control.focus(); return true; } return false; } function destroyRowEditFields(state) { if (!state || !state.fields) { return; } state.fields .querySelectorAll(".row-edit-plugin-control") .forEach(function (pluginWrap) { var pluginControl = pluginWrap._datasetteColumnField; var fieldApi = pluginWrap._datasetteColumnFormField; if (pluginControl && pluginControl.destroy) { try { pluginControl.destroy(fieldApi); } catch (error) { console.error("Error destroying column form control", error); } } }); state.fields.innerHTML = ""; } function createRowEditField(column, value, isPk, columnType, index, options) { options = options || {}; var field = document.createElement("div"); field.className = "row-edit-field"; var defaultExpression = defaultExpressionForContext( options.defaultExpression, ); var hasDefaultExpression = defaultExpression !== null; var useSqliteDefault = hasDefaultExpression && options.useSqliteDefault; var fieldId = "row-edit-field-" + index; var metaId = "row-edit-field-meta-" + index; var labelId = "row-edit-field-label-" + index; var label = document.createElement("label"); label.className = "row-edit-label"; label.id = labelId; label.setAttribute("for", fieldId); label.textContent = column; var controlWrap = document.createElement("div"); controlWrap.className = "row-edit-control-wrap"; var context = columnFormControlContext(column, isPk, columnType, options); var pluginControl = makeColumnField(options.manager, context); var useTextarea = (pluginControl && pluginControl.useTextarea === true) || shouldUseTextarea(value, columnType); var control = useTextarea ? document.createElement("textarea") : document.createElement("input"); control.className = "row-edit-input"; control.id = fieldId; control.name = column; control.value = valueToEditText(value); control.setAttribute("aria-describedby", metaId); control.dataset.initialValue = valueToEditText(value); control.dataset.initialValueKind = options.valueKind || rowEditValueKind(value); control.dataset.primaryKey = isPk ? "1" : "0"; control.dataset.currentValueKind = control.dataset.initialValueKind; if (hasDefaultExpression) { control.dataset.useSqliteDefault = useSqliteDefault ? "1" : "0"; } if (useSqliteDefault) { control.disabled = true; } if (options.omitIfBlank) { control.dataset.omitIfBlank = "1"; } if (control.nodeName === "TEXTAREA") { control.rows = Math.min(8, Math.max(3, control.value.split("\n").length)); } else { control.type = "text"; } if (isPk && options.primaryKeyReadonly !== false) { control.readOnly = true; } var meta = document.createElement("span"); meta.id = metaId; meta.className = "row-edit-field-meta"; if (options.autocompleteUrl) { meta.classList.add("row-edit-field-meta-autocomplete"); meta.dataset.reserveSpace = "1"; } var metaParts = []; if (isPk) { metaParts.push("Primary key"); } if (options.notnull) { metaParts.push("Required"); } if (hasDefaultExpression && !useSqliteDefault) { metaParts.push("SQLite default: " + defaultExpression); } if (value === null) { metaParts.push("Current value: NULL"); control.placeholder = "NULL"; } if (columnType && columnType.type) { metaParts.push("Custom type: " + columnType.type); } meta.dataset.baseMeta = metaParts.join(" · "); var baseMeta = document.createElement("span"); baseMeta.className = "row-edit-base-meta"; baseMeta.textContent = meta.dataset.baseMeta; meta.appendChild(baseMeta); if (options.autocompleteUrl) { var foreignKeyLinkWrap = document.createElement("span"); foreignKeyLinkWrap.className = "row-edit-fk-link-wrap"; foreignKeyLinkWrap.hidden = true; var foreignKeySeparator = document.createElement("span"); foreignKeySeparator.className = "row-edit-fk-separator"; foreignKeySeparator.textContent = meta.dataset.baseMeta ? " · " : ""; foreignKeySeparator.hidden = !meta.dataset.baseMeta; foreignKeyLinkWrap.appendChild(foreignKeySeparator); var foreignKeyPk = document.createElement("span"); foreignKeyPk.className = "row-edit-fk-pk"; foreignKeyLinkWrap.appendChild(foreignKeyPk); foreignKeyLinkWrap.appendChild(document.createTextNode(" ")); var foreignKeyLink = document.createElement("a"); foreignKeyLink.className = "row-edit-fk-link"; foreignKeyLink.target = "_blank"; foreignKeyLink.rel = "noopener noreferrer"; foreignKeyLinkWrap.appendChild(foreignKeyLink); meta.appendChild(foreignKeyLinkWrap); updateRowEditFieldMetaHidden(meta); } var fieldApi = createColumnFieldApi({ id: fieldId, labelId: labelId, descriptionId: metaId, control: control, meta: meta, input: control, form: options.form || null, dialog: options.dialog || null, context: context, }); field._datasetteColumnFormField = fieldApi; var pluginControlElement = renderColumnField(pluginControl, fieldApi); var controlElement = pluginControlElement || rowEditControlElement(control, options.autocompleteUrl); if (options.autocompleteUrl && !pluginControlElement) { control.addEventListener("input", function () { setForeignKeyMetaLink(meta, options.autocompleteUrl, null); }); control.addEventListener("change", function () { resolveForeignKeyMetaLink(control, options.autocompleteUrl, meta); }); controlElement.addEventListener( "datasette-autocomplete-select", function (ev) { setForeignKeyMetaLink( meta, options.autocompleteUrl, ev.detail && ev.detail.row, ); }, ); resolveForeignKeyMetaLink(control, options.autocompleteUrl, meta); } if (hasDefaultExpression) { var defaultBlock = document.createElement("div"); defaultBlock.className = "row-edit-default"; defaultBlock.setAttribute("aria-describedby", metaId); var defaultText = document.createElement("span"); defaultText.className = "row-edit-default-text"; defaultText.appendChild(document.createTextNode("default ")); var defaultCode = document.createElement("code"); defaultCode.className = "row-edit-default-code"; defaultCode.textContent = defaultExpression; defaultText.appendChild(defaultCode); var setValueButton = document.createElement("button"); setValueButton.type = "button"; setValueButton.className = "row-edit-default-button row-edit-default-set-value"; setValueButton.textContent = "Set value"; setValueButton.setAttribute("aria-label", "Set value for " + column); var customWrap = document.createElement("div"); customWrap.className = "row-edit-custom-value"; customWrap.hidden = true; var useSqliteDefaultButton = document.createElement("button"); useSqliteDefaultButton.type = "button"; useSqliteDefaultButton.className = "row-edit-default-button"; useSqliteDefaultButton.textContent = "Use default"; useSqliteDefaultButton.setAttribute( "aria-label", "Use SQLite default for " + column, ); setValueButton.addEventListener("click", function () { fieldApi.stopUsingSqliteDefault(); control.focus(); }); useSqliteDefaultButton.addEventListener("click", function () { fieldApi.useSqliteDefault(); setValueButton.focus(); }); defaultBlock.appendChild(defaultText); defaultBlock.appendChild(setValueButton); customWrap.appendChild(controlElement); customWrap.appendChild(useSqliteDefaultButton); controlWrap.appendChild(defaultBlock); controlWrap.appendChild(customWrap); fieldApi.syncSqliteDefaultUi = function () { var usingDefault = fieldApi.isUsingSqliteDefault(); defaultBlock.hidden = !usingDefault; customWrap.hidden = usingDefault; }; fieldApi.syncSqliteDefaultUi(); } else { controlWrap.appendChild(controlElement); } if (meta.textContent || options.autocompleteUrl) { controlWrap.appendChild(meta); } field.appendChild(label); field.appendChild(controlWrap); return field; } function clearRowEditDialogError(state) { state.error.hidden = true; state.error.textContent = ""; } function showRowEditDialogError(state, message) { state.error.hidden = false; state.error.textContent = message; state.error.focus(); } function updateRowEditDialogButtons(state) { state.saveButton.disabled = state.isLoading || state.isSaving || !state.hasLoaded; state.cancelButton.disabled = state.isSaving; var saveLabel = state.mode === "insert" ? "Insert row" : "Save"; state.saveButton.textContent = state.isSaving ? "Saving..." : saveLabel; state.form.setAttribute( "aria-busy", state.isLoading || state.isSaving ? "true" : "false", ); } function setRowEditDialogLoading(state, isLoading) { state.isLoading = isLoading; state.loading.hidden = !isLoading; updateRowEditDialogButtons(state); } function setRowEditDialogSaving(state, isSaving) { state.isSaving = isSaving; updateRowEditDialogButtons(state); } function valueFromRowEditControl(control) { var value = control.value; return valueFromRowEditText( control.name, value, rowEditControlValueKind(control), ); } function valueFromRowEditText(name, value, initialValueKind) { var trimmed = value.trim(); if (initialValueKind === "null" && value === "") { return null; } if (initialValueKind === "number") { if (trimmed === "") { return null; } var numberValue = Number(trimmed); if (Number.isNaN(numberValue)) { throw new Error(name + " must be a number"); } return numberValue; } if (initialValueKind === "boolean") { if (/^(true|1|yes)$/i.test(trimmed)) { return true; } if (/^(false|0|no)$/i.test(trimmed)) { return false; } throw new Error(name + " must be true or false"); } return value; } function initialValueFromRowEditControl(control) { return valueFromRowEditText( control.name, control.dataset.initialValue || "", control.dataset.initialValueKind || "string", ); } function rowEditControlValueKind(control) { return ( control.dataset.currentValueKind || control.dataset.initialValueKind || "string" ); } function rowEditControlCleanValue(control) { if (Object.prototype.hasOwnProperty.call(control.dataset, "cleanValue")) { return control.dataset.cleanValue; } return control.dataset.initialValue || ""; } function rowEditControlCleanValueKind(control) { return ( control.dataset.cleanValueKind || control.dataset.initialValueKind || "string" ); } function rowEditControlCleanUsesSqliteDefault(control) { if ( Object.prototype.hasOwnProperty.call( control.dataset, "cleanUseSqliteDefault", ) ) { return control.dataset.cleanUseSqliteDefault === "1"; } return false; } function markRowEditControlClean(control) { control.dataset.cleanValue = control.value; control.dataset.cleanValueKind = rowEditControlValueKind(control); control.dataset.cleanUseSqliteDefault = control.dataset.useSqliteDefault === "1" ? "1" : "0"; } function cleanValueFromRowEditControl(control) { return valueFromRowEditText( control.name, rowEditControlCleanValue(control), rowEditControlCleanValueKind(control), ); } function rowEditValuesMatch(left, right) { if (left === right) { return true; } if (left && right && typeof left === "object" && typeof right === "object") { return JSON.stringify(left) === JSON.stringify(right); } return false; } function rowEditControlHasChanged(control) { var usingSqliteDefault = control.dataset.useSqliteDefault === "1"; var cleanUsesSqliteDefault = rowEditControlCleanUsesSqliteDefault(control); if (usingSqliteDefault || cleanUsesSqliteDefault) { return usingSqliteDefault !== cleanUsesSqliteDefault; } if ( control.value === rowEditControlCleanValue(control) && rowEditControlValueKind(control) === rowEditControlCleanValueKind(control) ) { return false; } try { return !rowEditValuesMatch( valueFromRowEditControl(control), cleanValueFromRowEditControl(control), ); } catch (_error) { return true; } } function collectRowFormValues(state) { var values = {}; state.fields.querySelectorAll(".row-edit-input").forEach(function (control) { if ( state.mode === "edit" && (control.readOnly || control.dataset.primaryKey === "1") ) { return; } if (control.dataset.useSqliteDefault === "1") { return; } if (control.dataset.omitIfBlank === "1" && control.value === "") { return; } if ( state.mode === "edit" && control.value === (control.dataset.initialValue || "") && (control.dataset.currentValueKind || control.dataset.initialValueKind || "string") === (control.dataset.initialValueKind || "string") ) { return; } var value = valueFromRowEditControl(control); if (state.mode === "edit") { try { if ( rowEditValuesMatch(value, initialValueFromRowEditControl(control)) ) { return; } } catch (_error) { // If the original value cannot be parsed using the field's current // type, treat the field as changed and submit the corrected value. } } values[control.name] = value; }); return values; } function rowEditDialogHasChanges(state) { if (!state || !state.hasLoaded || state.isLoading) { return false; } var fields = state.fields.querySelectorAll(".row-edit-field"); for (var i = 0; i < fields.length; i += 1) { var fieldApi = fields[i]._datasetteColumnFormField; if (fieldApi && fieldApi.hasChanged && fieldApi.hasChanged()) { return true; } } return false; } function confirmDiscardRowEditChanges(state) { if (!rowEditDialogHasChanges(state)) { return true; } var message = state.mode === "insert" ? "Discard this new row?" : "Discard unsaved changes to this row?"; return window.confirm(message); } function closeRowEditDialogIfConfirmed(state) { if (!state || state.isSaving) { return false; } if (!confirmDiscardRowEditChanges(state)) { return false; } state.shouldRestoreFocus = true; state.dialog.close(); return true; } function scheduleCloseRowEditDialogIfConfirmed(state) { // Fix for an issue in Safari where hitting Esc would show // the confirm() prompt asking if state should be discarded // but the Esc key press would then cancel that dialog too. // Wait for keyup, then move the confirm() to a fresh timer tick. if (!state || state.isSaving || state.isClosePending) { return false; } if (!rowEditDialogHasChanges(state)) { state.shouldRestoreFocus = true; state.dialog.close(); return true; } state.isClosePending = true; var closeAfterKeyup = function () { if (!state.isClosePending) { return; } state.isClosePending = false; closeRowEditDialogIfConfirmed(state); }; var onKeyup = function (ev) { if (ev.key !== "Escape") { return; } document.removeEventListener("keyup", onKeyup, true); setTimeout(closeAfterKeyup, 0); }; document.addEventListener("keyup", onKeyup, true); return true; } function findDataRowElement(root, rowId) { var elements = root.querySelectorAll("[data-row]"); for (var i = 0; i < elements.length; i += 1) { if (elements[i].getAttribute("data-row") === rowId) { return elements[i]; } } return null; } async function fetchUpdatedRowElement(state) { if (!state.currentFragmentUrl || !state.currentRowId) { return null; } var response = await fetch(state.currentFragmentUrl, { headers: { Accept: "text/html", }, }); var html = await response.text(); if (!response.ok) { throw new Error("Could not refresh row: HTTP " + response.status); } var doc = new DOMParser().parseFromString(html, "text/html"); return findDataRowElement(doc, state.currentRowId); } function rowPathFromRowData(row, primaryKeys) { if (!row) { return null; } var keys = primaryKeys && primaryKeys.length ? primaryKeys : ["rowid"]; var bits = []; for (var i = 0; i < keys.length; i += 1) { var key = keys[i]; if (typeof row[key] === "undefined") { return null; } bits.push(tildeEncode(row[key])); } return bits.join(","); } function addInsertedRowToPage(rowElement) { var importedRow = document.importNode(rowElement, true); var firstRow = document.querySelector("[data-row]"); if (firstRow && firstRow.parentNode) { firstRow.parentNode.insertBefore(importedRow, firstRow); } else { var tbody = document.querySelector("table.rows-and-columns tbody"); if (!tbody) { return null; } tbody.appendChild(importedRow); } var zeroResults = document.querySelector(".zero-results"); if (zeroResults) { zeroResults.remove(); } return importedRow; } async function saveRowEditDialog(state) { if (state.isLoading || state.isSaving || !state.hasLoaded) { return; } clearRowEditDialogError(state); setRowEditDialogSaving(state, true); try { var url = state.mode === "insert" ? state.currentInsertUrl : state.currentUpdateUrl; if (!url) { throw new Error( state.mode === "insert" ? "Could not find the row insert URL" : "Could not find the row update URL", ); } var formValues = collectRowFormValues(state); if (state.mode === "edit" && !Object.keys(formValues).length) { state.shouldRestoreFocus = true; hideRowMutationStatus(); state.dialog.close(); return; } var payload = state.mode === "insert" ? { row: formValues, return: true } : { update: formValues, return: true }; var response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(payload), }); var data = null; try { data = await response.json(); } catch (_error) { data = null; } if (!response.ok || (data && data.ok === false)) { throw rowMutationRequestError(response, data); } if (state.mode === "insert") { var insertData = tableInsertData() || {}; var insertedRowData = data && data.rows && data.rows.length ? data.rows[0] : null; var insertedRowId = rowPathFromRowData( insertedRowData, insertData.primaryKeys || [], ); state.shouldRestoreFocus = false; if (!insertedRowId) { state.dialog.close(); var missingIdStatus = showRowMutationStatus( state.manager, "Inserted row. Refresh the page to see it.", false, ); missingIdStatus.focus(); return; } state.currentRowId = insertedRowId; state.currentFragmentUrl = rowFragmentUrlById(insertedRowId); var insertedRow = null; try { insertedRow = await fetchUpdatedRowElement(state); } catch (_error) { state.dialog.close(); var refreshFailedStatus = showRowMutationStatus( state.manager, "Inserted row, but could not refresh the table row. Refresh the page to see it.", true, ); refreshFailedStatus.focus(); return; } if (insertedRow) { var insertedStatusMessage = insertedRowStatusMessage( tildeDecode(insertedRowId), rowTitleLabel(insertedRow), ); var addedRow = addInsertedRowToPage(insertedRow); state.dialog.close(); showRowMutationStatus(state.manager, insertedStatusMessage, false); if (addedRow) { var insertedFocusTarget = addedRow.querySelector('button[data-row-action="edit"]') || addedRow; insertedFocusTarget.focus(); } } else { state.dialog.close(); var filteredStatus = showRowMutationStatus( state.manager, "Inserted row. It does not match the current filters.", false, ); filteredStatus.focus(); } return; } if (isRowPage()) { state.shouldRestoreFocus = false; state.dialog.close(); location.reload(); return; } var updatedRow = await fetchUpdatedRowElement(state); var focusTarget = null; if (updatedRow && state.currentRow && document.contains(state.currentRow)) { var importedRow = document.importNode(updatedRow, true); state.currentRow.replaceWith(importedRow); showRowMutationStatus( state.manager, state.currentPkPath ? "Updated row " + state.currentPkPath + "." : "Updated row.", false, ); focusTarget = importedRow.querySelector('button[data-row-action="edit"]') || importedRow; } else if (state.currentRow && document.contains(state.currentRow)) { focusTarget = nextRowActionFocusTarget(state.currentRow, "edit") || ensureRowMutationStatus(state.manager); state.currentRow.remove(); showRowMutationStatus( state.manager, state.currentPkPath ? "Updated row " + state.currentPkPath + ". It no longer matches the current filters." : "Updated row. It no longer matches the current filters.", false, ); } state.shouldRestoreFocus = false; state.dialog.close(); if (focusTarget && document.contains(focusTarget)) { focusTarget.focus(); } } catch (error) { setRowEditDialogSaving(state, false); showRowEditDialogError(state, error.message || "Could not save row"); } } function renderRowEditFields(state, data) { var row = data.rows && data.rows.length ? data.rows[0] : null; var columns = data.columns || (row ? Object.keys(row) : []); var primaryKeys = data.primary_keys || []; var columnTypes = data.column_types || {}; destroyRowEditFields(state); columns.forEach(function (column, index) { state.fields.appendChild( createRowEditField( column, row ? row[column] : null, primaryKeys.indexOf(column) !== -1, columnTypes[column], index, { autocompleteUrl: foreignKeyAutocompleteUrl(column), dialog: state.dialog, form: state.form, manager: state.manager, mode: state.mode, primaryKeyReadonly: true, }, ), ); }); state.hasLoaded = true; updateRowEditDialogButtons(state); if (!focusFirstRowEditControl(state, { skipReadonly: true })) { focusFirstRowEditControl(state) || state.cancelButton.focus(); } } function renderRowInsertFields(state, data) { var columns = data.columns || []; destroyRowEditFields(state); columns.forEach(function (column, index) { state.fields.appendChild( createRowEditField( column.name, "", !!column.is_pk, column.column_type, index, { autocompleteUrl: foreignKeyAutocompleteUrl(column.name), dialog: state.dialog, form: state.form, defaultExpression: column.default, manager: state.manager, mode: state.mode, notnull: column.notnull, primaryKeyReadonly: false, sqliteType: column.sqlite_type, useSqliteDefault: column.default !== null, valueKind: column.value_kind, }, ), ); }); if (!columns.length) { var emptyMessage = document.createElement("p"); emptyMessage.className = "row-edit-empty"; emptyMessage.textContent = "This row will use the table defaults."; state.fields.appendChild(emptyMessage); } state.hasLoaded = true; updateRowEditDialogButtons(state); var firstDefaultButton = state.fields.querySelector( ".row-edit-default-set-value", ); if (firstDefaultButton) { firstDefaultButton.focus(); } else { focusFirstRowEditControl(state, { skipReadonly: true }) || state.saveButton.focus(); } } function setRowDialogTitle(title, text, codeText, labelText) { title.textContent = ""; var action = document.createElement("span"); action.className = "row-dialog-action"; action.textContent = text; title.appendChild(action); if (!codeText) { return; } title.appendChild(document.createTextNode(" ")); var code = document.createElement("code"); code.textContent = codeText; title.appendChild(code); if (labelText && labelText !== codeText) { title.appendChild(document.createTextNode(" ")); var label = document.createElement("span"); label.className = "row-dialog-label"; label.textContent = labelText; title.appendChild(label); } } function ensureRowEditDialog(manager) { if (rowEditDialogState) { return rowEditDialogState; } if (!window.HTMLDialogElement) { 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 = `

Loading row...

`; document.body.appendChild(dialog); rowEditDialogState = { dialog: dialog, form: dialog.querySelector(".row-edit-form"), title: dialog.querySelector(".modal-title"), 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, currentInsertUrl: null, currentUpdateUrl: null, currentFragmentUrl: null, mode: "edit", loadId: 0, manager: manager, isLoading: false, isSaving: false, isClosePending: false, hasLoaded: false, shouldRestoreFocus: true, }; rowEditDialogState.form.addEventListener("submit", function (ev) { ev.preventDefault(); saveRowEditDialog(rowEditDialogState); }); rowEditDialogState.cancelButton.addEventListener("click", function () { if (!rowEditDialogState.isSaving) { rowEditDialogState.shouldRestoreFocus = true; dialog.close(); } }); dialog.addEventListener("click", function (ev) { if (ev.target === dialog) { closeRowEditDialogIfConfirmed(rowEditDialogState); } }); 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 () { var state = rowEditDialogState; state.loadId += 1; state.isClosePending = false; clearRowEditDialogError(state); state.hasLoaded = false; destroyRowEditFields(state); setRowEditDialogLoading(state, false); setRowEditDialogSaving(state, false); if ( state.shouldRestoreFocus && state.currentButton && document.contains(state.currentButton) ) { state.currentButton.focus(); } }); return rowEditDialogState; } async function openRowEditDialog(button, manager) { var row = rowElementForActionButton(button); if (!row || !row.getAttribute("data-row")) { return; } var state = ensureRowEditDialog(manager); if (!state) { return; } state.manager = manager; state.mode = "edit"; state.currentButton = button; state.currentRow = row; state.currentRowId = row.getAttribute("data-row") || ""; state.currentPkPath = rowDisplayLabel(row); state.currentInsertUrl = null; state.currentUpdateUrl = rowUpdateUrl(row); state.currentFragmentUrl = rowFragmentUrl(row); if (state.currentUpdateUrl) { state.form.action = new URL( state.currentUpdateUrl, location.href, ).toString(); } else { state.form.removeAttribute("action"); } state.shouldRestoreFocus = true; state.hasLoaded = false; state.loadId += 1; var loadId = state.loadId; clearRowEditDialogError(state); setRowEditDialogLoading(state, true); destroyRowEditFields(state); state.dialog.removeAttribute("aria-describedby"); setRowDialogTitle( state.title, "Edit row", state.currentPkPath || "this row", rowTitleLabel(row), ); state.summary.hidden = true; state.summary.textContent = ""; if (!state.dialog.open) { state.dialog.showModal(); } state.cancelButton.focus(); try { var response = await fetch(rowJsonUrl(row), { headers: { Accept: "application/json", }, }); var data = await response.json(); if (loadId !== state.loadId) { return; } if (!response.ok || data.ok === false) { throw rowMutationRequestError(response, data); } setRowEditDialogLoading(state, false); renderRowEditFields(state, data); } catch (error) { if (loadId !== state.loadId) { return; } setRowEditDialogLoading(state, false); showRowEditDialogError(state, error.message || "Could not load row"); state.cancelButton.focus(); } } function openRowInsertDialog(button, manager) { var insertData = tableInsertData(); if (!insertData) { return; } var state = ensureRowEditDialog(manager); if (!state) { return; } 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; if (state.currentInsertUrl) { state.form.action = new URL( state.currentInsertUrl, location.href, ).toString(); } else { state.form.removeAttribute("action"); } clearRowEditDialogError(state); setRowEditDialogLoading(state, false); destroyRowEditFields(state); state.dialog.removeAttribute("aria-describedby"); setRowDialogTitle( state.title, insertData.tableName ? "Insert row into " + insertData.tableName : "Insert row", ); state.summary.hidden = true; state.summary.textContent = ""; if (!state.dialog.open) { state.dialog.showModal(); } renderRowInsertFields(state, insertData); } function initRowEditActions(manager) { if (!window.fetch || !window.HTMLDialogElement) { return; } document.addEventListener("click", function (ev) { var button = ev.target.closest('button[data-row-action="edit"]'); if (!button) { return; } ev.preventDefault(); openRowEditDialog(button, manager); }); } function initRowInsertActions(manager) { if (!window.fetch || !window.HTMLDialogElement || !tableInsertData()) { return; } document.addEventListener("click", function (ev) { var button = ev.target.closest('button[data-table-action="insert-row"]'); if (!button) { return; } ev.preventDefault(); openRowInsertDialog(button, manager); }); } document.addEventListener("datasette_init", function (evt) { const { detail: manager } = evt; registerBuiltinColumnFieldPlugins(manager); initTableCreateActions(manager); initTableAlterActions(manager); initRowInsertActions(manager); initRowEditActions(manager); initRowDeleteActions(manager); });