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

CKEditor BalloonPanel滚动异常及关闭后复现问题排查

Fix: CKEditor BalloonPanel Reappears After Closing When Scrolling

Hey there, you've already nailed the scroll-shifting issue with BalloonPanel, but now you're facing a new problem: closed panels pop back up when you scroll the editor. Let's break down what's causing this and how to fix it.

What's Going Wrong

  1. Orphaned Scroll Listeners: Every time you run the grade_level_score command, you add a new scroll listener—but you never clean up the old ones. Those leftover listeners keep triggering attach calls, bringing closed panels back from the dead.
  2. Unupdated Panels Array: When a user closes a BalloonPanel, your code doesn't remove that panel instance from the panels array. The scroll listener still tries to reattach those already-destroyed panels.
  3. Dangling Variable Reference: The scroll listener uses responseGradeLevelScoreWS, a local variable from the command execution. When scrolling happens later, this might reference stale data, leading to incorrect element rebinding.

The Fix

We need to tweak three key parts of your code: manage scroll listeners properly, clean up the panels array when panels close, and make the scroll listener rely on current panel state instead of external variables.

Here's the updated full plugin code:

(function() { 
    var arr = []; 
    var panels = []; 
    var scrollListener = null; // Track the scroll listener to clean it up later

    var saveCmd = { 
        readOnly: 1, 
        modes: { wysiwyg: 1, source: 1 }, 
        exec: function( editor ) { 
            if ( editor.fire( 'grade_level_score' ) ) { 
                var $form = editor.element.$.form; 

                // Step 1: Remove old scroll listener if it exists
                if (scrollListener) {
                    editor.window.off('scroll', scrollListener.input);
                    scrollListener = null;
                }

                // Destroy existing panels and reset arrays
                let panel; 
                while ( ( panel = panels.pop() ) ) { 
                    panel.destroy(); 
                } 
                arr = []; 
                panels = []; 

                // Collect editable areas
                const ele = $(editor.editable().$); 
                let i = 1; 
                $('span', ele).each(function() { 
                    const iceIceEditableClass = "ice-ice-editable"; 
                    if ($(this).hasClass(iceIceEditableClass)) { 
                        const userEditableId = `user-editable-${i}`; 
                        this.setAttribute("id", userEditableId); 
                        arr.push({ 
                            id: userEditableId, 
                            userEditableArea: $(this).text() 
                        }); 
                        i++; 
                    } 
                }); 

                // Mock API response (keep your existing test data)
                const result = '{"textAreas":[{"id":"user-editable-1","userEditableArea":"[Insert information specific to what is being addressed (a brief description of request(s) and/or concern(s). Specific training resource document for letter writing assistance will be referenced here.] ","score":22.24,"readingGrade":7,"issues":["asdf","zxcv"]},{"id":"user-editable-2","userEditableArea":"[Insert information specific to what is being addressed (a brief description of request(s) and/or concern(s). Specific training resource document for letter writing assistance will be referenced here.] ","score":22.24,"readingGrade":0,"issues":[]},{"id":"user-editable-3","userEditableArea":"[Insert information specific to what is being addressed (a brief description of request(s) and/or concern(s). Specific training resource document for letter writing assistance will be referenced here.] ","score":22.24,"readingGrade":0,"issues":[]},{"id":"user-editable-4","userEditableArea":"[Insert information specific to what is being addressed (a brief description of request(s) and/or concern(s). Specific training resource document for letter writing assistance will be referenced here.] ","score":22.24,"readingGrade":0,"issues":[]}]}'; 
                const responseGradeLevelScoreWS = JSON.parse(result); 

                // Create panels and attach close handlers
                for (i = 0; i < responseGradeLevelScoreWS.textAreas.length; i++) { 
                    const area = responseGradeLevelScoreWS.textAreas[i];
                    if (area.readingGrade > 6) { 
                        let issues = ''; 
                        area.issues.forEach(issue => {
                            issues += `<p>${issue}</p>`;
                        }); 

                        panel = new CKEDITOR.ui.balloonPanel(editor, { 
                            title: `Grade: ${area.readingGrade}. Score: ${area.score}`, 
                            content: issues || 'There are no suggestions to decrease the grade level score', 
                            width: 500, 
                            height: 120 
                        }); 

                        const element = editor.document.getById(area.id); 
                        panel.attach(element); 
                        panel.registerFocusable(element); 

                        // Step 2: Remove panel from array and destroy when closed
                        panel.on('hide', function() {
                            const panelIndex = panels.indexOf(this);
                            if (panelIndex !== -1) {
                                panels.splice(panelIndex, 1);
                            }
                            this.destroy();
                        });

                        panels.push(panel); 
                    } 
                } 

                // Step 3: Add new scroll listener only if there are panels
                if (panels.length > 0) {
                    scrollListener = CKEDITOR.tools.eventsBuffer(100, function() { 
                        panels.forEach(panelInstance => {
                            // Only reattach if the panel is still valid
                            if (panelInstance.attachElement && panelInstance.attachElement.$) {
                                panelInstance.attach(panelInstance.attachElement, { 
                                    focusElement: false, 
                                    show: false 
                                });
                            }
                        });
                    }); 
                    editor.window.on('scroll', scrollListener.input); 
                }

                // Form submit logic (keep your existing code)
                if ($form) { 
                    try { 
                        //$form.submit(); 
                    } catch (e) { 
                        if ($form.submit.click) $form.submit.click(); 
                    } 
                } 
            } 
        } 
    }; 

    const pluginName = 'grade_level_score'; 
    CKEDITOR.plugins.add(pluginName, { 
        lang: 'en,en-au,en-ca,en-gb,es,es-mx', 
        icons: 'grade_level_score', 
        hidpi: true, 
        init: function(editor) { 
            if (editor.elementMode !== CKEDITOR.ELEMENT_MODE_REPLACE) return; 
            const command = editor.addCommand(pluginName, saveCmd); 
            command.startDisabled = !editor.element.$.form; 
            editor.ui.addButton && editor.ui.addButton('Grade_Level_Score', { 
                label: "Grade Level Score", 
                command: pluginName, 
                toolbar: 'custom,100' 
            }); 
        } 
    }); 
})();

Key Changes Explained

  1. Scroll Listener Cleanup: We now store the scroll listener reference in scrollListener and remove it before adding a new one. This prevents multiple listeners from piling up and triggering unwanted reattachments.
  2. Panel Close Handling: Each panel gets a hide event listener that removes it from the panels array and destroys it fully. This ensures closed panels aren't processed during scroll events.
  3. Scroll Listener Logic: Instead of relying on the stale responseGradeLevelScoreWS variable, the listener uses each panel's own attachElement property to reattach. We also add a check to make sure the panel is still valid before attempting to reattach.
  4. Conditional Listener Registration: We only add the scroll listener if there are active panels, which saves unnecessary performance overhead.

With these changes, closed panels will stay gone when you scroll, while keeping the fixed attachment behavior you already implemented.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:26:32