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

如何在触发特殊字符后切换焦点至下拉面板并实现插件导航

问题:斜杠命令下拉面板焦点切换失败,无法键盘导航

需求与现状

  • 需求:在编辑器输入特殊字符(.)触发斜杠命令下拉面板后,将焦点从编辑器切换至面板,并实现插件间的键盘导航
  • 当前问题:下拉面板能正常打开,但焦点未切换到面板,无法通过键盘上下键导航插件

相关实现代码

_getPluginListItemsDefinitons() {
        const editor = this.editor;
        const locale = editor.locale;

        this._dropdown = createDropdown(locale);
    
        this._commandItems = [];
        const commandNames = ['bold','italic','underline','horizontalLine','bulletedList','numberedList','insertTable','codeBlock','paragraph','table','subscript','superscript','widgetBox'];
    
        commandNames.forEach((commandName) => {
            const command = editor.commands.get(commandName);
            if (command) {
                const buttonModel = {
                    type: 'button',
                    model: {
                        label: commandName.charAt(0).toUpperCase() + commandName.slice(1),
                        withText: true,
                        icon: icons[commandName] || icons.cog,
                        commandName: commandName,
                    },
                };
                this._commandItems.push(buttonModel);
            }
        });
    
        this._commandItems.sort((a, b) => {
            return a.model.label.localeCompare(b.model.label);
        });
    
        this._addItemsToDropdown(this._commandItems);
        this._createCustomToClass();

        this._dropdown.isOpen = true;
        console.log("dropdown",this._dropdown)
        if(this._dropdown.isOpen === true){
            this._dropdown.render();
            this.focusTracker = new FocusTracker();
            console.log("focusTracker", this.focusTracker)
            const elementFocus = this._dropdown.panelView;
            this.focusTracker.isFocused = true
            this.focusTracker.focusedElement = elementFocus
            this.focusables = new ViewCollection();
            console.log("focusables", this.focusables)
            
            const focusableElements = this._dropdown.element.querySelectorAll('button, input, [tabindex]:not([tabindex="-1"])');
            focusableElements.forEach(element => {
                const view = new View(element); 
                this.focusables.add(view);
            });
            const firstButton = focusableElements[1];
                if (firstButton) {
                    firstButton.focus();
                    firstButton.style.setProperty('background-color', '#007bff', 'important');
                    firstButton.style.setProperty('color', '#ffff', 'important');
                }
            this.keystrokes = new KeystrokeHandler();
            this.focusCycler = new FocusCycler({
                focusables: focusableElements,
                focusTracker: this._dropdown.panelView,
                keystrokeHandler: this.keystrokes,
                actions: {
                    focusPrevious: ['arrowup'],
                    focusNext: ['arrowdown']
                }
            });
            console.log("focus cycler", this.focusCycler);
            this._dropdown.panelView.on('keydown', (evt, data) => {
                const keyCode = data.keyCode;
                if (keyCode === 38) { 
                    this.focusCycler.focusPrevious(); 
                    data.preventDefault();
                } else if (keyCode === 40) {
                    this.focusCycler.focusNext(); 
                    data.preventDefault();
                }
            });
        }

        this.listenTo(this._dropdown, 'execute', (evt, data) => {
            const commandName = evt.source.commandName;
            if (commandName) {
                this.customExecution(commandName);
                if (!this._dropdown.isOpen) {
                    editor.editing.view.focus();
                }
            }
            this._dropdown.isOpen = false;
            this.hideDropdown();
        });
    }

问题分析与修复方案

原代码的核心问题在于FocusTracker、FocusCycler的使用不符合CKEditor API规范,以及焦点设置时机错误,以下是针对性修复:

关键修复点

  1. 修正FocusTracker的使用:
    不能直接赋值isFocused和focusedElement,需通过add()方法注册要跟踪的元素,FocusTracker会自动监听焦点状态变化。

  2. 修正FocusCycler参数:
    focusables必须传入ViewCollection实例而非原生DOM数组;focusTracker需传入实例化的FocusTracker对象,而非面板视图本身。

  3. 确保DOM挂载后再设置焦点:
    面板render()后需等待元素插入DOM树再调用focus(),可通过setTimeout或编辑器的change方法确保DOM就绪,同时主动让编辑器失焦。

  4. 简化键盘事件处理:
    利用KeystrokeHandler的API绑定快捷键,而非直接监听keydown事件,更符合CKEditor的生态逻辑。

修复后的代码

_getPluginListItemsDefinitons() {
    const editor = this.editor;
    const locale = editor.locale;

    this._dropdown = createDropdown(locale);

    this._commandItems = [];
    const commandNames = ['bold','italic','underline','horizontalLine','bulletedList','numberedList','insertTable','codeBlock','paragraph','table','subscript','superscript','widgetBox'];

    commandNames.forEach((commandName) => {
        const command = editor.commands.get(commandName);
        if (command) {
            const buttonModel = {
                type: 'button',
                model: {
                    label: commandName.charAt(0).toUpperCase() + commandName.slice(1),
                    withText: true,
                    icon: icons[commandName] || icons.cog,
                    commandName: commandName,
                },
            };
            this._commandItems.push(buttonModel);
        }
    });

    this._commandItems.sort((a, b) => {
        return a.model.label.localeCompare(b.model.label);
    });

    this._addItemsToDropdown(this._commandItems);
    this._createCustomToClass();

    this._dropdown.isOpen = true;
    
    if(this._dropdown.isOpen === true){
        this._dropdown.render();
        
        // 1. 正确初始化FocusTracker
        this.focusTracker = new FocusTracker();
        this.focusables = new ViewCollection();
        
        // 2. 收集可聚焦元素并注册到跟踪器
        const focusableElements = this._dropdown.element.querySelectorAll('button, input, [tabindex]:not([tabindex="-1"])');
        focusableElements.forEach(element => {
            const view = new View(element); 
            this.focusables.add(view);
            this.focusTracker.add(element);
        });
        
        // 3. 正确初始化FocusCycler
        this.keystrokes = new KeystrokeHandler();
        this.keystrokes.listenTo(this._dropdown.panelView);
        
        this.focusCycler = new FocusCycler({
            focusables: this.focusables,
            focusTracker: this.focusTracker,
            keystrokeHandler: this.keystrokes,
            actions: {
                focusPrevious: 'arrowup',
                focusNext: 'arrowdown'
            }
        });
        
        // 4. DOM就绪后设置焦点到第一个元素
        editor.editing.view.change(writer => {
            setTimeout(() => {
                const firstFocusable = this.focusables.first;
                if (firstFocusable) {
                    firstFocusable.element.focus();
                    firstFocusable.element.style.setProperty('background-color', '#007bff', 'important');
                    firstFocusable.element.style.setProperty('color', '#fff', 'important');
                }
                // 让编辑器失焦,确保焦点切换到面板
                editor.editing.view.document.blur();
            }, 0);
        });
    }

    this.listenTo(this._dropdown, 'execute', (evt, data) => {
        const commandName = evt.source.commandName;
        if (commandName) {
            this.customExecution(commandName);
            if (!this._dropdown.isOpen) {
                editor.editing.view.focus();
            }
        }
        this._dropdown.isOpen = false;
        this.hideDropdown();
    });
}

额外排查点

  • 若焦点仍无法切换,检查编辑器是否有焦点锁定机制,需临时允许焦点切换到面板
  • 确认_addItemsToDropdown方法正确添加按钮,且按钮元素默认tabindex=0(可聚焦)
  • 可在focus()后打印document.activeElement,确认焦点是否真的落在目标元素上

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 04:59:50