Skip to content

Commit 0a7c0f4

Browse files
committed
Enhance sample source handling in editor and build page; update UI text for clarity
1 parent 86701b7 commit 0a7c0f4

3 files changed

Lines changed: 71 additions & 29 deletions

File tree

‎src/h5p/build/build-page.ts‎

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import { formatSource, jsonProblem, place, type Target } from './json-scan'
2222
* markup is `index.njk` beside this file, and its styles are the site's own, in `site.css`.
2323
*
2424
* Both of its visitors are people. One a chat sent, with the source in the link: it is built at
25-
* once. One came with no source and does not know the page: that one is told what it makes, and
26-
* shown a sample that plays (`SAMPLE`). The markup says which words are for whom, and the class
25+
* once. One came with no source and does not know the page: that one is told what it makes, shown
26+
* a sample that plays (`SAMPLE`), and finds the sample's source in the box, which the first paste
27+
* replaces whole (`prefillSample`). The markup says which words are for whom, and the class
2728
* `linked` on `<html>` shows them, set before the page paints by the script in `index.njk` and
2829
* kept in step with the address here.
2930
*/
@@ -106,6 +107,10 @@ let samplePlays = false
106107
/** Whether the sample has been asked to measure itself again, and the wait for it to be still. */
107108
let sampleAsked = false
108109
let sampleStill: ReturnType<typeof setTimeout> | undefined
110+
/** The sample's source as the page put it in the box, laid out; `null` before it has. */
111+
let sampleText: string | null = null
112+
/** Whether the box holds the sample's source as the page put it there, which a paste then replaces whole. */
113+
const sampleUntouched = (): boolean => sampleText !== null && sourceField.value === sampleText
109114

110115
function setStatus(text: string, state: 'idle' | 'building' | 'ready' | 'error'): void {
111116
status.textContent = text
@@ -381,13 +386,17 @@ function setSource(text: string): void {
381386
followLater()
382387
}
383388

384-
// Where the editor has not loaded, a source pasted whole is laid out here, as the editor does it.
389+
// Where the editor has not loaded, a source pasted whole is laid out here, as the editor does it,
390+
// and a paste over the sample as the page put it replaces it whole.
385391
sourceField.addEventListener('paste', (event) => {
386392
const text = event.clipboardData?.getData('text/plain')
387-
const laidOut = text && sourceField.selectionStart === 0 && sourceField.selectionEnd === sourceField.value.length ? formatSource(text) : null
388-
if (laidOut === null) return
393+
if (!text) return
394+
const replaced = sampleUntouched()
395+
if (!replaced && (sourceField.selectionStart !== 0 || sourceField.selectionEnd !== sourceField.value.length)) return
396+
const insert = formatSource(text) ?? (replaced ? text : null)
397+
if (insert === null) return
389398
event.preventDefault()
390-
sourceField.value = laidOut
399+
sourceField.value = insert
391400
sourceField.setSelectionRange(0, 0)
392401
sourceChanged()
393402
})
@@ -478,13 +487,38 @@ sample.addEventListener('error', (event) => {
478487
noteSample(`The sample does not play here: ${message}`)
479488
})
480489

481-
// The sample's source, for a visitor with none who wants to try the steps: it is theirs to build.
490+
async function fetchSampleSource(): Promise<string> {
491+
const response = await fetch(SAMPLE_SOURCE)
492+
if (!response.ok) throw new Error(String(response.status))
493+
return response.text()
494+
}
495+
496+
/** The sample's source in the box, as the page puts it there: a paste replaces it whole until it is changed. */
497+
function putSample(text: string): void {
498+
setSource(text)
499+
sampleText = sourceField.value
500+
}
501+
502+
/**
503+
* A visitor who came with no source finds the sample's in the box, to build as it is, to change, or
504+
* to paste theirs over. It does not go over a source that came meanwhile, by a link or by hand, and
505+
* where it cannot be fetched the box stays empty, with its placeholder.
506+
*/
507+
async function prefillSample(): Promise<void> {
508+
try {
509+
const text = await fetchSampleSource()
510+
if (linked || sourceField.value.trim()) return
511+
putSample(text)
512+
} catch {
513+
// The box stays empty.
514+
}
515+
}
516+
517+
// The sample's source again, for a visitor who changed or replaced it.
482518
sampleSource.addEventListener('click', async () => {
483519
sampleSourceResult.textContent = ''
484520
try {
485-
const response = await fetch(SAMPLE_SOURCE)
486-
if (!response.ok) throw new Error(String(response.status))
487-
setSource(await response.text())
521+
putSample(await fetchSampleSource())
488522
sampleSourceResult.textContent = 'It is in the box above: change it if you like, then Build.'
489523
} catch {
490524
sampleSourceResult.textContent = 'It could not be fetched; load the page again.'
@@ -516,11 +550,13 @@ async function fromLink(): Promise<void> {
516550

517551
updateFiles()
518552
void fromLink()
553+
// Once, as the page opens: `fromLink` has said by now whether the address carries a source.
554+
if (!linked) void prefillSample()
519555
window.addEventListener('hashchange', () => void fromLink())
520556

521557
// The editor is loaded apart: where it cannot be, the textarea is the source, as it was.
522558
import('./editor').then(({ mountEditor }) => {
523-
editor = mountEditor(sourceField, sourceChanged)
559+
editor = mountEditor(sourceField, sourceChanged, sampleUntouched)
524560
}).catch((error: unknown) => {
525561
console.warn('The source editor did not load; the source stays a plain text field.', error)
526562
})

‎src/h5p/build/editor.ts‎

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,25 @@ const typeOverSelection = EditorView.domEventHandlers({
9292
/**
9393
* A source pasted whole, into an empty editor or over all of its text, goes in laid out, and out of
9494
* the code fence a chat put it in, so that the page needs no button for it. A paste into the text,
95-
* or of text that is not JSON, goes in as it is.
95+
* or of text that is not JSON, goes in as it is. Where the text is one the page put there to be
96+
* pasted over (`over`), a paste anywhere in it replaces all of it, laid out where it is JSON.
9697
*/
97-
const pasteLaidOut = EditorView.domEventHandlers({
98-
paste(event, view) {
99-
const { from, to } = view.state.selection.main
100-
const text = event.clipboardData?.getData('text/plain')
101-
const laidOut = text && from === 0 && to === view.state.doc.length ? formatSource(text) : null
102-
if (laidOut === null) return false
103-
event.preventDefault()
104-
view.dispatch({ changes: { from, to, insert: laidOut }, selection: { anchor: 0 }, userEvent: 'input.paste', scrollIntoView: true })
105-
return true
106-
}
107-
})
98+
function pasteLaidOut(over: () => boolean) {
99+
return EditorView.domEventHandlers({
100+
paste(event, view) {
101+
const text = event.clipboardData?.getData('text/plain')
102+
if (!text) return false
103+
const { from, to } = view.state.selection.main
104+
const replaced = over()
105+
if (!replaced && (from !== 0 || to !== view.state.doc.length)) return false
106+
const insert = formatSource(text) ?? (replaced ? text : null)
107+
if (insert === null) return false
108+
event.preventDefault()
109+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert }, selection: { anchor: 0 }, userEvent: 'input.paste', scrollIntoView: true })
110+
return true
111+
}
112+
})
113+
}
108114

109115
/** The mistake in the text, as the editor marks it while the text is typed. */
110116
const mistakes = linter((view): Diagnostic[] => {
@@ -117,9 +123,10 @@ const mistakes = linter((view): Diagnostic[] => {
117123

118124
/**
119125
* Puts the editor where the textarea is, with the textarea's text, and calls `onChange` with the
120-
* text of every change, which it has already written into the textarea.
126+
* text of every change, which it has already written into the textarea. `pastedOver` says whether
127+
* the text is one the page put there to be replaced by the next paste, as the build page's sample.
121128
*/
122-
export function mountEditor(textarea: HTMLTextAreaElement, onChange: (text: string) => void): SourceEditor {
129+
export function mountEditor(textarea: HTMLTextAreaElement, onChange: (text: string) => void, pastedOver: () => boolean = () => false): SourceEditor {
123130
const host = document.createElement('div')
124131
host.className = 'source-editor'
125132
const shadow = host.attachShadow({ mode: 'open' })
@@ -155,7 +162,7 @@ export function mountEditor(textarea: HTMLTextAreaElement, onChange: (text: stri
155162
autocorrect: 'off'
156163
}),
157164
typeOverSelection,
158-
pasteLaidOut,
165+
pasteLaidOut(pastedOver),
159166
EditorView.updateListener.of((update) => {
160167
if (!update.docChanged) return
161168
const text = update.state.doc.toString()

‎src/h5p/build/index.njk‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
156156

157157
<section class="step" aria-labelledby="source-heading">
158158
<h3 id="source-heading"><span class="step-marker"><span class="step-number">1</span></span> Source</h3>
159-
<p class="when-unlinked">The source is your activity as the chat wrote it. Paste it here, or open it from a file.</p>
159+
<p class="when-unlinked">The source is your activity as the chat wrote it. The sample's is here to start with: paste yours over it, or open it from a file.</p>
160160
<p class="when-linked">The source is your activity as the chat wrote it. The link carried it here.</p>
161161
<div class="field">
162162
<label for="source" id="source-label">The source, as JSON</label>
@@ -175,8 +175,7 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
175175
<input type="file" id="source-file" accept=".json,application/json,text/plain">
176176
</p>
177177
<p class="when-unlinked">
178-
No source yet?
179-
<button type="button" id="sample-source" class="link-button">Use the sample's</button>
178+
<button type="button" id="sample-source" class="link-button">Put the sample's source back</button>
180179
<span class="hint" id="sample-source-result" role="status"></span>
181180
</p>
182181
</section>

0 commit comments

Comments
 (0)