/* joplock client application — extracted from templates.js */ /* Server config is passed via window._joplockConfig set inline before this script loads */ (function(){ var _cfg=window._joplockConfig||{}; var _dbg=_cfg.debug||false; function _log(){if(!_dbg)return;var a=Array.prototype.slice.call(arguments);a.unshift('[joplock]');console.log.apply(console,a)} if('serviceWorker' in navigator) navigator.serviceWorker.register('/service-worker.js').catch(function(){}); // If the browser restores this page from bfcache, force a reload so the server can validate the session window.addEventListener('pageshow',function(e){if(e.persisted){_log('bfcache restore detected, reloading');window.location.reload()}}); function syncThemeColor(){var meta=document.querySelector('meta[name="theme-color"]');if(!meta)return;var color=getComputedStyle(document.body).getPropertyValue('--theme-color').trim();if(color)meta.setAttribute('content',color)} function setTheme(t){document.body.classList.forEach(function(c){if(c.startsWith('theme-'))document.body.classList.remove(c)});document.body.classList.add('theme-'+t);syncThemeColor();localStorage.setItem('joplock-theme',t);fetch('/api/web/theme',{method:'PUT',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'theme='+encodeURIComponent(t)}).catch(function(){})} // --- Encryption (vault-based client-side E2EE) --- var PBKDF2_ITERATIONS=210000; var ENCRYPTION_VERSION=2; var ENCRYPTED_START=''; var ENCRYPTED_END=''; var ENCRYPTED_WRAPPER_HEAD='> **\uD83D\uDD12 This note is encrypted by Joplock**\n>\n> This note\'s content is encrypted and can only be viewed in Joplock.\n> Do not edit the data below \u2014 editing will permanently corrupt the encrypted content.\n\n'; var SVG_LOCK_CLOSED=''; var SVG_LOCK_OPEN=''; function isEncryptedBody(body){return typeof body==='string'&&body.indexOf(ENCRYPTED_START)>=0} function extractCiphertext(body){ var start=body.indexOf(ENCRYPTED_START); var end=body.indexOf(ENCRYPTED_END); if(start<0||end<0)return null; var json=body.slice(start+ENCRYPTED_START.length,end).trim(); try{var obj=JSON.parse(json);return obj.joplock_encrypted?json:null}catch(e){return null} } function wrapCiphertext(jsonString){ return ENCRYPTED_WRAPPER_HEAD+ENCRYPTED_START+'\n'+jsonString+'\n'+ENCRYPTED_END+'\n'; } function _b64Encode(buf){return btoa(String.fromCharCode.apply(null,new Uint8Array(buf)))} function _b64Decode(str){var bin=atob(str);var buf=new Uint8Array(bin.length);for(var i=0;i timestamp var _autoLockTimer=null; function touchVaultActivity(folderId){if(folderId)_autoLockActivity[folderId]=Date.now()} function startAutoLockTimer(){ if(_autoLockTimer||_autoLockMinutes<=0)return; _autoLockTimer=setInterval(function(){ if(_autoLockMinutes<=0)return; var now=Date.now(); var timeoutMs=_autoLockMinutes*60*1000; try{ var toRemove=[]; for(var i=0;itimeoutMs){ clearVaultKey(folderId); delete _autoLockActivity[folderId]; _log('auto-lock: locked vault',folderId); // If currently open note belongs to this vault, close it var form=activeEditorForm(); if(form){ var noteBodyVault=form.dataset.vaultId||getBodyVaultId((getTA()||{}).value||''); if(noteBodyVault===folderId){ var panel=form.closest('#editor-panel')||document.getElementById('editor-panel'); if(panel)panel.innerHTML='
Select a note
'; } } } }); }catch(e){} },30000); } // Do not clear vault keys on ordinary same-tab navigation between notes/pages. // They are session-scoped and are explicitly cleared on logout/login cleanup. ; var _defaultNoteOpenMode=_cfg.noteOpenMode||'preview'; var _mobileStartup=_cfg.mobileStartup||null; var _uiMode=_cfg.uiMode||'auto'; var _mobileShellMaxWidth=768; function viewportWidth(){return Math.max(window.innerWidth||0,document.documentElement&&document.documentElement.clientWidth||0)} var _lastViewportWidth=viewportWidth(); var _resizeTimer=null; var _traceKey='joplock-debug-trace'; function isMobileShellMode(){if(_uiMode==='mobile')return true;if(_uiMode==='desktop')return false;return viewportWidth()<=_mobileShellMaxWidth} function isDesktopMode(){return !isMobileShellMode()} function _trace(){if(!_dbg)return;try{var line='['+new Date().toISOString().slice(11,23)+'] '+Array.prototype.slice.call(arguments).map(function(v){return typeof v==='string'?v:JSON.stringify(v)}).join(' ');var arr=JSON.parse(sessionStorage.getItem(_traceKey)||'[]');arr.push(line);if(arr.length>80)arr=arr.slice(arr.length-80);sessionStorage.setItem(_traceKey,JSON.stringify(arr));console.log('[trace]',line)}catch(_e){}} function _traceDump(){if(!_dbg)return;try{var arr=JSON.parse(sessionStorage.getItem(_traceKey)||'[]');for(var i=0;iEdited','Edited');_log('markEdited')} function renderNoteMeta(){var src=document.getElementById('note-meta');var mobileBody=document.getElementById('mobile-editor-body');if(isMobileShellMode()&&mobileBody){src=mobileBody.querySelector('#note-meta')||src}var target;if(isMobileShellMode()){target=src}else{target=document.getElementById('status-note-meta');if(src&&target){target.setAttribute('data-created-time',src.getAttribute('data-created-time')||'0');target.setAttribute('data-updated-time',src.getAttribute('data-updated-time')||'0')}}if(!target)return;var c=Number(target.getAttribute('data-created-time')||0),u=Number(target.getAttribute('data-updated-time')||0);if(!c&&!u){target.textContent='';return}var months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];var fmt=function(ts){if(!ts)return '';var d=new Date(ts);return String(d.getDate()).padStart(2,'0')+'-'+months[d.getMonth()]+'-'+String(d.getFullYear()).slice(-2)};target.textContent='Created '+fmt(c)+' | Edited '+fmt(u)} var _folderMenuState={id:'',title:''}; function closeFolderContextMenu(){var menu=document.getElementById('folder-context-menu');if(menu)menu.hidden=true} function openFolderContextMenu(event,id,title){if(event){event.preventDefault();event.stopPropagation()}var menu=document.getElementById('folder-context-menu');if(!menu)return false;_folderMenuState={id:id,title:title};menu.hidden=false;menu.style.left=(event.clientX||16)+'px';menu.style.top=(event.clientY||16)+'px';return false} function closeFolderModal(){var modal=document.getElementById('folder-modal');var backdrop=document.getElementById('folder-modal-backdrop');if(modal)modal.hidden=true;if(backdrop)backdrop.hidden=true} function openFolderModal(){var input=document.getElementById('folder-edit-title');var modal=document.getElementById('folder-modal');var backdrop=document.getElementById('folder-modal-backdrop');if(modal&&modal.parentNode!==document.body)document.body.appendChild(modal);if(backdrop&&backdrop.parentNode!==document.body)document.body.appendChild(backdrop);if(input)input.value=_folderMenuState.title||'';if(modal)modal.hidden=false;if(backdrop)backdrop.hidden=false;closeFolderContextMenu();if(input)input.focus()} function editFolderFromMenu(){if(!_folderMenuState.id)return;openFolderModal()} function deleteFolderFromMenu(){if(!_folderMenuState.id)return;closeFolderContextMenu();if(confirm('Delete notebook "'+(_folderMenuState.title||'Untitled')+'"?')){htmx.ajax('DELETE','/fragments/folders/'+encodeURIComponent(_folderMenuState.id),{target:'#nav-panel',swap:'innerHTML'})}} function submitFolderEdit(event){if(event)event.preventDefault();var input=document.getElementById('folder-edit-title');var title=input?input.value.trim():'';if(!_folderMenuState.id||!title)return false;var folderId=_folderMenuState.id;closeFolderModal();if(window.isMobileShellMode&&window.isMobileShellMode()){fetch('/fragments/folders/'+encodeURIComponent(folderId),{method:'PUT',headers:{'Content-Type':'application/x-www-form-urlencoded','hx-request':'true'},body:'title='+encodeURIComponent(title)}).then(function(){htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'});var notesTitle=document.getElementById('mobile-notes-title');if(notesTitle&¬esTitle.textContent===_folderMenuState.title)notesTitle.textContent=title})}else{htmx.ajax('PUT','/fragments/folders/'+encodeURIComponent(folderId),{target:'#nav-panel',swap:'innerHTML',values:{title:title}})}return false} function navFolderState(){try{return JSON.parse(localStorage.getItem('joplock-nav-folders')||'{}')}catch(e){return {}}} function saveNavFolderState(s){localStorage.setItem('joplock-nav-folders',JSON.stringify(s))} function toggleNavFolder(id,force){ var el=document.querySelector('.nav-folder[data-folder-id="'+id.replace(/"/g,'\\"')+'"]'); if(!el)return; var collapsed=force===undefined?!el.classList.contains('collapsed'):!force; el.classList.toggle('collapsed',collapsed); var s=navFolderState();s[id]=collapsed?'0':'1';saveNavFolderState(s); // Lazy-load notes on first expand if(!collapsed){ var notesDiv=el.querySelector('.nav-folder-notes[data-folder-id]'); if(notesDiv&&!notesDiv.getAttribute('data-loaded')){ notesDiv.setAttribute('data-loaded','1'); var folderId=notesDiv.getAttribute('data-folder-id'); htmx.ajax('GET','/fragments/folder-notes?folderId='+encodeURIComponent(folderId),{target:notesDiv,swap:'innerHTML'}); } } } function getTA(){return queryActiveEditor('#note-body')} function getPV(){var pv=queryActiveEditor('#note-preview');return pv&&pv.style.display!=='none'?pv:null} function isMarkdownVisible(){var host=queryActiveEditor('#cm-host');return !!(host&&host.style.display!=='none')} function inMobileEditor(){var form=activeEditorForm();return !!(form&&form.closest&&form.closest('#mobile-editor-body'))} var _cmView=null; function getCM(){return _cmView} function cmVal(){return _cmView?_cmView.state.doc.toString():''} function cmSetVal(v){if(!_cmView)return;_cmView.dispatch({changes:{from:0,to:_cmView.state.doc.length,insert:v}})} function cmSyncToTA(){var ta=getTA();if(ta&&_cmView)ta.value=cmVal()} function initCM(host,content){ if(_cmView){_cmView.destroy();_cmView=null} var C=window.CM; var joplockTheme=C.EditorView.theme({ '&':{height:'100%',fontSize:'14px'}, '.cm-scroller':{overflow:'auto',fontFamily:'"Cascadia Mono",monospace',lineHeight:'1.65'}, '.cm-content':{padding:'16px 20px',caretColor:'var(--accent)'}, '.cm-gutters':{display:'none'}, '.cm-search.cm-panel':{display:'none'}, '.cm-searchMatch':{backgroundColor:'#ffe066',color:'#111',borderRadius:'2px'}, '.cm-searchMatch.cm-searchMatch-selected':{backgroundColor:'#ff9800',color:'#111',borderRadius:'2px'}, '.cm-selectionBackground':{backgroundColor:'color-mix(in srgb, var(--accent) 25%, transparent) !important'}, '&.cm-focused .cm-selectionBackground':{backgroundColor:'color-mix(in srgb, var(--accent) 30%, transparent) !important'}, '.cm-cursor':{borderLeftColor:'var(--accent)'}, '.cm-matchingBracket':{backgroundColor:'color-mix(in srgb, var(--accent) 25%, transparent)'} }); var joplockHighlight=C.HighlightStyle.define([ {tag:C.tags.heading1,fontWeight:'bold',fontSize:'1.6em',color:'var(--text-heading)'}, {tag:C.tags.heading2,fontWeight:'bold',fontSize:'1.35em',color:'var(--text-heading)'}, {tag:C.tags.heading3,fontWeight:'bold',fontSize:'1.15em',color:'var(--text-heading)'}, {tag:[C.tags.heading4,C.tags.heading5,C.tags.heading6],fontWeight:'bold',color:'var(--text-heading)'}, {tag:C.tags.strong,fontWeight:'bold',color:'var(--text-heading)'}, {tag:C.tags.emphasis,fontStyle:'italic'}, {tag:C.tags.strikethrough,textDecoration:'line-through'}, {tag:C.tags.link,color:'var(--accent)',textDecoration:'underline'}, {tag:C.tags.url,color:'var(--accent)'}, {tag:C.tags.processingInstruction,fontFamily:'"Cascadia Mono",monospace',color:'var(--accent)'}, {tag:C.tags.monospace,fontFamily:'"Cascadia Mono",monospace'}, {tag:C.tags.meta,color:'var(--text-dim)'}, {tag:C.tags.quote,color:'var(--text-dim)',fontStyle:'italic'}, {tag:C.tags.keyword,color:'#c678dd'}, {tag:[C.tags.string,C.tags.special(C.tags.brace)],color:'#98c379'}, {tag:C.tags.number,color:'#d19a66'}, {tag:C.tags.bool,color:'#d19a66'}, {tag:[C.tags.definition(C.tags.variableName),C.tags.function(C.tags.variableName)],color:'#61afef'}, {tag:C.tags.typeName,color:'#e5c07b'}, {tag:C.tags.comment,color:'var(--text-dim)',fontStyle:'italic'}, {tag:C.tags.operator,color:'#56b6c2'}, {tag:C.tags.className,color:'#e5c07b'}, {tag:C.tags.propertyName,color:'#e06c75'}, {tag:C.tags.attributeName,color:'#d19a66'}, {tag:C.tags.attributeValue,color:'#98c379'} ]); var onUpdate=C.EditorView.updateListener.of(function(upd){ if(upd.docChanged){cmSyncToTA();var ta=getTA();if(ta)ta.dispatchEvent(new Event('input',{bubbles:true}))} }); _cmView=new C.EditorView({ state:C.EditorState.create({ doc:content||'', extensions:[ C.markdown({base:C.markdownLanguage,codeLanguages:[ C.LanguageDescription.of({name:'javascript',alias:['js','jsx'],load:function(){return Promise.resolve(C.javascript({jsx:true}))}}), C.LanguageDescription.of({name:'typescript',alias:['ts','tsx'],load:function(){return Promise.resolve(C.javascript({typescript:true,jsx:true}))}}), C.LanguageDescription.of({name:'html',load:function(){return Promise.resolve(C.html())}}), C.LanguageDescription.of({name:'css',load:function(){return Promise.resolve(C.css())}}), C.LanguageDescription.of({name:'json',load:function(){return Promise.resolve(C.json())}}), C.LanguageDescription.of({name:'sql',load:function(){return Promise.resolve(C.sql())}}), C.LanguageDescription.of({name:'python',alias:['py'],load:function(){return Promise.resolve(C.python())}}), C.LanguageDescription.of({name:'xml',load:function(){return Promise.resolve(C.xml())}}), C.LanguageDescription.of({name:'go',alias:['golang'],load:function(){return Promise.resolve(C.go())}}), C.LanguageDescription.of({name:'c++',alias:['cpp','c'],load:function(){return Promise.resolve(C.cpp())}}), C.LanguageDescription.of({name:'yaml',alias:['yml','dockerfile','docker-compose'],load:function(){return Promise.resolve(C.yaml())}}), C.LanguageDescription.of({name:'shell',alias:['bash','sh','zsh'],load:function(){return Promise.resolve(C.StreamLanguage.define(C.shell))}}) ]}), C.syntaxHighlighting(joplockHighlight), C.syntaxHighlighting(C.defaultHighlightStyle,{fallback:true}), joplockTheme, C.drawSelection(), C.highlightActiveLine(), C.bracketMatching(), C.highlightSelectionMatches(), C.history(), C.keymap.of([...C.defaultKeymap,...C.historyKeymap,...C.searchKeymap.filter(function(b){var k=b.key||'';return k!=='Mod-f'&&k!=='F3'&&k!=='Mod-g'}),C.indentWithTab]), C.placeholder('Start writing...'), onUpdate, C.EditorView.lineWrapping ] }), parent:host }); } var _editorMode='markdown'; function syncEditorModeButtons(){var previewVisible=!!getPV();var markdownVisible=isMarkdownVisible();var mode=previewVisible?'preview':'markdown';_editorMode=mode;var mdBtn=document.getElementById('markdown-toggle');var pvBtn=document.getElementById('preview-toggle');if(mdBtn)mdBtn.classList.toggle('active',mode==='markdown');if(pvBtn)pvBtn.classList.toggle('active',mode==='preview');var mMd=document.getElementById('mobile-md-toggle');var mPv=document.getElementById('mobile-preview-toggle');if(mMd)mMd.classList.toggle('active',mode==='markdown');if(mPv)mPv.classList.toggle('active',mode==='preview');var tb=document.getElementById('editor-toolbar');var form=activeEditorForm();if(tb&&inMobileEditor()&&!(form&&form.dataset.encrypted==='1'))tb.style.display='flex';document.body.classList.toggle('mobile-markdown-mode',inMobileEditor()&&mode==='markdown')} function activeSearchInput(){if(isMobileShellMode()){var mobileInput=document.getElementById('mobile-editor-search-input');if(mobileInput)return mobileInput}return document.getElementById('nav-search')} function currentListSearchInput(){return document.getElementById('nav-search')||document.getElementById('mobile-search-input')} function currentListSearchTerm(){var input=currentListSearchInput();return input&&typeof input.value==='string'?input.value:''} function activeSearchTerm(){var input=activeSearchInput();return input&&typeof input.value==='string'?input.value:''} var _cmSearchMatches=[]; function clearCodeMirrorSearch(){_cmSearchMatches=[];if(_cmView&&window.CM&&window.CM.SearchQuery&&window.CM.setSearchQuery){_cmView.dispatch({effects:window.CM.setSearchQuery.of(new window.CM.SearchQuery({search:'',caseSensitive:false}))});}} function collectCodeMirrorSearchMatches(query){if(!_cmView||!query||!query.valid||!query.search)return[];var cursor=query.getCursor(_cmView.state.doc);var out=[];for(var next=cursor.next();!next.done;next=cursor.next())out.push({from:next.value.from,to:next.value.to});return out} function setCodeMirrorSearchActive(idx){if(!_cmView||!_cmSearchMatches.length)return;_searchMarkIdx=((idx%_cmSearchMatches.length)+_cmSearchMatches.length)%_cmSearchMatches.length;var match=_cmSearchMatches[_searchMarkIdx];var Sel=_cmView.state.selection.constructor;_cmView.dispatch({selection:Sel.cursor(match.from),scrollIntoView:true});searchNavShow(_cmSearchMatches.length,_searchMarkIdx)} function clearPreviewSearchMarks(root){if(!root)return;root.querySelectorAll('mark.search-highlight').forEach(function(m){var text=document.createTextNode(m.textContent);m.parentNode.replaceChild(text,m)});root.normalize()} function applyMobileTitleMode(){var ti=queryActiveEditor('.editor-title');if(!ti)return;var mobile=isMobileShellMode();var inMobileEditor=!!ti.closest('#mobile-editor-body');ti.contentEditable=(mobile&&!inMobileEditor)?'false':'true';ti.classList.toggle('editor-title-mobile-readonly',mobile&&!inMobileEditor)} var _pvSyncTimer=null;var _syncPVInFlight=false; var _previewDirty=false; function syncPV(){var pv=getPV(),ta=getTA();if(pv&&ta){var md=htmlToMarkdown(pv);if(ta.value!==md){ta.value=md;ta.dispatchEvent(new Event('input',{bubbles:true}));_previewDirty=false;return true}}_previewDirty=false;return false} function scheduleSyncPV(){if(_pvSyncTimer)clearTimeout(_pvSyncTimer);_pvSyncTimer=setTimeout(function(){_pvSyncTimer=null;_syncPVInFlight=true;var changed=syncPV();_syncPVInFlight=false;autoTitle();if(!changed){_log('scheduleSyncPV: no markdown change')}},150)} // Auto-title: first line of body becomes title unless user manually edited it var _titleManual=false; var stripMdForTitle=window.joplockStripNoteTitle||function(s){return String(s||'').trim()}; function syncTitleToHidden(opts){opts=opts||{};var ti=queryActiveEditor('.editor-title');var hi=queryActiveEditor('.editor-title-hidden');var mobileTitle=document.getElementById('mobile-editor-title');if(!hi)return '';var raw=ti?ti.textContent:'';var plain=stripMdForTitle(raw);if(ti&&plain!==raw.trim())ti.textContent=plain;hi.value=plain;if(mobileTitle&&document.activeElement!==mobileTitle&&mobileTitle.textContent!==plain)mobileTitle.textContent=plain||'Note';if(!opts.silent){markEdited();scheduleSaveTitle()}return plain} function syncTitle(){syncTitleToHidden()} function mobileSyncTitle(){var mobileTitle=document.getElementById('mobile-editor-title');if(!mobileTitle)return;var plain=stripMdForTitle(mobileTitle.textContent);var hi=queryActiveEditor('.editor-title-hidden');var ti=queryActiveEditor('.editor-title');if(hi)hi.value=plain;if(ti)ti.textContent=plain;_titleManual=true;markEdited()} function mobileSyncTitleAndSave(){mobileSyncTitle();scheduleSaveTitle()} function initAutoTitle(){_titleManual=false;var ti=queryActiveEditor('.editor-title');if(ti&&ti.style.display!=='none'){ti.addEventListener('input',function(){_titleManual=true;syncTitle()})}} function _autoTitleCandidate(line){var trimmed=(line||'').trim();if(!trimmed)return '';if(/^!\[[^\]]*\]\([^\)]+\)$/.test(trimmed))return '';if(/^]*\/?>(?:<\/img>)?$/i.test(trimmed))return '';return stripMdForTitle(trimmed.replace(/^#+\s*/,''));} function autoTitle(){if(_titleManual)return;var ta=getTA();var hi=queryActiveEditor('.editor-title-hidden');var ti=queryActiveEditor('.editor-title');var mobileTitle=document.getElementById('mobile-editor-title');if(!ta||!hi)return;var val=ta.value;var lines=val.split('\n');var firstPlain='';for(var i=0;i/g,'>');h=h.replace(/\*\*(.+?)\*\*/g,'$1');h=h.replace(/\*(.+?)\*/g,'$1');h=h.replace(/~~(.+?)~~/g,'$1');h=h.replace(/\+\+(.+?)\+\+/g,'$1');h=h.replace(/`([^`]+)`/g,'$1');return h} // Image resize via drag handles var _resizing=null; function initImgResize(pv){if(!pv||pv.dataset.imgResizeInit)return;pv.dataset.imgResizeInit='1';pv.addEventListener('mousedown',function(e){if(e.target.tagName==='IMG'&&e.target.classList.contains('preview-img')){var img=e.target,rect=img.getBoundingClientRect();var nearRight=e.clientX>rect.right-16,nearBottom=e.clientY>rect.bottom-16;if(nearRight||nearBottom){e.preventDefault();_resizing={img:img,startX:e.clientX,startY:e.clientY,startW:img.offsetWidth,startH:img.offsetHeight}}}})} document.addEventListener('mousemove',function(e){if(!_resizing)return;e.preventDefault();var dx=e.clientX-_resizing.startX,dy=e.clientY-_resizing.startY;var nw=Math.max(32,_resizing.startW+dx);var ratio=_resizing.startH/_resizing.startW;_resizing.img.style.width=nw+'px';_resizing.img.style.height=Math.round(nw*ratio)+'px'}); document.addEventListener('mouseup',function(){if(_resizing){_resizing=null;syncPV()}}); function pvBlockText(block){if(!block)return '';var text=typeof block.innerText==='string'?block.innerText:(block.textContent||'');return text.replace(/\r/g,'')} function insertPVText(text){var sel=window.getSelection();if(!sel||!sel.rangeCount)return false;var range=sel.getRangeAt(0);range.deleteContents();var node=document.createTextNode(text);range.insertNode(node);range.setStart(node,text.length);range.collapse(true);sel.removeAllRanges();sel.addRange(range);return true} function setPVCaret(node,offset){var sel=window.getSelection();if(!sel)return;var range=document.createRange();if(node&&node.nodeType===3){range.setStart(node,Math.min(offset,node.textContent.length));range.collapse(true)}else{range.selectNodeContents(node);range.collapse(false)}sel.removeAllRanges();sel.addRange(range)} function replacePVBlock(buildNode){var pv=getPV();if(!pv)return false;var sel=window.getSelection();if(!sel||!sel.rangeCount)return false;var range=sel.getRangeAt(0);if(!pv.contains(range.commonAncestorContainer))return false;var block=range.startContainer;while(block&&block!==pv&&block.nodeType!==1)block=block.parentNode;if(!block||block===pv)block=range.startContainer.parentNode;while(block&&block!==pv&&block.nodeType===1&&!/^(P|DIV|LI|BLOCKQUOTE|PRE|H[1-6])$/.test(block.nodeName))block=block.parentNode;var neo=buildNode(block,sel.toString(),range,pv);if(!neo)return false;if(block&&block.parentNode&&block!==pv){block.parentNode.replaceChild(neo,block)}else{range.deleteContents();range.insertNode(neo)}var focusNode=neo.querySelector?neo.querySelector('code'):null;if(!focusNode)focusNode=neo;var textNode=focusNode.firstChild&&focusNode.firstChild.nodeType===3?focusNode.firstChild:null;setPVCaret(textNode||focusNode,textNode?textNode.textContent.length:0);syncPV();pv.focus();return true} function transformPVBlock(tagName,defaultText){return replacePVBlock(function(block,selectedText,range,pv){var text=(!range.collapsed&&selectedText?selectedText:(block&&block!==pv?pvBlockText(block):selectedText))||defaultText;var neo=document.createElement(tagName);if(tagName==='pre'){neo.spellcheck=false;var code=document.createElement('code');code.textContent=text;neo.appendChild(code)}else{neo.textContent=text}return neo})} function clearFormat(){var pv=getPV();if(pv){document.execCommand('removeFormat',false,null);var sel=window.getSelection();if(sel&&sel.rangeCount){var range=sel.getRangeAt(0);var block=range.startContainer;while(block&&block!==pv&&block.nodeType!==1)block=block.parentNode;if(block&&block!==pv&&/^(H[1-6]|BLOCKQUOTE|PRE)$/.test(block.nodeName)){var p=document.createElement('p');p.textContent=block.textContent;block.parentNode.replaceChild(p,block);var r=document.createRange();r.selectNodeContents(p);sel.removeAllRanges();sel.addRange(r)}}syncPV();pv.focus();return}var cm=getCM();if(cm){var s=cm.state.selection.main;var from=s.from,to=s.to,sel=cm.state.sliceDoc(from,to);sel=sel.replace(/(\*{1,2}|~~|\+\+|`)(.*?)\1/g,'$2');sel=sel.replace(/^#{1,6}\s+/gm,'');sel=sel.replace(/^>\s?/gm,'');sel=sel.replace(/^[-*]\s/gm,'');sel=sel.replace(/^\d+\.\s/gm,'');cm.dispatch({changes:{from:from,to:to,insert:sel},selection:{anchor:from,head:from+sel.length}});cm.focus()}} function wrapSel(a,b){var pv=getPV();if(pv){var fenced=String.fromCharCode(10)+String.fromCharCode(96,96,96)+String.fromCharCode(10);var inlineCode=String.fromCharCode(96);if(a===fenced&&b===fenced&&transformPVBlock('pre','code'))return;if(a===inlineCode&&b===inlineCode){document.execCommand('insertHTML',false,''+(window.getSelection().toString()||'code')+'');syncPV();pv.focus();return}var cmdMap={'**':'bold','*':'italic','~~':'strikethrough','++':'underline'};var cmd=cmdMap[a];if(cmd){document.execCommand(cmd,false,null);syncPV();pv.focus();return}}var cm=getCM();if(cm){var s=cm.state.selection.main;var from=s.from,to=s.to,sel=cm.state.sliceDoc(from,to)||'text';var ins=a+sel+b;cm.dispatch({changes:{from:from,to:to,insert:ins},selection:{anchor:from+a.length,head:from+a.length+sel.length}});cm.focus()}} function insertPfx(p){var pv=getPV();if(pv){var sel=window.getSelection();if(sel.rangeCount){var range=sel.getRangeAt(0);var block=range.startContainer;while(block&&block!==pv&&block.nodeType!==1)block=block.parentNode;if(!block||block===pv)block=range.startContainer.parentNode;var hm=p.match(/^(#{1,6})\s/);if(hm){var lvl=hm[1].length;var tag='h'+lvl;if(block&&block.parentNode&&block!==pv){var neo=document.createElement(tag);neo.textContent=block.textContent;block.parentNode.replaceChild(neo,block)}else{document.execCommand('insertHTML',false,'<'+tag+'>'+(sel.toString()||'Heading')+'')}setTimeout(function(){syncPV();pv.focus()},0);return}if(p==='- [ ] '){var neo=document.createElement('div');neo.className='md-checkbox';var iconSpan=document.createElement('span');iconSpan.className='md-cb-icon';iconSpan.textContent='\u2610';neo.appendChild(iconSpan);var nbsp=document.createTextNode('\u00a0');neo.appendChild(nbsp);var sel2=window.getSelection();var range2=sel2.rangeCount?sel2.getRangeAt(0):null;if(range2){range2.deleteContents();range2.insertNode(neo);var r=document.createRange();r.setStart(nbsp,1);r.collapse(true);sel2.removeAllRanges();sel2.addRange(r)}else{pv.appendChild(neo)}neo.scrollIntoView({block:'nearest'});syncPV();pv.focus();return}if(p==='- '){document.execCommand('insertUnorderedList',false,null);syncPV();pv.focus();return}if(p==='1. '){document.execCommand('insertOrderedList',false,null);syncPV();pv.focus();return}if(p==='> '&&transformPVBlock('blockquote','Quote'))return}return}var cm=getCM();if(cm){var s=cm.state.selection.main;var line=cm.state.doc.lineAt(s.from);cm.dispatch({changes:{from:line.from,to:line.from,insert:p}});cm.focus()}} function insertTxt(x){var pv=getPV();if(pv){if(x==='\n---\n'){document.execCommand('insertHorizontalRule',false,null);syncPV();pv.focus();return}document.execCommand('insertText',false,x);syncPV();pv.focus();return}var cm=getCM();if(cm){var s=cm.state.selection.main;cm.dispatch({changes:{from:s.from,to:s.to,insert:x},selection:{anchor:s.from+x.length}});cm.focus()}} function insertStamp(kind){insertTxt(formatStamp(kind))} var _linkSavedRange=null;var _linkSavedTA=null; function closeLinkModal(){var modal=document.getElementById('link-modal');var backdrop=document.getElementById('link-modal-backdrop');if(modal)modal.hidden=true;if(backdrop)backdrop.hidden=true} function openLinkModal(){var pv=getPV();var cm=getCM();if(pv){var sel=window.getSelection();_linkSavedRange=sel&&sel.rangeCount?sel.getRangeAt(0).cloneRange():null;var labelInput=document.getElementById('link-edit-label');if(labelInput)labelInput.value=(sel&&sel.toString())||''}else if(cm){var s=cm.state.selection.main;var labelInput=document.getElementById('link-edit-label');if(labelInput)labelInput.value=cm.state.sliceDoc(s.from,s.to)}var modal=document.getElementById('link-modal');var backdrop=document.getElementById('link-modal-backdrop');var urlInput=document.getElementById('link-edit-url');if(urlInput)urlInput.value='';if(modal)modal.hidden=false;if(backdrop)backdrop.hidden=false;if(urlInput)urlInput.focus()} function submitLink(event){if(event)event.preventDefault();var url=document.getElementById('link-edit-url');var label=document.getElementById('link-edit-label');var u=(url?url.value:'').trim();if(!u)return false;var t=(label?label.value:'').trim()||u;closeLinkModal();var pv=getPV();if(pv){if(_linkSavedRange){var sel=window.getSelection();sel.removeAllRanges();sel.addRange(_linkSavedRange)}_linkSavedRange=null;document.execCommand('insertHTML',false,''+t.replace(/&/g,'&').replace(//g,'>')+'');syncPV();pv.focus();return false}var cm=getCM();if(cm){var md='['+t+']('+u+')';var s=cm.state.selection.main;cm.dispatch({changes:{from:s.from,to:s.to,insert:md},selection:{anchor:s.from+md.length}});cm.focus()}return false} function insertLink(){openLinkModal()} var _codeSavedSel=null; var _codeSavedRange=null; var _codeEditPre=null; var _codeModalCM=null; var _codeLangMap={'javascript':function(C){return C.javascript({jsx:true})},'typescript':function(C){return C.javascript({typescript:true,jsx:true})},'html':function(C){return C.html()},'css':function(C){return C.css()},'json':function(C){return C.json()},'sql':function(C){return C.sql()},'python':function(C){return C.python()},'xml':function(C){return C.xml()},'go':function(C){return C.go()},'c':function(C){return C.cpp()},'cpp':function(C){return C.cpp()},'yaml':function(C){return C.yaml()},'bash':function(C){return C.StreamLanguage.define(C.shell)}}; function _codeModalLangExt(lang){var C=window.CM;var fn=_codeLangMap[lang];return fn?fn(C):[]} function _initCodeModalCM(host,content,lang){if(_codeModalCM){_codeModalCM.destroy();_codeModalCM=null}var C=window.CM;var theme=C.EditorView.theme({'&':{height:'100%',fontSize:'13px'},'.cm-scroller':{overflow:'auto',fontFamily:'"Cascadia Mono",monospace',lineHeight:'1.5'},'.cm-content':{padding:'12px'},'.cm-gutters':{display:'none'},'.cm-activeLine':{backgroundColor:'var(--bg-hover)'},'.cm-selectionBackground':{backgroundColor:'var(--accent-dim) !important'},'&.cm-focused .cm-selectionBackground':{backgroundColor:'var(--accent-dim) !important'},'.cm-cursor':{borderLeftColor:'var(--accent)'}});_codeModalCM=new C.EditorView({state:C.EditorState.create({doc:content||'',extensions:[_codeModalLangExt(lang),C.syntaxHighlighting(C.defaultHighlightStyle,{fallback:true}),C.syntaxHighlighting(C.HighlightStyle.define([{tag:C.tags.keyword,color:'#c678dd'},{tag:[C.tags.string,C.tags.special(C.tags.brace)],color:'#98c379'},{tag:C.tags.number,color:'#d19a66'},{tag:C.tags.bool,color:'#d19a66'},{tag:[C.tags.definition(C.tags.variableName),C.tags.function(C.tags.variableName)],color:'#61afef'},{tag:C.tags.typeName,color:'#e5c07b'},{tag:C.tags.comment,color:'var(--text-dim)',fontStyle:'italic'},{tag:C.tags.operator,color:'#56b6c2'},{tag:C.tags.className,color:'#e5c07b'},{tag:C.tags.propertyName,color:'#e06c75'},{tag:C.tags.attributeName,color:'#d19a66'},{tag:C.tags.attributeValue,color:'#98c379'}])),theme,C.drawSelection(),C.highlightActiveLine(),C.bracketMatching(),C.history(),C.keymap.of([...C.defaultKeymap,...C.historyKeymap,C.indentWithTab]),C.placeholder('Paste or type code here...'),C.EditorView.lineWrapping]}),parent:host});_codeModalCM.focus()} function _updateCodeModalLang(lang){if(!_codeModalCM)return;var C=window.CM;var doc=_codeModalCM.state.doc.toString();var host=_codeModalCM.dom.parentElement;_codeModalCM.destroy();_initCodeModalCM(host,doc,lang)} function closeCodeModal(){if(_codeModalCM){_codeModalCM.destroy();_codeModalCM=null}var modal=document.getElementById('code-modal');if(modal)modal.hidden=true} function openCodeModal(editPre){var pv=getPV();var cm=getCM();var sel='';var lang='';_codeSavedSel=null;_codeSavedRange=null;_codeEditPre=editPre||null;if(_codeEditPre){var codeEl=_codeEditPre.querySelector('code[class*="language-"]');sel=codeEl?codeEl.textContent:(_codeEditPre.querySelector('code')||_codeEditPre).textContent;if(codeEl){var classes=(codeEl.getAttribute('class')||'').split(' ');for(var i=0;i/g,'>');var cls=l?' class="language-'+l+'"':'';document.execCommand('insertHTML',false,'
'+escaped+'
');initCopyButtons(pv);highlightCodeBlocks(pv);ensureEditableAfterPre(pv);syncPV();pv.focus();return false}var cm=getCM();if(cm){var s=_codeSavedSel||cm.state.selection.main;var md='\n```'+l+'\n'+code+'\n```\n';cm.dispatch({changes:{from:s.from,to:s.to,insert:md},selection:{anchor:s.from+md.length}});cm.focus()}_codeSavedSel=null;_codeSavedRange=null;_codeEditPre=null;return false} function insertImg(){var pv=getPV();if(pv){var u=prompt('Image URL:');if(!u)return;document.execCommand('insertHTML',false,'image');syncPV();pv.focus();return}var u=prompt('Image URL:');if(u)insertTxt('![image]('+u+')')} function handleFilePicker(input){ if(!input||!input.files||!input.files.length)return; var files=Array.prototype.slice.call(input.files); input.value=''; uploadFiles(files).catch(function(){}); } var _uploadBatchDepth=0; var _uploadBatchChanged=false; function uploadFiles(files){ if(!files||!files.length)return Promise.resolve(); _uploadBatchDepth++; var queue=Promise.resolve(); for(var i=0;iUploading '+pct+'%','Uploading'); } }; var done=false; return new Promise(function(resolve,reject){ xhr.onload=function(){ _log('uploadFile onload status',xhr.status,xhr.responseText.slice(0,120)); setSaveState('',''); var d; try{d=JSON.parse(xhr.responseText)}catch(e){alert('Upload failed');reject(e);return} if(d.error){alert(d.error);reject(new Error(d.error));return} var changed=_appendMarkdownAtCursor(d.markdown); if(changed){markEdited();if(_uploadBatchDepth>0)_uploadBatchChanged=true;else scheduleSave();} if(_refreshPreviewFromMarkdown(resolve))return; resolve(); }; xhr.onerror=function(){setSaveState('','');alert('Upload failed');reject(new Error('Upload failed'))}; if(s)setSaveState('Uploading 0%','Uploading'); xhr.open('POST','/fragments/upload'); xhr.send(fd); }); } // --- history modal --- var _historyNoteId=null;var _historySnapshotId=null; function openHistoryModal(noteId){_historyNoteId=noteId;_historySnapshotId=null;var modal=document.getElementById('history-modal');var backdrop=document.getElementById('history-modal-backdrop');var inner=document.getElementById('history-modal-inner');if(!modal||!backdrop||!inner)return;inner.innerHTML='
Loading...
';if(modal)modal.hidden=false;if(backdrop)backdrop.hidden=false;htmx.ajax('GET','/fragments/history/'+encodeURIComponent(noteId),{target:'#history-modal-inner',swap:'innerHTML'})} function closeHistoryModal(){var modal=document.getElementById('history-modal');var backdrop=document.getElementById('history-modal-backdrop');if(modal)modal.hidden=true;if(backdrop)backdrop.hidden=true} function selectHistorySnapshot(id){_historySnapshotId=id;document.querySelectorAll('.history-item').forEach(function(el){el.classList.toggle('history-item-active',el.dataset.snapshotId===id)});var label=document.getElementById('history-selected-label');var preview=document.getElementById('history-preview');if(preview)preview.innerHTML='
Loading...
';if(label)label.textContent='Loading...';htmx.ajax('GET','/fragments/history-snapshot/'+encodeURIComponent(id),{target:'#history-preview',swap:'innerHTML'}).then(function(){var d=new Date(parseInt(id)*1||0);var label=document.getElementById('history-selected-label');if(label)label.textContent=''});_log('selectHistorySnapshot',id)} function restoreHistorySnapshot(noteId){var sid=_historySnapshotId;if(!sid){alert('Select a snapshot first.');return}if(!confirm('Restore this version? The current note will be overwritten.'))return;var form=activeEditorForm();var cfi=(form&&form.querySelector('[name="currentFolderId"]'))?form.querySelector('[name="currentFolderId"]').value:'';closeHistoryModal();_log('restoreHistorySnapshot',noteId,sid);htmx.ajax('POST','/fragments/history/'+encodeURIComponent(noteId)+'/restore/'+encodeURIComponent(sid),{target:'#autosave-status',swap:'innerHTML',values:{currentFolderId:cfi}}).then(function(){var s=queryActiveEditor('#autosave-status');if(s&&!s.querySelector('.autosave-error'))s.innerHTML='Restored';_snapshots=[];_log('restore done')}).catch(function(e){alert('Restore failed: '+e.message)})} // --- client ring buffer (in-session undo) --- var _snapshots=[];var _snapshotMaxCount=20;var _undoTimer=null; function pushSnapshot(){var ta=getTA();var title=queryActiveEditor('[name="title"]');var body=ta?ta.value:'';var t=title?title.value:'';if(_snapshots.length>0&&_snapshots[_snapshots.length-1].body===body&&_snapshots[_snapshots.length-1].title===t)return;_snapshots.push({body:body,title:t,ts:Date.now()});if(_snapshots.length>_snapshotMaxCount)_snapshots.shift();var btn=queryActiveEditor('#undo-save-btn');if(btn)btn.hidden=_snapshots.length<2;_log('pushSnapshot count',_snapshots.length)} function undoSnapshot(){if(_snapshots.length<2){_log('undoSnapshot: nothing to undo');return}if(_undoTimer){clearTimeout(_undoTimer);_undoTimer=null}_snapshots.pop();var snap=_snapshots[_snapshots.length-1];var btn=queryActiveEditor('#undo-save-btn');if(btn)btn.hidden=_snapshots.length<2;_log('undoSnapshot restoring ts',snap.ts);var ta=getTA();var titleInput=queryActiveEditor('[name="title"]');var titleDiv=queryActiveEditor('.editor-title');var pv=getPV();if(ta)ta.value=snap.body;if(titleInput)titleInput.value=snap.title;if(titleDiv)titleDiv.textContent=snap.title;var cm=getCM();if(cm&&!pv)cmSetVal(snap.body);if(pv&&pv.style.display!=='none'){fetch('/fragments/preview',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'body='+encodeURIComponent(snap.body)}).then(function(r){return r.text()}).then(function(h){pv.innerHTML=h;syncPV()}).catch(function(){})}if(ta)ta.dispatchEvent(new Event('input',{bubbles:true}));scheduleSave();var s=queryActiveEditor('#autosave-status');if(s){s.innerHTML='Undone';clearTimeout(_undoTimer);_undoTimer=setTimeout(function(){var s2=queryActiveEditor('#autosave-status');if(s2&&s2.querySelector('.autosave-edited'))s2.innerHTML='Saved'},3000)}} function handleDrop(e){e.preventDefault();var files=e.dataTransfer&&e.dataTransfer.files;if(!files||!files.length)return;uploadFiles(files).catch(function(){})} var _tdService=null; function getTurndown(){ if(_tdService)return _tdService; var td=new TurndownService({headingStyle:'atx',hr:'---',codeBlockStyle:'fenced',bulletListMarker:'-',emDelimiter:'*',strongDelimiter:'**',br:'\n'}); // Joplin resource images (with optional resize dimensions) td.addRule('joplinImg',{filter:function(n){return n.nodeName==='IMG'},replacement:function(c,n){ var alt=n.getAttribute('alt')||'';var src=n.getAttribute('src')||''; var w=n.style.width||n.getAttribute('width');var h=n.style.height||n.getAttribute('height'); var rm=src.match(/^\/resources\/([0-9a-zA-Z]{32})$/); // Never embed data: URIs into markdown — they corrupt note storage if(src.startsWith('data:'))return alt?'['+alt+']':''; if(w||h){var iSrc=rm?':/'+rm[1]:src;return ''+alt+''} if(rm)return '!['+alt+'](:/'+rm[1]+')';return '!['+alt+']('+src+')'}}); // Joplin resource links td.addRule('joplinLink',{filter:function(n){return n.nodeName==='A'&&/^\/resources\/[0-9a-zA-Z]{32}(?:\?download=1)?$/.test((n.getAttribute('href')||'').split('#')[0])}, replacement:function(c,n){var m=(n.getAttribute('href')||'').match(/^\/resources\/([0-9a-zA-Z]{32})/);return '['+c+'](:/'+m[1]+')'}}); // md-blank-line divs — use placeholder to survive
normalization td.addRule('blankLine',{filter:function(n){return n.nodeName==='DIV'&&n.classList.contains('md-blank-line')&&!n.querySelector('img,a,pre,code,ul,ol,blockquote,table')&&!n.textContent.trim()},replacement:function(){return '\x00BL\x00'}}); // md-checkbox divs td.addRule('checkbox',{filter:function(n){return n.nodeName==='DIV'&&n.classList.contains('md-checkbox')}, replacement:function(c,n){var checked=n.classList.contains('checked');var txt=c.replace(/^[\u2611\u2610\u2612\u2705\u00a0 ]+/,'');return (checked?'- [x] ':'- [ ] ')+txt+'\n'}}); // Strikethrough td.addRule('strikethrough',{filter:['del','s','strike'],replacement:function(c){return c.trim()?'~~'+c.trim()+'~~':''}}); // Underline td.addRule('underline',{filter:'u',replacement:function(c){return c.trim()?'++'+c.trim()+'++':''}}); // Empty divs from contenteditable (Enter key creates

) — emit BL sentinel so // line 616 converts it to one extra newline (\n\n\n), which injectBlankLineBlocks turns into // exactly one md-blank-line div. Using '
' caused line 611 to produce 4 newlines (two divs). // Using '' made blank-line edits invisible to Turndown (hash never changed, note never saved). td.addRule('emptyDiv',{filter:function(n){return n.nodeName==='DIV'&&!n.classList.length&&!n.querySelector('img,a,pre,code,ul,ol,blockquote,table')&&(!n.textContent.trim()||n.innerHTML==='
')},replacement:function(){return '\x00BL\x00'}}); // Empty paragraphs from contenteditable (


) — same reasoning. td.addRule('emptyP',{filter:function(n){return n.nodeName==='P'&&!n.querySelector('img')&&(!n.textContent.trim()||n.innerHTML==='
')},replacement:function(){return '\x00BL\x00'}}); _tdService=td;return td} function htmlToMarkdown(el){ var root=el.cloneNode(true); root.querySelectorAll('.pre-copy-btn').forEach(function(btn){btn.remove()}); var md=getTurndown().turndown(root.innerHTML); var nbsp=String.fromCharCode(160); while(md.indexOf(nbsp)>=0)md=md.split(nbsp).join(' '); var nl=String.fromCharCode(10); var headingGapRe=new RegExp('^(#{1,6}[^'+nl+']*)'+nl+'{2,}(?=\\S)','gm'); var headingLeadRe=new RegExp('([^'+nl+'])'+nl+'{2,}(#{1,6}\\s)','g'); md=md.split('
').join('
'); md=md.split('
'+nl).join(nl); while(md.indexOf('

')>=0)md=md.split('

').join('
'+nl); md=md.replace(headingLeadRe,'$1'+nl+'$2'); md=md.replace(headingGapRe,'$1'+nl); md=md.replace(new RegExp(nl+nl+'
$'),''); md=md.replace(/\n*(?:\x00BL\x00\n*)+/g,function(m){var count=(m.match(/\x00BL\x00/g)||[]).length;return nl+nl+Array(count+1).join(nl)}); var out=''; for(var i=0;i';pv.appendChild(p)}})} function initCopyButtons(pv){if(!pv)return;pv.querySelectorAll('pre').forEach(function(pre){pre.contentEditable='false';pre.style.cursor='pointer';if(pre.querySelector('.pre-copy-btn'))return;var btn=document.createElement('button');btn.type='button';btn.className='pre-copy-btn';btn.title='Copy code';btn.textContent='Copy';btn.addEventListener('click',function(e){e.stopPropagation();var code=pre.querySelector('code');var text=code?code.textContent:(pre.textContent||'');navigator.clipboard.writeText(text).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy'},1500)}).catch(function(){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy'},1500)})});pre.insertBefore(btn,pre.firstChild);pre.addEventListener('click',function(e){if(e.target.closest('.pre-copy-btn'))return;e.preventDefault();e.stopPropagation();openCodeModal(pre)})})} function activatePV(pv){if(!pv)return;pv.contentEditable='true';initImgResize(pv);initCopyButtons(pv);highlightCodeBlocks(pv);ensureEditableAfterPre(pv);pv.oninput=function(){_previewDirty=true;scheduleSyncPV()};pv.onkeyup=null;if(pv.dataset.pvInit)return;pv.dataset.pvInit='1'; pv.addEventListener('click',function(e){var link=e.target.closest('a');if(link&&pv.contains(link)){var href=link.getAttribute('href')||'';if(href){e.preventDefault();window.open(href,'_blank','noopener');return}}}); // Click checkbox icon to toggle checked state pv.addEventListener('click',function(e){var cb=e.target.closest('.md-checkbox');if(!cb)return;var iconEl=cb.querySelector('.md-cb-icon');if(!iconEl){var txt=cb.firstChild;if(!txt||txt.nodeType!==3)return;var icon=txt.textContent.charAt(0);if(icon!=='\u2610'&&icon!=='\u2611')return;var r=document.createRange();r.setStart(txt,0);r.setEnd(txt,Math.min(2,txt.textContent.length));var iconRect=r.getBoundingClientRect();if(e.clientX>iconRect.right)return;e.preventDefault();var checked=!cb.classList.contains('checked');cb.classList.toggle('checked',checked);txt.textContent=(checked?'\u2611':'\u2610')+txt.textContent.slice(1);syncPV();return}var iconRect=iconEl.getBoundingClientRect();if(e.clientX>iconRect.right)return;e.preventDefault();var checked=!cb.classList.contains('checked');cb.classList.toggle('checked',checked);iconEl.textContent=checked?'\u2611':'\u2610';syncPV()}); // Enter inside code blocks should stay in the same block; Enter after checkbox creates new checkbox pv.addEventListener('keydown',function(e){if(e.key==='Enter'){var sel=window.getSelection();if(!sel.rangeCount)return;var range=sel.getRangeAt(0);var node=range.startContainer;var el=node.nodeType===3?node.parentElement:node;var pre=el&&el.closest?el.closest('pre'):null;if(pre&&pv.contains(pre)){e.preventDefault();var code=pre.querySelector('code')||pre;var txt=code.textContent||'';var atEnd=(node===code||node.parentElement===code)&&range.startOffset===(node.nodeType===3?node.textContent.length:code.childNodes.length)&&!range.toString();if(atEnd&&txt.endsWith('\n')){code.textContent=txt.slice(0,-1);var np=document.createElement('p');np.innerHTML='
';pre.parentNode.insertBefore(np,pre.nextSibling);var nr=document.createRange();nr.setStart(np,0);nr.collapse(true);sel.removeAllRanges();sel.addRange(nr);np.scrollIntoView({block:'nearest'});syncPV();return}if(insertPVText('\n'))syncPV();return}var cb=el&&el.closest?el.closest('.md-checkbox'):null;if(!cb&&node.nodeType===1&&range.startOffset>0){var prev=node.childNodes[range.startOffset-1];if(prev&&prev.nodeType===1&&prev.classList&&prev.classList.contains('md-checkbox'))cb=prev}if(!cb)return;e.preventDefault();var label=(cb.textContent||'').replace(/^[\u2610\u2611][\u00a0 ]*/,'').replace(/\u00a0|\s/g,'');if(!label){var para=document.createElement('p');para.innerHTML='
';if(cb.parentNode)cb.parentNode.replaceChild(para,cb);var rp=document.createRange();rp.setStart(para,0);rp.collapse(true);sel.removeAllRanges();sel.addRange(rp);para.scrollIntoView({block:'nearest'});syncPV();return}var neo=document.createElement('div');neo.className='md-checkbox';var iconSpan2=document.createElement('span');iconSpan2.className='md-cb-icon';iconSpan2.textContent='\u2610';neo.appendChild(iconSpan2);var tn=document.createTextNode('\u00a0');neo.appendChild(tn);cb.parentNode.insertBefore(neo,cb.nextSibling);var r=document.createRange();r.setStart(tn,1);r.collapse(true);sel.removeAllRanges();sel.addRange(r);neo.scrollIntoView({block:'nearest'});syncPV()}}); // Scroll to keep cursor visible while typing pv.addEventListener('input',function(){var sel=window.getSelection();if(sel&&sel.rangeCount){var r=sel.getRangeAt(0).getBoundingClientRect();var pr=pv.getBoundingClientRect();if(r.bottom>pr.bottom-8)pv.scrollTop+=r.bottom-pr.bottom+24}}); // Force plain-text paste — if inside
, insert raw text directly; otherwise wrap leading-space content in 

	pv.addEventListener('paste',function(e){
		// Image paste: upload and insert as resource
		var items=e.clipboardData&&e.clipboardData.items;
		_log('paste event, items:',(items?items.length:0));
		if(items){for(var i=0;i>>0;return h}
var _formHashExclude={baseUpdatedTime:true,forceSave:true,createCopy:true};function formHash(form){if(!form)return 0;var parts=[];var els=form.elements;for(var i=0;iSaved','Saved');return}_log('scheduleSave firing, hash',_savedHash,'->',h);htmx.trigger(form,'joplock:save')},2000)}
function scheduleSaveTitle(){var mobileTitle=document.getElementById('mobile-editor-title');if(mobileTitle&&document.activeElement===mobileTitle)return;// Don't save while user is still editing title
if(_saveTitleTimer)clearTimeout(_saveTitleTimer);if(_saveTimer)clearTimeout(_saveTimer);_saveTimer=null;_saveTitleTimer=setTimeout(function(){_saveTitleTimer=null;if(_anyModalOpen()){_log('scheduleSaveTitle deferred, modal open');scheduleSave();return}var form=activeEditorForm();if(!form)return;var h=formHash(form);if(h===_savedHash){_log('scheduleSaveTitle skip, hash unchanged',h);setSaveState('Saved','Saved');return}_log('scheduleSaveTitle firing');htmx.trigger(form,'joplock:save')},2000)}
function snapshotHash(){var form=activeEditorForm();_savedHash=formHash(form);_log('snapshotHash',_savedHash)}
function _isLockedOverlayEventTarget(target){return !!(target&&target.closest&&target.closest('#editor-locked'))}
function initEditorPanel(){var form=activeEditorForm();if(!form||form.dataset.editorInit)return;form.dataset.editorInit='1';_log('initEditorPanel',form.getAttribute('hx-put'));if(isMobileShellMode())closeNav();_previewDirty=false;setSaveState('','');snapshotHash();_snapshots=[];var undoBtn=queryActiveEditor('#undo-save-btn');if(undoBtn)undoBtn.hidden=true;pushSnapshot();form.addEventListener('input',function(e){if(_isLockedOverlayEventTarget(e.target))return;markEdited();scheduleSave()});form.addEventListener('change',function(e){if(_isLockedOverlayEventTarget(e.target))return;markEdited();scheduleSave()});initAutoTitle();applyMobileTitleMode();renderNoteMeta();var ta=getTA();if(ta){ta.addEventListener('input',function(){autoTitle()})}var pendingSearch=(window._pendingNoteSearchTerm||'').trim();var mobileEditor=inMobileEditor();if(mobileEditor&&pendingSearch){var header=document.getElementById('mobile-editor-header');var searchHeader=document.getElementById('mobile-editor-search-header');if(header)header.style.display='none';if(searchHeader)searchHeader.style.display=''}var searchInput=activeSearchInput();if(searchInput&&pendingSearch&&!searchInput.value)searchInput.value=pendingSearch;window._pendingNoteSearchTerm='';var pv=queryActiveEditor('#note-preview');var host=queryActiveEditor('#cm-host');if(form.dataset.encrypted==='1'){if(pv)pv.style.display='none';if(host)host.style.display='none';_editorMode='markdown';syncEditorModeButtons();return}var defaultMode=form.dataset.editorMode||_defaultNoteOpenMode||'preview';if(defaultMode!=='markdown')defaultMode='preview';form.dataset.editorMode=defaultMode;if(defaultMode==='preview'&&pv&&pv.style.display!=='none'){_editorMode='preview';activatePV(pv);_previewDirty=false;if(host)host.style.display='none';syncEditorModeButtons();applySearchHighlight()}else{_editorMode='markdown';form.dataset.editorMode='markdown';if(pv)pv.style.display='none';if(host){host.style.display='';initCM(host,ta?ta.value:'')}syncEditorModeButtons();applySearchHighlight()}}
function applySearchHighlight(){var term=activeSearchTerm();var bar=document.getElementById('search-nav-bar');if(bar)bar.hidden=true;_searchMarks=[];_searchMarkIdx=0;var pv=queryActiveEditor('#note-preview');if(pv)clearPreviewSearchMarks(pv);if(!term||!term.trim()){clearCodeMirrorSearch();return}term=term.trim();if(_editorMode==='preview'&&pv){clearCodeMirrorSearch();var savedHandler=pv.oninput;pv.oninput=null;highlightInPreview(pv,term);pv.oninput=savedHandler}else if(_editorMode==='markdown'&&_cmView&&window.CM&&window.CM.SearchQuery&&window.CM.setSearchQuery){			window.CM.openSearchPanel(_cmView);var q=new window.CM.SearchQuery({search:term,caseSensitive:false});_cmView.dispatch({effects:window.CM.setSearchQuery.of(q)});_cmSearchMatches=collectCodeMirrorSearchMatches(q);if(_cmSearchMatches.length)setCodeMirrorSearchActive(0);else searchNavShow(0,0)}}
function escapeRegex(s){var specials=['.','+','*','?','^','$','(',')','{','}','[',']','|','\\'];return s.split('').map(function(c){return specials.indexOf(c)>=0?'\\'+c:c}).join('')}
var _searchMarks=[];var _searchMarkIdx=0;
function searchNavShow(total,idx){var bar=document.getElementById('search-nav-bar');var counter=document.getElementById('search-nav-counter');if(bar){if(total===0){bar.hidden=true}else{bar.hidden=false;if(counter)counter.textContent=(idx+1)+' / '+total}}var mobileCounter=document.getElementById('mobile-search-nav-counter');var mobilePrev=document.getElementById('mobile-search-prev-btn');var mobileNext=document.getElementById('mobile-search-next-btn');if(mobileCounter){mobileCounter.hidden=total===0;if(total>0)mobileCounter.textContent=(idx+1)+' / '+total}if(mobilePrev)mobilePrev.hidden=total===0;if(mobileNext)mobileNext.hidden=total===0}
function searchNavSetActive(idx){_searchMarks.forEach(function(m,i){m.classList.toggle('search-highlight-active',i===idx)});var m=_searchMarks[idx];if(m)m.scrollIntoView({block:'center',behavior:'smooth'})}
function searchNavStep(dir){if(_editorMode==='markdown'&&_cmSearchMatches.length){setCodeMirrorSearchActive(_searchMarkIdx+dir);return}if(!_searchMarks.length)return;_searchMarkIdx=(_searchMarkIdx+dir+_searchMarks.length)%_searchMarks.length;searchNavSetActive(_searchMarkIdx);searchNavShow(_searchMarks.length,_searchMarkIdx)}
function searchNavDismiss(){var bar=document.getElementById('search-nav-bar');var mobileCounter=document.getElementById('mobile-search-nav-counter');var mobilePrev=document.getElementById('mobile-search-prev-btn');var mobileNext=document.getElementById('mobile-search-next-btn');if(bar)bar.hidden=true;if(mobileCounter)mobileCounter.hidden=true;if(mobilePrev)mobilePrev.hidden=true;if(mobileNext)mobileNext.hidden=true;var pv=queryActiveEditor('#note-preview');if(pv)clearPreviewSearchMarks(pv);_searchMarks=[];_searchMarkIdx=0;clearCodeMirrorSearch()}
function highlightInPreview(pv,term){if(!pv||!term)return;_searchMarks=[];_searchMarkIdx=0;var walker=document.createTreeWalker(pv,NodeFilter.SHOW_TEXT,{acceptNode:function(n){return n.parentElement&&n.parentElement.closest('script,style,mark')?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}},false);var nodes=[];var node;while((node=walker.nextNode()))nodes.push(node);var re=new RegExp(escapeRegex(term),'gi');nodes.forEach(function(n){var matches=[];var m;re.lastIndex=0;while((m=re.exec(n.textContent))!==null)matches.push({start:m.index,end:m.index+m[0].length});if(!matches.length)return;var frag=document.createDocumentFragment();var last=0;matches.forEach(function(r){if(r.start>last)frag.appendChild(document.createTextNode(n.textContent.slice(last,r.start)));var mark=document.createElement('mark');mark.className='search-highlight';mark.textContent=n.textContent.slice(r.start,r.end);_searchMarks.push(mark);frag.appendChild(mark);last=r.end});if(lastSaved','Saved');_log('afterRequest snapshotHash after save')}if(e.detail&&e.detail.successful&&document.body.classList.contains('is-offline')){clearOffline()}});
document.body.addEventListener('htmx:afterSwap',function(e){var target=e.detail&&e.detail.target;_log('htmx:afterSwap',target&&target.id);if(target&&target.id==='editor-panel'){hideNoteOverlay();if(_cmView){_cmView.destroy();_cmView=null}_searchMarks=[];_searchMarkIdx=0}});
function showOffline(){setSaveState('Offline','Offline');document.body.classList.add('is-offline');_log('offline indicator shown');showDisconnected()}
function clearOffline(){document.body.classList.remove('is-offline');_log('offline indicator cleared')}
document.body.addEventListener('htmx:sendError',function(e){var elt=e.detail&&e.detail.elt;_log('htmx:sendError',elt&&elt.id);if(elt&&elt.id==='note-editor-form')showOffline()});
document.body.addEventListener('htmx:responseError',function(e){var elt=e.detail&&e.detail.elt;var xhr=e.detail&&e.detail.xhr;_log('htmx:responseError',elt&&elt.id,xhr&&xhr.status);if(xhr&&xhr.status===401){_log('htmx 401, session invalid, logging out');window.location.assign('/logout');return;}if(elt&&elt.id==='note-editor-form')showOffline()});
// --- Disconnected overlay (server unreachable) ---
var _dcFailCount=0;
var _dcFailThreshold=1;
var _dcRetryIntervalSec=15;
var _dcRetryCountdown=0;
var _dcRetryTimer=null;
var _dcOverlay=null;
var _dcVisible=false;

function _createDcOverlay(){
	if(_dcOverlay)return _dcOverlay;
	var o=document.createElement('div');
	o.className='disconnected-overlay';
	o.innerHTML=''
		+'
' +'
' +'' +'
' +'
Connection lost
' +'
Unable to reach the server.
' +'
' +'
' +'' +'' +'
'; document.body.appendChild(o); o.querySelector('#dc-retry-btn').addEventListener('click',_dcRetryNow); _dcOverlay=o; return o; } function _dcUpdateCountdown(){ var el=document.getElementById('dc-countdown'); if(el)el.textContent='Retrying in '+_dcRetryCountdown+'s\u2026'; } function _dcPing(){ _log('connectivity ping'); return fetch('/heartbeat',{method:'POST',credentials:'same-origin'}).then(function(r){ if(r.status===401){ _log('ping 401, session expired'); window.location.assign('/logout'); return false; } if(!r.ok)throw new Error('HTTP '+r.status); return true; }); } function showDisconnected(){ if(_dcVisible)return; _dcVisible=true; _log('showDisconnected'); var o=_createDcOverlay(); o.style.display=''; document.body.classList.add('is-disconnected'); _dcRetryCountdown=_dcRetryIntervalSec; _dcUpdateCountdown(); if(_dcRetryTimer)clearInterval(_dcRetryTimer); _dcRetryTimer=setInterval(function(){ _dcRetryCountdown--; if(_dcRetryCountdown<=0){ _dcRetryCountdown=_dcRetryIntervalSec; _dcPing().then(function(ok){if(ok)clearDisconnected()}).catch(function(){}); } _dcUpdateCountdown(); },1000); } function clearDisconnected(){ if(!_dcVisible)return; _dcVisible=false; _dcFailCount=0; _log('clearDisconnected, reconnected'); if(_dcOverlay)_dcOverlay.style.display='none'; document.body.classList.remove('is-disconnected'); if(_dcRetryTimer){clearInterval(_dcRetryTimer);_dcRetryTimer=null} clearOffline(); // Re-save if dirty var status=queryActiveEditor('#autosave-status'); var dirty=status&&status.querySelector('.autosave-edited'); if(dirty){_log('clearDisconnected: re-saving dirty note');scheduleSave()} } function _dcRetryNow(){ var btn=document.getElementById('dc-retry-btn'); if(btn){btn.disabled=true;btn.textContent='Connecting\u2026'} _dcRetryCountdown=_dcRetryIntervalSec; _dcPing().then(function(ok){if(ok)clearDisconnected()}).catch(function(){}); setTimeout(function(){if(btn){btn.disabled=false;btn.textContent='Retry now'}},2000); } function _dcOnFetchFail(){ _dcFailCount++; if(_dcFailCount>=_dcFailThreshold)showDisconnected(); } function _dcOnFetchOk(){ _dcFailCount=0; if(_dcVisible)clearDisconnected(); } window.addEventListener('online',function(){_log('browser online event');if(_dcVisible){_dcPing().then(function(ok){if(ok)clearDisconnected()}).catch(function(){})}if(document.body.classList.contains('is-offline')){var s=document.getElementById('autosave-status');var dirty=s&&s.querySelector('.autosave-edited');if(dirty){scheduleSave()}else if(s){setSaveState('Reconnected','Saved')}clearOffline()}}); window.addEventListener('offline',function(){_log('browser offline event');showDisconnected()}); // Always-on connectivity ping (every 30s) — triggers disconnected overlay on failure (function(){var _cpMs=30000;function _connectivityPing(){_dcPing().then(function(ok){if(ok)_dcOnFetchOk();else _dcOnFetchFail()}).catch(function(){_dcOnFetchFail()})}var _cpInterval=setInterval(_connectivityPing,_cpMs);_connectivityPing()})(); window.addEventListener('load',function(){if(isMobileShellMode())return;initNavPanel();initEditorPanel()}); window.addEventListener('resize',applyMobileTitleMode); document.addEventListener('keydown',function(e){var mac=navigator.platform&&navigator.platform.indexOf('Mac')!==-1;var mod=mac?e.metaKey:e.ctrlKey;if(mod&&e.shiftKey&&e.key.toLowerCase()==='z'){e.preventDefault();undoSnapshot()}}); function flushSave(callback){var form=activeEditorForm();if(!form){_log('flushSave skip (no form)');if(callback)callback(true);return}if(_saveTimer){clearTimeout(_saveTimer);_saveTimer=null}if(_saveTitleTimer){clearTimeout(_saveTitleTimer);_saveTitleTimer=null}if(_pvSyncTimer){clearTimeout(_pvSyncTimer);_pvSyncTimer=null;_syncPVInFlight=true;syncPV();_syncPVInFlight=false}else{var pv=getPV();if(pv)syncPV();else cmSyncToTA()}syncTitleToHidden({silent:true});var h=formHash(form);if(h===_savedHash){_log('flushSave skip (hash unchanged)',h);if(callback)callback(true);return}setSaveState('Saving...','Saving...');var restoreReq=function(){};buildFlushRequest(form).then(function(req){if(!req){if(callback)callback(true);return}restoreReq=req.restore||restoreReq;_log('flushSave',req.url);return fetch(req.url,{method:'PUT',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:req.body}).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.text()}).then(function(html){restoreReq();_log('flushSave ok',html.slice(0,80));snapshotHash();window._mobileNewNoteId=null;setSaveState('Saved','Saved');if(callback)callback(true)})}).catch(function(err){restoreReq();_log('flushSave error',err);showOffline();if(callback)callback(false)})} function shouldInterceptNavigationClick(target){var navTarget=target&&target.closest?target.closest('.notelist-item,.sidebar-item,.nav-folder-row,[hx-get],[hx-post],[hx-delete]'):null;if(!navTarget)return null;if(navTarget.closest&&navTarget.closest('#note-editor-form'))return null;if(navTarget.closest&&navTarget.closest('#folder-context-menu,#folder-modal,#link-modal,#history-modal,#code-modal,#new-folder-modal,#vault-modal'))return null;return navTarget} document.addEventListener('click',function(e){var navTarget=shouldInterceptNavigationClick(e.target);if(!navTarget)return;var form=activeEditorForm();var status=queryActiveEditor('#autosave-status');var dirty=status&&status.querySelector('.autosave-edited');if(!form||!dirty)return;_log('navigation click intercepted, flushing save',navTarget.className||navTarget.id||navTarget.tagName);e.preventDefault();e.stopImmediatePropagation();flushSave(function(saved){if(saved){_log('flushSave done, re-clicking navigation target');navTarget.click()}})},true); window.joplockLiveSearch=_cfg.liveSearch||false; (function(){var _navSearchSavedValue=null;function enableLiveSearch(){var el=document.getElementById('nav-search');if(!el||!window.joplockLiveSearch||el.dataset.liveSearch)return;el.dataset.liveSearch='1';el.setAttribute('hx-trigger','search-submit, input changed delay:300ms');el.addEventListener('htmx:beforeRequest',function(e){var v=el.value;if(v.length>0&&v.length<3){e.preventDefault();return}});htmx.process(el)}function restoreNavSearch(){if(_navSearchSavedValue===null)return;var el=document.getElementById('nav-search');if(!el){_navSearchSavedValue=null;return;}el.value=_navSearchSavedValue;el.selectionStart=el.selectionEnd=el.value.length;_navSearchSavedValue=null}enableLiveSearch();document.body.addEventListener('htmx:beforeSwap',function(e){var target=e.detail&&e.detail.target;if(target&&target.id==='nav-panel'){var el=document.getElementById('nav-search');if(el)_navSearchSavedValue=el.value}});document.body.addEventListener('htmx:afterSettle',function(){enableLiveSearch();restoreNavSearch()})})(); function confirmLogout(event){ var ok=window.confirm('Log out?\n\nThis clears local data on this device, including the current session and saved UI state. Your notes and other server data remain on the server.'); if(!ok&&event)event.preventDefault(); return ok; } // --- Mobile navigation --- // SINGLE-SCREEN INVARIANT: Exactly one .mobile-screen carries .mobile-screen-active at any time. // All transitions MUST go through setMobileState(). Direct DOM toggling is forbidden. // renderMobile() is the only function that writes .mobile-screen-active and screen-driven UI // (titles, FAB). assertSingleActiveScreen() enforces the invariant after every render. (function(){ // Canonical state. Mutated only by setMobileState() (which calls renderMobile()). var _state={screen:'folders',folderId:'',folderTitle:'',noteId:'',noteTitle:''}; var _prevRenderedScreen=null; var _mobileInitDone=false; var _lastSyncWasMobile=null;// null=first call, true/false=previous syncResponsiveMode result function isMobile(){return isMobileShellMode()} function mobileScreenId(name){return'mobile-'+name+'-screen'} function assertSingleActiveScreen(){ var active=document.querySelectorAll('.mobile-screen.mobile-screen-active'); if(active.length===1)return; _trace('mobile-invariant-violation',{count:active.length,expected:_state.screen,ids:Array.prototype.map.call(active,function(e){return e.id})}); // Self-heal: force exactly one active. var screens=['folders','notes','editor']; screens.forEach(function(s){ var el=document.getElementById(mobileScreenId(s)); if(el)el.classList.toggle('mobile-screen-active',s===_state.screen); }); } // The ONLY function that writes .mobile-screen-active and screen-driven UI. function renderMobile(){ var screens=['folders','notes','editor']; screens.forEach(function(s){ var el=document.getElementById(mobileScreenId(s)); if(!el)return; el.classList.remove('mobile-screen-left','mobile-screen-right'); el.classList.toggle('mobile-screen-active',s===_state.screen); }); // Titles var notesTitle=document.getElementById('mobile-notes-title'); if(notesTitle&&_state.folderTitle)notesTitle.textContent=_state.folderTitle; var editorTitle=document.getElementById('mobile-editor-title'); if(editorTitle&&_state.noteTitle&&_prevRenderedScreen!=='editor')editorTitle.textContent=_state.noteTitle; // FAB var fab=document.getElementById('mobile-fab'); if(fab){ var fabVisible=_state.screen==='folders'||_state.screen==='notes'; fab.style.display=fabVisible?'flex':'none'; if(!fabVisible)mobileFabClose(); } // Editor search header should not persist across screen changes if(_state.screen!=='editor'&&_prevRenderedScreen==='editor'){ window.mobileEditorSearchClose&&window.mobileEditorSearchClose(); } _prevRenderedScreen=_state.screen; assertSingleActiveScreen(); } // THE one entry point for all mobile screen transitions. function setMobileState(patch){ if(!patch)return; Object.keys(patch).forEach(function(k){_state[k]=patch[k]}); renderMobile(); } // Read-only state access for debugging. window.joplockMobileState=function(){return JSON.parse(JSON.stringify(_state))}; window.mobilePushNotes=function(folderId,folderTitle){ if(!isMobile())return; setMobileState({screen:'notes',folderId:folderId,folderTitle:folderTitle||'Notes'}); var body=document.getElementById('mobile-notes-body');if(body)body.innerHTML='
Loading...
'; htmx.ajax('GET','/fragments/mobile/notes?folderId='+encodeURIComponent(folderId),{target:'#mobile-notes-body',swap:'innerHTML'}); }; window.mobilePushEditor=function(noteId,folderId){ if(!isMobile())return; setMobileState({screen:'editor',noteId:noteId,folderId:folderId||_state.folderId}); _trace('mobilePushEditor-start',{noteId:noteId,folderId:folderId||_state.folderId}); var body=document.getElementById('mobile-editor-body');if(body)body.innerHTML='
'; htmx.ajax('GET','/fragments/editor/'+encodeURIComponent(noteId)+'?currentFolderId='+encodeURIComponent(folderId||_state.folderId),{target:'#mobile-editor-body',swap:'innerHTML'}).then(function(){_trace('mobilePushEditor-ok',{noteId:noteId});hideNoteOverlay()}).catch(function(err){_trace('mobilePushEditor-err',{noteId:noteId,error:err&&err.message?err.message:String(err)});hideNoteOverlay()}); }; // Back-navigation: deterministic editor->notes->folders. function mobileBack(){ if(_state.screen==='editor'){ setMobileState({screen:_state.folderId?'notes':'folders'}); return'notes-or-folders'; } if(_state.screen==='notes'){ setMobileState({screen:'folders'}); return'folders'; } return'folders'; } window.mobilePopScreen=function(){ if(!isMobile())return; var prev=_state.screen; var dest=mobileBack(); if(prev==='editor'&&dest==='folders'){ // flush any dirty save when leaving editor flushSave(function(){}) } }; window.mobileEditorBack=function(){ var form=document.getElementById('note-editor-form'); if(form&&form.dataset.encrypted==='1'){ setMobileState({screen:'folders'}); return; } var titleEl=form&&form.querySelector('.editor-title'); var bodyEl=form&&form.querySelector('#note-body'); var noteId=_state.noteId; var title=((titleEl&&titleEl.textContent)||'').trim(); var body=((bodyEl&&bodyEl.value)||'').trim(); var shouldDiscard=!!(window._mobileNewNoteId&¬eId===window._mobileNewNoteId&&!body&&(title===''||title==='Untitled note')); if(shouldDiscard){ fetch('/fragments/notes/'+encodeURIComponent(noteId),{method:'DELETE',headers:{'hx-request':'true','hx-params':'none'}}) .then(function(){window._mobileNewNoteId=null;mobileRefreshNotes();mobilePopScreen()}) .catch(function(){mobilePopScreen()}); return; } flushSave(function(){mobileRefreshNotes();mobilePopScreen()}); }; // Wire mobile delete button after editor loads function wireMobileDeleteBtn(noteId,isDeleted){ var btn=document.getElementById('mobile-delete-btn'); if(!btn)return; btn.onclick=function(){ var msg=isDeleted?'Permanently delete this note?':'Move this note to trash?'; if(!confirm(msg))return; fetch('/fragments/notes/'+encodeURIComponent(noteId),{method:'DELETE',headers:{'hx-request':'true','hx-params':'none'}}) .then(function(){mobilePopScreen();mobileRefreshNotes()}); }; } function mobileRefreshNotes(){ if(_state.folderId){ var body=document.getElementById('mobile-notes-body'); if(body)htmx.ajax('GET','/fragments/mobile/notes?folderId='+encodeURIComponent(_state.folderId),{target:'#mobile-notes-body',swap:'innerHTML'}); } } window.mobileNewNote=function(){ var fid=_state.screen==='notes'?_state.folderId:''; htmx.ajax('POST','/fragments/mobile/notes/new',{target:'#mobile-notes-body',swap:'innerHTML',values:{folderId:fid||''}}); }; window.mobileFabOpen=function(){ if(_state.screen==='notes') return mobileNewNote(); var b=document.getElementById('mobile-fab-menu-backdrop'); var m=document.getElementById('mobile-fab-menu'); if(b)b.style.display=''; if(m)m.style.display=''; }; window.mobileFabClose=function(){ var b=document.getElementById('mobile-fab-menu-backdrop'); var m=document.getElementById('mobile-fab-menu'); if(b)b.style.display='none'; if(m)m.style.display='none'; }; window.mobileFabNewNote=function(){ mobileFabClose(); setMobileState({screen:'notes',folderId:'__all__',folderTitle:'All Notes'}); mobileNewNote(); }; window.mobileFabNewFolder=function(){ mobileFabClose(); openNewFolderModal('mobile'); }; window.mobileNewNoteInFolder=function(folderId,folderTitle,event){ if(event){event.preventDefault();event.stopPropagation();} setMobileState({screen:'notes',folderId:folderId,folderTitle:folderTitle||'Notes'}); mobileNewNote(); }; // Context menu (long-press on note rows) var _ctxNoteId=null,_ctxNoteTitle=null,_ctxLongPressTimer=null; function mobileCtxOpen(noteId,noteTitle){ _ctxNoteId=noteId;_ctxNoteTitle=noteTitle; var backdrop=document.getElementById('mobile-ctx-backdrop'); var sheet=document.getElementById('mobile-ctx-sheet'); var titleEl=document.getElementById('mobile-ctx-title'); var metaEl=document.getElementById('mobile-ctx-meta'); var moveBtn=document.getElementById('mobile-ctx-move'); var delBtn=document.getElementById('mobile-ctx-delete'); if(titleEl)titleEl.textContent=noteTitle||'Untitled'; if(metaEl){ var mbody=document.getElementById('mobile-editor-body'); var metaSrc=mbody?mbody.querySelector('#note-meta'):null; if(!metaSrc)metaSrc=document.getElementById('status-note-meta'); var c=metaSrc?Number(metaSrc.getAttribute('data-created-time')||0):0; var u=metaSrc?Number(metaSrc.getAttribute('data-updated-time')||0):0; if(c||u){ var months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; var fmt=function(ts){if(!ts)return '';var d=new Date(ts);return String(d.getDate()).padStart(2,'0')+'-'+months[d.getMonth()]+'-'+String(d.getFullYear()).slice(-2)}; metaEl.textContent='Created '+fmt(c)+' \u2022 Edited '+fmt(u); metaEl.style.display=''; }else{ metaEl.textContent=''; metaEl.style.display='none'; } } if(moveBtn)moveBtn.onclick=function(){mobileCtxMove()}; if(delBtn)delBtn.onclick=function(){mobileCtxDelete()}; if(backdrop)backdrop.style.display=''; if(sheet)sheet.style.display=''; } window.mobileCtxClose=function(){ var backdrop=document.getElementById('mobile-ctx-backdrop'); var sheet=document.getElementById('mobile-ctx-sheet'); if(backdrop)backdrop.style.display='none'; if(sheet)sheet.style.display='none'; _ctxNoteId=null;_ctxNoteTitle=null; }; window.mobileFolderPickerClose=function(){ var backdrop=document.getElementById('mobile-folder-picker-backdrop'); var sheet=document.getElementById('mobile-folder-picker-sheet'); var list=document.getElementById('mobile-folder-picker-list'); if(backdrop)backdrop.style.display='none'; if(sheet)sheet.style.display='none'; if(list)list.innerHTML=''; }; function mobileCtxDelete(){ if(!_ctxNoteId)return; var id=_ctxNoteId; mobileCtxClose(); if(_cfg.confirmTrash!==false&&!confirm('Move this note to trash?'))return; fetch('/fragments/notes/'+encodeURIComponent(id),{method:'DELETE',headers:{'hx-request':'true','hx-params':'none'}}) .then(function(){mobileRefreshNotes()}); } function mobileCtxMove(){ var form=activeEditorForm(); var select=form&&form.querySelector?form.querySelector('#editor-folder-select'):null; if(!form||!select)return; mobileCtxClose(); var options=Array.prototype.slice.call(select.options||[]); if(!options.length)return; var current=select.value||''; var backdrop=document.getElementById('mobile-folder-picker-backdrop'); var sheet=document.getElementById('mobile-folder-picker-sheet'); var list=document.getElementById('mobile-folder-picker-list'); if(!backdrop||!sheet||!list)return; list.innerHTML=''; options.forEach(function(opt){ var btn=document.createElement('button'); btn.type='button'; btn.className='mobile-ctx-btn mobile-folder-picker-btn'+(opt.value===current?' is-current':''); btn.textContent=opt.text+(opt.value===current?' (current)':''); btn.disabled=opt.value===current; btn.onclick=function(){ window.mobileFolderPickerClose(); select.value=opt.value; select.dispatchEvent(new Event('change',{bubbles:true})); }; list.appendChild(btn); }); backdrop.style.display=''; sheet.style.display=''; } window.mobileEditorMenuOpen=function(){ var form=activeEditorForm(); if(!form)return; var titleInput=form.querySelector('.editor-title'); mobileCtxOpen(form.dataset.noteId||_state.noteId,(titleInput&&titleInput.textContent)||document.getElementById('mobile-editor-title')&&document.getElementById('mobile-editor-title').textContent||'Untitled'); }; function wireNoteRowLongPress(container){ if(!container)return; container.querySelectorAll('.mobile-note-row[data-note-id]').forEach(function(row){ if(row.dataset.lpWired)return; row.dataset.lpWired='1'; row.addEventListener('touchstart',function(e){ var id=row.dataset.noteId,title=row.dataset.noteTitle; _ctxLongPressTimer=setTimeout(function(){ e.preventDefault(); mobileCtxOpen(id,title); },500); },{passive:true}); row.addEventListener('touchend',function(){if(_ctxLongPressTimer){clearTimeout(_ctxLongPressTimer);_ctxLongPressTimer=null}}); row.addEventListener('touchmove',function(){if(_ctxLongPressTimer){clearTimeout(_ctxLongPressTimer);_ctxLongPressTimer=null}}); }); wireFolderRowLongPress(container); } var _folderCtxId=null,_folderCtxTitle=null,_folderCtxLongPressTimer=null; function mobileFolderCtxOpen(folderId,folderTitle){ _folderCtxId=folderId;_folderCtxTitle=folderTitle||'Untitled'; var backdrop=document.getElementById('mobile-folder-ctx-backdrop'); var sheet=document.getElementById('mobile-folder-ctx-sheet'); var titleEl=document.getElementById('mobile-folder-ctx-title'); var renameBtn=document.getElementById('mobile-folder-ctx-rename'); var delBtn=document.getElementById('mobile-folder-ctx-delete'); if(titleEl)titleEl.textContent=_folderCtxTitle; if(renameBtn)renameBtn.onclick=function(){mobileFolderCtxRename()}; if(delBtn)delBtn.onclick=function(){mobileFolderCtxDelete()}; if(backdrop)backdrop.style.display=''; if(sheet)sheet.style.display=''; } window.mobileFolderCtxClose=function(){ var backdrop=document.getElementById('mobile-folder-ctx-backdrop'); var sheet=document.getElementById('mobile-folder-ctx-sheet'); if(backdrop)backdrop.style.display='none'; if(sheet)sheet.style.display='none'; }; function mobileFolderCtxRename(){ if(!_folderCtxId)return; _folderMenuState={id:_folderCtxId,title:_folderCtxTitle}; window.mobileFolderCtxClose(); openFolderModal(); } function mobileFolderCtxDelete(){ if(!_folderCtxId)return; var id=_folderCtxId,title=_folderCtxTitle; window.mobileFolderCtxClose(); if(!confirm('Delete notebook "'+(title||'Untitled')+'"?'))return; fetch('/fragments/folders/'+encodeURIComponent(id),{method:'DELETE',headers:{'hx-request':'true'}}) .then(function(){htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'})}); } function wireFolderRowLongPress(container){ if(!container)return; container.querySelectorAll('.mobile-folder-row').forEach(function(row){ if(row.dataset.flpWired)return; var onclickAttr=row.getAttribute('onclick')||''; var m=onclickAttr.match(/mobilePushNotes\(\s*(?:"([^"]+)"|'([^']+)')\s*,\s*(?:"([^"]*)"|'([^']*)')/); if(!m)return; var fid=m[1]||m[2]||''; var ftitle=m[3]||m[4]||''; if(!fid||fid==='__all__')return; row.dataset.flpWired='1'; row.addEventListener('touchstart',function(e){ _folderCtxLongPressTimer=setTimeout(function(){ e.preventDefault(); mobileFolderCtxOpen(fid,ftitle); },500); },{passive:true}); row.addEventListener('touchend',function(){if(_folderCtxLongPressTimer){clearTimeout(_folderCtxLongPressTimer);_folderCtxLongPressTimer=null}}); row.addEventListener('touchmove',function(){if(_folderCtxLongPressTimer){clearTimeout(_folderCtxLongPressTimer);_folderCtxLongPressTimer=null}}); row.addEventListener('contextmenu',function(e){e.preventDefault();mobileFolderCtxOpen(fid,ftitle)}); }); } // Search var _mobileSearchTimer=null; window.mobileSearchOpen=function(){ var fh=document.getElementById('mobile-folders-header'); var sh=document.getElementById('mobile-search-header'); var inp=document.getElementById('mobile-search-input'); if(fh)fh.style.display='none'; if(sh)sh.style.display=''; if(inp){inp.value='';inp.focus()} var body=document.getElementById('mobile-folders-body'); if(body)body.innerHTML=''; }; window.mobileSearchClose=function(){ var fh=document.getElementById('mobile-folders-header'); var sh=document.getElementById('mobile-search-header'); if(fh)fh.style.display=''; if(sh)sh.style.display='none'; htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'}); }; window.mobileSearchQuery=function(q){ if(_mobileSearchTimer)clearTimeout(_mobileSearchTimer); if(!q||!q.trim()){var body=document.getElementById('mobile-folders-body');if(body)body.innerHTML='';return} if(q.trim().length<2)return; _mobileSearchTimer=setTimeout(function(){ htmx.ajax('GET','/fragments/mobile/search?q='+encodeURIComponent(q.trim()),{target:'#mobile-folders-body',swap:'innerHTML'}); },300); }; window.mobileEditorSearchOpen=function(){ var header=document.getElementById('mobile-editor-header'); var searchHeader=document.getElementById('mobile-editor-search-header'); var input=document.getElementById('mobile-editor-search-input'); if(header)header.style.display='none'; if(searchHeader)searchHeader.style.display=''; if(input&&!input.value){var pending=window._pendingNoteSearchTerm||'';var listTerm=currentListSearchTerm();var seed=(pending&&pending.trim())||(listTerm&&listTerm.trim())||'';if(seed)input.value=seed;window._pendingNoteSearchTerm=''} if(input){input.focus();input.select();applySearchHighlight()} }; window.mobileEditorSearchClose=function(){ var header=document.getElementById('mobile-editor-header'); var searchHeader=document.getElementById('mobile-editor-search-header'); var mobileBar=document.getElementById('mobile-search-nav-bar'); var input=document.getElementById('mobile-editor-search-input'); if(input)input.value=''; if(searchHeader)searchHeader.style.display='none'; if(mobileBar)mobileBar.hidden=true; if(header)header.style.display=''; searchNavDismiss(); }; window.mobileEditorSearchQuery=function(){applySearchHighlight()}; function mobileInit(){ if(!isMobile())return; _trace('mobileInit-start',{initDone:_mobileInitDone}); document.getElementById('mobile-app').setAttribute('aria-hidden','false'); // Reset any stale active classes; renderMobile() (via setMobileState below) sets the correct one. ['folders','notes','editor'].forEach(function(name){ var screen=document.getElementById(mobileScreenId(name)); if(!screen)return; screen.classList.remove('mobile-screen-active','mobile-screen-left','mobile-screen-right'); screen.style.pointerEvents=''; }); if(_mobileInitDone){renderMobile();return} _mobileInitDone=true; // Check if server pre-rendered a note into mobile-editor-body (resumeLastNote) var startup=_mobileStartup; if(startup&&startup.noteId){ setMobileState({ screen:'editor', folderId:startup.folderId||'', folderTitle:startup.folderTitle||'Notes', noteId:startup.noteId, noteTitle:startup.noteTitle||'Note' }); // SSR already rendered editor content — init it directly, fetch lists in background initEditorPanel(); htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'}); if(_state.folderId)htmx.ajax('GET','/fragments/mobile/notes?folderId='+encodeURIComponent(_state.folderId),{target:'#mobile-notes-body',swap:'innerHTML'}); }else{ // Fresh load: start at folders screen setMobileState({screen:'folders'}); htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'}); } var fab=document.getElementById('mobile-fab'); if(fab&&!fab.dataset.debugWired)fab.dataset.debugWired='1'; // Swipe right to go back var startX=0,startY=0,swiping=false; document.getElementById('mobile-app').addEventListener('touchstart',function(e){startX=e.touches[0].clientX;startY=e.touches[0].clientY;swiping=true},{passive:true}); document.getElementById('mobile-app').addEventListener('touchend',function(e){ if(!swiping)return;swiping=false; var dx=e.changedTouches[0].clientX-startX; var dy=e.changedTouches[0].clientY-startY; if(Math.abs(dx)>Math.abs(dy)*1.5&&dx>60&&_state.screen!=='folders'){mobileEditorBack()} },{passive:true}); } // Redraw the current mobile screen after a shell switch (no reload needed) function redrawMobileUI(){ if(!isMobile())return; _trace('redrawMobileUI',{state:_state}); // Re-assert current state (renderMobile picks up DOM that may have been stale). renderMobile(); if(_state.screen==='editor'&&_state.noteId){ // Re-fetch editor; lists refresh after editor settles var body=document.getElementById('mobile-editor-body');if(body)body.innerHTML='
'; htmx.ajax('GET','/fragments/editor/'+encodeURIComponent(_state.noteId)+'?currentFolderId='+encodeURIComponent(_state.folderId),{target:'#mobile-editor-body',swap:'innerHTML'}).then(function(){ htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'}); if(_state.folderId)htmx.ajax('GET','/fragments/mobile/notes?folderId='+encodeURIComponent(_state.folderId),{target:'#mobile-notes-body',swap:'innerHTML'}); }); }else if(_state.screen==='notes'){ htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'}); if(_state.folderId)htmx.ajax('GET','/fragments/mobile/notes?folderId='+encodeURIComponent(_state.folderId),{target:'#mobile-notes-body',swap:'innerHTML'}); }else{ setMobileState({screen:'folders'}); htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'}); } } function syncResponsiveMode(){ var mobile=isMobile(); // For auto mode, ensure body classes reflect current viewport so CSS overrides work if(_uiMode==='auto'){ document.body.classList.toggle('force-mobile',mobile); document.body.classList.toggle('force-desktop',!mobile); } if(mobile){ if(!_mobileInitDone){ mobileInit(); }else if(_lastSyncWasMobile===false){ // Only redraw if we just crossed from desktop→mobile document.getElementById('mobile-app').setAttribute('aria-hidden','false'); redrawMobileUI(); } _lastSyncWasMobile=true; return; } var wasMobile=_lastSyncWasMobile; _lastSyncWasMobile=false; var app=document.getElementById('mobile-app'); if(app){ // Blur any focused element inside mobile-app before hiding to avoid aria-hidden warning var focused=app.contains(document.activeElement)?document.activeElement:null; if(focused)focused.blur(); app.setAttribute('aria-hidden','true'); } mobileFabClose(); mobileCtxClose(); var fab=document.getElementById('mobile-fab'); if(fab)fab.style.display='none'; var foldersHeader=document.getElementById('mobile-folders-header'); var searchHeader=document.getElementById('mobile-search-header'); if(foldersHeader)foldersHeader.style.display=''; if(searchHeader)searchHeader.style.display='none'; setMobileNav(false); // Mirror redrawMobileUI: if we just crossed mobile→desktop, re-init desktop UI. // Without this, a session that started in mobile mode never calls initNavPanel/ // initEditorPanel for the desktop form, leaving the preview/CM host in an // uninitialized state (both visible, wrong display values, stale scroll). if(wasMobile===true){initNavPanel();initEditorPanel();} } function initMobileToolbar(){ var tb=document.getElementById('editor-toolbar'); if(!tb||!inMobileEditor())return; if(tb.dataset.mobileToolbarInit==='1'){syncEditorModeButtons();return} tb.dataset.mobileToolbarInit='1'; tb.style.position='fixed'; tb.style.left='0';tb.style.right='0'; tb.style.bottom='0'; tb.style.zIndex='50'; tb.style.background='var(--bg-side)'; tb.style.borderTop='1px solid var(--border)'; // Adjust editor body padding so toolbar doesn't overlap content var body=document.getElementById('mobile-editor-body'); if(body)body.style.paddingBottom='90px'; tb.style.display='flex'; function positionToolbar(){ if(!inMobileEditor()||!tb)return; var vv=window.visualViewport; // Use innerHeight - vv.height so toolbar clears keyboard + iOS accessory bar var keyboardH=vv?Math.max(0,window.innerHeight-vv.height):0; tb.style.bottom=keyboardH+'px'; } if(window.visualViewport){ window.visualViewport.addEventListener('resize',positionToolbar); window.visualViewport.addEventListener('scroll',positionToolbar); } positionToolbar(); syncEditorModeButtons(); } // Update editor title when editor loads document.body.addEventListener('htmx:afterSettle',function(e){ var t=e.detail&&e.detail.target; if(t&&t.id==='mobile-editor-body'){ _trace('mobile-editor-settle-start'); if(_cmView){_cmView.destroy();_cmView=null} initEditorPanel(); var titleHidden=t.querySelector('.editor-title-hidden'); var titleEl=document.getElementById('mobile-editor-title'); if(titleEl&&titleHidden)titleEl.textContent=titleHidden.value||'Note'; var mobileStatus=document.getElementById('mobile-editor-status'); if(mobileStatus){ var dirty=t.querySelector('#autosave-status .autosave-edited'); var saved=t.querySelector('#autosave-status .autosave-ok'); mobileStatus.innerHTML=dirty?'Edited':(saved?'Saved':''); } // Hide desktop titlebar in mobile editor var titlebar=t.querySelector('.editor-titlebar'); if(titlebar&&isMobile())titlebar.style.display='none'; // Wire delete button var form=t.querySelector('#note-editor-form'); var noteId=form?decodeURIComponent((form.getAttribute('hx-put')||'').replace('/fragments/editor/','')):''; _trace('mobile-editor-settle-done',{hasForm:!!form,noteId:noteId,spinner:!!t.querySelector('.mobile-loading-note,.note-loading-ring')}); var isDeleted=!!t.querySelector('.btn-danger[hx-confirm*="Permanently"]'); wireMobileDeleteBtn(noteId,isDeleted); // Show FAB only when on notes screen var fab=document.getElementById('mobile-fab');if(fab)fab.style.display='none'; // Position toolbar above keyboard using visualViewport initMobileToolbar(); } if(t&&(t.id==='mobile-notes-body'||t.id==='mobile-folders-body')){ var fab=document.getElementById('mobile-fab'); var editorActive=!!document.querySelector('#mobile-editor-screen.mobile-screen-active'); if(fab)fab.style.display=editorActive?'none':'flex'; wireNoteRowLongPress(t); } }); // Handle new note response: push to editor document.body.addEventListener('htmx:afterRequest',function(e){ var t=e.detail&&e.detail.target; if(t&&t.id==='mobile-notes-body'){ var xhr=e.detail.xhr; var noteId=xhr&&xhr.getResponseHeader('X-Mobile-Note-Id'); if(noteId){window._mobileNewNoteId=noteId;mobilePushEditor(noteId,_state.folderId)} } }); window._syncResponsiveMode=syncResponsiveMode; window.addEventListener('resize',handleViewportResize); window.addEventListener('orientationchange',handleViewportResize); syncResponsiveMode(); })(); // --- Encryption UI flows (vault-centric) --- // _vaultModal: modal for creating vault password or unlocking a vault var _vaultModalFolderId=null; var _vaultModalMode=null; // 'create' | 'unlock' var _vaultModalCallback=null; // called with success/failure function _showVaultModal(folderId,mode,callback){ _vaultModalFolderId=folderId; _vaultModalMode=mode; _vaultModalCallback=callback; var modal=document.getElementById('vault-modal'); var backdrop=document.getElementById('vault-modal-backdrop'); var titleEl=document.getElementById('vault-modal-title'); var pw=document.getElementById('vault-modal-password'); var confirm=document.getElementById('vault-modal-confirm-wrap'); var warn=document.getElementById('vault-modal-warning'); var err=document.getElementById('vault-modal-error'); if(err)err.textContent=''; if(pw)pw.value=''; if(mode==='create'){ if(titleEl)titleEl.textContent='Create Vault'; if(confirm)confirm.style.display=''; if(warn)warn.style.display=''; }else{ if(titleEl)titleEl.textContent='Unlock Vault'; if(confirm)confirm.style.display='none'; if(warn)warn.style.display='none'; } if(modal)modal.hidden=false; if(backdrop)backdrop.hidden=false; if(pw)pw.focus(); } function closeVaultModal(){ var modal=document.getElementById('vault-modal'); var backdrop=document.getElementById('vault-modal-backdrop'); if(modal)modal.hidden=true; if(backdrop)backdrop.hidden=true; _vaultModalFolderId=null; _vaultModalMode=null; if(_vaultModalCallback){_vaultModalCallback(false);_vaultModalCallback=null} } async function submitVaultModal(event){ if(event)event.preventDefault(); var folderId=_vaultModalFolderId; var mode=_vaultModalMode; if(!folderId||!mode)return; var pw=document.getElementById('vault-modal-password'); var confirmInput=document.getElementById('vault-modal-confirm'); var err=document.getElementById('vault-modal-error'); var password=(pw?pw.value:'').trim(); if(!password){if(err)err.textContent='Password is required.';return} if(mode==='create'){ var confirmVal=(confirmInput?confirmInput.value:'').trim(); if(password!==confirmVal){if(err)err.textContent='Passwords do not match.';return} if(password.length<4){if(err)err.textContent='Password too short (minimum 4 characters).';return} try{ await createVault(folderId,password); var cb=_vaultModalCallback; _vaultModalCallback=null; _closeVaultModalSilent(); // Update nav icons to show vault unlocked _refreshVaultIcon(folderId,true); touchVaultActivity(folderId); startAutoLockTimer(); if(cb)cb(true); }catch(e){ if(err)err.textContent='Failed to create vault: '+(e.message||e); } }else{ // unlock mode try{ var ok=await unlockVault(folderId,password); if(!ok){if(err)err.textContent='Wrong password.';if(pw){pw.value='';pw.focus()}return} var cb=_vaultModalCallback; _vaultModalCallback=null; _closeVaultModalSilent(); _refreshVaultIcon(folderId,true); touchVaultActivity(folderId); startAutoLockTimer(); if(cb)cb(true); }catch(e){ if(err)err.textContent='Unlock error: '+(e.message||e); } } } function _closeVaultModalSilent(){ var modal=document.getElementById('vault-modal'); var backdrop=document.getElementById('vault-modal-backdrop'); if(modal)modal.hidden=true; if(backdrop)backdrop.hidden=true; _vaultModalFolderId=null; _vaultModalMode=null; } // Update vault folder lock icon in nav (client-side optimistic update) function _refreshVaultIcon(folderId,unlocked){ document.querySelectorAll('.vault-folder-lock[data-folder-id="'+folderId+'"]').forEach(function(el){ el.innerHTML=unlocked?SVG_LOCK_OPEN:SVG_LOCK_CLOSED; el.title=unlocked?'Lock vault':'Unlock vault'; }); // Also update note lock icons for notes in this vault document.querySelectorAll('.note-lock-icon').forEach(function(el){ var btn=el.closest('.notelist-item,.mobile-note-row'); if(btn&&btn.dataset.vaultId===folderId){ el.innerHTML=unlocked?SVG_LOCK_OPEN:SVG_LOCK_CLOSED; el.classList.toggle('note-lock-unlocked',unlocked); } }); } // Single source of truth: scan DOM for all vault folders and refresh their icons // based on isVaultUnlocked() state. Safe to call repeatedly. function refreshAllVaultIcons(){ document.querySelectorAll('.vault-folder-lock[data-folder-id]').forEach(function(el){ var folderId=el.getAttribute('data-folder-id'); if(!folderId)return; var unlocked=isVaultUnlocked(folderId); _refreshVaultIcon(folderId,unlocked); }); } // Toggle vault lock: if unlocked → lock; if locked → prompt unlock function toggleVaultLock(folderId){ var unlocked=isVaultUnlocked(folderId); _log('toggleVaultLock',folderId,{unlocked:unlocked}); if(unlocked){ // Lock the vault clearVaultKey(folderId); delete _autoLockActivity[folderId]; _refreshVaultIcon(folderId,false); // If current note belongs to this vault, close it (clear the editor) var form=activeEditorForm(); if(form){ var ta=getTA(); // Check form.dataset.vaultId first (set after unlock when textarea is plaintext) // Fall back to parsing the body (if still encrypted) var bodyVault=form.dataset.vaultId||( ta?getBodyVaultId(ta.value):null); _log('toggleVaultLock close-check',folderId,{formVaultId:form.dataset.vaultId||null,bodyVault:bodyVault,noteId:form.dataset.noteId||null}); if(bodyVault===folderId){ var panel=form.closest('#editor-panel')||document.getElementById('editor-panel'); if(panel)panel.innerHTML='
Select a note
'; } } }else{ _showVaultModal(folderId,'unlock',function(ok){ _log('toggleVaultLock unlock-callback',folderId,{ok:ok}); if(ok){ // Auto-decrypt if current note belongs to this vault var form=activeEditorForm(); if(form){ var noteId=form.dataset.noteId; var ta=getTA(); if(ta&&isEncryptedBody(ta.value)){ var bodyVault=getBodyVaultId(ta.value); _log('toggleVaultLock unlock-open-note-check',folderId,{bodyVault:bodyVault,noteId:noteId}); if(bodyVault===folderId){ getVaultKey(folderId).then(function(key){ if(!key)return; return _decryptWithKey(ta.value,key).then(function(pt){_completeUnlock(noteId,pt,folderId)}); }).catch(function(){}); } } } } }); } } // lockNote: lock a currently-unlocked note (by encrypting it into its vault) // If the note is not in a vault, do nothing (encryption requires vault) function lockNote(noteId){ var form=activeEditorForm(); if(!form)return; var ta=getTA(); if(!ta)return; // Determine vault from editor context (parentId select) var parentSelect=form.querySelector('[name="parentId"]'); var folderId=parentSelect?parentSelect.value:''; if(!folderId){ alert('Please move this note to a vault folder before encrypting it.'); return; } if(isVaultUnlocked(folderId)){ // Encrypt immediately _doEncryptNoteInVault(noteId,folderId); }else{ // Need to unlock vault first _showVaultModal(folderId,'unlock',function(ok){ if(ok)_doEncryptNoteInVault(noteId,folderId); }); } } async function _doEncryptNoteInVault(noteId,folderId){ try{ var ta=getTA(); if(!ta)return; var plaintext=ta.value; if(isEncryptedBody(plaintext)){_log('note already encrypted');return} var key=await getVaultKey(folderId); if(!key){_log('vault key missing');return} var salt=getVaultSalt(folderId); var ciphertext=await encryptForVault(plaintext,folderId,key,salt); touchVaultActivity(folderId); var form=activeEditorForm(); if(form){ form.dataset.encrypted='1'; form.dataset.vaultId=folderId; var restoreBodyField=_setOneShotEncryptedBody(form,ciphertext); htmx.trigger(form,'joplock:save'); setTimeout(restoreBodyField,0); } _updateLockToggle(noteId,true); _updateNoteLockIcon(noteId,true); }catch(e){ _log('_doEncryptNoteInVault error',e); alert('Encryption failed: '+e.message); } } // unlockNote: called from the locked editor overlay async function unlockNote(noteId){ var passwordInput=document.getElementById('editor-locked-password'); var errEl=document.getElementById('editor-locked-error'); var ta=getTA(); if(!ta)return; var form=activeEditorForm(); // Determine vaultId from the ciphertext var vaultId=getBodyVaultId(ta.value)||((form&&form.dataset.vaultId)||null); var encryptedBody=isEncryptedBody(ta.value); _log('unlockNote start',{noteId:noteId,vaultId:vaultId,encryptedBody:encryptedBody,formVaultId:form&&form.dataset.vaultId||null}); // Special case: note belongs to a vault but body is still plaintext in storage. // Unlock the vault, immediately encrypt+save this note, then keep editing. if(vaultId&&!encryptedBody){ var passwordPlain=(passwordInput?passwordInput.value:''); if(!isVaultUnlocked(vaultId)){ if(!passwordPlain){if(errEl)errEl.textContent='Enter vault password.';return} var unlockedPlain=await unlockVault(vaultId,passwordPlain); if(!unlockedPlain){if(errEl)errEl.textContent='Wrong password.';if(passwordInput){passwordInput.value='';passwordInput.focus()}return} } _completeUnlock(noteId,ta.value,vaultId); _doEncryptNoteInVault(noteId,vaultId); return; } // Try auto-unlock with cached vault key if(vaultId&&isVaultUnlocked(vaultId)){ try{ var key=await getVaultKey(vaultId); if(key){ var plaintext=await _decryptWithKey(ta.value,key); _completeUnlock(noteId,plaintext,vaultId); return; } }catch(e){_log('auto-unlock failed')} } // Manual password entered var password=(passwordInput?passwordInput.value:''); if(!password&&!vaultId){if(errEl)errEl.textContent='Enter a password.';return} if(!password&&vaultId){ // Try opening vault modal _showVaultModal(vaultId,'unlock',function(ok){ if(ok)unlockNote(noteId); }); return; } // Try to unlock vault with typed password if(vaultId){ try{ var ok=await unlockVault(vaultId,password); if(ok){ var key=await getVaultKey(vaultId); var plaintext=await _decryptWithKey(ta.value,key); _completeUnlock(noteId,plaintext,vaultId); return; } }catch(e){} // Fall through to v1 compat decrypt attempt } // v1 compat or orphaned note: try password directly (note has embedded salt) try{ var plaintext=await decryptBody(password,ta.value); _completeUnlock(noteId,plaintext,null); }catch(e){ if(errEl)errEl.textContent='Wrong password.'; if(passwordInput){passwordInput.value='';passwordInput.focus()} } } // _completeUnlock: shows plaintext in editor. vaultId may be null for v1 notes. function _completeUnlock(noteId,plaintext,vaultId){ if(vaultId)touchVaultActivity(vaultId); var ta=getTA(); var lockedDiv=document.getElementById('editor-locked'); var host=queryActiveEditor('#cm-host'); var pv=queryActiveEditor('#note-preview'); var tb=queryActiveEditor('#editor-toolbar'); var form=activeEditorForm(); if(ta){ ta.dataset.ciphertext=ta.value; ta.value=plaintext; ta.style.display=''; } if(form){ form.dataset.encrypted='1'; if(vaultId)form.dataset.vaultId=vaultId; form.dataset.vaultUnlocked='1'; } if(lockedDiv)lockedDiv.style.display='none'; if(tb)tb.style.display=''; var mdBtn=document.getElementById('markdown-toggle'); var pvBtn=document.getElementById('preview-toggle'); if(mdBtn)mdBtn.style.display=''; if(pvBtn)pvBtn.style.display=''; // Use user's default open mode if(form)delete form.dataset.editorMode; var defaultMode=_defaultNoteOpenMode||'preview'; if(defaultMode==='preview'&&pv){ pv.style.display=''; fetch('/fragments/preview',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'body='+encodeURIComponent(plaintext)}).then(function(r){return r.text()}).then(function(h){pv.innerHTML=h;activatePV(pv);_previewDirty=false;if(host)host.style.display='none';_editorMode='preview';syncEditorModeButtons()}); }else{ if(pv)pv.style.display='none'; if(host){host.style.display='';initCM(host,plaintext)} _editorMode='markdown'; syncEditorModeButtons(); } _updateLockToggle(noteId,true); _updateNoteLockIcon(noteId,true); if(vaultId)_refreshVaultIcon(vaultId,true); snapshotHash(); } // toggleNoteLock: single button in titlebar function toggleNoteLock(noteId){ var form=activeEditorForm(); var isEnc=form&&form.dataset.encrypted==='1'; var vaultId=form&&form.dataset.vaultId; if(isEnc&&vaultId&&isVaultUnlocked(vaultId)){ // Vault is unlocked, note is open → lock the vault toggleVaultLock(vaultId); }else if(isEnc&&(!vaultId||!isVaultUnlocked(vaultId))){ // Encrypted note, vault locked → unlock unlockNote(noteId); }else{ // Not encrypted → lock it (encrypt into current folder's vault) lockNote(noteId); } } function _updateLockToggle(noteId,unlocked){ var btn=document.getElementById('lock-toggle-btn'); if(!btn)return; btn.innerHTML=unlocked?SVG_LOCK_OPEN:SVG_LOCK_CLOSED; btn.title=unlocked?'Lock vault':'Unlock vault'; } function _updateNoteLockIcon(noteId,unlocked){ document.querySelectorAll('.note-lock-icon[data-note-id="'+noteId+'"]').forEach(function(el){ el.innerHTML=unlocked?SVG_LOCK_OPEN:SVG_LOCK_CLOSED; el.classList.toggle('note-lock-unlocked',unlocked); }); } // --- Autosave interceptor for encrypted notes --- document.body.addEventListener('htmx:configRequest',function(e){ // no-op: encryption is handled in scheduleSave override }); var _origScheduleSave=scheduleSave; function _setOneShotEncryptedBody(form,ciphertext){ if(!form)return function(){}; var ta=form.querySelector('textarea[name="body"], textarea.editor-body'); if(!ta)return function(){}; var originalName=ta.getAttribute('name'); var hidden=document.createElement('input'); hidden.type='hidden'; hidden.name='body'; hidden.value=ciphertext; hidden.setAttribute('data-joplock-temp-body','1'); // Prevent duplicate body fields during htmx form serialization. ta.removeAttribute('name'); form.appendChild(hidden); return function(){ if(originalName!==null)ta.setAttribute('name',originalName);else ta.removeAttribute('name'); if(hidden.parentNode)hidden.parentNode.removeChild(hidden); }; } function buildFlushRequest(form){ if(!form)return Promise.resolve(null); var url=form.getAttribute('hx-put'); if(!url)return Promise.resolve(null); var pv=getPV(); if(pv)syncPV();else cmSyncToTA(); syncTitle(); var ta=getTA(); if(form.dataset.encrypted==='1'&&form.dataset.vaultId&&ta&&!isEncryptedBody(ta.value)){ return getVaultKey(form.dataset.vaultId).then(function(key){ if(!key)throw new Error('Vault is locked'); var salt=getVaultSalt(form.dataset.vaultId); return encryptForVault(ta.value,form.dataset.vaultId,key,salt).then(function(ciphertext){ var restore=_setOneShotEncryptedBody(form,ciphertext); var fd=new FormData(form); var body=new URLSearchParams(fd).toString(); return { url:url, body:body, restore:restore }; }); }); } var fd=new FormData(form); var body=new URLSearchParams(fd).toString(); return Promise.resolve({ url:url, body:body, restore:function(){} }); } scheduleSave=function(){ var form=activeEditorForm(); if(!form||form.dataset.encrypted!=='1'){_origScheduleSave();return} var noteId=form.dataset.noteId; var vaultId=form.dataset.vaultId; if(!noteId){_origScheduleSave();return} // If not a vault note, pass through if(!vaultId){_origScheduleSave();return} if(_saveTimer)clearTimeout(_saveTimer); _saveTimer=setTimeout(async function(){ _saveTimer=null; if(_syncPVInFlight||_pvSyncTimer){scheduleSave();return} if(_anyModalOpen()){scheduleSave();return} if(!form)return; var h=formHash(form); if(h===_savedHash){_log('encrypted scheduleSave skip, hash unchanged',h);return} var ta=getTA(); if(!ta)return; var plaintext=ta.value; _log('encrypted save begin',vaultId,{noteId:noteId,plaintextLength:plaintext.length,alreadyEncrypted:isEncryptedBody(plaintext)}); // Skip if somehow the textarea already holds ciphertext if(isEncryptedBody(plaintext)){_origScheduleSave();return} try{ var key=await getVaultKey(vaultId); if(!key){_log('vault key gone during save for vault',vaultId);return} var salt=getVaultSalt(vaultId); var ciphertext=await encryptForVault(plaintext,vaultId,key,salt); _log('encrypted save ciphertext ready',vaultId,{noteId:noteId,ciphertextLength:ciphertext.length,hasMarker:isEncryptedBody(ciphertext)}); var restoreBodyField=_setOneShotEncryptedBody(form,ciphertext); htmx.trigger(form,'joplock:save'); setTimeout(restoreBodyField,0); touchVaultActivity(vaultId); }catch(e){ _log('encrypted save error',e); setSaveState('Encrypt error','Error'); } },2000); }; // Auto-unlock on editor init if vault key is cached var _origInitEditorPanel=initEditorPanel; initEditorPanel=function(){ _origInitEditorPanel(); var form=activeEditorForm(); if(!form){_log('initEditorPanel vault-check: no active form');return} if(form.dataset.vaultChecked){return} form.dataset.vaultChecked='1'; _log('initEditorPanel vault-check start',{noteId:form.dataset.noteId||null,encryptedFlag:form.dataset.encrypted||null,formVaultId:form.dataset.vaultId||null}); if(form.dataset.encrypted!=='1'){_log('initEditorPanel vault-check skip: form not encrypted');return} var noteId=form.dataset.noteId; if(!noteId){_log('initEditorPanel vault-check skip: no noteId');return} var ta=getTA(); if(!ta){_log('initEditorPanel vault-check skip: no textarea');return} var initialVaultId=form.dataset.vaultId||null; var encryptedBody=isEncryptedBody(ta.value); var editorUnlocked=form.dataset.vaultUnlocked==='1'; if(!encryptedBody){ _log('initEditorPanel vault plaintext-in-vault state',{noteId:noteId,vaultId:initialVaultId,editorUnlocked:editorUnlocked,bodyPreview:ta.value.slice(0,80)}); if(editorUnlocked){ _log('initEditorPanel vault plaintext-in-vault skip: already unlocked in editor',{noteId:noteId,vaultId:initialVaultId}); return; } // Vault-bound note with plaintext body. Keep it hidden while locked; if the vault // is already unlocked, immediately encrypt+save and then reveal normally. if(initialVaultId&&isVaultUnlocked(initialVaultId)){ _log('initEditorPanel vault plaintext-in-vault auto-encrypt', {noteId:noteId,vaultId:initialVaultId}); _completeUnlock(noteId,ta.value,initialVaultId); _doEncryptNoteInVault(noteId,initialVaultId); }else{ var lockedDiv=document.getElementById('editor-locked'); var host=queryActiveEditor('#cm-host'); var pv=queryActiveEditor('#note-preview'); var tb=queryActiveEditor('#editor-toolbar'); var mdBtn=document.getElementById('markdown-toggle'); var pvBtn=document.getElementById('preview-toggle'); if(lockedDiv)lockedDiv.style.display=''; if(tb)tb.style.display='none'; if(host)host.style.display='none'; if(pv)pv.style.display='none'; if(ta)ta.style.display='none'; if(mdBtn)mdBtn.style.display='none'; if(pvBtn)pvBtn.style.display='none'; var pwField=document.getElementById('editor-locked-password'); if(pwField){ _log('initEditorPanel prompting for vault password (plaintext note in vault)',{noteId:noteId,vaultId:initialVaultId}); pwField.focus(); pwField.addEventListener('keydown',function(e){ if(e.key==='Enter'){e.preventDefault();_showVaultModal(initialVaultId,'unlock',function(ok){if(ok)window.location.reload()})} }); } } return; } // Determine vault from ciphertext var vaultId=getBodyVaultId(ta.value); _log('initEditorPanel encrypted note detected',{noteId:noteId,vaultId:vaultId,bodyLength:ta.value.length}); // Store vaultId on form for autosave if(vaultId&&form)form.dataset.vaultId=vaultId; // Try auto-unlock with cached vault key var unlocked=vaultId&&isVaultUnlocked(vaultId); _log('initEditorPanel auto-unlock decision',{noteId:noteId,vaultId:vaultId,unlocked:!!unlocked}); if(unlocked){ getVaultKey(vaultId).then(function(key){ _log('initEditorPanel cached key lookup',{noteId:noteId,vaultId:vaultId,hasKey:!!key}); if(!key)return; return _decryptWithKey(ta.value,key).then(function(pt){_completeUnlock(noteId,pt,vaultId)}); }).catch(function(){ _log('auto-unlock failed for vault',vaultId); var pwField=document.getElementById('editor-locked-password'); if(pwField)pwField.focus(); }); return; } // Focus password field and handle Enter key var pwField=document.getElementById('editor-locked-password'); if(pwField){ _log('initEditorPanel prompting for vault password',{noteId:noteId,vaultId:vaultId}); pwField.focus(); pwField.addEventListener('keydown',function(e){ if(e.key==='Enter'){e.preventDefault();unlockNote(noteId)} }); } }; // Move note: encrypt/decrypt when folder changes // Called when user changes folder via the editor folder select (function(){ document.body.addEventListener('change',function(e){ var select=e.target; if(!select||select.id!=='editor-folder-select')return; var form=activeEditorForm(); if(!form)return; var noteId=form.dataset.noteId; var ta=getTA(); if(!ta||!noteId)return; var newFolderId=select.value; var oldVaultId=form.dataset.vaultId||null; var isEnc=form.dataset.encrypted==='1'; // Determine if destination is a vault (check nav DOM for vault icon) var newFolderIsVault=!!document.querySelector('.vault-folder-lock[data-folder-id="'+newFolderId+'"]'); if(!isEnc&&!newFolderIsVault)return; // plain note to plain folder, nothing to do if(isEnc&&!newFolderIsVault){ // Moving encrypted note out of vault → decrypt it if(!oldVaultId)return; if(!isVaultUnlocked(oldVaultId)){ select.value=oldVaultId; // revert _showVaultModal(oldVaultId,'unlock',function(ok){ if(ok){select.value=newFolderId;select.dispatchEvent(new Event('change',{bubbles:true}))} }); return; } getVaultKey(oldVaultId).then(function(key){ if(!key){_log('no vault key to decrypt on move');return} return _decryptWithKey(ta.value,key).then(function(pt){ ta.value=pt; delete form.dataset.encrypted; delete form.dataset.vaultId; _updateLockToggle(noteId,false); _updateNoteLockIcon(noteId,false); htmx.trigger(form,'joplock:save'); }); }).catch(function(e){_log('decrypt on move failed',e)}); }else if(!isEnc&&newFolderIsVault){ // Moving plain note into vault → encrypt it if(!isVaultUnlocked(newFolderId)){ _showVaultModal(newFolderId,'unlock',function(ok){ if(ok)_doEncryptNoteInVault(noteId,newFolderId); }); }else{ _doEncryptNoteInVault(noteId,newFolderId); } }else if(isEnc&&newFolderIsVault&&oldVaultId!==newFolderId){ // Moving between vaults → decrypt with old, re-encrypt with new if(!oldVaultId)return; var doReencrypt=function(){ getVaultKey(oldVaultId).then(function(oldKey){ if(!oldKey){_log('no old vault key');return} return _decryptWithKey(ta.value,oldKey).then(function(pt){ if(!isVaultUnlocked(newFolderId)){ _showVaultModal(newFolderId,'unlock',function(ok){ if(!ok)return; getVaultKey(newFolderId).then(function(newKey){ var salt=getVaultSalt(newFolderId); return encryptForVault(pt,newFolderId,newKey,salt).then(function(ct){ form.dataset.vaultId=newFolderId; var restoreBodyField=_setOneShotEncryptedBody(form,ct); htmx.trigger(form,'joplock:save'); setTimeout(restoreBodyField,0); }); }).catch(function(e){_log('re-encrypt failed',e)}); }); }else{ getVaultKey(newFolderId).then(function(newKey){ var salt=getVaultSalt(newFolderId); return encryptForVault(pt,newFolderId,newKey,salt).then(function(ct){ form.dataset.vaultId=newFolderId; var restoreBodyField=_setOneShotEncryptedBody(form,ct); htmx.trigger(form,'joplock:save'); setTimeout(restoreBodyField,0); }); }).catch(function(e){_log('re-encrypt failed',e)}); } }); }).catch(function(e){_log('move between vaults failed',e)}); }; if(!isVaultUnlocked(oldVaultId)){ select.value=oldVaultId; _showVaultModal(oldVaultId,'unlock',function(ok){ if(ok){select.value=newFolderId;doReencrypt()} }); }else{ doReencrypt(); } } }); })(); // v1 migration: scan for v1 encrypted notes, offer to migrate to a vault // Called after first vault is created async function migrateV1Notes(newVaultFolderId){ try{ var resp=await fetch('/api/web/notes',{method:'GET'}); if(!resp.ok)return; var data=await resp.json(); var notes=(data.items||[]).filter(function(n){return n.isEncrypted}); if(!notes.length)return; // Check if any are v1 (no vault field) // We can't tell without fetching each note body. Check first few. var v1candidates=[]; for(var i=0;i0)alert('Migrated '+migrated+' note(s) to your new vault.'); }catch(e){_log('migrateV1Notes error',e)} } // Create vault flow: called from folder creation modal (new vault checkbox) async function submitNewVaultFolder(event){ if(event)event.preventDefault(); var modal=document.getElementById('new-folder-modal'); var origin=modal&&modal.dataset?modal.dataset.origin:''; var titleInput=document.getElementById('new-folder-title'); var pwInput=document.getElementById('new-vault-password'); var confirmInput=document.getElementById('new-vault-confirm'); var errEl=document.getElementById('new-vault-error'); var title=(titleInput?titleInput.value:'').trim(); var password=(pwInput?pwInput.value:'').trim(); var confirmVal=(confirmInput?confirmInput.value:'').trim(); if(!title){if(errEl)errEl.textContent='Notebook name is required.';return} if(!password){if(errEl)errEl.textContent='Vault password is required.';return} if(password!==confirmVal){if(errEl)errEl.textContent='Passwords do not match.';return} if(password.length<4){if(errEl)errEl.textContent='Password too short (minimum 4 characters).';return} if(errEl)errEl.textContent=''; try{ // Create folder via API var folderResp=await fetch('/api/web/folders',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({title:title}) }); if(!folderResp.ok){var ferr=await folderResp.json().catch(function(){return{}});throw new Error(ferr.error||'Failed to create notebook')} var folderData=await folderResp.json(); var folderId=(folderData.item||{}).id; if(!folderId)throw new Error('No notebook id returned'); // Create vault await createVault(folderId,password); // Check for v1 notes to migrate migrateV1Notes(folderId); // Close modal and refresh relevant notebook list closeNewFolderModal(); refreshAfterFolderCreate(origin); }catch(e){ if(errEl)errEl.textContent='Error: '+(e.message||e); } } function closeNewFolderModal(){ var modal=document.getElementById('new-folder-modal'); var backdrop=document.getElementById('new-folder-modal-backdrop'); if(modal)modal.hidden=true; if(backdrop)backdrop.hidden=true; if(modal)delete modal.dataset.origin; } function openNewFolderModal(origin){ var modal=document.getElementById('new-folder-modal'); var backdrop=document.getElementById('new-folder-modal-backdrop'); var titleInput=document.getElementById('new-folder-title'); var errEl=document.getElementById('new-vault-error'); var isVaultCheck=document.getElementById('new-folder-is-vault'); var vaultFields=document.getElementById('new-vault-fields'); var pwInput=document.getElementById('new-vault-password'); var confirmInput=document.getElementById('new-vault-confirm'); if(titleInput)titleInput.value=''; if(errEl)errEl.textContent=''; if(isVaultCheck)isVaultCheck.checked=false; if(vaultFields)vaultFields.style.display='none'; if(pwInput)pwInput.value=''; if(confirmInput)confirmInput.value=''; if(modal){ if(origin)modal.dataset.origin=origin; else delete modal.dataset.origin; } if(modal)modal.hidden=false; if(backdrop)backdrop.hidden=false; if(titleInput)titleInput.focus(); } function refreshAfterFolderCreate(origin){ if(origin==='mobile'){ var body=document.getElementById('mobile-folders-body'); if(body)htmx.ajax('GET','/fragments/mobile/folders',{target:'#mobile-folders-body',swap:'innerHTML'}); return; } htmx.ajax('GET','/fragments/nav',{target:'#nav-panel',swap:'innerHTML'}); } function toggleNewFolderVault(checked){ var fields=document.getElementById('new-vault-fields'); if(fields)fields.style.display=checked?'':'none'; } async function submitNewFolderModal(event){ if(event)event.preventDefault(); var isVaultCheck=document.getElementById('new-folder-is-vault'); var modal=document.getElementById('new-folder-modal'); var origin=modal&&modal.dataset?modal.dataset.origin:''; if(isVaultCheck&&isVaultCheck.checked){ await submitNewVaultFolder(event); }else{ // Regular folder creation var titleInput=document.getElementById('new-folder-title'); var errEl=document.getElementById('new-vault-error'); var title=(titleInput?titleInput.value:'').trim(); if(!title){if(errEl)errEl.textContent='Notebook name is required.';return} htmx.ajax('POST','/fragments/folders',{target:origin==='mobile'?'#mobile-folders-body':'#nav-panel',swap:'none',values:{title:title}}).then(function(){ refreshAfterFolderCreate(origin); }); closeNewFolderModal(); } } // Expose functions needed by inline hx-on/onclick handlers (called from global scope by htmx eval) window.isMobileShellMode=isMobileShellMode; window.closeNav=closeNav; window.toggleNav=toggleNav; window.toggleNavFolder=toggleNavFolder; window.openFolderContextMenu=openFolderContextMenu; window.editFolderFromMenu=editFolderFromMenu; window.deleteFolderFromMenu=deleteFolderFromMenu; window.closeFolderModal=closeFolderModal; window.submitFolderEdit=submitFolderEdit; window.closeLinkModal=closeLinkModal; window.submitLink=submitLink; window.closeHistoryModal=closeHistoryModal; window.openHistoryModal=openHistoryModal; window.selectHistorySnapshot=selectHistorySnapshot; window.restoreHistorySnapshot=restoreHistorySnapshot; window.setEditorMode=setEditorMode; window.wrapSel=wrapSel; window.insertPfx=insertPfx; window.insertTxt=insertTxt; window.insertStamp=insertStamp; window.clearFormat=clearFormat; window.insertLink=insertLink; window.insertImg=insertImg; window.handleFilePicker=handleFilePicker; window.uploadFiles=uploadFiles; window.uploadFile=uploadFile; window.openCodeModal=openCodeModal; window.closeCodeModal=closeCodeModal; window.submitCode=submitCode; window.handleDrop=handleDrop; window.undoSnapshot=undoSnapshot; window.searchNavStep=searchNavStep; window.searchNavDismiss=searchNavDismiss; window.syncPV=syncPV; window.getPV=getPV; window.setTheme=setTheme; window.confirmLogout=confirmLogout; window.lockNote=lockNote; window.unlockNote=unlockNote; window.toggleNoteLock=toggleNoteLock; window.toggleVaultLock=toggleVaultLock; window.refreshAllVaultIcons=refreshAllVaultIcons; window.isVaultUnlocked=isVaultUnlocked; window.submitVaultModal=submitVaultModal; window.closeVaultModal=closeVaultModal; window.openNewFolderModal=openNewFolderModal; window.closeNewFolderModal=closeNewFolderModal; window.toggleNewFolderVault=toggleNewFolderVault; window.submitNewFolderModal=submitNewFolderModal; window.isEncryptedBody=isEncryptedBody; window.mobileSyncTitle=mobileSyncTitle; window.mobileSyncTitleAndSave=mobileSyncTitleAndSave; window.mobileTitleInput=function(){_titleManual=true}; // called oninput on #mobile-editor-title })(); // end main IIFE