You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TinyMCE脚注插件复制文本插入报错:$node未定义求助

问题:复制粘贴文本时TinyMCE脚注插件报错Uncaught ReferenceError: $node is not defined

我开发了一款TinyMCE脚注插件,功能类似学术论文的脚注。手动输入文本时插件运行正常,但从其他网站复制文本后插入脚注,会抛出错误:Uncaught ReferenceError: $node is not defined,多次尝试解决未果,请求排查。


代码文件:footnote.min.js

! function () {"use strict";var t, e = tinymce.util.Tools.resolve("tinymce.PluginManager");"function" != typeof Array.prototype.forEach && (Array.prototype.forEach = function (t) {for (var e = 0; e < this.length; e++) t.apply(this, [this[e], e, this])}), Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(Element.prototype, "textContent").get && (t = Object.getOwnPropertyDescriptor(Element.prototype, "innerText"), Object.defineProperty(Element.prototype, "textContent", {get: function () {return t.get.call(this)},set: function (e) {return t.set.call(this, e)}}));var n = function (t, e) {var n = t;for (var o in e) n = n.replace(/\{(\/?[^\}]+)\}/gm, e[o]);return n},o = function (t) {var e = t.selection.getNode(),o = "",r = "SPAN" == e.tagName && "fnoteWrap" === t.dom.getAttrib(e, "class"),i = "fnoteWrap" == e.className ? e.childNodes[0].firstChild.nodeValue.replace(/[^0-9]/g, "") : e.childNodes[0];r && (o = e.name || decodeURIComponent(e.childNodes[0].getAttribute("data-content")) || ""), t.windowManager.open({title: "Insert Contents",size: "normal",body: {type: "panel",items: [{type: "textarea",name: "name",multiline: !0,minWidth: 520,minHeight: 100}]},buttons: [{type: "cancel",name: "cancel",text: "Cancel"},{type: "submit",name: "save",text: "Save",primary: !0}],initialData: {name: o},onSubmit: function (e) {var o, r, a, c, l = e.getData().name,s = '<span class="fnoteWrap" id="#wk_ft{FOOTNOTE_INDEX}" contenteditable="false"><button type="button" class="fnoteBtn" data-content="' + l + '">{FOOTNOTE_INDEX}</button></span>',f = t.getDoc().querySelectorAll(".fnoteBtn"),u = f.length,p = (r = t.selection.getNode(), a = function (t) {var e = !1;return (e = [].filter.call(t.parentNode.children, function (n) {return n.previousElementSibling === t ? e = !0 : e})).map(e => Array.from(e.querySelectorAll("fnoteBtn").length) > 0 ? $node.nextElementSibling.classList.contains("fnoteBtn") ? $node.nextElementSibling.children.children : e.querySelectorAll(".fnoteBtn") : "BODY" === t.nodeName ? [] : a(t.parentNode))},c = function (t, e) {if (!e) return !1;if (e) return $node;var n = null;return e.children.forEach(function () {e && (n = c(t, this))}), n},function (t, e) {for (var n = a(e); 0 !== n.length;) {var o = c(t, n);if (null !== o) return o;n = a(n)}return n}(".fnoteBtn", r));if (p.length) {var d;for (p = p[0], d = 0; d < u && p != f[d]; d++);i < u ? o = n(s, {FOOTNOTE_INDEX: $(f[i - 1]).html()}) : (o = n(s, {FOOTNOTE_INDEX: $(f[d]).html()}), t.selection.collapse(0))} else o = n(s, {FOOTNOTE_INDEX: u + 1}), t.selection.collapse(0);t.execCommand("mceInsertContent", !1, o), e.close(), Array.from(t.getDoc().querySelectorAll(".fnoteBtn")).forEach(function (t, e) {t.textContent = e + 1, t.parentNode.setAttribute("id", "#wk_ft" + (e + 1))})}})},r = {register: function (t) {t.addCommand("footnotes", function () {o(t)})}},i = {register: function (t) {t.ui.registry.addToggleButton("footnotes", {icon: "fnote",tooltip: "Footnote",onAction: function () {return t.execCommand("footnotes")},onSetup: function (e) {return t.selection.selectorChangedWithUnbind("span.fnoteWrap", e.setActive).unbind}}), t.ui.registry.addMenuItem("footnotes", {icon: "fnote",onAction: function () {return t.execCommand("footnotes")}})}};e.add("footnotes", function (t) {t.ui.registry.addIcon("fnote", '<img src="' + tinyMCE.baseURL + '/plugins/footnotes/img/fn.png">'), r.register(t), i.register(t)})}();

代码文件:footnote.js

(function(){'use strict';var global = tinymce.util.Tools.resolve('tinymce.PluginManager');if (typeof Array.prototype.forEach !== 'function') {Array.prototype.forEach = function(cb){for (var i = 0; i < this.length; i++){cb.apply(this, [this[i], i, this]);}};}if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(Element.prototype, "textContent").get) {(function() {var innerText = Object.getOwnPropertyDescriptor(Element.prototype, "innerText");Object.defineProperty(Element.prototype, "textContent",{get: function() {return innerText.get.call(this);},set: function(s) {return innerText.set.call(this, s);}});})();}/** * * @param str * @param data * @returns {*} */var replaceTmpl = function(str, data) {var result = str;for (var key in data) {result = result.replace(/\{(\/?[^\}]+)\}/gm,data[key]);}return result;};/** * * @param editor */var open = function (editor) {var selectedNode = editor.selection.getNode(), name = '',isFootNotes = selectedNode.tagName == 'SPAN' && editor.dom.getAttrib(selectedNode, 'class') === 'fnoteWrap';var selectIndex = (function(){if (selectedNode.className == 'fnoteWrap') {var num = selectedNode.childNodes[0].firstChild.nodeValue.replace(/[^0-9]/g,'');return num;}else {return selectedNode.childNodes[0];}}());if (isFootNotes) {name = selectedNode.name || decodeURIComponent(selectedNode.childNodes[0].getAttribute('data-content')) || '';}editor.windowManager.open({title: 'Insert Contents',size: 'normal',body: {type: 'panel',items : [{type:'textarea',name: 'name',multiline: true,minWidth: 520,minHeight: 100,}]},buttons: [{type: 'cancel',name: 'cancel',text: 'Cancel'},{type: 'submit',name: 'save',text: 'Save',primary: true}],initialData: { name: name },onSubmit: function (e) {var newfootnoteContent = e.getData().name,fixFootnoteContent = (function () {return encodeURIComponent(newfootnoteContent);}()),htmlTemplate = '<span class="fnoteWrap" id="#wk_ft{FOOTNOTE_INDEX}" contenteditable="false"><button type="button" class="fnoteBtn" data-content="'+fixFootnoteContent+'">{FOOTNOTE_INDEX}</button></span>',totalFootNote = editor.getDoc().querySelectorAll('.fnoteBtn'),totalCount = totalFootNote.length,html;function findNextFD(node) {var getNext = function(el) {var nextAll = false,elements;nextAll = [].filter.call(el.parentNode.children, function (htmlElement) {return (htmlElement.previousElementSibling === el) ? nextAll = true : nextAll;});return nextAll.map(v => {if (Array.from(v.querySelectorAll('fnoteBtn').length) > 0) {$node.nextElementSibling.classList.contains('fnoteBtn') ?elements = $node.nextElementSibling.children.children :elements = v.querySelectorAll('.fnoteBtn');return elements}else {if (el.nodeName === 'BODY') return [];return getNext(el.parentNode);}})}var nextInDOM = function(_selector, el) {var next = getNext(el);while(next.length !== 0) {var found = searchFor(_selector, next);if(found !== null) {return found;}next = getNext(next);}return next;}var searchFor = function(_selector, el) {if (!el) {return false};if(el) {return $node;}else {var found = null;el.children.forEach(function() {if (el)found = searchFor(_selector, this);});return found;}return null;}var currentClassNot_NextClass = nextInDOM('.fnoteBtn', node);return currentClassNot_NextClass;}var nextFD = findNextFD(editor.selection.getNode());if(nextFD.length) {nextFD = nextFD[0];var foundIdx;for(foundIdx = 0; foundIdx < totalCount; foundIdx++) {if(nextFD == totalFootNote[foundIdx]) {break;}}if (selectIndex < totalCount) {// modifyhtml = replaceTmpl(htmlTemplate,{FOOTNOTE_INDEX : $(totalFootNote[selectIndex-1]).html()});}else {// anywhere addhtml = replaceTmpl(htmlTemplate,{FOOTNOTE_INDEX : $(totalFootNote[foundIdx]).html()});editor.selection.collapse(0);}} else {// last addhtml = replaceTmpl(htmlTemplate,{FOOTNOTE_INDEX : totalCount + 1});editor.selection.collapse(0);}editor.execCommand('mceInsertContent', false, html);e.close()// index realignmentvar fnoteBtn = Array.from(editor.getDoc().querySelectorAll('.fnoteBtn'));fnoteBtn.forEach(function(value,idx){value.textContent = idx+1;value.parentNode.setAttribute('id','#wk_ft' + (idx +1))})}});};var Dialog = { open: open };var register$1 = function (editor) {editor.ui.registry.addToggleButton('footnotes', {icon : 'fnote',tooltip : 'Footnote',onAction: function () {return editor.execCommand('footnotes');},onSetup: function (buttonApi) {return editor.selection.selectorChangedWithUnbind('span.fnoteWrap', buttonApi.setActive).unbind;}});editor.ui.registry.addMenuItem('footnotes', {icon: 'fnote',onAction: function () {return editor.execCommand('footnotes');}});};var register = function (editor) {editor.addCommand('footnotes', function () {Dialog.open(editor);});};var Commands = { register: register };var Buttons = { register: register$1 };function Plugin () {global.add('footnotes', function (editor) {editor.ui.registry.addIcon('fnote','<img src="'+ tinyMCE.baseURL + '/plugins/footnotes/img/fn.png' +'">')Commands.register(editor);Buttons.register(editor);});}Plugin();})()

问题根源与修复方案

错误原因

  1. 未定义变量$node:findNextFD函数内多处使用未声明的$node变量,复制粘贴的文本会生成嵌套层级更复杂的DOM结构,触发了这段有问题的逻辑;而手动输入的文本DOM结构简单,未进入该分支,所以未报错。
  2. jQuery依赖未处理:代码中使用$()语法,但未确保页面已引入jQuery,这是潜在的额外错误点。
  3. 选择器语法错误:v.querySelectorAll('fnoteBtn')缺少类选择器的.,应为v.querySelectorAll('.fnoteBtn')。
  4. selectIndex逻辑错误:选中非脚注元素时,selectIndex返回DOM节点而非数字,后续与totalCount比较会导致逻辑异常。

修复步骤

1. 替换未定义的$node变量

在findNextFD函数中,将$node替换为当前处理的元素参数:

// 修改getNext函数内的map回调
if (Array.from(v.querySelectorAll('.fnoteBtn')).length > 0) {
  elements = v.querySelectorAll('.fnoteBtn');
  return elements
}

// 修改searchFor函数
var searchFor = function(_selector, el) {
  if (!el) {
    return null;
  }
  if(el.matches(_selector)) {
    return el;
  } else {
    var found = null;
    el.children.forEach(function(child) {
      var result = searchFor(_selector, child);
      if (result) found = result;
    });
    return found;
  }
}

2. 移除jQuery依赖,改用原生JS

// 原代码
$(totalFootNote[selectIndex-1]).html()
// 修改为
totalFootNote[selectIndex-1].textContent

3. 修复selectIndex逻辑

var selectIndex = (function(){
  if (selectedNode.className == 'fnoteWrap') {
    var num = selectedNode.childNodes[0].firstChild.nodeValue.replace(/[^0-9]/g,'');
    return parseInt(num, 10) || 0;
  } else {
    return -1; // 非脚注元素返回无效值,避免后续比较错误
  }
})();

内容的提问来源于stack exchange,提问作者James Gantz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 18:54:23