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 |
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 |
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 |
private debouncedInitializeIconCache = debounce(this.initializeIconCache, 250);
Example #4
Source File: Kplayer.ts From agefans-enhance with MIT License | 5 votes |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
/**
* 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'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>
</>
);
}