Skip to content
Merged
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
163 changes: 143 additions & 20 deletions DocDiff.html

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/releases/v1.6.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# v1.6.3 — Faster start, a clearer first screen, reviewed changes that look done

- **Faster start.** The PDF and Word readers (about 1 MB of JavaScript) are still part of the single file, so everything works offline as before, but they now start only when a PDF or Word file is first opened. Opening the page no longer waits for them.
- **First screen of the start page.** On screens tall enough, the drop zones sit centred, "How it works" sits at the bottom edge, and an arrow below leads on to what Doc Diff can and cannot do. Lower windows keep the plain page flow.
- **Reviewed changes look done.** An accepted change reads as plain text with a green edge and a check mark; the removed words stay quietly struck through on the old side. A rejected change has a solid neutral edge and a cross. Nothing fades out any more, and no text moves.
- **Current change in blue.** The change you are on is marked in the selection blue instead of grey, in the document, the list and the overview bar.
- **List toggle.** On the desktop a plain sidebar icon at the right end of the top bar shows and hides the list of changes. Tablets keep the "List" button, which opens the list as a sheet.

## Offline version

Download `DocDiff.html` below and open it from disk. It works without an internet connection.
4 changes: 4 additions & 0 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ for (const page of [
assert.ok(!/<script>/.test(head), `${where}: executable scripts must not sit in <head>`);
assert.ok(firstScript > html.indexOf('id="faq-title"'), `${where}: scripts must follow the landing content`);
assert.ok(html.indexOf('id="pdfjs-worker-source"') > bodyStart, `${where}: PDF.js worker source must be present`);
// PDF.js and Mammoth are embedded but only started when a PDF or Word file is first read.
for (const id of ['lib-pdfjs', 'lib-mammoth']) {
assert.ok(new RegExp(`<script type="text/plain" id="${id}">`).test(html) && html.indexOf(`id="${id}"`) > bodyStart, `${where}: ${id} must be embedded as non-executed text`);
}

// Static landing text is rendered in the page language from i18n (what crawlers and no-JS visitors see)
let rendered = 0;
Expand Down
163 changes: 143 additions & 20 deletions site/en/index.html

Large diffs are not rendered by default.

163 changes: 143 additions & 20 deletions site/index.html

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/js/01-i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ const I18N = {
slotClearLeft: 'Alte Fassung entfernen',
slotClearRight: 'Neue Fassung entfernen',
howTitle: 'So funktioniert’s',
landingMore: 'Mehr erfahren',
how1Title: 'Zwei Fassungen wählen',
how1Text: 'Als PDF, Word-Datei oder eingefügten Text. Die Dateien werden nur in diesem Browser-Tab gelesen.',
how2Title: 'Änderungen durchgehen',
Expand Down Expand Up @@ -749,6 +750,7 @@ const I18N = {
slotClearLeft: 'Remove old version',
slotClearRight: 'Remove new version',
howTitle: 'How it works',
landingMore: 'Learn more',
how1Title: 'Choose two versions',
how1Text: 'As PDF, Word file or pasted text. The files are only read inside this browser tab.',
how2Title: 'Go through the changes',
Expand Down
14 changes: 14 additions & 0 deletions src/js/06-compare.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,20 @@ function bindEmptyStateButtons() {
document.querySelectorAll('.btn-load-sample').forEach(btn => {
btn.addEventListener('click', loadSampleComparison);
});

// Start page: from the first screen on to what Doc Diff can and cannot do.
const more = document.getElementById('landing-more');
if (more) {
more.addEventListener('click', () => {
const next = document.getElementById('landing-scope');
const pad = document.getElementById('empty-launchpad');
const heading = next.querySelector('h2');
heading.setAttribute('tabindex', '-1');
heading.focus({ preventScroll: true });
const top = next.getBoundingClientRect().top - pad.getBoundingClientRect().top + pad.scrollTop - 16;
pad.scrollTo({ top, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
});
}
}

// Moved blocks keep type 'deleted' (old position) / 'inserted' (new position); this note
Expand Down
19 changes: 18 additions & 1 deletion src/js/06a-compare-async.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,23 @@ function hideCompareStatus() {
compareStatus.el.classList.remove('is-visible', 'is-done');
}

/* ── Readers that start on first use ──────────────────────────────────────
* PDF.js and Mammoth (about 1 MB of JavaScript) ship as non-executed text and are started the
* first time a file of that type is read, so opening the page does not wait for them. An inserted
* inline script runs at once and is allowed by the page's script-src 'unsafe-inline'. */
function startEmbeddedLibrary(id, globalName) {
if (!window[globalName]) {
const srcEl = document.getElementById(id);
if (srcEl && srcEl.textContent.trim()) {
const script = document.createElement('script');
script.textContent = srcEl.textContent;
document.head.appendChild(script);
srcEl.textContent = '';
}
}
return window[globalName] || null;
}

/* ── PDF.js in a real worker ────────────────────────────────────────────── */

// The PDF.js worker source ships in a non-executed <script type="text/plain"> element. It is
Expand All @@ -305,7 +322,7 @@ let pdfJsWorkerSetup = null;
function ensurePdfJsWorker() {
if (pdfJsWorkerSetup) return pdfJsWorkerSetup;
pdfJsWorkerSetup = new Promise(resolve => {
const lib = window.pdfjsLib;
const lib = startEmbeddedLibrary('lib-pdfjs', 'pdfjsLib');
const srcEl = document.getElementById('pdfjs-worker-source');
if (!lib || !srcEl) {
resolve();
Expand Down
3 changes: 2 additions & 1 deletion src/js/09-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,8 @@ function prefixFirstText(element, prefix) {
}

async function extractDocxText(arrayBuffer) {
if (!window.mammoth || typeof window.mammoth.convertToHtml !== 'function') {
const mammoth = startEmbeddedLibrary('lib-mammoth', 'mammoth');
if (!mammoth || typeof mammoth.convertToHtml !== 'function') {
throw new ImportError('errUnsupportedType');
}

Expand Down
3 changes: 3 additions & 0 deletions src/js/10-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,8 @@ function applySidebarState() {
const docked = currentTier === 'desktop';
document.body.classList.toggle('sidebar-collapsed', docked && !state.sidebarOpen);
setMenuChecked('btn-toggle-sidebar', state.sidebarOpen);
// On the desktop the sidebar button at the right end of the top bar shows and hides the list.
document.getElementById('btn-rail').setAttribute('aria-expanded', String(docked && state.sidebarOpen));
scheduleRulerDraw();
}

Expand Down Expand Up @@ -428,6 +430,7 @@ document.getElementById('mob-step').addEventListener('click', e => {
openListSheet();
});
document.getElementById('btn-close-sidebar-sheet').addEventListener('click', () => closeListSheet());
document.getElementById('btn-rail').addEventListener('click', () => toggleSidebar());
document.getElementById('mobile-sheet-backdrop').addEventListener('click', () => {
if (openMenuState) closeMenu(openMenuState.id, true);
else closeListSheet();
Expand Down
2 changes: 1 addition & 1 deletion src/js/11-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* entry; version 1 files (no review status) still open.
*/

const DOCDIFF_APP_VERSION = '1.6.2';
const DOCDIFF_APP_VERSION = '1.6.3';
const SESSION_FORMAT_ID = 'docdiff-session';
const SESSION_FORMAT_VERSION = 2;
const SESSION_FILE_EXT = '.docdiff';
Expand Down
104 changes: 92 additions & 12 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@

--changed: 34 90% 46%; /* amber edge of a changed paragraph */
--changed-ink: 28 90% 25%;
--current: 215 25% 13%; /* edge of the current change */
--current-tint: 215 30% 50% / 0.07; /* background of the current change */
--current: 212.8 100% 42%; /* edge of the current change: the selection blue */
--current-tint: 212.8 100% 50% / 0.07; /* background of the current change */
--hover-tint: 215 20% 50% / 0.05;

--warning-ton-2: 45 100% 88%;
Expand Down Expand Up @@ -150,8 +150,8 @@

--changed: 38 70% 48%;
--changed-ink: 40 95% 72%;
--current: 0 0% 93%;
--current-tint: 0 0% 100% / 0.06;
--current: 210 100% 66%;
--current-tint: 210 100% 66% / 0.1;
--hover-tint: 0 0% 100% / 0.04;

--warning-ton-2: 42 70% 24%;
Expand Down Expand Up @@ -841,6 +841,73 @@ body.is-comparing .empty-launchpad {
margin: 0 auto;
padding: 44px 0 40px;
}
/* The first screen: the hero centred in the space above "How it works", which sits at the
bottom edge, with a quiet way on to the rest. */
.landing-more {
display: none;
}
@media (min-width: 768px) and (min-height: 760px) {
.landing-inner {
padding-top: 0;
}
.landing-first {
min-height: calc(100dvh - var(--topbar-h));
display: flex;
flex-direction: column;
}
.landing-first > .landing-hero {
flex: 1 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
padding: 40px 0;
}
.landing-first > .landing-section {
margin-top: 0;
}
.landing-more {
display: inline-flex;
align-items: center;
justify-content: center;
align-self: center;
width: 44px;
height: 44px;
margin: 12px 0 8px;
border: 0;
border-radius: 999px;
background: transparent;
color: hsl(var(--muted-foreground));
cursor: pointer;
}
.landing-more:hover {
color: hsl(var(--foreground));
background: hsl(var(--ton-2));
}
.landing-more .icon {
width: 22px;
height: 22px;
stroke-width: 2;
}
}
/* Lower windows (760–860px): a tighter first screen so the arrow stays visible. Below 760px the
first screen cannot hold the hero and "How it works", so the page stays a plain flow. */
@media (min-width: 768px) and (min-height: 760px) and (max-height: 860px) {
.landing-first > .landing-hero {
padding: 20px 0;
}
.landing-first > .landing-section {
padding-top: 20px;
}
.landing-first .how-steps {
margin-top: 12px;
}
.landing-first .landing-trust {
margin-top: 18px;
}
.landing-more {
margin: 4px 0;
}
}

.landing-title {
max-width: 30ch;
Expand Down Expand Up @@ -1628,13 +1695,19 @@ ins.tok-ins.tok-marker {
text-decoration-style: dotted;
}

/* Reviewed: accepted changes are muted; the mark sits in the margin of the new version */
/* Reviewed: a decided change reads as finished text. Accepted: the new wording is plain text
with a green edge; the removed words stay quietly struck through, so the change is still
visible. Only colours change, so reviewing never moves any text. */
.clause.is-accepted {
border-left-color: hsl(var(--muted-foreground) / 0.3);
border-left-color: hsl(var(--success));
background: transparent;
}
.clause.is-accepted del.tok-del,
.clause.is-accepted ins.tok-ins {
background: transparent;
color: inherit;
text-decoration-color: transparent;
}
.clause.is-accepted del.tok-del {
background: transparent;
color: hsl(var(--muted-foreground));
}
Expand All @@ -1647,8 +1720,9 @@ ins.tok-ins.tok-marker {
font-weight: 600;
color: hsl(var(--muted-foreground));
}
/* Rejected: the old wording is back as plain text, with a solid neutral edge */
.diff-row-anchor.has-ghost .clause {
border-left: 2px dotted hsl(var(--muted-foreground) / 0.6);
border-left: 2px solid hsl(var(--muted-foreground) / 0.7);
}
.diff-row-anchor.has-ghost .clause.is-active {
border-left-style: solid;
Expand Down Expand Up @@ -1820,7 +1894,7 @@ ins.tok-ins.tok-marker {
stroke-width: 2.2;
}
.review-status.is-accepted .rs-mark { color: hsl(var(--success-ink)); }
.review-status.is-rejected .rs-mark { color: hsl(var(--muted-foreground)); }
.review-status.is-rejected .rs-mark { color: hsl(var(--foreground)); }
.ra-undo {
display: inline-flex;
align-items: center;
Expand Down Expand Up @@ -2017,6 +2091,10 @@ body.tier-phone .bottom-bar .review-bar {
justify-content: space-between;
gap: var(--sp-2);
}
/* Shows and hides the docked list of changes (desktop only; tablets and phones use a sheet) */
body:not(.tier-desktop) #btn-rail {
display: none;
}
.list-title {
font-size: var(--text-title);
font-weight: 600;
Expand Down Expand Up @@ -2101,9 +2179,11 @@ body.tier-phone .bottom-bar .review-bar {
.nav-item.ins { border-left-color: hsl(var(--success)); }
.nav-item.del { border-left-color: hsl(var(--destructive)); }
.nav-item.moved { border-left-style: dashed; border-left-color: hsl(var(--muted-foreground) / 0.6); }
.nav-item.is-accepted,
.nav-item.is-accepted {
border-left-color: hsl(var(--success));
}
.nav-item.is-rejected {
border-left-color: hsl(var(--muted-foreground) / 0.35);
border-left-color: hsl(var(--muted-foreground) / 0.7);
}
.nav-item.is-active {
background: hsl(var(--current-tint));
Expand Down Expand Up @@ -2169,7 +2249,7 @@ body.tier-phone .bottom-bar .review-bar {
stroke-width: 2.4;
}
.nav-status.is-accepted { color: hsl(var(--success-ink)); }
.nav-status.is-rejected { color: hsl(var(--muted-foreground)); }
.nav-status.is-rejected { color: hsl(var(--foreground)); }
.nav-item.is-accepted .nav-item-ref,
.nav-item.is-rejected .nav-item-ref,
.nav-item.is-accepted .nav-item-snippet,
Expand Down
14 changes: 10 additions & 4 deletions src/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ <h2 class="sr-only" id="files-pop-title" data-i18n="filesTitle">Dokumente</h2>
<a class="menu-item" role="menuitem" href="https://github.com/tschnorpfeil/docdiff" target="_blank" rel="noopener noreferrer"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg><span data-i18n="githubMenu">Quellcode auf GitHub</span></a>
</div>
</div>
<button type="button" class="tb-icon-btn compare-only" id="btn-rail" aria-expanded="true" aria-controls="sidebar" aria-label="Änderungsliste" data-i18n-label="sidebarToggle"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/></svg></button>
</div>
</header>

Expand Down Expand Up @@ -322,6 +323,7 @@ <h1 class="sr-only compare-only" id="compare-heading" tabindex="-1">Doc Diff</h1
and the app re-translates it at runtime. The German text below is only a readable placeholder. -->
<div class="empty-launchpad landing" id="empty-launchpad">
<div class="landing-inner">
<div class="landing-first">
<p class="lang-hint" id="lang-hint" hidden><a id="lang-hint-link" href="./"></a></p>
<section class="landing-hero" aria-labelledby="landing-title">
<h1 class="landing-title" id="landing-title" tabindex="-1" data-i18n-html="heroTitle">Dokumente <del class="hero-del" aria-hidden="true" data-word="mühsam"></del> <ins class="hero-ins">einfach</ins> vergleichen.</h1>
Expand Down Expand Up @@ -376,8 +378,10 @@ <h2 id="how-title" data-i18n="howTitle">So funktioniert’s</h2>
<li><span class="how-num" aria-hidden="true">3</span><span><strong data-i18n="how3Title">Ergebnis sichern, wenn Sie es brauchen</strong> <span data-i18n="how3Text">Als Word-Datei mit markierten Änderungen, als PDF oder als Bericht speichern.</span></span></li>
</ol>
</section>
<button type="button" class="landing-more" id="landing-more" aria-label="Mehr erfahren" data-i18n-label="landingMore"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
</div>

<section class="landing-section" aria-labelledby="scope-title">
<section class="landing-section" id="landing-scope" aria-labelledby="scope-title">
<h2 id="scope-title" data-i18n="scopeTitle">Was es kann – und was nicht</h2>
<div class="scope-grid">
<div class="scope-col scope-can">
Expand Down Expand Up @@ -572,15 +576,17 @@ <h2 class="dialog-title" id="session-dialog-title" tabindex="-1"></h2>
</div>
</dialog>
<!-- Inlined libraries (PDF.js + PDF.js worker + Mammoth DOCX + jsdiff) and the app.
They sit at the end of <body> so the static landing page above renders first. -->
<script>
They sit at the end of <body> so the static landing page above renders first.
PDF.js and Mammoth are not executed here: they start the first time a PDF or Word file is
read (startEmbeddedLibrary), so opening the page does not wait for ~1 MB of JavaScript. -->
<script type="text/plain" id="lib-pdfjs">
__PDF_JS__
</script>
<!-- Not executed here: started as a Blob Web Worker on first PDF import (ensurePdfJsWorker) -->
<script type="text/plain" id="pdfjs-worker-source">
__PDF_WORKER_JS__
</script>
<script>
<script type="text/plain" id="lib-mammoth">
__MAMMOTH_JS__
</script>
<script>
Expand Down
Loading