Version 2, ready for beta
This commit was merged in pull request #1.
This commit is contained in:
322
src/admin-theme/wwwroot/admin.js
Normal file
322
src/admin-theme/wwwroot/admin.js
Normal file
@@ -0,0 +1,322 @@
|
||||
const Admin = {
|
||||
/** The next index for a metadata item */
|
||||
nextMetaIndex : 0,
|
||||
|
||||
/** The next index for a permalink */
|
||||
nextPermalink : 0,
|
||||
|
||||
/**
|
||||
* Set the next meta item index
|
||||
* @param idx The index to set
|
||||
*/
|
||||
setNextMetaIndex(idx) {
|
||||
this.nextMetaIndex = idx
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the next permalink index
|
||||
* @param idx The index to set
|
||||
*/
|
||||
setPermalinkIndex(idx) {
|
||||
this.nextPermalink = idx
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a metadata remove button
|
||||
* @returns {HTMLDivElement} The column with the remove button
|
||||
*/
|
||||
createMetaRemoveColumn() {
|
||||
const removeBtn = document.createElement("button")
|
||||
removeBtn.type = "button"
|
||||
removeBtn.className = "btn btn-sm btn-danger"
|
||||
removeBtn.innerHTML = "−"
|
||||
removeBtn.setAttribute("onclick", `Admin.removeMetaItem(${this.nextMetaIndex})`)
|
||||
|
||||
const removeCol = document.createElement("div")
|
||||
removeCol.className = "col-1 text-center align-self-center"
|
||||
removeCol.appendChild(removeBtn)
|
||||
|
||||
return removeCol
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a metadata name field
|
||||
* @returns {HTMLInputElement} The name input element
|
||||
*/
|
||||
createMetaNameField() {
|
||||
const nameField = document.createElement("input")
|
||||
|
||||
nameField.type = "text"
|
||||
nameField.name = "metaNames"
|
||||
nameField.id = `metaNames_${this.nextMetaIndex}`
|
||||
nameField.className = "form-control"
|
||||
nameField.placeholder = "Name"
|
||||
|
||||
return nameField
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a metadata name column using the given name input field
|
||||
* @param {HTMLInputElement} field The name field for the column
|
||||
* @returns {HTMLDivElement} The name column
|
||||
*/
|
||||
createMetaNameColumn(field) {
|
||||
const nameLabel = document.createElement("label")
|
||||
nameLabel.htmlFor = field.id
|
||||
nameLabel.innerText = field.placeholder
|
||||
|
||||
const nameFloat = document.createElement("div")
|
||||
nameFloat.className = "form-floating"
|
||||
nameFloat.appendChild(field)
|
||||
nameFloat.appendChild(nameLabel)
|
||||
|
||||
const nameCol = document.createElement("div")
|
||||
nameCol.className = "col-3"
|
||||
nameCol.appendChild(nameFloat)
|
||||
|
||||
return nameCol
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a metadata value field
|
||||
* @returns {HTMLInputElement} The metadata value field
|
||||
*/
|
||||
createMetaValueField() {
|
||||
const valueField = document.createElement("input")
|
||||
|
||||
valueField.type = "text"
|
||||
valueField.name = "metaValues"
|
||||
valueField.id = `metaValues_${this.nextMetaIndex}`
|
||||
valueField.className = "form-control"
|
||||
valueField.placeholder = "Value"
|
||||
|
||||
return valueField
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a metadata value column using the given input field
|
||||
* @param {HTMLInputElement} field The metadata value input field
|
||||
* @param {string|undefined} hintText Text to be added below the field
|
||||
* @returns {HTMLDivElement} The value column
|
||||
*/
|
||||
createMetaValueColumn(field, hintText) {
|
||||
const valueLabel = document.createElement("label")
|
||||
valueLabel.htmlFor = field.id
|
||||
valueLabel.innerText = field.placeholder
|
||||
|
||||
const valueFloat = document.createElement("div")
|
||||
valueFloat.className = "form-floating"
|
||||
valueFloat.appendChild(field)
|
||||
valueFloat.appendChild(valueLabel)
|
||||
|
||||
if (hintText) {
|
||||
const valueHint = document.createElement("div")
|
||||
valueHint.className = "form-text"
|
||||
valueHint.innerText = hintText
|
||||
valueFloat.appendChild(valueHint)
|
||||
}
|
||||
|
||||
const valueCol = document.createElement("div")
|
||||
valueCol.className = "col-8"
|
||||
valueCol.appendChild(valueFloat)
|
||||
|
||||
return valueCol
|
||||
},
|
||||
|
||||
/**
|
||||
* Construct and add a metadata item row
|
||||
* @param {HTMLDivElement} removeCol The column with the remove button
|
||||
* @param {HTMLDivElement} nameCol The column with the name field
|
||||
* @param {HTMLDivElement} valueCol The column with the value field
|
||||
*/
|
||||
createMetaRow(removeCol, nameCol, valueCol) {
|
||||
const newRow = document.createElement("div")
|
||||
newRow.className = "row mb-3"
|
||||
newRow.id = `meta_${this.nextMetaIndex}`
|
||||
newRow.appendChild(removeCol)
|
||||
newRow.appendChild(nameCol)
|
||||
newRow.appendChild(valueCol)
|
||||
|
||||
document.getElementById("metaItems").appendChild(newRow)
|
||||
this.nextMetaIndex++
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new row for metadata entry
|
||||
*/
|
||||
addMetaItem() {
|
||||
const nameField = this.createMetaNameField()
|
||||
|
||||
this.createMetaRow(
|
||||
this.createMetaRemoveColumn(),
|
||||
this.createMetaNameColumn(nameField),
|
||||
this.createMetaValueColumn(this.createMetaValueField(), undefined))
|
||||
|
||||
document.getElementById(nameField.id).focus()
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new row for a permalink
|
||||
*/
|
||||
addPermalink() {
|
||||
// Remove button
|
||||
const removeBtn = document.createElement("button")
|
||||
removeBtn.type = "button"
|
||||
removeBtn.className = "btn btn-sm btn-danger"
|
||||
removeBtn.innerHTML = "−"
|
||||
removeBtn.setAttribute("onclick", `Admin.removePermalink(${this.nextPermalink})`)
|
||||
|
||||
const removeCol = document.createElement("div")
|
||||
removeCol.className = "col-1 text-center align-self-center"
|
||||
removeCol.appendChild(removeBtn)
|
||||
|
||||
// Link
|
||||
const linkField = document.createElement("input")
|
||||
linkField.type = "text"
|
||||
linkField.name = "prior"
|
||||
linkField.id = `prior_${this.nextPermalink}`
|
||||
linkField.className = "form-control"
|
||||
linkField.placeholder = "Link"
|
||||
|
||||
const linkLabel = document.createElement("label")
|
||||
linkLabel.htmlFor = linkField.id
|
||||
linkLabel.innerText = linkField.placeholder
|
||||
|
||||
const linkFloat = document.createElement("div")
|
||||
linkFloat.className = "form-floating"
|
||||
linkFloat.appendChild(linkField)
|
||||
linkFloat.appendChild(linkLabel)
|
||||
|
||||
const linkCol = document.createElement("div")
|
||||
linkCol.className = "col-11"
|
||||
linkCol.appendChild(linkFloat)
|
||||
|
||||
// Put it all together
|
||||
const newRow = document.createElement("div")
|
||||
newRow.className = "row mb-3"
|
||||
newRow.id = `meta_${this.nextPermalink}`
|
||||
newRow.appendChild(removeCol)
|
||||
newRow.appendChild(linkCol)
|
||||
|
||||
document.getElementById("permalinks").appendChild(newRow)
|
||||
document.getElementById(linkField.id).focus()
|
||||
this.nextPermalink++
|
||||
},
|
||||
|
||||
/**
|
||||
* Add podcast episode metadata fields
|
||||
*/
|
||||
addPodcastFields() {
|
||||
[ [ "episode_media_file", true, "required; relative URL will be appended to configured base media path" ],
|
||||
[ "episode_media_length", true, "required; file size in bytes" ],
|
||||
[ "episode_duration", false, "suggested; format is HH:MM:SS" ],
|
||||
[ "episode_media_type", false, "optional; blank uses podcast default" ],
|
||||
[ "episode_image", false, "optional; relative URLs are served from this web log" ],
|
||||
[ "episode_subtitle", false, "optional" ],
|
||||
[ "episode_explicit", false, "optional; blank uses podcast default"]
|
||||
].forEach(([fieldName, isRequired, hintText]) => {
|
||||
const nameField = this.createMetaNameField()
|
||||
nameField.value = fieldName
|
||||
nameField.readOnly = true
|
||||
|
||||
const valueField = this.createMetaValueField()
|
||||
valueField.required = isRequired
|
||||
|
||||
this.createMetaRow(
|
||||
this.createMetaRemoveColumn(),
|
||||
this.createMetaNameColumn(nameField),
|
||||
this.createMetaValueColumn(valueField, hintText))
|
||||
})
|
||||
document.getElementById("addPodcastFieldButton").remove()
|
||||
},
|
||||
|
||||
/**
|
||||
* Check to enable or disable podcast fields
|
||||
*/
|
||||
checkPodcast() {
|
||||
document.getElementById("podcastFields").disabled = !document.getElementById("isPodcast").checked
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the source of a custom RSS feed
|
||||
* @param source The source that was selected
|
||||
*/
|
||||
customFeedBy(source) {
|
||||
const categoryInput = document.getElementById("sourceValueCat")
|
||||
const tagInput = document.getElementById("sourceValueTag")
|
||||
if (source === "category") {
|
||||
tagInput.value = ""
|
||||
tagInput.disabled = true
|
||||
categoryInput.disabled = false
|
||||
} else {
|
||||
categoryInput.selectedIndex = -1
|
||||
categoryInput.disabled = true
|
||||
tagInput.disabled = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a metadata item
|
||||
* @param idx The index of the metadata item to remove
|
||||
*/
|
||||
removeMetaItem(idx) {
|
||||
document.getElementById(`meta_${idx}`).remove()
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a permalink
|
||||
* @param idx The index of the permalink to remove
|
||||
*/
|
||||
removePermalink(idx) {
|
||||
document.getElementById(`link_${idx}`).remove()
|
||||
},
|
||||
|
||||
/**
|
||||
* Show messages that may have come with an htmx response
|
||||
* @param messages The messages from the response
|
||||
*/
|
||||
showMessage(messages) {
|
||||
const msgs = messages.split(", ")
|
||||
msgs.forEach(msg => {
|
||||
const parts = msg.split("|||")
|
||||
if (parts.length < 2) return
|
||||
|
||||
const msgDiv = document.createElement("div")
|
||||
msgDiv.className = `alert alert-${parts[0]} alert-dismissible fade show`
|
||||
msgDiv.setAttribute("role", "alert")
|
||||
msgDiv.innerHTML = parts[1]
|
||||
|
||||
const closeBtn = document.createElement("button")
|
||||
closeBtn.type = "button"
|
||||
closeBtn.className = "btn-close"
|
||||
closeBtn.setAttribute("data-bs-dismiss", "alert")
|
||||
closeBtn.setAttribute("aria-label", "Close")
|
||||
msgDiv.appendChild(closeBtn)
|
||||
|
||||
if (parts.length === 3) {
|
||||
msgDiv.innerHTML += `<hr>${parts[2]}`
|
||||
}
|
||||
document.getElementById("msgContainer").appendChild(msgDiv)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Set all "success" alerts to close after 4 seconds
|
||||
*/
|
||||
dismissSuccesses() {
|
||||
[...document.querySelectorAll(".alert-success")].forEach(alert => {
|
||||
setTimeout(() => {
|
||||
(bootstrap.Alert.getInstance(alert) ?? new bootstrap.Alert(alert)).close()
|
||||
}, 4000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
htmx.on("htmx:afterOnLoad", function (evt) {
|
||||
const hdrs = evt.detail.xhr.getAllResponseHeaders()
|
||||
// Show messages if there were any in the response
|
||||
if (hdrs.indexOf("x-message") >= 0) {
|
||||
Admin.showMessage(evt.detail.xhr.getResponseHeader("x-message"))
|
||||
Admin.dismissSuccesses()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user