diff --git a/js/blogtini.js b/js/blogtini.js
index 28f7404..88095d5 100644
--- a/js/blogtini.js
+++ b/js/blogtini.js
@@ -744,8 +744,6 @@ function add_interactivity() {
})
})
- import('./staticman.js')
-
search_setup(STORAGE.docs, cfg)
}
diff --git a/js/staticman.js b/js/staticman.js
deleted file mode 100644
index da8eea1..0000000
--- a/js/staticman.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import { cfg } from './blogtini.js'
-
-function find_form() {
- return document.querySelector('bt-post-full')?.shadowRoot?.querySelector('bt-comments')?.shadowRoot?.querySelector('.new-comment')
-}
-
-function comment_setup() {
- const form = find_form()
- if (!form) {
- // eslint-disable-next-line no-console
- console.log('comment form not found')
- return
- }
-
- form.querySelector('#comment-form-submit').addEventListener('click', () => {
- form.classList.add('loading')
- form.querySelector('#comment-form-submit').classList.add('hidden') // hide "submit"
- form.querySelector('#comment-form-submitted').classList.remove('hidden') // show "submitted"
-
- // Construct form action URL form JS to avoid spam
- const { api } = cfg.staticman
- const gitProvider = cfg.git_provider
- const username = cfg.user
- const { repo } = cfg
- const { branch } = cfg.staticman
- const url = ['https:/', api, 'v3/entry', gitProvider, username, repo, branch, 'comments'].join('/')
-
- // Convert form fields to a JSON-friendly string
- const formObj = Object.fromEntries(new FormData(form))
- const xhrObj = { fields: {}, options: {} }
- Object.entries(formObj).forEach(([key, value]) => {
- const a = key.indexOf('[')
- const b = key.indexOf('reCaptcha')
- if (a === -1) { // key = "g-recaptcha-response"
- xhrObj[key] = value
- } else if (a === 6 || (a === 7 && b === -1)) { // key = "fields[*]", "options[*]"
- xhrObj[key.slice(0, a)][key.slice(a + 1, -1)] = value
- } else { // key = "options[reCaptcha][*]"
- // define xhrObj.options.reCaptcha if it doesn't exist
- xhrObj.options.reCaptcha = xhrObj.options.reCaptcha || {}
- xhrObj.options.reCaptcha[key.slice(b + 11, -1)] = value
- }
- })
- const formData = JSON.stringify(xhrObj) // some API don't accept FormData objects
-
- const xhr = new XMLHttpRequest()
- xhr.open('POST', url)
- xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
- xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
- xhr.onreadystatechange = function () {
- if (xhr.readyState === XMLHttpRequest.DONE) {
- const { status } = xhr
- if (status >= 200 && status < 400) {
- // eslint-disable-next-line no-use-before-define
- showAlert(form, 'success')
- // eslint-disable-next-line no-use-before-define
- setTimeout(() => { clearForm() }, 3000) // display success message for 3s
- form.classList.remove('loading')
- } else {
- // eslint-disable-next-line no-use-before-define
- showAlert(form, 'failed')
- form.classList.remove('loading')
- }
- }
- }
- xhr.send(formData)
- })
-
- // record reply target when one of the "reply" buttons is pressed
- for (const e of document.querySelector('bt-post-full').shadowRoot.querySelector('bt-comments').shadowRoot.querySelectorAll('bt-comment')) {
- e.shadowRoot.querySelector('.comment-reply-btn').addEventListener('click', (evt) => {
- // eslint-disable-next-line no-use-before-define
- resetReplyTarget()
- let cmt = evt.currentTarget
- while (!cmt.matches('.comment')) // find the comment containing the clicked "reply" button
- cmt = cmt.parentNode
- form.querySelector('input[name="fields[replyID]"]').value = cmt.getAttribute('id')
- const replyName = cmt.querySelector('.comment-author').innerText
-
- // display reply name
- form.querySelector('.reply-notice').classList.remove('hidden')
- form.querySelector('.reply-name').innerText = replyName
- })
- }
-
- // handle removal of reply target when '×' is pressed
- // eslint-disable-next-line no-use-before-define
- form.querySelector('.reply-close-btn').addEventListener('click', resetReplyTarget)
-
- // clear form when reset button is clicked
- // eslint-disable-next-line no-use-before-define
- form.querySelector('#comment-form-reset').addEventListener('click', clearForm)
-}
-
-
-function showAlert(form, msg) {
- if (msg === 'success') {
- form.querySelector('.submit-success').classList.remove('hidden') // show submit success message
- form.querySelector('.submit-failed').classList.add('hidden') // hide submit failed message
- } else {
- form.querySelector('.submit-success').classList.add('hidden') // hide submit success message
- form.querySelector('.submit-failed').classList.remove('hidden') // show submit failed message
- }
- form.querySelector('#comment-form-submit').classList.remove('hidden') // show "submit"
- form.querySelector('#comment-form-submitted').classList.add('hidden') // hide "submitted"
-}
-
-function resetReplyTarget() {
- const form = find_form()
- form.querySelector('.reply-notice .reply-name').innerText = ''
- form.querySelector('.reply-notice').classList.add('hidden') // hide reply target display
- // empty all hidden fields whose name starts from "reply"
- // eslint-disable-next-line no-return-assign
- Array.from(form.elements).filter((e) => e.name.indexOf('fields[reply') === 0).forEach((e) => e.value = '')
-}
-
-// empty all text & hidden fields but not options
-function clearForm() {
- resetReplyTarget()
- const form = find_form()
- form.querySelector('.submit-success').classList.add('hidden') // hide submission status
- form.querySelector('.submit-failed').classList.add('hidden') // hide submission status
-}
-
-comment_setup()
-
-export default comment_setup
diff --git a/theme/future-imperfect/bt-comment.js b/theme/future-imperfect/bt-comment.js
index d510e89..0cbf7b8 100644
--- a/theme/future-imperfect/bt-comment.js
+++ b/theme/future-imperfect/bt-comment.js
@@ -36,7 +36,7 @@ customElements.define('bt-comment', class extends LitElement {
-
+
-
`
}
+ // Dispatches a custom event (to parent component) when the reply button is clicked
+ reply_clicked() {
+ this.dispatchEvent(new CustomEvent('bt-comment-reply-clicked', {
+ detail: { id: this.id, author: this.name }, // Pass up event details
+ bubbles: true, // Make sure the event bubbles up to the parent
+ composed: true, // Allow the event to cross shadow DOM boundaries
+ }))
+ }
+
static get styles() {
return [
css_post(), // xxxcc figure these out
diff --git a/theme/future-imperfect/bt-comments.js b/theme/future-imperfect/bt-comments.js
index e13f765..c11a514 100644
--- a/theme/future-imperfect/bt-comments.js
+++ b/theme/future-imperfect/bt-comments.js
@@ -1,5 +1,7 @@
import { LitElement, html, css } from 'https://esm.ext.archive.org/lit@3.2.1'
-import { fetcher, state, cssify } from '../../js/blogtini.js'
+import {
+ fetcher, state, cssify, cfg,
+} from '../../js/blogtini.js'
import {
css_dark, css_headers, css_buttons, css_post, css_forms,
} from './index.js'
@@ -14,19 +16,11 @@ customElements.define('bt-comments', class extends LitElement {
}
render() {
- if (typeof this.comments === 'undefined') {
- // use a default base in case url is relative (pathname) only
- this.comments_get(this.entryid).then(
- (comments) => {
- this.comments = comments
- },
- )
- }
+ if (typeof this.comments === 'undefined')
+ this.comments_get()
- if (this.comments && this.comments.length) {
+ if (this.comments && this.comments.length)
this.comments_insert()
- import('../../js/staticman.js') // xxxxxx move into this class
- }
return html`
@@ -36,7 +30,7 @@ customElements.define('bt-comments', class extends LitElement {
`
}
+ /**
+ * handles evnets from children
+ * @param {*} event
+ */
+ events_handler(event) {
+ if (event.type === 'bt-comment-reply-clicked') {
+ // console.log(this, `comment reply event: ${event.detail.id} by ${event.detail.author}`)
+
+ const form = this.find_form()
+ this.resetReplyTarget()
+ form.querySelector('input[name="fields[replyID]"]').value = event.detail.id
+
+ // display reply name
+ form.querySelector('.reply-notice').classList.remove('hidden')
+ form.querySelector('.reply-name').innerText = event.detail.author
+ }
+ }
+
+ /**
+ * Dynamically attaches event listener for all bt-comment events
+ */
+ connectedCallback() {
+ super.connectedCallback()
+ this.addEventListener('bt-comment-reply-clicked', this.events_handler)
+ }
+
+ /**
+ * Cleans up event listener when the component is removed
+ */
+ disconnectedCallback() {
+ super.disconnectedCallback()
+ this.removeEventListener('bt-comment-reply-clicked', this.events_handler)
+ }
/**
* Cleverly use DOM to add (potentially nested) comment elements into a div
@@ -111,20 +138,19 @@ customElements.define('bt-comments', class extends LitElement {
}
- /**
- * @param {string} path
- */
- async comments_get(path) {
+ async comments_get() {
let posts_with_comments
try {
posts_with_comments = (await fetcher(`${state.top_dir}comments/index.txt`))?.split('\n')
/* eslint-disable-next-line no-empty */ // deno-lint-ignore no-empty
} catch {}
- if (!posts_with_comments?.includes(path))
- return []
+ if (!posts_with_comments?.includes(this.entryid)) {
+ this.comments = []
+ return
+ }
// oldest comments (or oldest in a thread) first
- return (await fetcher(`${state.top_dir}comments/${path}/index.json`))?.filter((e) => Object.keys(e).length).sort((a, b) => a.date < b.date).map((e) => {
+ this.comments = (await fetcher(`${state.top_dir}comments/${this.entryid}/index.json`))?.filter((e) => Object.keys(e).length).sort((a, b) => a.date < b.date).map((e) => {
// delete any unused keys in each comment hashmap
for (const [k, v] of Object.entries(e)) {
if (v === '' || v === undefined || v === null)
@@ -136,6 +162,91 @@ customElements.define('bt-comments', class extends LitElement {
})
}
+ find_form() {
+ return this.shadowRoot.querySelector('.new-comment')
+ }
+
+ submitted() {
+ const form = this.find_form()
+ form.classList.add('loading')
+ form.querySelector('#comment-form-submit').classList.add('hidden') // hide "submit"
+ form.querySelector('#comment-form-submitted').classList.remove('hidden') // show "submitted"
+
+ // Construct form action URL form JS to avoid spam
+ const { api } = cfg.staticman
+ const gitProvider = cfg.git_provider
+ const username = cfg.user
+ const { repo } = cfg
+ const { branch } = cfg.staticman
+ const url = ['https:/', api, 'v3/entry', gitProvider, username, repo, branch, 'comments'].join('/')
+
+ // Convert form fields to a JSON-friendly string
+ const formObj = Object.fromEntries(new FormData(form))
+ const xhrObj = { fields: {}, options: {} }
+ Object.entries(formObj).forEach(([key, value]) => {
+ const a = key.indexOf('[')
+ const b = key.indexOf('reCaptcha')
+ if (a === -1) { // key = "g-recaptcha-response"
+ xhrObj[key] = value
+ } else if (a === 6 || (a === 7 && b === -1)) { // key = "fields[*]", "options[*]"
+ xhrObj[key.slice(0, a)][key.slice(a + 1, -1)] = value
+ } else { // key = "options[reCaptcha][*]"
+ // define xhrObj.options.reCaptcha if it doesn't exist
+ xhrObj.options.reCaptcha = xhrObj.options.reCaptcha || {}
+ xhrObj.options.reCaptcha[key.slice(b + 11, -1)] = value
+ }
+ })
+ const formData = JSON.stringify(xhrObj) // some API don't accept FormData objects
+
+ const xhr = new XMLHttpRequest()
+ xhr.open('POST', url)
+ xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ const { status } = xhr
+ if (status >= 200 && status < 400) {
+ this.showAlert(form, 'success')
+ setTimeout(() => { this.clearForm() }, 3000) // display success message for 3s
+ form.classList.remove('loading')
+ } else {
+ this.showAlert(form, 'failed')
+ form.classList.remove('loading')
+ }
+ }
+ }
+ xhr.send(formData)
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ showAlert(form, msg) {
+ if (msg === 'success') {
+ form.querySelector('.submit-success').classList.remove('hidden') // show submit success message
+ form.querySelector('.submit-failed').classList.add('hidden') // hide submit failed message
+ } else {
+ form.querySelector('.submit-success').classList.add('hidden') // hide submit success message
+ form.querySelector('.submit-failed').classList.remove('hidden') // show submit failed message
+ }
+ form.querySelector('#comment-form-submit').classList.remove('hidden') // show "submit"
+ form.querySelector('#comment-form-submitted').classList.add('hidden') // hide "submitted"
+ }
+
+ resetReplyTarget() {
+ const form = this.find_form()
+ form.querySelector('.reply-notice .reply-name').innerText = ''
+ form.querySelector('.reply-notice').classList.add('hidden') // hide reply target display
+ // empty all hidden fields whose name starts from "reply"
+ // eslint-disable-next-line no-return-assign
+ Array.from(form.elements).filter((e) => e.name.indexOf('fields[reply') === 0).forEach((e) => e.value = '')
+ }
+
+ // empty all text & hidden fields but not options
+ clearForm() {
+ this.resetReplyTarget()
+ const form = this.find_form()
+ form.querySelector('.submit-success').classList.add('hidden') // hide submission status
+ form.querySelector('.submit-failed').classList.add('hidden') // hide submission status
+ }
static get styles() {
return [
diff --git a/theme/future-imperfect/bt-post-full.js b/theme/future-imperfect/bt-post-full.js
index 2d51a7b..b1956c6 100644
--- a/theme/future-imperfect/bt-post-full.js
+++ b/theme/future-imperfect/bt-post-full.js
@@ -23,6 +23,7 @@ customElements.define('bt-post-full', class extends LitElement {
const post = url2post(this.url)
const body = markdown_to_html(post.body_raw)
+ // use a default base in case url is relative (pathname) only
const comments_entryid = new URL(post.url, 'https://blogtini.com').pathname.replace(/^\/+/, '').replace(/\/+$/, '') // xxx
const socnet_share = share_buttons(post)