@@ -44,6 +44,10 @@ 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" ;
49+ import { max } from "underscore" ;
50+ import { _0 } from "../../../react_components/Progress/ProgressBar.stories" ;
4751
4852export const GamePromptDialog : React . FunctionComponent < IGamePromptDialogProps > = props => {
4953 const promptL10nId = props . prompt ?. getAttribute ( "data-caption-l10nid" ) ;
@@ -357,6 +361,7 @@ const initializeDialog = (prompt: HTMLElement, tg: HTMLElement | null) => {
357361 if ( ! promptEditable ) {
358362 throw new Error ( "No promptEditable in dragActivity" ) ;
359363 }
364+
360365 promptEditable . innerHTML = editable . innerHTML ; // copy back to the permanent element so it gets saved.
361366 const promptText = editable . textContent ?? "" ;
362367 // Split the prompt text into letter groups consisting of a base letter and any combining marks.
@@ -368,77 +373,134 @@ const initializeDialog = (prompt: HTMLElement, tg: HTMLElement | null) => {
368373 // any letters. (Can become display:none if we have no letters.)
369374 setDraggableText ( draggables [ 0 ] , letters [ 0 ] ) ;
370375 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- ) ;
376+ const separation = 15 ; // enhance: may want to increase this
377+
377378 // truncate to the number of draggables we can display
378379 // This is important because (e.g., with autorepeat or paste) we can get a massive number of draggables
379380 // very quickly, and performance degrades badly, making it hard to recover. Also, until the page relaods,
380381 // ones beyond this would be off-page and difficult to deal with. And when it does reload they will all
381382 // be on top of each other and only just visible.
382- letters . splice ( maxBubbles ) ;
383383 const newBubbles : HTMLElement [ ] = [ ] ;
384- if ( draggables . length < letters . length ) {
384+ const makeNewDraggable = ( ) => {
385385 // We have more letters than draggables. We'll add more draggables.
386386 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"
396- ) ;
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 ) ;
409- }
410- }
387+ const newDraggable = lastDraggable . cloneNode ( true ) as HTMLElement ;
388+ setGeneratedDraggableId ( newDraggable ) ;
389+ // Ensure the new draggable starts out empty. See BL-14348.
390+ // (This covers all languages present, visible or not.)
391+ const paras = newDraggable . querySelectorAll ( "div.Letter-style>p" ) ;
392+ paras . forEach ( p => {
393+ p . textContent = "" ;
394+ GameTool . setBlankClass ( p . parentElement as HTMLElement ) ;
395+ } ) ;
396+ lastDraggable . parentElement ?. appendChild ( newDraggable ) ;
397+ makeTargetForDraggable (
398+ newDraggable ,
399+ page . getElementsByClassName ( "bloom-target-row" ) [ 0 ] as
400+ | HTMLElement
401+ | undefined
402+ ) ;
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 ) ;
409+ } ;
410+ // We need this loop to run over all the draggables and all the letters.
411+ // When there are more letters than draggables, we will add a new one if there is room.
412+ // When there are more draggables, or no more room, the remaining ones will be set
413+ // empty for this language.
414+ const iterations = Math . max ( letters . length , draggables . length ) ;
415+ const minHeight = 50 ; // Enhance: get from page somehow
416+ const minWidth = 50 ; // Enhance: get from page somehow
417+ let newHeight = minHeight ;
411418 // We deliberately don't remove draggables we don't need for this word. They might be in use
412419 // in some other language. This loop, as well as making the ones we want have the right content,
413420 // 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 ] ) ;
421+ for ( let i = 0 ; i < iterations ; i ++ ) {
422+ if ( i >= draggables . length ) {
423+ makeNewDraggable ( ) ;
424+ }
425+ const draggable = draggables [ i ] ;
426+ const target = getTarget ( draggable ) as HTMLElement ;
427+ let stopMakingLetters = false ;
428+ if ( i < letters . length ) {
429+ draggable . classList . remove ( "bloom-unused-in-lang" ) ;
430+ target . classList . remove ( "bloom-unused-in-lang" ) ;
431+ setDraggableText ( draggable , letters [ i ] ) ;
432+ copyContentToTarget ( draggable ) ;
433+ target . style . width = `${ minWidth } px` ;
434+ target . style . height = `${ minHeight } px` ;
435+ // We'll measure the overflow of the target, because it has the extra border,
436+ // so it can actually overflow when the draggable doesn't.
437+ const editable = target ?. getElementsByClassName (
438+ "bloom-visibility-code-on"
439+ ) [ 0 ] as HTMLElement ;
440+ // as currently implemented these indicate the overflow of the editable. But it may be bigger than the
441+ // target, and certainly does not allow for a border on the target.
442+ const [
443+ overflowX ,
444+ overflowY
445+ ] = OverflowChecker . getSelfOverflowAmounts ( editable ) ;
446+ const row = target . parentElement as HTMLElement ;
447+ const bloomCanvas = row . parentElement as HTMLElement ;
448+ // The width the editable is currently, plus any overflow, plus the target border and padding in that direction.
449+ const newWidth =
450+ overflowX +
451+ editable . offsetWidth +
452+ ( target . offsetWidth - target . clientWidth ) ;
453+ if (
454+ row . offsetLeft + target . offsetLeft + newWidth >
455+ bloomCanvas . clientWidth
456+ ) {
457+ // This one won't fit. We'll stop without adding it.
458+ stopMakingLetters = true ;
459+ } else {
460+ draggable . style . width = `${ newWidth } px` ;
461+ target . style . width = `${ newWidth } px` ;
462+ newHeight = Math . max (
463+ newHeight ,
464+ // again, the actual height of the editable, plus our overflow estimate, plus any padding and border of the target.
465+ overflowY +
466+ editable . offsetHeight +
467+ ( target . offsetHeight - target . clientHeight )
468+ ) ;
469+ }
470+ } else {
471+ stopMakingLetters = true ;
472+ }
473+ if ( stopMakingLetters ) {
474+ // either added them all or ran out of space
475+ // We have more draggables than letters. Set the extra ones empty and invisible.
476+ // (Don't delete them, because they might be in use in some other language.)
477+ setDraggableText ( draggable , "" ) ;
478+ copyContentToTarget ( draggable ) ;
479+ draggable . classList . add ( "bloom-unused-in-lang" ) ;
480+ target . classList . add ( "bloom-unused-in-lang" ) ;
481+ draggable . style . width = `${ minWidth } px` ;
482+ draggable . style . height = `${ minHeight } px` ;
483+ target . style . width = `${ minWidth } px` ;
484+ target . style . height = `${ minHeight } px` ;
485+ }
486+ }
487+ for ( const d of draggables ) {
488+ d . style . height = `${ newHeight } px` ;
489+ const target = getTarget ( d ) as HTMLElement ;
490+ target . style . height = `${ newHeight } px` ;
426491 }
427- const shuffledDraggables = draggables . slice ( ) ;
428- shuffledDraggables . splice ( letters . length ) ; // don't want any invisible ones taking up space
492+ const shuffledDraggables = draggables . slice (
493+ 0 ,
494+ letters . length
495+ ) as HTMLElement [ ] ; // don't want any invisible ones taking up space
429496 shuffle ( shuffledDraggables , isTheTextInDraggablesTheSame ) ;
497+ let left = draggableX ;
430498 for ( let i = 0 ; i < shuffledDraggables . length ; i ++ ) {
431- shuffledDraggables [ i ] . style . left = `${ draggableX +
432- i * separation } px` ;
499+ shuffledDraggables [ i ] . style . left = `${ left } px` ;
500+ left += shuffledDraggables [ i ] . offsetWidth + separation ;
433501 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` ;
440- }
441502 }
503+ // Target shouldn't need adjusting, but we'll show the arrow for this one.
442504 adjustTarget ( draggables [ 0 ] , getTarget ( draggables [ 0 ] ) , true ) ;
443505 // Must do this AFTER we finish setting the content. Among many other things it does,
444506 // it will attach a ckeditor to the new editable. That does very complex things
@@ -453,6 +515,7 @@ const initializeDialog = (prompt: HTMLElement, tg: HTMLElement | null) => {
453515 false // don't make it active.
454516 ) ;
455517 } ) ;
518+
456519 // This seems to at least somewhat reduce the likelihood of losing focus
457520 // after a keystroke, especially with Keyman multi-character inserts (BL-14098).
458521 // I think it is harmless, since I can't think of any case where the text in the
0 commit comments