Skip to content

Commit cddcf82

Browse files
committed
Make letter boxes grow wide enough for letters (BL-14541)
BL-14541 grew to be much more complex than this. The current state of this card is handling width fairly well, but failing to get a reasonable height for မြန်မာ unless line height is 1.5, in which case ordinary roman letters overflow mysteriously. Something is wrong with measuring text. There are also many related tasks in the card which are not yet addressed here.
1 parent 063ddb4 commit cddcf82

7 files changed

Lines changed: 193 additions & 90 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: 118 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ 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";
49+
import { max } from "underscore";
50+
import { _0 } from "../../../react_components/Progress/ProgressBar.stories";
4751

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

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

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -277,31 +277,37 @@ const makeArrowShape = (
277277
// These values make a line from the center of the draggable to the center of the target.
278278
const startX = draggable.offsetLeft + draggable.offsetWidth / 2;
279279
const startY = draggable.offsetTop + draggable.offsetHeight / 2;
280-
const endXCenter = target.offsetLeft + target.offsetWidth / 2;
281-
const endYCenter = target.offsetTop + target.offsetHeight / 2;
280+
let targetLeft = target.offsetLeft;
281+
let targetTop = target.offsetTop;
282+
if (target.parentElement !== draggable.parentElement) {
283+
targetLeft += target.parentElement!.offsetLeft;
284+
targetTop += target.parentElement!.offsetTop;
285+
}
286+
const endXCenter = targetLeft + target.offsetWidth / 2;
287+
const endYCenter = targetTop + target.offsetHeight / 2;
282288

283289
// Figure out where the tip of the arrow should be. At least one of these will be changed,
284290
// so that we end up at a corner or side midpoint of the target.
285291
// (This assumes the two don't overlap. If they do, we don't make an arrow at all.)
286292
let endX = endXCenter;
287293
let endY = endYCenter;
288-
if (target.offsetLeft > startX) {
294+
if (targetLeft > startX) {
289295
// The target is entirely to the right of the center of the draggable.
290296
// We will go for one of the points on the left of the target.
291-
endX = target.offsetLeft;
292-
} else if (target.offsetLeft + target.offsetWidth < startX) {
297+
endX = targetLeft;
298+
} else if (targetLeft + target.offsetWidth < startX) {
293299
// The target is entirely to the left of the center of the draggable.
294300
// We will go for one of the points on the right of the target.
295-
endX = target.offsetLeft + target.offsetWidth;
301+
endX = targetLeft + target.offsetWidth;
296302
}
297-
if (target.offsetTop > startY) {
303+
if (targetTop > startY) {
298304
// The target is entirely below the center of the draggable.
299305
// We will go for one of the points on the top of the target.
300-
endY = target.offsetTop;
301-
} else if (target.offsetTop + target.offsetHeight < startY) {
306+
endY = targetTop;
307+
} else if (targetTop + target.offsetHeight < startY) {
302308
// The target is entirely above the center of the draggable.
303309
// We will go for one of points on the bottom of the target.
304-
endY = target.offsetTop + target.offsetHeight;
310+
endY = targetTop + target.offsetHeight;
305311
}
306312

307313
if (!arrow) {
@@ -1964,7 +1970,8 @@ function pxToNumber(dimension: string): number {
19641970
}
19651971

19661972
export const makeTargetForDraggable = (
1967-
canvasElement: HTMLElement
1973+
canvasElement: HTMLElement,
1974+
parentForTarget?: HTMLElement
19681975
): HTMLElement => {
19691976
const id = canvasElement.getAttribute("data-draggable-id");
19701977
if (!id) {
@@ -1973,28 +1980,35 @@ export const makeTargetForDraggable = (
19731980
// don't simplify to 'document.createElement'; may be in a different iframe
19741981
const target = canvasElement.ownerDocument.createElement("div");
19751982
target.setAttribute("data-target-of", id);
1976-
const left = pxToNumber(canvasElement.style.left);
1977-
const top = pxToNumber(canvasElement.style.top);
19781983
const width = pxToNumber(canvasElement.style.width);
19791984
const height = pxToNumber(canvasElement.style.height);
1980-
let newLeft = left + 20;
1981-
let newTop = top + height + 30;
1982-
if (newTop + height > canvasElement.parentElement!.clientHeight) {
1983-
newTop = Math.max(0, top - height - 30);
1984-
}
1985-
if (newLeft + width > canvasElement.parentElement!.clientWidth) {
1986-
newLeft = Math.max(0, left - width - 30);
1985+
if (!parentForTarget) {
1986+
// if we're putting it in some other parent, then positioning it relative to the
1987+
// thing for which it's a target doesn't make sense. It definitely isn't wanted for
1988+
// the current application, making a row of them in the letter game.
1989+
const left = pxToNumber(canvasElement.style.left);
1990+
const top = pxToNumber(canvasElement.style.top);
1991+
1992+
let newLeft = left + 20;
1993+
let newTop = top + height + 30;
1994+
if (newTop + height > canvasElement.parentElement!.clientHeight) {
1995+
newTop = Math.max(0, top - height - 30);
1996+
}
1997+
if (newLeft + width > canvasElement.parentElement!.clientWidth) {
1998+
newLeft = Math.max(0, left - width - 30);
1999+
}
2000+
2001+
// Review: can we do any more to make sure it's visible and not overlapping canvas element?
2002+
// Should we try to avoid overlapping other canvas elements and/or targets?
2003+
target.style.left = `${newLeft}px`;
2004+
target.style.top = `${newTop}px`;
19872005
}
1988-
// Review: can we do any more to make sure it's visible and not overlapping canvas element?
1989-
// Should we try to avoid overlapping other canvas elements and/or targets?
1990-
target.style.left = `${newLeft}px`;
1991-
target.style.top = `${newTop}px`;
19922006
target.style.width = `${width}px`;
19932007
target.style.height = `${height}px`;
19942008
// This allows it to get focus, which allows it to get the shadow effect we want when
19952009
// clicked. But is that really right? We can't actually type there.
19962010
target.setAttribute("tabindex", "0");
1997-
canvasElement.parentElement!.appendChild(target);
2011+
(parentForTarget ?? canvasElement.parentElement)!.appendChild(target);
19982012
enableDraggingTargets(target);
19992013
adjustTarget(canvasElement, target);
20002014
return target;

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ function doesPageHaveSameSizeMode(
1414
if (!page) {
1515
return false;
1616
}
17-
return page.getAttribute("data-same-size") !== "false";
17+
// Don't support same-size mode for this game. The new source page sets this false, but earlier
18+
// ones just didn't have it, since same-size is usually the default.
19+
return (
20+
page.getAttribute("data-same-size") !== "false" &&
21+
page.getAttribute("data-activity") !== "drag-letter-to-target"
22+
);
1823
}
1924

2025
// Returns true if we need to take into account that this element must be kept the same size

src/BloomBrowserUI/utils/measureText.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ export class MeasureText {
7777
.bottom;
7878
const baselineOfTextWithDefaultLineSpace = block.getBoundingClientRect()
7979
.bottom;
80-
const fontDescent =
80+
const fontDescent = Math.ceil(
8181
bottomOfTextWithDefaultLineSpace -
82-
baselineOfTextWithDefaultLineSpace;
82+
baselineOfTextWithDefaultLineSpace
83+
);
8384

8485
if (lineHeight !== null) div.style.lineHeight = lineHeight;
8586
const bottomOfTextWithActualLineSpace = div.getBoundingClientRect()
@@ -103,7 +104,9 @@ export class MeasureText {
103104
// line, but we decided it was not worth the effort to be that perfectionistic.
104105

105106
// We only need enough space to draw what the browser thinks is the descent.
106-
const canvasHeight = fontDescent;
107+
// Plus a little extra because sometimes the font descent is not actually enough
108+
// for the lowest possible descenders.
109+
const canvasHeight = fontDescent + 4;
107110
const testingCanvas = this.createCanvas(width, canvasHeight);
108111
// The number of pixels of descent is one more than the index of
109112
// the bottom line on which we found part of a descender.
@@ -200,8 +203,11 @@ export class MeasureText {
200203
fontSize: number
201204
): void {
202205
const context = canvas.getContext("2d");
203-
if (context != null) {
206+
if (context !== null) {
204207
context.textAlign = "start"; // was "top", which is not a valid choice
208+
// means that the baseline of the text will align with the y coordinate passed
209+
// as the last argument to fillText, which is zero, so basically we draw just
210+
// the descenders at the top of the canvas.
205211
context.textBaseline = "alphabetic";
206212
context.font = `${fontSize}px '${fontFamily}'`;
207213
context.fillStyle = "white";

0 commit comments

Comments
 (0)