javascript safari浏览器使用document.execCommend()中文兼容bug

由于需求需要编写一个简易编辑器,仅仅只有加粗,倾斜和下划线,要求不能使用第三方库,所以就使用了div的contenteditable="true"属性加document.execCommend()的api来自己编写,
编写后发现了safari的中文兼容bug,查看了网上许多简易编辑的案例,发现都有这个bug,
即:safari 英文输入切换正常
中文输入模式下,点击取消加粗,依然是加粗样式

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
  <style>
    .content-edit i {
      font-style: italic !important;
    }

    .content-edit b {
      font-weight: bold !important;
    }

    .upload-header {
      width: 100%;
      box-sizing: border-box;
      padding: 43px 58px 31px 58px;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }

    .upload-header div {
      width: 156px;
      font-size: 52px;
    }

    .upload-header i {
      width: 37px;
      height: 69px;
      display: block;
      background: url("../../static/fanhui.svg") no-repeat;
      background-size: 100%;
    }

    .upload-header .center {
      text-align: center;
    }

    .upload-header .right {
      text-align: right;
    }

    .title-edit {
      padding: 54px 58px 48px 58px;
      box-sizing: border-box;
      border-bottom: 1px solid rgba(216, 216, 216, 1);
      font-size: 42px;
      font-weight: 600;
      color: rgba(54, 54, 54, 1);
    }

    .title-edit input {
      font-weight: 600;
      color: rgba(54, 54, 54, 1);
      width: 100%;
      border: none;
    }

    .content-edit {
      height: 300px;
      padding: 48px 58px;
      box-sizing: border-box;
      overflow-y: scroll;
      font-size: 42px;
      color: rgba(73, 73, 73, 1);
    }

    .content-edit * {
      font-size: 42px;
      color: rgba(73, 73, 73, 1);
    }

    .edit-tool {
      padding: 0 58px;
      box-sizing: border-box;
      width: 100%;
      height: 144px;
      display: flex;
      justify-content: space-between;
      border: 1px solid rgba(218, 218, 218, 1);
      border-left: none;
      border-right: none;
    }

    .edit-tool .left .active {
      color: blue;
    }

    .edit-tool .right .active {
      border: 1px solid #c8c8c8;
    }

    .edit-tool .left {
      height: 144px;
      display: flex;
      align-items: center;
    }

    .edit-tool .right>div {
      float: left;
      overflow: hidden;
      margin-top: 15px;
    }

    .edit-tool .left input {
      display: block;
      width: 40px;
      padding: 14px 55px;
      border: none;
      font-size: 64px;
      background: none;
    }

    .edit-tool .left input:nth-child(1) {
      padding: 14px 55px;
      padding-left: 14px;
    }

    .underline {
      text-decoration: underline;
    }

    .edit-tool .right button {
      padding: 0;
      display: block;
      overflow: hidden;
      width: 60px;
      height: 60px;
      border-radius: 100% 100% 100% 100%;
      -webkit-appearance: none;
      margin: 24px 24px;
      border: none;
    }

    .edit-tool .right .active {
      border: 10px solid #c8c8c8;
    }
  </style>
</head>

<body>
  <!-- app -->
  <div id="app">
    <div class="edit-container">
      <header class="upload-header">
        <div class="left" @click="returnPage">
          <i></i>
        </div>
        <div class="center">编辑</div>
        <div class="right">保存</div>
      </header>
      <div class="title-edit">
        <input type="text" v-model="htmlObj.title">
      </div>
      <div v-html="htmlObj.content" spellcheck="false" class="content-edit" contenteditable="true" @input="editChange"
        ref="editor" @paste="pasteText" @click="onClick($event)">
        <!-- <p>{{htmlObj.content}}</p> -->
      </div>
      <!-- <new-edit></new-edit> -->
      <div class="edit-tool">
        <div class="left">
          <input class="bold" :class="{ active: iconList[0].choose }" value="B" type="button"
            @click="iconClick($event, 'bold', 'style')">
          <input class="bias" :class="{ active: iconList[1].choose }" value="I" type="button"
            @click="iconClick($event, 'italic', 'style')">
          <input class="underline" :class="{ active: iconList[2].choose }" value="U" type="button"
            @click="iconClick($event, 'underline', 'style')">
        </div>
        <div class="right">
          <div>
            <button style="background:rgba(110,110,110,1);" :class="{ active: iconList[3].choose }"
              @click="iconClick($event, 'foreColor1', 'style')"></button>
          </div>
          <div>
            <button style="background:rgba(241,89,108,1);" :class="{ active: iconList[4].choose }"
              @click="iconClick($event, 'foreColor2', 'style')"></button>
          </div>
          <div>
            <button style="background:rgba(241,223,3,1);" :class="{ active: iconList[5].choose }"
              @click="iconClick($event, 'foreColor3', 'style')"></button>
          </div>
          <div>
            <button style="background:rgba(80,227,194,1);" :class="{ active: iconList[6].choose }"
              @click="iconClick($event, 'foreColor4', 'style')"></button>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
<script>
  var app = new Vue({
    el: "#app",
    data() {
      return {
        innerText: "",
        htmlObj: {
          title: "",
          content: ""
        },
        selectedRange: "",
        iconList: [{
            // hover名字
            name: "粗体",
            // 点击事件处理
            type: "bold",
            // 是否被选中
            canChoose: true,
            choose: false
          },
          {
            name: "斜体",
            type: "italic",
            canChoose: true,
            choose: false
          },
          {
            name: "下划线",
            type: "underline",
            canChoose: true,
            choose: false
          },
          {
            name: "字体颜色",
            type: "foreColor1",
            canChoose: true,
            drop: true,
            choose: false
          },
          {
            name: "字体颜色",
            type: "foreColor2",
            drop: true,
            canChoose: true,
            choose: false
          },
          {
            name: "字体颜色",
            type: "foreColor3",
            drop: true,
            canChoose: true,
            choose: false
          },
          {
            name: "字体颜色",
            type: "foreColor4",
            drop: true,
            canChoose: true,
            choose: false
          }
        ],
        activeIconList: []
      };
    },
    mounted() {
      window.localStorage.setItem(
        "moduleList",
        '[{"type":1,"title":"12","content":"<p>13333</p>"}]'
      );
      this.htmlObj = JSON.parse(window.localStorage.getItem("moduleList"))[0];
    },
    methods: {
      onClick(event) {
        let that = this;
        console.log(event);
        if (event.target.tagName == 'DIV') {
          return false;
        }
        this.activeIconList = [];
        let target = event.target;
        this.changeTagStyle(target);
        let newArr = this.activeIconList.filter(function (x, index, self) {
          return self.indexOf(x) === index;
        });
        for (let i = 0; i < this.iconList.length; i++) {
          if (newArr[i] === "B") {
            this.iconList[0].choose = true;

          } else if (newArr[i] === "U") {
            this.iconList[2].choose = true;
          } else if (newArr[i] === "I") {
            this.iconList[1].choose = true;
          } else {
            this.iconList[i].choose = false;
          }
        }
      },
      isToolTag(el) {
        if (el.tagName == "B") return true;
        else return false;
      },
      changeTagStyle(el) {
        if (
          el.tagName == "B" ||
          el.tagName == "I" ||
          el.tagName == "U" ||
          el.tagName == "FONT"
        ) {
          this.activeIconList.push(el.tagName);
          console.log(el.parentNode.tagName);
        } else if (el.tagName == "DIV") {
          return;
        }
        this.changeTagStyle(el.parentNode);
      },
      changeStyle(type) {
        switch (type) {
          case "bold":
            console.log("blod");
            console.log(this.iconList[0].choose);
            if (window.navigator.userAgent.indexOf('AppleWebKit') > -1 && this.iconList[0].choose === true) {
              console.log('取消选中 光标后移')
              let sel = document.getSelection();
              console.log(sel.anchorNode.nodeType); //文本节点为3,元素节点为1
              console.log(sel.anchorNode.parentNode.parentNode);
              // window.getSelection().removeAllRanges();
              // console.log(sel);
              this.keepLastIndex(sel.anchorNode.parentNode.parentNode);
              // let cursorPos = selection.anchorOffset;
              // let oldContent = selection.sel.nodeValue;
              // let newContent =
              // oldContent.substring(0, cursorPos) +
              //   toInsert +
              // oldContent.substring(cursorPos);
              // selection.anchorNode.nodeValue = newContent;
              // let range=sel.getRangeAt(0);
              // range.setStart(range.startContainer,3);
              // console.log(sel);
              // console.log(sel.getRangeAt(0));
              // console.log(sel.focusOffset);

            }
            document.execCommand("bold", false);
            // document.execCommand("insertHtml", 'a',false);
            break;
          case "underline":
            if (this.iconList[2].choose === false) {
              document.execCommand("underline", false);
            } else {
              document.execCommand("removeFormat", false);
            }
            // document.execCommand("underline", false);
            break;
          case "italic":
            document.execCommand("italic", false);
            break;
          case "foreColor1":
            document.execCommand("foreColor", 0, "rgba(110,110,110,1)");
            break;
          case "foreColor2":
            document.execCommand("foreColor", 0, "rgba(241,89,108,1)");
            break;
          case "foreColor3":
            document.execCommand("foreColor", 0, "rgba(241,223,3,1)");
            break;
          case "foreColor4":
            document.execCommand("foreColor", 0, "rgba(80,227,194,1)");
            break;
          default:
            console.log("none");
        }
      },
      // insertAfter(newNode, curNode) {
      //   console.log(curNode.parentNode);
      //   curNode.parentNode.insertBefore(newNode, curNode.nextElementSibling);
      // },
      iconClick(event, type, dropType) {
        event.preventDefault();
        console.log(event);
        this.$refs.editor.focus();
        // let $el=this.$refs.editor;
        // this.keepLastIndex($el);
        this.selectedRange = this.getSelect();
        if (event.target.classList.contains("active")) {
          //解决safari中文光标位置
          //获取光标所在位置的节点,
          console.log(
            window.getSelection().getRangeAt(0).startContainer.parentElement
          );
          let el = window.getSelection().getRangeAt(0).startContainer.parentElement;
          // let span=document.createElement('span');
          // this.insertAfter(span,el);
          // console.log(el);
          // el.focus();
          // var selection = window.getSelection();
          // let span = document.createElement("span");
          // let range = document.createRange();
          // range.collapse(false);
          // range.insertNode(span);
          // selection.removeAllRanges(); /*清空所有Range对象*/
          // range.setStart(
          //   span,
          //   0
          // );
          // range.setEnd(
          //    span,
          //     1
          // );
          // selection.addRange(range);
          // this.placeCaretAtEnd(span);
        }
        // 恢复光标
        // this.restoreSelection();
        // 恢复光标
        // this.restoreSelection();
        // 修改所选区域的样式
        this.changeStyle(type);
        this.$nextTick(() => {
          // if (dropType) {//下拉菜单的type
          //   type = dropType
          // }
          //改变当前元素的样式
          let sourceArr = JSON.parse(JSON.stringify(this.iconList));
          let arr = sourceArr.map((val, index) => {
            if (type === val.type && val.canChoose) {
              val.choose = val.choose ? false : true;
            }
            // else {
            //   if (val.drop) {
            //     val.choose = false;
            //   }
            // }
            return val;
          });

          // if (type === 'clear') {
          //   var a = this.getSelect()
          //   if (a.startOffset === a.endOffset) {
          //     document.execCommand('insertHTML', false, '&nbsp')
          //     // return false
          //   }
          //   arr = arr.map((val, index) => {
          //     val.choose = false
          //     return val
          //   })
          // }
          this.iconList = arr;
        });
      },
      //获取选中
      getSelect() {
        if (window.getSelection) {
          /*主流的浏览器,包括chrome、Mozilla、Safari*/
          var sel = window.getSelection();
          console.log(sel.getRangeAt(0));
          if (sel.rangeCount > 0) {
            return sel.getRangeAt(0);
          }
        } else if (document.selection) {
          /*IE下的处理*/
          return document.selection.createRange();
        }
        return null;
      },
      placeCaretAtEnd(el) {
        //传入光标要去的jq节点对象
        el.focus();
        if (
          typeof window.getSelection != "undefined" &&
          typeof document.createRange != "undefined"
        ) {
          var range = document.createRange();
          range.selectNodeContents(el);
          range.collapse(false);
          var sel = window.getSelection();
          sel.removeAllRanges();
          sel.addRange(range);
        } else if (typeof document.body.createTextRange != "undefined") {
          var textRange = document.body.createTextRange();
          textRange.moveToElementText(el);
          textRange.collapse(false);
          textRange.select();
        }
      },
      restoreSelection() {
        var selection = window.getSelection();
        console.log(this.selectedRange);
        let range = document.createRange();
        if (this.selectedRange) {
          try {
            selection.removeAllRanges(); /*清空所有Range对象*/
            range.setStart(
              this.selectedRange.startContainer,
              this.selectedRange.startOffset
            );
            range.setEnd(
              this.selectedRange.endContainer,
              this.selectedRange.endOffset
            );
          } catch (ex) {
            /*IE*/
            document.body.createTextRange().select();
            document.selection.empty();
          }
          /*恢复保存的范围*/
          selection.addRange(this.selectedRange);
        }
      },
      returnPage() {
        this.$router.go(-1);
      },
      editChange() {
        // console.log(this.$refs.editor.children);
        let that = this;
        if (this.$refs.editor.children.length == 0) {
          this.$refs.editor.value = `<p></p>`;
          // setTimeout(() => {
          //   that.keepLastIndex(e.target);
          // }, 5);
        }

      },
      //获取光标
      getCaret() {},
      //设置光标
      setCaret() {},
      //保存光标
      saveCaret() {},
      //插入节点
      insertHtmlAtCaret(html) {
        let sel, range;
        if (window.getSelection) {
          // IE9 and non-IE
          sel = window.getSelection();

          if (sel.getRangeAt && sel.rangeCount) {
            range = sel.getRangeAt(0);

            range.deleteContents();

            // Range.createContextualFragment() would be useful here but is

            // non-standard and not supported in all browsers (IE9, for one)

            let el = document.createElement("div");

            el.innerHTML = html;

            let frag = document.createDocumentFragment(),
              node,
              lastNode;

            while ((node = el.firstChild)) {
              lastNode = frag.appendChild(node);
            }

            range.insertNode(frag);

            // Preserve the selection

            if (lastNode) {
              range = range.cloneRange();

              range.setStartAfter(lastNode);

              range.collapse(true);

              sel.removeAllRanges();

              sel.addRange(range);
            }
          }
        } else if (document.selection && document.selection.type != "Control") {
          // IE < 9
          document.selection.createRange().pasteHTML(html);
        }
      },
      //光标定位在末尾
      keepLastIndex(obj) {
        if (window.getSelection) {
          //ie11 10 9 ff safari
          obj.focus(); //解决ff不获取焦点无法定位问题
          var range = window.getSelection(); //创建range
          range.selectAllChildren(obj); //range 选择obj下所有子内容
          range.collapseToEnd(); //光标移至最后
        } else if (document.selection) {
          //ie10 9 8 7 6 5
          var range = document.selection.createRange(); //创建选择对象
          //var range = document.body.createTextRange();
          range.moveToElementText(obj); //range定位到obj
          range.collapse(false); //光标移至最后
          range.select();
        }
      },
      //粘贴事件处理
      async pasteText(e) {
        e.stopPropagation();
        e.preventDefault();
        let text;
        if (navigator.clipboard) {
          text = await navigator.clipboard.readText();
        } else {
          text = e.clipboardData.getData("text/plain");
        }
        let self = this;

        // 在等待一段时间后,在当前光标位置,粘贴处理后的文本
        setTimeout(function () {
          let selection = document.getSelection();
          let cursorPos = selection.anchorOffset;
          let oldContent = selection.anchorNode.nodeValue;
          // 通过 Dom 去除所有样式
          // let oDiv = document.createElement("div");
          // oDiv.innerHTML = pasteData;
          let toInsert = text;
          let newContent =
            oldContent.substring(0, cursorPos) +
            toInsert +
            oldContent.substring(cursorPos);
          selection.anchorNode.nodeValue = newContent;
          // if(window.navigator.userAgent.indexOf('AppleWebKit')>-1){
          let rag = document.createRange();
          rag.selectNodeContents(selection.anchorNode); //必须传node
          rag.collapse(false);
          let sel = window.getSelection();
          sel.removeAllRanges();
          sel.addRange(rag);

          // }
          // console.log(selection);
          // let range=selection.getRangeAt(0);
          // range.setStart(range.startContainer,parseInt(cursorPos+toInsert.length));
          // console.log(range);
        }, 200);
        return false;
      }
    }
  })
  // document.onselectionchange = function() {
  //   console.log('New selection made');
  //   let selection = document.getSelection();
  //   console.log(selection.type);
  // }
</script>

</html>
Csdn user default icon
上传中...
上传图片
插入图片
抄袭、复制答案,以达到刷声望分或其他目的的行为,在CSDN问答是严格禁止的,一经发现立刻封号。是时候展现真正的技术了!
立即提问
相关内容推荐