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
- Orphaned Scroll Listeners: Every time you run the
grade_level_scorecommand, you add a new scroll listener—but you never clean up the old ones. Those leftover listeners keep triggeringattachcalls, bringing closed panels back from the dead. - Unupdated Panels Array: When a user closes a BalloonPanel, your code doesn't remove that panel instance from the
panelsarray. The scroll listener still tries to reattach those already-destroyed panels. - 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
- Scroll Listener Cleanup: We now store the scroll listener reference in
scrollListenerand remove it before adding a new one. This prevents multiple listeners from piling up and triggering unwanted reattachments. - Panel Close Handling: Each panel gets a
hideevent listener that removes it from thepanelsarray and destroys it fully. This ensures closed panels aren't processed during scroll events. - Scroll Listener Logic: Instead of relying on the stale
responseGradeLevelScoreWSvariable, the listener uses each panel's ownattachElementproperty to reattach. We also add a check to make sure the panel is still valid before attempting to reattach. - 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
相关产品推荐
相关产品推荐

