Skip to content

Commit 38b1a81

Browse files
authored
Fix drawing EPUB decorators with vertical text (#671)
1 parent e5f5ad1 commit 38b1a81

File tree

5 files changed

+175
-76
lines changed

5 files changed

+175
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. Take a look
1212

1313
* Fixed vertical text scrolling in EPUB for right-to-left reading progression (contributed by [@shovel-kun](https://github.com/readium/kotlin-toolkit/pull/656)).
1414
* Fixed notifying the current location when using vertical text scrolling in EPUB (contributed by [@shovel-kun](https://github.com/readium/kotlin-toolkit/pull/656)).
15+
* Fixed drawing EPUB decorators with vertical text (contributed by [@shovel-kun](https://github.com/readium/kotlin-toolkit/pull/671)).
1516

1617

1718
## [3.1.1]

readium/navigator/src/main/assets/_scripts/src/decorator.js

Lines changed: 138 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ let styles = new Map();
1515
let groups = new Map();
1616
var lastGroupId = 0;
1717

18+
/**
19+
* Returns the document body's writing mode.
20+
*/
21+
function getDocumentWritingMode() {
22+
return getComputedStyle(document.body).writingMode;
23+
}
24+
25+
/**
26+
* Returns the closest element ancestor of the given node.
27+
*/
28+
function getContainingElement(node) {
29+
return node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
30+
}
31+
1832
/**
1933
* Registers a list of additional supported Decoration Templates.
2034
*
@@ -172,46 +186,108 @@ export function DecorationGroup(groupId, groupName) {
172186
}
173187

174188
let itemContainer = document.createElement("div");
175-
itemContainer.setAttribute("id", item.id);
176-
itemContainer.setAttribute("data-style", item.decoration.style);
177-
itemContainer.style.setProperty("pointer-events", "none");
178-
179-
let viewportWidth = window.innerWidth;
180-
let columnCount = parseInt(
181-
getComputedStyle(document.documentElement).getPropertyValue(
182-
"column-count"
183-
)
184-
);
185-
let pageWidth = viewportWidth / (columnCount || 1);
186-
let scrollingElement = document.scrollingElement;
187-
let xOffset = scrollingElement.scrollLeft;
188-
let yOffset = scrollingElement.scrollTop;
189-
190-
function positionElement(element, rect, boundingRect) {
189+
itemContainer.id = item.id;
190+
itemContainer.dataset.style = item.decoration.style;
191+
itemContainer.style.pointerEvents = "none";
192+
193+
const documentWritingMode = getDocumentWritingMode();
194+
const isVertical =
195+
documentWritingMode === "vertical-rl" ||
196+
documentWritingMode === "vertical-lr";
197+
198+
const scrollingElement = document.scrollingElement;
199+
const { scrollLeft: xOffset, scrollTop: yOffset } = scrollingElement;
200+
const viewportWidth = isVertical ? window.innerHeight : window.innerWidth;
201+
const viewportHeight = isVertical ? window.innerWidth : window.innerHeight;
202+
203+
const columnCount =
204+
parseInt(
205+
getComputedStyle(document.documentElement).getPropertyValue(
206+
"column-count"
207+
)
208+
) || 1;
209+
const pageSize =
210+
(isVertical ? viewportHeight : viewportWidth) / columnCount;
211+
212+
function positionElement(element, rect, boundingRect, writingMode) {
191213
element.style.position = "absolute";
192-
193-
if (style.width === "wrap") {
194-
element.style.width = `${rect.width}px`;
195-
element.style.height = `${rect.height}px`;
196-
element.style.left = `${rect.left + xOffset}px`;
197-
element.style.top = `${rect.top + yOffset}px`;
198-
} else if (style.width === "viewport") {
199-
element.style.width = `${viewportWidth}px`;
200-
element.style.height = `${rect.height}px`;
201-
let left = Math.floor(rect.left / viewportWidth) * viewportWidth;
202-
element.style.left = `${left + xOffset}px`;
203-
element.style.top = `${rect.top + yOffset}px`;
204-
} else if (style.width === "bounds") {
205-
element.style.width = `${boundingRect.width}px`;
206-
element.style.height = `${rect.height}px`;
207-
element.style.left = `${boundingRect.left + xOffset}px`;
208-
element.style.top = `${rect.top + yOffset}px`;
209-
} else if (style.width === "page") {
210-
element.style.width = `${pageWidth}px`;
211-
element.style.height = `${rect.height}px`;
212-
let left = Math.floor(rect.left / pageWidth) * pageWidth;
213-
element.style.left = `${left + xOffset}px`;
214-
element.style.top = `${rect.top + yOffset}px`;
214+
const isVerticalRL = writingMode === "vertical-rl";
215+
const isVerticalLR = writingMode === "vertical-lr";
216+
217+
if (isVerticalRL || isVerticalLR) {
218+
if (style.width === "wrap") {
219+
element.style.width = `${rect.width}px`;
220+
element.style.height = `${rect.height}px`;
221+
if (isVerticalRL) {
222+
element.style.right = `${
223+
-rect.right - xOffset + scrollingElement.clientWidth
224+
}px`;
225+
} else {
226+
// vertical-lr
227+
element.style.left = `${rect.left + xOffset}px`;
228+
}
229+
element.style.top = `${rect.top + yOffset}px`;
230+
} else if (style.width === "viewport") {
231+
element.style.width = `${rect.height}px`;
232+
element.style.height = `${viewportWidth}px`;
233+
const top = Math.floor(rect.top / viewportWidth) * viewportWidth;
234+
if (isVerticalRL) {
235+
element.style.right = `${-rect.right - xOffset}px`;
236+
} else {
237+
// vertical-lr
238+
element.style.left = `${rect.left + xOffset}px`;
239+
}
240+
element.style.top = `${top + yOffset}px`;
241+
} else if (style.width === "bounds") {
242+
element.style.width = `${boundingRect.height}px`;
243+
element.style.height = `${viewportWidth}px`;
244+
if (isVerticalRL) {
245+
element.style.right = `${
246+
-boundingRect.right - xOffset + scrollingElement.clientWidth
247+
}px`;
248+
} else {
249+
// vertical-lr
250+
element.style.left = `${boundingRect.left + xOffset}px`;
251+
}
252+
element.style.top = `${boundingRect.top + yOffset}px`;
253+
} else if (style.width === "page") {
254+
element.style.width = `${rect.height}px`;
255+
element.style.height = `${pageSize}px`;
256+
const top = Math.floor(rect.top / pageSize) * pageSize;
257+
if (isVerticalRL) {
258+
element.style.right = `${
259+
-rect.right - xOffset + scrollingElement.clientWidth
260+
}px`;
261+
} else {
262+
// vertical-lr
263+
element.style.left = `${rect.left + xOffset}px`;
264+
}
265+
element.style.top = `${top + yOffset}px`;
266+
}
267+
} else {
268+
if (style.width === "wrap") {
269+
element.style.width = `${rect.width}px`;
270+
element.style.height = `${rect.height}px`;
271+
element.style.left = `${rect.left + xOffset}px`;
272+
element.style.top = `${rect.top + yOffset}px`;
273+
} else if (style.width === "viewport") {
274+
element.style.width = `${viewportWidth}px`;
275+
element.style.height = `${rect.height}px`;
276+
const left = Math.floor(rect.left / viewportWidth) * viewportWidth;
277+
element.style.left = `${left + xOffset}px`;
278+
element.style.top = `${rect.top + yOffset}px`;
279+
} else if (style.width === "bounds") {
280+
element.style.width = `${boundingRect.width}px`;
281+
element.style.height = `${rect.height}px`;
282+
element.style.left = `${boundingRect.left + xOffset}px`;
283+
element.style.top = `${rect.top + yOffset}px`;
284+
} else if (style.width === "page") {
285+
element.style.width = `${pageSize}px`;
286+
element.style.height = `${rect.height}px`;
287+
const left = Math.floor(rect.left / pageSize) * pageSize;
288+
element.style.left = `${left + xOffset}px`;
289+
element.style.top = `${rect.top + yOffset}px`;
290+
}
215291
}
216292
}
217293

@@ -230,32 +306,38 @@ export function DecorationGroup(groupId, groupName) {
230306
}
231307

232308
if (style.layout === "boxes") {
233-
let doNotMergeHorizontallyAlignedRects = true;
234-
let clientRects = getClientRectsNoOverlap(
309+
const doNotMergeHorizontallyAlignedRects =
310+
!documentWritingMode.startsWith("vertical");
311+
const startElement = getContainingElement(item.range.startContainer);
312+
// Decorated text may have a different writingMode from document body
313+
const decoratorWritingMode = getComputedStyle(startElement).writingMode;
314+
315+
const clientRects = getClientRectsNoOverlap(
235316
item.range,
236317
doNotMergeHorizontallyAlignedRects
237-
);
238-
239-
clientRects = clientRects.sort((r1, r2) => {
240-
if (r1.top < r2.top) {
241-
return -1;
242-
} else if (r1.top > r2.top) {
243-
return 1;
318+
).sort((r1, r2) => {
319+
if (r1.top !== r2.top) return r1.top - r2.top;
320+
if (decoratorWritingMode === "vertical-rl") {
321+
return r2.left - r1.left;
322+
} else if (decoratorWritingMode === "vertical-lr") {
323+
return r1.left - r2.left;
244324
} else {
245-
return 0;
325+
return r1.left - r2.left;
246326
}
247327
});
248328

249329
for (let clientRect of clientRects) {
250330
const line = elementTemplate.cloneNode(true);
251-
line.style.setProperty("pointer-events", "none");
252-
positionElement(line, clientRect, boundingRect);
331+
line.style.pointerEvents = "none";
332+
line.dataset.writingMode = decoratorWritingMode;
333+
positionElement(line, clientRect, boundingRect, documentWritingMode);
253334
itemContainer.append(line);
254335
}
255336
} else if (style.layout === "bounds") {
256337
const bounds = elementTemplate.cloneNode(true);
257-
bounds.style.setProperty("pointer-events", "none");
258-
positionElement(bounds, boundingRect, boundingRect);
338+
bounds.style.pointerEvents = "none";
339+
bounds.dataset.writingMode = documentWritingMode;
340+
positionElement(bounds, boundingRect, boundingRect, documentWritingMode);
259341

260342
itemContainer.append(bounds);
261343
}
@@ -276,9 +358,9 @@ export function DecorationGroup(groupId, groupName) {
276358
function requireContainer() {
277359
if (!container) {
278360
container = document.createElement("div");
279-
container.setAttribute("id", groupId);
280-
container.setAttribute("data-group", groupName);
281-
container.style.setProperty("pointer-events", "none");
361+
container.id = groupId;
362+
container.dataset.group = groupName;
363+
container.style.pointerEvents = "none";
282364
document.body.append(container);
283365
}
284366
return container;

readium/navigator/src/main/assets/readium/scripts/readium-fixed.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readium/navigator/src/main/assets/readium/scripts/readium-reflowable.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -131,32 +131,48 @@ public data class HtmlDecorationTemplate(
131131
): HtmlDecorationTemplate {
132132
val className = createUniqueClassName(if (asHighlight) "highlight" else "underline")
133133
val padding = Padding(left = 1, right = 1)
134+
134135
return HtmlDecorationTemplate(
135136
layout = Layout.BOXES,
136137
element = { decoration ->
137138
val tint = (decoration.style as? Style.Tinted)?.tint ?: defaultTint
138139
val isActive = (decoration.style as? Style.Activable)?.isActive ?: false
139-
var css = ""
140-
if (asHighlight || isActive) {
141-
css += "background-color: ${tint.toCss(alpha = alpha)} !important;"
142-
}
143-
if (!asHighlight || isActive) {
144-
css += "border-bottom: ${lineWeight}px solid ${tint.toCss()};"
140+
val css = buildString {
141+
if (asHighlight || isActive) {
142+
append("background-color: ${tint.toCss(alpha = alpha)} !important;")
143+
}
144+
if (!asHighlight || isActive) {
145+
append("--underline-color: ${tint.toCss()};")
146+
}
145147
}
146-
"""
147-
<div class="$className" style="$css"/>"
148-
"""
148+
"""<div class="$className" style="$css"/>"""
149149
},
150150
stylesheet = """
151-
.$className {
152-
margin-left: ${-padding.left}px;
153-
padding-right: ${padding.left + padding.right}px;
154-
margin-top: ${-padding.top}px;
155-
padding-bottom: ${padding.top + padding.bottom}px;
156-
border-radius: ${cornerRadius}px;
157-
box-sizing: border-box;
158-
}
159-
"""
151+
.$className {
152+
margin: ${-padding.top}px ${-padding.left}px 0 0;
153+
padding: 0 ${padding.left + padding.right}px ${padding.top + padding.bottom}px 0;
154+
border-radius: ${cornerRadius}px;
155+
box-sizing: border-box;
156+
border: 0 solid var(--underline-color);
157+
}
158+
159+
/* Horizontal (default) */
160+
[data-writing-mode="horizontal-tb"].$className {
161+
border-bottom-width: ${lineWeight}px;
162+
}
163+
164+
/* Vertical right-to-left */
165+
[data-writing-mode="vertical-rl"].$className,
166+
[data-writing-mode="sideways-rl"].$className {
167+
border-left-width: ${lineWeight}px;
168+
}
169+
170+
/* Vertical left-to-right */
171+
[data-writing-mode="vertical-lr"].$className,
172+
[data-writing-mode="sideways-lr"].$className {
173+
border-right-width: ${lineWeight}px;
174+
}
175+
"""
160176
)
161177
}
162178

0 commit comments

Comments
 (0)