WP DEVELOP

EP102. “可选:jQuery-free 版 MyNotes.js(原生 JS + Axios)”

首页 WordPress 开发课程 MY NOTES 前端功能 · EP102
约 27 分钟· #EP102#MY NOTES 前端功能
🔒 登录后可标记已读

📌 并入 EP101 的提醒:早前为了让 jQuery 版的展开/收起动画顺滑,曾要求把 css/modules/my-notes.css 底部「Reveal and Hide Fade Transitions」那段 CSS 注释掉;这一讲改用原生 JS 之后要用回这段动画 CSS,记得把 /**/ 去掉,恢复这段代码。

这是跟 Search.js(EP084)同一模式的补录加课:提供一份不依赖 jQuery、用原生 DOM API + Axios 重写的 MyNotes.js,可以直接整份替换。核心逻辑(编辑/取消状态切换、增删改查、笔记数量上限提示)完全不变,区别只在于两个地方:① 用事件委托手写了一个 clickHandler() 统一分发点击事件,替代 jQuery 的 .on(event, selector, handler) 委托写法;② 因为原生 CSS 没法直接「过渡到 auto 高度」,新建笔记时的淡入动画要手动用 JS 量出元素的实际高度再做动画,删除时则用 setTimeout 手动错开「加渐隐 class」和「真正移除元素」两个步骤,模拟出类似 jQuery slideUp()/slideDown() 的效果。


涉及文件

  • wp-content/themes/fictional-university-theme/src/modules/MyNotes.js (整份替换)
  • wp-content/themes/fictional-university-theme/css/modules/my-notes.css (取消注释,恢复过渡动画 CSS)

代码实现

完整的 jQuery-free 版本(课程资源直接提供下载,整份复制替换掉原文件):

// wp-content/themes/fictional-university-theme/src/modules/MyNotes.js
import axios from "axios"

class MyNotes {
  constructor() {
    if (document.querySelector("#my-notes")) {
      axios.defaults.headers.common["X-WP-Nonce"] = universityData.nonce
      this.myNotes = document.querySelector("#my-notes")
      this.events()
    }
  }

  events() {
    this.myNotes.addEventListener("click", e => this.clickHandler(e))
    document.querySelector(".submit-note").addEventListener("click", () => this.createNote())
  }

  clickHandler(e) {
    if (e.target.classList.contains("delete-note") || e.target.classList.contains("fa-trash-o")) this.deleteNote(e)
    if (e.target.classList.contains("edit-note") || e.target.classList.contains("fa-pencil") || e.target.classList.contains("fa-times")) this.editNote(e)
    if (e.target.classList.contains("update-note") || e.target.classList.contains("fa-arrow-right")) this.updateNote(e)
  }

  findNearestParentLi(el) {
    let thisNote = el
    while (thisNote.tagName != "LI") {
      thisNote = thisNote.parentElement
    }
    return thisNote
  }

  editNote(e) {
    const thisNote = this.findNearestParentLi(e.target)

    if (thisNote.getAttribute("data-state") == "editable") {
      this.makeNoteReadOnly(thisNote)
    } else {
      this.makeNoteEditable(thisNote)
    }
  }

  makeNoteEditable(thisNote) {
    thisNote.querySelector(".edit-note").innerHTML = '<i class="fa fa-times" aria-hidden="true"></i> Cancel'
    thisNote.querySelector(".note-title-field").removeAttribute("readonly")
    thisNote.querySelector(".note-body-field").removeAttribute("readonly")
    thisNote.querySelector(".note-title-field").classList.add("note-active-field")
    thisNote.querySelector(".note-body-field").classList.add("note-active-field")
    thisNote.querySelector(".update-note").classList.add("update-note--visible")
    thisNote.setAttribute("data-state", "editable")
  }

  makeNoteReadOnly(thisNote) {
    thisNote.querySelector(".edit-note").innerHTML = '<i class="fa fa-pencil" aria-hidden="true"></i> Edit'
    thisNote.querySelector(".note-title-field").setAttribute("readonly", "true")
    thisNote.querySelector(".note-body-field").setAttribute("readonly", "true")
    thisNote.querySelector(".note-title-field").classList.remove("note-active-field")
    thisNote.querySelector(".note-body-field").classList.remove("note-active-field")
    thisNote.querySelector(".update-note").classList.remove("update-note--visible")
    thisNote.setAttribute("data-state", "cancel")
  }

  async deleteNote(e) {
    const thisNote = this.findNearestParentLi(e.target)

    try {
      const response = await axios.delete(universityData.root_url + "/wp-json/wp/v2/note/" + thisNote.getAttribute("data-id"))
      thisNote.style.height = `${thisNote.offsetHeight}px`
      setTimeout(function () {
        thisNote.classList.add("fade-out")
      }, 20)
      setTimeout(function () {
        thisNote.remove()
      }, 401)
      if (response.data.userNoteCount < 5) {
        document.querySelector(".note-limit-message").classList.remove("active")
      }
    } catch (e) {
      console.log("Sorry")
    }
  }

  async updateNote(e) {
    const thisNote = this.findNearestParentLi(e.target)

    var ourUpdatedPost = {
      "title": thisNote.querySelector(".note-title-field").value,
      "content": thisNote.querySelector(".note-body-field").value
    }

    try {
      const response = await axios.post(universityData.root_url + "/wp-json/wp/v2/note/" + thisNote.getAttribute("data-id"), ourUpdatedPost)
      this.makeNoteReadOnly(thisNote)
    } catch (e) {
      console.log("Sorry")
    }
  }

  async createNote() {
    var ourNewPost = {
      "title": document.querySelector(".new-note-title").value,
      "content": document.querySelector(".new-note-body").value,
      "status": "publish"
    }

    try {
      const response = await axios.post(universityData.root_url + "/wp-json/wp/v2/note/", ourNewPost)

      if (response.data != "You have reached your note limit.") {
        document.querySelector(".new-note-title").value = ""
        document.querySelector(".new-note-body").value = ""
        document.querySelector("#my-notes").insertAdjacentHTML(
          "afterbegin",
          ` <li data-id="${response.data.id}" class="fade-in-calc">
            <input readonly class="note-title-field" value="${response.data.title.raw}">
            <span class="edit-note"><i class="fa fa-pencil" aria-hidden="true"></i> Edit</span>
            <span class="delete-note"><i class="fa fa-trash-o" aria-hidden="true"></i> Delete</span>
            <textarea readonly class="note-body-field">${response.data.content.raw}</textarea>
            <span class="update-note btn btn--blue btn--small"><i class="fa fa-arrow-right" aria-hidden="true"></i> Save</span>
          </li>`
        )

        // 新 <li> 先给 fade-in-calc class 让它暂时不可见,方便量出它的自然高度
        let finalHeight // 浏览器没法直接过渡到 auto 高度,必须先算出具体像素值
        let newlyCreated = document.querySelector("#my-notes li")

        // 等 30ms 让这个隐藏的元素先被浏览器渲染出来
        setTimeout(function () {
          finalHeight = `${newlyCreated.offsetHeight}px`
          newlyCreated.style.height = "0px"
        }, 30)

        // 再等 20ms,让浏览器有机会先量出隐藏状态下的高度
        setTimeout(function () {
          newlyCreated.classList.remove("fade-in-calc")
          newlyCreated.style.height = finalHeight
        }, 50)

        // 等 CSS 过渡动画播完,再把写死的高度值去掉,恢复响应式布局
        setTimeout(function () {
          newlyCreated.style.removeProperty("height")
        }, 450)
      } else {
        document.querySelector(".note-limit-message").classList.add("active")
      }
    } catch (e) {
      console.error(e)
    }
  }
}

export default MyNotes

跟 jQuery 版的主要差异

方面jQuery 版原生版
事件委托$("#my-notes").on("click", ".delete-note", fn)监听器统一挂在 #my-notes 上,自己写 clickHandler(e)e.target.classList.contains(...) 判断点的是哪个按钮(注意还要兼容点在图标 <i> 上而不是外层 <span> 上的情况)
找父级 <li>$(e.target).parents("li")自己写 findNearestParentLi(),用 while 循环沿 parentElement 一路往上找,直到 tagName == "LI"
发请求$.ajax({ beforeSend, url, type, data, success, error })axios.delete() / axios.post() + async/await + try/catch;nonce 不用每次请求单独设置,而是在 constructor 里用 axios.defaults.headers.common["X-WP-Nonce"] 全局设置一次,之后所有请求自动带上
展开/收起动画.slideDown() / .slideUp() 一行搞定没有现成方法,要手写:删除时用两个 setTimeout 错开「加渐隐 class」和「真正调用 .remove()」;新建时先让元素以隐藏状态插入 DOM 量出真实高度,再手动把 height0px 过渡到量出来的具体像素值,动画播完后再去掉写死的 height,让元素恢复响应式(因为 CSS transition 没法直接对 height: auto做动画,必须先用 JS 算出具体数值)

Hook / Function 速查

名称类型用途
axios.defaults.headers.common[...]Axios API全局设置默认请求头,之后每次请求自动携带,不用每次单独写
element.classList.contains(name)浏览器原生 API判断元素是否含有指定 CSS class
element.parentElement浏览器原生 API获取父级元素,配合循环可以手动实现「查找最近的祖先元素」
element.offsetHeight浏览器原生 API读取元素实际渲染高度(像素),常用于手写高度过渡动画前先测量
element.style.removeProperty(name)浏览器原生 API移除通过 JS 设置的内联样式属性

常见坑

  • clickHandler() 只判断点击的是外层 <span>(如 .delete-note),没有兼容点在里面的图标 <i class="fa fa-trash-o"> 上——原生事件的 e.target 是精确点到的那个元素,点在子元素上不会自动被外层的 class 判断覆盖到,需要把图标的 class 也一起纳入判断条件
  • CSS 过渡动画写 transition: height ... 却想直接从固定高度变到 auto——浏览器不支持对 auto 做动画过渡,必须先用 JS 量出目标的具体像素高度,再对这个具体数值做过渡

Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 19, EP102(含 EP101 提醒并入)