返回

TinyMCE自定义对话框TabPanel标签切换事件失效?3招解决!

javascript

TinyMCE 自定义对话框 TabPanel 标签切换事件失效问题解决

在 TinyMCE V6 自定义对话框中,使用 TabPanel 组件时,你可能遇到 onTabChange 事件无法触发的问题。试图用 onChange 事件也无济于事。 这篇博客会帮你彻底解决这个问题,让你能够顺利捕获标签页切换事件。

问题原因分析

问题的根源在于 TinyMCE 自定义对话框的 API 版本和事件处理方式。 原代码 onTabChange: (dialogApi, details) => {} 试图在 dialog 定义级别上处理这个事件, 这个方式通常用在 dialog 的 button 一类。 TinyMCE 内部处理 TabPanel 组件的 tabchange 事件时,需要特定的绑定方式才能正确触发。直接在 dialog 对象上定义 onTabChange 是不能生效的。

解决方案

提供三种解决策略,由易到难,分别针对不同需求场景。

方案一:使用 onChange 事件(简单方案)

虽然问题的原贴说无法用, 但是我们根据官方文档还是有办法用的, 但要求 tab 内组件至少有一个 可以联动的组件,比如一个输入框或选择框。如果 tab 内没有这种, 则不能使用这个方案。

  1. 原理: onChange 事件会在对话框内任何表单字段的值发生变化时触发。我们可以巧妙利用这一点,通过一个隐藏的输入框,来间接触发并获取当前激活的标签页。

  2. 代码示例:

let activeTab = 'Insert_UGC_From_ID'; // 默认激活的标签页
const InsertUgcPanel = {
    title: 'Insertion',
    body: {
        type: 'tabpanel',
        tabs: [
            {
                name: 'Insert_UGC_From_ID',
                title: '通过 ID 插入',
                items: [
                     //...其他组件
                    {
                        type: 'input',
                        name: 'active_tab_hidden_id', //用于追踪 tab
                        hidden: true, //添加隐藏
                        value: 'Insert_UGC_From_ID' // 初始值
                    }
                ]
            },
           {
                name: 'Insert_UGC_From_URL',
                title: '通过 URL 插入',
                items: [
                     //...其他组件
                    {
                        type: 'input',
                        name: 'active_tab_hidden_url', //用于追踪 tab
                        hidden: true,
                        value: 'Insert_UGC_From_URL' //初始化 value 无意义,必须存在
                    }
                ]
            },
            // ... 其他标签页
        ]
    },
    buttons: [/* ... 按钮配置 ... */],
     onChange: (dialogApi, details) => {
        if (details.name.startsWith('active_tab_hidden')) { // 如果与追踪 tab 相关的事件触发
            activeTab = dialogApi.getData()[details.name]; // 获得新值
        }

        console.log("当前标签:", activeTab); //debug 使用, 请部署时移除
    },
    onAction: (dialogApi, details) => {
        if (details.name === 'insert-UGC-button') {
             //可以读取到被修改过的 activeTab
            const data = dialogApi.getData();
            console.log("最后点击确定时候的 Tab:",activeTab);
            dialogApi.close();
        } else if (details.name === 'doesnothing') {
            dialogApi.close();
        }
    }
};

  1. 实现思路解释:
    每个 tab 内添加一个 input 组件。
    name 分别命名 active_tab_hidden_id 以及 active_tab_hidden_url, 来追踪当前点击的是哪个 tab。
    初始 value 没有作用, 随意赋值, 但必须要保证有一个初始的 value, 可以用 tabname 来赋值.

  2. 安全建议: 这个隐藏字段只是用来辅助触发事件,不存储敏感数据。

方案二:利用事件冒泡 (进阶技巧)

如果你希望在不添加额外隐藏组件的情况下实现对 tab 切换进行监听,可以使用手动事件触发, 但稍显繁琐

  1. 原理: TinyMCE 的对话框组件基于 HTML 构建。我们可以通过模拟点击标签页的 DOM 元素,手动触发 tabchange 事件。

  2. 代码示例:

let activeTab = 'Insert_UGC_From_ID'; // 默认激活的标签页

const InsertUgcPanel = {
    title: 'Insertion',
    body: {
        type: 'tabpanel',
        tabs: [
             {
                name: 'Insert_UGC_From_ID',
                title: '通过 ID 插入',
                items: [
                    // 其他项目
                ]
            },
            {
                name: 'Insert_UGC_From_URL',
                title: '通过URL插入',
                items: [
                    //其他项目
                ]
            }
        ]
    },
    buttons: [ /* ... 按钮配置... */ ],
      onAction: (dialogApi, details) => {
        if (details.name === 'insert-UGC-button') {
            //使用activeTab的值
            const data = dialogApi.getData();
            console.log("最终Tab:",activeTab);
            dialogApi.close();
        } else if (details.name === 'doesnothing') {
            dialogApi.close();
        }
    },
    onClose: (dialogApi)=>{
         //对话框销毁时候移除监听, 避免重复调用.
        const dialogRoot = dialogApi.getRootEl(); //获取根元素,这里是整个dialog的div
        dialogRoot.removeEventListener('click',tabChangeListener);
    },
    onMount: (dialogApi) => {
          // 添加 tab 页的点击事件监听

        const dialogRoot = dialogApi.getRootEl(); //获取根元素,这里是整个dialog的div
        dialogRoot.addEventListener('click',tabChangeListener);

         function tabChangeListener(event) {

             //判断是 tab 的点击
             if (event.target.classList.contains('tox-tab')) {
                //通过解析tox-tab的aria-controls="tab-name"中的tab-name值
                 const targetTabId = event.target.getAttribute('aria-controls');

                if (targetTabId)
                 {

                     const tabName=targetTabId;
                       if (activeTab !== tabName)
                        {

                           activeTab = tabName; //进行值的变更
                         console.log("切换了, 新tab:", activeTab); //输出debug,部署请移除
                      }
                 }

            }

         }
    }
};

  1. 代码解析:
  • onMount:在对话框渲染完成后执行,添加对整个对话框元素的 click 监听,用于后续判断事件发生源。
  • 通过 getRootEl() 获取对话框的根元素。
  • 增加一个名为 tabChangeListener 的监听函数,并绑定到对话框元素的 click 事件。
  • 对事件的 target (触发事件的具体 dom)做判断。 通过分析,所有的 tab 均有 tox-tab 的 class,所以通过 event.target.classList.contains('tox-tab') 判断这个 click 事件是否由 tab 发出。
  • 再从 tab 对象中取得 `aria-controls`,根据分析,可以从其中拿到 `tabName`.
    
  • 进行 activeTab 变量的值修改, 并执行自己的 onTabChange 逻辑。
  • onClose: 添加移除 click 监听逻辑。
  1. 进阶- 事件委托: 如果对话框结构比较复杂,也可以使用事件委托,将事件监听绑定到对话框的根元素,然后根据事件目标(event.target)来判断是否是标签页的点击事件。

方案三: 反射获取并修改(非必要不使用)

警告: 反射属于非公开的 api,TinyMCE 随时可以进行版本升级,所以尽量不使用这种方式 。如果你的业务属于必须确保稳定的类型, 请跳过这种方式. 如果前两种能实现,就不要使用这个方式。

这个是万策尽的方式,提供给其他方法完全不行的时候尝试使用。

  1. 原理: 通过 TinyMCE 提供的底层 getEl() 以及 getData 反射获得值

  2. 代码及使用:

let activeTab = 'Insert_UGC_From_ID';

const InsertUgcPanel = {
     title: 'Insertion',
    body: {
        type: 'tabpanel',
        tabs: [
             {
                name: 'Insert_UGC_From_ID',
                title: '通过 ID 插入',
                items: [
                ]
            },
            {
                name: 'Insert_UGC_From_URL',
                title: '通过URL插入',
                items: [
                ]
            }

        ]
    },
    buttons: [/* ... 按钮 ... */],
    onAction: (dialogApi, details) => {
        if (details.name === 'insert-UGC-button') {
             //可以拿到 activeTab 的值
             console.log("最后选定:",activeTab)
            const data = dialogApi.getData();
            dialogApi.close();
        } else if (details.name === 'doesnothing') {
            dialogApi.close();
        }
    },
      onMount:(dialogApi)=>{
         //利用反射获得实例
        let instance = dialogApi.getInstance().componentStack[0];

        //利用反射, 对 onChange 进行 override
        let originalOnChange = instance.onChange;

        // 重写
        instance.onChange =  (...a)=>{

             originalOnChange.apply(instance, a);
            let data = dialogApi.getData();

              for (let k in data)
                {
                  if(Object.hasOwn(data, k))//遍历,找到被选中的 Tab
                    {
                        let nowValue = data[k];
                       if(typeof(nowValue)=='boolean' && nowValue == true)
                        {

                            //判断 Tab 是否发生了改变
                          if(activeTab !== k)
                            {

                                activeTab = k;  //直接赋值为 tab 的 name

                                //执行你的逻辑

                              console.log('现在选中的 Tab:',activeTab);
                            }

                            break;
                        }
                   }
               }
        };

    }
};
  1. 代码分析:
    利用getInstance().componentStack[0]获取组件堆栈并修改onChange属性, hook 住内部的onChange. 再使用getData获取当前的数据变化,由于组件运行机制, 一旦切换, 必定会引起一次数据的变更.

    利用 tab 被切换的特性,我们可以分析 tab 中, 被切换的必定导致一个boolean类型 valuetrue,找到这个并确认与之前的记录不符合, 进行修改.

总结

以上三种方法都可以实现 TinyMCE 自定义对话框中 TabPanel 组件的标签切换事件监听。方案一最简单,但需要界面配合增加元素。方案二更为灵活, 可以使用自定义的事件, 不需要修改结构,但使用了事件冒泡相关的技术。方案三用了一些"非常规"手段,可以作为最终解决方案。可以根据实际情况选择最适合你的方案。