@@ -44,6 +44,8 @@ import {
4444import { splitIntoGraphemes } from "../../../utils/textUtils" ;
4545import { kCanvasElementClass } from "../overlay/canvasElementUtils" ;
4646import { kBloomCanvasSelector } from "../../js/bloomImages" ;
47+ import OverflowChecker from "../../OverflowChecker/OverflowChecker" ;
48+ import { doesContainingPageHaveSameSizeMode } from "./gameUtilities" ;
4749
4850export const GamePromptDialog : React . FunctionComponent < IGamePromptDialogProps > = props => {
4951 const promptL10nId = props . prompt ?. getAttribute ( "data-caption-l10nid" ) ;
@@ -357,102 +359,158 @@ const initializeDialog = (prompt: HTMLElement, tg: HTMLElement | null) => {
357359 if ( ! promptEditable ) {
358360 throw new Error ( "No promptEditable in dragActivity" ) ;
359361 }
360- promptEditable . innerHTML = editable . innerHTML ; // copy back to the permanent element so it gets saved.
361- const promptText = editable . textContent ?? "" ;
362- // Split the prompt text into letter groups consisting of a base letter and any combining marks.
363- const letters = splitIntoGraphemes ( promptText ) ;
364- const draggables = Array . from (
365- page . getElementsByClassName ( kCanvasElementClass + " draggable-text" )
366- ) as HTMLElement [ ] ;
367- // make sure we get some reasonable offsetWidth for the first one, if there are
368- // any letters. (Can become display:none if we have no letters.)
369- setDraggableText ( draggables [ 0 ] , letters [ 0 ] ) ;
370- draggables [ 0 ] . classList . remove ( "bloom-unused-in-lang" ) ;
371- const separation = draggables [ 0 ] . offsetWidth + 15 ; // enhance: may want to increase this
372- // How many can we fit in one row inside the parent?
373- const maxBubbles = Math . floor (
374- ( draggables [ 0 ] . parentElement ?. offsetWidth ?? 0 - draggableX ) /
375- separation
376- ) ;
377- // truncate to the number of draggables we can display
378- // This is important because (e.g., with autorepeat or paste) we can get a massive number of draggables
379- // very quickly, and performance degrades badly, making it hard to recover. Also, until the page relaods,
380- // ones beyond this would be off-page and difficult to deal with. And when it does reload they will all
381- // be on top of each other and only just visible.
382- letters . splice ( maxBubbles ) ;
383- const newBubbles : HTMLElement [ ] = [ ] ;
384- if ( draggables . length < letters . length ) {
385- // We have more letters than draggables. We'll add more draggables.
386- const lastDraggable = draggables [ draggables . length - 1 ] ;
387- for ( let i = draggables . length ; i < letters . length ; i ++ ) {
388- const newDraggable = lastDraggable . cloneNode (
389- true
390- ) as HTMLElement ;
391- setGeneratedDraggableId ( newDraggable ) ;
392- // Ensure the new draggable starts out empty. See BL-14348.
393- // (This covers all languages present, visible or not.)
394- const paras = newDraggable . querySelectorAll (
395- "div.Letter-style>p"
362+ // This 'forever' loop normally exits after one iteration, but it makes it possible to restart
363+ // with 'continue' if we need to adjust the width of the draggables.
364+ for ( ; ; ) {
365+ promptEditable . innerHTML = editable . innerHTML ; // copy back to the permanent element so it gets saved.
366+ const promptText = editable . textContent ?? "" ;
367+ // Split the prompt text into letter groups consisting of a base letter and any combining marks.
368+ const letters = splitIntoGraphemes ( promptText ) ;
369+ const draggables = Array . from (
370+ page . getElementsByClassName (
371+ kCanvasElementClass + " draggable-text"
372+ )
373+ ) as HTMLElement [ ] ;
374+ // make sure we get some reasonable offsetWidth for the first one, if there are
375+ // any letters. (Can become display:none if we have no letters.)
376+ setDraggableText ( draggables [ 0 ] , letters [ 0 ] ) ;
377+ draggables [ 0 ] . classList . remove ( "bloom-unused-in-lang" ) ;
378+ const separation = draggables [ 0 ] . offsetWidth + 15 ; // enhance: may want to increase this
379+ // How many can we fit in one row inside the parent?
380+ const maxBubbles = Math . floor (
381+ ( draggables [ 0 ] . parentElement ?. offsetWidth ?? 0 - draggableX ) /
382+ separation
383+ ) ;
384+ // truncate to the number of draggables we can display
385+ // This is important because (e.g., with autorepeat or paste) we can get a massive number of draggables
386+ // very quickly, and performance degrades badly, making it hard to recover. Also, until the page relaods,
387+ // ones beyond this would be off-page and difficult to deal with. And when it does reload they will all
388+ // be on top of each other and only just visible.
389+ letters . splice ( maxBubbles ) ;
390+ const newBubbles : HTMLElement [ ] = [ ] ;
391+ if ( draggables . length < letters . length ) {
392+ // We have more letters than draggables. We'll add more draggables.
393+ const lastDraggable = draggables [ draggables . length - 1 ] ;
394+ for ( let i = draggables . length ; i < letters . length ; i ++ ) {
395+ const newDraggable = lastDraggable . cloneNode (
396+ true
397+ ) as HTMLElement ;
398+ setGeneratedDraggableId ( newDraggable ) ;
399+ // Ensure the new draggable starts out empty. See BL-14348.
400+ // (This covers all languages present, visible or not.)
401+ const paras = newDraggable . querySelectorAll (
402+ "div.Letter-style>p"
403+ ) ;
404+ paras . forEach ( p => {
405+ p . textContent = "" ;
406+ GameTool . setBlankClass ( p . parentElement as HTMLElement ) ;
407+ } ) ;
408+ lastDraggable . parentElement ?. appendChild ( newDraggable ) ;
409+ makeTargetForDraggable ( newDraggable ) ;
410+ // It's available to push letter groups into
411+ draggables . push ( newDraggable ) ;
412+ // It needs refreshBubbleEditing to be called on it later.
413+ newBubbles . push ( newDraggable ) ;
414+ // It should be removed if we Cancel. This list has a longer lifetime.
415+ createdBubbles . push ( newDraggable ) ;
416+ }
417+ }
418+ // We deliberately don't remove draggables we don't need for this word. They might be in use
419+ // in some other language. This loop, as well as making the ones we want have the right content,
420+ // makes the ones we don't want invisible and empty.
421+ for ( let i = 0 ; i < draggables . length ; i ++ ) {
422+ setDraggableText ( draggables [ i ] , letters [ i ] ) ;
423+ // up to the number of letters we have, they should be visible; others, not.
424+ draggables [ i ] . classList . toggle (
425+ "bloom-unused-in-lang" ,
426+ i >= letters . length
427+ ) ;
428+ getTarget ( draggables [ i ] ) ?. classList . toggle (
429+ "bloom-unused-in-lang" ,
430+ i >= letters . length
396431 ) ;
397- paras . forEach ( p => {
398- p . textContent = "" ;
399- GameTool . setBlankClass ( p . parentElement as HTMLElement ) ;
400- } ) ;
401- lastDraggable . parentElement ?. appendChild ( newDraggable ) ;
402- makeTargetForDraggable ( newDraggable ) ;
403- // It's available to push letter groups into
404- draggables . push ( newDraggable ) ;
405- // It needs refreshBubbleEditing to be called on it later.
406- newBubbles . push ( newDraggable ) ;
407- // It should be removed if we Cancel. This list has a longer lifetime.
408- createdBubbles . push ( newDraggable ) ;
432+ copyContentToTarget ( draggables [ i ] ) ;
409433 }
410- }
411- // We deliberately don't remove draggables we don't need for this word. They might be in use
412- // in some other language. This loop, as well as making the ones we want have the right content,
413- // makes the ones we don't want invisible and empty.
414- for ( let i = 0 ; i < draggables . length ; i ++ ) {
415- setDraggableText ( draggables [ i ] , letters [ i ] ) ;
416- // up to the number of letters we have, they should be visible; others, not.
417- draggables [ i ] . classList . toggle (
418- "bloom-unused-in-lang" ,
419- i >= letters . length
420- ) ;
421- getTarget ( draggables [ i ] ) ?. classList . toggle (
422- "bloom-unused-in-lang" ,
423- i >= letters . length
424- ) ;
425- copyContentToTarget ( draggables [ i ] ) ;
426- }
427- const shuffledDraggables = draggables . slice ( ) ;
428- shuffledDraggables . splice ( letters . length ) ; // don't want any invisible ones taking up space
429- shuffle ( shuffledDraggables , isTheTextInDraggablesTheSame ) ;
430- for ( let i = 0 ; i < shuffledDraggables . length ; i ++ ) {
431- shuffledDraggables [ i ] . style . left = `${ draggableX +
432- i * separation } px`;
433- shuffledDraggables [ i ] . style . top = `${ draggableY } px` ;
434- // Note that we use draggables, not shuffledDraggables, here. We want the targets
435- // in the correct order, not the random order.
436- const t = getTarget ( draggables [ i ] ) ;
437- if ( t ) {
438- t . style . left = `${ targetX + i * separation } px` ;
439- t . style . top = `${ targetY } px` ;
434+ const shuffledDraggables = draggables . slice ( ) ;
435+ shuffledDraggables . splice ( letters . length ) ; // don't want any invisible ones taking up space
436+ shuffle ( shuffledDraggables , isTheTextInDraggablesTheSame ) ;
437+ for ( let i = 0 ; i < shuffledDraggables . length ; i ++ ) {
438+ shuffledDraggables [ i ] . style . left = `${ draggableX +
439+ i * separation } px`;
440+ shuffledDraggables [ i ] . style . top = `${ draggableY } px` ;
441+ // Note that we use draggables, not shuffledDraggables, here. We want the targets
442+ // in the correct order, not the random order.
443+ const t = getTarget ( draggables [ i ] ) ;
444+ if ( t ) {
445+ t . style . left = `${ targetX + i * separation } px` ;
446+ t . style . top = `${ targetY } px` ;
447+ }
448+ }
449+ adjustTarget ( draggables [ 0 ] , getTarget ( draggables [ 0 ] ) , true ) ;
450+ // Must do this AFTER we finish setting the content. Among many other things it does,
451+ // it will attach a ckeditor to the new editable. That does very complex things
452+ // involving timeouts, and by the end of the process, the text gets set back to
453+ // what it was when we started adding the ckeditor. So changes we make after that
454+ // get lost.
455+ newBubbles . forEach ( ( newDraggable : HTMLElement ) => {
456+ theOneCanvasElementManager ! . refreshCanvasElementEditing (
457+ newDraggable . closest ( kBloomCanvasSelector ) as HTMLElement ,
458+ new Bubble ( newDraggable ) ,
459+ true , // attach events
460+ false // don't make it active.
461+ ) ;
462+ } ) ;
463+ // If the bubbles need to get wider, grow them and restart the process.
464+ // This variable also acts as a flag: it gets set only if at least one needed
465+ // to grow.
466+ let maxWidth = 0 ;
467+ const sameSize = doesContainingPageHaveSameSizeMode ( draggables [ 0 ] ) ;
468+ for ( let i = 0 ; i < letters . length ; i ++ ) {
469+ const draggable = draggables [ i ] ;
470+ // might be redundant with the mutation observer, but make sure.
471+ copyContentToTarget ( draggable ) ;
472+ const target = getTarget ( draggables [ i ] ) ;
473+ // We'll measure the overflow of the target, because it has the extra border,
474+ // so it can actually overflow when the draggable doesn't.
475+ const editable = target ?. getElementsByClassName (
476+ "bloom-visibility-code-on"
477+ ) [ 0 ] as HTMLElement ;
478+ if ( editable ) {
479+ const overflowX = OverflowChecker . getSelfOverflowAmounts (
480+ editable
481+ ) [ 0 ] ;
482+ if ( overflowX > 0 ) {
483+ maxWidth = Math . max (
484+ maxWidth ,
485+ overflowX + draggable . offsetWidth
486+ ) ;
487+ if ( ! sameSize ) {
488+ // just adjust this one
489+ draggable . style . width = `${ overflowX +
490+ draggable . offsetWidth } px`;
491+ const target = getTarget ( draggable ) ;
492+ if ( target ) {
493+ target . style . width = `${ overflowX +
494+ draggable . offsetWidth } px`;
495+ }
496+ }
497+ }
498+ }
499+ }
500+ if ( maxWidth > 0 && sameSize ) {
501+ draggables [ 0 ] . style . width = `${ maxWidth } px` ;
502+ const target = getTarget ( draggables [ 0 ] ) ;
503+ adjustTarget ( draggables [ 0 ] , target , true ) ;
504+ }
505+ // Usually we will stop. If we had to adjust a width, start over.
506+ // (This might mean we can't fit as many letters. We also need to reposition
507+ // with a new separation. The loop should not run more than twice, since the
508+ // first iteration will do any growing that is needed.)
509+ if ( maxWidth === 0 ) {
510+ break ;
440511 }
441512 }
442- adjustTarget ( draggables [ 0 ] , getTarget ( draggables [ 0 ] ) , true ) ;
443- // Must do this AFTER we finish setting the content. Among many other things it does,
444- // it will attach a ckeditor to the new editable. That does very complex things
445- // involving timeouts, and by the end of the process, the text gets set back to
446- // what it was when we started adding the ckeditor. So changes we make after that
447- // get lost.
448- newBubbles . forEach ( ( newDraggable : HTMLElement ) => {
449- theOneCanvasElementManager ! . refreshCanvasElementEditing (
450- newDraggable . closest ( kBloomCanvasSelector ) as HTMLElement ,
451- new Bubble ( newDraggable ) ,
452- true , // attach events
453- false // don't make it active.
454- ) ;
455- } ) ;
513+
456514 // This seems to at least somewhat reduce the likelihood of losing focus
457515 // after a keystroke, especially with Keyman multi-character inserts (BL-14098).
458516 // I think it is harmless, since I can't think of any case where the text in the
0 commit comments