This is the third article in my project record series. At present, the progress of the project is somewhat stalled, mainly because I am lazy in other things recently, so I force myself to optimize the process of clicking the icon to jump out of the popover in these days, summarize and record in time, and let everyone know that I am still alive.

This article will introduce a series of effects generated by clicking the Dock icon in the current project, such as generating a pop-up window that can be dragged, etc. Currently, only four ICONS such as calculator and artboard can be used.

All codes in this paper are in the project code, which will be optimized all the time. Welcome Watch and Star.

Process analysis

In the last post, we implemented the Dock dynamic effect, so we will definitely want to click on the icon. When we click on the icon, first there will be a bouncing effect of the icon, then there will be a corresponding application box of the icon, and at the same time there will be a small highlight dot below the icon. Next, I will use drawing board as an example to show the code. I will not introduce the detailed content of drawing board in this paper, which is expected to become the protagonist of the fourth chapter.

The corresponding directory of the code content appears in this article:

Icon click interaction

Dynamic effect to realize

When we first click the icon to make it active, there should be an interactive animation:

Here I refer to bounce. CSS in animate

// footer/index.scss

@keyframes bounce {

  from,

  20%,

  53%,

  to {

    animation-timing-functioncubic-bezier(0.215.0.61.0.355.1);

    transform: translate3d(0.0.0);

  }



  40%,

  43% {

    animation-timing-functioncubic-bezier(0.755.0.05.0.855.0.06);

    transform: translate3d(0.- 35px, 0) scaleY(1.1);

  }



  70% {

    animation-timing-functioncubic-bezier(0.755.0.05.0.855.0.06);

    transform: translate3d(0.- 35px, 0) scaleY(1.05);

  }



  80% {

    transition-timing-functioncubic-bezier(0.215.0.61.0.355.1);

    transform: translate3d(0.0.0) scaleY(0.95);

  }



  90% {

    transform: translate3d(0.- 6px, 0) scaleY(1.02);

  }

}

.bounce {

  animation-duration: 2s;

  animation-name: top; 

}

Copy the code

IsDrawingOpen (App open, app close) and isDrawingShow (App Display, minimize)

Add a click event to an icon to determine which icon it is by its name. For each icon we give a Boolean object, such as isDrawingOpen, which is an object that records a Boolean type as a popup switch (used only to open and close applications); An index record icon corresponds to the order.

Click and add.bounce to the corresponding icon. At this time, the bounce animation starts. At the same time, we change the type (artboard appears) and record index after 2.5s, and remove the class selector, so that we can click it again next time.

// Footer.tsx

interface OpenTypes {

  type: boolean;

index? : number;

}



const [isDrawingOpen, setDrawingOpen] = useState<OpenTypes>({

  type: false

});



const [isDrawingShow, setDrawingShow] = useState(true);



const dockItemClick = useCallback(

  (item: string, index: number) => {

    if(! dockRef.current) {

      return;

    }

    const imgList = dockRef.current.childNodes;

    const img = imgList[index] as HTMLDivElement;

    switch (item) {

      case "PrefApp.png":

        if(! isDrawingOpen.type) {

          img.classList.add("bounce");

          setTimeout(() => {

setDrawingOpen({ type: ! isDrawingOpen.type, index });

            img.classList.remove("bounce");

          }, 2500);

          return;

        }

setDrawingShow(! isDrawingShow);

        return;

    }

  },

  [isDrawingOpen, isDrawingShow]

);

Copy the code

At the same time, you can see that there is a separate Boolean value: isDrawingShow, which toggles the display state when the app is activated by clicking the icon or the minimize button.

useEffect((a)= > {

  if(! dockRef.current) {

    return;

  }

  const imgList = dockRef.current.childNodes;

  [isDrawingOpen].forEach((item)= > {

    if (item.index) {

      const img = imgList[item.index] as HTMLDivElement;

! item.type

        ? setTimeout((a)= > {

img? .classList.remove("active");

          }, 1000)

        : img.classList.add("active");

    }

  });

}, [isDrawingOpen]);

Copy the code

Since closing the application is not controlled by the Dock, we need to listen for isDrawingOpen to determine whether to add the class selector Active, which is mainly used to highlight the icon dot switch

The implementation of small dots

// footer/index.scss



#DockItem {

  position: relative;

  display: flex;

  &.active {

    &::after {

      content: "Low";

      font-size: 0.1em;

      position: absolute;

      bottom: 7 -px;

    }

  }

}

Copy the code

CreateContext implements component communication:

Here our artboard components are definitely separate files, so opening and closing popovers requires component communication.

export const FooterContext = createContext<any>([]);

.

return (

   <React.Fragment>

    <FooterContext.Provider

      value={[isDrawingOpen, setDrawingOpen.isDrawingShow.setDrawingShow]}

      >


      <Drawing />

    </FooterContext.Provider>

    <div ref={dockRef} style={{ height: defaultWidth}} >

      {dockList.map((item, index) => {

        return (

          <div

            id="DockItem"

            style={

              {

                backgroundImage: "url(" + require("./image/" + item) + ") ",

                backgroundPosition: "center",

                backgroundSize: "cover",

                backgroundRepeat: "no-repeat",

              } as CSSProperties

            }

            key={index + item}

            onClick={()= >
 dockItemClick(item, index)}

          />

        );

      })}

    </div>

  </React.Fragment>

);

Copy the code

As those of you who saw the second part of the series may remember, we used to have an img icon, but now we have a div icon. The main purpose of this is to work with fake elements under active (img doesn’t work with ::after).

We generate a FooterContext by createContext, As our Drawing child passes [isDrawingOpen, setDrawingOpen, isDrawingShow, setDrawingShow], the child can also call the FooterContext to change the application state.

Here is the complete code for the child Drawing using FooterContext:

// drawing/index.tsx

import React, { useContext, useEffect, useState, useCallback } from "react";

import { useModal } from ".. /modal/UseModal";

import { FooterContext } from ".. /footer/Footer";

import { TitleBar } from "react-desktop/macOs";

import Canvas from "./Canvas";

import "./index.scss";

/// <reference path="react-desktop.d.ts" />



export const Drawing = React.memo((a)= > {

  const { open, close, RenderModal } = useModal();

  const [

    isDrawingOpen,

    setDrawingOpen,

    isDrawingShow,

    setDrawingShow,

  ] = useContext(FooterContext);

  const [style, setStyle] = useState({ width1200.height800 });

  const [isFullscreen, setFullscreen] = useState(false);



  useEffect(isDrawingOpen.type ? open : close, [isDrawingOpen]);

  const maximizeClick = useCallback((a)= > {

    if (isFullscreen) {

      setStyle({ width1200.height800 });

    } else {

      setStyle({ width- 1.height- 1 });

    }

setFullscreen(! isFullscreen);

  }, [isFullscreen]);



  return (

    <RenderModal

      data={{

        width: style.width.

        height: style.height.

        id: "DrawingView",

        moveId: "DrawingMove",

        isShow: isDrawingShow.

      }}

    >


      <div className="drawing-wrapper">

        <TitleBar

          controls

          id="DrawingMove"

          isFullscreen={isFullscreen}

          onCloseClick={()= >
 {

            close();

setDrawingOpen({ ... isDrawingOpen, type: false });

          }}

          onMinimizeClick={() => {

            setDrawingShow(false);

          }}

          onMaximizeClick={maximizeClick}

          onResizeClick={maximizeClick}

        ></TitleBar>

        <Canvas

          height={isFullscreen ? document.body.clientHeight - 32 : style.height}

          width={isFullscreen ? document.body.clientWidth : style.width}

        />


      </div>

    </RenderModal>

  );

});

Copy the code

The useModal here is a popbox component, explained below. Canvas is the main body of drawing, which we will not introduce here.

React-desktop /macOs use and custom declaration files

You can see that I use the React desktop/macOs component, a react desktop UI, but the library does not have @types, so I need to write.d.ts:

// tsconfig.json

{

  "compilerOptions": {

    "baseUrl"". /".

    "target""es5".

    "lib": ["dom"."dom.iterable"."esnext"].

    "allowJs"true.

    "skipLibCheck"true.

    "esModuleInterop"true.

    "allowSyntheticDefaultImports"true.

    "strict"true.

    "forceConsistentCasingInFileNames"true.

    "module""esnext".

    "moduleResolution""node".

    "resolveJsonModule"true.

    "isolatedModules"true.

    "noEmit"true.

    "jsx""react"

  },

  "include": ["src"."typings"// Typings are added here

}

Copy the code
// typings/react-desktop.d.ts

declare module "react-desktop/macOs" {

  export const View: JSX;

  export const Radio: JSX;

  export const TitleBar: JSX;

  export const Toolbar: JSX;

  export const Text: JSX;

  export const Box: JSX;

  export const ListView: JSX;

  export const ListViewRow: JSX;

  export const Window: JSX;

  export const Dialog: JSX;

  export const Button: JSX;

}

Copy the code

It can then be imported into TypeScript in the following way

/// <reference path="react-desktop.d.ts" />

Copy the code

TitleBar

Let’s move on to our drawing/ Index.tsX, where TitleBar is used

You can see useModal has open, close, RenderModal, and RenderModal is the popover that WE’ll talk about in a minute, and the first two are the switches that control popovers.

When we click on the red close, we’re going to call isDrawingOpen, setDrawingOpen; And the yellow minimize button is going to call setDrawingShow(false), so we’re just going to set it to false because we’re going to show it again by clicking on the icon, so the highlights shouldn’t go away when you minimize it; The maximizeClick function is used for the green maximize button, where I use width and height -1 to tell Modal full screen that pop-ups and drags need to include the values passed by the data inside them.

Implement the popover component with Portal

Each applet in the project is essentially a popover, so it is necessary to implement a reusable component, which we can do quickly thanks to Portal. I directly reused the React Hooks version of the Portal implementation from this article.

Drag popover:

// The code is long, you can first see the above reference blog version

// Modal.tsx

import ReactDOM from "react-dom";

import React, {

  useState,

  useCallback,

  useMemo,

  useEffect,

  CSSProperties,

from "react";



type Props = {

  children: React.ReactChild;

  closeModal: (a)= > void;

  onDrag: (T: any) = > void;

  onDragEnd: (a)= > void;

  data: {

    width: number;

    height: number;

    id: string;

    moveId: string;

    isShow: boolean;

  };

};



const Modal = React.memo(

  ({ children, closeModal, onDrag, onDragEnd, data }: Props) = > {

    const domEl = document.getElementById("main-view"as HTMLDivElement;

    if(! domEl)return null;

    const dragEl = document.getElementById(data.id) as HTMLDivElement;

    const moveEl = document.getElementById(data.moveId) as HTMLDivElement;

    const localPosition = localStorage.getItem(data.id) || null;

    const initPosition = localPosition

      ? JSON.parse(localPosition)

      : {

          x: data.width === - 1 ? 0 : (window.innerWidth - data.width) / 2.

          y: data.height === - 1 ? 0 : (window.innerHeight - data.height) / 2.

        };

    const [state, setState] = useState({

      isDraggingfalse.

      origin: { x0.y0 },

      position: initPosition,

    });



    const handleMouseDown = useCallback(({ clientX, clientY }) = > {

      setState((state) = > ({

. state,

        isDraggingtrue.

        origin: {

          x: clientX - state.position.x,

          y: clientY - state.position.y,

        },

      }));

} []);



    const handleMouseMove = useCallback(

      ({ clientX, clientY, target }) = > {

        if(! state.isDragging || (moveEl && target ! == moveEl))return;

        let x = clientX - state.origin.x;

        let y = clientY - state.origin.y;

        if (x <= 0) {

          x = 0;

        } else if (x > window.innerWidth - dragEl.offsetWidth) {

          x = window.innerWidth - dragEl.offsetWidth;

        }

        if (y <= 0) {

          y = 0;

        } else if (y > window.innerHeight - dragEl.offsetHeight) {

          y = window.innerHeight - dragEl.offsetHeight;

        }

        const newPosition = { x, y };

        setState((state) = > ({

. state,

          position: newPosition,

        }));

        onDrag({ newPosition, domEl });

      },

      [state.isDragging, state.origin, moveEl, dragEl, onDrag, domEl]

    );



    const handleMouseUp = useCallback((a)= > {

      if (state.isDragging) {

        setState((state) = > ({

. state,

          isDraggingfalse.

        }));



        onDragEnd();

      }

    }, [state.isDragging, onDragEnd]);



    useEffect((a)= > {

      if (data.width === - 1) {

        setState({

          isDraggingfalse.

          origin: { x0.y0 },

          position: { x0.y0 },

        });

      }

    }, [data.width]);



    useEffect((a)= > {

      if(! domEl)return;

      domEl.addEventListener("mousemove", handleMouseMove);

      domEl.addEventListener("mouseup", handleMouseUp);

      return (a)= > {

        domEl.removeEventListener("mousemove", handleMouseMove);

        domEl.removeEventListener("mouseup", handleMouseUp);

        if(data.width ! = =- 1) {

          localStorage.setItem(data.id, JSON.stringify(state.position));

        }

      };

},

      domEl,

      handleMouseMove,

      handleMouseUp,

      data.id,

      data.width,

      state.position,

    ]);



    const styles = useMemo(

      (a)= > ({

        left`${state.position.x}px`.

        top`${state.position.y}px`.

        zIndex: state.isDragging ? 2 : 1.

        display: data.isShow ? "block" : "none".

        position"absolute".

      }),

      [state.isDragging, state.position, data.isShow]

    );



    return ReactDOM.createPortal(

      <div

        style={styles as CSSProperties}

        onMouseDown={handleMouseDown}

        id={data.id}

      >


        {children}

      </div>
.

      domEl

    );

  }

);

Copy the code

As you can see, I added drag and drop to modal.tsx. The code is long, but the principle is relatively simple. You can look at the pure Modal version in the blog first and then look at the version with drag and drop.

Here I directly show the complete code, originally intended to introduce it like the second article on dynamic effects, but in fact, the idea is very similar, both use useEffect to listen for mouse events, so I will briefly introduce the idea for easy understanding:

First we see three DOM elements: domEl, dragEl, moveEl: domEl is the same as in the reference article, mainly the popover DOM, which I added to app.tsx; DragEl represents the application body DOM (in this case Drawing); MoveEl is the draggable part inside the application component, usually the TitleBar.

Since the application is simulated, we need to record the current position of the application, so localStorage is used, initPosition is used to initialize the position of the application, and -1 is used to determine whether the application is in full screen.

State is used to record mouse data and whether it can be dragged; HandleMouseDown records the current mouse coordinates and enables drag and drop. HandleMouseMove calculates the movement, assigns it to position, and pays attention to the boundary, but of course I’m simplifying things here by not allowing it to go out of the screen; HandleMouseUp closes drag; CloseModal, onDrag, onDragEnd are popover internal close functions, additional drag events, and stop events, respectively. This is the main idea of the box component and drag.

UseModal

UseModal is basically consistent with the text:

// UseModal.tsx

import React, { useState } from "react";



import Modal from "./Modal";



// The two most basic events of the Modal component, open/close

export const useModal = (a)= > {

  const [isVisible, setIsVisible] = useState(false);



  const open = (a)= > setIsVisible(true);

  const close = (a)= > setIsVisible(false);



  const RenderModal = ({

    children,

    data,

  }: {

    children: React.ReactChild;

    data: {

      width: number;

      height: number;

      id: string;

      moveId: string;

      isShow: boolean;

    };

}) = > (

    <React.Fragment>

      {isVisible && (

        <Modal

          data={data}

          closeModal={close}

          onDrag={()= >
 console.log("onDrag")}

          onDragEnd={() => console.log("onDragEnd")}

        >

          {children}

        </Modal>

      )}

    </React.Fragment>

  );



  return {

    open,

    close,

    RenderModal,

  };

};

Copy the code

We covered how to use this component earlier, so you can look back if you forget.

At this point, we have completed the initial process analysis.

summary

This article introduces the implementation process of the project from clicking the Dock to present the application to closing the application. There are many details in it, which are worth repeating and optimizing.

This article is longer than the previous two, you can see that there is true love (study and ME). In that case, give me a thumbs-up 🍮.

At present, some functions of the project have been completed, including simple Settings, basic calculator, basic drawing board, etc. Even these existing functions still need to be improved.

Later, I will slowly optimize and update the series of articles from time to time when the corresponding module code is optimized to a certain extent.