متن خبر

نحوه ایجاد یک بوم به سبک Figma / Miro با React و TypeScript

نحوه ایجاد یک بوم به سبک Figma / Miro با React و TypeScript

شناسهٔ خبر: 458498 -




Miro و Figma ابزارهایی از نوع بوم مشارکتی آنلاین هستند که در طول همه گیری بسیار محبوب شدند.

به‌جای چسباندن یادداشت‌های آن بر روی یک دیوار فیزیکی، اکنون می‌توانید پست مجازی آن (و مجموعه‌ای گیج‌کننده از چیزهای دیگر) را به یک بوم مجازی اضافه کنید. این به تیم‌ها اجازه می‌دهد تا به‌طور مجازی به روش‌هایی همکاری کنند که از دنیای فیزیکی احساس آشنایی داشته باشند.

Figma و Miro محصولات بزرگ و بالغی هستند و HTML و CSS را به طور کامل دور می زنند. آن‌ها از فناوری‌هایی مانند WebAssembly، WebGL، C++ و موارد مشابه استفاده می‌کنند، به دلیل الزامات عملکرد بسیار سخت.

اما می‌توانیم با استفاده از React، TypeScript و چند بسته، آپشن های مشابهی از نوع بوم مجازی، بدون پیچیدگی ایجاد کنیم. ما قصد داریم از یک نوع "کارت" پشتیبانی کنیم، که فقط حاوی متن ساده است، تا این راهنما مختصر باشد، اما گسترش راه حل برای پشتیبانی از موارد استفاده دقیق تر آسان است.

عملکردی که ما قصد داریم پیاده سازی کنیم این است:

کشیدن کارت ها در اطراف بوم

اضافه کردن کارت های جدید به بوم

حرکت و بزرگنمایی بوم

این مقاله یک راهنمای گام به گام است که نحوه ایجاد این ویژگی ها را توضیح می دهد و کد همراه در GitHub با دموهای زنده وجود دارد.

راه حل ساخته شده خراش ما به سرعت Figma یا Miro نخواهد بود، اما اگر نیازهای شما ساده تر است، احتمالاً کافی است.

تحلیل اجمالی پروژه

ما از DndKit برای کشیدن و رها کردن و D3 Zoom برای حرکت و بزرگنمایی استفاده خواهیم کرد. کار کردن با هر دوی این ابزارها برایم لذت بخش بود.

کد در GitHub است و یک نسخه آزمایشی زنده وجود دارد تا بتوانید آن را امتحان کنید.

تنها 4 جزء ( App ، Canvas ، Draggable و Addable ) و تنها حدود 250 خط کد وجود دارد.

این راهنما برای توسعه دهندگان React / TypeScript سطح متوسط ​​طراحی شده است. اگر قبلاً کامپوننت‌ها ، هوک‌ها ، نحوه تفکر در React ، وضعیت ، ref‌های قابل تغییر و مواردی از این قبیل را درک کرده باشید، بسیار آسان‌تر خواهد بود.

قسمت 3
گرفتن صفحه نمایش راه حل نهایی بوم را نشان می دهد

خوب، اکنون برای اینکه ببینیم چگونه می توانید این را بسازید، بیایید وارد آن شویم.

مرحله 1 - نحوه کشیدن کارت ها در اطراف بوم

برای شروع، از DndKit برای کشیدن کارت ها در اطراف بوم استفاده می کنیم. ابزارهایی را که برای ساختن پروژه نیاز داریم نصب می کنیم، یک نوع Card ساده ایجاد می کنیم و اجزای ساده App ، Canvas و Draggable را ایجاد می کنیم.

مؤلفه App وضعیت card را ذخیره می کند و Canvas را ارائه می دهد.

مولفه Canvas با DndKit یکپارچه می شود، وضعیت card را به روز می کند و cards به عنوان اجزای Draggable تبدیل می کند.

مولفه Draggable با DndKit ادغام می شود و از استایل CSS برای قرارگیری صحیح خود بر روی بوم استفاده می کند.

مرحله 1 کد در GitHub.

در اینجا تصویری از آنچه که ما در این بخش خواهیم ساخت ارائه شده است، و همچنین یک نسخه نمایشی زنده وجود دارد که می توانید خودتان آن را امتحان کنید:

قسمت 1
گرفتن صفحه نمایش بخش 1 محلول بوم را نشان می دهد

راه اندازی پروژه

ما یک پروژه جدید با Vite ایجاد می کنیم و DndKit را با استفاده از دستورات زیر نصب می کنیم:

 npm create vite@latest figma-miro-canvas -- --template react-ts npm install npm install @dnd-kit/core

App.tsx

مؤلفه App وضعیت card را ذخیره می کند و Canvas را ارائه می دهد.

ما یک نوع Card اضافه می کنیم، در این دمو کارت ها به سادگی حاوی متنی خواهند بود. UniqueIdentifier از DndKit است و ترسناک به نظر می رسد، اما فقط یک String | number . Coordinates نیز از DndKit می آید و حاوی موقعیت x و y است.

 export type Card = { id: UniqueIdentifier; coordinates: Coordinates; text: string; };

سپس باید چند کارت ایجاد کنیم و آنها را به بوم منتقل کنیم.

 export const App = () => { const [cards, setCards] = useState<Card[]>([ { id: "Hello", coordinates: { x: 0, y: 0 }, text: "Hello" }, ]); return (<Canvas cards={cards} />); }

App.tsx در GitHub

Canvas.tsx

مولفه Canvas با DndKit یکپارچه می شود، وضعیت card را به روز می کند و cards به عنوان اجزای Draggable تبدیل می کند.

cards و setCards به‌عنوان لوازم جانبی می‌گیرد، زیرا حالت در بالای درخت در App ذخیره می‌شود. این در حال حاضر به شدت ضروری نیست، اما در مراحل بعدی مفید است.

 type Props = { cards: Card[]; setCards: (cards: Card[]) => void; }

ما تابعی را برای فراخوانی setCards با به روز رسانی پس از اتمام عملیات کشیدن اضافه می کنیم. این به سادگی فاصله کشیدن / دلتا را به مقادیر x و y برای کارتی که در حال کشیدن است اضافه می کند.

DragEndEvent از DndKit می آید و شامل آیتم active در حال کشیدن می شود، پس ما می توانیم از آن برای تحلیل اینکه کدام card را به روز کنیم استفاده کنیم.

 const updateDraggedCardPosition = ({ delta, active }: DragEndEvent) => { if (!delta.x && !delta.y) return; setCards( cards.map((card) => { if (card.id === active.id) { return { ...card, coordinates: { x: card.coordinates.x + delta.x, y: card.coordinates.y + delta.y, }, }; } return card; }) ); };

یک div برای نمایش بوم، یک DndContext (از DndKit) اضافه می کنیم و برای هر کارت یک Draggable ارائه می کنیم. ما تابع جدید خود را با رویداد onDragEnd از DndContext متصل می کنیم تا وضعیت card پس از یک عملیات کشیدن موفقیت آمیز به روز شود.

 <div className="canvas"> <DndContext onDragEnd={updateDraggedCardPosition}> {cards.map((card) => ( <Draggable card={card} key={card.id} /> ))} </DndContext> </div>

Canvas.tsx در GitHub

Draggable.tsx

مولفه Draggable با DndKit ادغام می شود و از استایل CSS ( position ، top و left ) استفاده می کند تا خود را به درستی روی بوم قرار دهد.

useDraggable از DndKit می‌آید، و ما کورکورانه attributes ، listeners و setNodeRef را که باز می‌گرداند به div ما ارسال می‌کنیم، که به آن اجازه می‌دهد به رویدادهای onClick و مواردی از این قبیل پاسخ دهد.

ما همچنین از transform DndKit برای اعمال CSS استفاده می کنیم تا کارت را در موقعیتی تغییر یافته در زمانی که کشیدن در حال انجام است، ارائه دهیم. کمی گیج کننده، نام ویژگی CSS نیز transform نامیده می شود.

 export const Draggable = ({ card }: { card: Card }) => { // hook up to DndKit const { attributes, listeners, setNodeRef, transform } = useDraggable({ id: card.id, }); return ( <div className="card" style={{ // position card on canvas position: "absolute", top: `${card.coordinates.y}px`, left: `${card.coordinates.x}px`, // temporary change to this position when dragging ...(transform ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0px)`, } : {}), }} ref={setNodeRef} {...listeners} {...attributes} > {card.text} </div> ); };

Draggable.tsx در GitHub

App.css

در نهایت می‌توانیم یک استایل CSS اضافه کنیم تا همه چیز به‌طور مبهم قابل قبول به نظر برسد. این کاملاً ضروری نیست، اما حتی با وجود مهارت های طراحی محدود من، بسیار بهتر به نظر می رسد.

App.css در GitHub

مرحله 2 - نحوه اضافه کردن کارت های جدید به بوم

در این مرحله، یک کامپوننت Addable جدید ایجاد می‌کنیم، تا کارت‌هایی را نشان دهد که در حال حاضر روی بوم نیستند، اما می‌توان آن‌ها را روی آن کشید.

ما App به‌روزرسانی می‌کنیم تا یک div "سینی" اضافه کنیم تا حاوی این کارت‌های Addable جدید باشد. ما همچنین یک DndContext دیگر اضافه می‌کنیم (این DndContext جدید با کشیدن کشیدن از سینی به بوم، و DndContext موجود در Canvas ، کشیدن کارت‌ها را در اطراف بوم کنترل می‌کند)، و به رویدادهای آن متصل می‌شود. این به ما اجازه می‌دهد تا زمانی که کارت‌های Addable روی بوم کشیده می‌شوند، وضعیت را به‌روزرسانی کنیم.

ما Canvas به‌روزرسانی می‌کنیم تا آن را به یک هدف DndKit تبدیل کنیم تا کارت‌های Addable روی آن رها شوند.

کد مرحله 2 در GitHub

در اینجا عملکردی است که در این بخش اضافه خواهیم کرد، و همچنین یک نسخه آزمایشی زنده وجود دارد که می توانید خودتان آن را امتحان کنید:

قسمت 2

App.tsx

مؤلفه App اکنون به یک div «سینی» نیاز دارد تا حاوی کارت‌های Addable باشد.

همچنین به DndContext دیگری نیاز دارد (این DndContext جدید کشیدن کشیدن از سینی به بوم را کنترل می کند، و DndContext موجود در Canvas کشیدن کارت ها را در اطراف بوم کنترل می کند). باید به رویدادهای خود متصل شود تا زمانی که کارت‌های Addable روی بوم کشیده می‌شوند، وضعیت را به‌روزرسانی کنیم.

ما فهرست ی از کارت ها را اضافه می کنیم تا در سینی ظاهر شوند:

 const trayCards = [ // the coordinates aren't used for the tray cards, we could create a new type without them { id: "World", coordinates: { x: 0, y: 0 }, text: "World" }, ];

ما یک تابع اضافه می کنیم تا موقعیت روی بوم را در پایان عملیات کشیدن دراپ مشخص کنیم. این باید موقعیت اولیه کارت، فاصله کشیدن / دلتا و موقعیت بالای سمت چپ بوم را بداند. همه این جزئیات توسط DndKit DragEndEvent ارائه شده است.

 const calculateCanvasPosition = ( initialRect: ClientRect, over: Over, delta: Translate ): Coordinates => ({ x: initialRect.left + delta.x - (over?.rect?.left ?? 0), y: initialRect.top + delta.y - (over?.rect?.top ?? 0), });

حالت را برای ذخیره کارت سینی در حال کشیدن به همراه عملکردی برای به روز رسانی وضعیت cards پس از کشیدن / رها کردن از سینی اضافه می کنیم. DragEndEvent از DndKit می آید و شامل آیتم active در حال کشیدن می شود، پس می توانیم از آن برای ایجاد یک card جدید و اضافه کردن آن به آرایه cards استفاده کنیم. ما همچنین تحلیل هایی را انجام می دهیم تا مطمئن شویم که "بوم" هدف رها شده است و ما تمام داده های مورد نیاز را داریم.

 const [draggedTrayCardId, setDraggedTrayCardId] = useState<UniqueIdentifier | null>(null); const addDraggedTrayCardToCanvas = ({ over, active, delta }: DragEndEvent) => { setDraggedTrayCardId(null); if (over?.id !== "canvas") return; if (!active.rect.current.initial) return; setCards([ ...cards, { id: active.id, coordinates: calculateCanvasPosition( active.rect.current.initial, over, delta ), text: active.id.toString(), }, ]); };

ما DndContext جدید، اضافی، یک div برای نمایش سینی و یک DragOverlay اضافه می کنیم.

کامپوننت DragOverlay از DndKit می آید و ما کارت سینی را که داخل آن کشیده می شود رندر می کنیم. کار سخت نشان دادن کارت سینی را در حالی که در حال کشیدن است انجام می دهد. استفاده از آن با کشیدن / رها کردن بسیار راحت است، اما در هنگام کشیدن به اندازه کافی مفید نیست، به همین دلیل است که ما قبلاً هنگام کشیدن کارت ها در اطراف بوم از آن استفاده نکردیم.

 <DndContext onDragStart={({ active }) => setDraggedTrayCardId(active.id)} onDragEnd={addDraggedTrayCardToCanvas} > <div className="tray"> {trayCards.map((trayCard) => { // this line removes the card from the tray if it's on the canvas if (cards.find((card) => card.id === trayCard.id)) return null; return <Addable card={trayCard} key={trayCard.id} />; })} </div> <Canvas cards={cards} setCards={setCards} /> <DragOverlay> {/* this works because the id of the card is the same as the text in this example so we can just render the id inside a div. In more complex cases you would have a component to render the card, and use that here. */} <div className="trayOverlayCard">{draggedTrayCardId}</div> </DragOverlay> </DndContext>

App.tsx در GitHub

Addable.tsx

جزء Addable با DndKit ادغام می شود و برای نشان دادن کارت هایی استفاده می شود که در حال حاضر روی بوم نیستند، اما می توان آنها را روی آن کشید.

کامپوننت را با DndKit متصل می کنیم و متن کارت را در یک div رندر می کنیم. useDraggable از DndKit می‌آید، و ما کورکورانه attributes ، listeners و setNodeRef را که برمی‌گرداند به div ما ارسال می‌کنیم. این به آن اجازه می دهد تا به رویدادهای onClick و مواردی از این قبیل پاسخ دهد.

 export const Addable = ({ card } : { card: Card; }) => { const { attributes, listeners, setNodeRef } = useDraggable({ card.id, }); return ( <div className="trayCard" ref={setNodeRef} {...listeners} {...attributes}> {card.text} </div> ); };

Addable.tsx در GitHub

Canvas.tsx

Canvas اکنون باید با DndKit ادغام شود تا آن را به یک هدف رهاسازی تبدیل کند تا بتوان کارت های Addable را روی آن انداخت.

ما بوم را به عنوان یک هدف در حال سقوط با DndKit متصل می کنیم. useDroppable از DndKit می آید، و ما فقط ref را به div خود منتقل می کنیم. این به این دلیل است که DndKit بتواند آن را شناسایی کند و زمانی که روی آن کشیده می شود، id آن را به دست آورد.

 const { setNodeRef } = useDroppable({ id: "canvas" });
 <div className="canvas" ref={setNodeRef} ... > ... </div>

Canvas.tsx در GitHub

مرحله 3 - نحوه حرکت به اطراف و بزرگنمایی و بزرگنمایی از روی بوم

در این مرحله نهایی، d3-zoom را نصب می کنیم، آن را به بوم متصل می کنیم و سپس برخی از محاسبات و استایل ها را به روز می کنیم تا همه چیز در جای درست ظاهر شود.

ما App را به روز می کنیم تا transform (پان و بزرگنمایی بوم) را از d3-zoom ذخیره کنیم و سبک DragOverlay و calculateCanvasPosition را برای در نظر گرفتن transform به روز می کنیم.

ما Canvas به‌روزرسانی می‌کنیم تا با d3-zoom ادغام شود و از استایل CSS برای در نظر گرفتن transform استفاده کنیم.

ما Draggable با استفاده از استایل CSS به‌روزرسانی می‌کنیم تا transform در نظر بگیریم، چه در حالت ثابت و چه در حین کشیدن.

d3-zoom رویدادهای ماوس و اشاره گر را برای حرکت و بزرگنمایی به صورت خودکار کنترل می کند، پس نیازی به اضافه کردن کدی برای آن نداریم (اما اگر می خواهید دکمه "Zoom In" یا موارد مشابه را اضافه کنید، انجام این کار آسان است).

کد مرحله 3 در GitHub

در اینجا چیزی است که ما در این بخش می سازیم، و همچنین یک نسخه نمایشی زنده وجود دارد که می توانید خودتان آن را امتحان کنید:

قسمت 3-1

قبل از ادامه، مطمئن شوید که d3-zoom را نصب کرده اید:

 npm install d3-zoom npm install --save-dev @types/d3-zoom

App.tsx

اکنون App باید transform (پان و بزرگنمایی بوم) را از d3-zoom ذخیره کند و سبک DragOverlay و calculateCanvasPosition را برای در نظر گرفتن transform به روز کند.

ما transform فعلی را از d3-zoom ذخیره می کنیم. این هم پان ( transform.x و transform.y ) و هم بزرگنمایی ( transform.k ) را نشان می‌دهد.

 const [transform, setTransform] = useState(zoomIdentity);

ما CSS را به DragOverlay اضافه می کنیم، به طوری که کارت هایی که از سینی کشیده می شوند به همان اندازه ای باشند که روی بوم هستند.

 style={{ transformOrigin: "top left", transform: `scale(${transform.k})`, }}

تابع accountCanvasPosition را به روز می کنیم زیرا اکنون باید بزرگنمایی بوم ( transform.k ) و همچنین موقعیت بالا سمت چپ را در نظر بگیرد.

 const calculateCanvasPosition = ( initialRect: ClientRect, over: Over, delta: Translate, transform: ZoomTransform ): Coordinates => ({ x: (initialRect.left + delta.x - (over?.rect?.left ?? 0) - transform.x) / transform.k, y: (initialRect.top + delta.y - (over?.rect?.top ?? 0) - transform.y) / transform.k, });

App.tsx در GitHub

Canvas.tsx

Canvas اکنون باید با d3-zoom ادغام شود و از استایل CSS برای در نظر گرفتن transform از d3-zoom استفاده کند.

ما props را برای transform و setTransform اضافه می کنیم (اینها را از App.tsx منتقل می کنیم).

 type Props = { cards: Card[]; setCards: (cards: Card[]) => void; transform: ZoomTransform; setTransform(transform: ZoomTransform): void; }

ما d3-zoom را وصل می کنیم. هم DndKit و هم d3-zoom به یک مرجع برای عنصر نیاز دارند، پس ما canvasRef و updateAndForwardRef را ایجاد می کنیم که به هر دوی آنها اجازه می دهد به یک HTMLDivElement اشاره کنند.

d3-zoom یک کتابخانه جاوا اسکریپت است، نه یک مؤلفه React، به همین دلیل است که ما باید از کدهای باطنی زیر استفاده کنیم، مانند useMemo و useLayoutEffect (البته هر دوی اینها را تقریباً در هر پایگاه کد React با اندازه معقول مشاهده خواهید کرد).

 const canvasRef = useRef<HTMLDivElement | null>(null); const updateAndForwardRef = (div: HTMLDivElement) => { canvasRef.current = div; setNodeRef(div); }; // create the d3 zoom object, and useMemo to retain it for rerenders const zoomBehavior = useMemo(() => zoom<HTMLDivElement, unknown>(), []); // update the transform when d3 zoom notifies of a change. const updateTransform = useCallback( ({ transform }: { transform: ZoomTransform }) => { setTransform(transform); }, [setTransform] ); useLayoutEffect(() => { if (!canvasRef.current) return; // get transform change notifications from d3 zoom zoomBehavior.on("zoom", updateTransform); // attach d3 zoom to the canvas div element, which will handle // mousewheel, gesture and drag events automatically for pan / zoom select<HTMLDivElement, unknown>(canvasRef.current).call(zoomBehavior); }, [zoomBehavior, canvasRef, updateTransform] );

ما یک بسته بندی / پنجره در اطراف بوم اضافه می کنیم. Div بوم اکنون حرکت می کند و بزرگنمایی می کند ( پس به مقدار زیادی در صفحه حرکت می کند)، پس آن را در یک div دیگری با موقعیت و اندازه ثابت قرار می دهیم و هر سرریزی را پنهان می کنیم، به طوری که یک "پنجره" داشته باشیم که قسمت مربوطه را نشان می دهد. بوم

همچنین سبک‌های CSS را به بوم اضافه می‌کنیم تا پان و بزرگ‌نمایی را در نظر بگیریم، از تابع جدید updateAndForwardRef استفاده می‌کنیم و ref را از بوم به پنجره بوم منتقل می‌کنیم.

 <div ref={updateAndForwardRef} className="canvasWindow"> <div className="canvas" style={{ // apply the transform from d3 transformOrigin: "top left", transform: `translate3d(${transform.x}px, ${transform.y}px, ${transform.k}px)`, position: "relative", height: "300px", }} > ... </div> </div>

Canvas.tsx در GitHub

Draggable.tsx

اکنون Draggable به استایل CSS متفاوتی نیاز دارد تا transform d3-zoom را در نظر بگیرد، چه در حالت ثابت و چه در حین کشیدن.

ما یک پایه برای تبدیل d3-zoom اضافه می کنیم که آن را canvasTransform می نامیم، زیرا در حال حاضر از یک متغیر transform از DndKit استفاده می کنیم.

 type Props = { card: Card; canvasTransform: ZoomTransform; }

ما CSS را به‌روزرسانی می‌کنیم تا پانل و بزرگنمایی بوم را در نظر بگیرد. ما باید به دو مورد رسیدگی کنیم، هم زمانی که در حال کشیدن است و هم زمانی که نیست.

 style={{ position: "absolute", top: `${card.coordinates.y * canvasTransform.k}px`, left: `${card.coordinates.x * canvasTransform.k}px`, transformOrigin: "top left", ...(transform ? { // temporary change to this position when dragging transform: `translate3d(${transform.x}px, ${transform.y}px, 0px) scale(${canvasTransform.k})`, } : { // zoom to canvas zoom transform: `scale(${canvasTransform.k})`, }), }}

ما همچنین حباب کردن رویداد onPointerDown را به سمت بوم متوقف می کنیم، در غیر این صورت توسط d3-zoom مدیریت می شود و به عنوان یک درخواست برای شروع کشیدن تفسیر می شود، که منجر به کشیدن بوم و کارت به طور همزمان می شود (یک اثر جالب اما نامطلوب !)

 onPointerDown={(e) => { listeners?.onPointerDown?.(e); e.preventDefault(); }}

Draggable.tsx در GitHub

نتیجه

مقداری پیچیدگی در محاسبات مختلف موقعیت / تبدیل وجود دارد، اما خیلی هم دیوانه کننده نیست و فقط دو وابستگی برای نصب وجود دارد.

تنها چهار مؤلفه ( App ، Canvas ، Draggable و Addable ) و تنها حدود 250 خط کد برای همه آنها وجود دارد که به نظر می رسد مقدار بسیار کمی برای همه عملکردها باشد.

این نسخه ی نمایشی بسیار ساده است، اما شامل بسیاری از قابلیت های بوم مجازی است، و استفاده از آن به عنوان پایه و ساختن چیزهای پیچیده تر در بالا آسان است.

خبرکاو

ارسال نظر




تبليغات ايهنا تبليغات ايهنا

تمامی حقوق مادی و معنوی این سایت متعلق به خبرکاو است و استفاده از مطالب با ذکر منبع بلامانع است