Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions DistFiles/localization/en/Bloom.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -1460,6 +1460,11 @@
<source xml:lang="en">Video</source>
<note>ID: EditTab.CustomPage.Video</note>
</trans-unit>
<trans-unit id="EditTab.CustomPage.Table" translate="no" sil:dynamic="true">
<source xml:lang="en">Table</source>
<note>ID: EditTab.CustomPage.Table</note>
<note>Shown as an option when a Custom Page section can contain a table (experimental feature)</note>
</trans-unit>
<trans-unit id="EditTab.CustomPage.TextBox" sil:dynamic="true">
<source xml:lang="en">Text Box</source>
<note>ID: EditTab.CustomPage.TextBox</note>
Expand Down
5 changes: 5 additions & 0 deletions DistFiles/localization/en/BloomMediumPriority.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
<note>This checkbox enables the experimental publish-to-apps workflow.</note>
<note>Obsolete as of Bloom 6.5</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.Experimental.Tables" translate="no">
<source xml:lang="en">Tables</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.Experimental.Tables</note>
<note>Label of a checkbox in the Experimental Features list of the Advanced tab of Collection Settings. Turning it on lets the user add a table to a custom page.</note>
</trans-unit>
<trans-unit id="Feature.AiImageEditing" translate="no">
<source xml:lang="en">Edit Images with AI</source>
<note>ID: Feature.AiImageEditing</note>
Expand Down
10 changes: 8 additions & 2 deletions src/BloomBrowserUI/bookEdit/StyleEditor/StyleEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1102,8 +1102,14 @@ export default class StyleEditor {
fmtButton.style.top = bottom / scale - this.fmtButtonHeight + "px";
fmtButton.style.left = "unset";
fmtButton.style.right = "0";
} else if (element.closest(kCanvasElementSelector)) {
// This element is inside a text-over-picture element.
} else if (
element.closest(kCanvasElementSelector) &&
!element.closest(".bloom-cell")
) {
// This element is inside a text-over-picture element. (A table cell is
// excluded: a canvas element holding a table has a box per cell, so the
// button belongs inside the cell, where the code below puts it, and not
// out to the left of the whole table.)
fmtButton.style.top = bottom / scale + "px";
fmtButton.style.left = -5 - this.fmtButtonWidth + "px";
} else {
Expand Down
9 changes: 9 additions & 0 deletions src/BloomBrowserUI/bookEdit/css/editMode.less
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,15 @@ body:has(#canvas-element-context-controls:hover) .bloom-page,
}
}

// A table cell is often smaller than the 63 by 52 pixel image buttons, so a button
// there hides the picture instead of helping the user edit it. In a cell the user
// clicks the picture, which selects it and puts the canvas element toolbar below the
// cell. See the matching test in SetupBloomCanvas (bloomImages.ts), which does not
// even make the "change image" button for a cell.
.bloom-cell .imageButton {
display: none !important;
}

/*....NB: we have 2 rules because some pageLabels may be within the margin box, some outside*/
.marginBox div.pageLabel {
/*+placement:anchor-top-left -1px -29px;*/
Expand Down
23 changes: 23 additions & 0 deletions src/BloomBrowserUI/bookEdit/css/origamiEditing.less
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@
color: @OrigamiActiveColor;
}

// The Table entry when the collection's subscription tier is below the one tables
// need. It stays on offer so the user can see that tables exist, but it is dimmed
// and carries the same badge the toolbox puts on its subscription-only tools.
// Clicking it opens the dialog that says which subscription tables need; see
// createTableSelector in origami.ts.
.selector-links .origami-tableSelector {
a.origami-featureNeedsSubscription {
opacity: 0.38; // matches kBloomDisabledOpacity
cursor: pointer;
text-decoration: none;
}
.subscription-badge {
display: inline-block;
vertical-align: text-bottom;
height: 1.2em;
width: 1.2em;
margin-left: 3px;
cursor: pointer;
background: no-repeat center/contain
url("../../images/bloom-enterprise-badge.svg");
}
}

.origami-controls {
.button,
.origami-controls .button:visited {
Expand Down
61 changes: 58 additions & 3 deletions src/BloomBrowserUI/bookEdit/editablePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import {
} from "./js/canvasElementManager/CanvasElementManager";
import { kCanvasElementSelector } from "./toolbox/canvas/canvasElementConstants";
import { renderDragActivityTabControl } from "./js/AbovePageControls";
import { tableHistoryManager } from "bloom-table";
import {
getCkeditorChangeOrder,
getTableChangeOrder,
shouldUndoGoToTable,
} from "./js/undoOrdering";

function getPageId(): string {
const page = document.querySelector(".bloom-page");
Expand Down Expand Up @@ -68,10 +74,14 @@ export interface IPageFrameExports {
getTheOneCanvasElementManager(): CanvasElementManager;

ckeditorCanUndo(): boolean;
ckeditorUndo(): void;
ckeditorUndo(): boolean;
imageOperationCanUndo(): boolean;
imageOperationUndo(): boolean;

tableShouldHandleUndo(): boolean;
tableCanUndo(): boolean;
tableUndo(): void;

addRequestPageContentDelay(id: string): void;
removeRequestPageContentDelay(id: string): void;

Expand Down Expand Up @@ -334,9 +344,48 @@ export function ckeditorCanUndo(): boolean {
return false;
}

export function ckeditorUndo() {
/**
* Undo the most recent change ckeditor knows about, and say whether it undid anything.
*
* undo() answers false when it finds nothing to restore, which happens even after
* ckeditorCanUndo() said yes: ckeditor sets its hasUndo flag on the first keystroke of a
* group and clears it only when it next refreshes its state, so a manager whose snapshots
* have already been restored keeps claiming an undo it cannot perform. The caller needs to
* know, so that an Undo the person pressed is not swallowed here. (See workspaceRoot.handleUndo.)
*/
export function ckeditorUndo(): boolean {
// review: do we need to examine all instances?
(<any>CKEDITOR.currentInstance).undoManager.undo();
return (<any>CKEDITOR.currentInstance).undoManager.undo();
}

// Whether the next Undo belongs to the bloom-table library (which lives in this page iframe,
// where the tables are attached) rather than to CKEditor. Called cross-frame from
// workspaceRoot.canUndo()/handleUndo().
//
// It is not enough that the library has an operation to undo: its history holds every structural
// operation until that operation is undone, so after "add a row, then type in a cell" both it and
// CKEditor have something, and the typing is what came last. See undoOrdering.ts.
export function tableShouldHandleUndo(): boolean {
return shouldUndoGoToTable({
tableCanUndo: tableHistoryManager.canUndo(),
ckeditorCanUndo: ckeditorCanUndo(),
tableChangeOrder: getTableChangeOrder(),
ckeditorChangeOrder: getCkeditorChangeOrder(),
});
}

// Whether the bloom-table library has an operation in its history that it could undo. This is
// the plain question, with no reckoning of what CKEditor has done; tableShouldHandleUndo() above
// is the one that decides whose Undo it is.
export function tableCanUndo(): boolean {
return tableHistoryManager.canUndo();
}

// Undo the most recent bloom-table operation. Called cross-frame from
// workspaceRoot.handleUndo(). undoLast() finds the relevant attached table on
// its own, so the caller needn't hold a table reference.
export function tableUndo(): void {
tableHistoryManager.undoLast();
}

for (let j = 0; j < styleSheets.length; j++) {
Expand Down Expand Up @@ -422,6 +471,9 @@ interface EditablePageBundleApi {
getTheOneCanvasElementManager: typeof getTheOneCanvasElementManager;
ckeditorCanUndo: typeof ckeditorCanUndo;
ckeditorUndo: typeof ckeditorUndo;
tableShouldHandleUndo: typeof tableShouldHandleUndo;
tableCanUndo: typeof tableCanUndo;
tableUndo: typeof tableUndo;
addRequestPageContentDelay: typeof addRequestPageContentDelay;
removeRequestPageContentDelay: typeof removeRequestPageContentDelay;
e2eSetActiveCanvasElementByIndex: typeof e2eSetActiveCanvasElementByIndex;
Expand Down Expand Up @@ -501,6 +553,9 @@ window.editablePageBundle = {
getTheOneCanvasElementManager,
ckeditorCanUndo,
ckeditorUndo,
tableShouldHandleUndo,
tableCanUndo,
tableUndo,
addRequestPageContentDelay,
removeRequestPageContentDelay,
e2eSetActiveCanvasElementByIndex,
Expand Down
86 changes: 78 additions & 8 deletions src/BloomBrowserUI/bookEdit/js/bloomEditing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
} from "./bloomVideo";
import { SetupWidgetEditing } from "./bloomWidgets";
import { setupOrigami, cleanupOrigami } from "./origami";
import { SetupTableEditing, TeardownTableEditing } from "./tableEditing";
import { removeTableEditingArtifacts } from "bloom-table";
import theOneLocalizationManager from "../../lib/localizationManager/localizationManager";
import StyleEditor from "../StyleEditor/StyleEditor";
import OverflowChecker from "../OverflowChecker/OverflowChecker";
Expand Down Expand Up @@ -79,6 +81,7 @@ import { handleUndo } from "../workspaceRoot";
import { setupPageLayoutMenu } from "../toolbox/canvas/customXmatterPage";
import { setupTextContextMenu } from "../textContextMenu/TextContextMenu";
import { resetAbovePageControls } from "./AbovePageControls";
import { noteCkeditorChange } from "./undoOrdering";

// Allows toolbox code to make an element properly in the context of this iframe.
export function makeElement(
Expand Down Expand Up @@ -330,7 +333,7 @@ function AddEditKeyHandlers(container) {
// But there may be yet others that are not visible when we run this but which soon will be,
// such as image descriptions. We don't seem to need the optimization, so let's just do
// them all.)
function AddLanguageTags(container) {
export function AddLanguageTags(container) {
$(container)
.find(".bloom-editable[contentEditable=true]")
.each(function () {
Expand Down Expand Up @@ -572,6 +575,7 @@ export function SetupElements(

SetupVideoEditing(container);
SetupWidgetEditing(container);
SetupTableEditing(container);
initializeCanvasElementManager();
initChoiceWidgetsForEditing();

Expand Down Expand Up @@ -1333,6 +1337,8 @@ function removeEditingDebris() {
textLabels[i].remove();
}
removeTransientVideoTimestampParams(document.body);
removeTableEditingArtifacts(document);
TeardownTableEditing(document.body);
cleanupNiceScroll(); // don't leave the nicescroll debris around
}

Expand Down Expand Up @@ -1667,15 +1673,38 @@ export const copySelection = () => {
copyImpl();
};

/**
* The editable that a clipboard command acts on in its entirety, when there is no
* text selection to act on instead.
*
* Normally that is the canvas element's own text. A table canvas element has no
* such thing: it holds one editable per cell, so the first one found is the
* top-left cell whichever cell the user is working in, and a command acting on it
* would read or overwrite text the user is not looking at. So inside a table the
* cell that holds the caret is what the command acts on.
*/
function editableForWholeElementClipboard(
activeCanvasElement: HTMLElement | undefined,
): HTMLElement | undefined {
const focusedEditable = (
document.activeElement as HTMLElement | null
)?.closest<HTMLElement>(".bloom-editable");
if (focusedEditable?.closest(".bloom-cell")) {
return focusedEditable;
}
return activeCanvasElement?.getElementsByClassName(
"bloom-editable bloom-visibility-code-on",
)[0] as HTMLElement | undefined;
}

async function copyImpl() {
const sel = document.getSelection();
if (!sel?.toString()) {
const activeCanvasElement =
theOneCanvasElementManager?.getActiveElement();
const activeCanvasElementEditable =
activeCanvasElement?.getElementsByClassName(
"bloom-editable bloom-visibility-code-on",
)[0] as HTMLElement;
const activeCanvasElementEditable = editableForWholeElementClipboard(
activeCanvasElement,
) as HTMLElement;

// No active text selection to copy; copy the canvas element's entire content.
// There's a slight chance that the user wanted to copy some trailing
Expand Down Expand Up @@ -1793,16 +1822,25 @@ async function pasteImpl(imageAvailable: boolean) {
// Enhance: might there be a case where text should be pasted as a new canvas element?
// Enhance: we'd like to be able to copy and paste entire canvas overlays (including target if any).
const activeElement = canvasElementManager?.getActiveElement();
const activeCanvasElementEditable = activeElement?.getElementsByClassName(
"bloom-editable bloom-visibility-code-on",
)[0] as HTMLElement;
const activeCanvasElementEditable = editableForWholeElementClipboard(
activeElement,
) as HTMLElement;
// With the caret in a table cell the paste belongs at the caret, like any other
// typing in that cell, so the "replace the element's whole content" branch below
// must not claim it. (The Ctrl+V route never reaches here at all: pasteHandler
// leaves a paste inside a bloom-editable to the browser. This is the top bar's
// Paste button.)
const caretIsInTableCell = !!(
document.activeElement as HTMLElement | null
)?.closest(".bloom-cell");

const textToPaste = await navigator.clipboard.readText();
if (!textToPaste) {
return;
}
if (
activeCanvasElementEditable &&
!caretIsInTableCell &&
activeElement !== canvasElementManager.theCanvasElementWeAreTextEditing
) {
// We've issued a paste command on a canvas element that isn't active for editing.
Expand Down Expand Up @@ -1967,6 +2005,30 @@ export function attachToCkEditor(element) {
$("body").addClass("hideAllCKEditors");
const ckedit = CKEDITOR.inline(element);

// CKEditor copies the element's content when the instance is created and writes that copy
// back into the element when its asynchronous startup finishes, which is typically half a
// second later and has been measured at more than a second. The element is contenteditable
// the whole time, so anything typed during that window is silently overwritten by the copy.
// A table cell hits this every time: the library builds the cell empty, Bloom attaches an
// editor to it, and the user can type in it at once. Remember what the element holds just
// before the startup write (our "loaded" handler runs before it) and, if the write did
// change the content, put it back and give the editor the same content, so what was typed
// survives and the editor agrees with the DOM.
// The comparison is on the text rather than the markup, because CKEditor's startup write
// tidies an empty paragraph (<p></p> becomes <p><br /></p>) even when nothing was typed,
// and that is not a loss worth undoing.
let contentBeforeStartupWrite = element.innerHTML;
let textBeforeStartupWrite = element.textContent;
ckedit.on("loaded", () => {
contentBeforeStartupWrite = element.innerHTML;
textBeforeStartupWrite = element.textContent;
});
ckedit.on("instanceReady", () => {
if (element.textContent !== textBeforeStartupWrite) {
ckedit.setData(contentBeforeStartupWrite);
}
});

// Record the div of the edit box for use later in positioning the format bar.
mapCkeditDiv[ckedit.id] = element;

Expand Down Expand Up @@ -2043,6 +2105,14 @@ export function attachToCkEditor(element) {
}
});

// A table's own undo stack is separate from this one, and whichever of the two was written
// to last is the one the next Undo belongs to. See undoOrdering.ts. The undoable() test keeps
// the changes ckeditor makes while it attaches itself to a box out of the reckoning: only a
// change a person could undo counts as a change the person made.
ckedit.on("change", () => {
if (ckedit.undoManager?.undoable()) noteCkeditorChange();
});

// hide the toolbar when ckeditor starts
ckedit.on("instanceReady", (evt) => {
const editor = evt["editor"];
Expand Down
8 changes: 7 additions & 1 deletion src/BloomBrowserUI/bookEdit/js/bloomImages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,13 @@ function SetupBloomCanvas(bloomCanvas: HTMLElement) {
// rely on a call to SetImageDisplaySize() when the image is added.
const img = $(bloomCanvas).find("img");
SetImageDisplaySizeIfCalledFor($(bloomCanvas), img);
SetupChangeImageButton(bloomCanvas);
// In a table cell we leave the image with no overlay button at all. The button is
// 63 by 52 pixels, which is larger than many cells, so it hides the picture it is
// supposed to edit. Clicking the picture selects the background image instead, and
// that puts the canvas element toolbar under the cell, where there is room for it.
if (!bloomCanvas.closest(".bloom-cell")) {
SetupChangeImageButton(bloomCanvas);
}

$(bloomCanvas)
.mouseenter(function () {
Expand Down
Loading