require('./editPage.less'); const React = require('react'); const _ = require('lodash'); const Nav = require('naturalcrit/nav/nav.jsx'); const Navbar = require('../../../navbar/navbar.jsx'); const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx'); const HelpNavItem = require('../../../navbar/help.navitem.jsx'); const PrintNavItem = require('../../../navbar/print.navitem.jsx'); const ErrorNavItem = require('../../../navbar/error-navitem.jsx'); const AccountNavItem = require('../../../navbar/account.navitem.jsx'); const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both; const VaultNavItem = require('../../../navbar/vault.navitem.jsx'); const SplitPane = require('client/components/splitPane/splitPane.jsx'); const Editor = require('../../../editor/editor.jsx'); const BrewRenderer = require('../../../brewRenderer/brewRenderer.jsx'); const { fetchThemeBundle } = require('../../../../../shared/helpers.js'); import { useEffect, useState, useRef } from 'react'; import Markdown from 'naturalcrit/markdown.js'; const BREWKEY = 'homebrewery-new'; const STYLEKEY = 'homebrewery-new-style'; const METAKEY = 'homebrewery-new-meta'; const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`; const SAVE_TIMEOUT = 10000; const BaseEditPage = (props)=>{ const [brew, setBrew] = useState(() => props.brew); const [savedBrew, setSavedBrew] = useState(brew); const [isSaving, setIsSaving] = useState(false); const [lastSavedTime, setLastSavedTime] = useState(new Date()); const [saveGoogle, setSaveGoogle] = useState(() => (global.account?.googleId ? true : false)); const [welcomeText, setWelcomeText] = useState(() => props.brew?.text ?? ''); const [error, setError] = useState(undefined); const [htmlErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text)); const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle, setThemeBundle] = useState({}); const [unsavedChanges, setUnsavedChanges] = useState(false); const [autoSaveEnabled, setAutoSaveEnabled] = useState(true); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(false); const editorRef = useRef(null); let lastSavedBrew = useRef(JSON.parse(JSON.stringify(this.propcopys.brew))); //Deep copy const saveTimeout = useRef(null); const unsavedChangesTimer = useRef(null); const handleSplitMove = ()=>{ editorRef.current.update(); }; const handleEditorViewPageChange = (pageNumber)=>{ setCurrentEditorViewPageNum(pageNumber); }; const handleEditorCursorPageChange = (pageNumber)=>{ setCurrentEditorCursorPageNum(pageNumber); }; const handleBrewRendererPageChange = (pageNumber)=>{ setCurrentBrewRendererPageNum(pageNumber); }; const handleTextChange = (text)=>{ //If there are HTML errors, run the validator on every change to give quick feedback if(htmlErrors.length) htmlErrors = Markdown.validate(text); setHTMLErrors(htmlErrors); setBrew((prevBrew) => ({ ...prevBrew, text })); // TODO: ONLY ON /NEW PAGE localStorage.setItem(BREWKEY, text); }; const handleStyleChange = (style)=>{ setBrew((prevBrew) => ({ ...prevBrew, style })); if(props.useLocalStorage) localStorage.setItem(STYLEKEY, style); }; const handleSnipChange = (snippet)=>{ //If there are HTML errors, run the validator on every change to give quick feedback if(htmlErrors.length) htmlErrors = Markdown.validate(text); setHTMLErrors(htmlErrors); setBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); }; const handleMetaChange = (metadata, field=undefined)=>{ if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); setBrew((prevBrew) => ({ ...prevBrew, ...metadata })); if(props.useLocalStorage) localStorage.setItem(METAKEY, JSON.stringify({ 'renderer' : metadata.renderer, 'theme' : metadata.theme, 'lang' : metadata.lang })); }; const updateBrew = (newData) => { setBrew(prevBrew => ({ //TODO: May be able to just directly use setBrew instead of a wrapper, if its safe to assume we want all the data from newData ...prevBrew, //OR: Somehow combine handleTextChange, handleStyleChange, handleMetaChange, and handleSnipChange into one function that calls this style: newData.style, text: newData.text, snippets: newData.snippets })); }; const clearError = ()=>{ setError(null); setIsSaving(false); }; const save = async (immediate=false)=>{ if(isSaving) return; if(!unsavedChanges && !immediate) return; clearTimeout(saveTimeout.current); const timeout = immediate ? 0 : 10000; saveTimeout.current = setTimeout(async () => { setIsSaving(true); await props.performSave(brew, saveGoogle) .catch((err)=>{ setError(err); }); setIsSaving(false); setLastSavedTime(new Date()); setTimeout(setWarnUnsavedChanges(true), 900000); // 15 minutes between unsaved work warnings }, timeout); }; const handleControlKeys = (e)=>{ if(!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; const P_KEY = 80; if(e.keyCode == S_KEY) save(); if(e.keyCode == P_KEY) BrewRenderer.printCurrentBrew(); if(e.keyCode == P_KEY || e.keyCode == S_KEY){ e.stopPropagation(); e.preventDefault(); } }; const hasChanges =()=>{ return !_.isEqual(brew, savedBrew); }; useEffect(() => { props.loadBrew?.(brew, setBrew, setSaveGoogle); //Initial load from localStorage/etc. //Load settings setAutoSaveEnabled(JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true); setHTMLErrors(Markdown.validate(brew.text)); document.addEventListener('keydown', handleControlKeys); window.onbeforeunload = ()=>{ if(isSaving || unsavedChanges) return 'You have unsaved changes!'; }; return () => { document.removeEventListener('keydown', handleControlKeys); window.onbeforeunload = null; } }, []); useEffect(() => { fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme); }, [brew.renderer, brew.theme]); useEffect(() => { const hasChange = hasChanges(); if(unsavedChanges !== hasChange) setUnsavedChanges(hasChange); if(autoSaveEnabled) save(); }, [brew]); const resetUnsavedChangesWarning = ()=>{ setTimeout(setWarnUnsavedChanges(false), 4000); // Display warning for 4 seconds setTimeout(setWarnUnsavedChanges(true) , 90000); // 15 minutes between warnings }; const toggleAutoSave = ()=>{ if(unsavedChangesTimer.current) clearTimeout(unsavedChangesTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); setAutoSaveEnabled(!autoSaveEnabled); setWarnUnsavedChanges(false); }; const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING if(isSaving) return saving...; // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING if(unsavedChanges && warnUnsavedChanges){ resetUnsavedChangesWarning(); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; return Reminder...
{text}
; } // #3 - Unsaved changes exist, click to save, show SAVE NOW if(unsavedChanges) return save(true)} color='blue' icon='fas fa-save'>Save Now; // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED if(autoSaveEnabled) return auto-saved.; // DEFAULT - No unsaved changes, show SAVED return saved.; }; const renderAutoSaveButton = ()=>{ return Autosave ; }; return (
{props.brew.title} {error ? : {renderSaveButton()} {renderAutoSaveButton()} } {props.renderUniqueNav?.()}
{props.children?.(welcomeText, brew, save)}
); }; module.exports = BaseEditPage;