feat: add inline title editing JavaScript

This commit is contained in:
2026-05-21 21:29:54 -04:00
parent 1f7b55af6f
commit 7b51c617de
+77
View File
@@ -549,6 +549,82 @@ recipeImageUploadJs =
, "})();" , "})();"
] ]
-- | Inline JavaScript for in-place recipe title editing.
titleEditJs :: Text
titleEditJs =
T.unlines
[ "(function(){"
, "'use strict';"
, "var h1 = document.querySelector('.roux-recipe-title');"
, "if (!h1) return;"
, "var originalText = '';"
, "var filenameEl = document.getElementById('roux-current-recipe');"
, "if (!filenameEl) return;"
, "var recipeFilename = filenameEl.textContent.trim();"
, ""
, "h1.addEventListener('click', function(e) {"
, " if (h1.getAttribute('contenteditable') === 'true') return;"
, " originalText = h1.textContent.trim();"
, " h1.setAttribute('contenteditable', 'true');"
, " h1.classList.add('roux-editing');"
, " var range = document.createRange();"
, " var sel = window.getSelection();"
, " range.selectNodeContents(h1);"
, " range.collapse(false);"
, " sel.removeAllRanges();"
, " sel.addRange(range);"
, "});"
, ""
, "h1.addEventListener('keydown', function(e) {"
, " if (e.key === 'Enter' && !e.shiftKey) {"
, " e.preventDefault();"
, " h1.blur();"
, " } else if (e.key === 'Escape') {"
, " e.preventDefault();"
, " h1.textContent = originalText;"
, " h1.removeAttribute('contenteditable');"
, " h1.classList.remove('roux-editing');"
, " }"
, "});"
, ""
, "h1.addEventListener('blur', function() {"
, " if (h1.getAttribute('contenteditable') !== 'true') return;"
, " var newText = h1.textContent.trim();"
, " if (newText === originalText || !newText) {"
, " h1.textContent = originalText;"
, " h1.removeAttribute('contenteditable');"
, " h1.classList.remove('roux-editing');"
, " return;"
, " }"
, " h1.removeAttribute('contenteditable');"
, " h1.classList.remove('roux-editing');"
, " var xhr = new XMLHttpRequest();"
, " xhr.open('PATCH', '/recipes/' + encodeURIComponent(recipeFilename) + '/title', true);"
, " xhr.setRequestHeader('Content-Type', 'application/json');"
, " xhr.onload = function() {"
, " if (xhr.status === 200) {"
, " try {"
, " var resp = JSON.parse(xhr.responseText);"
, " h1.textContent = resp.title || newText;"
, " } catch(e) {"
, " h1.textContent = newText;"
, " }"
, " } else {"
, " h1.textContent = originalText;"
, " h1.classList.add('roux-title-error');"
, " setTimeout(function() { h1.classList.remove('roux-title-error'); }, 2000);"
, " }"
, " };"
, " xhr.onerror = function() {"
, " h1.textContent = originalText;"
, " h1.classList.add('roux-title-error');"
, " setTimeout(function() { h1.classList.remove('roux-title-error'); }, 2000);"
, " };"
, " xhr.send(JSON.stringify({ title: newText }));"
, "});"
, "})();"
]
sseIndexJs :: Text sseIndexJs :: Text
sseIndexJs = sseIndexJs =
T.unlines T.unlines
@@ -713,6 +789,7 @@ recipePage info =
H.script ! A.type_ "text/javascript" $ H.preEscapedText sseRecipeJs H.script ! A.type_ "text/javascript" $ H.preEscapedText sseRecipeJs
H.script ! A.type_ "text/javascript" $ H.preEscapedText timerJs H.script ! A.type_ "text/javascript" $ H.preEscapedText timerJs
H.script ! A.type_ "text/javascript" $ H.preEscapedText recipeImageUploadJs H.script ! A.type_ "text/javascript" $ H.preEscapedText recipeImageUploadJs
H.script ! A.type_ "text/javascript" $ H.preEscapedText titleEditJs
-- | Render the body of a recipe page (no page shell). -- | Render the body of a recipe page (no page shell).
renderRecipe :: FilePath -> Recipe -> Html renderRecipe :: FilePath -> Recipe -> Html