@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
22import { TextSelection } from 'prosemirror-state' ;
33import { initTestEditor , loadTestDataForEditorTests } from '../../tests/helpers/helpers.js' ;
44import { calculateResolvedParagraphProperties } from './resolvedPropertiesCache.js' ;
5+ import { hasOnlyBreakContent } from './paragraph.js' ;
56
67describe ( 'Paragraph Node' , ( ) => {
78 let docx , media , mediaFiles , fonts , editor ;
@@ -124,4 +125,165 @@ describe('Paragraph Node', () => {
124125
125126 expect ( editor . state . doc . textContent ) . toBe ( 't' ) ;
126127 } ) ;
128+
129+ describe ( 'hasOnlyBreakContent' , ( ) => {
130+ it ( 'returns true for a list paragraph containing only a lineBreak' , ( ) => {
131+ let paragraphPos = null ;
132+ editor . state . doc . descendants ( ( node , pos ) => {
133+ if ( node . type . name === 'paragraph' && paragraphPos == null ) {
134+ paragraphPos = pos ;
135+ return false ;
136+ }
137+ return true ;
138+ } ) ;
139+
140+ const lineBreakNode = editor . schema . nodes . lineBreak . create ( ) ;
141+ const tr = editor . state . tr . insert ( paragraphPos + 1 , lineBreakNode ) ;
142+ editor . view . dispatch ( tr ) ;
143+
144+ const paragraph = editor . state . doc . nodeAt ( paragraphPos ) ;
145+ expect ( hasOnlyBreakContent ( paragraph ) ) . toBe ( true ) ;
146+ } ) ;
147+
148+ it ( 'returns false for a paragraph with visible text' , ( ) => {
149+ editor . commands . insertContent ( 'visible text' ) ;
150+ const paragraph = editor . state . doc . content . content [ 0 ] ;
151+ expect ( hasOnlyBreakContent ( paragraph ) ) . toBe ( false ) ;
152+ } ) ;
153+
154+ it ( 'returns false for an empty paragraph with no content at all' , ( ) => {
155+ const paragraph = editor . state . doc . content . content [ 0 ] ;
156+ expect ( hasOnlyBreakContent ( paragraph ) ) . toBe ( false ) ;
157+ } ) ;
158+
159+ it ( 'returns false for null or non-paragraph nodes' , ( ) => {
160+ expect ( hasOnlyBreakContent ( null ) ) . toBe ( false ) ;
161+ expect ( hasOnlyBreakContent ( undefined ) ) . toBe ( false ) ;
162+
163+ const runNode = editor . schema . nodes . run . create ( ) ;
164+ expect ( hasOnlyBreakContent ( runNode ) ) . toBe ( false ) ;
165+ } ) ;
166+ } ) ;
167+
168+ it ( 'handles beforeinput in a list paragraph with only a lineBreak (SD-1707)' , ( ) => {
169+ let paragraphPos = null ;
170+ let paragraphNode = null ;
171+ editor . state . doc . descendants ( ( node , pos ) => {
172+ if ( node . type . name === 'paragraph' && paragraphPos == null ) {
173+ paragraphPos = pos ;
174+ paragraphNode = node ;
175+ return false ;
176+ }
177+ return true ;
178+ } ) ;
179+
180+ const numberingProperties = { numId : 1 , ilvl : 0 } ;
181+ const listRendering = {
182+ markerText : '1.' ,
183+ suffix : 'tab' ,
184+ justification : 'left' ,
185+ path : [ 1 ] ,
186+ numberingType : 'decimal' ,
187+ } ;
188+
189+ // Make the paragraph a list item
190+ let tr = editor . state . tr . setNodeMarkup ( paragraphPos , null , {
191+ ...paragraphNode . attrs ,
192+ paragraphProperties : {
193+ ...( paragraphNode . attrs . paragraphProperties || { } ) ,
194+ numberingProperties,
195+ } ,
196+ numberingProperties,
197+ listRendering,
198+ } ) ;
199+ editor . view . dispatch ( tr ) ;
200+
201+ // Insert a lineBreak so the paragraph has only break content
202+ const lineBreakNode = editor . schema . nodes . lineBreak . create ( ) ;
203+ tr = editor . state . tr . insert ( paragraphPos + 1 , lineBreakNode ) ;
204+ editor . view . dispatch ( tr ) ;
205+
206+ const updatedParagraph = editor . state . doc . nodeAt ( paragraphPos ) ;
207+ calculateResolvedParagraphProperties ( editor , updatedParagraph , editor . state . doc . resolve ( paragraphPos ) ) ;
208+
209+ // Place cursor inside the paragraph
210+ tr = editor . state . tr . setSelection ( TextSelection . create ( editor . state . doc , paragraphPos + 1 ) ) ;
211+ editor . view . dispatch ( tr ) ;
212+
213+ const beforeInputEvent = new InputEvent ( 'beforeinput' , {
214+ data : 'a' ,
215+ inputType : 'insertText' ,
216+ bubbles : true ,
217+ cancelable : true ,
218+ } ) ;
219+ editor . view . dom . dispatchEvent ( beforeInputEvent ) ;
220+
221+ expect ( editor . state . doc . textContent ) . toBe ( 'a' ) ;
222+ } ) ;
223+
224+ it ( 'does NOT intercept beforeinput for a list paragraph with visible text' , ( ) => {
225+ let paragraphPos = null ;
226+ let paragraphNode = null ;
227+ editor . state . doc . descendants ( ( node , pos ) => {
228+ if ( node . type . name === 'paragraph' && paragraphPos == null ) {
229+ paragraphPos = pos ;
230+ paragraphNode = node ;
231+ return false ;
232+ }
233+ return true ;
234+ } ) ;
235+
236+ const numberingProperties = { numId : 1 , ilvl : 0 } ;
237+ const listRendering = {
238+ markerText : '1.' ,
239+ suffix : 'tab' ,
240+ justification : 'left' ,
241+ path : [ 1 ] ,
242+ numberingType : 'decimal' ,
243+ } ;
244+
245+ // Insert text first, then make it a list item
246+ editor . commands . insertContent ( 'hello' ) ;
247+
248+ paragraphPos = null ;
249+ paragraphNode = null ;
250+ editor . state . doc . descendants ( ( node , pos ) => {
251+ if ( node . type . name === 'paragraph' && paragraphPos == null ) {
252+ paragraphPos = pos ;
253+ paragraphNode = node ;
254+ return false ;
255+ }
256+ return true ;
257+ } ) ;
258+
259+ let tr = editor . state . tr . setNodeMarkup ( paragraphPos , null , {
260+ ...paragraphNode . attrs ,
261+ paragraphProperties : {
262+ ...( paragraphNode . attrs . paragraphProperties || { } ) ,
263+ numberingProperties,
264+ } ,
265+ numberingProperties,
266+ listRendering,
267+ } ) ;
268+ editor . view . dispatch ( tr ) ;
269+
270+ const updatedParagraph = editor . state . doc . nodeAt ( paragraphPos ) ;
271+ calculateResolvedParagraphProperties ( editor , updatedParagraph , editor . state . doc . resolve ( paragraphPos ) ) ;
272+
273+ // Place cursor at the end of the text
274+ const endPos = paragraphPos + updatedParagraph . nodeSize - 1 ;
275+ tr = editor . state . tr . setSelection ( TextSelection . create ( editor . state . doc , endPos ) ) ;
276+ editor . view . dispatch ( tr ) ;
277+
278+ const beforeInputEvent = new InputEvent ( 'beforeinput' , {
279+ data : 'x' ,
280+ inputType : 'insertText' ,
281+ bubbles : true ,
282+ cancelable : true ,
283+ } ) ;
284+
285+ // The handler should NOT intercept because the paragraph has visible text
286+ const prevented = ! editor . view . dom . dispatchEvent ( beforeInputEvent ) ;
287+ expect ( prevented ) . toBe ( false ) ;
288+ } ) ;
127289} ) ;
0 commit comments