精华内容
下载资源
问答
  • 2021-04-27 14:54:14

    在这里插入图片描述
    这是一个Vue+element ui的后台管理项目。如上图所示这里有一个修改的对话框里边是表单和ckeditor5富文本编辑器,点击第一个更新按钮打开对话框后,表单和编辑器获取并显示当前可以修改的内容。但是当我关闭对话框再次点击其他更新按钮的时候打开的编辑器内容都是第一次打开对话框中编辑器的内容,除非我手动刷新一下浏览器。很显然在对话框关闭的监听事件中没有对富文本编辑器的内容进行清除更新才造成了这种现象。我用的是ckeditor5富文本编辑器封装的组件,试了很多方法都没办法把富文本编辑器中的内容清除更新,最后没办法只能另辟蹊径对el-dialog对话框做处理。如下图代码:
    在这里插入图片描述
    我在el-dialog中加入了v-if="editDialogVisible"绑定的是控制对话框显示隐藏的字段,editDialogVisible在data中定义为false,这样就使得对话框在关闭时被销毁再点开时又被重新渲染包括里边的数据。element ui中也有destroy-on-close销毁对话框的方法,但不知为何我试了对我的编辑器没用。虽然用v-if能达到我想要的效果(如果不考虑性能的话)。但v-if不是最优的办法,还是要对ckeditor5富文本编辑器的内容做清空刷新。

    更多相关内容
  • 这个Tinymce富文本编辑器是vue-element-admin内集成好的,使用过后体验非常不错,很简单易用。这里分享一下,同时又看到了网上帖子都没什么人写前后端同时展示的,很多人想知道编辑器编辑的文章格式展示在前端的。...

    前言:

    这个Tinymce富文本编辑器是vue-element-admin内集成好的,使用过后体验非常不错,很简单易用。这里分享一下,同时又看到了网上帖子都没什么人写前后端同时展示的,很多人想知道编辑器编辑的文章格式展示在前端的。这里统一写一下前后端代码都展示,并配上注释,相信大家看起来会很清晰明了。

    效果图

    编辑文章的时候,我是直接百度搜了一个小说复制了一段直接粘贴来的,格式还可以自动保留
    在这里插入图片描述
    文章保存后,在页面展示格式也是存在的
    在这里插入图片描述
    编辑器样式:鼠标移上去都会有功能介绍,功能很全
    在这里插入图片描述

    安装步骤

    1,git下载vue-element-admin代码

    直接新建个文件夹,用这个命令下载下来。

    国内:
    git clone -b i18n https://gitee.com/panjiachen/vue-element-admin.git
     
    国外:
    git clone -b i18n https://github.com/PanJiaChen/vue-element-admin.git
    

    2,把这个Tinymce文件夹整个复制到你项目的components文件夹下面

    这是Tinymce的路径
    在这里插入图片描述
    复制到components文件下
    在这里插入图片描述

    3,用npm安装一下tinymce

    npm install @tinymce/tinymce-vue -S
    npm install tinymce -S
    

    用到sass没有的也安装下

    cnpm install node-sass -s
    

    4,使用方法:

    直接引入组件,使用就行
    在这里插入图片描述
    在这里插入图片描述

    设置中文

    刚下载好的编辑器是英文的,所以设置一下中文
    把这句话this.languageTypeList[‘zh’]替换上面注释的。

        language() {
          // return this.languageTypeList[this.$store.getters.language]
          return this.languageTypeList['zh']
        },
    

    在这里插入图片描述

    字体和字号设置添加

    刚下载的编辑器也是没有带字号和字体设置的。所以可以添加一下
    首先在initTinymce方法内添加这句话

    fontsize_formats: "8pt 10pt 12pt 14pt 18pt 24pt 36pt",
    

    在这里插入图片描述
    然后在toolbar内后面加入这两个词,toolbar就是配置项
    前面是字号,后面是字体的意思,注意每个词中间要有空格啊

    fontsizeselect fontselect
    

    在这里插入图片描述

    ----------------------好了,到这就设置完了,可以使用用看看了--------------------------

    代码(前端)

    这里和上面的使用方法一样啊,就是注册个组件放你要的位置就行了。content_first就是你输入的文章内容了

    <template>
      <div class="release_wrap">
                  <el-input
                    v-model="title"
                    placeholder="请输入标题"
                  ></el-input>
                  <tinymce v-model="content_first" :height="300" />
      </div>
    </template>
    
    <script>
    import Tinymce from "@/components/Tinymce";
    export default {
      components: {
        //富文本组件
        Tinymce,
      },
      data() {
        return {
          //富文本输入的值
          //内容
          content_first: "请输入内容",
          //标题
          title:""
        };
      },
    
    };
    </script>
    

    文章按照格式显示

    核心就是v-html。从后端把保存的文章拿出来,然后用v-html把文章展示出来就会有格式了,很简单

    <template>
      <div class="release_wrap">
        <div class="release_title">{{ title }}</div>
        <el-card class="release_card">
          <el-button
            type="primary"
            round
            icon="el-icon-arrow-left"
            style="margin-bottom: 40px"
            @click="jump_home"
            >返回</el-button
          >
          <div v-html="content"></div>
        </el-card>
      </div>
    </template>
    
    <script>
    
    import { mapState } from "vuex";
    export default {
      data() {
        return {
          //文章的标题
          title: "",
          //文章的内容
          content:''
        };
      },
      computed: {
        //引入vuex中state的变量,可以直接this.xxx调用到
        ...mapState(["articleIndex2"]),
      },
      created(){
        this.searchHtml()
      },
      methods: {
        jump_home() {
          this.$router.go(-1);
        },
        //请求后台,搜索信息拿到文章的数据
        searchHtml(){
          this.$axios.get("/Content/searchContentId",{
            id:this.articleIndex2
          }).then(res=>{
            this.title=res.contentTitle
            this.content=res.contentInfo
            console.log(res);
          })
        }
      },
    };
    </script>
    
    

    代码(后端)

    结构:

    在这里插入图片描述

    ContentData

    数据库实体类,存放数据库字段映射。

    package com.example.demo.entity;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    
    import java.sql.Timestamp;
    
    public class ContentData {
        private int id;
        private String entryName;
        private String contentTitle;
        private String contentInfo;
        //日期时间返回的格式规定
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
        private Timestamp createTime;
    
        public ContentData() {
        }
    
        public ContentData(int id, String entryName, String contentTitle, String contentInfo, Timestamp createTime) {
            this.id = id;
            this.entryName = entryName;
            this.contentTitle = contentTitle;
            this.contentInfo = contentInfo;
            this.createTime = createTime;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getEntryName() {
            return entryName;
        }
    
        public void setEntryName(String entryName) {
            this.entryName = entryName;
        }
    
        public String getContentTitle() {
            return contentTitle;
        }
    
        public void setContentTitle(String contentTitle) {
            this.contentTitle = contentTitle;
        }
    
        public String getContentInfo() {
            return contentInfo;
        }
    
        public void setContentInfo(String contentInfo) {
            this.contentInfo = contentInfo;
        }
    
        public Timestamp getCreateTime() {
            return createTime;
        }
    
        public void setCreateTime(Timestamp createTime) {
            this.createTime = createTime;
        }
    }
    
    

    ContentController

    给前端的接口写在这里

    package com.example.demo.controller;
    
    import com.example.demo.entity.ContentData;
    import com.example.demo.entity.EntryList;
    
    import com.example.demo.mapper.ContentMapper;
    
    import org.springframework.web.bind.annotation.*;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    @RestController
    @RequestMapping("/Content")
    public class ContentController {
        @Resource
        ContentMapper contentMapper;
        //保存词条,文章标题,文章内容
        @CrossOrigin(origins ="*",maxAge = 3600)
        @GetMapping("/setContent")
        public void findContent(@RequestParam("entryName") String entryName, @RequestParam("contentTitle") String contentTitle, @RequestParam("contentInfo") String contentInfo){
            contentMapper.save(entryName,contentTitle,contentInfo);
        }
        //搜索用于展示的表格,分页查询文章的标题等展示
        @CrossOrigin(origins = "*", maxAge = 3600)
        @GetMapping("/SelectContentList")
        public EntryList findContent(@RequestParam("currentPage") int currentPage, @RequestParam("pageSize") int pageSize, @RequestParam("searchValue") String searchValue,@RequestParam("entryName") String entryName) {
            if (searchValue == "") {
                //判断:如果searchValue内为空,代表输入框没有输入值,那就正常查询所有表格数据返回
                //当前页
                int cuIndex = (currentPage - 1) * pageSize;
                //调用数据库的方法返回的表格数据用数组保存
                List list = contentMapper.ContentAll(cuIndex, pageSize, searchValue,entryName);
                //存下当前页
                int currentPageIndex = currentPage;
                //存下每页数
                int pageSizeIndex = pageSize;
                //调用数据库表格总数方法返回的值存下来
                int totalIndex = contentMapper.ContentIndex();
                //new出要返回给前端的对象,不new出对象无法往里面赋值
                EntryList entryList = new EntryList();
                //往对象内list添加表格数据
                entryList.setList(list);
                //把当前页赋值进去
                entryList.setCurrentPage(currentPageIndex);
                //把每页数赋值进去
                entryList.setPageSize(pageSizeIndex);
                //把总数赋值进去
                entryList.setTotal(totalIndex);
                //对象的数据上面赋值都拿到了,返回整个对象给前端
                return entryList;
            } else {
                //在搜索框输入了值就走这边。
    
                //当前页
                int cuIndex = (currentPage - 1) * pageSize;
                //调用数据库的方法返回的表格数据用数组保存
                List list = contentMapper.ContentAll(cuIndex, pageSize, searchValue,entryName);
                //存下当前页
                int currentPageIndex = currentPage;
                //存下每页数
                int pageSizeIndex = pageSize;
                //调用数据库表格总数方法返回的值存下来,这里专门调用的查询对应条件的总数方法
                int totalIndex = contentMapper.ContentSearchIndex(searchValue);
                //new出要返回给前端的对象,不new出对象无法往里面赋值
                EntryList entryList = new EntryList();
                //往对象内list添加表格数据
                entryList.setList(list);
                //把当前页赋值进去
                entryList.setCurrentPage(currentPageIndex);
                //把每页数赋值进去
                entryList.setPageSize(pageSizeIndex);
                //把总数赋值进去
                entryList.setTotal(totalIndex);
                //对象的数据上面赋值都拿到了,返回整个对象给前端
                return entryList;
            }
        }
    
        @CrossOrigin(origins = "*", maxAge = 3600)
        @GetMapping("/searchContentId")
        //根据id查询对应文章
        public ContentData searchContent(@RequestParam("id") Long id){
            return contentMapper.searchById(id);
        }
    }
    
    

    ContentMapper

    后端链接数据库操作的文件

    package com.example.demo.mapper;
    
    import com.example.demo.entity.ContentData;
    import com.example.demo.entity.KnowledgeData;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    import java.util.List;
    
    public interface ContentMapper {
        //把知识点的数据存进数据库,参数:词条名,文章标题,文章内容
        @Update("INSERT INTO `content_data`( `entryName`, `contentTitle`, `contentInfo`) VALUES (#{entryName},#{contentTitle},#{contentInfo});")
        void save(@Param("entryName") String entryName, @Param("contentTitle") String contentTitle, @Param("contentInfo") String contentInfo);
    
        //搜索数据库分页对应数据,搜索数据库模糊查询(这里参数要这么写:like concat('%',#{searchValue},'%'))
        @Select("select * from content_data where contentTitle like concat('%',#{searchValue},'%') and entryName=#{entryName} limit #{currentPage},#{pageSize}")
        List<ContentData> ContentAll(@Param("currentPage") int currentPage, @Param("pageSize") int pageSize, @Param("searchValue") String searchValue,@Param("entryName") String entryName);
    
        //搜索数据库数据总条数
        @Select("SELECT count(*) FROM content_data")
        int ContentIndex();
    
        //搜索数据库数据条件筛选后的总条数
        @Select("select count(*) from content_data where contentTitle like concat('%',#{searchValue},'%')")
        int ContentSearchIndex(@Param("searchValue") String searchValue);
    
        @Select("select * from content_data where id=#{id}")
        ContentData searchById(Long id);
    }
    
    

    解释一下:因为我做了分页还有保存等操作,所以有三个接口,如果不需要的,只看@GetMapping(“/searchContentId”)的接口就行了。然后改改比如不用id根据别的来查询都可以。

    数据库接收文章,类型设置为text。varchar不够长,如果再不够就longtext最长

    在这里插入图片描述

    清空富文本编辑器内容方法

    刚下载的里面不包含清空方法,所以需要手动加一个。

    1,在编辑器文件内加一个clear方法,里面写上这句话

        clear(){
          //清空富文本的内容
          window.tinymce.get(this.tinymceId).setContent( '')
        },
    

    在这里插入图片描述
    2,在你的组件上加上ref绑定dom

                  <tinymce
                    v-model="content_third"
                    :height="300"
                    ref="knowledgeText"
                  />
    

    在这里插入图片描述
    3,操作dom调用子组件里面的clear方法清空数据

        setContent() {
          this.$axios
            .get("/Content/setContent", {
              entryName: this.entryText,
              contentTitle: this.form.title,
              contentInfo: this.content_first,
            })
            .then((res) => {
              this.$message({
                message: "业务内容发布成功!",
                type: "success",
              });
              this.form.title = "";
              //清空富文本编辑器内容
              this.$refs.contentText.clear();
            });
        },
    

    在这里插入图片描述

    后端接收前端发来的文章提示请求头太大,Request header is too large 报错处理

    问题原因:一个新增的接口,因为前端传入一段很长的JSON数组,导致了打印台报错
    错误描述:java.lang.IllegalArgumentException: Request header is too large

    请求头超过了tomcat的限值。本来post请求是没有参数大小限制,但是服务器有自己的默认大小
    那就修改服务器的大小

    普通tomcat
    在server.xml中
    处加上maxHttpHeaderSize =”102400”

    SpringBoot
    SpringBoot项目更方便了,在application.properties文件中添加

    server.max-http-header-size=102400
    
    展开全文
  • 富文本编辑器踩坑

    2020-03-07 13:45:27
    开发富文本编辑器的一些经验 以下是我在开发一个本业务场景下的富文本编辑器的一些经验: 在开源富文本编辑器的基础上开发 知乎上有个问题,叫做为什么都说富文本编辑器是天坑?,里面提到的很多开发富文本编辑器会...

    前端架构

    webpack配置

    Vue是一个非常优秀的前端MVVM框架,轻量、快速、文档友好又详细,代码组织也非常优雅,是我比较偏爱的MVVM架构。Vue官方提供了非常方便快速上手的脚手架Vue-cli,但是由于跟我们这边使用的Java Web架构有一些不太适合的地方,所以我并没有使用它,不过我也是对Vue-cli做了一番详细的学习后来搭建自己的webpack配置。

    下面是我的生产环境的部分webpack配置,其实并不复杂,因为我的业务场景也并不复杂,现在的各种插件功能也足够强大。

    webpack.prod.config.js

    devtool: 'source-map', plugins: [     new CleanWebpackPlugin(['dist']),     new ExtractTextPlugin('[name].css'),     new webpack.DefinePlugin({         'process.env': {             NODE_ENV: '"production"'         }     }),     new webpack.optimize.CommonsChunkPlugin({         name: 'vendor',         minChunks: function(module, count) {             return (                 module.resource &&                 /\.js$/.test(module.resource) &&                 module.resource.indexOf('node_modules') >= 0             )         }     }),     new webpack.optimize.CommonsChunkPlugin({         name: 'manifest',         filename: 'manifest.js',         chunks: ['vendor']     }),     new webpack.optimize.UglifyJsPlugin({         sourceMap: true,         compress: {             warnings: false         }     }), ]
    

    主要就是借鉴了Vue-cli中的code split思路,开发环境的webpack配置区别不大,只是sourcmap设置改为了devtool: ‘#cheap-module-eval-source-map’,去掉了代码压缩等。

    需要注意的一点是,我在生成环境下的webpack配置中使用了vue-loader附带的postcss预处理器中的cssnano插件进行css部分的代码压缩,但是这个插件打包时会将z-index:10压缩成z-index:1,需要添加设置zindex: false才能避免这个问题,而且cssnano插件默认还有一个特性就是会删除没有使用到的css部分,比如我们为CSS3动画所需构建的keyframes,居然也会被cssnano认为是没有被使用的css,压缩过程中也删掉了,这个就有点费解了,所以为了避免这种情况,我们需要增加设置discardUnused: false:

    webpack.prod.config.js

    rules: [{     test: /\.vue$/,     loader: 'vue-loader',     options: {         loaders: {             css: ExtractTextPlugin.extract({                 use: 'css-loader',                 fallback: 'vue-style-loader'             }),             scss: ExtractTextPlugin.extract({                 use: ['css-loader','sass-loader'],                 fallback: 'vue-style-loader'             })         },         postcss: [             require('autoprefixer')({                 browsers: ['> 1%']             }),             require('cssnano')({                 zindex: false,                 discardUnused: false             })         ],      } }]
    

    与Java Web的结合

    为了将css文件抽离出来,我在开发环境也没有使用Hot Module Reload机制(使用了ExtractTextPlugin抽离css文件后,修改css样式不能通过HMR自动更新,需手动刷新)。

    我们部门这边的Java Web除了一些简单的静态活动页,主要页面的承载页都会配置在另外的一个存放freeMarker的ftl文件的文件夹中,有别于静态文件的存放位置,这是部门中的Java Web一直沿用的文件结构,不好也没太大必要去改变它。

    这就使得Vue-cli或者一些常见的webpack配置中的根据文件hash生成打包文件再使用html-webpack-plugin自动注入承载页的功能不太好实现,所以就需要结合部门自己的情况定制比较符合自己项目的打包流程。

    我们有个网站应用自动部署平台,它的功能除了解析和编译后端工程代码,还会自动分析页面引用的静态资源,然后将资源的URL替换为对应的CDN域名的下的资源链接并添加资源MD5值相关的查询值后缀,比如/static/js/app.js会在自动部署后变成//yuedust.yuedu.126.net/snail_st/static/js/app.js?a63ed8a8。

    所以既然目前项目中已经有了CDN域名替换和文件hash计算的功能,我在webpack打包中就没必要再多此一举了,而且,我还可以利用这一特性,固定的设置承载页引用的静态资源的URL,部分代码如下:

    index.ftl

    <!doctype html> <html> <head>     <meta charset="utf-8">     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">     <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">     <link rel="shortcut icon" href="/static/images/favicon.ico" />     <title>蜗牛阅读-书评编辑</title>        <link rel="stylesheet" href="/static/bookreview/dist/app.css"> </head> <body>     <input type="hidden" id="csrfToken" name="csrfToken" value="${csrfToken!?html}" />     <div id="app"></div>         <script src="/static/bookreview/dist/manifest.js"></script>     <script src="/static/bookreview/dist/vendor.js"></script>     <script src="/static/bookreview/dist/app.js"></script> </body> </html>
    

    这样设置好后无论本地开发还是部署线上都不需要再修改ftl文件的内容了,既有效的利用到了Code Split加快打包速度和缓存利用率高的优点,也使得开发和部署变得简单,页面引用的静态资源一旦添加,就不需要再去更改路径了。

    当然,这只是结合自己项目的Java Web工程结构和特点设置的一套webpack使用方式,仅供参考

    开发富文本编辑器的教训

    由于项目的时间较紧张,我在页面上应用了Vue框架的背景下,想当然的想要把Vue也应用于富文本编辑器的开发,事实证明这是不太可行的。

    富文本中的数据渲染

    Vue是数据和展现双向绑定的,这使得特定格式的数据渲染成对应的html非常的方便。

    但是网页上的富文本编辑器普遍都是利用的是元素的contenteditable属性,这个属性是无法实现双向绑定的,要想实时保存富文本数据,只能监控元素的输入事件,然后读取元素的innerText后再去修改数据,但是一旦修改了数据,就会触发Vue的视图更新,导致你编辑元素的innerText被重新渲染,元素一旦被重新渲染,用户输入时的获取的光标焦点就消失了,而且在windows和mac os下的输入法实现有些不一样,mac下的输入法输入中文会先将用户输入的拼写填充到输入元素中,导致获取的innerText不准确,所以想要利用Vue的数据双向绑定机制来开发富文本部分,又想要实现数据的实时保存,存在很多问题。

    富文本中的不可编辑区域

    我们的书评内容的数据结构是一个各种item类型组成数组,item的类型有:文字、图片、书籍和笔记,富文本编辑器需要将这些数据展现出来并且可编辑,其中书籍和笔记的数据结构只能添加或者删除,而不能修改,这就与传统的富文本编辑器存在一定的区别,即富文本编辑器区域需要插入或者删除不能修改的元素。这个需求使得一个普通的富文本编辑器变得特殊起来,一开始我的思路是在contenteditable="true"的编辑器主体内插入contenteditable="false"的dom结构,这导致插入部分的文本无法与编辑器很好的交互,包括删除、撤销、选中等,最后找到了另外一种比较理想的解决办法。

    开发富文本编辑器的一些经验

    以下是我在开发一个本业务场景下的富文本编辑器的一些经验:

    在开源富文本编辑器的基础上开发

    知乎上有个问题,叫做为什么都说富文本编辑器是天坑?,里面提到的很多开发富文本编辑器会遇到的一些难点,而我的第一版也是想着自己从头开始开发,但是的确碰到了很多没想到的问题,修修补补最终结果还是不满意。

    所以如果是需要一个常规功能的富文本编辑器,尽量选择成熟稳定的开源项目,保证稳定可靠,如果需要像我一样开发一个符合特定业务场景的富文本编辑器,也尽量在开源项目的基础上进行二次开发,这样虽然会有一些代码冗余,但是能帮助你避开许多前人已经踩过的坑,而且也能从阅读这些项目的源码中学习到不少忽视的知识和特性。

    我选择的是国内的一个个人开发者维护的叫做wangEditor的项目,它比较轻量,源码也比较清晰便于二次开发。

    基于DOM的数据渲染

    要想在WEB端实现富文本编辑,经过我踩的一些坑,我觉得最终还是要回归于DOM的,Vue或者其他MVVM框架确实给开发和维护带来很大的遍历,但是在富文本编辑这块,还是没有DOM API来的可控。我的方案是根据服务端提供的一篇书评的items,组织出相应的HTML,然后再交给富文本编辑器进行初始化。

    基于浏览器的document.execCommand API进行开发

    当一个HTML文档处于设计模式(designMode)或者一个HTML元素设置了contentEditable="true"时,我们可以使用execCommand方法,运行一些命令来操纵可编辑区域的内容,这个API可以快速可靠的对富文本区域的选区内容进行一系列的操作,最关键是,支持撤销和重做功能,并且在撤销和重做的过程中能够完美的保持选区的状态,这一点非常重要,我们可以通过保存html来实现内容的撤销和重做,但是选区或者说光标的撤销和重做,用Javascript很难完美的控制,如果只是保存之前选区的range对象,是不能复原选区或者光标的。

    具体支持的API可以参考MDN的文档。

    即使对于一些文档中不支持的API,也建议通过以上API来组合实现,比如一段HTML内容的替换,应该先通过Javascript建立相应的选区,然后运行delete命令删除该段内容,再通过insertHTML来插入所需的HTML,这样才能充分的利用浏览器的撤销和重做功能,并且与其他的操作串联起来。

    富文本中的换行

    富文本编辑器中的换行是一个值得注意的问题,我在开发书评编辑器的时候,遇到了一些问题:

    富文本中展示换行看起来很容易,有几个方案,比如设置CSS的white-space再配合换行符,或者在DOM中添加
    元素,看起来都能达到目的。但是书评编辑器特殊的地方在于,这是一个已经制定好了数据结构并且在客户端上也有编辑器,这就涉及到Web、iOS、Andorid三个端的一致性问题。

    • 因为在客户端上是没有
      概念的,客户端编辑器上需要换行位置插入的都是回车符,也就是\n,而这些换行符在WEB上如果需要显示成换行,就需要设置white-space为pre或者pre-line

    • 如果设置为white-space: pre;,确实可以原样显示文本换行,但是如果是这样一条数据:
      在这里插入图片描述
      这是书评中的一条文本数据,其中有两个换行符,代表要展示成三行,其中有一个空行,实际需要展示的效果是下图这样的:
      在这里插入图片描述
      这样的数据如果要展示在一个DOM节点中,设置为white-space: pre;,换行虽然保留了,但是由于第一行数据是连续的,white-space: pre;原样保持了数据的换行,导致了第一行超出了DOM的最大宽度,这样的方式显然就行不通了。

    在这里插入图片描述

    • 如果设置成white-space: pre-line,pre-line可以在正确显示换行符的同时让超出一行的文字自动换到下一行,看起来很完美。但是,一旦在换行符之后(比如中间空的那行)输入文字,问题又出现了,在white-space: pre-line的元素中,如果在换行符之后输入文字,换行符会被删除,文字将会跳动到上一行继续显示,这样显然是不行的。

    • 最终的方案只有剩插入
      元素来实现换行了,通过
      实现的换行,不会出现输入文字换行失效的问题,也不需要父元素设置white-space: pre;,所以我们需要将客户端在文本中插入的\n转换成
      ,最后把HTML结构重新解析成书评数据的时候,又需要将它们转换回来以便保证客户端编辑和展示的一致性,当然这中间还有一系列的转换逻辑,包括针对客户端老版本的编辑器的一些BUG做的兼容,最后为了实现一致还是废了一番功夫的。

    富文本中的不可编辑区域

    如上面两图,我们的书评中有一部分内容是用户引用的某一本书籍、或是用户在阅读时记录的书籍原文,这些数据结构都是不能被修改的,只能插入或者删除,一开始我的思路是把该部分DOM结构设置为contenteditable=“false”,但是这样的设置代码上不管怎么去弥补体验上都不够好。

    后来我转变了思路,既然这就是一段不可编辑只能观看的DOM,而富文本编辑器里插入的图片是能够很好的与文字一起被很好的操作和维护的,那么为什么不把不可编辑的展示区域直接转换为图片插入到富文本区域呢,事实证明这个思路最后的体验非常好,除了一个小的技术问题,下面一点会说明。

    将DOM转换为图片

    要将一个DOM转化为图片,社区里已经有不少很成熟的开源库可以使用,比如我使用的是dom-to-image,需要注意的就是一个问题:DOM转化为图片,基本都利用到了canvas的toDataUrl()功能将图片转化转化为base64编码的URL,这里面有一个安全策略,就是如果canvas中绘制的DOM结构中有图片,而该图片与当前页面的域名不一样(这在我们的开发场景中很常见),出于安全策略的限制,此时浏览器是不允许调用canvas的toDataUrl()方法的,而我们的书籍卡片中必定会有书籍的封面,该封面的域名是我们的CDN域名,所以转换成图片被限制了。

    在这里插入图片描述
    要想解决这个办法,就涉及到一个前端的IMG标签的属性:crossOrigin,如果将这个属性设置为anonymous,浏览器就会为这张图片的请求的Request Headers 中附带Origin为当前域名的这一行信息,告诉图片所在的静态资源服务器,这张图片我需要跨域访问以及我的域名,请在图片的Response Headers中附加Access-Control-Allow-Methods和Access-Control-Allow-Origin这两行信息,如下图:
    在这里插入图片描述
    这样请求得到的图片渲染到canvas中,浏览器才不会限制该canvas转化为base64的URL。

    这一特性需要服务端的支持,有的服务端就算附加了这个Request Headers字段依然不会返回想要的Response。

    但是在支持这一特性的服务端,有时候设置了crossOrigin="anonymous"依然显示这个错误,不是这个属性没生效,而是我们的图片一般是存放在CDN上的,而CDN为了更快的返回用户的请求,会把图片的响应缓存下来,而这些缓存下来的响应显然是没有Access-Control-Allow-Methods和Access-Control-Allow-Origin这两行信息的,所以这时候即使我们认为自己的请求包含了crossOrigin=“anonymous”,CDN服务器不认为这是一个不同的请求,所以返回给我们的响应是之前就缓存好的,导致了这个问题的发生。

    这种情况就需要我们为我们请求的图片URL后添加一个时间戳来避免CDN服务器的缓存。

    避免使用CDN来提高渲染速度

    前端开发中说到提高页面的加载速度,一般都会提到最大限度的利用CDN缓存静态资源,以提高静态资源的访问速度,从而更快的将网页内容呈现给用户。

    但是,我上面提到的将含有跨域CDN图片的DOM节点渲染成图片的情况下,向CDN代理节点请求图片资源反而会比我们直接向静态资源源站点请求要来的慢,其实这也很好理解:

    • 为了将含有跨域CDN图片的DOM利用HTML5``canvasAPI渲染成图片,我们就需要为该图片的添加crossOrigin="anonymous"属性,并且为图片的请求URL添加一个时间戳

    • 如果我们访问的是CDN域名下的图片,同时又为URL添加了一个全新的时间戳,那么这个图片资源的请求对于CDN代理节点来说肯定是全新的,也就是会认为本节点上没有这个资源的缓存

    • CDN代理节点遇到一个自己没有缓存的资源,它就会向静态资源的源站点去请求,得到结果后再转发给用户,这等于说我们这个带有时间戳的图片URL的请求,不但没能利用的CDN的缓存提速,反而由CDN代理节点充当了一次中介,这显然会增加资源的返回耗时

    在这里插入图片描述
    在这里插入图片描述
    上面两图分别就是请求CDN域名图片的耗时和请求源站点图片的耗时,经过多次测试,可以发现请求CDN域名图片的耗时基本在200ms以上,而向源站点的请求基本都在100ms以下,所以,有的时候,比如这种特殊情况下,请求CDN域名下的资源可能反而会增加请求的耗时。

    Promise大法好

    根据上面提到的流程,需要我把从服务端拿到的一个包含各种类型item的数组解析成一个HTML字符串,其中包含了书籍和笔记类型的item需要转化成的base64格式的图片,这就出现了时序上的问题:

    文本和图片类型的item,可以直接得到对应的HTML字符串,而书籍和笔记类型的item,则需要通过网络请求和canvas转换,但是最终我又需要得到整个的初始HTML内容来初始化富文本编辑器,然后再让用户可以去在这些HTML DOM节点上进行编辑,这就需要用到Promise.all这个API了,代码示例如下:

    App.vue

    /**  * 将服务端返回的书评items转换为html string传输给富文本编辑器  * @param  {json array} items 书评items  * @return {promise}       所有items处理好后返回resolve(htmlStr), 否则reject(error)  */ convertItemsToHtml(items){     return new Promise ( (resolve, reject) => {         let htmlStr = '';         let itemStr = '';         let itemPromises = items.map( item => {             return new Promise( (resolve, reject) => {                 switch(item.resourceType){                     case 'Text':                         itemStr =  `<p>"Text">${item.text}</p>`;                         resolve(itemStr);                         break;                     ...                     case 'BookNote':                         let $BookNoteEle = $(`<div>${item.bookNote.markText}</div>`).appendTo($('body'));                         domtoimage.toPng($BookNoteEle[0], {style: {opacity: 1, zIndex: 1}})                             .then(function (dataUrl) {                                 itemStr =  `<p>"BookNote"><img >"BookNote" >'${escape(JSON.stringify(item))}' src="${dataUrl}"></p>`;                                 $BookNoteEle.remove();                                 resolve(itemStr);                             })                             .catch(function (error) {                                 console.error('图片生成失败', error);                                 reject(error);                             });                         break;                 }             })         })         Promise.all(itemPromises).then( ([...itemStrs]) => {             htmlStr = itemStrs.reduce( (acc, val) => {                 return acc + val             }, '');             resolve(htmlStr);         }).catch( (error) => {             reject(error);         })     }) },
    

    利用Promise.all和其他一些ES6的特性,可以使我们的代码变得更加强大而简洁。

    以上就是我在开发特定业务需求的富文本编辑器中遇到的一些问题和总结的一些经验,可能会有一些错误,希望帮忙指正。 其他一些常见的富文本编辑中会遇到的问题,可以通过学习一些开源的成熟富文本编辑器项目来得到解答。

    原作者:网易云社区
    链接:https://www.jianshu.com/p/193395d310b6
    来源:简书

    展开全文
  • 本文主要分享基于Angular和Slate开发富文本编辑器的实践历程,基于Angular做编辑器对我们来说也是一个新的尝试,社区关于Angular编辑器的实现更多的是基于原生编辑器组件化包装(比如基于Quill、Prosemirror的...

    作者:杨振兴
    Worktile 前端工程师,PingCode Wiki 产品技术负责人

    PingCode Wiki 提供结构化知识库来记载信息和知识,便于团队沉淀经验、共享资源,欢迎大家注册试用

    本文主要分享基于Angular和Slate开发富文本编辑器的实践历程,基于Angular做编辑器对我们来说也是一个新的尝试,社区关于Angular编辑器的实现更多的是基于原生编辑器组件化包装(比如基于Quill、Prosemirror的Angular组件),我们借助Slate编辑器框架,实现基于Angular的视图层,这意味着编辑器的交互事件、数据渲染全部交给Angular处理,写编辑器功能就是写基于编辑器业务的Angular组件,所以我觉用Angular写编辑器是体验很高、很有意思的一件事情。

    一、编辑器业务

    特点
    内容多样
    交互多样

    富文本要支持的内容是多样的,再加上跟用户的交互方式也是多样性的,比如普通输入、粘贴、拖拽等,所以编辑器的业务是复杂且难治理的,所以编辑器也一直被认为是天坑的存在

    传统实现

    1. 操作DOM
    2. 输出HTML字符串

    传统编辑器的开发主要是依赖浏览器原生的编辑能力,直接操作DOM修改富文本内容,输出的结果是HTML字符串,这其实是非常混乱的,随着编辑器支持特性的增多,代码的拆分和富文本数据的维护会越来越麻烦

    数据流:
    在这里插入图片描述

    这种技术实现过于单薄,编辑器底层实现和功能拓展没有明确的边界,代码的理解成本可能会越来越大

    结合Angular实现

    编辑器是前端框架的一个绝佳应用场景,因为编辑器足够复杂,引入主流的编程思想或者框架可以有效拆解编辑器的复杂度,比如:

    1. 引入抽象的数据模型描述富文本数据
    2. 使用状态管理的思想统一编辑器的数据流
    3. 使用前端框架编写编辑器的交互界面

    所以我觉得引入主流的编程实践和前端框架到编辑器中是有很大的进步意义的,并且引入框架其实可以很大程度上降低编辑器的开发难度,让编辑器的开发更大众化

    使用Slate和Angular开发编辑器

    Slate大概是在2016年开源的一款编辑器框架,从开源开始到现在一直保持着架构的创新性,底层不对编辑器的功能做任何的预设,抽离出了独立的视图层(让Angular开发编辑器成为可能),并且底层的所有代码已经全部用TypeScript重写,是非常有潜力的编辑器框架

    Slate数据的抽象

    在这里插入图片描述
    有了对数据的抽象,结合Angular来做编辑器就变得很容易,每一种数据都可以转换成由特定的Angular组件来渲染

    对应状态流转

    在这里插入图片描述

    当然也有人认为编辑器引入前端框架是错误的,它会让编辑器的性能变差,不可否认引入框架会有一些性能损失,但从我的实践来看编辑器整体性能表现还可以,并且还可以根据框架特点做进一步优化

    二、职责划分

    Slate 职责

    • 提供编辑器内核实现,定义数据模型(把富文本数据抽象成对象模型)
    • 提供数据管理逻辑,基于不可变数据模型,定义底层的数据变换方法

    Angular 职责

    由于官方只提供基于React的视图层实现(插件机制、内容编辑代理机制),所以要想在Angular下开发编辑器,必须要实现一个基于Angular的视图层

    • 使用Angular提供视图层的实现
    • 插件逻辑加Angular组件的形式拓展编辑器功能,视图层本身不包含任何编辑器功能
      在这里插入图片描述
      扩展一个编辑器功能(Alert)

    介绍下使用Angular开发Alert(提示框)功能的一个流程
    在这里插入图片描述
    1、定义节点类型
    在这里插入图片描述
    type是一个唯一标识,每一个块级节点都有一个唯一的数据类型
    alertType是alert插件独有的,标识提醒类型
    它有四种取值:

    export enum ThyAlertType {
    success = ‘success’,
    warning = ‘warning’,
    danger = ‘danger’,
    info = ‘info’
    }

    children定义子节点

    2、开发组件

    开发一个Angular组件,显示该数据类型的UI
    组件效果:

    在这里插入图片描述
    对应的组件模板:

    在这里插入图片描述
    虽然是写Angular组件来实现可编辑内容的显示,但是与写普通的Angular组件还是有一些差别的,主要有两点:

    1. 使用给子节点占位,这个需要根据需求把它放在正确的位置
    2. 因为整个组件是在可编辑区域内渲染的,所有要在指定的标签上增加contenteditable="false"让该区域不可编辑,避免造成焦点错乱。

    可以看出来,在编写节点组件的时候我大量使用thy前缀的组件或者样式类,所以一些通用的UI我们是可以直接使用组件库来做的(thy其实是我们自己组件库的前缀)。

    3、实现交互操作

    Alert对应的交互操作其实有两类:

    1. 创建/删除Alert节点(可以是在空白区域创建,也可以是选择一个段落创建)
    2. 切换Alert节点的提示类型

    为创建节点相对比较复杂,这里只介绍比较简单的提示类型的切换。

    重新看下前面的模板,工具栏中的操作icon都绑定了mousedown事件,对应的处理函数是switchType,看下处理函数的实现:
    在这里插入图片描述
    第二个参数是单击的alertType的新值,然后通过Slate提供的Transform方法直接修改该节点对应的属性,当编辑器数据发生变化,框架会自动刷新UI,实现数据的驱动。

    总结
    扩展一个编辑器插件或者支持一种新的数据类型只需要简单的三步就完成了,跟开发普通业务功能差不多了,同样只需要修改数据,同样可以使用组件库进行UI实现,开发体验已经非常的好了。

    三、Angular视图层机制

    通过Alert插件的实现,大家了使用Angular扩展编辑器功能的一个基本体验,这种开发体验的背后其实是需要视图层的支持的,下面我会针对组件渲染、组件受控、性能优化等方面介绍下Angular视图层内部的实现机制

    组件渲染

    Slate把富文本数据抽象成节点树,不同的数据类型有不同的层级,理论上还可以互相嵌套,比如表格可以嵌套列表,那么最终的节点层级就达到了六级,可以看下嵌套的节点具体长啥样

    提示框
    在这里插入图片描述
    表格

    在这里插入图片描述
    每一个块节点必须有一个children字段,节点理论上可以无限嵌套,只要保证叶子节点是text就行
    所以视图层肯定要实现这些节点的对应组件的渲染,并且每个节点类型对应的UI组件需要支持由外部提供,是由运行时决定的。
    在这里插入图片描述

    组件渲染的一个结构图,所以要实现嵌套渲染、组件动态配置还是有些复杂度的

    模板递归

    目前我们是通过模板递归的方式显示数据的渲染,核心是依赖Angular的ng-template,大致思路如下:

    1. 为一种类型的数据开发一个组件
    2. 把编辑器支持的有限组件先转换成模板定义
    3. 每个节点除了定义它自身的组件外还要定义子节点的模板插槽,因为子节点一定是一个数组,每一项有可能还是块节点,也有可能是Text节点(叶子节点),如果是块节点则会继续找它的模板定义,继续使用ngTemplatOutlet渲染,如此下去就是实现了递归渲染,如果遇到Text节点则直接使用特定的Text组件渲染,结束递归
    4. 视图层会根据插件提供的renderElement函数获取该类型指定的模板定义,然后直接使用ngTemplateOutlet渲染

    模板定义:
    在这里插入图片描述
    视图层调用模板定义

    在这里插入图片描述

    关键点在于baseChildren,它是子节点的渲染模板,并且如果子节点项仍然是块级节点,它会继续把baseChildren作为context中的childrenTemplate参数传递给模板定义,如此就在模板层面实现了递归调用

    renderElement实现

    在这里插入图片描述
    组件递归

    其实我们是使用动态创建组件的方式来做的,虽然也能实现需求,但是有以下几个问题:

    1. 需要操作DOM把子节点放到动态创建组件的指定节点下,非常low
    2. 因为要往动态创建组件的指定节点下移动DOM,所以我需要在组件创建完成后立即调用detectChanges,这样一来组件原本的生命周期就会被破坏了,这给写插件带来了一些麻烦
    3. 第三个是使用动态创建组件后无法实现组件参数的自动更新,比如节点属性修改了,我只能通过给组件实例重新赋值的方式来通知组件

    递归的理解可能稍微有点复杂,总之目前的做法是递归逻辑完全在模板中实现,数据的更新完全依赖Angular的检测机制,既实现不同数据使用不同组件渲染,也不用使用动态创建组件。

    组件受控

    虽然组件是由插件提供,但是我们还是需要对这些插件组件有一些基本的控制的,比如在组件渲染的根节点上增加统一的样式类,增加固定的属性,并且还要维护每一个数据节点与渲染的DOM的一个Map关系。
    我们是目前是通过在视图层提供组件基类,通过所有插件组件统一继承这个基类,来实现这个需求的,看下基类的定义:

    提供init、destroy、setContext三个函数的实现:

    在这里插入图片描述
    然后规定所有的渲染组件都必须要在相应的声明周期中调用这三个函数
    init、destroy 分别在组件的ngOnInit和ngOnDestroy时调用
    setContext 在当节点数据更新时调用

    Alert组件的示意

    在这里插入图片描述
    性能优化

    下面介绍下我基于Angular框架所做的一些性能优化

    OnPush
    OnPush是基本的,从使用Angular开发编辑器开始,视图层以及编辑器插件层所涉及到的组件基本都是OnPush模式,整个页面的变化检测不会传递到编辑器组件内部,这样基本保证编辑器组件不会拖累整体页面的性能。
    
    NgZone
    编辑器内部是需要监听很多DOM事件来监控内容输入的,比如keyup、click、beforeinput事件族等,在快速输入的场景,如果每一次事件触发都触发变化检测,系统性能必然会受到影响,所以编辑器内部的所有事件监听都是运行在runOutsideAngular之下,减少输入过程中不必要的变化检测开销。

    在这里插入图片描述
    trackBy
    因为组件的渲染是其实是循环递归的模式,通常情况下一个节点内容的变更不会影响其它节点,所以可以通过trackBy进行进一步的优化

    在这里插入图片描述
    我这边的处理办法是为每一个节点都生成一个唯一key,底层根据这个key值进行trackBy,通常情况下key值不变化,组件只需要刷新状态即可,不需要组件的销毁和重建。
    在这里插入图片描述
    首次加载

    在这里插入图片描述
    从测试的结果看,中等偏大(6000字)一些的文章还是可以实现秒级渲染的

    输入流畅度

    在这里插入图片描述

    指标:从输入一个字符开始,到这个逻辑执行完毕和DOM刷新完成所花费的时间,60ms以下基本很流畅
    所以除了特别大的样本有一些卡顿外,整体测试都是非常流畅的

    踩过的坑

    其实回顾使用Angular编辑器的过程,还有有遇到过一些坑的,这里分享几个跟Angular关联比较大的点:

    comment 意外删除

    Angular的模板语法会生成很多的comment节点,这些comment节点影响着模板内容的刷新,而在编辑器中整个组件的渲染都是可编辑器内的,所以在中文输入的某些情况下会造成comment节点被意外删除,然只要该节点数据再次变更,Angular就会把改模板对应的整个标签全部删除掉,然后造成内容意外丢失,编辑器失焦等等的现象。
    这个问题还是比较严重的,目前的解决办法是在节点旁边增加隐藏的span,阻止浏览器默认删除comment节点的行为

    中文输入性能问题

    这个问题是前一段时间刚遇到的,因为使用模板递归渲染导致的,使用模板递归的这种方式会造成每渲染一个节点会都生成很多个comment,这些comment会造成中文输入非常卡顿,而且非常奇怪,这个问题只在Chrome下出现,Safari以及Firefox下都没有这个问题。
    所以当初断定这个性能问题应该跟编辑的JS执行没有关系,应该就跟浏览器的一些机制有关。
    这个问题也有点乌龙,目前的解决办法在模板渲染的最外层增加了一层自定义的组件标签(可能是减少了根节点下comment的的数量)

    在这里插入图片描述
    有了这层标签性能问题一下就解决了,非常神奇!

    依赖注入问题
    在这里插入图片描述

    这个问题主要出在表格插件上,我想实现每一个表格共享一套Angular的服务,我在最外层表格上配置provider,以为它的子组件比如theTr、theTd,都可以通过依赖注入访问到这个服务,但结果让我很意外,子组件根本取不到表格上提供的服务,所以考虑到可能是Angular依赖注链关系的维护可能是在组件声明的时候决定的,而按照我们模板定义的实现,声明的时候Table组件和Td组件并没有直接的父子关系,导致依赖注入链没有续上。
    最终的解决方案是,通过Map存储表格节点与表格渲染组件的这层关系,这样就可以通过节点的父子关系找到对应表格组件的实例,进而访问到表格所注入的服务了。

    总结

    了解Slate + Angular开发编辑器的基本情况
    通过Alert插件的实现,体验了扩展一个编辑功能都需要做那些事情
    通过组件渲染、组件受控、性能优化介绍了Angular视图层的核心机制

    作者:杨振兴,Worktile 前端工程师,所在Team主要负责PingCode Wiki产品的研发,PingCode Wiki是一款企业级的知识库产品,提供模块化的页面帮助企业沉淀信息、分享经验,是重度依赖富文本编辑器的场景,欢迎大家试用体验.

    展开全文
  • 在日常工作用,肯定有用到富文本编辑器的时候,富文本编辑器功能强大使用方便,我用的是百度富文本编辑器,首先需要下载好百度编辑器的demo,然后创建ueditor.html文件,引入百度编辑器,然后在html文件内引入,然后再用...
  • ckeditor富文本解决文段首行缩进问题

    千次阅读 2021-03-11 16:12:18
    今天使用富文本的时候,客户有个需求,需要在文段的前面空两格达到缩进效果 一开始我用了两种办法 1.文字前加空格(空格源码为&nbsp;小程序前端不会有渲染效果) 2.开启ckeditor的Outdent和Indent减小缩进和...
  • 在日常工作用,肯定有用到富文本编辑器的时候,富文本编辑器功能强大使用方便,假如你用百度富文本编辑器,首先需要下载好百度编辑器的demo,然后创建ueditor.html文件,引入百度编辑器,然后在html文件内引入,然后再用...
  • 富文本框和文本框一样,每次输入文字都会有刷新动作,每次刷新之后,你所绘制的图片也就没了。所以需要每次文字输入都重新绘制背景。这里有个设置文本框背景的代码,希望对你有所帮助:---------------------------------...
  • 之前在《富文本编辑器之游戏角色升级 ing》一文中,跟大家分享了富文本编辑器的发展历程、选型技巧和扩展方案。今天将和大家一起聊一聊“富文本及编辑器跨平台方案”那些事。 大家应该注意到了,标题用的是“富文本...
  • 最近一个消息插件中遇到一个特殊需求,就是一旦ueditor编辑器获取焦点以后,除非让编辑器失去焦点,否则window的键盘监听事件就失去作用了,在这种情况下如何才能使用ctrl+enter发送已经编辑好的内容呢?...
  • 今天测试反馈了一个bug,后台商品管理的富文本编辑器,在进行标签页切换的时候都会造成富文本内容清空,其他属性值正常显示。听到这问题时,我一个后端仔怎么解决这么"高难度"的问题。先是各种浏览器找解决办法,但...
  • CKEditor富文本编辑器的使用及bug修改 为什么要用富文本编译器? 在运营后台,运营人员需要录入并编辑详情信息, 详情信息不是普通的文本,可以是包含了HTML语法格式的字符串。为了快速简单的让用户能够在页面中...
  • 在百度富文本编辑器编辑的时候担心在手机端打开格式乱掉,编辑提交再去手机端刷新查看比较麻烦,希望在编辑器可以直接查看,效果如下: 代码修改如下: 文件一、路径:ueditor\themes\default\css\ueditor.css ...
  • 随着互联网的不断发展与前端开发技术的不断进步,越来越多的人想在前端开发市场中分一杯羹,而HTML语言凭着它简单易懂的特性成为了不少计算机萌新的入门语言。...HTML文本是由HTML命令组成的描述性文本,HTML命令...
  • 修改配置为富文本编辑器增加支持图片类型
  • 项目中很多地方都会用到富文本的内容:比如一般的商品详情,视频详情,资讯详情等,运营人员通过后台的富文本编辑器编辑的内容,前端拿到的就是一段富文本的字符串,这富文本大多都是图片和文字的组合。我们今天介绍...
  • Layui中layedit富文本遇到的坑小结

    千次阅读 2021-06-16 18:08:42
    Layui中layedit富文本遇到的坑小结 最近在开发一个微信小程序课程发布的后台页面,使用富文本编辑器因为是使用layui 我就直接考虑layui 提供的富文本编辑器,在开发的过程中就遇到了很多坑,我建议能不使用layedit ...
  • 富文本是个啥呢。富文本就是一个编辑器。比如一个网站有时候需要新闻、还有博客等东西的撰写就需要一个富文本编辑器。然后这里介绍的编辑器是wangEditor。 官网是www.wangeditor.com 官网上有详细的使用教程。 正文...
  • 前言本文将介绍笔者在React的项目中使用百度的富文本编辑器Ueditor的过程。注意本文不提供一条龙式的使用方法,只是将使用过程中的一些实现思路进行总结,供以参考。react项目中导入ueditor,会存在各种不正交的问题...
  • 最近加入了一套回车自动查询的方法,好几个界面都加好了,可是就是有一个界面的回车事件总是不生效并且似乎还自动刷新了整个界面。 $("input[name='projectName']").keyup(function(){ if(event.keyCode == 13){ ...
  • 1.介绍UEditor是由百度web前端研发部开发所见即所得富文本web编辑器,具有轻量,可定制,注重用户体验等特点,开源基于MIT协议,允许自由使用和修改代码...2.下载下载地址 :选择开发版 ,因为我...
  • 我们在在之前的文章中讲了百度ueditor富文本 的 配置和初始化的方法。我们可以给它配置更多的插件,全部插件可参考官网:如果官网提供的插件仍不能满足我们的需要时,则可以自定义插件按钮。比如 我们这里可以 自定义...
  • 支持Django的富文本编辑器很多,这里我推荐使用DjangoUeditor,Ueditor是百度开发的一个富文本编辑器,功能强大。下面教大家安装如何使用DjangoUeditor。 1、首先我们先下载DjangoUeditor包。点击下面的链接进行下载...
  • 在项目中使用dialog组件并且组件中使用Tinymce编辑器,发现创建一个表单提交成功后,不刷新页面,再次点击后Tinymce编辑器会携带缓存内容 解决方案: 搞东搞西搞死清除不掉,就直接v-if暴力删除元素 <el-...
  • 最近我们的产品有一个需求是要在PC端做一个面向用户的书评编辑器,让用户和编辑在蜗牛读书上能方便快捷的编辑和产出一些优质的文章,它的主要难点就是富文本编辑器部分。这虽然是个业务需求,但是做业务的同时也要...
  • NG+ 开发者大会2020 主题视频知乎视频​www.zhihu.com视频文字版本本文主要分享基于Angular和Slate开发富文本编辑器的实践历程,基于Angular做编辑器对我们来说也是一个新的尝试,社区关于Angular编辑器的实现更多的...
  • 用于通告、用户协议等可编辑的文本内容,要求页面和编辑框内容同步,一开始是思路是编辑时直接在段落盒子上创造一个editor实例,但是发现在保存并销毁实例时会把内容也摧毁掉,所以想了两种解决方法。 1.点击编辑弹...
  • UEditor百度富文本编辑器的initialFrameWidth属性,默认值是1000. 不能够自适应屏幕宽度.如图1: 刚开始的时候,我是直接设置initialFrameWidth=null的.效果如图2: 这样子UEditor百度富文本编辑器会在第一次加载...
  • 在react中使用富文本编辑器 react+wangEditor前言第一步第二步第三步第四步第五步完整代码写在最后 前言 近日在使用react写项目的过程中需要使用到富文本编辑器,之前使用的一直是百度富文本框,这次想换一个,于是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,951
精华内容 2,780
关键字:

自动刷新富文本

友情链接: CardGame.rar