Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 145 additions & 20 deletions src/BloomBrowserUI/bookEdit/js/CanvasElementContextControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ import {
kBloomButtonClass,
kBloomCanvasSelector,
} from "../toolbox/canvas/canvasElementUtils";
import { getString, post, useApiObject } from "../../utils/bloomApi";
import { wrapWithRequestPageContentDelay } from "./bloomEditing";
import { get, post, useApiObject } from "../../utils/bloomApi";
import { ILanguageNameValues } from "../bookSettings/FieldVisibilityGroup";
import OverflowChecker from "../OverflowChecker/OverflowChecker";

interface IMenuItemWithSubmenu extends ILocalizableMenuItemProps {
subMenu?: ILocalizableMenuItemProps[];
Expand Down Expand Up @@ -938,6 +940,76 @@ const CanvasElementContextControls: React.FunctionComponent<{
// So until I get a better idea, I'm just putting in a hard-coded list.
const fieldsControlledByAppearanceSystem = ["bookTitle"];

function adjustAutoSizeForVisibleEditableInTranslationGroup(tg: HTMLElement) {
const visibleEditable = tg.getElementsByClassName(
"bloom-editable bloom-visibility-code-on",
)[0] as HTMLElement;
if (!visibleEditable) {
return;
}
OverflowChecker.AdjustSizeOrMarkOverflow(visibleEditable);
}

function setEditableContentFromKnownDataBookValueIfAny(
editable: HTMLElement,
dataBook: string | null,
tg: HTMLElement,
) {
if (!dataBook) {
return;
}
wrapWithRequestPageContentDelay(
() =>
new Promise<void>((resolve, reject) => {
get(
`editView/getDataBookValue?lang=${editable.getAttribute("lang")}&dataBook=${dataBook}`,
(result) => {
try {
const content = result.data;
// content comes from a source that looked empty, we don't want to overwrite something the user may
// already have typed here.
// But it may well have something in it, because we usually have an empty paragraph to start with.
// To test whether it looks empty, we put the text into a newly created element and then
// see whether it's textContent is empty.
// The logic of overwriting something which the user has typed here is that if we keep what's here,
// then the user may never know that there was already something in that field. But if we overwrite, then
// the user can always correct it back to what he just typed.
const temp = document.createElement("div");
temp.innerHTML = content || "";
if (temp.textContent.trim() !== "")
editable.innerHTML = content;
adjustAutoSizeForVisibleEditableInTranslationGroup(
tg,
);
resolve();
} catch (error) {
reject(error);
}
},
(error) => {
reject(error);
},
);
}),
"setCanvasFieldValueFromDataBook",
);
}

function applyAppearanceClassForEditable(editable: HTMLElement) {
editable.classList.remove(
"bloom-contentFirst",
"bloom-contentSecond",
"bloom-contentThird",
);
if (editable.classList.contains("bloom-content1")) {
editable.classList.add("bloom-contentFirst");
} else if (editable.classList.contains("bloom-contentNational1")) {
editable.classList.add("bloom-contentSecond");
} else if (editable.classList.contains("bloom-contentNational2")) {
editable.classList.add("bloom-contentThird");
}
}

function makeLanguageMenuItem(
ce: HTMLElement,
menuOptions: IMenuItemWithSubmenu[],
Expand All @@ -962,7 +1034,7 @@ function makeLanguageMenuItem(
tg.setAttribute("data-default-languages", dataDefaultLang);
const editables = Array.from(
tg.getElementsByClassName("bloom-editable"),
);
) as HTMLElement[];
if (editables.length === 0) return; // not able to handle this yet.
let editableInLang = editables.find(
(e) => e.getAttribute("lang") === langCode,
Expand Down Expand Up @@ -1013,11 +1085,18 @@ function makeLanguageMenuItem(
}
}

setEditableContentFromKnownDataBookValueIfAny(
editableInLang,
dataBookValue,
tg,
);

// and conversely remove them from the others
for (const editable of editables) {
// Ensure visibility code is off for others.
editable.classList.remove("bloom-visibility-code-on");
}
adjustAutoSizeForVisibleEditableInTranslationGroup(tg);
setMenuOpen(false);
};

Expand Down Expand Up @@ -1183,6 +1262,37 @@ function makeFieldTypeMenuItem(
}
};

const removeConflictingStyleClasses = (
fieldType: {
editableClasses: string[];
classes: string[];
},
editables: HTMLElement[],
) => {
const newStyleClasses = new Set(
[...fieldType.classes, ...fieldType.editableClasses].filter((c) =>
c.endsWith("-style"),
),
);
if (newStyleClasses.size === 0) {
return;
}

const stripStyleClasses = (element: HTMLElement) => {
Array.from(element.classList).forEach((className) => {
if (
className.endsWith("-style") &&
!newStyleClasses.has(className)
) {
element.classList.remove(className);
}
});
};

stripStyleClasses(tg);
editables.forEach((editable) => stripStyleClasses(editable));
};
Comment on lines +1265 to +1294
Copy link

@devin-ai-integration devin-ai-integration bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 removeConflictingStyleClasses strips all non-matching -style classes, including non-field-type ones

The new removeConflictingStyleClasses function at line 1255-1284 removes ALL -style classes from both tg and editables that aren't in the new field type's set. This goes beyond what clearFieldTypeClasses does (which only removes known field-type classes). If an editable had a pre-existing style class like Normal-style from before any field type was set, switching to e.g. bookTitle would permanently remove it. Switching back to "None" field type would not restore it, since the "None" handler only calls clearFieldTypeClasses() without adding any style class back. This is likely the intended behavior (ensuring the field type's style takes full precedence), but it means the style class removal is one-way and irreversible through the UI.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no way to get back to the class it might have had before the Field was first set, unless we save it in some data- attribute. Having more than one -style class on an element is asking for trouble (e.g., which one would style editor let you edit?), so I think removing all the others is necessary. I did add restoring the usual default style for comic bubbles, since I'm not sure what might fail if there is not at least one -style class. We might still want some improvement here, but I think we need users to tell us what.


const activeType = tg
.getElementsByClassName("bloom-editable bloom-visibility-code-on")[0]
?.getAttribute("data-book");
Expand All @@ -1194,9 +1304,21 @@ function makeFieldTypeMenuItem(
clearFieldTypeClasses();
for (const editable of Array.from(
tg.getElementsByClassName("bloom-editable"),
)) {
) as HTMLElement[]) {
editable.removeAttribute("data-book");
// There's a bit of guess-work involved in what would be most helpful here.
// clearFieldTypeClasses removes any field-type-specific style class,
// and we generally expect a bloom-editable to have some style class.
// Should it be Normal-style or Bubble-style? Bubble-style is the default
// for canvas elements, so I decided to go with that.
const hasStyleClass = Array.from(editable.classList).some(
(className) => className.endsWith("-style"),
);
if (!hasStyleClass) {
editable.classList.add("Bubble-style");
}
}
adjustAutoSizeForVisibleEditableInTranslationGroup(tg);
setMenuOpen(false);
},
icon: !activeType && <CheckIcon css={getMenuIconCss()} />,
Expand All @@ -1210,7 +1332,7 @@ function makeFieldTypeMenuItem(
clearFieldTypeClasses();
const editables = Array.from(
tg.getElementsByClassName("bloom-editable"),
);
) as HTMLElement[];
if (fieldType.readOnly) {
const readOnlyDiv = document.createElement("div");
readOnlyDiv.setAttribute(
Expand All @@ -1232,34 +1354,37 @@ function makeFieldTypeMenuItem(
// Reload the page to get the derived content loaded.
post("common/saveChangesAndRethinkPageEvent", () => {});
} else {
removeConflictingStyleClasses(fieldType, editables);
tg.classList.add(...fieldType.classes);
for (const editable of editables) {
editable.classList.add(...fieldType.editableClasses);
editable.setAttribute("data-book", fieldType.dataBook);
if (
fieldsControlledByAppearanceSystem.includes(
fieldType.dataBook,
)
) {
applyAppearanceClassForEditable(editable);
} else {
editable.classList.remove(
"bloom-contentFirst",
"bloom-contentSecond",
"bloom-contentThird",
);
}
if (
editable.classList.contains(
"bloom-visibility-code-on",
)
) {
getString(
`editView/getDataBookValue?lang=${editable.getAttribute("lang")}&dataBook=${fieldType.dataBook}`,
(content) => {
// content comes from a source that looked empty, we don't want to overwrite something the user may
// already have typed here.
// But it may well have something in it, because we usually have an empty paragraph to start with.
// To test whether it looks empty, we put the text into a newly created element and then
// see whether it's textContent is empty.
// The logic of overwriting something which the user has typed here is that if we keep what's here,
// then the user may never know that there was already something in that field. But if we overwrite, then
// the user can always correct it back to what he just typed.
const temp = document.createElement("div");
temp.innerHTML = content || "";
if (temp.textContent.trim() !== "")
editable.innerHTML = content;
},
setEditableContentFromKnownDataBookValueIfAny(
editable,
fieldType.dataBook,
tg,
);
}
}
adjustAutoSizeForVisibleEditableInTranslationGroup(tg);
}
setMenuOpen(false);
},
Expand Down