Skip to content

Commit 9fe955c

Browse files
committed
Make page gutter understandable
wip Remove edge-to-edge work to separate branch
1 parent 95c8722 commit 9fe955c

4 files changed

Lines changed: 288 additions & 1 deletion

File tree

src/BloomBrowserUI/AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ When working in the front-end, cd to src/BloomBrowserUI
1717
## Code Style
1818

1919
- Always use arrow functions and function components in React
20+
- For functions, prefer typescript "function" syntax over const foo = () ==> functions.
21+
- When writing less, use new css features supported by our current version of webview2. E.g. "is()".
2022

2123
- Avoid removing existing comments.
2224
- Avoid adding a comment like "// add this line".

src/BloomBrowserUI/bookEdit/css/editMode.less

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,42 @@ body {
6868
transition: transform 20ms;
6969
}
7070

71+
.bloom-page-tooltip {
72+
position: fixed;
73+
z-index: @formatButtonZIndex;
74+
display: none;
75+
pointer-events: none;
76+
white-space: nowrap;
77+
padding: 2px 6px;
78+
border: none;
79+
border-radius: 3px;
80+
background-color: black;
81+
color: white;
82+
font-family: @UIFontStack;
83+
font-size: 8pt;
84+
}
85+
86+
.bloom-page-tooltip-target {
87+
position: absolute;
88+
pointer-events: auto;
89+
background-color: transparent;
90+
}
91+
92+
.bloom-gutter-tooltip-target {
93+
top: 0;
94+
bottom: 0;
95+
width: var(--page-gutter);
96+
z-index: 3;
97+
}
98+
99+
.bloom-gutter-target-side-left {
100+
right: 0;
101+
}
102+
103+
.bloom-gutter-target-side-right {
104+
left: 0;
105+
}
106+
71107
// See comments on .bloom-mediaBox in basePage.less for a description of the mediaBox.
72108
// Here, we are causing it to be visible when desired.
73109
.bloom-mediaBox {
@@ -142,6 +178,41 @@ body:has(.MuiPaper-root:hover) > .qtip {
142178
outline: none !important;
143179
}
144180

181+
// Show the paper-page gutter (binding space) in edit mode.
182+
// Show this only while the page is active (hover/focus) like other edit affordances.
183+
.bloom-page:is(.side-left, .side-right):not(.outsideFrontCover):not(
184+
.outsideBackCover
185+
)::before {
186+
content: "";
187+
position: absolute;
188+
top: 0;
189+
bottom: 0;
190+
width: var(--page-gutter);
191+
background-color: var(--page-structure-color);
192+
box-sizing: border-box;
193+
pointer-events: none;
194+
z-index: 2;
195+
opacity: 0;
196+
}
197+
.bloom-page.side-left:not(.outsideFrontCover):not(.outsideBackCover)::before {
198+
right: 0;
199+
border-left: 2px dashed white;
200+
}
201+
.bloom-page.side-right:not(.outsideFrontCover):not(.outsideBackCover)::before {
202+
left: 0;
203+
border-right: 2px dashed white;
204+
}
205+
206+
.bloom-page:is(:hover, :focus-within):is(.side-left, .side-right):not(
207+
.outsideFrontCover
208+
):not(.outsideBackCover)::before,
209+
body:has(#canvas-element-context-controls:hover)
210+
.bloom-page:is(.side-left, .side-right):not(.outsideFrontCover):not(
211+
.outsideBackCover
212+
)::before {
213+
opacity: 1;
214+
}
215+
145216
// Keep together here all the effects we want when hovering (previously: also when focus-within)
146217
// the bloom-page. All of them need to also apply when the canvas element context controls are hovered,
147218
// even though that is no longer a child of the bloom-page. (That's what the second rule is for.)

src/BloomBrowserUI/bookEdit/js/bloomEditing.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ import {
4040
getToolboxBundleExports,
4141
} from "./bloomFrames";
4242
import { showInvisibles, hideInvisibles } from "./showInvisibles";
43+
import {
44+
cleanupPageHoverTooltips,
45+
getBodyInnerHtmlWithoutPageHoverTooltips,
46+
setupPageHoverTooltips,
47+
} from "./pageHoverTooltips";
4348

4449
//promise may be needed to run tests with phantomjs
4550
//import promise = require('es6-promise');
@@ -155,6 +160,7 @@ function Cleanup() {
155160
cleanupImages();
156161
cleanupOrigami();
157162
cleanupNiceScroll();
163+
cleanupPageHoverTooltips();
158164
}
159165

160166
//add a delete button which shows up when you hover
@@ -1096,6 +1102,7 @@ export function bootstrap() {
10961102

10971103
SetupElements(document.body);
10981104
OneTimeSetup();
1105+
setupPageHoverTooltips();
10991106

11001107
// configure ckeditor
11011108
if (typeof CKEDITOR === "undefined") return; // this happens during unit testing
@@ -1338,7 +1345,7 @@ export function getBodyContentForSavePage() {
13381345
);
13391346
}
13401347

1341-
const result = document.body.innerHTML;
1348+
const result = getBodyInnerHtmlWithoutPageHoverTooltips();
13421349

13431350
if (canvasElementEditingOn) {
13441351
theOneCanvasElementManager.turnOnCanvasElementEditing();
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import $ from "jquery";
2+
import theOneLocalizationManager from "../../lib/localizationManager/localizationManager";
3+
4+
const kTooltipTargetClass = "bloom-page-tooltip-target";
5+
const kGutterTargetClass = "bloom-gutter-tooltip-target";
6+
const kGutterLeftClass = "bloom-gutter-target-side-left";
7+
const kGutterRightClass = "bloom-gutter-target-side-right";
8+
const kTooltipTextAttribute = "data-bloom-page-tooltip-text";
9+
const kTooltipBubbleClass = "bloom-page-tooltip";
10+
const kTooltipUiMarkerAttribute = "data-bloom-ui";
11+
const kTooltipUiMarkerValue = "page-hover-tooltip";
12+
const kDefaultGutterTooltipText =
13+
"Page Gutter: space for staples or other binding.";
14+
15+
let pageHoverTooltip: JQuery | undefined;
16+
let gutterTooltipText = kDefaultGutterTooltipText;
17+
let formatDialogTitleTooltipText = "Format";
18+
let activeTooltipText: string | undefined;
19+
let tooltipLocalizationInitialized = false;
20+
21+
interface MousePositionEvent {
22+
clientX: number;
23+
clientY: number;
24+
}
25+
26+
// This module supports tooltips for page editing regions that may not have their own interactive DOM elements.
27+
// For those cases, we create temporary "target" elements (class bloom-page-tooltip-target) and position
28+
// them over the page region (currently, the paper gutter). These targets all get the bloom-ui class so they
29+
// are treated as edit-time markup. Also, when saving, bloomEditing.ts uses
30+
// getBodyInnerHtmlWithoutPageHoverTooltips() so these temporary elements are removed from the HTML that is
31+
// written to disk, even though they remain available in the live editing DOM.
32+
33+
function hidePageHoverTooltip(): void {
34+
activeTooltipText = undefined;
35+
pageHoverTooltip?.hide();
36+
}
37+
38+
function ensurePageHoverTooltipBubble(): void {
39+
if (pageHoverTooltip) {
40+
return;
41+
}
42+
43+
pageHoverTooltip = $(`<div class='bloom-ui ${kTooltipBubbleClass}'></div>`);
44+
pageHoverTooltip.attr(kTooltipUiMarkerAttribute, kTooltipUiMarkerValue);
45+
pageHoverTooltip.text(gutterTooltipText);
46+
$("body").append(pageHoverTooltip);
47+
}
48+
49+
function removeTooltipTargets(): void {
50+
$(`.${kTooltipTargetClass}`).remove();
51+
}
52+
53+
function makeTooltipTarget(
54+
page: HTMLElement,
55+
extraClasses: string[],
56+
tooltipText: string,
57+
): void {
58+
const target = document.createElement("div");
59+
target.classList.add("bloom-ui", kTooltipTargetClass, ...extraClasses);
60+
target.setAttribute(kTooltipTextAttribute, tooltipText);
61+
target.setAttribute(kTooltipUiMarkerAttribute, kTooltipUiMarkerValue);
62+
page.appendChild(target);
63+
}
64+
65+
function addGutterTooltipTargets(): void {
66+
document.querySelectorAll("div.bloom-page").forEach((pageElement) => {
67+
const page = pageElement as HTMLElement;
68+
if (
69+
page.classList.contains("outsideFrontCover") ||
70+
page.classList.contains("outsideBackCover")
71+
) {
72+
return;
73+
}
74+
75+
if (page.classList.contains("side-left")) {
76+
makeTooltipTarget(
77+
page,
78+
[kGutterTargetClass, kGutterLeftClass],
79+
gutterTooltipText,
80+
);
81+
return;
82+
}
83+
84+
if (page.classList.contains("side-right")) {
85+
makeTooltipTarget(
86+
page,
87+
[kGutterTargetClass, kGutterRightClass],
88+
gutterTooltipText,
89+
);
90+
}
91+
});
92+
}
93+
94+
function updateGutterTooltipTexts(): void {
95+
document
96+
.querySelectorAll(`.${kGutterTargetClass}`)
97+
.forEach((targetElement) => {
98+
targetElement.setAttribute(
99+
kTooltipTextAttribute,
100+
gutterTooltipText,
101+
);
102+
});
103+
}
104+
105+
function getTooltipTextForTarget(target: HTMLElement): string | undefined {
106+
if (target.id === "formatButton") {
107+
return formatDialogTitleTooltipText;
108+
}
109+
110+
const value = target.getAttribute(kTooltipTextAttribute);
111+
return value || undefined;
112+
}
113+
114+
function setActiveTooltipText(tooltipText: string | undefined): void {
115+
activeTooltipText = tooltipText;
116+
if (!activeTooltipText) {
117+
hidePageHoverTooltip();
118+
return;
119+
}
120+
121+
pageHoverTooltip?.text(activeTooltipText).show();
122+
}
123+
124+
function moveTooltipBubble(event: MousePositionEvent): void {
125+
if (!activeTooltipText) {
126+
return;
127+
}
128+
129+
pageHoverTooltip?.css({
130+
left: event.clientX + 12,
131+
top: event.clientY + 12,
132+
});
133+
}
134+
135+
function setupTooltipLocalization(): void {
136+
if (tooltipLocalizationInitialized) {
137+
return;
138+
}
139+
140+
tooltipLocalizationInitialized = true;
141+
142+
theOneLocalizationManager
143+
.asyncGetText("EditTab.Tooltip.Gutter", kDefaultGutterTooltipText, "")
144+
.done((result) => {
145+
gutterTooltipText = result;
146+
updateGutterTooltipTexts();
147+
});
148+
149+
theOneLocalizationManager
150+
.asyncGetText("EditTab.FormatDialog.Format", "Format", "")
151+
.done((result) => {
152+
formatDialogTitleTooltipText = result;
153+
});
154+
}
155+
156+
export function setupPageHoverTooltips(): void {
157+
ensurePageHoverTooltipBubble();
158+
removeTooltipTargets();
159+
addGutterTooltipTargets();
160+
setupTooltipLocalization();
161+
162+
$(document)
163+
.off("mouseenter.bloomPageTooltip")
164+
.on(
165+
"mouseenter.bloomPageTooltip",
166+
`.${kTooltipTargetClass}, #formatButton`,
167+
function (e) {
168+
const target = this as HTMLElement;
169+
setActiveTooltipText(getTooltipTextForTarget(target));
170+
moveTooltipBubble(e as unknown as MousePositionEvent);
171+
},
172+
)
173+
.off("mousemove.bloomPageTooltip")
174+
.on("mousemove.bloomPageTooltip", function (e) {
175+
moveTooltipBubble(e as unknown as MousePositionEvent);
176+
})
177+
.off("mouseleave.bloomPageTooltip")
178+
.on(
179+
"mouseleave.bloomPageTooltip",
180+
`.${kTooltipTargetClass}, #formatButton`,
181+
() => {
182+
setActiveTooltipText(undefined);
183+
},
184+
);
185+
}
186+
187+
export function cleanupPageHoverTooltips(): void {
188+
hidePageHoverTooltip();
189+
$(document).off(".bloomPageTooltip");
190+
removeTooltipTargets();
191+
pageHoverTooltip?.remove();
192+
pageHoverTooltip = undefined;
193+
}
194+
195+
function removePageHoverTooltipMarkupFrom(root: ParentNode): void {
196+
root.querySelectorAll(
197+
`[${kTooltipUiMarkerAttribute}="${kTooltipUiMarkerValue}"]`,
198+
).forEach((element) => {
199+
element.remove();
200+
});
201+
}
202+
203+
export function getBodyInnerHtmlWithoutPageHoverTooltips(): string {
204+
const bodyClone = document.body.cloneNode(true) as HTMLElement;
205+
removePageHoverTooltipMarkupFrom(bodyClone);
206+
return bodyClone.innerHTML;
207+
}

0 commit comments

Comments
 (0)