This paper mainly analyzes the following technical points:

  1. The editor
  2. Custom components
  3. Drag and drop
  4. Delete components and adjust layers
  5. Zoom in on
  6. Undo, redo
  7. Component Property Settings
  8. The adsorption
  9. Preview and save the code
  10. The binding event
  11. The binding of animation
  12. Import the PSD
  13. Mobile phone model

To make this article easier to understand, I’ve put together a visual drag-and-drop component library DEMO:

  • Github project address
  • The online preview

It is recommended to read the source code together for better results (this DEMO uses the Vue technology stack).

1. The editor

Take a look at the overall structure of the page.

The editor in this video is really the canvas in the middle. This is what it does: when you drag a component from the left component list into the canvas, the canvas renders the component.

This editor is implemented as follows:

  1. With an arraycomponentDataMaintain the data in the editor.
  2. When dragging a component onto the canvaspush()Method to add new component data tocomponentData.
  3. Editor usev-forInstructions to traverse thecomponentData, render each component individually to the canvas (you can also combine it using JSX syntaxrender()Method instead).

The core code for editor rendering looks like this:

<component 
  v-for="item in componentData"
  :key="item.id"
  :is="item.component"
  :style="item.style"
  :propValue="item.propValue"
/>
Copy the code

Each component data looks something like this:

{
    component: 'v-text'.// The name of the component that needs to be registered with Vue in advance
    label: 'words'.// The name displayed in the left component list
    propValue: 'words'.// The value used by the component
    icon: 'el-icon-edit'.// The name displayed in the left component list
    animations: [].// Animation list
    events: {}, // Event list
    style: { // Component styles
        width: 200.height: 33.fontSize: 14.fontWeight: 500.lineHeight: ' '.letterSpacing: 0.textAlign: ' '.color: ' ',}}Copy the code

When traversing the componentData componentData, the is attribute is used to identify which component is really being rendered.

For example, if the component data to render is {component: ‘v-text’},
will be converted to
. Of course, you also need to register this component with Vue in advance.

If you want to learn more about the IS properties, please check out the official documentation.

2. Customize components

In principle, it is possible to use third-party components, but it is recommended that you encapsulate them. The properties required for each component, whether third-party or custom, may be different, so each component data can expose a property, propValue, for passing values.

For example, if the A component only needs one property, your propValue could read: propValue: ‘aaa’. If multiple properties are required, propValue can be a single object:

propValue: {
  a: 1.b: 'text'
}
Copy the code

I’ve defined three components in this DEMO component library.

Picture component:

<template>
    <div style="overflow: hidden">
        <img :src="propValue">
    </div>
</template>

<script>
export default {
    props: {
        propValue: {
            type: String.require: true,,}}}</script>
Copy the code

VButton component:

<template>
    <button class="v-button">{{ propValue }}</button>
</template>

<script>
export default {
    props: {
        propValue: {
            type: String.default: ' ',,}}}</script>
Copy the code

Text component VText:

<template>
    <textarea 
        v-if="editMode == 'edit'"
        :value="propValue"
        class="text textarea"
        @input="handleInput"
        ref="v-text"
    ></textarea>
    <div v-else class="text disabled">
        <div v-for="(text, index) in propValue.split('\n')" :key="index">{{ text }}</div>
    </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
    props: {
        propValue: {
            type: String,},element: {
            type: Object,}},computed: mapState([
        'editMode',]),methods: {
        handleInput(e) {
            this.$emit('input'.this.element, e.target.value)
        },
    },
}
</script>
Copy the code

3. Drag and drop

From the list of components to the canvas

To make an element dragable, you must add a draggable attribute to it. In addition, there are two events that play a key role in dragging a component from the component list onto the canvas:

  1. dragstartEvent that is triggered at the beginning of the drag. It is primarily used to pass drag-and-drop component information to the canvas.
  2. dropEvent that is fired when the drag ends. It is used to receive drag-and-drop component information.

Look at the code for the list of components on the left:

<div @dragstart="handleDragStart" class="component-list">
    <div v-for="(item, index) in componentList" :key="index" class="list" draggable :data-index="index">
        <i :class="item.icon"></i>
        <span>{{ item.label }}</span>
    </div>
</div>
Copy the code
handleDragStart(e) {
    e.dataTransfer.setData('index', e.target.dataset.index)
}
Copy the code

You can see that the draggable property is set for each component in the list. In addition, datatransfer.setData () is used to transmit data when the DragStart event is triggered. Again, look at the code that receives the data:

<div class="content" @drop="handleDrop" @dragover="handleDragOver" @click="deselectCurComponent">
    <Editor />
</div>
Copy the code
handleDrop(e) {
    e.preventDefault()
    e.stopPropagation()
    const component = deepCopy(componentList[e.dataTransfer.getData('index')])
    this.$store.commit('addComponent', component)
}
Copy the code

When the drop event is triggered, the component is rendered by receiving the index data transmitted with datatransfer.getData () and then finding the corresponding component data based on the index and adding it to the canvas.

Components move across the canvas

First set the drawing to position: relative and then set each component to position: Absolute. In addition to this, the movement is made by listening for three events:

  1. mousedownEvent, when the mouse is pressed over a component, records the current position of the component, i.e., xy coordinateslefttop.
  2. mousemoveEvent, each time the mouse moves, the current latest XY coordinate is subtracted from the original XY coordinate to calculate the move distance, and then the component position is changed.
  3. mouseupEvent when the mouse is raised to end the movement.
handleMouseDown(e) {
    e.stopPropagation()
    this.$store.commit('setCurComponent', { component: this.element, zIndex: this.zIndex })

    constpos = { ... this.defaultStyle }const startY = e.clientY
    const startX = e.clientX
    // If you modify the attribute directly, the value will be of string type, so convert to numeric
    const startTop = Number(pos.top)
    const startLeft = Number(pos.left)

    const move = (moveEvent) = > {
        const currX = moveEvent.clientX
        const currY = moveEvent.clientY
        pos.top = currY - startY + startTop
        pos.left = currX - startX + startLeft
        // Modify the current component style
        this.$store.commit('setShapeStyle', pos)
    }

    const up = () = > {
        document.removeEventListener('mousemove', move)
        document.removeEventListener('mouseup', up)
    }

    document.addEventListener('mousemove', move)
    document.addEventListener('mouseup', up)
}
Copy the code

4. Delete components and adjust layers

Change layer hierarchy

Because drag components into the canvas is sequential, you can assign layers in a data order.

For example, if five new components abcDE are added to the canvas, their order in the canvas data is [A, B, C, D, e], and their layer hierarchy and index correspond one by one, that is, their z-index attribute value is 01234 (trailing behind). The code is as follows:

<div v-for="(item, index) in componentData" :zIndex="index"></div>
Copy the code

If you are not familiar with the Z-index attribute, please refer to the MDN documentation.

Once you understand this, changing the layer hierarchy is easy to do. Changing the layer hierarchy changes the order of componentData in the componentData array. For example, there are three components [A, B, c], and their layers are ABC from lowest to highest (the larger the index, the higher the hierarchy).

To move component B up, simply switch it to c:

const temp = componentData[1]
componentData[1] = componentData[2]
componentData[2] = temp
Copy the code

The same is true for top and bottom. For example, if I want to place component A at the top, I simply switch the order of component A and the last component:

const temp = componentData[0]
componentData[0] = componentData[componentData.lenght - 1]
componentData[componentData.lenght - 1] = temp
Copy the code

Remove components

Deleting a component is as simple as one line of code: ComponentData.splice (index, 1).

5. Zoom in and out

Careful users may notice that when you click on a component on the canvas, it will appear as eight small dots. These eight dots are used to zoom in and out. The implementation principle is as follows:

1. Cover each component with a layerShapeComponents,ShapeThe component contains eight dots and one dot<slot>Slot for placing components.

<! -- Page component list display -->
<Shape v-for="(item, index) in componentData"
    :defaultStyle="item.style"
    :style="getShapeStyle(item.style, index)"
    :key="item.id"
    :active="item === curComponent"
    :element="item"
    :zIndex="index"
>
    <component
        class="component"
        :is="item.component"
        :style="getComponentStyle(item.style)"
        :propValue="item.propValue"
    />
</Shape>
Copy the code

Shape component internal structure:

<template>
    <div class="shape" :class="{ active: this.active }" @click="selectCurComponent" @mousedown="handleMouseDown"
    @contextmenu="handleContextMenu">
        <div
            class="shape-point"
            v-for="(item, index) in (active? pointList : [])"
            @mousedown="handleMouseDownOnPoint(item)"
            :key="index"
            :style="getPointStyle(item)">
        </div>
        <slot></slot>
    </div>
</template>
Copy the code

2. When you click the component, the eight dots are displayed.

What works is this line :active=”item === curComponent”.

3. Calculate the position of each dot.

Let’s look at the code that calculates the position of the dot:

const pointList = ['t'.'r'.'b'.'l'.'lt'.'rt'.'lb'.'rb']

getPointStyle(point) {
    const { width, height } = this.defaultStyle
    const hasT = /t/.test(point)
    const hasB = /b/.test(point)
    const hasL = /l/.test(point)
    const hasR = /r/.test(point)
    let newLeft = 0
    let newTop = 0

    // Four corner points
    if (point.length === 2) {
        newLeft = hasL? 0 : width
        newTop = hasT? 0 : height
    } else {
        // The width of the upper and lower points is in the middle
        if (hasT || hasB) {
            newLeft = width / 2
            newTop = hasT? 0 : height
        }

        // The height of the left and right points is in the middle
        if (hasL || hasR) {
            newLeft = hasL? 0 : width
            newTop = Math.floor(height / 2)}}const style = {
        marginLeft: hasR? '-4px' : '-3px'.marginTop: '-3px'.left: `${newLeft}px`.top: `${newTop}px`.cursor: point.split(' ').reverse().map(m= > this.directionKey[m]).join(' ') + '-resize',}return style
}
Copy the code

To calculate the position of the dots, you need to obtain some information:

  • Height of componentheight, width,width

Note that the dots are also absolutely positioned, relative to the Shape component. So there are four dots that are pretty well positioned:

  1. The dot in the upper left corner, coordinatesleft: 0, top: 0
  2. The dot on the top right, the coordinatesleft: width, top: 0
  3. The little dot in the lower left, coordinatesleft: 0, top: height
  4. The dot in the bottom right, the coordinatesleft: width, top: height

The other four dots need to be calculated indirectly. For example, for the left center dot, the calculation formula is left: 0, top: height / 2, and the other dot is the same.

4. Click the dot to zoom in and out.

handleMouseDownOnPoint(point) {
    const downEvent = window.event
    downEvent.stopPropagation()
    downEvent.preventDefault()

    constpos = { ... this.defaultStyle }const height = Number(pos.height)
    const width = Number(pos.width)
    const top = Number(pos.top)
    const left = Number(pos.left)
    const startX = downEvent.clientX
    const startY = downEvent.clientY

    // Whether to save the snapshot
    let needSave = false
    const move = (moveEvent) = > {
        needSave = true
        const currX = moveEvent.clientX
        const currY = moveEvent.clientY
        const disY = currY - startY
        const disX = currX - startX
        const hasT = /t/.test(point)
        const hasB = /b/.test(point)
        const hasL = /l/.test(point)
        const hasR = /r/.test(point)
        const newHeight = height + (hasT? -disY : hasB? disY : 0)
        const newWidth = width + (hasL? -disX : hasR? disX : 0)
        pos.height = newHeight > 0? newHeight : 0
        pos.width = newWidth > 0? newWidth : 0
        pos.left = left + (hasL? disX : 0)
        pos.top = top + (hasT? disY : 0)
        this.$store.commit('setShapeStyle', pos)
    }

    const up = () = > {
        document.removeEventListener('mousemove', move)
        document.removeEventListener('mouseup', up)
        needSave && this.$store.commit('recordSnapshot')}document.addEventListener('mousemove', move)
    document.addEventListener('mouseup', up)
}
Copy the code

Here’s how it works:

  1. When the dot is clicked, record the coordinate XY that is clicked.
  2. Let’s say we drag it down now, so the y-coordinate is going to increase.
  3. You take the new y coordinate minus the original y coordinate, and you know how far you’ve moved in the vertical direction.
  4. Finally, the move distance is added to the height of the original component to obtain the new component height.
  5. If it is positive, it is pulling down and the height of the component is increasing. If it is negative, it is pulling up and the height of the component is decreasing.

Undo, redo

The implementation of undo rework is actually quite simple, first look at the code:

snapshotData: [], // The editor snapshots the data
snapshotIndex: -1.// Snapshot index
        
undo(state) {
    if (state.snapshotIndex >= 0) {
        state.snapshotIndex--
        store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
    }
},

redo(state) {
    if (state.snapshotIndex < state.snapshotData.length - 1) {
        state.snapshotIndex++
        store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
    }
},

setComponentData(state, componentData = []) {
    Vue.set(state, 'componentData', componentData)
},

recordSnapshot(state) {
    // Add a new snapshot
    state.snapshotData[++state.snapshotIndex] = deepCopy(state.componentData)
    // In the undo process, when adding a new snapshot, delete the snapshots behind it
    if (state.snapshotIndex < state.snapshotData.length - 1) {
        state.snapshotData = state.snapshotData.slice(0, state.snapshotIndex + 1)}},Copy the code

Use an array to hold the editor’s snapshot data. Saving a snapshot is to keep performing a push() operation, pushing the current editor data into the snapshotData array, and adding the snapshotIndex snapshotIndex. Currently, the following actions trigger the snapshot saving operation:

  • The new component
  • Remove components
  • Change layer hierarchy
  • When dragging the component is finished

.

undo

Suppose that four snapshots are now saved in snapshotData. That is, [A, B, C, d]. The corresponding snapshot index is 3. If undo is performed, we need to subtract 1 from the snapshot index and assign the corresponding snapshot data to the canvas.

For example, the current canvas data is D. After undo, the index is -1. Now the canvas data is C.

redo

Once you understand undo, it makes sense to redo, which is to increment the snapshot index by 1 and assign the corresponding snapshot data to the canvas.

One more thing to note, however, is that there is a new operation in the undo operation, what to do? There are two solutions:

  1. The new operation replaces all data following the current snapshot index. I’m going to use the same data[a, b, c, d]For example, if two undo operations are performed, the snapshot index changes to 1, and the corresponding snapshot data is B. If a new undo operation is performed, the corresponding snapshot data is e. Then e will top off the CD, and now the snapshot data is[a, b, e].
  2. Add a new record to the original snapshot without overthrowing data. Using the example above, e does not topple the CD, but inserts it before the CD, which means that the snapshot data becomes[a, b, e, c, d].

I used the first option.

7. The adsorption

What is adsorption? When you drag a component, if it’s close enough to another component, it will automatically snap together.

Adsorbed code is about 300 lines, it is recommended to open the source file to see (file path: SRC \components\Editor\ markline.vue). I will not post the code here, but mainly talk about how to implement the principle.

marking

Create six lines on the page, three horizontal and three vertical. The purpose of these six lines is to align. When do they appear?

  1. Vertical lines appear when two components are aligned left, center, and right in the up and down direction
  2. Horizontal lines appear when left and right components are aligned at the top, center, and bottom

The specific calculation formula is mainly based on the XY coordinates and width and height of each component. For example, to determine whether the left side of ab is aligned, we need to know the x coordinate of each component. And if you want to know if they’re aligned on the right, you have to know, in addition to the x-coordinate, you also have to know their respective widths.

// Left align conditions
a.x == b.x

// Right align conditions
a.x + a.width == b.x + b.width
Copy the code

When aligned, the marking is displayed.

Also determine whether the two components ab are “sufficiently” close. If they’re close enough, they stick together. Whether it’s close enough depends on one variable:

diff: 3.// Pixels separated by DFF will automatically snap
Copy the code

Less than or equal to diff pixels automatically adsorb.

The adsorption

How is the adsorption effect achieved?

Let’s say we have ab component, a component xy is 0, width is 100, height is 100. Now suppose that component A is not moving and we are dragging component B. When component B is dragged to the coordinates x: 0, y: 103, they can be determined to be close enough because 103-100 <= 3(diff). You need to manually set the y coordinate of component B to 100, and this will adsorb components together.

To optimize the

It is not aesthetically pleasing to display all 6 lines when dragging. So we can optimize to display at most one line in both directions. The implementation principle is as follows:

  1. Component A is not moving to the left, we are dragging component B towards component A.
  2. In this case, the first thing they line up is to the right of A and to the left of B, so you only need one line.
  3. If ab is approaching and B continues to move to the left, determine whether the two are aligned in the middle.
  4. If b continues to drag, it is necessary to determine whether the left side of A is aligned with the right side of B, and only one line is needed.

As you can see, the key is to know the direction of the two components. So ab and AB are close together, and we want to know whether B is to the left or to the right of A.

This can be determined by the mouseDown event, which, as I mentioned earlier in the drag and drop section, records the starting point when the mouseDown event is triggered. So each time the mousemove event is fired, you can determine the direction of the component by subtracting the original coordinate from the current coordinate. For example, in the x direction, if the difference between B.x-A.x is positive, b is to the right of A. Otherwise, it is to the left.

// Trigger the element movement event, used to display the marking, adsorption function
// The next two parameters represent the direction of the mouse movement
// Curry-starty > 0
// currx-startx > 0
eventBus.$emit('move'.this.$el, currY - startY > 0, currX - startX > 0)
Copy the code

8. Set component properties

Each component has some common properties and some unique properties, and we need to provide a place to display and modify the properties.

// The data for each component looks like this
{
    component: 'v-text'.// The name of the component that needs to be registered with Vue in advance
    label: 'words'.// The name displayed in the left component list
    propValue: 'words'.// The value used by the component
    icon: 'el-icon-edit'.// The name displayed in the left component list
    animations: [].// Animation list
    events: {}, // Event list
    style: { // Component styles
        width: 200.height: 33.fontSize: 14.fontWeight: 500.lineHeight: ' '.letterSpacing: 0.textAlign: ' '.color: ' ',}}Copy the code

I defined an AttrList component to display the attributes of each component.

<template>
    <div class="attr-list">
        <el-form>
            <el-form-item v-for="(key, index) in styleKeys" :key="index" :label="map[key]">
                <el-color-picker v-if="key == 'borderColor'" v-model="curComponent.style[key]"></el-color-picker>
                <el-color-picker v-else-if="key == 'color'" v-model="curComponent.style[key]"></el-color-picker>
                <el-color-picker v-else-if="key == 'backgroundColor'" v-model="curComponent.style[key]"></el-color-picker>
                <el-select v-else-if="key == 'textAlign'" v-model="curComponent.style[key]">
                    <el-option
                        v-for="item in options"
                        :key="item.value"
                        :label="item.label"
                        :value="item.value"
                    ></el-option>
                </el-select>
                <el-input type="number" v-else v-model="curComponent.style[key]" />
            </el-form-item>
            <el-form-item label="Content" v-if="curComponent && curComponent.propValue && ! excludes.includes(curComponent.component)">
                <el-input type="textarea" v-model="curComponent.propValue" />
            </el-form-item>
        </el-form>
    </div>
</template>
Copy the code

The code logic is simply to iterate through the component’s style object, iterating over each property. And it needs to be displayed by different components according to specific properties. For example, color properties need to be displayed by color picker. The properties of a numeric class need to be displayed with an input component of type=number, and so on.

To make it easier for users to modify property values, I use the V-Model to bind components and values together.

Preview and save the code

Preview and edit are rendered on the same principle, the difference is that no editing is required. So you just need to change the original rendering component code a little bit.

<! -- Page component list display -->
<Shape v-for="(item, index) in componentData"
    :defaultStyle="item.style"
    :style="getShapeStyle(item.style, index)"
    :key="item.id"
    :active="item === curComponent"
    :element="item"
    :zIndex="index"
>
    <component
        class="component"
        :is="item.component"
        :style="getComponentStyle(item.style)"
        :propValue="item.propValue"
    />
</Shape>
Copy the code

After the introduction just now, we know that Shape component has the function of drag and drop, zoom in and out. Now you just need to remove the Shape component and replace it with a normal DIV (actually you don’t need this DIV, but you need it to bind events).

<! -- Page component list display -->
<div v-for="(item, index) in componentData" :key="item.id">
    <component
        class="component"
        :is="item.component"
        :style="getComponentStyle(item.style)"
        :propValue="item.propValue"
    />
</div>
Copy the code

Save the code function is also very simple, only need to save the data on the canvas componentData. There are two options for saving:

  1. Save to the server
  2. Local preservation

The localStorage I used in DEMO is saved locally.

10. Bind events

Each component has a Events object that stores bound events. So far I’ve only defined two events:

  • Alert events
  • Redirect events
// Editor custom events
const events = {
    redirect(url) {
        if (url) {
            window.location.href = url
        }
    },

    alert(msg) {
        if (msg) {
            alert(msg)
        }
    },
}

const mixins = {
    methods: events,
}

const eventList = [
    {
        key: 'redirect'.label: 'Jump Event'.event: events.redirect,
        param: ' '}, {key: 'alert'.label: 'alert events'.event: events.alert,
        param: ' ',},]export {
    mixins,
    events,
    eventList,
}
Copy the code

But it can’t be triggered while editing, it can be triggered while previewing.

Add event

Render the list of events with the V-for directive:

<el-tabs v-model="eventActiveName">
    <el-tab-pane v-for="item in eventList" :key="item.key" :label="item.label" :name="item.key" style="padding: 0 20px">
        <el-input v-if="item.key == 'redirect'" v-model="item.param" type="textarea" placeholder="Please enter the full URL" />
        <el-input v-if="item.key == 'alert'" v-model="item.param" type="textarea" placeholder="Please enter what you want to alert." />
        <el-button style="margin-top: 20px;" @click="addEvent(item.key, item.param)">determine</el-button>
    </el-tab-pane>
</el-tabs>
Copy the code

When the event is selected, the event is added to the component’s Events object.

Triggering event

When you preview or actually render the page, you also need to wrap a DIV around each component so that you can attach a click event to the DIV, which triggers the event we just added when clicked.

<template>
    <div @click="handleClick">
        <component
            class="conponent"
            :is="config.component"
            :style="getStyle(config.style)"
            :propValue="config.propValue"
        />
    </div>
</template>
Copy the code
handleClick() {
    const events = this.config.events
    // The loop fires the bound event
    Object.keys(events).forEach(event= > {
        this[event](events[event])
    })
}
Copy the code

11. Bind animation

Animations and events work the same way: render all animations using the V-for command, then click on the animations to add the animations to the component’s animations array. As with events, execution iterates through all of the component’s animations and executes.

For convenience, we use the animate. CSS animation library.

// main.js
import '@/styles/animate.css'
Copy the code

Now we define all the animation data in advance:

export default[{label: '进入'.children: [{label: 'come'.value: 'fadeIn' },
            { label: 'Enter right'.value: 'fadeInLeft' },
            { label: 'Enter left'.value: 'fadeInRight' },
            { label: 'Up into'.value: 'fadeInUp' },
            { label: 'Down into'.value: 'fadeInDown' },
            { label: 'Enter long distance to the right'.value: 'fadeInLeftBig' },
            { label: 'Long left entry'.value: 'fadeInRightBig' },
            { label: 'Up long distance into'.value: 'fadeInUpBig' },
            { label: 'Down long distance into'.value: 'fadeInDownBig' },
            { label: 'Spin in'.value: 'rotateIn' },
            { label: 'Turn clockwise left'.value: 'rotateInDownLeft' },
            { label: 'Right counterclockwise rotation'.value: 'rotateInDownRight' },
            { label: 'Left counterclockwise rotation'.value: 'rotateInUpLeft' },
            { label: 'Right counterclockwise rotation'.value: 'rotateInUpRight' },
            { label: 'into'.value: 'bounceIn' },
            { label: 'Spring in to the right'.value: 'bounceInLeft' },
            { label: 'Catapult to the left'.value: 'bounceInRight' },
            { label: 'Bounce up'.value: 'bounceInUp' },
            { label: 'Spring down'.value: 'bounceInDown' },
            { label: 'The speed of light enters from the right.'.value: 'lightSpeedInRight' },
            { label: 'The speed of light enters from the left'.value: 'lightSpeedInLeft' },
            { label: 'The speed of light exits from the right'.value: 'lightSpeedOutRight' },
            { label: 'The speed of light exits to the left'.value: 'lightSpeedOutLeft' },
            { label: 'Rotation on the Y-axis'.value: 'flip' },
            { label: 'Rotation on the X axis of the center'.value: 'flipInX' },
            { label: 'Rotation on the central Y-axis'.value: 'flipInY' },
            { label: 'Left long radius rotation'.value: 'rollIn' },
            { label: 'Go from small to large.'.value: 'zoomIn' },
            { label: 'Left big enter'.value: 'zoomInLeft' },
            { label: 'Right big in'.value: 'zoomInRight' },
            { label: 'Up and into'.value: 'zoomInUp' },
            { label: 'Down and into'.value: 'zoomInDown' },
            { label: 'Slide to the right'.value: 'slideInLeft' },
            { label: 'Slide to the left'.value: 'slideInRight' },
            { label: 'Slide up and unfold'.value: 'slideInUp' },
            { label: 'Slide down and expand'.value: 'slideInDown'},],}, {label: 'stress'.children: [{label: 'bounce'.value: 'bounce' },
            { label: 'flashing'.value: 'flash' },
            { label: 'Zoom in and out'.value: 'pulse' },
            { label: 'Zoom in and out spring'.value: 'rubberBand' },
            { label: 'Rocking from side to side'.value: 'headShake' },
            { label: 'Fan sway'.value: 'swing' },
            { label: 'Amplifying and shaking out'.value: 'tada' },
            { label: 'Fan swing'.value: 'wobble' },
            { label: 'Jiggle up and down.'.value: 'jello' },
            { label: 'Rotation on the Y-axis'.value: 'flip'},],}, {label: 'exit'.children: [{label: 'fade'.value: 'fadeOut' },
            { label: 'Exit left'.value: 'fadeOutLeft' },
            { label: 'Exit right'.value: 'fadeOutRight' },
            { label: 'Exit up'.value: 'fadeOutUp' },
            { label: 'Exit down'.value: 'fadeOutDown' },
            { label: 'Long left exit'.value: 'fadeOutLeftBig' },
            { label: 'Long distance to the right'.value: 'fadeOutRightBig' },
            { label: 'Up long distance exit'.value: 'fadeOutUpBig' },
            { label: 'Down long distance exit'.value: 'fadeOutDownBig' },
            { label: 'Exit from rotation'.value: 'rotateOut' },
            { label: 'Turn clockwise left'.value: 'rotateOutDownLeft' },
            { label: 'Right counterclockwise rotation'.value: 'rotateOutDownRight' },
            { label: 'Left counterclockwise rotation'.value: 'rotateOutUpLeft' },
            { label: 'Right counterclockwise rotation'.value: 'rotateOutUpRight' },
            { label: 'pop up'.value: 'bounceOut' },
            { label: 'Pop left'.value: 'bounceOutLeft' },
            { label: 'Pop to the right'.value: 'bounceOutRight' },
            { label: 'Pop up'.value: 'bounceOutUp' },
            { label: 'Pop down'.value: 'bounceOutDown' },
            { label: 'Rotation on the X axis of the center'.value: 'flipOutX' },
            { label: 'Rotation on the central Y-axis'.value: 'flipOutY' },
            { label: 'Left long radius rotation'.value: 'rollOut' },
            { label: 'Exit from small to large'.value: 'zoomOut' },
            { label: 'Left big exit'.value: 'zoomOutLeft' },
            { label: 'Right big exit'.value: 'zoomOutRight' },
            { label: 'Up and out'.value: 'zoomOutUp' },
            { label: 'Down big exit'.value: 'zoomOutDown' },
            { label: 'Slide left and fold up'.value: 'slideOutLeft' },
            { label: 'Slide to the right and fold up'.value: 'slideOutRight' },
            { label: 'Slide up and fold up'.value: 'slideOutUp' },
            { label: 'Slide down and fold up'.value: 'slideOutDown'},],},]Copy the code

Then use the V-for command to render the animated list.

Add animation

<el-tabs v-model="animationActiveName">
    <el-tab-pane v-for="item in animationClassData" :key="item.label" :label="item.label" :name="item.label">
        <el-scrollbar class="animate-container">
            <div
                class="animate"
                v-for="(animate, index) in item.children"
                :key="index"
                @mouseover="hoverPreviewAnimate = animate.value"
                @click="addAnimation(animate)"
            >
                <div :class="[hoverPreviewAnimate === animate.value && animate.value + ' animated']">
                    {{ animate.label }}
                </div>
            </div>
        </el-scrollbar>
    </el-tab-pane>
</el-tabs>
Copy the code

Clicking on the animation will call addAnimation(Animate) to add the animation to the component’s animations array.

Trigger the animation

The code to run the animation:

export default async function runAnimation($el, animations = []) {
    const play = (animation) = > new Promise(resolve= > {
        $el.classList.add(animation.value, 'animated')
        const removeAnimation = () = > {
            $el.removeEventListener('animationend', removeAnimation)
            $el.removeEventListener('animationcancel', removeAnimation)
            $el.classList.remove(animation.value, 'animated')
            resolve()
        }
            
        $el.addEventListener('animationend', removeAnimation)
        $el.addEventListener('animationcancel', removeAnimation)
    })

    for (let i = 0, len = animations.length; i < len; i++) {
        await play(animations[i])
    }
}
Copy the code

Running an animation takes two parameters: the component’s corresponding DOM element (obtained from the component using this.$el) and its animation data: animations. You also need to listen for the AnimationEnd event and the AnimationCancel event: one when the animation ends and one when the animation ends unexpectedly.

Using this in conjunction with a Promise, you can run each animation of the component individually.

12. Import the PSD

I haven’t done that yet due to time constraints. Here’s a brief description of how to do this. That’s using the psD.js library, which parses PSD files.

Using the PSD library to parse the PSD file, the following data is obtained:

{ children: [{type: 'group'.visible: false.opacity: 1.blendingMode: 'normal'.name: 'Version D'.left: 0.right: 900.top: 0.bottom: 600.height: 600.width: 900.children: [{type: 'layer'.visible: true.opacity: 1.blendingMode: 'normal'.name: 'Make a change and save.'.left: 275.right: 636.top: 435.bottom: 466.height: 31.width: 361.mask: {},
            text: 
             { value: 'Make a change and save.'.font: 
                { name: 'HelveticaNeue-Light'.sizes: [ 33].colors: [[85.96.110.255]],alignment: [ 'center']},left: 0.top: 0.right: 0.bottom: 0.transform: { xx: 1.xy: 0.yx: 0.yy: 1.tx: 456.ty: 459}},image: {}}]}],document: 
       { width: 900.height: 600.resources: 
          { layerComps: [{id: 692243163.name: 'Version A'.capturedInfo: 1 },
               { id: 725235304.name: 'Version B'.capturedInfo: 1 },
               { id: 730932877.name: 'Version C'.capturedInfo: 1}].guides: [].slices: []}}}Copy the code

As you can see from the above code, this data is very similar to CSS. With this in mind, the PSD file can be turned into a rendering component by writing a conversion function to convert this data into the data required by our component. At present, quark-H5 and Luban-H5 both implement the PSD conversion function in this way.

13. Phone mode

Since the canvas can be resized, we can use the resolution of the iphone6 to develop mobile pages.

The developed page can also be viewed normally on the phone, but there may be a style bias. Because the three components I customized are not adapted, if you need to develop a mobile page, the custom components must use the mobile UI component library. Or develop your own custom components for mobile terminals.

conclusion

Since the DEMO is full of code, I’ll just post the key code for each function point. So you’ll notice that the source code for the DEMO is a little bit different than the code I posted here, so don’t worry about it.

In addition, the style of the DEMO is relatively simple, mainly because I have many things recently, so I don’t have much time to write better. Please forgive me.

Visual Drag and Drop series:

  • Analysis of some technical essentials of visual drag and drop component library
  • Analysis of some Technical Points of Visual Drag-and-drop Component Library (part 2)
  • Analysis of some Technical Points of Visual Drag-and-drop Component Library (3)

The resources

  • ref-line
  • quark-h5
  • luban-h5
  • Yi qi xiu
  • Drag events