diff --git a/.changeset/long-cats-move.md b/.changeset/long-cats-move.md new file mode 100644 index 00000000..881ca4b6 --- /dev/null +++ b/.changeset/long-cats-move.md @@ -0,0 +1,5 @@ +--- +'@dotlottie/react-player': patch +--- + +fix: 🐛 updating src doesn't clear previously loaded states \ No newline at end of file diff --git a/apps/dotlottie-playground/public/lf_interactivity_page.lottie b/apps/dotlottie-playground/public/interactivity_example.lottie similarity index 93% rename from apps/dotlottie-playground/public/lf_interactivity_page.lottie rename to apps/dotlottie-playground/public/interactivity_example.lottie index 85bf3bdd..b5479d3d 100644 Binary files a/apps/dotlottie-playground/public/lf_interactivity_page.lottie and b/apps/dotlottie-playground/public/interactivity_example.lottie differ diff --git a/apps/dotlottie-playground/src/app.tsx b/apps/dotlottie-playground/src/app.tsx index 54e09fd8..d2ca3b2a 100644 --- a/apps/dotlottie-playground/src/app.tsx +++ b/apps/dotlottie-playground/src/app.tsx @@ -13,7 +13,7 @@ import '@dotlottie/react-player/dist/index.css'; const SAMPLE_FILES = [ { name: 'toggle.lottie', path: `${import.meta.env.BASE_URL}toggle.lottie` }, - { name: 'lf_interactivity_page.lottie', path: `${import.meta.env.BASE_URL}lf_interactivity_page.lottie` }, + { name: 'interactivity_example.lottie', path: `${import.meta.env.BASE_URL}interactivity_example.lottie` }, { name: 'aniki_hamster.lottie', path: `${import.meta.env.BASE_URL}aniki_hamster.lottie` }, ]; diff --git a/apps/dotlottie-playground/src/components/editable-title.tsx b/apps/dotlottie-playground/src/components/editable-title.tsx new file mode 100644 index 00000000..70b99852 --- /dev/null +++ b/apps/dotlottie-playground/src/components/editable-title.tsx @@ -0,0 +1,58 @@ +/** + * Copyright 2023 Design Barn Inc. + */ + +import React, { useCallback, useState, useRef, type KeyboardEventHandler } from 'react'; +import { BiSolidEdit } from 'react-icons/bi'; + +import { cn } from '../utils'; + +interface EditableTitleProps { + onChange: (value: string) => void; + title: string; +} + +export const EditableTitle: React.FC = ({ onChange, title }) => { + const [edit, setEdit] = useState(false); + const inputRef = useRef(null); + + const handleClick = useCallback((): void => { + if (!edit) { + setEdit(true); + } + }, [edit]); + + const handleSave = useCallback(() => { + onChange(inputRef.current?.value ? `${inputRef.current.value}.lottie` : title); + setEdit(false); + }, [onchange, setEdit]); + + const checkForEnterKey = useCallback>( + (event) => { + if (event.key === 'Enter') { + handleSave(); + } + }, + [handleSave], + ); + + return ( +
+ {edit && ( + + )} + {!edit && {title || 'unnamed.lottie'}} + +
+ ); +}; diff --git a/apps/dotlottie-playground/src/components/file-tree/add-new.tsx b/apps/dotlottie-playground/src/components/file-tree/add-new.tsx index a6caa104..dc124099 100644 --- a/apps/dotlottie-playground/src/components/file-tree/add-new.tsx +++ b/apps/dotlottie-playground/src/components/file-tree/add-new.tsx @@ -12,7 +12,7 @@ interface AddNewProps { extension: SupportedFileTypes; onAdd: (value: string) => void; } -export const AddNew: React.FC = ({ extension, onAdd }) => { +export const AddNew: React.FC = ({ onAdd }) => { const ref = useRef>(null); const handleBlur = useCallback>( @@ -38,8 +38,14 @@ export const AddNew: React.FC = ({ extension, onAdd }) => { - - {extension} + ); }; diff --git a/apps/dotlottie-playground/src/components/file-tree/editable-item.tsx b/apps/dotlottie-playground/src/components/file-tree/editable-item.tsx new file mode 100644 index 00000000..2d1c3714 --- /dev/null +++ b/apps/dotlottie-playground/src/components/file-tree/editable-item.tsx @@ -0,0 +1,99 @@ +/** + * Copyright 2023 Design Barn Inc. + */ + +import React, { + type HTMLAttributes, + useCallback, + type MouseEventHandler, + useState, + useRef, + type KeyboardEventHandler, +} from 'react'; +import { BiSolidEdit } from 'react-icons/bi'; +import { RxCross2 } from 'react-icons/rx'; + +import { processFilename } from '../../utils'; + +import { FileIcon } from './file-icon'; + +import { type SupportedFile } from '.'; + +interface EditableItemProps extends HTMLAttributes { + editable?: boolean; + file: SupportedFile; + onClick?: () => void; + onRemove?: (fileName: string) => void; + onRename?: (previousId: string, newId: string) => void; +} + +export const EditableItem: React.FC = ({ editable, file, onRemove, onRename, ...props }) => { + const [editMode, setEditMode] = useState(false); + const inputRef = useRef(null); + + const handleRemove = useCallback>( + (event) => { + event.stopPropagation(); + onRemove?.(file.name); + }, + [onRemove, file.name], + ); + + const enterEditMode = useCallback>( + (event) => { + event.stopPropagation(); + setEditMode(true); + }, + [setEditMode, editMode], + ); + + const triggerRename = useCallback(() => { + if (inputRef.current && inputRef.current.value !== file.name) { + onRename?.(file.name, processFilename(inputRef.current.value)); + } + setEditMode(false); + }, [onRename, setEditMode, inputRef]); + + const checkForEnterKey = useCallback>( + (event) => { + if (event.key === 'Enter') { + triggerRename(); + } + }, + [triggerRename], + ); + + return ( + + ); +}; diff --git a/apps/dotlottie-playground/src/components/file-tree/file-icon.tsx b/apps/dotlottie-playground/src/components/file-tree/file-icon.tsx index 3174b8e0..b40484f8 100644 --- a/apps/dotlottie-playground/src/components/file-tree/file-icon.tsx +++ b/apps/dotlottie-playground/src/components/file-tree/file-icon.tsx @@ -7,7 +7,7 @@ import { BsFiletypeJson, BsFiletypeCss } from 'react-icons/bs'; import type { SupportedFileTypes } from '.'; -export const FileIcon = ({ type }: { type: SupportedFileTypes }): React.ReactNode => { +export const FileIcon = ({ type }: { type: SupportedFileTypes }): JSX.Element => { if (type === 'lss') { return ; } else { diff --git a/apps/dotlottie-playground/src/components/file-tree/index.tsx b/apps/dotlottie-playground/src/components/file-tree/index.tsx index cbbb8265..bfd56b93 100644 --- a/apps/dotlottie-playground/src/components/file-tree/index.tsx +++ b/apps/dotlottie-playground/src/components/file-tree/index.tsx @@ -3,14 +3,14 @@ */ import React, { useCallback, useMemo, useState } from 'react'; -import { RxCross2 } from 'react-icons/rx'; import { useKey } from 'react-use'; +import { useDotLottie } from '../../hooks/use-dotlottie'; import { useAppSelector } from '../../store/hooks'; import { Dropzone } from '../dropzone'; import { AddNew } from './add-new'; -import { FileIcon } from './file-icon'; +import { EditableItem } from './editable-item'; import { Title } from './title'; const FILE_TYPES = ['json', 'lss'] as const; @@ -41,6 +41,8 @@ export const FileTree: React.FC = ({ onUpload, title, }) => { + const { renameDotLottieAnimation } = useDotLottie(); + const handleClick = useCallback( (fileName: string) => { return () => { @@ -54,10 +56,7 @@ export const FileTree: React.FC = ({ const handleRemove = useCallback( (fileName: string) => { - return (event: React.MouseEvent) => { - event.stopPropagation(); - onRemove?.(title, fileName); - }; + onRemove?.(title, fileName); }, [onRemove, title], ); @@ -84,6 +83,15 @@ export const FileTree: React.FC = ({ [onAddNew, title, fileExtention], ); + const handleRename = useCallback( + (id: string, previousId: string) => { + if (title === 'Animations') { + renameDotLottieAnimation(id, previousId); + } + }, + [title, renameDotLottieAnimation], + ); + const handleUpload = useCallback( (file: File) => { onUpload?.(title, file); @@ -122,10 +130,10 @@ export const FileTree: React.FC = ({ )}
    {Array.isArray(files) && - files.map((file, index) => { + files.map((file) => { return (
  • = ({ : 'text-gray-400' }`} > - + />
  • ); })} diff --git a/apps/dotlottie-playground/src/components/input-fields/input-color-picker.tsx b/apps/dotlottie-playground/src/components/input-fields/input-color-picker.tsx new file mode 100644 index 00000000..0dbdb976 --- /dev/null +++ b/apps/dotlottie-playground/src/components/input-fields/input-color-picker.tsx @@ -0,0 +1,45 @@ +/** + * Copyright 2023 Design Barn Inc. + */ + +import React, { type ChangeEventHandler, useCallback, useRef } from 'react'; + +interface InputColorPickerProps { + label: string; + onChange?: (value: string) => void; + value?: string; +} + +export const InputColorPicker: React.FC = ({ label, onChange, value }) => { + const inputRef = useRef(null); + const handleChange = useCallback>( + (event) => { + onChange?.(event.target.value); + }, + [onChange], + ); + + const openPicker = useCallback(() => inputRef.current?.click(), [inputRef]); + + return ( +
    + {label} + + +
    + ); +}; diff --git a/apps/dotlottie-playground/src/components/player.tsx b/apps/dotlottie-playground/src/components/player.tsx index 8121e8e8..f268680f 100644 --- a/apps/dotlottie-playground/src/components/player.tsx +++ b/apps/dotlottie-playground/src/components/player.tsx @@ -17,15 +17,22 @@ export const Player: React.FC = () => { const currentPlayerUrl = useAppSelector((state) => state.playground.playerUrl); const [playerStates, setPlayerStates] = useState([]); const [activeStateId, setActiveStateId] = useState(''); + const [currentFrame, setCurrentFrame] = useState(0); const handlePlayerEvents = useCallback( - (event: PlayerEvents) => { + (event: PlayerEvents, params: unknown) => { if (event === PlayerEvents.Ready) { const _states = lottiePlayer.current?.getManifest()?.states; setPlayerStates(_states || []); } + if (event === PlayerEvents.Frame) { + const { frame } = params as { frame: number }; + + setCurrentFrame(Math.floor(frame)); + } + const currentState = lottiePlayer.current?.getState(); if (currentState) { @@ -63,7 +70,14 @@ export const Player: React.FC = () => { lottieRef={lottiePlayer} src={currentPlayerUrl} > - +
    + +
    + + # {currentFrame} + +
    +
    diff --git a/apps/dotlottie-playground/src/components/playground.tsx b/apps/dotlottie-playground/src/components/playground.tsx index 83c818cf..cf054b8a 100644 --- a/apps/dotlottie-playground/src/components/playground.tsx +++ b/apps/dotlottie-playground/src/components/playground.tsx @@ -25,6 +25,7 @@ import { formatJSON, getMockDotLottieState, processFilename } from '../utils'; import { Button } from './button'; import { Dropzone } from './dropzone'; +import { EditableTitle } from './editable-title'; import { FileTree } from './file-tree'; import { PlaybackOptionsEditor } from './playback-options-editor'; import { Player } from './player'; @@ -302,6 +303,13 @@ export const Playground: React.FC = ({ file: dotLottieFile, fil [dispatch], ); + const handleChangeTitle = useCallback( + (value: string): void => { + dispatch(setWorkingFileName(value)); + }, + [setWorkingFileName, dispatch], + ); + return (
    @@ -314,7 +322,7 @@ export const Playground: React.FC = ({ file: dotLottieFile, fil )}
    - {workingFileName || 'unnamed.lottie'} +
    diff --git a/apps/dotlottie-playground/src/hooks/use-dotlottie.tsx b/apps/dotlottie-playground/src/hooks/use-dotlottie.tsx index 02cfb184..c0a572c5 100644 --- a/apps/dotlottie-playground/src/hooks/use-dotlottie.tsx +++ b/apps/dotlottie-playground/src/hooks/use-dotlottie.tsx @@ -2,9 +2,15 @@ * Copyright 2023 Design Barn Inc. */ -import { DotLottie, type PlayMode, type DotLottieStateMachine } from '@dotlottie/dotlottie-js'; +import { + DotLottie, + type PlayMode, + type DotLottieStateMachine, + DotLottieStateMachineSchema, +} from '@dotlottie/dotlottie-js'; import { type Animation } from '@lottiefiles/lottie-types'; import React, { type ReactNode, createContext, useCallback, useContext, useState } from 'react'; +import { toast } from 'react-toastify'; import { setAnimations } from '../store/animation-slice'; import { type EditorAnimationOptions } from '../store/editor-slice'; @@ -25,6 +31,7 @@ interface DotLottieContextProps { removeDotLottieAnimation: (animationId: string) => void; removeDotLottieState: (stateId: string) => void; removeDotLottieTheme: (themeId: string) => void; + renameDotLottieAnimation: (animationId: string, newAnimationId: string) => void | Promise; setAnimationOptions: (animationId: string, options: EditorAnimationOptions) => void | Promise; setDotLottie: (dotLottie: DotLottie) => void | Promise; } @@ -41,6 +48,7 @@ const DotLottieContext = createContext({ removeDotLottieAnimation: () => undefined, removeDotLottieState: () => undefined, removeDotLottieTheme: () => undefined, + renameDotLottieAnimation: () => undefined, buildAndUpdateUrl: () => undefined, }); @@ -58,26 +66,32 @@ export const DotLottieProvider: React.FC<{ children: ReactNode }> = ({ children ); const fetchAndUpdateDotLottie = useCallback(async () => { - const _anims = dotLottie.manifest.animations.map((item) => { - return { - name: `${item.id}`, - type: 'json', - }; - }); + const _anims = dotLottie.manifest.animations + .map((item) => { + return { + name: `${item.id}`, + type: 'json', + }; + }) + .sort((item1, item2) => (item1.name > item2.name ? 1 : -1)); - const _states = dotLottie.stateMachines.map((item) => { - return { - name: `${item.id}`, - type: 'json', - }; - }); + const _states = dotLottie.stateMachines + .map((item) => { + return { + name: `${item.id}`, + type: 'json', + }; + }) + .sort((item1, item2) => (item1.name > item2.name ? 1 : -1)); - const _themes = dotLottie.themes.map((item) => { - return { - name: `${item.id}`, - type: 'lss', - }; - }); + const _themes = dotLottie.themes + .map((item) => { + return { + name: `${item.id}`, + type: 'lss', + }; + }) + .sort((item1, item2) => (item1.name > item2.name ? 1 : -1)); dispatch(setAnimations(_anims)); dispatch(setStates(_states)); @@ -101,9 +115,21 @@ export const DotLottieProvider: React.FC<{ children: ReactNode }> = ({ children } }, [dotLottie]); + const requiresValidStateMachineSchema = useCallback((stateMachine: DotLottieStateMachine) => { + try { + DotLottieStateMachineSchema.parse(stateMachine); + } catch (error) { + toast('Invalid state schema. Please verify the json.', { type: 'error' }); + throw error; + } + }, []); + // Add State const addDotLottieStateMachine = useCallback( (stateMachine: DotLottieStateMachine, previousStateId?: string): void => { + // dispaly and throw Error + requiresValidStateMachineSchema(stateMachine); + if (previousStateId) { dotLottie.removeStateMachine(previousStateId); } else { @@ -130,6 +156,35 @@ export const DotLottieProvider: React.FC<{ children: ReactNode }> = ({ children [dotLottie, fetchAndUpdateDotLottie], ); + // Add Animation + const renameDotLottieAnimation = useCallback( + async (animationId: string, newAnimationId: string) => { + const animation = await dotLottie.getAnimation(animationId); + + if (animation) { + try { + dotLottie.addAnimation({ + id: newAnimationId, + data: animation.data, + }); + + dotLottie.removeAnimation(animationId); + } catch (error) { + toast(error.message, { + type: 'error', + }); + + // eslint-disable-next-line no-console + console.error(error); + } + } + + fetchAndUpdateDotLottie(); + buildAndUpdateUrl(); + }, + [dotLottie, fetchAndUpdateDotLottie, buildAndUpdateUrl], + ); + // Add Animation const addDotLottieAnimation = useCallback( (animation: Animation, animationId: string) => { @@ -236,6 +291,7 @@ export const DotLottieProvider: React.FC<{ children: ReactNode }> = ({ children removeDotLottieAnimation, removeDotLottieState, removeDotLottieTheme, + renameDotLottieAnimation, setDotLottie, setAnimationOptions, }} diff --git a/packages/react-player/src/hooks/use-dotlottie-player.ts b/packages/react-player/src/hooks/use-dotlottie-player.ts index 5bbffe7a..5a7f2e7e 100644 --- a/packages/react-player/src/hooks/use-dotlottie-player.ts +++ b/packages/react-player/src/hooks/use-dotlottie-player.ts @@ -78,7 +78,7 @@ export const useDotLottiePlayer = ( config?: DotLottieConfig & { lottieRef?: MutableRefObject; }, -): DotLottiePlayer => { +): { dotLottiePlayer: DotLottiePlayer; initDotLottiePlayer: () => void } => { const [dotLottiePlayer, setDotLottiePlayer] = useState(() => { return new DotLottiePlayer(src, container.current, config); }); @@ -89,7 +89,11 @@ export const useDotLottiePlayer = ( dl.load(); return dl; - }, [container]); + }, [container, src, config]); + + const initDotLottiePlayer = useCallback(() => { + setDotLottiePlayer(getDotLottiePlayer()); + }, [getDotLottiePlayer]); if (config?.lottieRef) { useImperativeHandle( @@ -227,12 +231,15 @@ export const useDotLottiePlayer = ( } useEffectOnce(() => { - setDotLottiePlayer(getDotLottiePlayer()); + initDotLottiePlayer(); return () => { dotLottiePlayer.destroy(); }; }); - return dotLottiePlayer; + return { + dotLottiePlayer, + initDotLottiePlayer, + }; }; diff --git a/packages/react-player/src/react-player.tsx b/packages/react-player/src/react-player.tsx index e5520b30..d53c83d4 100644 --- a/packages/react-player/src/react-player.tsx +++ b/packages/react-player/src/react-player.tsx @@ -60,7 +60,7 @@ export const DotLottiePlayer: React.FC = ({ }) => { const container = useRef(null); - const dotLottiePlayer = useDotLottiePlayer(src, container, { + const { dotLottiePlayer, initDotLottiePlayer } = useDotLottiePlayer(src, container, { lottieRef, renderer, activeAnimationId, @@ -182,7 +182,7 @@ export const DotLottiePlayer: React.FC = ({ useUpdateEffect(() => { if (typeof src !== 'undefined') { - dotLottiePlayer.updateSrc(src); + initDotLottiePlayer(); } }, [src]);