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