diff --git a/DistFiles/localization/en/BloomMediumPriority.xlf b/DistFiles/localization/en/BloomMediumPriority.xlf
index 4d540aaf3a23..6e55ef939b95 100644
--- a/DistFiles/localization/en/BloomMediumPriority.xlf
+++ b/DistFiles/localization/en/BloomMediumPriority.xlf
@@ -769,6 +769,7 @@
Fill the front cover with a single imageBookSettings.CoverIsImage
+ Obsolete as of Bloom 6.4Using this option turns on the [Print Bleed](https://en.wikipedia.org/wiki/Bleed_%28printing%29) indicators on paper layouts. See [Full Page Cover Images](https://docs.bloomlibrary.org/full-page-cover-images) for information on sizing your image to fit.
@@ -778,6 +779,7 @@
Replace the front cover content with a single full-bleed image. See [Full Page Cover Images](https://docs.bloomlibrary.org/full-page-cover-images) for information on sizing your image to fit.BookSettings.CoverIsImage.Description.V2
+ Obsolete as of Bloom 6.4Use full bleed page layout
diff --git a/src/BloomBrowserUI/bookEdit/StyleEditor/StyleEditor.ts b/src/BloomBrowserUI/bookEdit/StyleEditor/StyleEditor.ts
index 77f260cd9971..d4c136e5ca51 100644
--- a/src/BloomBrowserUI/bookEdit/StyleEditor/StyleEditor.ts
+++ b/src/BloomBrowserUI/bookEdit/StyleEditor/StyleEditor.ts
@@ -1151,6 +1151,34 @@ export default class StyleEditor {
private uiLang: string;
+ // We allow read-only fields to have a style dialog on custom-layout pages
+ // for elements that have a -style class to edit.
+ public static shouldAllowNonEditableStyleDialogTarget(
+ targetBox: HTMLElement,
+ ): boolean {
+ if (targetBox.classList.contains("bloom-editable")) {
+ return false;
+ }
+ if (StyleEditor.GetStyleClassFromElement(targetBox) === null) {
+ return false;
+ }
+ return (
+ targetBox.closest(
+ ".bloom-page.bloom-customLayout[data-custom-layout-id]",
+ ) !== null
+ );
+ }
+
+ // When we allow an element that's not a bloom-editable to have a format dialog,
+ // we always use the default (not langauge-dependent) version of the rules,
+ // since we don't have the structure of a translation group containing multiple
+ // bloom-editables that might need different formatting.
+ private targetUsesLanguageIndependentRules(
+ targetBox: HTMLElement,
+ ): boolean {
+ return StyleEditor.shouldAllowNonEditableStyleDialogTarget(targetBox);
+ }
+
public AttachToBox(targetBox: HTMLElement) {
this.uiLang = theOneLocalizationManager.getCurrentUILocale();
@@ -1498,7 +1526,11 @@ export default class StyleEditor {
// BL-5616 This also applies if the textbox's default language is '*',
// like it is for an Arithmetic Equation.
const tag = $(this.boxBeingEdited).attr("lang");
- if (this.shouldSetDefaultRule() || tag === "*") {
+ if (
+ this.shouldSetDefaultRule() ||
+ this.targetUsesLanguageIndependentRules(this.boxBeingEdited) ||
+ tag === "*"
+ ) {
theOneLocalizationManager
.asyncGetText(
"BookEditor.DefaultForText",
@@ -2217,11 +2249,15 @@ export default class StyleEditor {
if (!styleName) {
return null; // bizarre, since we put up the dialog
}
- const langAttrValue = StyleEditor.GetLangValueOrNull(target);
+ const useLanguageIndependentRule =
+ ignoreLanguage || this.targetUsesLanguageIndependentRules(target);
+ const langAttrValue = useLanguageIndependentRule
+ ? null
+ : StyleEditor.GetLangValueOrNull(target);
return this.GetOrCreateRuleForStyle(
styleName,
langAttrValue,
- ignoreLanguage,
+ useLanguageIndependentRule,
forChildPara,
);
}
diff --git a/src/BloomBrowserUI/bookEdit/bookSettings/BookSettingsDialog.tsx b/src/BloomBrowserUI/bookEdit/bookSettings/BookSettingsDialog.tsx
index 270213542984..e218a040ef03 100644
--- a/src/BloomBrowserUI/bookEdit/bookSettings/BookSettingsDialog.tsx
+++ b/src/BloomBrowserUI/bookEdit/bookSettings/BookSettingsDialog.tsx
@@ -263,15 +263,6 @@ export const BookSettingsDialog: React.FunctionComponent<{
"BookSettings.Gutter.DefaultLabel",
);
- const coverIsImageLabel = useL10n(
- "Fill the front cover with a single image",
- "BookSettings.CoverIsImage",
- );
- const coverIsImageDescription = useL10n(
- "Replace the front cover content with a single full-bleed image. See [Full Page Cover Images](https://docs.bloomlibrary.org/full-page-cover-images) for information on sizing your image to fit.",
- "BookSettings.CoverIsImage.Description.V2",
- );
-
const fullBleedLabel = useL10n(
"Use full bleed page layout",
"BookSettings.FullBleed",
@@ -389,9 +380,6 @@ export const BookSettingsDialog: React.FunctionComponent<{
setMigratedTheme("");
};
- const tierAllowsFullPageCoverImage =
- useGetFeatureStatus("fullPageCoverImage")?.enabled;
-
const tierAllowsFullBleed = useGetFeatureStatus("PrintshopReady")?.enabled;
function saveSettingsAndCloseDialog() {
@@ -490,35 +478,6 @@ export const BookSettingsDialog: React.FunctionComponent<{
)}
-
+ StyleEditor.shouldAllowNonEditableStyleDialogTarget(candidate),
+ );
+}
+
// This is the controls bar that appears beneath a canvas element when it is selected. It contains buttons
// for the most common operations that apply to the canvas element in its current state, and a menu for less common
// operations.
@@ -356,6 +375,8 @@ const CanvasElementContextControls: React.FunctionComponent<{
const editableTextElement = props.canvasElement.getElementsByClassName(
"bloom-editable bloom-visibility-code-on",
)[0] as HTMLElement;
+ const formatTargetElement = getFormatTargetElement(props.canvasElement);
+ const showFormatButton = !!formatTargetElement && !isNavButton;
if (isNavButton) {
menuOptions.splice(0, 0, {
@@ -456,6 +477,13 @@ const CanvasElementContextControls: React.FunctionComponent<{
languageNameValues,
noneLabel,
);
+ } else if (formatTargetElement && !isNavButton) {
+ menuOptions.push({
+ l10nId: "EditTab.Toolbox.ComicTool.Options.Format",
+ english: "Format",
+ onClick: () => GetEditor().runFormatDialog(formatTargetElement),
+ icon: ,
+ });
}
const runMetadataDialog = () => {
@@ -617,7 +645,7 @@ const CanvasElementContextControls: React.FunctionComponent<{
)}
)}
- {editableTextElement && !isNavButton && (
+ {showFormatButton && (
{
if (!props.canvasElement) return;
GetEditor().runFormatDialog(
- editableTextElement,
+ formatTargetElement,
);
}}
/>
@@ -650,7 +678,7 @@ const CanvasElementContextControls: React.FunctionComponent<{
)}
{(!(hasImage && isPlaceHolder) &&
- !editableTextElement &&
+ !showFormatButton &&
!(hasVideo && !videoAlreadyChosen)) || (
// Add a spacer if there is any button before these
{
+ requestAnimationFrame(() => {
+ theOneCanvasElementManager.setActiveElement(
+ bgImageCe,
+ );
+ });
+ };
+ activateConvertedBackground();
+ if (!haveRealBgImage) {
+ const fallbackHandle = setTimeout(() => {
+ activateConvertedBackground();
+ }, 120);
+ bgImg.addEventListener(
+ "load",
+ () => {
+ clearTimeout(fallbackHandle);
+ activateConvertedBackground();
+ },
+ { once: true },
+ );
+ }
setMenuOpen(false);
},
});
diff --git a/src/BloomBrowserUI/bookEdit/toolbox/canvas/customXmatterPage.tsx b/src/BloomBrowserUI/bookEdit/toolbox/canvas/customXmatterPage.tsx
index 927dd4e7534a..a26a519610c0 100644
--- a/src/BloomBrowserUI/bookEdit/toolbox/canvas/customXmatterPage.tsx
+++ b/src/BloomBrowserUI/bookEdit/toolbox/canvas/customXmatterPage.tsx
@@ -166,6 +166,9 @@ export async function convertXmatterPageToCustom(
// where to put its new canvas element.
ceContent = elem.cloneNode(true) as HTMLElement;
}
+
+ preserveHintMetadata(elem as HTMLElement, ceContent as HTMLElement);
+
// make a new canvas element to hold this. Not sure what problems it will
// cause when it's NOT a TG, but we have at least topic and language name that
// never are and are commonly on the front cover.
@@ -232,6 +235,77 @@ async function getLanguageNameValues(): Promise {
.data as ILanguageNameValues;
}
+function copyHintAttributes(source: Element, target: HTMLElement): void {
+ [
+ "data-hint",
+ "data-i18n",
+ "data-link-text",
+ "data-link-target",
+ "data-functiononhintclick",
+ ].forEach((attr) => {
+ const value = source.getAttribute(attr);
+ if (value && !target.hasAttribute(attr)) {
+ target.setAttribute(attr, value);
+ }
+ });
+}
+
+function preserveHintMetadata(
+ sourceElement: HTMLElement,
+ convertedElement: HTMLElement,
+): void {
+ // If we've already preserved label content or explicit hint metadata, leave it alone.
+ if (
+ convertedElement.querySelector("label.bubble") ||
+ convertedElement.hasAttribute("data-hint")
+ ) {
+ return;
+ }
+
+ const translationGroup = sourceElement.closest(
+ ".bloom-translationGroup",
+ ) as HTMLElement | null;
+ if (!translationGroup) {
+ copyHintAttributes(sourceElement, convertedElement);
+ if (
+ !convertedElement.hasAttribute("data-hint") &&
+ sourceElement.parentElement
+ ) {
+ copyHintAttributes(sourceElement.parentElement, convertedElement);
+ }
+ return;
+ }
+
+ copyHintAttributes(translationGroup, convertedElement);
+
+ const label = translationGroup.querySelector("label.bubble");
+ if (!label) {
+ if (
+ !convertedElement.hasAttribute("data-hint") &&
+ translationGroup.parentElement
+ ) {
+ copyHintAttributes(
+ translationGroup.parentElement,
+ convertedElement,
+ );
+ }
+ return;
+ }
+
+ const labelText = label.textContent?.trim();
+ if (labelText && !convertedElement.hasAttribute("data-hint")) {
+ convertedElement.setAttribute("data-hint", labelText);
+ }
+ copyHintAttributes(label, convertedElement);
+
+ if (
+ !convertedElement.hasAttribute("data-hint") &&
+ translationGroup.parentElement
+ ) {
+ copyHintAttributes(translationGroup.parentElement, convertedElement);
+ }
+}
+
function setDataDefault(
ceContent: HTMLElement,
lang: string,
diff --git a/src/BloomExe/Book/AppearanceSettings.cs b/src/BloomExe/Book/AppearanceSettings.cs
index df99e4bf78c8..c47d686cd437 100644
--- a/src/BloomExe/Book/AppearanceSettings.cs
+++ b/src/BloomExe/Book/AppearanceSettings.cs
@@ -110,14 +110,6 @@ public string FirstPossiblyOffendingCssFile
// The default here is rarely if ever relevant. Usually a newly created instance will be initialized from a folder, and the default will be overwritten,
// either to whatever we find in appearance.json, or to "legacy-5-6" if there is no appearance.json.
new StringPropertyDef("cssThemeName", "cssThemeName", "default"),
- // BooleanPropertyDef, not CssXDef because this is not a css variable.
- new BooleanPropertyDef(
- "coverIsImage",
- "coverIsImage",
- defaultValue: false,
- requiresXmatterUpdate: true,
- valueRequiredIfLegacyTheme: false
- ), // If true, cover page is just a full bleed image.
// If true, book uses full bleed page layout in edit mode. Printing that way is still optional.
new BooleanPropertyDef("fullBleed", "fullBleed", defaultValue: false),
// Does not correspond to a css variable. We will set the relevant page number css variables based on this setting.
@@ -183,11 +175,8 @@ public string CssThemeName
// Some setting's values are not allowed in legacy mode.
// REVIEW:
// This concept of forcing a value based on the legacy theme was introduced at the time coverIsImage was added.
- // And currently (Dec 2024), it is the only property that has a valueRequiredIfLegacyTheme.
- // But I think the properties which existed before that and which get disabled by setting the theme
- // to legacy should also be set. e.g. cover-topic-show
- // Without this, the user can set the theme to non-legacy, change the property to whatever he wants,
- // then change the theme back to legacy.
+ // Since then that was removed and (as of March 2026) no property has a valueRequiredIfLegacyTheme.
+ // Possibly more should have it.
private void SetRequiredValuesIfLegacyTheme()
{
if (CssThemeName != "legacy-5-6") // Can't use UsingLegacy here because it includes logic about the syncing of files which we don't want.
@@ -222,11 +211,6 @@ private void SetProperty(KeyValuePair property)
Properties[property.Key] = property.Value;
}
- public bool CoverIsImage
- {
- get { return _properties.coverIsImage; }
- }
-
public bool FullBleed
{
get { return _properties.fullBleed; }
diff --git a/src/BloomExe/Book/Book.cs b/src/BloomExe/Book/Book.cs
index 1f6ca06d034a..4a8489cf1f04 100644
--- a/src/BloomExe/Book/Book.cs
+++ b/src/BloomExe/Book/Book.cs
@@ -1764,6 +1764,8 @@ out dummy
///
public void EnsureUpToDateMemory(IProgress progress)
{
+ Storage.CaptureInitialStateForMigration();
+
string oldMetaData = "";
if (RobustFile.Exists(BookInfo.MetaDataPath))
{
@@ -1852,6 +1854,7 @@ public void EnsureUpToDateMemory(IProgress progress)
// but it takes almost no time when the book IS already up-to-date.
// These methods work with the same book metadata to determine what migration has
// already been done, so they must be called in exactly this order.
+ Storage.RestoreStuffBeforeMigration();
Storage.MigrateMaintenanceLevels();
Storage.MigrateToMediaLevel1ShrinkLargeImages();
Storage.MigrateToLevel2RemoveTransparentComicalSvgs();
@@ -1866,6 +1869,7 @@ public void EnsureUpToDateMemory(IProgress progress)
// Migration 11 does not exist.
Storage.MigrateToLevel12PageNumberPosition();
Storage.MigrateToLevel13SplitPaneMarginBoxes();
+ Storage.MigrateToLevel14CoverIsImageToCustomLayout(_bookData);
Storage.DoBackMigrations();
@@ -2562,8 +2566,7 @@ public void BringXmatterHtmlUpToDate(HtmlDom bookDOM)
layout,
BookInfo.UseDeviceXMatter,
_bookData.MetadataLanguage1Tag,
- oldIds,
- CoverIsImage
+ oldIds
);
foreach (
var page in bookDOM
@@ -4125,15 +4128,6 @@ public void InsertFullBleedMarkup(SafeXmlElement body)
}
}
- public bool CoverIsImage =>
- BookInfo.AppearanceSettings.CoverIsImage
- && FeatureStatus
- .GetFeatureStatus(
- CollectionSettings.Subscription,
- FeatureName.FullPageCoverImage,
- this
- )
- .Enabled;
public bool FullBleed =>
PageSizeSupportsFullBleed()
&& BookInfo.AppearanceSettings.FullBleed
diff --git a/src/BloomExe/Book/BookStorage.cs b/src/BloomExe/Book/BookStorage.cs
index 238f88310af2..a744e72ff721 100644
--- a/src/BloomExe/Book/BookStorage.cs
+++ b/src/BloomExe/Book/BookStorage.cs
@@ -102,6 +102,8 @@ void CleanupUnusedSupportFiles(
bool LinkToLocalCollectionStyles { get; set; }
IEnumerable GetActivityFolderNamesReferencedInBook();
+ void CaptureInitialStateForMigration();
+ void RestoreStuffBeforeMigration();
void MigrateMaintenanceLevels();
void MigrateToMediaLevel1ShrinkLargeImages();
void MigrateToLevel2RemoveTransparentComicalSvgs();
@@ -116,6 +118,7 @@ void CleanupUnusedSupportFiles(
void MigrateToLevel10GameHeader();
void MigrateToLevel12PageNumberPosition(); // Level 11 was skipped.
void MigrateToLevel13SplitPaneMarginBoxes();
+ void MigrateToLevel14CoverIsImageToCustomLayout(BookData bookData);
void DoBackMigrations();
@@ -185,12 +188,13 @@ public class BookStorage : IBookStorage
/// Bloom 6.3 11 = unused migration (decided operation wasn't needed after 12 introduced)
/// Bloom 6.3 12 = change appearance settings page number control to use page number position system
/// Bloom 6.3 13 = fix split-pane-component margin boxes with positioning style
+ /// Bloom 6.4 14 = migrate legacy coverIsImage setting to custom-layout cover
/// History of kMediaMaintenanceLevel (introduced in 6.0)
/// missing: set it to 0 if maintenanceLevel is 0 or missing, otherwise 1
/// 0 = No media maintenance has been done
/// Bloom 6.0: 1 = maintenanceLevel at least 1 (so images are opaque and not too big)
///
- public const int kMaintenanceLevel = 13;
+ public const int kMaintenanceLevel = 14;
public const int kMediaMaintenanceLevel = 1;
public const string PrefixForCorruptHtmFiles = "_broken_";
@@ -3112,6 +3116,7 @@ private void LoadCurrentBrandingFilesIntoBookFolder()
private string _cachedXmatterPackName;
private HtmlDom _cachedXmatterDom;
private BookInfo _cachedXmatterBookInfo;
+ private SafeXmlElement _preMigrationOutsideFrontCover;
private XMatterHelper XMatterHelper
{
@@ -3933,6 +3938,53 @@ private int GetMaintenanceLevel()
return level;
}
+ public void CaptureInitialStateForMigration()
+ {
+ _preMigrationOutsideFrontCover = null;
+ if (GetMaintenanceLevel() >= 14)
+ return;
+
+ var outsideFrontCover = Dom.SafeSelectNodes(
+ "//body//div[contains(@class,'bloom-page') and contains(@class,'outsideFrontCover')]"
+ )
+ .Cast()
+ .FirstOrDefault();
+ if (outsideFrontCover == null || !outsideFrontCover.HasClass("cover-is-image"))
+ return;
+
+ _preMigrationOutsideFrontCover = outsideFrontCover.CloneNode(true) as SafeXmlElement;
+ }
+
+ public void RestoreStuffBeforeMigration()
+ {
+ if (_preMigrationOutsideFrontCover == null)
+ return;
+
+ var currentOutsideFrontCover = Dom.SafeSelectNodes(
+ "//body//div[contains(@class,'bloom-page') and contains(@class,'outsideFrontCover')]"
+ )
+ .Cast()
+ .FirstOrDefault();
+ if (currentOutsideFrontCover == null)
+ return;
+
+ var preservedMarginBox = GetMarginBox(_preMigrationOutsideFrontCover);
+ var currentMarginBox = GetMarginBox(currentOutsideFrontCover);
+ if (preservedMarginBox == null || currentMarginBox == null)
+ return;
+
+ while (currentMarginBox.HasChildNodes)
+ currentMarginBox.RemoveChild(currentMarginBox.ChildNodes[0]);
+
+ var preservedChildren = preservedMarginBox.ChildNodes.Cast().ToList();
+ foreach (var child in preservedChildren)
+ {
+ currentMarginBox.AppendChild(
+ currentMarginBox.OwnerDocument.ImportNode(child, true)
+ );
+ }
+ }
+
///
/// Bloom 4.9 and later (a bit later than the above 4.9 and therefore a separate maintenance
/// level) will only put comical-generated svgs in Bloom imageContainers if they are
@@ -4521,6 +4573,51 @@ public void MigrateToLevel13SplitPaneMarginBoxes()
Dom.UpdateMetaElement("maintenanceLevel", "13");
}
+ public void MigrateToLevel14CoverIsImageToCustomLayout(BookData bookData)
+ {
+ if (GetMaintenanceLevel() >= 14)
+ return;
+
+ var hadLegacyCoverSetting =
+ BookInfo.AppearanceSettings.TryGetBooleanPropertyValue(
+ "coverIsImage",
+ out var legacyCoverSettingValue
+ ) && legacyCoverSettingValue;
+ BookInfo.AppearanceSettings.Properties.Remove("coverIsImage");
+
+ if (hadLegacyCoverSetting)
+ {
+ var outsideFrontCover = Dom.SafeSelectNodes(
+ "//body//div[contains(@class,'bloom-page') and contains(@class,'outsideFrontCover')]"
+ )
+ .Cast()
+ .FirstOrDefault();
+ if (outsideFrontCover != null)
+ {
+ outsideFrontCover.RemoveClass("cover-is-image");
+ outsideFrontCover.AddClass("bloom-customLayout");
+
+ BookInfo.AppearanceSettings.PendingChangeRequiresXmatterUpdate = true;
+
+ if (
+ outsideFrontCover.HasClass("bloom-customLayout")
+ && !string.IsNullOrEmpty(
+ outsideFrontCover.GetAttribute("data-custom-layout-id")
+ )
+ )
+ {
+ var onePageDom = new HtmlDom("");
+ onePageDom.Body.AppendChild(
+ onePageDom.RawDom.ImportNode(outsideFrontCover, true)
+ );
+ bookData.SuckInDataFromEditedDom(onePageDom, BookInfo);
+ }
+ }
+ }
+
+ Dom.UpdateMetaElement("maintenanceLevel", "14");
+ }
+
///
/// This method reduces the list of features to the most specific one, based largely on the
/// subscription tier. If there are multiple features that are at the same subscription
diff --git a/src/BloomExe/Book/XMatterHelper.cs b/src/BloomExe/Book/XMatterHelper.cs
index 83b46211d675..af0a7e5fd280 100644
--- a/src/BloomExe/Book/XMatterHelper.cs
+++ b/src/BloomExe/Book/XMatterHelper.cs
@@ -22,7 +22,6 @@ public class XMatterHelper
{
private readonly HtmlDom _bookDom;
private readonly string _nameOfXMatterPack;
- private readonly IFileLocator _fileLocator;
///
/// Constructs by finding the file and folder of the xmatter pack, given the its key name e.g. "Factory", "SILIndonesia".
@@ -41,7 +40,6 @@ public XMatterHelper(
bool useDeviceVersionIfAvailable = false
)
{
- _fileLocator = fileLocator;
string directoryPath = null;
_bookDom = bookDom;
var bookSpecificXMatterPack = bookDom.GetMetaValue("xmatter", null);
@@ -318,8 +316,7 @@ public void InjectXMatter(
Layout layout,
bool orderXmatterForDeviceUse,
string metadataLangTag,
- List oldIds = null,
- bool coverIsImage = false
+ List oldIds = null
)
{
//don't want to pollute shells with this content
@@ -361,29 +358,7 @@ SafeXmlElement xmatterPage in XMatterDom.SafeSelectNodes(
)
)
{
- var newPageSource = xmatterPage;
-
- // If we are using an image for the front cover, replace the typical front cover with
- // a special one which has a full-page bloom-canvas.
- if (coverIsImage && IsOutsideFrontCoverPage(xmatterPage))
- {
- var directoryPath = GetXMatterDirectory(
- "CoverIsImage",
- _fileLocator,
- null,
- true
- );
- var coverIsImageDom = XmlHtmlConverter.GetXmlDomFromHtmlFile(
- directoryPath.CombineForPath("CoverIsImage-XMatter.html"),
- false
- );
- var coverIsImagePage = coverIsImageDom.SelectSingleNode(
- "/html/body/div[contains(@data-page,'required')]"
- );
- newPageSource = coverIsImagePage as SafeXmlElement;
- }
-
- var newPageDiv = _bookDom.RawDom.ImportNode(newPageSource, true) as SafeXmlElement;
+ var newPageDiv = _bookDom.RawDom.ImportNode(xmatterPage, true) as SafeXmlElement;
// If we're updating an existing book, we want to keep the IDs (as much as possible; sometimes
// the number of xmatter pages changes and we have to add IDs). In this case, oldIds is obtained
diff --git a/src/BloomExe/Book/XMatterPackFinder.cs b/src/BloomExe/Book/XMatterPackFinder.cs
index e8897a00c3d4..5e5e68e9b57d 100644
--- a/src/BloomExe/Book/XMatterPackFinder.cs
+++ b/src/BloomExe/Book/XMatterPackFinder.cs
@@ -40,7 +40,6 @@ public IEnumerable All
"sharp",
"forunittest",
"templatestarter",
- "coverisimage",
};
public IEnumerable GetXMattersToOfferInSettings(
diff --git a/src/BloomExe/SubscriptionAndFeatures/FeatureRegistry.cs b/src/BloomExe/SubscriptionAndFeatures/FeatureRegistry.cs
index 05dc1841dd17..166c23ce3c33 100644
--- a/src/BloomExe/SubscriptionAndFeatures/FeatureRegistry.cs
+++ b/src/BloomExe/SubscriptionAndFeatures/FeatureRegistry.cs
@@ -16,7 +16,6 @@ public enum FeatureName
ViewBookHistory, // Sort of tied to team collections now, but nothing says it has to be in the future...
Motion,
Music,
- FullPageCoverImage, // The whole front cover is one full-bleed image
CustomXMatterPage,
WholeTextBoxAudio,
@@ -161,21 +160,6 @@ public static class FeatureRegistry
PreventPublishingInUnsupportedMediums = PreventionMethod.Remove,
},
new FeatureInfo
- {
- Feature = FeatureName.FullPageCoverImage,
- SubscriptionTier = SubscriptionTier.Pro,
- ExistsInPageXPath = "self::div[contains(@class,'cover-is-image')]",
- // This disabling doesn't get a chance to work, because when we bring a book's XMatter
- // up to date, we update the cover-is-image class to something consistent with both
- // the AppearanceSettings.CoverIsImage and Subscription.HaveActiveSubscription.
- // (The latter should be changed to something involving this feature, but even then,
- // the class will be removed by the time we execute the code that processes these properties.)
- PreventPublishingInDerivativeBooks = PreventionMethod.DisabledByModifyingDom,
- PreventPublishingInOriginalBooks = PreventionMethod.DisabledByModifyingDom,
- ClassesToRemoveToDisable = "cover-is-image no-margin-page",
- L10NId = "BookSettings.CoverIsImage",
- },
- new FeatureInfo
{
Feature = FeatureName.CustomXMatterPage,
SubscriptionTier = SubscriptionTier.Pro,
diff --git a/src/BloomTests/Book/BookStorageTests.cs b/src/BloomTests/Book/BookStorageTests.cs
index 67e3ebcad096..df7a57c7c2ac 100644
--- a/src/BloomTests/Book/BookStorageTests.cs
+++ b/src/BloomTests/Book/BookStorageTests.cs
@@ -2205,6 +2205,199 @@ public void PerformNecessaryMaintenanceOnBook_HandlesMultipleSVGs()
);
}
+ [Test]
+ public void MigrateToLevel14CoverIsImageToCustomLayout_ConvertsCoverClassAndCompletes()
+ {
+ var storage = GetInitialStorageWithCustomHtml(
+ @"
+
+
+
+"
+ );
+
+ storage.BookInfo.AppearanceSettings.UpdateFromDynamic(
+ new Newtonsoft.Json.Linq.JObject { ["coverIsImage"] = true }
+ );
+
+ storage.MigrateToLevel14CoverIsImageToCustomLayout(null);
+
+ var maintLevel = storage.Dom.GetMetaValue("maintenanceLevel", "0");
+ Assert.That(maintLevel, Is.EqualTo("14"));
+ Assert.That(
+ storage.Dom.SafeSelectNodes("//div[contains(@class,'cover-is-image')]").Count,
+ Is.EqualTo(0)
+ );
+ Assert.That(
+ storage.Dom.SafeSelectNodes("//div[contains(@class,'bloom-customLayout')]").Count,
+ Is.EqualTo(1)
+ );
+ var appearanceProperties = (System.Collections.Generic.IDictionary)
+ storage.BookInfo.AppearanceSettings.TestOnlyPropertiesAccess;
+ Assert.That(appearanceProperties.ContainsKey("coverIsImage"), Is.False);
+ }
+
+ [Test]
+ public void MigrateToLevel14CoverIsImageToCustomLayout_WhenClassMissingButSettingPresent_MarksOutsideFrontCoverCustomLayout()
+ {
+ var storage = GetInitialStorageWithCustomHtml(
+ @"
+
+
+
+"
+ );
+
+ storage.BookInfo.AppearanceSettings.UpdateFromDynamic(
+ new Newtonsoft.Json.Linq.JObject { ["coverIsImage"] = true }
+ );
+
+ storage.MigrateToLevel14CoverIsImageToCustomLayout(null);
+
+ Assert.That(
+ storage
+ .Dom.SafeSelectNodes(
+ "//div[contains(@class,'outsideFrontCover') and contains(@class,'bloom-customLayout')]"
+ )
+ .Count,
+ Is.EqualTo(1)
+ );
+ var appearanceProperties = (System.Collections.Generic.IDictionary)
+ storage.BookInfo.AppearanceSettings.TestOnlyPropertiesAccess;
+ Assert.That(appearanceProperties.ContainsKey("coverIsImage"), Is.False);
+ }
+
+ [Test]
+ public void MigrateToLevel14CoverIsImageToCustomLayout_WhenSettingIsFalse_RemovesPropertyWithoutConvertingCover()
+ {
+ var storage = GetInitialStorageWithCustomHtml(
+ @"
+
+
+
+"
+ );
+
+ storage.BookInfo.AppearanceSettings.UpdateFromDynamic(
+ new Newtonsoft.Json.Linq.JObject { ["coverIsImage"] = false }
+ );
+
+ storage.MigrateToLevel14CoverIsImageToCustomLayout(null);
+
+ Assert.That(
+ storage.Dom.SafeSelectNodes("//div[contains(@class,'cover-is-image')]").Count,
+ Is.EqualTo(1)
+ );
+ Assert.That(
+ storage.Dom.SafeSelectNodes("//div[contains(@class,'bloom-customLayout')]").Count,
+ Is.EqualTo(0)
+ );
+ var appearanceProperties = (System.Collections.Generic.IDictionary)
+ storage.BookInfo.AppearanceSettings.TestOnlyPropertiesAccess;
+ Assert.That(appearanceProperties.ContainsKey("coverIsImage"), Is.False);
+ Assert.That(storage.Dom.GetMetaValue("maintenanceLevel", "0"), Is.EqualTo("14"));
+ }
+
+ [Test]
+ public void RestoreStuffBeforeMigration_WhenLegacyCoverCaptured_RestoresOutsideFrontCoverMarginBoxContent()
+ {
+ var storage = GetInitialStorageWithCustomHtml(
+ @"
+
+
+
+
+ "
+ );
+
+ var book = CreateBook();
+
+ book.BringBookUpToDate(new NullProgress());
+
+ AssertThatXmlIn
+ .Dom(book.RawDom)
+ .HasSpecifiedNumberOfMatchesForXpath(
+ "//div[@id='bloomDataDiv']/div[@data-book='customOutsideFrontCover' and @lang='*']//div[contains(@class,'migrated-marker') and text()='preserve me']",
+ 1
+ );
+ }
+
[Test]
public void BringBookUpToDate_RepairQuestionsPages_DoesNotMessUpGoodPages()
{
diff --git a/src/BloomTests/Publish/PublishHelperTests.cs b/src/BloomTests/Publish/PublishHelperTests.cs
index 3472a62b83c4..a3f55c33d733 100644
--- a/src/BloomTests/Publish/PublishHelperTests.cs
+++ b/src/BloomTests/Publish/PublishHelperTests.cs
@@ -1078,15 +1078,15 @@ public void RemovePagesByFeatureSystem_DoesNotRemoveFullCoverImagePages()
}
[Test]
- public void RemoveClassesToDisableFeatures_RemovesClassesFromFullCoverImagePages()
+ public void RemoveClassesToDisableFeatures_RemovesClassesFromCustomXMatterPages()
{
var doc = SafeXmlDocument.Create();
doc.LoadXml(
@"
-
-
Front Cover
+
+
Back Cover
-
+
"
);
var pageElts = doc.SafeSelectNodes("//div[contains(@class,'bloom-page')]")
@@ -1103,15 +1103,13 @@ public void RemoveClassesToDisableFeatures_RemovesClassesFromFullCoverImagePages
AssertThatXmlIn
.Element(pageElts[0])
.HasSpecifiedNumberOfMatchesForXpath(
- "//div[@id='fullCoverImage' and @class='bloom-page cover coverColor bloom-frontMatter frontCover outsideFrontCover side-right Device16x9Landscape']",
+ "//div[@id='customBackCover' and @class='bloom-page outsideBackCover side-left']",
1
);
Assert.That(
messages,
Has.Member(
- // Would like it to be this, but the localization manager doesn't work well enough in unit tests.
- //@"The feature ""Fill the front cover with a single image"" was removed from this book because it requires a higher subscription tier"
- @"The feature ""BookSettings.CoverIsImage"" was removed from this book because it requires a higher subscription tier"
+ @"The feature ""PageLayout.CustomXMatterPage"" was removed from this book because it requires a higher subscription tier"
)
);
}
diff --git a/src/content/templates/xMatter/CoverIsImage-XMatter/CoverIsImage-XMatter.pug b/src/content/templates/xMatter/CoverIsImage-XMatter/CoverIsImage-XMatter.pug
deleted file mode 100644
index 71d8072a5644..000000000000
--- a/src/content/templates/xMatter/CoverIsImage-XMatter/CoverIsImage-XMatter.pug
+++ /dev/null
@@ -1,8 +0,0 @@
-include ../bloom-xmatter-mixins.pug
-
-doctype html
-html
- head
- body
- +page-cover('Front Cover')(data-export='front-matter-cover', data-xmatter-page='frontCover').cover-is-image.no-margin-page.frontCover.outsideFrontCover#b5169df5-6a40-4c52-bd30-4cab45afe0ed
- +standard-cover-image.bloom-imageObjectFit-cover