EP102. “可选:jQuery-free 版 MyNotes.js(原生 JS + Axios)”
🔒 登录后可标记已读📌 并入 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 量出真实高度,再手动把 height 从 0px 过渡到量出来的具体像素值,动画播完后再去掉写死的 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 提醒并入)