Browse Source

refactor: 从EditableItem.vue中抽离设置焦点逻辑

ChenYangfan 6 years ago
parent
commit
68d54c4766

+ 10 - 0
doc/todos.md

@@ -0,0 +1,10 @@
+# 课堂助手编辑器逻辑细化
+
+- [ ] 上下箭头在位置在 range 发生改变的时候不更改默认操作,只有在按下上下键之后 range 仍然没有改变(说明在头部或者尾部)才触发更新焦点的事件
+- [ ]
+- [ ] 在可编辑项有类型的地方的开头退格,此时清除这个项的类型
+- [ ] 有序列表使用 div 进行模拟,序号操作由 js 进行控制
+- [ ] 加粗更改为使用不同的颜色字体进行表示
+- [ ] 在有序列表换行、在无序列表换行,下一个项应该是对应的有序或者无序列表
+- [ ] 在项的属性更改之后,需要重新设置焦点的元素,
+- [ ] 特殊的两个项:hr、和 ‘===’。hr 使用 div 进行模拟,‘===’不显示或者更改样式

+ 0 - 0
src/common/read-mode.scss


+ 0 - 0
src/components/editor/reset.scss → src/common/reset.scss


+ 161 - 0
src/common/show-mode.scss

@@ -0,0 +1,161 @@
+.slide {
+    box-sizing: border-box;
+    text-align: justify;
+    margin: 0 auto;
+    outline: none;
+  
+    line-height: 1.6;
+    word-break: break-word;
+    word-wrap: break-word;
+  
+    font-size: 22px;
+  
+    h1,
+    h2,
+    h3,
+    h4,
+    h5,
+    h6 {
+      font-weight: 600;
+      text-align: left;
+    }
+  
+    h1 {
+      font-size: 1.88em;
+      margin: 1.25em 0 0.5em 0;
+    }
+  
+    h2 {
+      font-size: 1.56em;
+      margin: 2.5em 0 0.3em 0;
+    }
+  
+    h3 {
+      font-size: 1.38em;
+      margin: 2.19em 0 0.15em 0;
+    }
+  
+    h4 {
+      font-size: 1.25em;
+      margin: 1.56em 0 0.1em 0;
+    }
+  
+    h5 {
+      font-size: 1em;
+      margin: 1.25em 0 0.1em 0;
+    }
+  
+    h6 {
+      font-size: 1em;
+      margin: 1.25em 0 0.1em 0;
+    }
+  
+    p {
+      margin: 0.44em 0;
+    }
+  
+    li {
+      margin: 0.19em;
+    }
+  
+    ul,
+    ol {
+      list-style: none;
+      padding-left: 1em;
+      margin-top: 0.3em;
+      margin-bottom: 0.8em;
+    }
+  
+    ul > li::before {
+      content: '\02022';
+      display: inline-block;
+      color: #1976d2;
+      width: 1em;
+      margin-left: -1em;
+    }
+  
+    ol {
+      counter-reset: li;
+      margin: 0 0 0 0.5em;
+  
+      li {
+        text-align: left;
+      }
+    }
+  
+    ol > li::before {
+      font-family: 'Courier', 'Courier New', monospace !important;
+      overflow: hidden;
+      font-size: 1em;
+      line-height: 0;
+      content: counter(li) '.';
+      color: #1976d2;
+      display: inline;
+      margin-left: -1.6em;
+      margin-right: 0.4em;
+      text-align: right;
+      counter-increment: li;
+    }
+  
+    blockquote {
+      margin: 0.63em 0;
+      padding: 0.31em 0.94em;
+      border-left: #fbc02d 0.25em solid;
+      background: #fffad3;
+      position: relative;
+  
+      &::before {
+        content: '!';
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 0.75em;
+        font-family: monospace;
+        color: white;
+        position: absolute;
+        left: -0.69em;
+        top: 1em;
+        width: 0.88em;
+        height: 0.88em;
+        background: #fbc02d;
+        border: 0.13em #fbc02d solid;
+        border-radius: 50%;
+      }
+    }
+  
+    a {
+      text-decoration: underline solid transparent;
+      transition: 0.2s all ease-out;
+  
+      &:hover {
+        text-decoration: underline solid #ccc;
+      }
+    }
+  
+    li > a,
+    p > a {
+      color: #1976d2;
+    }
+  
+    img {
+      max-width: 100%;
+      max-height: 100vh;
+      margin: 0.94em auto 1.88em auto;
+      display: block;
+      box-shadow: 0 0.13em 0.63em 0 #e5e5e5;
+    }
+  
+    pre {
+      padding: 0.63em 0.94em;
+      border: gray 0.06em solid;
+    }
+  
+    code {
+      overflow: auto;
+      word-break: normal;
+      padding: 0 0.19em;
+      font-family: 'Menlo', 'Courier New', monospace;
+      color: #1899ba;
+      font-size: 0.88em;
+    }
+  }

+ 52 - 46
src/components/editor/EditableItem.vue

@@ -20,9 +20,11 @@ import blockTypeRegs from '../../utils/block-type-regs'
 import {
   MODIFY_ITEM,
   ADD_ITEM_AFTER,
-  DELETE_ITEM
+  DELETE_ITEM,
+  SET_FOCUS_AFTER,
+  SET_FOCUS_BEFORE,
+  SET_FOCUS
 } from '../../store/types/editor-types'
-import { mapState } from 'vuex'
 export default {
   name: 'editable-item',
   props: ['item', 'pageId'],
@@ -32,57 +34,60 @@ export default {
       range: null
     }
   },
-  computed: {
-    ...mapState({})
-  },
-  mounted() {},
-  updated() {},
   methods: {
-    handleCompositionStart() {
-      this.isCompositionStarted = true
-    },
-    handleCompositionEnd(e) {
-      this.isCompositionStarted = false
-      this.handleInputValue(e)
-    },
     handleKeyDown(e) {
-      switch (e.key) {
-        case 'Enter':
-          if (!this.isCompositionStarted) {
+      if (!this.isCompositionStarted) {
+        switch (e.key) {
+          case 'Enter':
             e.preventDefault()
             this.$store.commit(ADD_ITEM_AFTER, {
               pageId: this.pageId,
               itemId: this.item.id
             })
-            this.$nextTick(() => {
-              const focusNode = document.getElementById(this.item.id)
-                .nextSibling
-              focusNode.focus()
+            this.$store.commit(SET_FOCUS_AFTER, {
+              itemId: this.item.id
             })
-          }
-          break
+            break
+
+          case 'Backspace':
+            if (!this.item.content) {
+              e.preventDefault()
+              this.$store.commit(SET_FOCUS_BEFORE, {
+                itemId: this.item.id
+              })
+              this.$store.commit(DELETE_ITEM, {
+                pageId: this.pageId,
+                itemId: this.item.id
+              })
+            }
+            break
 
-        case 'Backspace':
-          if (!this.isCompositionStarted && !this.item.content) {
+          case 'ArrowUp':
             e.preventDefault()
-            this.$store.commit(DELETE_ITEM, {
-              pageId: this.pageId,
+            this.$store.commit(SET_FOCUS_BEFORE, {
               itemId: this.item.id
             })
-            const focusNode = document.getElementById(this.item.id)
-              .previousElementSibling
-            this.$nextTick(() => {
-              focusNode.focus()
-              const focusRange = window.getSelection().getRangeAt(0)
-              focusRange.setStart(focusRange.startContainer.parentNode, 1)
+            break
+
+          case 'ArrowDown':
+            e.preventDefault()
+            this.$store.commit(SET_FOCUS_AFTER, {
+              itemId: this.item.id
             })
-          }
-          break
+            break
 
-        default:
-          break
+          default:
+            break
+        }
       }
     },
+    handleCompositionStart() {
+      this.isCompositionStarted = true
+    },
+    handleCompositionEnd(e) {
+      this.isCompositionStarted = false
+      this.handleInputValue(e)
+    },
     handleInput(e) {
       !this.isCompositionStarted && this.handleInputValue(e)
     },
@@ -102,17 +107,13 @@ export default {
         itemId: this.item.id,
         diff
       })
+
       this.range = window.getSelection().getRangeAt(0)
       const offset = this.range.startOffset
 
-      this.$nextTick(() => {
-        if (this.range.startContainer.firstChild) {
-          this.range.setStart(this.range.startContainer.firstChild, offset)
-        }
-
-        this.range.collapse()
-        window.getSelection().removeAllRanges()
-        window.getSelection().addRange(this.range)
+      this.$store.commit(SET_FOCUS, {
+        focusItemId: this.item.id,
+        focusItemOffset: offset
       })
     }
   }
@@ -133,9 +134,14 @@ export default {
     background: #f9fafc;
   }
 
-  &:empty:before {
+  &:empty:focus:before {
     content: attr(placeholder);
     color: #bbb;
   }
 }
+
+h1:empty:before {
+  content: attr(placeholder);
+  color: #bbb;
+}
 </style>

+ 7 - 7
src/components/editor/EditingPage.vue

@@ -1,7 +1,11 @@
 <template>
   <section class="page">
     <template v-for="item in page.items">
-      <editable-item-vue :key="item.id" :item="item" :pageId="page.id" />
+      <editable-item-vue
+        :key="item.id"
+        :item="item"
+        :pageId="page.id"
+      />
     </template>
   </section>
 </template>
@@ -9,15 +13,11 @@
 <script>
 import EditableItemVue from './EditableItem.vue'
 export default {
-  name: 'editing-slide',
+  name: 'editing-page',
   props: ['page'],
   components: {
     EditableItemVue
-  },
-  data: function() {
-    return {}
-  },
-  updated() {}
+  }
 }
 </script>
 

+ 62 - 165
src/components/editor/Editor.vue

@@ -3,187 +3,84 @@
     <template v-for="page in pages">
       <editing-page-vue :key="page.id" :page="page"></editing-page-vue>
     </template>
+    <div>{{ focus }}</div>
   </div>
 </template>
 
 <script>
 import { mapState } from 'vuex'
 import EditingPageVue from './EditingPage.vue'
+import { USE_FOCUS } from '../../store/types/editor-types'
 export default {
   name: 'editor',
   components: {
     EditingPageVue
   },
-  created() {},
-  mounted() {},
   computed: {
     ...mapState({
       pages: state => state.editor.pages
-    })
-  }
-}
-</script>
-<style lang="scss" scoped>
-@import './reset.scss';
-.slide {
-  box-sizing: border-box;
-  text-align: justify;
-  margin: 0 auto;
-  outline: none;
-
-  line-height: 1.6;
-  word-break: break-word;
-  word-wrap: break-word;
-
-  font-size: 22px;
-
-  h1,
-  h2,
-  h3,
-  h4,
-  h5,
-  h6 {
-    font-weight: 600;
-    text-align: left;
-  }
-
-  h1 {
-    font-size: 1.88em;
-    margin: 1.25em 0 0.5em 0;
-  }
-
-  h2 {
-    font-size: 1.56em;
-    margin: 2.5em 0 0.3em 0;
-  }
-
-  h3 {
-    font-size: 1.38em;
-    margin: 2.19em 0 0.15em 0;
-  }
-
-  h4 {
-    font-size: 1.25em;
-    margin: 1.56em 0 0.1em 0;
-  }
-
-  h5 {
-    font-size: 1em;
-    margin: 1.25em 0 0.1em 0;
-  }
-
-  h6 {
-    font-size: 1em;
-    margin: 1.25em 0 0.1em 0;
-  }
-
-  p {
-    margin: 0.44em 0;
-  }
-
-  li {
-    margin: 0.19em;
-  }
-
-  ul,
-  ol {
-    list-style: none;
-    padding-left: 1em;
-    margin-top: 0.3em;
-    margin-bottom: 0.8em;
-  }
-
-  ul > li::before {
-    content: '\02022';
-    display: inline-block;
-    color: #1976d2;
-    width: 1em;
-    margin-left: -1em;
-  }
-
-  ol {
-    counter-reset: li;
-    margin: 0 0 0 0.5em;
-
-    li {
-      text-align: left;
+    }),
+    focus() {
+      const res = this.$store.state.editor.focus
+      res !== null &&
+        this.$nextTick(() => {
+          this.checkFocus()
+        })
+      return this.$store.state.editor.focus
     }
-  }
-
-  ol > li::before {
-    font-family: 'Courier', 'Courier New', monospace !important;
-    overflow: hidden;
-    font-size: 1em;
-    line-height: 0;
-    content: counter(li) '.';
-    color: #1976d2;
-    display: inline;
-    margin-left: -1.6em;
-    margin-right: 0.4em;
-    text-align: right;
-    counter-increment: li;
-  }
-
-  blockquote {
-    margin: 0.63em 0;
-    padding: 0.31em 0.94em;
-    border-left: #fbc02d 0.25em solid;
-    background: #fffad3;
-    position: relative;
-
-    &::before {
-      content: '!';
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-size: 0.75em;
-      font-family: monospace;
-      color: white;
-      position: absolute;
-      left: -0.69em;
-      top: 1em;
-      width: 0.88em;
-      height: 0.88em;
-      background: #fbc02d;
-      border: 0.13em #fbc02d solid;
-      border-radius: 50%;
-    }
-  }
-
-  a {
-    text-decoration: underline solid transparent;
-    transition: 0.2s all ease-out;
-
-    &:hover {
-      text-decoration: underline solid #ccc;
+  },
+  methods: {
+    checkFocus() {
+      if (this.focus !== null) {
+        if (this.focus.focusItemId) {
+          this.$nextTick(() => {
+            const item = document.getElementById(this.focus.focusItemId)
+            item.focus()
+            const offset = this.focus.focusItemOffset
+            const computeResult = this.computeContainerAndOffset(item, offset)
+
+            const range = document.getSelection().getRangeAt(0)
+            range.setStart(computeResult.container, computeResult.offset)
+            range.collapse()
+
+            this.$store.commit(USE_FOCUS)
+          })
+        }
+      }
+    },
+    computeContainerAndOffset(item, offset) {
+      // todo test
+      if (offset === 0) {
+        return { container: item, offset: 0 }
+      }
+
+      let restOffset = offset
+      if (item.hasChildNodes()) {
+        for (const node of item.childNodes) {
+          if (node.length > restOffset) {
+            return {
+              container: node,
+              offset: restOffset
+            }
+          } else {
+            restOffset -= node.length
+          }
+        }
+      } else {
+        return { container: item, offset: 0 }
+      }
+
+      // default return value is the end of this element
+      // notice that when the element is not Text, offset 1 means the end of element
+      return {
+        container: item,
+        offset: 1
+      }
     }
   }
-
-  li > a,
-  p > a {
-    color: #1976d2;
-  }
-
-  img {
-    max-width: 100%;
-    max-height: 100vh;
-    margin: 0.94em auto 1.88em auto;
-    display: block;
-    box-shadow: 0 0.13em 0.63em 0 #e5e5e5;
-  }
-
-  pre {
-    padding: 0.63em 0.94em;
-    border: gray 0.06em solid;
-  }
-
-  code {
-    overflow: auto;
-    word-break: normal;
-    padding: 0 0.19em;
-    font-family: 'Menlo', 'Courier New', monospace;
-    color: #1899ba;
-    font-size: 0.88em;
-  }
 }
+</script>
+<style lang="scss" scoped>
+@import '@/common/reset.scss';
+@import '@/common/show-mode.scss';
 </style>

+ 75 - 9
src/store/modules/editor.js

@@ -1,12 +1,15 @@
 import {
   INIT_BY_STATE,
   INIT_BY_DEFAULT,
-  SET_RANGE,
   ADD_PAGE_AFTER,
   DELETE_PAGE,
   ADD_ITEM_AFTER,
   MODIFY_ITEM,
-  DELETE_ITEM
+  DELETE_ITEM,
+  SET_FOCUS,
+  SET_FOCUS_BEFORE,
+  SET_FOCUS_AFTER,
+  USE_FOCUS
 } from '../types/editor-types'
 
 import uuid from 'uuid'
@@ -14,25 +17,27 @@ import uuid from 'uuid'
 export default {
   state: {
     pages: [],
-    range: null
+    focus: null
   },
 
   mutations: {
     [INIT_BY_STATE](state, payload) {},
     [INIT_BY_DEFAULT](state, payload) {
+      const newItemId = uuid.v4()
+      const newPageId = uuid.v4()
       state.pages = [{
-        id: uuid.v4(),
+        id: newPageId,
         items: [{
-          id: uuid.v4(),
+          id: newItemId,
           type: 'h1',
           placeholder: '未命名文档标题',
           content: ''
         }]
       }]
-    },
-    [SET_RANGE](state, payload) {
-      state.range = {
-        ...payload.range
+      state.focus = {
+        focusPageId: newPageId,
+        focusItemId: newItemId,
+        focusItemOffset: 0
       }
     },
     [ADD_PAGE_AFTER](state, payload) {
@@ -109,6 +114,67 @@ export default {
           }
         })
       ]
+    },
+    [SET_FOCUS](state, payload) {
+      state.focus = {
+        ...state.focus,
+        ...payload
+      }
+    },
+    [SET_FOCUS_AFTER](state, payload) {
+      let focusId = ''
+      let targetedFlag = false
+      const isHit = state.pages.some(page => {
+        page.items.some(item => {
+          if (targetedFlag) {
+            focusId = item.id
+            return true
+          }
+          if (item.id === payload.itemId) {
+            targetedFlag = true
+          }
+          return false
+        })
+        return targetedFlag
+      })
+
+      if (isHit) {
+        state.focus = {
+          focusItemId: focusId,
+          focusItemOffset: payload.offset
+        }
+      } else {
+        // todo log
+      }
+    },
+    [SET_FOCUS_BEFORE](state, payload) {
+      let focusId = ''
+      let targetedFlag = false
+      const isHit = state.pages.some(page => {
+        [...page.items].reverse().some(item => {
+          if (targetedFlag) {
+            focusId = item.id
+            return true
+          }
+          if (item.id === payload.itemId) {
+            targetedFlag = true
+          }
+          return false
+        })
+        return targetedFlag
+      })
+
+      if (isHit && targetedFlag) {
+        state.focus = {
+          focusItemId: focusId,
+          focusItemOffset: payload.offset ? payload.offset : 9999
+        }
+      } else {
+        // todo log
+      }
+    },
+    [USE_FOCUS](state, payload) {
+      state.focus = null
     }
   },
   actions: {},

+ 5 - 3
src/store/types/editor-types.js

@@ -1,10 +1,12 @@
 export const INIT_BY_STATE = 'initByState'
 export const INIT_BY_DEFAULT = 'initByDefault'
-export const SET_RANGE = 'setRange'
 export const ADD_PAGE_AFTER = 'addPageAfter'
 export const DELETE_PAGE = 'initByState'
-// export const SPLITE_PAGE = 'splitePage'
-// export const JOIN_PAGE = 'joinPage'
 export const ADD_ITEM_AFTER = 'addItemAfter'
 export const MODIFY_ITEM = 'modifyItem'
 export const DELETE_ITEM = 'deleteItem'
+
+export const SET_FOCUS = 'setFocus'
+export const SET_FOCUS_BEFORE = 'setFocusBefore'
+export const SET_FOCUS_AFTER = 'setFocusAfter'
+export const USE_FOCUS = 'useFocus'