lodash-es#debounce TypeScript Examples

The following examples show how to use lodash-es#debounce. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: asset.ts    From atlas with GNU General Public License v3.0 6 votes vote down vote up
private sendEvents = debounce(async () => {
    if (!this.pendingEvents.length) return
    if (!this.logUrl) return

    ConsoleLogger.debug(`Sending ${this.pendingEvents.length} asset events`)

    const payload = {
      events: this.pendingEvents,
    }
    this.pendingEvents = []

    try {
      await axios.post(this.logUrl, payload)
    } catch (e) {
      SentryLogger.error('Failed to send asset events', 'AssetLogger', e, { request: { url: this.logUrl } })
    }
  }, 2000)
Example #2
Source File: History.ts    From LogicFlow with Apache License 2.0 6 votes vote down vote up
watch(model) {
    this.stopWatch && this.stopWatch();

    // 把当前watch的model转换一下数据存起来,无需清空redos。
    this.undos.push(model.modelToGraphData());

    this.stopWatch = deepObserve(model, debounce(() => {
      // 数据变更后,把最新的当前model数据存起来,并清空redos。
      // 因为这个回调函数的触发,一般是用户交互而引起的,所以按正常逻辑需要清空redos。
      const data = model.modelToHistoryData();
      if (data) {
        this.add(data);
      }
    }, this.waitTime));
  }
Example #3
Source File: mapIconsCache.ts    From NewWorldMinimap with MIT License 5 votes vote down vote up
private debouncedInitializeIconCache = debounce(this.initializeIconCache, 250);
Example #4
Source File: Kplayer.ts    From agefans-enhance with MIT License 5 votes vote down vote up
hideControlsDebounced = debounce(() => {
    const dom = document.querySelector('.plyr')
    if (!this.isHoverControls) dom?.classList.add('plyr--hide-controls')
  }, 1000)
Example #5
Source File: Kplayer.ts    From agefans-enhance with MIT License 5 votes vote down vote up
hideCursorDebounced = debounce(() => {
    const dom = document.querySelector('.plyr')
    dom?.classList.add('plyr--hide-cursor')
  }, 1000)
Example #6
Source File: useSearchResults.ts    From atlas with GNU General Public License v3.0 5 votes vote down vote up
useSearchResults = ({ searchQuery, limit = 50, videoWhereInput }: SearchResultData) => {
  const [text, setText] = useState(searchQuery)
  const [typing, setTyping] = useState(false)
  const debouncedQuery = useRef(
    debounce((query: string) => {
      setText(query)
      setTyping(false)
    }, 500)
  )

  useEffect(() => {
    if (searchQuery.length) {
      setTyping(true)
      debouncedQuery.current(searchQuery)
    }
  }, [searchQuery])

  const { data, loading, error } = useSearch(
    {
      text,
      limit,
      whereVideo: {
        media: {
          isAccepted_eq: true,
        },
        thumbnailPhoto: {
          isAccepted_eq: true,
        },
        isPublic_eq: true,
        isCensored_eq: false,
        ...videoWhereInput,
      },
    },
    {
      skip: !searchQuery,
      onError: (error) => SentryLogger.error('Failed to fetch search results', 'SearchResults', error),
    }
  )

  const getChannelsAndVideos = (loading: boolean, data: SearchQuery['search'] | undefined) => {
    if (loading || !data) {
      return { channels: [], videos: [] }
    }
    const results = data
    const videos = results.flatMap((result) => (result.item.__typename === 'Video' ? [result.item] : []))
    const channels = results.flatMap((result) => (result.item.__typename === 'Channel' ? [result.item] : []))
    return { channels, videos }
  }

  const { channels, videos } = useMemo(() => getChannelsAndVideos(loading, data), [loading, data])

  return {
    channels,
    videos,
    error,
    loading: loading || typing,
  }
}
Example #7
Source File: VideoForm.hooks.ts    From atlas with GNU General Public License v3.0 5 votes vote down vote up
useVideoFormDraft = (
  watch: UseFormWatch<VideoWorkspaceVideoFormFields>,
  dirtyFields: FieldNamesMarkedBoolean<VideoWorkspaceVideoFormFields>
) => {
  const { activeChannelId } = useAuthorizedUser()
  const { editedVideoInfo, setEditedVideo } = useVideoWorkspace()
  const { updateDraft, addDraft } = useDraftStore((state) => state.actions)

  // we pass the functions explicitly so the debounced function doesn't need to change when those functions change
  const debouncedDraftSave = useRef(
    debounce(
      (
        channelId: string,
        tab: VideoWorkspace,
        data: VideoWorkspaceVideoFormFields,
        addDraftFn: typeof addDraft,
        updateDraftFn: typeof updateDraft,
        updateSelectedTabFn: typeof setEditedVideo
      ) => {
        const draftData: RawDraft = {
          ...data,
          channelId: activeChannelId,
          type: 'video',
          publishedBeforeJoystream: isDateValid(data.publishedBeforeJoystream)
            ? formatISO(data.publishedBeforeJoystream as Date)
            : null,
        }
        if (tab.isNew) {
          addDraftFn(draftData, tab.id)
          updateSelectedTabFn({ ...tab, isNew: false })
        } else {
          updateDraftFn(tab.id, draftData)
        }
      },
      700
    )
  )

  // save draft on form fields update
  useEffect(() => {
    if (!editedVideoInfo?.isDraft) {
      return
    }

    const subscription = watch((data) => {
      if (!Object.keys(dirtyFields).length) {
        return
      }

      debouncedDraftSave.current(activeChannelId, editedVideoInfo, data, addDraft, updateDraft, setEditedVideo)
    })
    return () => {
      subscription.unsubscribe()
    }
  }, [addDraft, dirtyFields, editedVideoInfo, updateDraft, setEditedVideo, watch, activeChannelId])

  const flushDraftSave = useCallback(() => {
    debouncedDraftSave.current.flush()
  }, [])

  return { flushDraftSave }
}
Example #8
Source File: TimeSelect.tsx    From UUI with MIT License 4 votes vote down vote up
TimeSelect = UUIFunctionComponent({
  name: 'TimeSelect',
  nodes: {
    Root: 'div',
    SelectZone: 'div',
    Separator: 'div',
    OptionList: 'div',
    Option: 'div',
  },
  propTypes: TimeSelectPropTypes,
}, (props: TimeSelectFeatureProps, { nodes, NodeDataProps, ref }) => {
  const {
    Root, SelectZone, Separator,
    OptionList, Option,
  } = nodes

  const allOptions = useMemo(() => {
    return {
      hours: range(0, 24),
      minutes: range(0, 60),
      seconds: range(0, 60),
    }
  }, [])

  const activeOptionValue = {
    hours: props.value.getHours(),
    minutes: props.value.getMinutes(),
    seconds: props.value.getSeconds(),
  }

  const [disableHandleScroll, setDisableHandleScroll] = useState(false)
  const hourListRef = useRef<HTMLDivElement | null>(null)
  const minuteListRef = useRef<HTMLDivElement | null>(null)
  const secondListRef = useRef<HTMLDivElement | null>(null)

  const getItemHeight = useCallback((target: HTMLElement) => {
    const styles = window.getComputedStyle(target)
    const optionHeightPx = styles.getPropertyValue('--option-height')
    return Number(optionHeightPx.replace('px', ''))
  }, [])
  const scrollToValue = useCallback((value: Date, animate?: boolean) => {
    setDisableHandleScroll(true)
    const targetScrollTo = (ref: React.MutableRefObject<HTMLDivElement | null>, value: number, animate?: boolean) => {
      const target = ref.current as HTMLElement
      const itemHeight = getItemHeight(target)
      target.scrollTo({ top: value * itemHeight, behavior: animate ? "smooth" : "auto" })
    }
    targetScrollTo(hourListRef, value.getHours(), animate)
    targetScrollTo(minuteListRef, value.getMinutes(), animate)
    targetScrollTo(secondListRef, value.getSeconds(), animate)
    setTimeout(() => {
      setDisableHandleScroll(false)
    }, 500)
  }, [getItemHeight])

  useImperativeHandle(ref, () => {
    return {
      scrollToValue: scrollToValue,
    }
  })

  const scrollTo = useCallback((target: HTMLElement, top: number) => {
    target.scrollTo({ top, behavior: "smooth" })
  }, [])

  const debouncedScrollOnChange = useRef({
    hours: debounce(scrollTo, 300),
    minutes: debounce(scrollTo, 300),
    seconds: debounce(scrollTo, 300),
  })

  const handleScroll = useCallback((type: TimeSelectType) => {
    if (disableHandleScroll) return;
    const options = allOptions[type]
    return (event: React.UIEvent<HTMLDivElement, UIEvent>) => {
      const target = event.target as HTMLElement
      const itemHeight = getItemHeight(target)
      const scrollTop = target.scrollTop
      const currentIndex = Math.round((scrollTop) / itemHeight)

      const newValue = options[currentIndex];
      props.onChange(set(props.value, { [type]: newValue }))
      debouncedScrollOnChange.current[type](target, currentIndex * itemHeight)
    }
  }, [allOptions, disableHandleScroll, getItemHeight, props])

  return (
    <Root>
      {TimeSelectTypeArray.map((type, index) => {
        return (
          <React.Fragment key={type}>
            {index !== 0 && (
              <Separator>:</Separator>
            )}
            <OptionList
              ref={[hourListRef, minuteListRef, secondListRef][index]}
              key={`option-list-${type}`}
              onScroll={handleScroll(type)}
            >
              {allOptions[type].map((option) => {
                const active = activeOptionValue[type] === option
                return (
                  <Option
                    {...NodeDataProps({
                      'active': active,
                    })}
                    key={`${type}-${option}`}
                    onClick={() => {
                      const newValue = set(props.value, { [type]: option })
                      props.onChange(newValue)
                      scrollToValue(newValue)
                    }}
                  >{padStart(String(option), 2, '0')}</Option>
                )
              })}
            </OptionList>
          </React.Fragment>
        )
      })}
      <SelectZone />
    </Root>
  )
})
Example #9
Source File: useInfiniteGrid.ts    From atlas with GNU General Public License v3.0 4 votes vote down vote up
useInfiniteGrid = <
  TRawData,
  TPaginatedData extends PaginatedData<unknown>,
  TArgs extends PaginatedDataArgs
>({
  query,
  dataAccessor,
  isReady,
  targetRowsCount,
  itemsPerRow,
  skipCount,
  onScrollToBottom,
  onError,
  queryVariables,
  onDemand,
  onDemandInfinite,
  activatedInfinteGrid,
}: UseInfiniteGridParams<TRawData, TPaginatedData, TArgs>): UseInfiniteGridReturn<TPaginatedData> => {
  const targetDisplayedItemsCount = targetRowsCount * itemsPerRow
  const targetLoadedItemsCount = targetDisplayedItemsCount + skipCount

  const queryVariablesRef = useRef(queryVariables)

  const {
    loading,
    data: rawData,
    error,
    fetchMore,
    refetch,
    networkStatus,
  } = useQuery<TRawData, TArgs>(query, {
    notifyOnNetworkStatusChange: true,
    skip: !isReady,
    variables: {
      ...queryVariables,
      first: targetDisplayedItemsCount + PREFETCHED_ITEMS_COUNT,
    },
    onError,
  })

  const data = dataAccessor(rawData)

  const loadedItemsCount = data?.edges.length ?? 0
  const allItemsLoaded = data ? !data.pageInfo.hasNextPage : false
  const endCursor = data?.pageInfo.endCursor

  // handle fetching more items
  useEffect(() => {
    if (loading || error || !isReady || !fetchMore || allItemsLoaded) {
      return
    }

    const missingItemsCount = targetLoadedItemsCount - loadedItemsCount

    if (missingItemsCount <= 0) {
      return
    }

    fetchMore({
      variables: { ...queryVariables, first: missingItemsCount + PREFETCHED_ITEMS_COUNT, after: endCursor },
    })
  }, [
    loading,
    error,
    fetchMore,
    allItemsLoaded,
    queryVariables,
    targetLoadedItemsCount,
    loadedItemsCount,
    endCursor,
    isReady,
  ])

  useEffect(() => {
    if (!isEqual(queryVariablesRef.current, queryVariables)) {
      queryVariablesRef.current = queryVariables
      refetch()
    }
  }, [queryVariables, refetch])

  // handle scroll to bottom
  useEffect(() => {
    if (onDemand || (onDemandInfinite && !activatedInfinteGrid)) {
      return
    }
    if (error) return

    const scrollHandler = debounce(() => {
      const scrolledToBottom =
        window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight
      if (onScrollToBottom && scrolledToBottom && isReady && !loading && !allItemsLoaded) {
        onScrollToBottom()
      }
    }, 100)

    window.addEventListener('scroll', scrollHandler)
    return () => window.removeEventListener('scroll', scrollHandler)
  }, [error, isReady, loading, allItemsLoaded, onScrollToBottom, onDemand, onDemandInfinite, activatedInfinteGrid])

  const edges = data?.edges

  const isRefetching = networkStatus === NetworkStatus.refetch

  const displayedEdges = edges?.slice(skipCount, targetLoadedItemsCount) ?? []
  const displayedItems = isRefetching ? [] : displayedEdges.map((edge) => edge.node)

  const displayedItemsCount = data
    ? Math.min(targetDisplayedItemsCount, data.totalCount - skipCount)
    : targetDisplayedItemsCount
  const placeholdersCount = isRefetching ? targetDisplayedItemsCount : displayedItemsCount - displayedItems.length

  return {
    displayedItems,
    placeholdersCount,
    allItemsLoaded,
    error,
    loading,
    totalCount: data?.totalCount || 0,
  }
}
Example #10
Source File: SearchBox.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
SearchBox: React.FC<SearchBoxProps> = React.memo(
  ({
    searchQuery,
    onSelectRecentSearch,
    className,
    selectedItem,
    onLastSelectedItem,
    onSelectItem,
    handleSetNumberOfItems,
    onMouseMove,
    hasFocus,
  }) => {
    const { channels, videos, loading } = useSearchResults({ searchQuery })
    const { recentSearches, deleteRecentSearch } = usePersonalDataStore((state) => ({
      recentSearches: state.recentSearches,
      deleteRecentSearch: state.actions.deleteRecentSearch,
    }))
    const containerRef = useRef<HTMLDivElement>(null)
    const topRef = useRef(0)
    const [visualViewportHeight, setVisualViewportHeight] = useState(window.visualViewport.height)

    // Calculate searchbox height whether keyboard is open or not
    useEffect(() => {
      const debouncedVisualViewportChange = debounce(() => {
        setVisualViewportHeight(window.visualViewport.height)
      }, 100)
      window.visualViewport.addEventListener('resize', debouncedVisualViewportChange)

      return () => {
        window.visualViewport.removeEventListener('resize', debouncedVisualViewportChange)
      }
    }, [])

    const scrollToSelectedItem = useCallback(
      (top: number, title?: string | null) => {
        const offsetTop = -250
        const offsetBottom = -50
        onSelectItem(title)
        if (!containerRef.current) {
          return
        }
        const { offsetHeight } = containerRef.current
        if (selectedItem === 0 || top < offsetHeight) {
          containerRef?.current?.scrollTo(0, 0)
        }
        if (top >= offsetHeight + (top < topRef.current ? offsetTop : offsetBottom)) {
          containerRef?.current?.scrollTo(0, top + (top < topRef.current ? offsetBottom : offsetTop))
        }
        topRef.current = top
      },
      [onSelectItem, selectedItem]
    )

    const placeholders = useMemo(() => {
      const min = 20
      const max = 80
      const placeholderItems = Array.from({ length: 6 }, () => ({ id: undefined }))
      return placeholderItems.map((_, idx) => {
        const generatedWidth = Math.floor(Math.random() * (max - min)) + min
        return (
          <PlaceholderWrapper key={`placeholder-${idx}`}>
            <SkeletonAvatar width="32px" height="32px" rounded />
            <SkeletonLoader width={`${generatedWidth}%`} height="16px" />
          </PlaceholderWrapper>
        )
      })
    }, [])

    const handleRecentSearchDelete = (title: string) => {
      deleteRecentSearch(title)
    }

    const filteredRecentSearches = searchQuery.length
      ? recentSearches
          .filter((item) =>
            new RegExp(`${searchQuery.replace(SPECIAL_CHARACTERS, '\\$&').replace(/\s+/g, '|')}`, 'i').test(
              item.title || ''
            )
          )
          .slice(0, 3)
      : recentSearches
    const slicedVideos = videos.slice(0, 3)
    const slicedChannels = channels.slice(0, 3)

    // Pass number off all results
    useEffect(() => {
      handleSetNumberOfItems(filteredRecentSearches.length + slicedVideos.length + slicedChannels.length)
    }, [handleSetNumberOfItems, filteredRecentSearches.length, slicedVideos.length, slicedChannels.length])

    // Fire when user select last result
    useEffect(() => {
      if (selectedItem === filteredRecentSearches.length + slicedVideos.length + slicedChannels.length) {
        onLastSelectedItem()
      }
    }, [
      recentSearches.length,
      slicedVideos.length,
      slicedChannels.length,
      onLastSelectedItem,
      selectedItem,
      filteredRecentSearches.length,
    ])

    return (
      <Container
        isVisible={!!filteredRecentSearches.length || !!slicedVideos.length || !!slicedChannels.length || loading}
        className={className}
        ref={containerRef}
        onMouseMove={onMouseMove}
        hasQuery={searchQuery}
        visualViewportHeight={visualViewportHeight}
        hasFocus={hasFocus}
        data-scroll-lock-scrollable
      >
        {!!filteredRecentSearches.length && (
          <Section>
            <Caption secondary variant="t100">
              Recent searches
            </Caption>
            {filteredRecentSearches.map((recentSearch, idx) => (
              <RecentSearchItem
                key={`RecentSearchItem-${recentSearch.title}`}
                onDelete={() => handleRecentSearchDelete(recentSearch.title)}
                title={recentSearch.title}
                query={searchQuery}
                selected={idx === selectedItem}
                handleSelectedItem={scrollToSelectedItem}
                onClick={onSelectRecentSearch}
                selectedItem={selectedItem}
              />
            ))}
          </Section>
        )}
        {loading && !!searchQuery && <Section>{placeholders}</Section>}
        {!!slicedVideos.length && !loading && (
          <Section>
            <Caption secondary variant="t100">
              Videos
            </Caption>
            {slicedVideos.map((video, idx) => (
              <Result
                key={`result-video-${video.id}`}
                video={video}
                query={searchQuery}
                selected={selectedItem === idx + filteredRecentSearches.length}
                handleSelectedItem={scrollToSelectedItem}
                selectedItem={selectedItem}
              />
            ))}
          </Section>
        )}
        {!!slicedChannels.length && !loading && (
          <Section>
            <Caption secondary variant="t100">
              Channels
            </Caption>
            {slicedChannels.map((channel, idx) => (
              <Result
                key={`result-channel-${channel.id}`}
                channel={channel}
                query={searchQuery}
                selected={selectedItem === idx + filteredRecentSearches.length + slicedVideos.length}
                handleSelectedItem={scrollToSelectedItem}
                selectedItem={selectedItem}
              />
            ))}
          </Section>
        )}
        <ShortcutsWrapper>
          <ShortcutsGroup>
            <StyledShortcutIndicator group>↓</StyledShortcutIndicator>
            <StyledShortcutIndicator>↑</StyledShortcutIndicator>
            to navigate
          </ShortcutsGroup>
          <ShortcutsGroup>
            <StyledShortcutIndicator>↩</StyledShortcutIndicator>
            to select
          </ShortcutsGroup>
          <ShortcutsGroup>
            <StyledShortcutIndicator>/</StyledShortcutIndicator>
            to search
          </ShortcutsGroup>
        </ShortcutsWrapper>
      </Container>
    )
  }
)
Example #11
Source File: VideoPlayer.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, VideoPlayerProps> = (
  {
    isVideoPending,
    className,
    playing,
    nextVideo,
    channelId,
    videoId,
    autoplay,
    videoStyle,
    isEmbedded,
    isPlayNextDisabled,
    ...videoJsConfig
  },
  externalRef
) => {
  const [player, playerRef] = useVideoJsPlayer(videoJsConfig)
  const [isPlaying, setIsPlaying] = useState(false)
  const {
    currentVolume,
    cachedVolume,
    cinematicView,
    actions: { setCurrentVolume, setCachedVolume, setCinematicView },
  } = usePersonalDataStore((state) => state)
  const [volumeToSave, setVolumeToSave] = useState(0)

  const [videoTime, setVideoTime] = useState(0)
  const [isFullScreen, setIsFullScreen] = useState(false)
  const [isPiPEnabled, setIsPiPEnabled] = useState(false)

  const [playerState, setPlayerState] = useState<PlayerState>('loading')
  const [isLoaded, setIsLoaded] = useState(false)
  const [needsManualPlay, setNeedsManualPlay] = useState(!autoplay)
  const mdMatch = useMediaMatch('md')

  const playVideo = useCallback(
    async (player: VideoJsPlayer | null, withIndicator?: boolean, callback?: () => void) => {
      if (!player) {
        return
      }
      withIndicator && player.trigger(CustomVideojsEvents.PlayControl)
      try {
        setNeedsManualPlay(false)
        const playPromise = await player.play()
        if (playPromise && callback) callback()
      } catch (error) {
        if (error.name === 'AbortError') {
          // this will prevent throwing harmless error `the play() request was interrupted by a call to pause()`
          // Video.js doing something similiar, check:
          // https://github.com/videojs/video.js/issues/6998
          // https://github.com/videojs/video.js/blob/4238f5c1d88890547153e7e1de7bd0d1d8e0b236/src/js/utils/promise.js
          return
        }
        if (error.name === 'NotAllowedError') {
          ConsoleLogger.warn('Video playback failed', error)
        } else {
          SentryLogger.error('Video playback failed', 'VideoPlayer', error, {
            video: { id: videoId, url: videoJsConfig.src },
          })
        }
      }
    },
    [videoId, videoJsConfig.src]
  )

  const pauseVideo = useCallback((player: VideoJsPlayer | null, withIndicator?: boolean, callback?: () => void) => {
    if (!player) {
      return
    }
    withIndicator && player.trigger(CustomVideojsEvents.PauseControl)
    callback?.()
    player.pause()
  }, [])

  useEffect(() => {
    if (!isVideoPending) {
      return
    }
    setPlayerState('pending')
  }, [isVideoPending])

  // handle hotkeys
  useEffect(() => {
    if (!player) {
      return
    }

    const handler = (event: KeyboardEvent) => {
      if (
        (document.activeElement?.tagName === 'BUTTON' && event.key === ' ') ||
        document.activeElement?.tagName === 'INPUT'
      ) {
        return
      }

      const playerReservedKeys = ['k', ' ', 'ArrowLeft', 'ArrowRight', 'j', 'l', 'ArrowUp', 'ArrowDown', 'm', 'f', 'c']
      if (
        !event.altKey &&
        !event.ctrlKey &&
        !event.metaKey &&
        !event.shiftKey &&
        playerReservedKeys.includes(event.key)
      ) {
        event.preventDefault()
        hotkeysHandler(event, player, playVideo, pauseVideo, () => setCinematicView(!cinematicView))
      }
    }
    document.addEventListener('keydown', handler)

    return () => document.removeEventListener('keydown', handler)
  }, [cinematicView, pauseVideo, playVideo, player, playerState, setCinematicView])

  // handle error
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      setPlayerState('error')
    }
    player.on('error', handler)
    return () => {
      player.off('error', handler)
    }
  })

  // handle video loading
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = (event: Event) => {
      if (event.type === 'waiting' || event.type === 'seeking') {
        setPlayerState('loading')
      }
      if (event.type === 'canplay' || event.type === 'seeked') {
        setPlayerState('playingOrPaused')
      }
    }
    player.on(['waiting', 'canplay', 'seeking', 'seeked'], handler)
    return () => {
      player.off(['waiting', 'canplay', 'seeking', 'seeked'], handler)
    }
  }, [player, playerState])

  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      setPlayerState('ended')
    }
    player.on('ended', handler)
    return () => {
      player.off('ended', handler)
    }
  }, [nextVideo, player])

  // handle loadstart
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      setIsLoaded(true)
    }
    player.on('loadstart', handler)
    return () => {
      player.off('loadstart', handler)
    }
  }, [player])

  // handle autoplay
  useEffect(() => {
    if (!player || !isLoaded || !autoplay) {
      return
    }
    const playPromise = player.play()
    if (playPromise) {
      playPromise
        .then(() => {
          setIsPlaying(true)
        })
        .catch((e) => {
          setNeedsManualPlay(true)
          ConsoleLogger.warn('Video autoplay failed', e)
        })
    }
  }, [player, isLoaded, autoplay])

  // handle playing and pausing from outside the component
  useEffect(() => {
    if (!player) {
      return
    }
    if (playing) {
      playVideo(player)
    } else {
      player.pause()
    }
  }, [playVideo, player, playing])

  // handle playing and pausing
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = (event: Event) => {
      if (event.type === 'play') {
        setIsPlaying(true)
      }
      if (event.type === 'pause') {
        setIsPlaying(false)
      }
    }
    player.on(['play', 'pause'], handler)
    return () => {
      player.off(['play', 'pause'], handler)
    }
  }, [player, playerState])

  useEffect(() => {
    if (!externalRef) {
      return
    }
    if (typeof externalRef === 'function') {
      externalRef(playerRef.current)
    } else {
      externalRef.current = playerRef.current
    }
  }, [externalRef, playerRef])

  // handle video timer
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      const currentTime = round(player.currentTime())
      setVideoTime(currentTime)
    }
    player.on('timeupdate', handler)
    return () => {
      player.off('timeupdate', handler)
    }
  }, [player])

  // handle seeking
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      if (playerState === 'ended') {
        playVideo(player)
      }
    }
    player.on('seeking', handler)
    return () => {
      player.off('seeking', handler)
    }
  }, [playVideo, player, playerState])

  // handle fullscreen mode
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      // will remove focus from fullscreen button and apply to player.
      player.focus()
      setIsFullScreen(player.isFullscreen())
    }
    player.on('fullscreenchange', handler)
    return () => {
      player.off('fullscreenchange', handler)
    }
  }, [player])

  // handle picture in picture
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = (event: Event) => {
      if (event.type === 'enterpictureinpicture') {
        setIsPiPEnabled(true)
      }
      if (event.type === 'leavepictureinpicture') {
        setIsPiPEnabled(false)
      }
    }
    player.on(['enterpictureinpicture', 'leavepictureinpicture'], handler)
    return () => {
      player.off(['enterpictureinpicture', 'leavepictureinpicture'], handler)
    }
  }, [player])

  // update volume on keyboard input
  useEffect(() => {
    if (!player) {
      return
    }
    const events = [
      CustomVideojsEvents.VolumeIncrease,
      CustomVideojsEvents.VolumeDecrease,
      CustomVideojsEvents.Muted,
      CustomVideojsEvents.Unmuted,
    ]

    const handler = (event: Event) => {
      if (event.type === CustomVideojsEvents.Muted) {
        if (currentVolume) {
          setCachedVolume(currentVolume)
        }
        setCurrentVolume(0)
        return
      }
      if (event.type === CustomVideojsEvents.Unmuted) {
        setCurrentVolume(cachedVolume || VOLUME_STEP)
        return
      }
      if (event.type === CustomVideojsEvents.VolumeIncrease || CustomVideojsEvents.VolumeDecrease) {
        setCurrentVolume(player.volume())
      }
    }
    player.on(events, handler)
    return () => {
      player.off(events, handler)
    }
  }, [currentVolume, player, cachedVolume, setCachedVolume, setCurrentVolume])

  const debouncedVolumeChange = useRef(
    debounce((volume: number) => {
      setVolumeToSave(volume)
    }, 125)
  )
  // update volume on mouse input
  useEffect(() => {
    if (!player) {
      return
    }
    player?.volume(currentVolume)

    debouncedVolumeChange.current(currentVolume)
    if (currentVolume) {
      player.muted(false)
    } else {
      if (volumeToSave) {
        setCachedVolume(volumeToSave)
      }
      player.muted(true)
    }
  }, [currentVolume, volumeToSave, player, setCachedVolume])

  // button/input handlers
  const handlePlayPause = useCallback(() => {
    if (playerState === 'error') {
      return
    }
    if (isPlaying) {
      pauseVideo(player, true, () => setIsPlaying(false))
    } else {
      playVideo(player, true, () => setIsPlaying(true))
    }
  }, [isPlaying, pauseVideo, playVideo, player, playerState])

  const handleChangeVolume = (event: React.ChangeEvent<HTMLInputElement>) => {
    setCurrentVolume(Number(event.target.value))
  }

  const handleMute = () => {
    if (currentVolume === 0) {
      setCurrentVolume(cachedVolume || 0.05)
    } else {
      setCurrentVolume(0)
    }
  }

  const handlePictureInPicture = (event: React.MouseEvent) => {
    event.stopPropagation()
    if (document.pictureInPictureElement) {
      // @ts-ignore @types/video.js is outdated and doesn't provide types for some newer video.js features
      player.exitPictureInPicture()
    } else {
      if (document.pictureInPictureEnabled) {
        // @ts-ignore @types/video.js is outdated and doesn't provide types for some newer video.js features
        player.requestPictureInPicture().catch((e) => {
          ConsoleLogger.warn('Picture in picture failed', e)
        })
      }
    }
  }

  const handleFullScreen = (event: React.MouseEvent) => {
    event.stopPropagation()
    if (!isFullScreenEnabled) {
      return
    }
    if (player?.isFullscreen()) {
      player?.exitFullscreen()
    } else {
      player?.requestFullscreen()
    }
  }

  const onVideoClick = useCallback(
    () =>
      player?.paused()
        ? player?.trigger(CustomVideojsEvents.PauseControl)
        : player?.trigger(CustomVideojsEvents.PlayControl),
    [player]
  )

  const renderVolumeButton = () => {
    if (currentVolume === 0) {
      return <StyledSvgPlayerSoundOff />
    } else {
      return currentVolume <= 0.5 ? <StyledSvgControlsSoundLowVolume /> : <StyledSvgPlayerSoundOn />
    }
  }

  const toggleCinematicView = (event: React.MouseEvent) => {
    event.stopPropagation()
    setCinematicView(!cinematicView)
  }

  const showPlayerControls = isLoaded && playerState
  const showControlsIndicator = playerState !== 'ended'

  return (
    <Container isFullScreen={isFullScreen} className={className}>
      <div data-vjs-player onClick={handlePlayPause}>
        {needsManualPlay && (
          <BigPlayButtonContainer onClick={handlePlayPause}>
            <BigPlayButton onClick={handlePlayPause}>
              <StyledSvgControlsPlay />
            </BigPlayButton>
          </BigPlayButtonContainer>
        )}
        <video style={videoStyle} ref={playerRef} className="video-js" onClick={onVideoClick} />
        {showPlayerControls && (
          <>
            <ControlsOverlay isFullScreen={isFullScreen}>
              <CustomTimeline
                playVideo={playVideo}
                pauseVideo={pauseVideo}
                player={player}
                isFullScreen={isFullScreen}
                playerState={playerState}
                setPlayerState={setPlayerState}
              />
              <CustomControls isFullScreen={isFullScreen} isEnded={playerState === 'ended'}>
                <PlayControl isLoading={playerState === 'loading'}>
                  {(!needsManualPlay || mdMatch) && (
                    <PlayButton
                      isEnded={playerState === 'ended'}
                      onClick={handlePlayPause}
                      tooltipText={isPlaying ? 'Pause (k)' : playerState === 'ended' ? 'Play again (k)' : 'Play (k)'}
                      tooltipPosition="left"
                    >
                      {playerState === 'ended' ? (
                        <StyledSvgControlsReplay />
                      ) : isPlaying ? (
                        <StyledSvgControlsPause />
                      ) : (
                        <StyledSvgControlsPlay />
                      )}
                    </PlayButton>
                  )}
                </PlayControl>
                <VolumeControl onClick={(e) => e.stopPropagation()}>
                  <VolumeButton tooltipText="Volume" showTooltipOnlyOnFocus onClick={handleMute}>
                    {renderVolumeButton()}
                  </VolumeButton>
                  <VolumeSliderContainer>
                    <VolumeSlider
                      step={0.01}
                      max={1}
                      min={0}
                      value={currentVolume}
                      onChange={handleChangeVolume}
                      type="range"
                    />
                  </VolumeSliderContainer>
                </VolumeControl>
                <CurrentTimeWrapper>
                  <CurrentTime variant="t200">
                    {formatDurationShort(videoTime)} / {formatDurationShort(round(player?.duration() || 0))}
                  </CurrentTime>
                </CurrentTimeWrapper>
                <ScreenControls>
                  {mdMatch && !isEmbedded && !player?.isFullscreen() && (
                    <PlayerControlButton
                      onClick={toggleCinematicView}
                      tooltipText={cinematicView ? 'Exit cinematic mode (c)' : 'Cinematic view (c)'}
                    >
                      {cinematicView ? (
                        <StyledSvgControlsVideoModeCompactView />
                      ) : (
                        <StyledSvgControlsVideoModeCinemaView />
                      )}
                    </PlayerControlButton>
                  )}
                  {isPiPSupported && (
                    <PlayerControlButton onClick={handlePictureInPicture} tooltipText="Picture-in-picture">
                      {isPiPEnabled ? <StyledSvgControlsPipOff /> : <StyledSvgControlsPipOn />}
                    </PlayerControlButton>
                  )}
                  <PlayerControlButton
                    isDisabled={!isFullScreenEnabled}
                    tooltipPosition="right"
                    tooltipText={isFullScreen ? 'Exit full screen (f)' : 'Full screen (f)'}
                    onClick={handleFullScreen}
                  >
                    {isFullScreen ? <StyledSvgControlsSmallScreen /> : <StyledSvgControlsFullScreen />}
                  </PlayerControlButton>
                </ScreenControls>
              </CustomControls>
            </ControlsOverlay>
          </>
        )}
        <VideoOverlay
          videoId={videoId}
          isFullScreen={isFullScreen}
          isPlayNextDisabled={isPlayNextDisabled}
          playerState={playerState}
          onPlay={handlePlayPause}
          channelId={channelId}
          currentThumbnailUrl={videoJsConfig.posterUrl}
          playRandomVideoOnEnded={!isEmbedded}
        />
        {showControlsIndicator && <ControlsIndicator player={player} isLoading={playerState === 'loading'} />}
      </div>
    </Container>
  )
}
Example #12
Source File: useStartFileUpload.ts    From atlas with GNU General Public License v3.0 4 votes vote down vote up
useStartFileUpload = () => {
  const navigate = useNavigate()
  const { displaySnackbar } = useSnackbar()
  const { getRandomStorageOperatorForBag, markStorageOperatorFailed } = useStorageOperators()

  const { addAssetFile, addAssetToUploads, setUploadStatus, addProcessingAsset } = useUploadsStore(
    (state) => state.actions
  )
  const assetsFiles = useUploadsStore((state) => state.assetsFiles)
  const pendingUploadingNotificationsCounts = useRef(0)
  const assetsNotificationsCount = useRef<{
    uploads: {
      [key: string]: number
    }
    uploaded: {
      [key: string]: number
    }
  }>({
    uploads: {},
    uploaded: {},
  })

  const displayUploadingNotification = useRef(
    debounce(() => {
      displaySnackbar({
        title: `${pendingUploadingNotificationsCounts.current} ${
          pendingUploadingNotificationsCounts.current > 1 ? 'files' : 'file'
        } added to uploads`,
        iconType: 'uploading',
        timeout: UPLOADING_SNACKBAR_TIMEOUT,
        actionText: 'Inspect',
        onActionClick: () => navigate(absoluteRoutes.studio.uploads()),
      })
      pendingUploadingNotificationsCounts.current = 0
    }, 700)
  )

  const startFileUpload = useCallback(
    async (file: File | Blob | null, asset: InputAssetUpload, opts?: StartFileUploadOptions) => {
      let uploadOperator: OperatorInfo
      const bagId = ASSET_CHANNEL_BAG_PREFIX + asset.owner
      try {
        const storageOperator = await getRandomStorageOperatorForBag(bagId)
        if (!storageOperator) {
          SentryLogger.error('No storage operator available for upload', 'useStartFileUpload')
          return
        }
        uploadOperator = storageOperator
      } catch (e) {
        SentryLogger.error('Failed to get storage operator for upload', 'useStartFileUpload', e)
        return
      }

      ConsoleLogger.debug('Starting file upload', {
        contentId: asset.id,
        uploadOperator,
      })

      const setAssetStatus = (status: Partial<UploadStatus>) => {
        setUploadStatus(asset.id, status)
      }
      const fileInState = assetsFiles?.find((file) => file.contentId === asset.id)
      if (!fileInState && file) {
        addAssetFile({ contentId: asset.id, blob: file })
      }

      const assetKey = `${asset.parentObject.type}-${asset.parentObject.id}`

      try {
        const fileToUpload = opts?.changeHost ? fileInState?.blob : file
        if (!fileToUpload) {
          throw new Error('File was not provided nor found')
        }

        if (!opts?.isReUpload && !opts?.changeHost && file) {
          addAssetToUploads({ ...asset, size: file.size.toString() })
        }

        setAssetStatus({ lastStatus: 'inProgress', progress: 0 })

        const setUploadProgress = ({ loaded, total }: ProgressEvent) => {
          setAssetStatus({ progress: (loaded / total) * 100 })

          if ((loaded / total) * 100 === 100) {
            addProcessingAsset(asset.id)
            setAssetStatus({ lastStatus: 'processing', progress: (loaded / total) * 100 })
          }
        }

        pendingUploadingNotificationsCounts.current++
        displayUploadingNotification.current()
        assetsNotificationsCount.current.uploads[assetKey] =
          (assetsNotificationsCount.current.uploads[assetKey] || 0) + 1

        const formData = new FormData()
        formData.append('dataObjectId', asset.id)
        formData.append('storageBucketId', uploadOperator.id)
        formData.append('bagId', bagId)
        formData.append('file', fileToUpload, (file as File).name)

        rax.attach()
        const raxConfig: RetryConfig = {
          retry: RETRIES_COUNT,
          noResponseRetries: RETRIES_COUNT,
          retryDelay: RETRY_DELAY,
          backoffType: 'static',
          onRetryAttempt: (err) => {
            const cfg = rax.getConfig(err)
            if (cfg?.currentRetryAttempt || 0 >= 1) {
              setAssetStatus({ lastStatus: 'reconnecting', retries: cfg?.currentRetryAttempt })
            }
          },
        }

        await axios.post(createAssetUploadEndpoint(uploadOperator.endpoint), formData, {
          raxConfig,
          onUploadProgress: setUploadProgress,
        })

        assetsNotificationsCount.current.uploaded[assetKey] =
          (assetsNotificationsCount.current.uploaded[assetKey] || 0) + 1
      } catch (e) {
        SentryLogger.error('Failed to upload asset', 'useStartFileUpload', e, {
          asset: { dataObjectId: asset.id, uploadOperator },
        })

        setAssetStatus({ lastStatus: 'error', progress: 0 })

        const axiosError = e as AxiosError
        const networkFailure =
          axiosError.isAxiosError &&
          (!axiosError.response?.status || (axiosError.response.status < 400 && axiosError.response.status >= 500))
        if (networkFailure) {
          markStorageOperatorFailed(uploadOperator.id)
        }

        const snackbarDescription = networkFailure ? 'Host is not responding' : 'Unexpected error occurred'
        displaySnackbar({
          title: 'Failed to upload asset',
          description: snackbarDescription,
          actionText: 'Go to uploads',
          onActionClick: () => navigate(absoluteRoutes.studio.uploads()),
          iconType: 'warning',
        })
      }
    },
    [
      assetsFiles,
      getRandomStorageOperatorForBag,
      setUploadStatus,
      addAssetFile,
      addProcessingAsset,
      addAssetToUploads,
      displaySnackbar,
      markStorageOperatorFailed,
      navigate,
    ]
  )

  return startFileUpload
}
Example #13
Source File: editor.tsx    From gosling.js with MIT License 4 votes vote down vote up
/**
 * React component for editing Gosling specs
 */
function Editor(props: RouteComponentProps) {
    // Determines whether the screen is too small (e.g., mobile)
    const IS_SMALL_SCREEN = window.innerWidth <= 500;

    // custom spec contained in the URL
    const urlParams = new URLSearchParams(props.location.search);
    const urlSpec = urlParams.has('spec') ? JSONCrush.uncrush(urlParams.get('spec')!) : null;
    const urlGist = urlParams.get('gist');
    const urlExampleId = urlParams.get('example') ?? '';

    const defaultCode =
        urlGist || urlExampleId ? emptySpec() : stringify(urlSpec ?? (INIT_DEMO.spec as gosling.GoslingSpec));
    const defaultJsCode = urlGist || urlExampleId || !INIT_DEMO.specJs ? json2js(defaultCode) : INIT_DEMO.specJs;

    const previewData = useRef<PreviewData[]>([]);
    const [refreshData, setRefreshData] = useState<boolean>(false);
    const [language, changeLanguage] = useState<EditorLangauge>('json');

    const [demo, setDemo] = useState(
        examples[urlExampleId] ? { id: urlExampleId, ...examples[urlExampleId] } : INIT_DEMO
    );
    const [isImportDemo, setIsImportDemo] = useState(false);
    const [theme, setTheme] = useState<gosling.Theme>('light');
    const [hg, setHg] = useState<HiGlassSpec>();
    const [code, setCode] = useState(defaultCode);
    const [jsCode, setJsCode] = useState(defaultJsCode); //[TO-DO: more js format examples]
    const [goslingSpec, setGoslingSpec] = useState<gosling.GoslingSpec>();
    const [log, setLog] = useState<ReturnType<typeof gosling.validateGoslingSpec>>({ message: '', state: 'success' });
    // const [mouseEventInfo, setMouseEventInfo] =
    //     useState<{ type: 'mouseOver' | 'click'; data: Datum[]; position: string }>();
    const [showExamples, setShowExamples] = useState(false);
    const [autoRun, setAutoRun] = useState(true);
    const [selectedPreviewData, setSelectedPreviewData] = useState<number>(0);
    const [gistTitle, setGistTitle] = useState<string>();
    const [description, setDescription] = useState<string | null>();
    const [expertMode, setExpertMode] = useState(false);

    // This parameter only matter when a markdown description was loaded from a gist but the user wants to hide it
    const [hideDescription, setHideDescription] = useState<boolean>(IS_SMALL_SCREEN || false);

    // Determine the size of description panel
    const [descPanelWidth, setDescPanelWidth] = useState(getDescPanelDefultWidth());

    // whether to show HiGlass' viewConfig on the left-bottom
    const [showVC, setShowVC] = useState<boolean>(false);

    // whether the code editor is read-only
    const [readOnly, setReadOnly] = useState<boolean>(urlGist ? true : false);

    // whether to hide source code on the left
    const [isHideCode, setIsHideCode] = useState<boolean>(IS_SMALL_SCREEN || urlParams.get('full') === 'true' || false);

    // whether to show widgets for responsive window
    const [isResponsive, setIsResponsive] = useState<boolean>(true);
    const [screenSize, setScreenSize] = useState<undefined | { width: number; height: number }>();
    const [visibleScreenSize, setVisibleScreenSize] = useState<undefined | { width: number; height: number }>();

    // whether to show data preview on the right-bottom
    const [isShowDataPreview, setIsShowDataPreview] = useState<boolean>(false);

    // whether to show a find box
    const [isFindCode, setIsFindCode] = useState<boolean | undefined>(undefined);

    // whether to use larger or smaller font
    const [isFontZoomIn, setIsfontZoomIn] = useState<boolean | undefined>(undefined);
    const [isFontZoomOut, setIsfontZoomOut] = useState<boolean | undefined>(undefined);

    // whether description panel is being dragged
    const [isDescResizing, setIsDescResizing] = useState(false);

    // whether to show "about" information
    const [isShowAbout, setIsShowAbout] = useState(false);

    // Resizer `div`
    const descResizerRef = useRef<any>();

    // Drag event for resizing description panel
    const dragX = useRef<any>();

    // for using HiGlass JS API
    // const hgRef = useRef<any>();
    const gosRef = useRef<gosling.GoslingRef>(null);

    const debounceCodeEdit = useRef(
        debounce((code: string, language: EditorLangauge) => {
            if (language == 'json') {
                setCode(code);
            } else {
                setJsCode(code);
            }
        }, 1500)
    );

    // publish event listeners to Gosling.js
    useEffect(() => {
        if (gosRef.current) {
            // gosRef.current.api.subscribe('rawdata', (type, data) => {
            // console.log('rawdata', data);
            // gosRef.current.api.zoomTo('bam-1', `chr${data.data.chr1}:${data.data.start1}-${data.data.end1}`, 2000);
            // gosRef.current.api.zoomTo('bam-2', `chr${data.data.chr2}:${data.data.start2}-${data.data.end2}`, 2000);
            // console.log('click', data.data);
            // TODO: show messages on the right-bottom of the editor
            // gosRef.current.api.subscribe('mouseOver', (type, eventData) => {
            //     setMouseEventInfo({ type: 'mouseOver', data: eventData.data, position: eventData.genomicPosition });
            // });
            // gosRef.current.api.subscribe('click', (type, eventData) => {
            //     setMouseEventInfo({ type: 'click', data: eventData.data, position: eventData.genomicPosition });
            // });
            // Range Select API
            // gosRef.current.api.subscribe('rangeSelect', (type, eventData) => {
            //     console.warn(type, eventData.id, eventData.genomicRange, eventData.data);
            // });
        }
        return () => {
            // gosRef.current.api.unsubscribe('mouseOver');
            // gosRef.current.api.unsubscribe('click');
            // gosRef.current?.api.unsubscribe('rangeSelect');
        };
    }, [gosRef.current]);

    /**
     * Editor mode
     */
    useEffect(() => {
        previewData.current = [];
        setSelectedPreviewData(0);
        if (isImportDemo) {
            const jsonCode = stringifySpec(demo.spec as gosling.GoslingSpec);
            setCode(jsonCode);
            setJsCode(demo.specJs ?? json2js(jsonCode));
        } else if (urlExampleId && !validateExampleId(urlExampleId)) {
            // invalida url example id
            setCode(emptySpec(`Example id "${urlExampleId}" does not exist.`));
            setJsCode(emptySpec(`Example id "${urlExampleId}" does not exist.`));
        } else if (urlSpec) {
            setCode(urlSpec);
            setJsCode(json2js(urlSpec));
        } else if (urlGist) {
            setCode(emptySpec('loading....'));
        } else {
            const jsonCode = stringifySpec(demo.spec as gosling.GoslingSpec);
            setCode(jsonCode);
            setJsCode(demo.specJs ?? json2js(jsonCode));
        }
        setHg(undefined);
    }, [demo]);

    const deviceToResolution = {
        Auto: undefined,
        UHD: { width: 3840, height: 2160 },
        FHD: { width: 1920, height: 1080 },
        'Google Nexus Tablet': { width: 1024, height: 768 },
        'iPhone X': { width: 375, height: 812 }
    };

    const ResponsiveWidget = useMemo(() => {
        return (
            <div
                style={{
                    width: screenSize ? screenSize.width - 20 : 'calc(100% - 20px)',
                    background: 'white',
                    marginBottom: '6px',
                    padding: '10px',
                    height: '20px',
                    lineHeight: '20px'
                }}
            >
                <span
                    style={{
                        marginRight: 10,
                        color: 'gray',
                        verticalAlign: 'middle',
                        display: 'inline-block',
                        marginTop: '2px'
                    }}
                >
                    {getIconSVG(ICONS.SCREEN, 16, 16)}
                </span>
                <span className="screen-size-dropdown">
                    <select
                        style={{ width: '80px' }}
                        onChange={e => {
                            const device = e.target.value;
                            if (Object.keys(deviceToResolution).includes(device)) {
                                setScreenSize((deviceToResolution as any)[device]);
                                setVisibleScreenSize((deviceToResolution as any)[device]);
                            }
                        }}
                    >
                        {[...Object.keys(deviceToResolution)].map(d =>
                            d !== '-' ? (
                                <option key={d} value={d}>
                                    {d}
                                </option>
                            ) : (
                                // separator (https://stackoverflow.com/questions/899148/html-select-option-separator)
                                <optgroup label="──────────"></optgroup>
                            )
                        )}
                    </select>
                </span>
                <span style={{ marginLeft: '20px', visibility: screenSize ? 'visible' : 'collapse' }}>
                    <span style={{ marginRight: 10, color: '#EEBF4D' }}>{getIconSVG(ICONS.RULER, 12, 12)}</span>
                    <input
                        type="number"
                        min="350"
                        max="3000"
                        value={visibleScreenSize?.width}
                        onChange={e => {
                            const width = +e.target.value >= 350 ? +e.target.value : 350;
                            setVisibleScreenSize({ width: +e.target.value, height: screenSize?.height ?? 1000 });
                            setScreenSize({ width, height: screenSize?.height ?? 1000 });
                        }}
                    />
                    {' x '}
                    <input
                        type="number"
                        min="100"
                        max="3000"
                        value={visibleScreenSize?.height}
                        onChange={e => {
                            const height = +e.target.value >= 100 ? +e.target.value : 100;
                            setVisibleScreenSize({ width: screenSize?.width ?? 1000, height: +e.target.value });
                            setScreenSize({ width: screenSize?.width ?? 1000, height });
                        }}
                    />
                    <span
                        style={{
                            marginLeft: 10,
                            color: 'gray',
                            verticalAlign: 'middle',
                            display: 'inline-block',
                            marginTop: '2px',
                            cursor: 'pointer'
                        }}
                        onClick={() => {
                            setVisibleScreenSize({
                                width: visibleScreenSize?.height ?? 1000,
                                height: visibleScreenSize?.width ?? 1000
                            });
                            setScreenSize({ width: screenSize?.height ?? 1000, height: screenSize?.width ?? 1000 });
                        }}
                    >
                        {getIconSVG(ICONS.REPEAT, 20, 20)}
                    </span>
                </span>
            </div>
        );
    }, [screenSize]);

    useEffect(() => {
        let active = true;

        if (!urlGist || typeof urlGist !== 'string') return undefined;

        fetchSpecFromGist(urlGist)
            .then(({ code, jsCode, language, description, title }) => {
                if (active) {
                    setReadOnly(false);
                    setJsCode(jsCode);
                    setCode(code);
                    changeLanguage(language);
                    setGistTitle(title);
                    setDescription(description);
                }
            })
            .catch(error => {
                if (active) {
                    setReadOnly(false);
                    setCode(emptySpec(error));
                    setJsCode(emptySpec(error));
                    setDescription(undefined);
                    setGistTitle('Error loading gist! See code for details.');
                }
            });

        return () => {
            setReadOnly(false);
            active = false;
        };
    }, [urlGist]);

    const runSpecUpdateVis = useCallback(
        (run?: boolean) => {
            if (isEqual(emptySpec(), code) && isEqual(emptySpec(), jsCode)) {
                // this means we do not have to compile. This is when we are in the middle of loading data from gist.
                return;
            }

            let editedGos;
            let valid;

            if (language === 'json') {
                try {
                    editedGos = JSON.parse(stripJsonComments(code));
                    valid = gosling.validateGoslingSpec(editedGos);
                    setLog(valid);
                } catch (e) {
                    const message = '✘ Cannnot parse the code.';
                    console.warn(message);
                    setLog({ message, state: 'error' });
                }
                if (!editedGos || valid?.state !== 'success' || (!autoRun && !run)) return;

                setGoslingSpec(editedGos);
            } else if (language === 'typescript') {
                transpile(jsCode)
                    .then(toJavaScriptDataURI)
                    .then(uri => import(/* @vite-ignore */ uri))
                    .then(ns => {
                        const editedGos = ns.spec;
                        if (urlGist && !isImportDemo) {
                            setCode(stringifySpec(editedGos));
                        }
                        valid = gosling.validateGoslingSpec(editedGos);
                        setLog(valid);
                        if (!editedGos || valid?.state !== 'success' || (!autoRun && !run)) return;
                        setGoslingSpec(editedGos);
                    })
                    .catch(e => {
                        const message = '✘ Cannnot parse the code.';
                        console.warn(message, e);
                        setLog({ message, state: 'error' });
                    });
            } else {
                setLog({ message: `${language} is not supported`, state: 'error' });
            }
        },
        [code, jsCode, autoRun, language, readOnly]
    );

    /**
     * Update theme of the editor based on the theme of Gosling visualizations
     */
    // useEffect(() => {
    //     const gosTheme = getTheme(goslingSpec?.theme);
    //     if (gosTheme.base !== theme) {
    //         setTheme(gosTheme.base);
    //     }
    // }, [goslingSpec]);

    /**
     * Things to do upon spec change
     */
    useEffect(() => {
        const newIsResponsive =
            typeof goslingSpec?.responsiveSize === 'undefined'
                ? false
                : typeof goslingSpec?.responsiveSize === 'boolean'
                ? goslingSpec?.responsiveSize === true
                : typeof goslingSpec?.responsiveSize === 'object'
                ? goslingSpec?.responsiveSize.width === true || goslingSpec?.responsiveSize.height === true
                : false;
        if (newIsResponsive !== isResponsive && newIsResponsive) {
            setScreenSize(undefined); // reset the screen
            setVisibleScreenSize(undefined);
        }
        setIsResponsive(newIsResponsive);
    }, [goslingSpec]);

    /**
     * Subscribe preview data that is being processed in the Gosling tracks.
     */
    useEffect(() => {
        // We want to show data preview in the editor.
        const token = PubSub.subscribe('data-preview', (_: string, data: PreviewData) => {
            // Data with different `dataConfig` is shown separately in data preview.
            const id = `${data.dataConfig}`;
            const newPreviewData = previewData.current.filter(d => d.id !== id);
            previewData.current = [...newPreviewData, { ...data, id }];
        });
        return () => {
            PubSub.unsubscribe(token);
        };
    });

    /**
     * Render visualization when edited
     */
    useEffect(() => {
        previewData.current = [];
        setSelectedPreviewData(0);
        runSpecUpdateVis();
    }, [code, jsCode, autoRun, language, theme]);

    // Uncommnet below to use HiGlass APIs
    // useEffect(() => {
    //     if(hgRef.current) {
    //         hgRef.current.api.activateTool('select');
    //     }
    // }, [hg, hgRef]); // TODO: should `hg` be here?

    function getDataPreviewInfo(dataConfig: string) {
        // Detailed information of data config to show in the editor
        const dataConfigObj = JSON.parse(dataConfig);
        if (!dataConfigObj.data?.type) {
            // We do not have enough information
            return '';
        }

        let info = '';
        if (dataConfigObj.data) {
            Object.keys(dataConfigObj.data).forEach(key => {
                if (typeof dataConfigObj.data[key] === 'object') {
                    info += `${JSON.stringify(dataConfigObj.data[key])} | `;
                } else {
                    info += `${dataConfigObj.data[key]} | `;
                }
            });
        }

        return info.slice(0, info.length - 2);
    }

    // Set up the d3-drag handler functions (started, ended, dragged).
    const started = useCallback(() => {
        if (!hideDescription) {
            // Drag is enabled only when the description panel is visible
            dragX.current = d3Event.sourceEvent.clientX;
            setIsDescResizing(true);
        }
    }, [dragX, descPanelWidth]);

    const dragged = useCallback(() => {
        if (dragX.current) {
            const diff = d3Event.sourceEvent.clientX - dragX.current;
            setDescPanelWidth(descPanelWidth - diff);
        }
    }, [dragX, descPanelWidth]);

    const ended = useCallback(() => {
        dragX.current = null;
        setIsDescResizing(false);
    }, [dragX, descPanelWidth]);

    // Detect drag events for the resize element.
    useEffect(() => {
        const resizer = descResizerRef.current;

        const drag = d3Drag().on('start', started).on('drag', dragged).on('end', ended);

        d3Select(resizer).call(drag);

        return () => {
            d3Select(resizer).on('.drag', null);
        };
    }, [descResizerRef, started, dragged, ended]);

    function openDescription() {
        setDescPanelWidth(getDescPanelDefultWidth());
        setHideDescription(false);
    }

    function closeDescription() {
        setHideDescription(true);
    }

    // console.log('editor.render()');
    return (
        <>
            <div
                className={`demo-navbar ${theme === 'dark' ? 'dark' : ''}`}
                onClick={() => {
                    if (!gosRef.current) return;

                    // To test APIs, uncomment the following code.
                    // // ! Be aware that the first view is for the title/subtitle track. So navigation API does not work.
                    // const id = gosRef.current.api.getViewIds()?.[1]; //'view-1';
                    // if(id) {
                    //     gosRef.current.api.zoomToExtent(id);
                    // }
                    //
                    // // Static visualization rendered in canvas
                    // const { canvas } = gosRef.current.api.getCanvas({
                    //     resolution: 1,
                    //     transparentBackground: true,
                    // });
                    // const testDiv = document.getElementById('preview-container');
                    // if(canvas && testDiv) {
                    //     testDiv.appendChild(canvas);
                    // }
                }}
            >
                <span
                    style={{ cursor: 'pointer', lineHeight: '40px' }}
                    onClick={() => window.open(`${window.location.pathname}`, '_blank')}
                >
                    <span className="logo">{GoslingLogoSVG(20, 20)}</span>
                    Gosling.js Editor
                </span>
                {urlSpec && <small> Displaying a custom spec contained in URL</small>}
                {gistTitle && !IS_SMALL_SCREEN && (
                    <>
                        <span className="gist-title">{gistTitle}</span>
                        <span
                            title="Open GitHub Gist"
                            style={{ marginLeft: 10 }}
                            className="description-github-button"
                            onClick={() => window.open(`https://gist.github.com/${urlGist}`, '_blank')}
                        >
                            {getIconSVG(ICONS.UP_RIGHT, 14, 14)}
                        </span>
                    </>
                )}
                <span className="demo-label" onClick={() => setShowExamples(true)}>
                    <b>{demo.group}</b>: {demo.name}
                </span>
                {/* <span className="demo-dropdown" hidden={urlSpec !== null || urlGist !== null || urlExampleId !== ''}>
                    <select
                        style={{ maxWidth: IS_SMALL_SCREEN ? window.innerWidth - 180 : 'none' }}
                        onChange={e => {
                            setDemo({ id: e.target.value, ...examples[e.target.value] } as any);
                        }}
                        value={demo.id}
                    >
                        {SHOWN_EXAMPLE_LIST.map(d => (
                            <option key={d.id} value={d.id}>
                                {d.name + (d.underDevelopment ? ' (under development)' : '')}
                            </option>
                        ))}
                    </select>
                </span> */}
                {expertMode ? (
                    <select
                        style={{ maxWidth: IS_SMALL_SCREEN ? window.innerWidth - 180 : 'none' }}
                        onChange={e => {
                            if (Object.keys(Themes).indexOf(e.target.value) !== -1) {
                                setTheme(e.target.value as any);
                            }
                        }}
                        defaultValue={theme as any}
                    >
                        {Object.keys(Themes).map((d: string) => (
                            <option key={d} value={d}>
                                {`Theme: ${d}`}
                            </option>
                        ))}
                    </select>
                ) : null}
                {demo.underDevelopment ? (
                    <span
                        style={{
                            paddingLeft: 12,
                            fontStyle: 'normal',
                            fontSize: 13
                        }}
                    >
                        ? This example is under development ?
                    </span>
                ) : null}
                <input type="hidden" id="spec-url-exporter" />
                {description ? (
                    <span title="Open Textual Description" className="description-button" onClick={openDescription}>
                        {getIconSVG(ICONS.INFO_CIRCLE, 23, 23)}
                    </span>
                ) : null}
            </div>
            {/* ------------------------ Main View ------------------------ */}
            <div className={`editor ${theme === 'dark' ? 'dark' : ''}`}>
                <SplitPane className="side-panel-spliter" split="vertical" defaultSize="50px" allowResize={false}>
                    <div className={`side-panel ${theme === 'dark' ? 'dark' : ''}`}>
                        <span
                            title="Example Gallery"
                            className="side-panel-button"
                            onClick={() => setShowExamples(!showExamples)}
                        >
                            {showExamples ? getIconSVG(ICONS.GRID, 20, 20, '#E18343') : getIconSVG(ICONS.GRID)}
                            <br />
                            EXAMPLE
                        </span>
                        <span
                            title="Automatically update visualization upon editing code"
                            className="side-panel-button"
                            onClick={() => setAutoRun(!autoRun)}
                        >
                            {autoRun
                                ? getIconSVG(ICONS.TOGGLE_ON, 23, 23, '#E18343')
                                : getIconSVG(ICONS.TOGGLE_OFF, 23, 23)}
                            <br />
                            AUTO
                            <br />
                            RUN
                        </span>
                        <span title="Run Code" className="side-panel-button" onClick={() => runSpecUpdateVis(true)}>
                            {getIconSVG(ICONS.PLAY, 23, 23)}
                            <br />
                            RUN
                        </span>
                        <span
                            title="Find"
                            className="side-panel-button"
                            onClick={() => {
                                setIsFindCode(!isFindCode);
                            }}
                        >
                            {getIconSVG(ICONS.FIND, 23, 23)}
                            <br />
                            FIND
                        </span>
                        <span
                            title="Use Larger Font"
                            className="side-panel-button"
                            onClick={() => {
                                setIsfontZoomIn(!isFontZoomIn);
                            }}
                        >
                            {getIconSVG(ICONS.TEXT, 23, 23)}
                            +
                            <br />
                            LARGER
                        </span>
                        <span
                            title="Use Larger Font"
                            className="side-panel-button"
                            onClick={() => {
                                setIsfontZoomOut(!isFontZoomOut);
                            }}
                        >
                            {getIconSVG(ICONS.TEXT, 15, 15)}
                            -
                            <br />
                            SMALLER
                        </span>
                        <span
                            title="Show or hide a code panel"
                            className="side-panel-button"
                            onClick={() => setIsHideCode(!isHideCode)}
                        >
                            {getIconSVG(ICONS.SPLIT, 23, 23)}
                            <br />
                            LAYOUT
                        </span>
                        <span
                            title="Show or hide a data preview"
                            className="side-panel-button"
                            onClick={() => setIsShowDataPreview(!isShowDataPreview)}
                        >
                            {getIconSVG(ICONS.TABLE, 23, 23)}
                            <br />
                            DATA
                            <br />
                            PREVIEW
                        </span>
                        <span
                            title="Save PNG file"
                            className="side-panel-button"
                            onClick={() => {
                                gosRef.current?.api.exportPng();
                            }}
                        >
                            {getIconSVG(ICONS.IMAGE, 23, 23)}
                            <br />
                            PNG
                        </span>
                        <span
                            title="Save PDF file"
                            className="side-panel-button"
                            onClick={() => {
                                gosRef.current?.api.exportPdf();
                            }}
                        >
                            {getIconSVG(ICONS.PDF, 23, 23)}
                            <br />
                            PDF
                        </span>
                        <span
                            title="Save HTML file"
                            className="side-panel-button"
                            onClick={() => {
                                // TODO (05-02-2022): Release a support of `responsiveSize` on `.embed()` first
                                const spec = { ...goslingSpec, responsiveSize: false } as gosling.GoslingSpec;

                                const a = document.createElement('a');
                                a.setAttribute(
                                    'href',
                                    `data:text/plain;charset=utf-8,${encodeURIComponent(
                                        getHtmlTemplate(stringifySpec(spec))
                                    )}`
                                );
                                a.download = 'gosling-visualization.html';
                                document.body.appendChild(a);
                                a.click();
                                document.body.removeChild(a);
                            }}
                        >
                            {getIconSVG(ICONS.HTML, 23, 23)}
                        </span>
                        <span
                            title={
                                stringifySpec(goslingSpec).length <= LIMIT_CLIPBOARD_LEN
                                    ? `Copy unique URL of current view to clipboard (limit: ${LIMIT_CLIPBOARD_LEN} characters)`
                                    : `The current code contains characters more than ${LIMIT_CLIPBOARD_LEN}`
                            }
                            className={
                                stringifySpec(goslingSpec).length <= LIMIT_CLIPBOARD_LEN
                                    ? 'side-panel-button'
                                    : 'side-panel-button side-panel-button-not-active'
                            }
                            onClick={() => {
                                if (stringifySpec(goslingSpec).length <= LIMIT_CLIPBOARD_LEN) {
                                    // copy the unique url to clipboard using `<input/>`
                                    const crushedSpec = encodeURIComponent(JSONCrush.crush(stringifySpec(goslingSpec)));
                                    const url = `${window.location.origin}${window.location.pathname}?full=${isHideCode}&spec=${crushedSpec}`;

                                    navigator.clipboard
                                        .writeText(url)
                                        .then(() =>
                                            // eslint-disable-next-line no-alert
                                            alert(`URL of the current visualization is copied to your clipboard! `)
                                        )
                                        .catch(
                                            // eslint-disable-next-line no-alert
                                            e => alert(`something went wrong ${e}`)
                                        );
                                }
                            }}
                        >
                            {getIconSVG(ICONS.LINK, 23, 23)}
                            <br />
                            SAVE
                            <br />
                            URL
                        </span>
                        <span
                            title="Expert mode that turns on additional features, such as theme selection"
                            className="side-panel-button"
                            onClick={() => setExpertMode(!expertMode)}
                        >
                            {expertMode ? getIconSVG(ICONS.TOGGLE_ON, 23, 23, '#E18343') : getIconSVG(ICONS.TOGGLE_OFF)}
                            <br />
                            EXPERT
                            <br />
                            MODE
                        </span>
                        <span
                            title="Open GitHub repository"
                            className="side-panel-button"
                            onClick={() => window.open('https://github.com/gosling-lang/gosling.js', '_blank')}
                        >
                            {getIconSVG(ICONS.GITHUB, 23, 23)}
                            <br />
                            GITHUB
                        </span>
                        <span
                            title="Open Docs"
                            className="side-panel-button"
                            onClick={() => window.open('http://gosling-lang.org/docs/', '_blank')}
                        >
                            {getIconSVG(ICONS.DOCS, 23, 23)}
                            <br />
                            DOCS
                        </span>
                        <span title="About" className="side-panel-button" onClick={() => setIsShowAbout(!isShowAbout)}>
                            {getIconSVG(ICONS.INFO_RECT_FILLED, 23, 23)}
                            <br />
                            ABOUT
                        </span>
                    </div>
                    <SplitPane
                        split="vertical"
                        defaultSize={'calc(40%)'}
                        size={isHideCode ? '0px' : 'calc(40%)'}
                        minSize="0px"
                    >
                        <SplitPane
                            split="horizontal"
                            defaultSize={`calc(100% - ${BOTTOM_PANEL_HEADER_HEIGHT}px)`}
                            maxSize={window.innerHeight - EDITOR_HEADER_HEIGHT - BOTTOM_PANEL_HEADER_HEIGHT}
                            onChange={(size: number) => {
                                const secondSize = window.innerHeight - EDITOR_HEADER_HEIGHT - size;
                                if (secondSize > BOTTOM_PANEL_HEADER_HEIGHT && !showVC) {
                                    setShowVC(true);
                                } else if (secondSize <= BOTTOM_PANEL_HEADER_HEIGHT && showVC) {
                                    // hide the viewConfig view when no enough space assigned
                                    setShowVC(false);
                                }
                            }}
                        >
                            {/* Gosling Editor */}
                            <>
                                <div className="tabEditor">
                                    <div className="tab">
                                        <button
                                            className={`tablinks ${language == 'json' && 'active'}`}
                                            onClick={() => {
                                                changeLanguage('json');
                                                setLog({ message: '', state: 'success' });
                                            }}
                                        >
                                            JSON {` `}
                                            <span className="tooltip">
                                                {getIconSVG(ICONS.INFO_CIRCLE, 10, 10)}
                                                <span className="tooltiptext">
                                                    In this JSON editor, the whole JSON object will be used to create
                                                    Gosling visualizations.
                                                </span>
                                            </span>
                                        </button>
                                        <button
                                            className={`tablinks ${language == 'typescript' && 'active'}`}
                                            onClick={() => {
                                                changeLanguage('typescript');
                                                setLog({ message: '', state: 'success' });
                                            }}
                                        >
                                            JavaScript{` `}
                                            <span className="tooltip">
                                                {getIconSVG(ICONS.INFO_CIRCLE, 10, 10)}
                                                <span className="tooltiptext">
                                                    In this JavaScript Editor, the variable{` `}
                                                    <code style={{ backgroundColor: '#e18343' }}>spec</code> will be
                                                    used to create Gosling visualizations.
                                                </span>
                                            </span>
                                        </button>
                                    </div>

                                    <div className={`tabContent ${language == 'json' ? 'show' : 'hide'}`}>
                                        <EditorPanel
                                            code={code}
                                            readOnly={readOnly}
                                            openFindBox={isFindCode}
                                            fontZoomIn={isFontZoomIn}
                                            fontZoomOut={isFontZoomOut}
                                            onChange={debounceCodeEdit.current}
                                            isDarkTheme={theme === 'dark'}
                                            language="json"
                                        />
                                    </div>
                                    <div className={`tabContent ${language == 'typescript' ? 'show' : 'hide'}`}>
                                        <EditorPanel
                                            code={jsCode}
                                            readOnly={readOnly}
                                            openFindBox={isFindCode}
                                            fontZoomIn={isFontZoomIn}
                                            fontZoomOut={isFontZoomOut}
                                            onChange={debounceCodeEdit.current}
                                            isDarkTheme={theme === 'dark'}
                                            language="typescript"
                                        />
                                    </div>
                                </div>
                                <div className={`compile-message compile-message-${log.state}`}>{log.message}</div>
                            </>
                            {/* HiGlass View Config */}
                            <SplitPane split="vertical" defaultSize="100%">
                                <>
                                    <div className={`editor-header ${theme === 'dark' ? 'dark' : ''}`}>
                                        Compiled HiGlass ViewConfig (Read Only)
                                    </div>
                                    <div style={{ height: '100%', visibility: showVC ? 'visible' : 'hidden' }}>
                                        <EditorPanel
                                            code={stringify(hg)}
                                            readOnly={true}
                                            isDarkTheme={theme === 'dark'}
                                            language="json"
                                        />
                                    </div>
                                </>
                                {/**
                                 * TODO: This is only for showing a scroll view for the higlass view config editor
                                 * Remove the below line and the nearest SplitPane after figuring out a better way
                                 * of showing the scroll view.
                                 */}
                                <></>
                            </SplitPane>
                        </SplitPane>
                        <ErrorBoundary>
                            <SplitPane
                                split="horizontal"
                                defaultSize={`calc(100% - ${BOTTOM_PANEL_HEADER_HEIGHT}px)`}
                                size={isShowDataPreview ? '40%' : `calc(100% - ${BOTTOM_PANEL_HEADER_HEIGHT}px)`}
                                maxSize={window.innerHeight - EDITOR_HEADER_HEIGHT - BOTTOM_PANEL_HEADER_HEIGHT}
                            >
                                <div
                                    id="preview-container"
                                    className={`preview-container ${theme === 'dark' ? 'dark' : ''}`}
                                >
                                    {isResponsive && !IS_SMALL_SCREEN ? ResponsiveWidget : null}
                                    <div
                                        style={{
                                            width: isResponsive && screenSize?.width ? screenSize.width : '100%',
                                            height:
                                                isResponsive && screenSize?.height
                                                    ? screenSize.height
                                                    : 'calc(100% - 50px)',
                                            background: isResponsive ? 'white' : 'none'
                                        }}
                                    >
                                        <gosling.GoslingComponent
                                            ref={gosRef}
                                            spec={goslingSpec}
                                            theme={theme}
                                            padding={60}
                                            margin={0}
                                            border={'none'}
                                            id={'goslig-component-root'}
                                            className={'goslig-component'}
                                            experimental={{ reactive: true }}
                                            compiled={(_, h) => {
                                                setHg(h);
                                            }}
                                        />
                                    </div>
                                    {/* {expertMode && false ? (
                                        <div
                                            style={{
                                                position: 'absolute',
                                                right: '2px',
                                                bottom: '2px',
                                                padding: '20px',
                                                background: '#FAFAFAAA',
                                                border: '1px solid black'
                                            }}
                                        >
                                            <div style={{ fontWeight: 'bold' }}>
                                                {`${mouseEventInfo?.data.length} Marks Selected By Mouse ${
                                                    mouseEventInfo?.type === 'click' ? 'Click' : 'Over'
                                                }`}
                                            </div>
                                            <div style={{}}>{`The event occurs at ${mouseEventInfo?.position}`}</div>
                                            <table>
                                                {mouseEventInfo?.data && mouseEventInfo?.data.length !== 0
                                                    ? Object.entries(mouseEventInfo?.data[0]).map(([k, v]) => (
                                                          <tr key={k}>
                                                              <td>{k}</td>
                                                              <td>{v}</td>
                                                          </tr>
                                                      ))
                                                    : null}
                                            </table>
                                        </div>
                                    ) : null} */}
                                </div>
                                <SplitPane split="vertical" defaultSize="100%">
                                    <>
                                        <div
                                            className={`editor-header ${theme === 'dark' ? 'dark' : ''}`}
                                            style={{ cursor: 'pointer' }}
                                            onClick={() => setIsShowDataPreview(!isShowDataPreview)}
                                        >
                                            Data Preview (~100 Rows, Data Before Transformation)
                                        </div>
                                        <div className="editor-data-preview-panel">
                                            <div
                                                title="Refresh preview data"
                                                className="data-preview-refresh-button"
                                                onClick={() => setRefreshData(!refreshData)}
                                            >
                                                {getIconSVG(ICONS.REFRESH, 23, 23)}
                                                <br />
                                                {'REFRESH DATA'}
                                            </div>
                                            {previewData.current.length > selectedPreviewData &&
                                            previewData.current[selectedPreviewData] &&
                                            previewData.current[selectedPreviewData].data.length > 0 ? (
                                                <>
                                                    <div className="editor-data-preview-tab">
                                                        {previewData.current.map((d: PreviewData, i: number) => (
                                                            <button
                                                                className={
                                                                    i === selectedPreviewData
                                                                        ? 'selected-tab'
                                                                        : 'unselected-tab'
                                                                }
                                                                key={JSON.stringify(d)}
                                                                onClick={() => setSelectedPreviewData(i)}
                                                            >
                                                                {`${(
                                                                    JSON.parse(d.dataConfig).data.type as string
                                                                ).toLocaleLowerCase()} `}
                                                                <small>{i}</small>
                                                            </button>
                                                        ))}
                                                    </div>
                                                    <div className="editor-data-preview-tab-info">
                                                        {getDataPreviewInfo(
                                                            previewData.current[selectedPreviewData].dataConfig
                                                        )}
                                                    </div>
                                                    <div className="editor-data-preview-table">
                                                        <table>
                                                            <tbody>
                                                                <tr>
                                                                    {Object.keys(
                                                                        previewData.current[selectedPreviewData].data[0]
                                                                    ).map((field: string, i: number) => (
                                                                        <th key={i}>{field}</th>
                                                                    ))}
                                                                </tr>
                                                                {previewData.current[selectedPreviewData].data.map(
                                                                    (row: Datum, i: number) => (
                                                                        <tr key={i}>
                                                                            {Object.keys(row).map(
                                                                                (field: string, j: number) => (
                                                                                    <td key={j}>
                                                                                        {row[field]?.toString()}
                                                                                    </td>
                                                                                )
                                                                            )}
                                                                        </tr>
                                                                    )
                                                                )}
                                                            </tbody>
                                                        </table>
                                                    </div>
                                                </>
                                            ) : null}
                                        </div>
                                    </>
                                    {/**
                                     * TODO: This is only for showing a scroll view for the higlass view config editor
                                     * Remove the below line and the nearest SplitPane after figuring out a better way
                                     * of showing the scroll view.
                                     */}
                                    <></>
                                </SplitPane>
                            </SplitPane>
                        </ErrorBoundary>
                    </SplitPane>
                </SplitPane>
                {/* Description Panel */}
                <div
                    className={`description ${hideDescription ? '' : 'description-shadow '}${
                        isDescResizing ? '' : 'description-transition'
                    } ${theme === 'dark' ? 'dark' : ''}`}
                    style={{ width: !description || hideDescription ? 0 : descPanelWidth }}
                >
                    <div
                        className={hideDescription ? 'description-resizer-disabled' : 'description-resizer'}
                        ref={descResizerRef}
                    />
                    <div className="description-wrapper">
                        <header>
                            <button className="hide-description-button" onClick={closeDescription}>
                                Close
                            </button>
                            <br />
                            <br />
                            <span
                                title="Open GitHub Gist"
                                className="description-github-button"
                                onClick={() => window.open(`https://gist.github.com/${urlGist}`, '_blank')}
                            >
                                {getIconSVG(ICONS.UP_RIGHT, 14, 14)} Open GitHub Gist to see raw files.
                            </span>
                        </header>
                        {description && <ReactMarkdown plugins={[gfm]} source={description} />}
                    </div>
                </div>
                {/* About Modal View */}
                <div
                    className={isShowAbout ? 'about-modal-container' : 'about-modal-container-hidden'}
                    onClick={() => setIsShowAbout(false)}
                ></div>
                <div className={isShowAbout ? 'about-modal' : 'about-modal-hidden'}>
                    <span
                        className="about-model-close-button"
                        onClick={() => {
                            setIsShowAbout(false);
                        }}
                    >
                        {getIconSVG(ICONS.CLOSE, 30, 30)}
                    </span>
                    <div>
                        <span className="logo">{GoslingLogoSVG(80, 80)}</span>
                    </div>
                    <h3>Gosling.js Editor</h3>
                    {`Gosling.js v${gosling.version}`}
                    <br />
                    <br />
                    <a
                        href="https://github.com/gosling-lang/gosling.js/blob/master/CHANGELOG.md"
                        target="_blank"
                        rel="noopener noreferrer"
                    >
                        Change Log
                    </a>
                    <br />
                    <br />
                    <a
                        href="https://github.com/gosling-lang/gosling.js/blob/master/LICENSE.md"
                        target="_blank"
                        rel="noopener noreferrer"
                    >
                        MIT License
                    </a>
                    <br />
                    <br />
                    <h4>Team</h4>
                    <span>
                        Sehi L&apos;Yi (
                        <a href="https://twitter.com/sehi_lyi" target="_blank" rel="noopener noreferrer">
                            @sehi_lyi
                        </a>
                        )
                        <br />
                        Qianwen Wang (
                        <a href="https://twitter.com/WangQianwenToo" target="_blank" rel="noopener noreferrer">
                            @WangQianwenToo
                        </a>
                        )
                        <br />
                        Fritz Lekschas (
                        <a href="https://twitter.com/flekschas" target="_blank" rel="noopener noreferrer">
                            @flekschas
                        </a>
                        )
                        <br />
                        Nils Gehlenborg (
                        <a href="https://twitter.com/gehlenborg" target="_blank" rel="noopener noreferrer">
                            @gehlenborg
                        </a>
                        )
                    </span>
                    <br />
                    <br />
                    <a href="http://gehlenborglab.org/" target="_blank" rel="noopener noreferrer">
                        Gehlenborg Lab
                    </a>
                    , Harvard Medical School
                </div>
            </div>
            {/* ---------------------- Example Gallery -------------------- */}
            <div
                className={showExamples ? 'about-modal-container' : 'about-modal-container-hidden'}
                onClick={() => setShowExamples(false)}
            />
            <div
                className="example-gallery-container"
                style={{
                    visibility: showExamples ? 'visible' : 'collapse'
                }}
            >
                <div
                    className="example-gallery-sidebar"
                    style={{
                        opacity: showExamples ? 1 : 0
                    }}
                >
                    {ExampleGroups.filter(_ => _.name !== 'Doc' && _.name !== 'Unassigned').map(group => {
                        return (
                            <>
                                <a className="siderbar-group" key={group.name} href={`#${group.name}`}>
                                    {group.name}
                                </a>
                                {Object.entries(examples)
                                    .filter(d => !d[1].hidden)
                                    .filter(d => d[1].group === group.name)
                                    .map(d => (
                                        <a key={d[1].name} href={`#${d[1].group}_${d[1].name}`}>
                                            {d[1].name}
                                        </a>
                                    ))}
                            </>
                        );
                    })}
                </div>
                <div
                    className="example-gallery"
                    style={{
                        opacity: showExamples ? 1 : 0
                    }}
                >
                    <h1>Gosling.js Examples</h1>
                    {ExampleGroups.filter(_ => _.name !== 'Doc' && _.name !== 'Unassigned').map(group => {
                        return (
                            <>
                                <h2 id={`${group.name}`}>{group.name}</h2>
                                <h5>{group.description}</h5>
                                <div className="example-group" key={group.name}>
                                    {Object.entries(examples)
                                        .filter(d => !d[1].hidden)
                                        .filter(d => d[1].group === group.name)
                                        .map(d => {
                                            return (
                                                <div
                                                    id={`${d[1].group}_${d[1].name}`}
                                                    title={d[1].name}
                                                    key={d[0]}
                                                    className="example-card"
                                                    onClick={() => {
                                                        setShowExamples(false);
                                                        closeDescription();
                                                        setIsImportDemo(true);
                                                        setDemo({ id: d[0], ...examples[d[0]] } as any);
                                                    }}
                                                >
                                                    <div
                                                        className="example-card-bg"
                                                        style={{
                                                            backgroundImage: d[1].image ? `url(${d[1].image})` : 'none'
                                                        }}
                                                    />
                                                    <div className="example-card-name">{d[1].name}</div>
                                                </div>
                                            );
                                        })}
                                </div>
                            </>
                        );
                    })}
                    {/* Just an margin on the bottom */}
                    <div style={{ height: '40px' }}></div>
                </div>
            </div>
        </>
    );
}