diff --git a/src/Roux/Html.hs b/src/Roux/Html.hs
index 944f28c..d6018d0 100644
--- a/src/Roux/Html.hs
+++ b/src/Roux/Html.hs
@@ -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 =
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 timerJs
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).
renderRecipe :: FilePath -> Recipe -> Html