11import React , { useState , useRef , useEffect , useContext , useCallback , useMemo } from 'react' ;
22import ReactJson from 'react-json-view' ;
33import Parse from 'parse' ;
4+ import { useBeforeUnload } from 'react-router-dom' ;
45
56import CodeEditor from 'components/CodeEditor/CodeEditor.react' ;
67import Toolbar from 'components/Toolbar/Toolbar.react' ;
@@ -176,11 +177,11 @@ export default function Playground() {
176177 const containerRef = useRef ( null ) ;
177178
178179 // Tab management state
180+ const initialTabId = useMemo ( ( ) => crypto . randomUUID ( ) , [ ] ) ;
179181 const [ tabs , setTabs ] = useState ( [
180- { id : 1 , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE }
182+ { id : initialTabId , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE }
181183 ] ) ;
182- const [ activeTabId , setActiveTabId ] = useState ( 1 ) ;
183- const [ nextTabId , setNextTabId ] = useState ( 2 ) ;
184+ const [ activeTabId , setActiveTabId ] = useState ( initialTabId ) ;
184185 const [ renamingTabId , setRenamingTabId ] = useState ( null ) ;
185186 const [ renamingValue , setRenamingValue ] = useState ( '' ) ;
186187 const [ savedTabs , setSavedTabs ] = useState ( [ ] ) ; // All saved tabs including closed ones
@@ -235,8 +236,6 @@ export default function Playground() {
235236
236237 if ( tabsToOpen . length > 0 ) {
237238 setTabs ( tabsToOpen ) ;
238- const maxId = Math . max ( ...allScripts . map ( tab => tab . id ) ) ;
239- setNextTabId ( maxId + 1 ) ;
240239
241240 // Set active tab to the first one
242241 setActiveTabId ( tabsToOpen [ 0 ] . id ) ;
@@ -249,26 +248,24 @@ export default function Playground() {
249248 const firstScript = { ...allScripts [ 0 ] , order : 0 } ;
250249 setTabs ( [ firstScript ] ) ;
251250 setActiveTabId ( firstScript . id ) ;
252- const maxId = Math . max ( ...allScripts . map ( tab => tab . id ) ) ;
253- setNextTabId ( maxId + 1 ) ;
254251
255252 // Save it as open
256253 await scriptManagerRef . current . openScript ( context . applicationId , firstScript . id , 0 ) ;
257254
258255 setSavedTabs ( allScripts . filter ( script => script . saved !== false ) ) ;
259256 } else {
260257 // Fallback to default tab if no scripts exist
261- setTabs ( [ { id : 1 , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE , order : 0 } ] ) ;
262- setActiveTabId ( 1 ) ;
263- setNextTabId ( 2 ) ;
258+ const defaultTabId = crypto . randomUUID ( ) ;
259+ setTabs ( [ { id : defaultTabId , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE , order : 0 } ] ) ;
260+ setActiveTabId ( defaultTabId ) ;
264261 }
265262 }
266263 } catch ( error ) {
267264 console . warn ( 'Failed to load scripts via ScriptManager:' , error ) ;
268265 // Fallback to default tab if loading fails
269- setTabs ( [ { id : 1 , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE , order : 0 } ] ) ;
270- setActiveTabId ( 1 ) ;
271- setNextTabId ( 2 ) ;
266+ const defaultTabId = crypto . randomUUID ( ) ;
267+ setTabs ( [ { id : defaultTabId , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE , order : 0 } ] ) ;
268+ setActiveTabId ( defaultTabId ) ;
272269 }
273270
274271 // Load other data from localStorage
@@ -317,18 +314,19 @@ export default function Playground() {
317314
318315 // Tab management functions
319316 const createNewTab = useCallback ( ( ) => {
317+ const newTabId = crypto . randomUUID ( ) ;
318+ const tabCount = tabs . length + 1 ;
320319 const newTab = {
321- id : nextTabId ,
322- name : `Tab ${ nextTabId } ` ,
320+ id : newTabId ,
321+ name : `Tab ${ tabCount } ` ,
323322 code : '' , // Start with empty code instead of default value
324323 saved : false , // Mark as unsaved initially
325324 order : tabs . length // Assign order as the last position
326325 } ;
327326 const updatedTabs = [ ...tabs , newTab ] ;
328327 setTabs ( updatedTabs ) ;
329- setActiveTabId ( nextTabId ) ;
330- setNextTabId ( nextTabId + 1 ) ;
331- } , [ tabs , nextTabId ] ) ;
328+ setActiveTabId ( newTabId ) ;
329+ } , [ tabs ] ) ;
332330
333331 const closeTab = useCallback ( async ( tabId ) => {
334332 if ( tabs . length <= 1 ) {
@@ -591,11 +589,6 @@ export default function Playground() {
591589 setTabs ( updatedTabs ) ;
592590 setActiveTabId ( savedTab . id ) ;
593591
594- // Update nextTabId if necessary
595- if ( savedTab . id >= nextTabId ) {
596- setNextTabId ( savedTab . id + 1 ) ;
597- }
598-
599592 // Save the open state through ScriptManager
600593 if ( scriptManagerRef . current && context ?. applicationId ) {
601594 try {
@@ -604,7 +597,151 @@ export default function Playground() {
604597 console . error ( 'Failed to open script:' , error ) ;
605598 }
606599 }
607- } , [ tabs , nextTabId , switchTab , context ?. applicationId ] ) ;
600+ } , [ tabs , switchTab , context ?. applicationId ] ) ;
601+
602+ // Navigation confirmation for unsaved changes
603+ useBeforeUnload (
604+ useCallback (
605+ ( event ) => {
606+ // Check for unsaved changes across all tabs
607+ let hasChanges = false ;
608+
609+ for ( const tab of tabs ) {
610+ // Check if tab is marked as unsaved (like legacy scripts)
611+ if ( tab . saved === false ) {
612+ hasChanges = true ;
613+ break ;
614+ }
615+
616+ // Get current content for the tab
617+ let currentContent = '' ;
618+ if ( tab . id === activeTabId && editorRef . current ) {
619+ // For active tab, get content from editor
620+ currentContent = editorRef . current . value ;
621+ } else {
622+ // For inactive tabs, use stored code
623+ currentContent = tab . code ;
624+ }
625+
626+ // Find the saved version of this tab
627+ const savedTab = savedTabs . find ( saved => saved . id === tab . id ) ;
628+
629+ if ( ! savedTab ) {
630+ // If tab was never saved, it has unsaved changes if it has any content
631+ if ( currentContent . trim ( ) !== '' ) {
632+ hasChanges = true ;
633+ break ;
634+ }
635+ } else {
636+ // Compare current content with saved content
637+ if ( currentContent !== savedTab . code ) {
638+ hasChanges = true ;
639+ break ;
640+ }
641+ }
642+ }
643+
644+ if ( hasChanges ) {
645+ const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?' ;
646+ event . preventDefault ( ) ;
647+ event . returnValue = message ;
648+ return message ;
649+ }
650+ } ,
651+ [ tabs , activeTabId , savedTabs ]
652+ )
653+ ) ;
654+
655+ // Handle navigation confirmation for internal route changes
656+ useEffect ( ( ) => {
657+ const checkForUnsavedChanges = ( ) => {
658+ // Check for unsaved changes across all tabs
659+ for ( const tab of tabs ) {
660+ // Check if tab is marked as unsaved (like legacy scripts)
661+ if ( tab . saved === false ) {
662+ return true ;
663+ }
664+
665+ // Get current content for the tab
666+ let currentContent = '' ;
667+ if ( tab . id === activeTabId && editorRef . current ) {
668+ // For active tab, get content from editor
669+ currentContent = editorRef . current . value ;
670+ } else {
671+ // For inactive tabs, use stored code
672+ currentContent = tab . code ;
673+ }
674+
675+ // Find the saved version of this tab
676+ const savedTab = savedTabs . find ( saved => saved . id === tab . id ) ;
677+
678+ if ( ! savedTab ) {
679+ // If tab was never saved, it has unsaved changes if it has any content
680+ if ( currentContent . trim ( ) !== '' ) {
681+ return true ;
682+ }
683+ } else {
684+ // Compare current content with saved content
685+ if ( currentContent !== savedTab . code ) {
686+ return true ;
687+ }
688+ }
689+ }
690+ return false ;
691+ } ;
692+
693+ const handleLinkClick = ( event ) => {
694+ if ( event . defaultPrevented ) {
695+ return ;
696+ }
697+ if ( event . button !== 0 ) {
698+ return ;
699+ }
700+ if ( event . metaKey || event . altKey || event . ctrlKey || event . shiftKey ) {
701+ return ;
702+ }
703+
704+ const anchor = event . target . closest ( 'a[href]' ) ;
705+ if ( ! anchor || anchor . target === '_blank' ) {
706+ return ;
707+ }
708+
709+ const href = anchor . getAttribute ( 'href' ) ;
710+ if ( ! href || href === '#' ) {
711+ return ;
712+ }
713+
714+ // Check if it's an internal navigation (starts with / or #)
715+ if ( href . startsWith ( '/' ) || href . startsWith ( '#' ) ) {
716+ if ( checkForUnsavedChanges ( ) ) {
717+ const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?' ;
718+ if ( ! window . confirm ( message ) ) {
719+ event . preventDefault ( ) ;
720+ event . stopPropagation ( ) ;
721+ }
722+ }
723+ }
724+ } ;
725+
726+ const handlePopState = ( ) => {
727+ if ( checkForUnsavedChanges ( ) ) {
728+ const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?' ;
729+ if ( ! window . confirm ( message ) ) {
730+ window . history . go ( 1 ) ;
731+ }
732+ }
733+ } ;
734+
735+ // Add event listeners
736+ document . addEventListener ( 'click' , handleLinkClick , true ) ;
737+ window . addEventListener ( 'popstate' , handlePopState ) ;
738+
739+ // Cleanup event listeners
740+ return ( ) => {
741+ document . removeEventListener ( 'click' , handleLinkClick , true ) ;
742+ window . removeEventListener ( 'popstate' , handlePopState ) ;
743+ } ;
744+ } , [ tabs , activeTabId , savedTabs ] ) ;
608745
609746 // Focus input when starting to rename
610747 useEffect ( ( ) => {
0 commit comments