preface
In my spare time last year, I developed a visual page editor. This time, when I saw the project review activity of Nuggets, I could write an article and share it with you. I don’t know if I can make it to the event
Before you start, you can try:
The online preview
Making the address
Main function of editor
- Elements drag, zoom in, zoom out, rotate freely
- Add images, text, rectangles, backgrounds. Multiple editing functions (font, background, size, margins, etc.)
- Components automatically adsorb, real-time guide (components can automatically adsorb and align with canvas, custom guide and other components, and display real-time guide. Press Alt key to temporarily close during dragging)
- Ruler, guide line, you can customize the reference line (click on the ruler to generate the reference line, drag the reference line to change the position, double click to delete the reference line)
- Undo, redo (support shortcut keys, configurable number of steps to undo)
- Components copy, paste, lock, hide, etc
- CTRL + Drag components to quickly copy components
- Right-click the menu, the menu can be configured, can be flexibly generated according to the current state of the component (that is, different components can generate different menus)
- Layers panel, you can drag and drop to change component layers, rename them, and quickly lock, delete, and hide components in layers Panel
- Select multiple components at the same time (CTRL + left key) to align multiple components
- Data backup, stored locally (automatic or manual) through an indexDB database, and data can be recovered from backups
- One-click generation of H5 code
- Edit canvas size
- Multiple shortcut keys
- Set the center, can set the undo function, backup function, etc
- Secondary development through plug-in system
Because there are more details inside, certainly can’t talk about all points, I will pick a few main writing, some may write a brief, the specific implementation can see the source code on GitHub.
The overall architecture
This editor is generally divided into three areas, left, middle and right, on the left side to add components, in the middle of the operation, on the right side to edit some properties of the components, I refer to the design of Yiqixiu, in the middle and right part of a shortcut bar, there are some commonly used Settings.
These different areas correspond to different functions, so in the code we also need to separate these different functional areas:
<! -- index.vue -->
<template>
<div class="poster-editor" :class="{ 'init-loading': initLoading }">
<div class="base">
<! Add component bar to left -->
<left-side />
<! -- Main operating area -->
<main-component ref="main" />
<! -- Common function bar -->
<extend-side-bar />
<! -- Component editing area -->
<control-component />
</div>
<! Layers Panel -->
<transition name="el-zoom-in-top">
<layer-panel v-show="layerPanelOpened" />
</transition>
</div>
</template>
Copy the code
Then there is the data, which includes canvas properties, component properties, current editor state, and so on, stored in VUEX:
const state = {
activityId: ' '.pageConfigId: ' '.pageTitle: ' '.canvasSize: {
width: 338.height: 600
},
canvasPosition: {
top: null.left: null
},
background: null.posterItems: [].// List of components
activeItems: [].// The currently selected component
assistWidgets: [].// Auxiliary components
layerPanelOpened: true.// Whether to open the Layers panel
referenceLineOpened: true.// Whether to open the guide
copiedWidgets: null.// WidgetItem[]
referenceLine: {
// A user defined guide
row: [].col: []},matchedLine: null.{row:[],col:[]}
mainPanelScrollY: 0.isUnsavedState: false // Whether it is in the unsaved state
}
Copy the code
The main thing here is the posterItems property, which is an array that holds all the current components. Adding a component is to push data into this array, then iterate over the posterItems to place the component on the canvas, where it can be edited.
Component implementation
Because there are a lot of kinds of components, and these components have some same attributes, such as the position and size information, whether to lock, whether hidden, and so on, these same attributes may not each write it again, so want to implement a basic components, the basic component contains all of the components common attributes, and then other components through expanding the component, I use class to implement this:
const defaultWidgetConfig = () = > {
return {
id: ' './ / component id
type: ' './ / type
typeLabel: ' '.// Type tag
componentName: ' '.// Name of the dynamic component
icon: ' './ / icon class
wState: {}, // Internal component state data, style properties and other information
dragInfo: { w: 100.h: 100.x: 0.y: 0.rotateZ: 0 }, // Component position, size, rotation Angle
rename: ' '.// typeLabel Rename
lock: false.// Whether it is locked
visible: true.// Whether it is visible
initHook: null.// Function is created when the component is initialized
layerPanelVisible: true.// Whether it is visible in the images panel
replicable: true.// Whether it can be copied
isCopied: false.// Whether the component is copied (the component obtained by the copy operation)
removable: true.// Whether to delete
couldAddToActive: true.// Whether it can be added to activeItems
componentState: null // Function Returns data inside the original component when the component is copied. Componentstate. count indicates the number of times to be copied
/ * * *@property {Int} _copyCount Number of copies *@property {String} _copyFrom replication source command | drag *@property {Boolean} _isBackup Whether the component is restored through backup *@property {Int} _widgetCountLimit The number of components *@property {Int} _sort component layer sort */}}// Component parent class
export default class Widget {
constructor(config) {
const item = _merge(defaultWidgetConfig(), config, {
id: uniqueId(config.typeLabel + The '-')})// this._config = item
Object.keys(item).forEach((key) = > {
this[key] = item[key]
})
}
/ / component mixins
static widgetMixin(options) {
/ /... For a moment about}}Copy the code
DefaultWidgetConfig is the component’s configuration item. Widgets initialize these configurations, and then other components inherit the widgets. For example, we want to implement a text component:
/ / text Widget
export default class TextWidget extends Widget {
constructor(config) {
config = _merge(
{
type: 'text'.typeLabel: 'text'.componentName: 'text-widget'.icon: 'icon-text'.lock: false.visible: true.wState: {
text: 'Double click edit text'.style: {
margin: '10px'.wordBreak: 'break-all'.color: '# 000'.textAlign: 'center'.fontSize: '14px'.// px
padding: 0.// px
borderColor: '# 000'.borderWidth: 0.// px
borderStyle: 'solid'.lineHeight: '100%'./ / %
letterSpacing: 0./ / %
backgroundColor: ' '.fontWeight: ' '.fontStyle: ' '.textDecoration: ' '
}
}
},
config
)
super(config)
}
}
Copy the code
This constructor initializes some configuration, uses a new one, and adds it to the posterItems, simplifying it to something like this:
// Add a text component
store.dispatch('poster/addItem'.new TextWidget())
const actions = {
addItem(state, item) {
if (item instanceof Widget) {
state.posterItems.push(item)
}
}
}
Copy the code
After adding, the action area in the middle of the editor traverses the posterItems as mentioned above:
<component
v-for="item in posterItems"
:key="item.id"
:item="item"
:is="item.componentName"
/>
Copy the code
ComponentName is a text-widget for a TextWidget component. For example, componentName is a text-widget for TextWidget.
<! -- textWidget.vue -->
<template>
<div class="text-widget">demo</div>
</template>
<script>
import { TextWidget } from 'poster/widgetConstructor'
export default {
mixins: [TextWidget.widgetMixin()],
data() {
return{}}}</script>
<style lang="scss" scoped></style>
Copy the code
Now that you have added the components, you can see the demo on the canvas. This is just an example, but more details can be found in GitHub’s source code.
Note that a mixin is introduced here, and the textwidge. widgetMixin is actually on the Widget:
export default class Widget {
constructor(config) {
// ...
}
/ / component mixins
static widgetMixin(options) {
// ...}}Copy the code
It is the common logic of some components. In fact, it is better to use higher-order components here. In the case of mixin, each component needs to be written again, which is cumbersome, but I didn’t think clearly at that time. This mixin is going to be used in a moment, but when it’s used again, here’s an impression.
Drag and drop zoom function
Vue: Resizable: Vue: resizable: Vue: draggable: resizable: Vue: draggable: resizable
<! -- textWidget.vue -->
<template>
<vue-draggable-resizable>
<div class="text-widget">demo</div>
</vue-draggable-resizable>
</template>
Copy the code
Although this is feasible, but one problem is that we have not only text components, there are pictures, rectangular, background, may be later add other components, and this is not a drag it is ok to set, we need to write a lot of other logic, such as drag when the x and y data updated in real time to the properties of components, but also has the functions such as adsorption alignment, You can’t write this for every component, but you can actually separate drag and drop from component. Drag is a container, and then you have nested components inside it.
<vue-draggable-resizable v-for="item in posterItems" :key="item.id">
<component
:is="item.componentName"
ref="widget"
:item="item"
:is-active="isActive"
v-on="$listeners"
@draggableChange="draggable = $event"
/>
</vue-draggable-resizable>
Copy the code
This is an extra layer of drag and drop logic in the drag container, we only need to implement the inner “component” logic.
Setting component Properties
Once you have the component on the canvas, you can also edit the component in the edit area on the right, such as the size, background, border, etc. The TextWidget has a style property, and the size changes this property.
So what we’re going to do is when we click on this component, we’re going to make it active, and then we’re going to have the property editor on the right side, and we’re going to have different Settings for different components, for example, TextWidget needs a font size, a font color, but if we’re going to make a rectangular component, Setting items are definitely different from TextWidget, so we need to implement property editors for each component separately, and then determine the type of component currently in the active state and invoke different property editors based on the type.
Copy the components
Editor is one of the most essential function is to copy component, copy the components are divided into “copy”, “paste” two steps, when copying all the components in the current configuration, and paste when you take out this configuration, create a component with this configuration, add to the posterItems, here is a simplified code:
// Copy the component
const mutations = {
[MTS.COPY_WIDGET](state, item) {
const config = _.cloneDeep(item)
state.copiedWidgets = config
}
}
Copy the code
export default class CopiedWidget extends Widget {
constructor(config) {
config._copyCount += 1
const configCopy = Object.assign({}, _.cloneDeep(config), {
typeLabel: config.typeLabel + '-copy'.isCopied: true
})
super(configCopy)
}
}
Copy the code
Then you just need to state. PosterItems. Push (new CopiedWidget(state.copiedWidgets)) to paste.
Automatic adsorption
Here I say my implementation ideas, the code is not posted, because more, interested partners can view the source code.
In fact, the idea is very simple, the component in the X-axis direction corresponds to the “up”, “middle” and “down” three lines, in the Y-axis corresponds to the “left”, “middle” and “right” three lines:
Suppose there are two components A and B, now in drag component B, drag in the process, we need to real-time monitoring of B left right three sides and A is enough to close, will get B on the left and the left right comparison respectively, then take B side and A left right, get B on the right and left the centre-right contrast, contrast A total of three rounds, For example, the left side of B is 5 pixels away from the right side of A. In this case, you can manually change the X coordinates of B to align B with A:
Correspondingly, the top, middle, and bottom three edges are also compared separately, and if they match, change the Y coordinate of B.
Github is not the only one with two components, but also supports custom guides to align with the edges of the canvas.
conclusion
This article may be brief in some places, because it is not a tutorial type, but a share of previous projects, so it is brief. If you have any ideas, or want to know the specific implementation details of which function points, please feel free to communicate with me.
If you think this project is good, please give it a thumbs up. Thank you