用数据驱动框架Vandf构建大纲编辑器
对于常规的开发框架,采用html的contenteditable构建编辑器,难度是很大的。而采用数据驱动框架Vandf可以轻松的构建一个大纲编辑器,它的优势在于通过数据驱动,可以大大简化编辑器的复杂度,可控性很强,且流畅程度极好。vandf的说明文档见vandf - npm (npmjs.com),示例代码见:vandfexample - npm (npmjs.com)
1.定义大纲编辑器
import {Van} from "vandf"//导入组件库
class Outliner extends Van {//继承Van
constructor (id) {
super(id).view(OutlinerView)
//创建用于存放大纲的结构的容器组件
let container = this.kid('outliner-container')
//创建条目子组件
MainContainer.new('main-block',container.id)
//创建子条目容器组件
ChildContainer.new('children-block',container.id)
}
}
//定义大纲组件视图模型
class OutlinerView {
constructor(){
this.$editor = undefined;
}
create (node) {
//鼠标划出编辑区域时,取消编辑,使可以选择大纲所有区域。
node.on('mouseover', (e) => {
if(e.rawEvent
&& e.rawEvent.buttons === 1
&& this.$editor
&& e.first !== this.$editor
){
this.$editor.attr('contenteditable', undefined)
this.$editor = undefined;
node.update()//通过update来实现取消“contenteditable”属性生效
}
})
//鼠标按下切换编辑实现编辑条目
.on('mousedown', (e)=>{
let editor = e.first;
if( editor != this.$editor){
if(this.$editor){
//取消焦点,变成非编辑状态
this.$editor.attr('contenteditable', undefined)
}
editor.attr('contenteditable', true);
editor.update();//通过update来实现设置“contenteditable”属性生效
this.$editor = editor;
}
})
}
}
2. 定义条目组件
class MainContainer extends Van {
constructor(id){
super(id)
//这里可以定义条目头部的小圆点,拖动控制点等
//BlockControlWrap.new('control-wrap', this.id)
//创建条目内容显示组件
BlockContent.new('content-block', this.id)
}
}
//定义条目内容显示组件
class BlockContent extends Van {
constructor(id){
super(id)
this.view(ContentView)
}
}
//定义条目内容组件的视图模型
class ContentView {
constructor(){
this.$isInputZh = false;//判断输入是否为输入法输出的文字
}
create (node) {
node
.style('outline','none')
.on('mousedown',(e) => {//注册鼠标按下事件
}, true)
.on('mouseover',(e) => {//注册鼠标划过事件
},true)
.on('click',(e) => {//注册鼠标单击事件
node.element.focus()//鼠标单击松开后获取焦点
},true)
.on('compositionstart', (e) => {//输入法开始输入
this.$isInputZh = true;
},true)
.on('compositionend', (e) => {//输入法输出
this.$isInputZh = false;
//触发内容变化事件
node.emit("valuechanged",node.html())
},true)
.on('input', (e) => {
//输入英文数字等非输入法输出的文字
if(this.$isInputZh){return}
//触发内容变化事件
node.emit("valuechanged",node.html())
}, true)
.on('keydown',(e) => {
e.rawEvent.stopPropagation();
//按下回车符
if(e.rawEvent.key === 'Enter'){
e.rawEvent.preventDefault();
//触发回车事件
node.emit('enter')
}
},true)
}
}
3.定义子条目容器组件和视图模型
class ChildContainer extends Van {
constructor(id){
super(id)
this.kid('left-border')//子条目容器中同级大纲左侧的线条
let container = this.parent();//大纲容器组件'outliner-container'
this.kid('repeat-block')
.repeat(container)//在条目子容器中,根据数据递归渲染'outliner-container'
}
}
4.使用Outliner组件
import {Van} from "vandf"//导入组件库
import Outliner from "../comp/outliner"
let app = Van.new('app')
let editor = Outliner.new('outliner-editor',app.id)
.supply(data)//提供数据
editor.select('outliner-container')//选择大纲容器组件
.view(OutlinerView)//注册用户视图模型
.model(OutlinerRepeatModel)//注册用户数据模型
editor.select('repeat-block')
.model(OutlinerRepeatModel)
editor.select('content-block')
.view(ContentBlockView)
.model(ContentBlockModel)
app.attach(document.querySelector('#app'));
5.定义用户视图模型和数据模型
//定义大纲用户视图模型
class OutlinerView {
create (node,model,states, config) {
node.on('enter',(e)=>{
e.stopPropagation();
//回车时创建新条目,并通过配置记录光标将移动到新条目
config.focus = model.createChild(node.serial);
//刷新同级条目
node.parent().update()
})
}
}
//定义大纲重复条目的数据模型
class OutlinerRepeatModel{
#data;
supply (datum) {
this.#data = datum?.children || [];//获取子条目数据
return this.#data;
}
insertChild(item, index){//插入子条目
let selfItem = this.#data[index];
if(selfItem?.children){
selfItem.children.splice(0, 0, item)
}
else{
this.#data.splice(index + 1, 0, item)
}
}
createChild (index) {//创建新条目
let item = {content:""};
this.insertChild(item, index);
return item;
}
}
//定义条目内容数据模型
class ContentBlockModel {
#data;
supply (datum) {
this.#data = datum;
return datum
}
get value () {
return this.#data?.content
}
set value (value) {
this.#data.content = value;
}
}
//定义条目内容用户视图模型
class ContentBlockView {
create (node, model) {
//输入内容变化时更新数据模型
node.on('valuechanged', e=>{
model.value = node.html()
})
}
render (node) {
//用html方式显示内容
node.html(d=>d?.content)
}
done (node, model,states, config) {
if(config.focus === node.datum){
console.log("光标进入到新建条目",node.datum)
}
}
}
6.准备数据
const data = [
{
content:"铭记历史,展望未来",
children:[
{
content:"中华文明是四大文明中唯一存续的文明",
children:[
{
content:"其它文明为什么会断流呢?",
children:[
{content:"不曾统一的文字"},
{content:"没有长期稳定统一的国家"},
]
},
{
content:"中华文明存续的基础是什么?",
children:[
{content:"统一的文字,不断演变却不曾断流"},
{content:"文字在这片土地上传承"},
]
}
]
},
]
}
]
7.tailwind样式
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply h-screen w-screen
}
#app {
@apply h-full w-full bg-gray-100
}
.main-block {
@apply relative;
}
.children-block {
@apply relative ml-8
}
.left-border {
@apply z-0 w-4 -left-6 top-0 h-full cursor-pointer bg-clip-content bg-transparent absolute opacity-60 border-r-2
}
.block-content-wrapper {
@apply min-w-full select-text min-h-[24px] mt-0 pr-[6px] overflow-x-visible
}
.content-block {
@apply max-w-full min-h-[32px] whitespace-pre-wrap break-words cursor-text p-0 opacity-40 text-lg;
}
6.总结
1.用Vandf开发的组件具有优秀的解耦能力。可以将组件模型、视图模型、数据模型解耦的基础上实现有机互动。
2.组件开发与组件使用两者具有极高的独立性,组件开发关心组件的行为模式,提供交互事件,而组件使用时关心数据如何通过视图模型和数据模型进行展示和互操作。