Add ink integration UI and media playback
This commit is contained in:
@@ -9,7 +9,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
super('ui-display-handler', 'UI Display Handler');
|
||||
|
||||
// Module dependencies
|
||||
this.dependencies = ['layout-renderer', 'playback-coordinator'];
|
||||
this.dependencies = ['layout-renderer', 'playback-coordinator', 'game-config', 'localization'];
|
||||
|
||||
// DOM elements
|
||||
this.container = null;
|
||||
@@ -31,10 +31,19 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
// Bind methods using parent's bindMethods utility
|
||||
this.bindMethods([
|
||||
'initializeContainers',
|
||||
'applyGameConfig',
|
||||
'applyTranslations',
|
||||
'displayText',
|
||||
'renderSentence',
|
||||
'handleDeferredMediaBlock',
|
||||
'renderImageBlock',
|
||||
'calculateImageMetrics',
|
||||
'readFirstFiniteNumber',
|
||||
'waitForSkippablePause',
|
||||
'scrollStoryToEnd',
|
||||
'animatePageScroll',
|
||||
'scrollToTurn',
|
||||
'handleStoryScroll',
|
||||
'rerenderStory',
|
||||
'clear',
|
||||
'scheduleRerender',
|
||||
@@ -46,6 +55,10 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
|
||||
console.log('UIDisplayHandler: Constructor initialized');
|
||||
}
|
||||
|
||||
t(key, params = {}) {
|
||||
return this.localization?.translate?.(key, params) || key;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
@@ -61,6 +74,8 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
// Get references to required modules using parent's getModule method
|
||||
this.layoutRenderer = this.getModule('layout-renderer');
|
||||
this.playbackCoordinator = this.getModule('playback-coordinator');
|
||||
this.gameConfig = this.getModule('game-config');
|
||||
this.localization = this.getModule('localization');
|
||||
|
||||
this.reportProgress(50, "Initializing display containers");
|
||||
|
||||
@@ -73,6 +88,24 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
this.addEventListener(document, 'book:resized', () => {
|
||||
this.scheduleRerender();
|
||||
});
|
||||
this.addEventListener(document, 'game:config', (event) => {
|
||||
this.applyGameConfig(event.detail);
|
||||
});
|
||||
this.addEventListener(document, 'localization:languageChanged', () => {
|
||||
this.applyTranslations();
|
||||
});
|
||||
this.addEventListener(document, 'story:scroll-to-turn', (event) => {
|
||||
this.scrollToTurn(event.detail?.turnId);
|
||||
});
|
||||
this.addEventListener(document, 'story:process-state', (event) => {
|
||||
const state = event.detail?.state || 'ready';
|
||||
const remark = document.getElementById('remark_text');
|
||||
if (remark) {
|
||||
remark.textContent = state === 'paused'
|
||||
? this.t('title.continueHint')
|
||||
: this.t('title.fastForwardHint');
|
||||
}
|
||||
});
|
||||
|
||||
if (window.ResizeObserver && this.paragraphContainer) {
|
||||
this.storyResizeObserver = new ResizeObserver((entries) => {
|
||||
@@ -196,9 +229,9 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
const header = document.createElement('div');
|
||||
header.className = 'header';
|
||||
header.innerHTML = `
|
||||
<h2 class="byline l10n-by">powered by Generative AI</h2>
|
||||
<h1 class="title">AI Interactive Fiction</h1>
|
||||
<h3 class="subtitle">An open-world text adventure</h3>
|
||||
<h2 class="byline" id="game_author"></h2>
|
||||
<h1 class="title" id="game_title"></h1>
|
||||
<h3 class="subtitle" id="game_subtitle"></h3>
|
||||
<div class="separator"><double>❦</double></div>
|
||||
`;
|
||||
this.pageLeft.appendChild(header);
|
||||
@@ -208,12 +241,13 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
controls.id = 'controls';
|
||||
controls.className = 'buttons';
|
||||
controls.innerHTML = `
|
||||
<a class="l10n-speech" id="speech" title="Toggle text to speech">speech</a>
|
||||
<span><a id="speed_reset"><span class="l10n-speed">speed<sup>*</sup></span></a><input type="range" min="50" max="150" value="100" id="speed" name="speed" /></span>
|
||||
<a class="l10n-restart" id="rewind" title="Start a new game">new game</a>
|
||||
<a class="l10n-save" id="save" title="Save progress">save</a>
|
||||
<a class="l10n-load" id="reload" title="Reload from save point" disabled="disabled">load</a>
|
||||
<a class="l10n-options" id="options" title="Options">options</a>
|
||||
<a id="speech"></a>
|
||||
<a id="autoplay"></a>
|
||||
<span><a id="speed_reset"><span id="speed_label"></span><sup>*</sup></a><input type="range" min="50" max="150" value="100" id="speed" name="speed" /></span>
|
||||
<a id="rewind"></a>
|
||||
<a id="save"></a>
|
||||
<a id="reload" disabled="disabled"></a>
|
||||
<a id="options"></a>
|
||||
`;
|
||||
this.pageLeft.appendChild(controls);
|
||||
|
||||
@@ -232,7 +266,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
commandInput.id = 'command_input';
|
||||
commandInput.innerHTML = `
|
||||
<div class="input-wrapper">
|
||||
<textarea id="player_input" placeholder="Enter your command..." rows="1" autofocus></textarea>
|
||||
<textarea id="player_input" rows="1" autofocus autocomplete="off" autocorrect="off" autocapitalize="sentences" spellcheck="true" aria-autocomplete="none" data-form-type="other" data-1p-ignore="true" data-lpignore="true" data-bwignore="true"></textarea>
|
||||
<span id="cursor"></span>
|
||||
</div>
|
||||
`;
|
||||
@@ -243,8 +277,10 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
// Create remark
|
||||
const remark = document.createElement('div');
|
||||
remark.id = 'remark';
|
||||
remark.className = 'l10n-remark';
|
||||
remark.innerHTML = '<i><sup>*</sup>click on page or press spacebar to fast forward text animation</i>';
|
||||
remark.innerHTML = `
|
||||
<div id="remark_hint"><i><sup>*</sup><span id="remark_text"></span></i></div>
|
||||
<div id="game_legal"></div>
|
||||
`;
|
||||
this.pageLeft.appendChild(remark);
|
||||
|
||||
bookContainer.appendChild(this.pageLeft);
|
||||
@@ -272,7 +308,6 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
if (!document.getElementById('start_prompt')) {
|
||||
const startPrompt = document.createElement('div');
|
||||
startPrompt.id = 'start_prompt';
|
||||
startPrompt.textContent = 'Klick on new game or load to start the game';
|
||||
this.pageRight.appendChild(startPrompt);
|
||||
}
|
||||
|
||||
@@ -302,6 +337,66 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
}
|
||||
|
||||
console.log('UIDisplayHandler: All containers initialized');
|
||||
this.applyGameConfig(this.gameConfig?.getConfig?.());
|
||||
this.applyTranslations();
|
||||
if (this.pageRight && !this.pageRight.dataset.turnScrollBound) {
|
||||
this.pageRight.dataset.turnScrollBound = 'true';
|
||||
this.pageRight.addEventListener('scroll', this.handleStoryScroll, { passive: true });
|
||||
}
|
||||
}
|
||||
|
||||
applyGameConfig(config) {
|
||||
const metadata = config?.metadata || this.gameConfig?.getMetadata?.() || {};
|
||||
const titleElement = document.getElementById('game_title');
|
||||
const authorElement = document.getElementById('game_author');
|
||||
const subtitleElement = document.getElementById('game_subtitle');
|
||||
const legalElement = document.getElementById('game_legal');
|
||||
document.getElementById('game_version')?.remove();
|
||||
document.getElementById('game_copyright')?.remove();
|
||||
|
||||
if (titleElement) titleElement.textContent = metadata.title || '';
|
||||
if (authorElement) authorElement.textContent = metadata.author ? this.t('title.byAuthor', { author: metadata.author }) : '';
|
||||
if (subtitleElement) subtitleElement.textContent = metadata.subtitle || '';
|
||||
if (legalElement) {
|
||||
const items = [
|
||||
metadata.version ? this.t('title.version', { version: metadata.version }) : '',
|
||||
metadata.copyright || ''
|
||||
].filter(Boolean);
|
||||
legalElement.textContent = items.join(' · ');
|
||||
}
|
||||
}
|
||||
|
||||
applyTranslations() {
|
||||
this.localization = this.getModule('localization') || this.localization;
|
||||
|
||||
const setText = (id, key) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) element.textContent = this.t(key);
|
||||
};
|
||||
const setTitle = (id, key) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) element.setAttribute('title', this.t(key));
|
||||
};
|
||||
|
||||
setText('speech', 'topbar.speech');
|
||||
setText('autoplay', 'topbar.autoplay');
|
||||
setText('speed_label', 'topbar.speed');
|
||||
setText('rewind', 'topbar.newGame');
|
||||
setText('save', 'topbar.save');
|
||||
setText('reload', 'topbar.load');
|
||||
setText('options', 'topbar.options');
|
||||
setText('remark_text', 'title.fastForwardHint');
|
||||
setText('start_prompt', 'title.startPrompt');
|
||||
setTitle('speech', 'topbar.speechTitle');
|
||||
setTitle('autoplay', 'topbar.autoplayTitle');
|
||||
setTitle('rewind', 'topbar.newGameTitle');
|
||||
setTitle('save', 'topbar.saveTitle');
|
||||
setTitle('reload', 'topbar.loadTitle');
|
||||
setTitle('options', 'topbar.optionsTitle');
|
||||
|
||||
const input = document.getElementById('player_input');
|
||||
if (input) input.setAttribute('placeholder', this.t('input.placeholder'));
|
||||
this.applyGameConfig(this.gameConfig?.getConfig?.());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,17 +422,13 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
|
||||
|
||||
/**
|
||||
* Display text in the UI (backward compatibility)
|
||||
* Note: Text should flow through SentenceQueue instead
|
||||
* @param {string} text - Text to display
|
||||
* @param {Object} options - Display options
|
||||
* @returns {Promise<HTMLElement>} - Promise resolving to the displayed paragraph element
|
||||
* Display a local UI message outside the server turn protocol.
|
||||
* Story output must flow through structured TurnResult objects instead.
|
||||
*/
|
||||
displayText(text, options = {}) {
|
||||
if (!text) return Promise.resolve(null);
|
||||
|
||||
// For backward compatibility, delegate to sentence queue
|
||||
console.warn('UIDisplayHandler.displayText called directly, text should flow through SentenceQueue');
|
||||
console.warn('UIDisplayHandler.displayText called directly; story text should come from TurnResult');
|
||||
|
||||
const sentenceQueue = this.getModule('sentence-queue');
|
||||
if (sentenceQueue) {
|
||||
@@ -369,6 +460,10 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
sentence.layout,
|
||||
{ id: sentence.id }
|
||||
);
|
||||
if (sentence.turnId != null) {
|
||||
paragraphElement.dataset.turnId = String(sentence.turnId);
|
||||
paragraphElement.classList.add('story-turn-block');
|
||||
}
|
||||
|
||||
// Append to container
|
||||
if (this.paragraphContainer) {
|
||||
@@ -386,6 +481,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
this.renderedItems.push({
|
||||
type: sentence.kind === 'heading' ? 'heading' : 'paragraph',
|
||||
id: sentence.id,
|
||||
turnId: sentence.turnId ?? null,
|
||||
text: sentence.text,
|
||||
metadata: {
|
||||
layoutText: sentence.layout?.sourceLayoutText || sentence.text,
|
||||
@@ -427,9 +523,25 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
this.paragraphContainer.innerHTML = '';
|
||||
|
||||
for (const item of this.renderedItems) {
|
||||
if (item.type === 'image') {
|
||||
const sentenceQueue = this.getModule('sentence-queue');
|
||||
const imageLayout = sentenceQueue && typeof sentenceQueue.prepareImageLayout === 'function'
|
||||
? await sentenceQueue.prepareImageLayout(item.metadata || {})
|
||||
: null;
|
||||
this.renderImageBlock({
|
||||
...(item.metadata || {}),
|
||||
imageLayout: imageLayout || item.metadata?.imageLayout
|
||||
}, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.type === 'heading') {
|
||||
const layout = await sentenceQueue.prepareLayout(item.text, item.metadata || {});
|
||||
const heading = this.layoutRenderer.renderParagraph(layout, { id: item.id });
|
||||
if (item.turnId != null) {
|
||||
heading.dataset.turnId = String(item.turnId);
|
||||
heading.classList.add('story-turn-block');
|
||||
}
|
||||
heading.querySelectorAll('.word').forEach(word => {
|
||||
word.style.transition = 'none';
|
||||
word.style.animation = 'none';
|
||||
@@ -446,6 +558,10 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
|
||||
const layout = await sentenceQueue.prepareLayout(item.text, item.metadata || {});
|
||||
const paragraph = this.layoutRenderer.renderParagraph(layout, { id: item.id });
|
||||
if (item.turnId != null) {
|
||||
paragraph.dataset.turnId = String(item.turnId);
|
||||
paragraph.classList.add('story-turn-block');
|
||||
}
|
||||
paragraph.querySelectorAll('.word').forEach(word => {
|
||||
word.style.transition = 'none';
|
||||
word.style.animation = 'none';
|
||||
@@ -468,13 +584,74 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
this.pageRight.scrollTo({
|
||||
top: Math.max(0, this.pageRight.scrollHeight - this.pageRight.clientHeight),
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
});
|
||||
this.animatePageScroll(
|
||||
Math.max(0, this.pageRight.scrollHeight - this.pageRight.clientHeight),
|
||||
smooth ? 720 : 0
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
animatePageScroll(targetTop, duration = 720) {
|
||||
if (!this.pageRight) return;
|
||||
if (!duration) {
|
||||
this.pageRight.scrollTop = targetTop;
|
||||
return;
|
||||
}
|
||||
|
||||
const startTop = this.pageRight.scrollTop;
|
||||
const delta = targetTop - startTop;
|
||||
if (Math.abs(delta) < 1) return;
|
||||
|
||||
const startedAt = performance.now();
|
||||
const ease = (t) => 1 - Math.pow(1 - t, 3);
|
||||
const step = (now) => {
|
||||
const progress = Math.min(1, (now - startedAt) / duration);
|
||||
this.pageRight.scrollTop = startTop + (delta * ease(progress));
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
scrollToTurn(turnId) {
|
||||
if (!this.pageRight || turnId == null) return;
|
||||
const escapedTurnId = CSS.escape(String(turnId));
|
||||
const target = this.paragraphContainer?.querySelector(`[data-turn-id="${escapedTurnId}"]`);
|
||||
if (!target) return;
|
||||
this.pageRight.scrollTo({
|
||||
top: Math.max(0, target.offsetTop - 12),
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
handleStoryScroll() {
|
||||
if (!this.pageRight || !this.paragraphContainer) return;
|
||||
|
||||
const blocks = Array.from(this.paragraphContainer.querySelectorAll('[data-turn-id]'));
|
||||
if (blocks.length === 0) return;
|
||||
|
||||
const viewportMiddle = this.pageRight.scrollTop + (this.pageRight.clientHeight / 2);
|
||||
let best = null;
|
||||
let bestDistance = Infinity;
|
||||
|
||||
blocks.forEach((block) => {
|
||||
const blockMiddle = block.offsetTop + (block.offsetHeight / 2);
|
||||
const distance = Math.abs(blockMiddle - viewportMiddle);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = block;
|
||||
}
|
||||
});
|
||||
|
||||
if (best?.dataset?.turnId && this.activeTurnId !== best.dataset.turnId) {
|
||||
this.activeTurnId = best.dataset.turnId;
|
||||
document.dispatchEvent(new CustomEvent('story:visible-turn', {
|
||||
detail: { turnId: Number(best.dataset.turnId) }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async handleDeferredMediaBlock(sentence) {
|
||||
document.dispatchEvent(new CustomEvent('story:media-block', {
|
||||
detail: {
|
||||
@@ -484,12 +661,27 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
}
|
||||
}));
|
||||
|
||||
if (sentence.kind === 'music') {
|
||||
const leadInSeconds = Number(sentence.metadata?.leadInSeconds || sentence.metadata?.leadIn || 0);
|
||||
if (leadInSeconds > 0) {
|
||||
console.log(`UIDisplayHandler: Waiting ${leadInSeconds}s before continuing after music block`);
|
||||
await new Promise(resolve => setTimeout(resolve, leadInSeconds * 1000));
|
||||
if (sentence.kind === 'image') {
|
||||
const element = this.renderImageBlock(sentence.metadata || {}, true);
|
||||
this.renderedItems.push({
|
||||
type: 'image',
|
||||
id: sentence.id,
|
||||
turnId: sentence.turnId ?? null,
|
||||
text: '',
|
||||
metadata: sentence.metadata || {}
|
||||
});
|
||||
|
||||
this.scrollStoryToEnd(true);
|
||||
|
||||
if (sentence.onComplete) {
|
||||
sentence.onComplete();
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
if (sentence.kind === 'music') {
|
||||
console.log('UIDisplayHandler: Music block started', sentence.metadata || {});
|
||||
}
|
||||
|
||||
if (sentence.onComplete) {
|
||||
@@ -499,7 +691,145 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
return null;
|
||||
}
|
||||
|
||||
readFirstFiniteNumber(...values) {
|
||||
for (const value of values) {
|
||||
const number = Number(value);
|
||||
if (Number.isFinite(number)) {
|
||||
return Math.max(0, number);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
waitForSkippablePause(seconds, kind = 'media') {
|
||||
const duration = Math.max(0, Number(seconds) || 0) * 1000;
|
||||
if (duration <= 0) return Promise.resolve(false);
|
||||
|
||||
document.documentElement.dataset.skippablePause = 'true';
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
detail: { state: 'playing-ready', reason: `${kind}-pause-start`, duration }
|
||||
}));
|
||||
|
||||
return new Promise(resolve => {
|
||||
let finished = false;
|
||||
let timeoutId = null;
|
||||
|
||||
const finish = (skipped) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
clearTimeout(timeoutId);
|
||||
document.removeEventListener('ui:command', onCommand);
|
||||
delete document.documentElement.dataset.skippablePause;
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
detail: { state: 'playing-ready', reason: `${kind}-pause-${skipped ? 'skipped' : 'complete'}` }
|
||||
}));
|
||||
resolve(skipped);
|
||||
};
|
||||
|
||||
const onCommand = (event) => {
|
||||
if (event.detail?.type === 'continue') {
|
||||
finish(true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('ui:command', onCommand);
|
||||
timeoutId = setTimeout(() => finish(false), duration);
|
||||
});
|
||||
}
|
||||
|
||||
renderImageBlock(metadata = {}, animate = true) {
|
||||
if (!this.paragraphContainer) return null;
|
||||
|
||||
const metrics = metadata.imageLayout || this.calculateImageMetrics(metadata.size);
|
||||
const figure = document.createElement('figure');
|
||||
figure.className = [
|
||||
'story-image-block',
|
||||
`story-image-${metrics.size || 'landscape'}`,
|
||||
metrics.floatSide === 'right' ? 'story-image-float-right' : '',
|
||||
metrics.floatSide === 'left' ? 'story-image-float-left' : '',
|
||||
animate ? 'story-image-pending' : 'story-image-visible'
|
||||
].filter(Boolean).join(' ');
|
||||
figure.style.width = `${metrics.width}px`;
|
||||
figure.style.height = `${metrics.height}px`;
|
||||
figure.style.marginTop = `${metrics.verticalMargin || 0}px`;
|
||||
figure.style.marginBottom = `${metrics.verticalMargin || 0}px`;
|
||||
figure.dataset.animationMs = '900';
|
||||
if (metadata.turnId != null) {
|
||||
figure.dataset.turnId = String(metadata.turnId);
|
||||
figure.classList.add('story-turn-block');
|
||||
}
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = metadata.url || metadata.filename || '';
|
||||
img.alt = metadata.alt || '';
|
||||
img.decoding = 'async';
|
||||
img.loading = 'eager';
|
||||
figure.appendChild(img);
|
||||
|
||||
this.paragraphContainer.appendChild(figure);
|
||||
|
||||
if (animate) {
|
||||
window.requestAnimationFrame(() => {
|
||||
figure.classList.remove('story-image-pending');
|
||||
figure.classList.add('story-image-visible');
|
||||
});
|
||||
} else {
|
||||
figure.classList.remove('story-image-pending');
|
||||
figure.classList.add('story-image-visible');
|
||||
}
|
||||
|
||||
return figure;
|
||||
}
|
||||
|
||||
calculateImageMetrics(size = 'landscape') {
|
||||
const storyElement = document.getElementById('story');
|
||||
const pageWidth = storyElement?.clientWidth || 600;
|
||||
const probe = document.createElement('p');
|
||||
probe.style.visibility = 'hidden';
|
||||
probe.style.position = 'absolute';
|
||||
probe.style.left = '-8000px';
|
||||
probe.style.top = '-8000px';
|
||||
(storyElement || document.body).appendChild(probe);
|
||||
const lineHeight = parseFloat(window.getComputedStyle(probe).lineHeight) || 24;
|
||||
probe.remove();
|
||||
|
||||
const normalizedSize = String(size || 'landscape').toLowerCase() === 'widescreen'
|
||||
? 'landscape'
|
||||
: String(size || 'landscape').toLowerCase();
|
||||
const aspect = normalizedSize === 'portrait' ? (9 / 16) : normalizedSize === 'square' ? 1 : (16 / 9);
|
||||
const imageGap = lineHeight * 0.9;
|
||||
const maxWidth = normalizedSize === 'portrait' ? pageWidth * 0.5 : pageWidth;
|
||||
const naturalHeight = maxWidth / aspect;
|
||||
const imageLineCount = Math.max(1, Math.floor(naturalHeight / lineHeight));
|
||||
const height = imageLineCount * lineHeight;
|
||||
const width = Math.min(maxWidth, height * aspect);
|
||||
const verticalMargin = lineHeight / 2;
|
||||
const lineCount = imageLineCount + 1;
|
||||
|
||||
return {
|
||||
size: normalizedSize,
|
||||
aspect,
|
||||
width,
|
||||
height,
|
||||
gap: imageGap,
|
||||
lineCount,
|
||||
imageLineCount,
|
||||
lineHeight,
|
||||
verticalMargin,
|
||||
floatSide: 'left',
|
||||
pageWidth
|
||||
};
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (document.documentElement.dataset.skippablePause === 'true') {
|
||||
document.dispatchEvent(new CustomEvent('ui:command', {
|
||||
detail: { moduleId: this.id, type: 'continue', source: 'display-clear' }
|
||||
}));
|
||||
delete document.documentElement.dataset.skippablePause;
|
||||
}
|
||||
|
||||
if (this.container) {
|
||||
this.container.innerHTML = '';
|
||||
this.paragraphContainer = document.createElement('div');
|
||||
|
||||
Reference in New Issue
Block a user