import { ref , computed , watch , Ref , onMounted } from '@vue/composition-api' ; import { ComponentTreeNode , EditStatePayload , InspectedComponentData } from '@vue/devtools-api' ; import Vue from 'vue' ; import groupBy from 'lodash/groupBy' ; import { BridgeEvents , sortByKey , searchDeepInObject , BridgeSubscriptions , isChrome , openInEditor , setStorage , } from '@vue-devtools/shared-utils' ; import { getBridge , useBridge } from '@front/features/bridge' ; import { AppRecord , waitForAppSelect , useCurrentApp } from '@front/features/apps' ; import { useRoute , useRouter } from '@front/util/router' ; export const rootInstances = ref < ComponentTreeNode [ ] > ( [ ] ) ; export const componentsMap = ref < Record < ComponentTreeNode [ 'id' ] , ComponentTreeNode > > ( { } ) ; let componentsParent = { } ; const treeFilter = ref ( '' ) ; export const selectedComponentId = ref < ComponentTreeNode [ 'id' ] > ( null ) ; export const selectedComponentData = ref < InspectedComponentData > ( null ) ; const selectedComponentStateFilter = ref ( '' ) ; export const selectedComponentPendingId = ref < ComponentTreeNode [ 'id' ] > ( null ) ; let lastSelectedApp = null ; export const lastSelectedComponentId = { } ; export const expandedMap = ref < Record < ComponentTreeNode [ 'id' ] , boolean > > ( { } ) ; export function useComponentRequests ( ) { const router = useRouter ( ) ; function selectComponent ( id , replace = false ) { if ( selectedComponentId . value !== id ) { router [ replace ? 'replace' : 'push' ] ( { params : { appId : getAppIdFromComponentId ( id ) , componentId : id , } , } ) ; } else { loadComponent ( id ) ; } } return { requestComponentTree , selectComponent , } ; } export function useComponents ( ) { const { onBridge , subscribe } = useBridge ( ) ; const route = useRoute ( ) ; const { requestComponentTree , selectComponent , } = useComponentRequests ( ) ; const { currentAppId } = useCurrentApp ( ) ; watch ( treeFilter , ( ) => { requestComponentTree ( ) ; } ) ; watch ( ( ) => route . value . params . componentId , ( ) => { const value = route . value . params . componentId ; if ( value && getAppIdFromComponentId ( value ) === currentAppId . value ) { selectedComponentId . value = value ; loadComponent ( value ) ; } } , { immediate : true , } ) ; function subscribeToSelectedData ( ) { let unsub ; watch ( selectedComponentId , value => { if ( unsub ) { unsub ( ) ; unsub = null ; } if ( value != null ) { unsub = subscribe ( BridgeSubscriptions . SELECTED_COMPONENT_DATA , { instanceId : value , } ) ; } } , { immediate : true , } ) ; } watch ( componentsMap , ( ) => { if ( selectedComponentId . value && selectedComponentPendingId . value !== selectedComponentId . value && ! selectedComponentData . value ) { selectComponent ( selectedComponentId . value ) ; } } , { immediate : true , deep : true , } ) ; onBridge ( BridgeEvents . TO_FRONT_APP_SELECTED , async ( { id } ) => { await waitForAppSelect ( ) ; requestComponentTree ( ) ; selectedComponentData . value = null ; if ( lastSelectedApp !== null ) { selectLastComponent ( ) ; } lastSelectedApp = id ; } ) ; function selectLastComponent ( ) { const id = lastSelectedComponentId [ currentAppId . value ] ; if ( id ) { selectComponent ( id , true ) ; } } return { rootInstances : computed ( ( ) => rootInstances . value ) , treeFilter , selectedComponentId : computed ( ( ) => selectedComponentId . value ) , requestComponentTree , selectComponent , selectLastComponent , subscribeToSelectedData , } ; } export function useComponent ( instance ) { const { selectComponent , requestComponentTree } = useComponentRequests ( ) ; const { subscribe } = useBridge ( ) ; const isExpanded = computed ( ( ) => isComponentOpen ( instance . value . id ) ) ; const isExpandedUndefined = computed ( ( ) => expandedMap . value [ instance . value . id ] == null ) ; function toggleExpand ( ) { if ( ! instance . value . hasChildren ) return ; setComponentOpen ( instance . value . id , ! isExpanded . value ) ; if ( isComponentOpen ( instance . value . id ) ) { requestComponentTree ( instance . value . id ) ; } } const isSelected = computed ( ( ) => selectedComponentId . value === instance . value . id ) ; function select ( id = instance . value . id ) { selectComponent ( id ) ; } function subscribeToComponentTree ( ) { let unsub ; watch ( ( ) => instance . value . id , value => { if ( unsub ) { unsub ( ) ; unsub = null ; } if ( value != null ) { unsub = subscribe ( BridgeSubscriptions . COMPONENT_TREE , { instanceId : value , } ) ; } } , { immediate : true , } ) ; } onMounted ( ( ) => { if ( isExpanded . value ) { requestComponentTree ( instance . value . id ) ; } } ) ; return { isExpanded , isExpandedUndefined , isComponentOpen , toggleExpand , isSelected , select , subscribeToComponentTree , } ; } export function setComponentOpen ( id , isOpen ) { Vue . set ( expandedMap . value , id , isOpen ) ; } export function isComponentOpen ( id ) { return ! ! expandedMap . value [ id ] ; } export function useSelectedComponent ( ) { const data = computed ( ( ) => selectedComponentData . value ) ; const state = computed ( ( ) => selectedComponentData . value ? groupBy ( sortByKey ( selectedComponentData . value . state . filter ( el => { try { return searchDeepInObject ( { [ el . key ] : el . value , } , selectedComponentStateFilter . value ) ; } catch ( e ) { return { [ el . key ] : e , } ; } } ) ) , 'type' ) : ( { } ) ) ; const fileIsPath = computed ( ( ) => data . value ?. file && /[/\\]/ . test ( data . value . file ) ) ; function inspectDOM ( ) { if ( ! data . value ) return ; if ( isChrome ) { getBridge ( ) . send ( BridgeEvents . TO_BACK_COMPONENT_INSPECT_DOM , { instanceId : data . value . id } ) ; } else { window . alert ( 'DOM inspection is not supported in this shell.' ) ; } } function openFile ( ) { if ( ! data . value ) return ; openInEditor ( data . value . file ) ; } const { bridge } = useBridge ( ) ; function editState ( dotPath , payload , type ? ) { bridge . send ( BridgeEvents . TO_BACK_COMPONENT_EDIT_STATE , { instanceId : data . value . id , dotPath , type , ... payload , } ) ; } function scrollToComponent ( ) { bridge . send ( BridgeEvents . TO_BACK_COMPONENT_SCROLL_TO , { instanceId : data . value . id , } ) ; } return { data , state , stateFilter : selectedComponentStateFilter , inspectDOM , fileIsPath , openFile , editState , scrollToComponent , selectedComponentId , } ; } export function resetComponents ( ) { rootInstances . value = [ ] ; componentsMap . value = { } ; componentsParent = { } ; } export const requestedComponentTree = new Set ( ) ; export async function requestComponentTree ( instanceId = null ) { if ( ! instanceId ) { instanceId = '_root' ; } if ( requestedComponentTree . has ( instanceId ) ) { return ; } requestedComponentTree . add ( instanceId ) ; await waitForAppSelect ( ) ; getBridge ( ) . send ( BridgeEvents . TO_BACK_COMPONENT_TREE , { instanceId , filter : treeFilter . value , } ) ; } export function ensureComponentsMapData ( data ) { let component = componentsMap . value [ data . id ] ; if ( ! component ) { component = addToComponentsMap ( data ) ; } else { component = updateComponentsMapData ( data ) ; } return component ; } function ensureComponentsMapChildren ( id , children ) { const result = children . map ( child => ensureComponentsMapData ( child ) ) ; for ( const child of children ) { componentsParent [ child . id ] = id ; } return result ; } function updateComponentsMapData ( data ) { const component = componentsMap . value [ data . id ] ; for ( const key in data ) { if ( key === 'children' ) { if ( ! data . hasChildren || data . children . length ) { const children = ensureComponentsMapChildren ( component . id , data . children ) ; Vue . set ( component , key , children ) ; } } else { Vue . set ( component , key , data [ key ] ) ; } } return component ; } function addToComponentsMap ( data ) { if ( ! data . hasChildren || data . children . length ) { data . children = ensureComponentsMapChildren ( data . id , data . children ) ; } componentsMap . value [ data . id ] = data ; return data ; } export async function loadComponent ( id ) { if ( ! id || selectedComponentPendingId . value === id ) return ; lastSelectedComponentId [ getAppIdFromComponentId ( id ) ] = id ; setStorage ( 'lastSelectedComponentId' , lastSelectedComponentId ) ; selectedComponentPendingId . value = id ; await waitForAppSelect ( ) ; getBridge ( ) . send ( BridgeEvents . TO_BACK_COMPONENT_SELECTED_DATA , id ) ; } export function sortChildren ( children ) { return children . slice ( ) . sort ( ( a , b ) => { if ( a . inactive && ! b . inactive ) { return 1 ; } else if ( ! a . inactive && b . inactive ) { return - 1 ; } const order = compareIndexLists ( a . domOrder ?? [ ] , b . domOrder ?? [ ] ) ; if ( order === 0 ) { return a . id . localeCompare ( b . id ) ; } else { return order ; } } ) ; } function compareIndexLists ( a , b ) { if ( ! a . length || ! b . length ) { return 0 ; } else if ( a [ 0 ] === b [ 0 ] ) { return compareIndexLists ( a . slice ( 1 ) , b . slice ( 1 ) ) ; } else { return a [ 0 ] - b [ 0 ] ; } } export function getAppIdFromComponentId ( id ) { const index = id . indexOf ( ':' ) ; const appId = id . substring ( 0 , index ) ; return appId ; }