Skip to content

Commit e9ff266

Browse files
committed
Make letter boxes grow wide enough for letters (BL-14541)
1 parent 9c65119 commit e9ff266

2 files changed

Lines changed: 151 additions & 92 deletions

File tree

src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,6 +1728,7 @@ export class CanvasElementManager {
17281728
editable
17291729
);
17301730
this.alignControlFrameWithActiveElement();
1731+
this.adjustTarget(this.activeElement);
17311732
}
17321733

17331734
// Determine which of the side handles, if any, should have the class "bloom-currently-cropped"

src/BloomBrowserUI/bookEdit/toolbox/games/GamePromptDialog.tsx

Lines changed: 150 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import {
4444
import { splitIntoGraphemes } from "../../../utils/textUtils";
4545
import { kCanvasElementClass } from "../overlay/canvasElementUtils";
4646
import { kBloomCanvasSelector } from "../../js/bloomImages";
47+
import OverflowChecker from "../../OverflowChecker/OverflowChecker";
48+
import { doesContainingPageHaveSameSizeMode } from "./gameUtilities";
4749

4850
export 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

Comments
 (0)