Merge branch 'feat/mr-view-ux-mobile' into feat/mr-view-ux
# Conflicts: # internal/adapters/web/jstest/mr.test.mjs # internal/adapters/web/static/js/mr.js
15 files · +480 -211
M internal/adapters/web/diff.templ +6 -2
| @@ -117,7 +117,7 @@ templ diffViewMenu(p DiffView) { | |||
| 117 | 117 | <details class="menu vmenu"> | |
| 118 | 118 | <summary class="btn" aria-label="Diff view"> | |
| 119 | 119 | if p.Both { | |
| 120 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> | |
| 120 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> | |
| 121 | 121 | } else { | |
| 122 | 122 | { viewLabel(p.View) } | |
| 123 | 123 | } | |
| @@ -139,12 +139,16 @@ templ viewLink(p DiffView, view, label, key, href string) { | |||
| 139 | 139 | class="btn vbtn" | |
| 140 | 140 | data-view={ view } | |
| 141 | 141 | aria-keyshortcuts={ key } | |
| 142 | + | if view == "split" { | |
| 143 | + | :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" | |
| 144 | + | :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" | |
| 145 | + | } | |
| 142 | 146 | if p.View == view { | |
| 143 | 147 | aria-current="true" | |
| 144 | 148 | } | |
| 145 | 149 | if p.Both { | |
| 146 | 150 | data-fire={ view } | |
| 147 | - | :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false" | |
| 151 | + | :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false" | |
| 148 | 152 | } | |
| 149 | 153 | >{ label }<kbd aria-hidden="true">{ key }</kbd></a> | |
| 150 | 154 | } | |
| @@ -117,7 +117,7 @@ templ diffViewMenu(p DiffView) { | |||||
| 117 | <details class="menu vmenu"> | 117 | <details class="menu vmenu"> | ||
| 118 | <summary class="btn" aria-label="Diff view"> | 118 | <summary class="btn" aria-label="Diff view"> | ||
| 119 | if p.Both { | 119 | if p.Both { | ||
| 120 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> | 120 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> |
| 121 | } else { | 121 | } else { | ||
| 122 | { viewLabel(p.View) } | 122 | { viewLabel(p.View) } | ||
| 123 | } | 123 | } | ||
| @@ -139,12 +139,16 @@ templ viewLink(p DiffView, view, label, key, href string) { | |||||
| 139 | class="btn vbtn" | 139 | class="btn vbtn" | ||
| 140 | data-view={ view } | 140 | data-view={ view } | ||
| 141 | aria-keyshortcuts={ key } | 141 | aria-keyshortcuts={ key } | ||
| 142 | + | if view == "split" { | |||
| 143 | + | :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" | |||
| 144 | + | :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" | |||
| 145 | + | } | |||
| 142 | if p.View == view { | 146 | if p.View == view { | ||
| 143 | aria-current="true" | 147 | aria-current="true" | ||
| 144 | } | 148 | } | ||
| 145 | if p.Both { | 149 | if p.Both { | ||
| 146 | data-fire={ view } | 150 | data-fire={ view } | ||
| 147 | - | :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false" | 151 | + | :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false" |
| 148 | } | 152 | } | ||
| 149 | >{ label }<kbd aria-hidden="true">{ key }</kbd></a> | 153 | >{ label }<kbd aria-hidden="true">{ key }</kbd></a> | ||
| 150 | } | 154 | } | ||
M internal/adapters/web/diff_templ.go +190 -184 generated file: collapsed
M internal/adapters/web/jstest/a11y.test.mjs +23 -0
| @@ -358,3 +358,26 @@ test('activity moves a home row up only in newest activity order, preserving foc | |||
| 358 | 358 | assert.ok(fresh.hasAttribute('data-cursor')); | |
| 359 | 359 | } | |
| 360 | 360 | }); | |
| 361 | + | ||
| 362 | + | test('mobile MR layout shortcuts preserve the saved preference until back on desktop', () => { | |
| 363 | + | const diffs = el('div', { id: 'diffs', attrs: { 'data-mr': '', 'data-diff': 'split', 'data-mobile': 'true' } }); | |
| 364 | + | dom.body.replaceChildren(diffs); | |
| 365 | + | const oldQuery = doc.querySelector; | |
| 366 | + | doc.querySelector = query.one; | |
| 367 | + | const previous = globalThis.sessionStorage; | |
| 368 | + | const writes = []; | |
| 369 | + | globalThis.sessionStorage = { setItem: (...args) => writes.push(args) }; | |
| 370 | + | try { | |
| 371 | + | window.ermite.layout('unified'); | |
| 372 | + | window.ermite.layout('split'); | |
| 373 | + | assert.equal(diffs.dataset.diff, 'split'); | |
| 374 | + | assert.deepEqual(writes, []); | |
| 375 | + | diffs.dataset.mobile = 'false'; | |
| 376 | + | window.ermite.layout('unified'); | |
| 377 | + | assert.equal(diffs.dataset.diff, 'unified'); | |
| 378 | + | assert.deepEqual(writes, [['diff', 'unified']]); | |
| 379 | + | } finally { | |
| 380 | + | globalThis.sessionStorage = previous; | |
| 381 | + | doc.querySelector = oldQuery; | |
| 382 | + | } | |
| 383 | + | }); | |
| @@ -358,3 +358,26 @@ test('activity moves a home row up only in newest activity order, preserving foc | |||||
| 358 | assert.ok(fresh.hasAttribute('data-cursor')); | 358 | assert.ok(fresh.hasAttribute('data-cursor')); | ||
| 359 | } | 359 | } | ||
| 360 | }); | 360 | }); | ||
| 361 | + | ||||
| 362 | + | test('mobile MR layout shortcuts preserve the saved preference until back on desktop', () => { | |||
| 363 | + | const diffs = el('div', { id: 'diffs', attrs: { 'data-mr': '', 'data-diff': 'split', 'data-mobile': 'true' } }); | |||
| 364 | + | dom.body.replaceChildren(diffs); | |||
| 365 | + | const oldQuery = doc.querySelector; | |||
| 366 | + | doc.querySelector = query.one; | |||
| 367 | + | const previous = globalThis.sessionStorage; | |||
| 368 | + | const writes = []; | |||
| 369 | + | globalThis.sessionStorage = { setItem: (...args) => writes.push(args) }; | |||
| 370 | + | try { | |||
| 371 | + | window.ermite.layout('unified'); | |||
| 372 | + | window.ermite.layout('split'); | |||
| 373 | + | assert.equal(diffs.dataset.diff, 'split'); | |||
| 374 | + | assert.deepEqual(writes, []); | |||
| 375 | + | diffs.dataset.mobile = 'false'; | |||
| 376 | + | window.ermite.layout('unified'); | |||
| 377 | + | assert.equal(diffs.dataset.diff, 'unified'); | |||
| 378 | + | assert.deepEqual(writes, [['diff', 'unified']]); | |||
| 379 | + | } finally { | |||
| 380 | + | globalThis.sessionStorage = previous; | |||
| 381 | + | doc.querySelector = oldQuery; | |||
| 382 | + | } | |||
| 383 | + | }); | |||
M internal/adapters/web/jstest/mr.test.mjs +106 -0
| @@ -135,3 +135,109 @@ test('footer tabs switch panels, support arrows, and open on checks hash', () => | |||
| 135 | 135 | globalThis.document=oldDocument; globalThis.location=oldLocation; globalThis.addEventListener=oldAdd; | |
| 136 | 136 | } | |
| 137 | 137 | }); | |
| 138 | + | ||
| 139 | + | test('touch range: first tap anchors, second tap opens the sorted range', () => { | |
| 140 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |
| 141 | + | const first = base + '?on=new:24:a.go#f1'; | |
| 142 | + | const last = base + '?on=new:10:a.go#f1'; | |
| 143 | + | assert.deepEqual(C.touchRange(null, first, base), { start: first, url: null }); | |
| 144 | + | const result = C.touchRange(first, last, base); | |
| 145 | + | assert.equal(result.start, null); | |
| 146 | + | assert.equal(new URL(result.url).searchParams.get('on'), 'new:10-24:a.go'); | |
| 147 | + | assert.deepEqual(C.touchRange(first, first, base), { start: null, url: first }); | |
| 148 | + | }); | |
| 149 | + | ||
| 150 | + | test('touch range: a different file, side or request becomes a new anchor', () => { | |
| 151 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |
| 152 | + | const first = base + '?on=new:24:a.go#f1'; | |
| 153 | + | for (const next of [first.replace('new:', 'old:'), first.replace('a.go', 'b.go'), first.replace('/1?', '/2?')]) { | |
| 154 | + | assert.deepEqual(C.touchRange(first, next, base), { start: next, url: null }); | |
| 155 | + | } | |
| 156 | + | }); | |
| 157 | + | ||
| 158 | + | test('mobile unified fallback only redirects single-layout split pages and preserves query/hash', () => { | |
| 159 | + | const href = 'https://forge.test/demo/merge-requests/1?view=split&from=25&on=new:3:a.go#f26'; | |
| 160 | + | assert.equal(C.mobileURL(href, 'split', false), null); | |
| 161 | + | assert.equal(C.mobileURL(href, undefined, true), null, 'both layouts are switched by CSS'); | |
| 162 | + | assert.equal(C.mobileURL(href, 'unified', true), null, 'no redirect loop'); | |
| 163 | + | const url = new URL(C.mobileURL(href, 'split', true)); | |
| 164 | + | assert.equal(url.searchParams.get('view'), 'unified'); | |
| 165 | + | assert.equal(url.searchParams.get('from'), '25'); | |
| 166 | + | assert.equal(url.searchParams.get('on'), 'new:3:a.go'); | |
| 167 | + | assert.equal(url.hash, '#f26'); | |
| 168 | + | }); | |
| 169 | + | ||
| 170 | + | test('touch control: lazy links enable toolbar; ordinary taps pass through; ranges navigate and cancel', async () => { | |
| 171 | + | const saved = Object.fromEntries(['document', 'location', 'addEventListener', 'localStorage', 'sessionStorage', 'fetch', 'matchMedia'].map((k) => [k, globalThis[k]])); | |
| 172 | + | const make = () => ({ | |
| 173 | + | attrs: {}, events: {}, children: [], textContent: '', | |
| 174 | + | setAttribute(k, v) { this.attrs[k] = v; }, | |
| 175 | + | removeAttribute(k) { delete this.attrs[k]; }, | |
| 176 | + | addEventListener(k, fn) { this.events[k] = fn; }, | |
| 177 | + | append(...nodes) { this.children.push(...nodes); }, | |
| 178 | + | prepend(node) { this.children.unshift(node); }, | |
| 179 | + | querySelector() { return null; }, querySelectorAll() { return []; }, | |
| 180 | + | }); | |
| 181 | + | const diffs = make(); | |
| 182 | + | diffs.dataset = { diff: 'split' }; | |
| 183 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |
| 184 | + | const line = (n) => Object.assign(make(), { href: `${base}?on=new:${n}:a.go#f1`, textContent: String(n) }); | |
| 185 | + | const a = line(20), b = line(10); | |
| 186 | + | let loaded = false; | |
| 187 | + | diffs.querySelector = () => loaded ? a : null; | |
| 188 | + | let assigned = null; | |
| 189 | + | globalThis.document = { | |
| 190 | + | querySelector: (sel) => sel === '#diffs[data-mr]' || sel === '#diffs' ? diffs : null, | |
| 191 | + | querySelectorAll: () => [], createElement: (tag) => tag === 'template' ? { content: { querySelectorAll: () => [] } } : make(), | |
| 192 | + | }; | |
| 193 | + | globalThis.location = { href: base, pathname: '/demo/merge-requests/1', hash: '', assign(url) { assigned = url; } }; | |
| 194 | + | globalThis.addEventListener = () => {}; | |
| 195 | + | let resize; | |
| 196 | + | const media = { matches: true, addEventListener(type, fn) { resize = fn; } }; | |
| 197 | + | globalThis.matchMedia = () => media; | |
| 198 | + | globalThis.fetch = async () => { loaded = true; return { ok: true, status: 200, text: async () => '' }; }; | |
| 199 | + | globalThis.localStorage = globalThis.sessionStorage = { getItem: () => null }; | |
| 200 | + | const click = (link, modifiers = {}) => { | |
| 201 | + | const e = { target: { closest: () => link }, prevented: false, stopped: false, | |
| 202 | + | preventDefault() { this.prevented = true; }, stopPropagation() { this.stopped = true; }, ...modifiers }; | |
| 203 | + | diffs.events.click(e); | |
| 204 | + | return e; | |
| 205 | + | }; | |
| 206 | + | try { | |
| 207 | + | window.ermiteMR.start(); | |
| 208 | + | const bar = diffs.children[0]; | |
| 209 | + | const [button, status] = bar.children; | |
| 210 | + | assert.equal(bar.hidden, true, 'no commentable links in the initial window'); | |
| 211 | + | assert.equal(typeof diffs.events.click, 'function', 'delegation installed before lazy load'); | |
| 212 | + | await window.ermiteMR.load({ dataset: { src: '/files?from=25' } }); | |
| 213 | + | assert.equal(bar.hidden, false, 'lazy window refresh reveals range control'); | |
| 214 | + | assert.equal(diffs.dataset.mobile, 'true'); | |
| 215 | + | assert.equal(diffs.dataset.diff, 'split', 'preferred layout is retained'); | |
| 216 | + | media.matches = false; | |
| 217 | + | resize(); | |
| 218 | + | assert.equal(diffs.dataset.mobile, 'false', 'desktop menu returns to preferred layout'); | |
| 219 | + | assert.equal(diffs.dataset.diff, 'split'); | |
| 220 | + | assert.equal(click(a).prevented, false, 'normal single-line navigation is untouched'); | |
| 221 | + | button.events.click(); | |
| 222 | + | assert.equal(button.attrs['aria-pressed'], 'true'); | |
| 223 | + | assert.equal(click(a, { shiftKey: true }).prevented, false, 'desktop range selection is untouched'); | |
| 224 | + | const event = click(a); | |
| 225 | + | assert.equal(event.prevented, true); | |
| 226 | + | assert.equal(event.stopped, true); | |
| 227 | + | assert.ok('data-range-start' in a.attrs); | |
| 228 | + | assert.match(status.textContent, /20/); | |
| 229 | + | click(b); | |
| 230 | + | assert.equal(new URL(assigned).searchParams.get('on'), 'new:10-20:a.go'); | |
| 231 | + | assert.equal('data-range-start' in a.attrs, false); | |
| 232 | + | click(a); | |
| 233 | + | button.events.click(); | |
| 234 | + | assert.equal(button.attrs['aria-pressed'], 'false'); | |
| 235 | + | assert.equal(status.textContent, ''); | |
| 236 | + | assert.equal('data-range-start' in a.attrs, false); | |
| 237 | + | assert.equal(click(b).prevented, false); | |
| 238 | + | } finally { | |
| 239 | + | for (const [k, v] of Object.entries(saved)) { | |
| 240 | + | if (v === undefined) delete globalThis[k]; else globalThis[k] = v; | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | }); | |
| @@ -135,3 +135,109 @@ test('footer tabs switch panels, support arrows, and open on checks hash', () => | |||||
| 135 | globalThis.document=oldDocument; globalThis.location=oldLocation; globalThis.addEventListener=oldAdd; | 135 | globalThis.document=oldDocument; globalThis.location=oldLocation; globalThis.addEventListener=oldAdd; | ||
| 136 | } | 136 | } | ||
| 137 | }); | 137 | }); | ||
| 138 | + | ||||
| 139 | + | test('touch range: first tap anchors, second tap opens the sorted range', () => { | |||
| 140 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |||
| 141 | + | const first = base + '?on=new:24:a.go#f1'; | |||
| 142 | + | const last = base + '?on=new:10:a.go#f1'; | |||
| 143 | + | assert.deepEqual(C.touchRange(null, first, base), { start: first, url: null }); | |||
| 144 | + | const result = C.touchRange(first, last, base); | |||
| 145 | + | assert.equal(result.start, null); | |||
| 146 | + | assert.equal(new URL(result.url).searchParams.get('on'), 'new:10-24:a.go'); | |||
| 147 | + | assert.deepEqual(C.touchRange(first, first, base), { start: null, url: first }); | |||
| 148 | + | }); | |||
| 149 | + | ||||
| 150 | + | test('touch range: a different file, side or request becomes a new anchor', () => { | |||
| 151 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |||
| 152 | + | const first = base + '?on=new:24:a.go#f1'; | |||
| 153 | + | for (const next of [first.replace('new:', 'old:'), first.replace('a.go', 'b.go'), first.replace('/1?', '/2?')]) { | |||
| 154 | + | assert.deepEqual(C.touchRange(first, next, base), { start: next, url: null }); | |||
| 155 | + | } | |||
| 156 | + | }); | |||
| 157 | + | ||||
| 158 | + | test('mobile unified fallback only redirects single-layout split pages and preserves query/hash', () => { | |||
| 159 | + | const href = 'https://forge.test/demo/merge-requests/1?view=split&from=25&on=new:3:a.go#f26'; | |||
| 160 | + | assert.equal(C.mobileURL(href, 'split', false), null); | |||
| 161 | + | assert.equal(C.mobileURL(href, undefined, true), null, 'both layouts are switched by CSS'); | |||
| 162 | + | assert.equal(C.mobileURL(href, 'unified', true), null, 'no redirect loop'); | |||
| 163 | + | const url = new URL(C.mobileURL(href, 'split', true)); | |||
| 164 | + | assert.equal(url.searchParams.get('view'), 'unified'); | |||
| 165 | + | assert.equal(url.searchParams.get('from'), '25'); | |||
| 166 | + | assert.equal(url.searchParams.get('on'), 'new:3:a.go'); | |||
| 167 | + | assert.equal(url.hash, '#f26'); | |||
| 168 | + | }); | |||
| 169 | + | ||||
| 170 | + | test('touch control: lazy links enable toolbar; ordinary taps pass through; ranges navigate and cancel', async () => { | |||
| 171 | + | const saved = Object.fromEntries(['document', 'location', 'addEventListener', 'localStorage', 'sessionStorage', 'fetch', 'matchMedia'].map((k) => [k, globalThis[k]])); | |||
| 172 | + | const make = () => ({ | |||
| 173 | + | attrs: {}, events: {}, children: [], textContent: '', | |||
| 174 | + | setAttribute(k, v) { this.attrs[k] = v; }, | |||
| 175 | + | removeAttribute(k) { delete this.attrs[k]; }, | |||
| 176 | + | addEventListener(k, fn) { this.events[k] = fn; }, | |||
| 177 | + | append(...nodes) { this.children.push(...nodes); }, | |||
| 178 | + | prepend(node) { this.children.unshift(node); }, | |||
| 179 | + | querySelector() { return null; }, querySelectorAll() { return []; }, | |||
| 180 | + | }); | |||
| 181 | + | const diffs = make(); | |||
| 182 | + | diffs.dataset = { diff: 'split' }; | |||
| 183 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |||
| 184 | + | const line = (n) => Object.assign(make(), { href: `${base}?on=new:${n}:a.go#f1`, textContent: String(n) }); | |||
| 185 | + | const a = line(20), b = line(10); | |||
| 186 | + | let loaded = false; | |||
| 187 | + | diffs.querySelector = () => loaded ? a : null; | |||
| 188 | + | let assigned = null; | |||
| 189 | + | globalThis.document = { | |||
| 190 | + | querySelector: (sel) => sel === '#diffs[data-mr]' || sel === '#diffs' ? diffs : null, | |||
| 191 | + | querySelectorAll: () => [], createElement: (tag) => tag === 'template' ? { content: { querySelectorAll: () => [] } } : make(), | |||
| 192 | + | }; | |||
| 193 | + | globalThis.location = { href: base, pathname: '/demo/merge-requests/1', hash: '', assign(url) { assigned = url; } }; | |||
| 194 | + | globalThis.addEventListener = () => {}; | |||
| 195 | + | let resize; | |||
| 196 | + | const media = { matches: true, addEventListener(type, fn) { resize = fn; } }; | |||
| 197 | + | globalThis.matchMedia = () => media; | |||
| 198 | + | globalThis.fetch = async () => { loaded = true; return { ok: true, status: 200, text: async () => '' }; }; | |||
| 199 | + | globalThis.localStorage = globalThis.sessionStorage = { getItem: () => null }; | |||
| 200 | + | const click = (link, modifiers = {}) => { | |||
| 201 | + | const e = { target: { closest: () => link }, prevented: false, stopped: false, | |||
| 202 | + | preventDefault() { this.prevented = true; }, stopPropagation() { this.stopped = true; }, ...modifiers }; | |||
| 203 | + | diffs.events.click(e); | |||
| 204 | + | return e; | |||
| 205 | + | }; | |||
| 206 | + | try { | |||
| 207 | + | window.ermiteMR.start(); | |||
| 208 | + | const bar = diffs.children[0]; | |||
| 209 | + | const [button, status] = bar.children; | |||
| 210 | + | assert.equal(bar.hidden, true, 'no commentable links in the initial window'); | |||
| 211 | + | assert.equal(typeof diffs.events.click, 'function', 'delegation installed before lazy load'); | |||
| 212 | + | await window.ermiteMR.load({ dataset: { src: '/files?from=25' } }); | |||
| 213 | + | assert.equal(bar.hidden, false, 'lazy window refresh reveals range control'); | |||
| 214 | + | assert.equal(diffs.dataset.mobile, 'true'); | |||
| 215 | + | assert.equal(diffs.dataset.diff, 'split', 'preferred layout is retained'); | |||
| 216 | + | media.matches = false; | |||
| 217 | + | resize(); | |||
| 218 | + | assert.equal(diffs.dataset.mobile, 'false', 'desktop menu returns to preferred layout'); | |||
| 219 | + | assert.equal(diffs.dataset.diff, 'split'); | |||
| 220 | + | assert.equal(click(a).prevented, false, 'normal single-line navigation is untouched'); | |||
| 221 | + | button.events.click(); | |||
| 222 | + | assert.equal(button.attrs['aria-pressed'], 'true'); | |||
| 223 | + | assert.equal(click(a, { shiftKey: true }).prevented, false, 'desktop range selection is untouched'); | |||
| 224 | + | const event = click(a); | |||
| 225 | + | assert.equal(event.prevented, true); | |||
| 226 | + | assert.equal(event.stopped, true); | |||
| 227 | + | assert.ok('data-range-start' in a.attrs); | |||
| 228 | + | assert.match(status.textContent, /20/); | |||
| 229 | + | click(b); | |||
| 230 | + | assert.equal(new URL(assigned).searchParams.get('on'), 'new:10-20:a.go'); | |||
| 231 | + | assert.equal('data-range-start' in a.attrs, false); | |||
| 232 | + | click(a); | |||
| 233 | + | button.events.click(); | |||
| 234 | + | assert.equal(button.attrs['aria-pressed'], 'false'); | |||
| 235 | + | assert.equal(status.textContent, ''); | |||
| 236 | + | assert.equal('data-range-start' in a.attrs, false); | |||
| 237 | + | assert.equal(click(b).prevented, false); | |||
| 238 | + | } finally { | |||
| 239 | + | for (const [k, v] of Object.entries(saved)) { | |||
| 240 | + | if (v === undefined) delete globalThis[k]; else globalThis[k] = v; | |||
| 241 | + | } | |||
| 242 | + | } | |||
| 243 | + | }); | |||
A internal/adapters/web/static/css/85-mobile.css +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | @layer components { | |
| 2 | + | .mr-touch-range { display: none; } | |
| 3 | + | ||
| 4 | + | /* MR only: retain the shell's desktop layout and other pages' spacing. */ | |
| 5 | + | @media (max-width: 720px) { | |
| 6 | + | body:has(#diffs[data-mr]) { | |
| 7 | + | --pad: 0px; | |
| 8 | + | --hdr-h: 97px; | |
| 9 | + | overflow-x: clip; | |
| 10 | + | } | |
| 11 | + | body:has(#diffs[data-mr]) .work { padding-inline: 0; margin-inline: 0; gap: var(--s3); } | |
| 12 | + | body:has(#diffs[data-mr]) main { min-width: 0; padding-inline: 0; } | |
| 13 | + | body:has(#diffs[data-mr]) :is(.card, .mr-card, .mr-desc, .dfile, .dscroll, .code, .rail section) { | |
| 14 | + | margin-inline: 0; border-inline: 0; border-radius: 0; min-width: 0; max-width: 100%; | |
| 15 | + | } | |
| 16 | + | body:has(#diffs[data-mr]) :is(.mr-h, .mr-desc.prose, .card-h) { padding-inline: 4px; } | |
| 17 | + | body:has(#diffs[data-mr]) .mr-desc.prose > pre { margin-inline: -4px; padding-inline: 0; max-width: calc(100% + 8px); } | |
| 18 | + | #diffs[data-mr] .dfile > summary { padding-inline: 0; border-inline: 0; border-radius: 0; } | |
| 19 | + | #diffs[data-mr] :is(.dscroll, .code) { overflow-x: auto; overscroll-behavior-x: contain; padding-inline: 0; } | |
| 20 | + | #diffs[data-mr] .dt .ln { width: calc(var(--lnw) + .5ch); min-width: calc(var(--lnw) + .5ch); padding-inline: .25ch; } | |
| 21 | + | #diffs[data-mr] .dt .m { width: 1ch; } | |
| 22 | + | #diffs[data-mr][data-diff] .uni { display: table; } | |
| 23 | + | #diffs[data-mr][data-diff] .spl { display: none; } | |
| 24 | + | #diffs[data-mr] .fnote td { padding-inline: 0; } | |
| 25 | + | #diffs[data-mr] :is(.cform, .cmt) { width: min(65ch, calc(100vw - 5rem)); max-width: calc(100vw - 5rem); overflow-wrap: anywhere; } | |
| 26 | + | body:has(#diffs[data-mr]) :is(input, textarea, select) { font-size: 16px; } | |
| 27 | + | body:has(#diffs[data-mr]) .f-btns { flex-wrap: wrap; } | |
| 28 | + | body:has(#diffs[data-mr]) .menu .menu-pop { | |
| 29 | + | position: fixed; inset: auto 0 0; width: auto; max-width: 100%; | |
| 30 | + | max-height: 75dvh; overflow-y: auto; border-radius: var(--r) var(--r) 0 0; | |
| 31 | + | padding-bottom: max(var(--s4), env(safe-area-inset-bottom)); | |
| 32 | + | } | |
| 33 | + | body:has(#diffs[data-mr]) .mr-bar { position: static; } | |
| 34 | + | /* A wrapping toolbar has no reliable fixed sticky height. */ | |
| 35 | + | body:has(#diffs[data-mr]) { --bar-h: 0px; } | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | @media (pointer: coarse), (hover: none) { | |
| 39 | + | .mr-touch-range { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); } | |
| 40 | + | /* Real, disjoint cells: never expand invisible targets into other lines | |
| 41 | + | or into the neighbouring old/new gutter. */ | |
| 42 | + | #diffs[data-mr] .dt { line-height: max(var(--lh-code), 40px); } | |
| 43 | + | #diffs[data-mr] .dt .ln { width: max(calc(var(--lnw) + .5ch), 40px); min-width: 40px; padding: 0; } | |
| 44 | + | #diffs[data-mr] .spl .c-ln { width: max(calc(var(--lnw) + .5ch), 40px); } | |
| 45 | + | #diffs[data-mr] .ln a { display: block; width: 100%; min-height: 40px; text-align: center; text-decoration: underline; } | |
| 46 | + | .mr-touch-range[hidden] { display: none; } | |
| 47 | + | #diffs[data-mr] .ln a[data-range-start] { outline: 2px solid var(--focus); background: var(--sel); } | |
| 48 | + | body:has(#diffs[data-mr]) :is(.cform .btn, .menu .btn, .mr-touch-range .btn, .c-act button) { min-height: 40px; height: auto; padding-block: var(--s2); } | |
| 49 | + | #diffs[data-mr] .cform { scroll-margin-block: calc(var(--hdr-h) + 12px); } | |
| 50 | + | } | |
| 51 | + | } | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | @layer components { | |||
| 2 | + | .mr-touch-range { display: none; } | |||
| 3 | + | ||||
| 4 | + | /* MR only: retain the shell's desktop layout and other pages' spacing. */ | |||
| 5 | + | @media (max-width: 720px) { | |||
| 6 | + | body:has(#diffs[data-mr]) { | |||
| 7 | + | --pad: 0px; | |||
| 8 | + | --hdr-h: 97px; | |||
| 9 | + | overflow-x: clip; | |||
| 10 | + | } | |||
| 11 | + | body:has(#diffs[data-mr]) .work { padding-inline: 0; margin-inline: 0; gap: var(--s3); } | |||
| 12 | + | body:has(#diffs[data-mr]) main { min-width: 0; padding-inline: 0; } | |||
| 13 | + | body:has(#diffs[data-mr]) :is(.card, .mr-card, .mr-desc, .dfile, .dscroll, .code, .rail section) { | |||
| 14 | + | margin-inline: 0; border-inline: 0; border-radius: 0; min-width: 0; max-width: 100%; | |||
| 15 | + | } | |||
| 16 | + | body:has(#diffs[data-mr]) :is(.mr-h, .mr-desc.prose, .card-h) { padding-inline: 4px; } | |||
| 17 | + | body:has(#diffs[data-mr]) .mr-desc.prose > pre { margin-inline: -4px; padding-inline: 0; max-width: calc(100% + 8px); } | |||
| 18 | + | #diffs[data-mr] .dfile > summary { padding-inline: 0; border-inline: 0; border-radius: 0; } | |||
| 19 | + | #diffs[data-mr] :is(.dscroll, .code) { overflow-x: auto; overscroll-behavior-x: contain; padding-inline: 0; } | |||
| 20 | + | #diffs[data-mr] .dt .ln { width: calc(var(--lnw) + .5ch); min-width: calc(var(--lnw) + .5ch); padding-inline: .25ch; } | |||
| 21 | + | #diffs[data-mr] .dt .m { width: 1ch; } | |||
| 22 | + | #diffs[data-mr][data-diff] .uni { display: table; } | |||
| 23 | + | #diffs[data-mr][data-diff] .spl { display: none; } | |||
| 24 | + | #diffs[data-mr] .fnote td { padding-inline: 0; } | |||
| 25 | + | #diffs[data-mr] :is(.cform, .cmt) { width: min(65ch, calc(100vw - 5rem)); max-width: calc(100vw - 5rem); overflow-wrap: anywhere; } | |||
| 26 | + | body:has(#diffs[data-mr]) :is(input, textarea, select) { font-size: 16px; } | |||
| 27 | + | body:has(#diffs[data-mr]) .f-btns { flex-wrap: wrap; } | |||
| 28 | + | body:has(#diffs[data-mr]) .menu .menu-pop { | |||
| 29 | + | position: fixed; inset: auto 0 0; width: auto; max-width: 100%; | |||
| 30 | + | max-height: 75dvh; overflow-y: auto; border-radius: var(--r) var(--r) 0 0; | |||
| 31 | + | padding-bottom: max(var(--s4), env(safe-area-inset-bottom)); | |||
| 32 | + | } | |||
| 33 | + | body:has(#diffs[data-mr]) .mr-bar { position: static; } | |||
| 34 | + | /* A wrapping toolbar has no reliable fixed sticky height. */ | |||
| 35 | + | body:has(#diffs[data-mr]) { --bar-h: 0px; } | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | @media (pointer: coarse), (hover: none) { | |||
| 39 | + | .mr-touch-range { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); } | |||
| 40 | + | /* Real, disjoint cells: never expand invisible targets into other lines | |||
| 41 | + | or into the neighbouring old/new gutter. */ | |||
| 42 | + | #diffs[data-mr] .dt { line-height: max(var(--lh-code), 40px); } | |||
| 43 | + | #diffs[data-mr] .dt .ln { width: max(calc(var(--lnw) + .5ch), 40px); min-width: 40px; padding: 0; } | |||
| 44 | + | #diffs[data-mr] .spl .c-ln { width: max(calc(var(--lnw) + .5ch), 40px); } | |||
| 45 | + | #diffs[data-mr] .ln a { display: block; width: 100%; min-height: 40px; text-align: center; text-decoration: underline; } | |||
| 46 | + | .mr-touch-range[hidden] { display: none; } | |||
| 47 | + | #diffs[data-mr] .ln a[data-range-start] { outline: 2px solid var(--focus); background: var(--sel); } | |||
| 48 | + | body:has(#diffs[data-mr]) :is(.cform .btn, .menu .btn, .mr-touch-range .btn, .c-act button) { min-height: 40px; height: auto; padding-block: var(--s2); } | |||
| 49 | + | #diffs[data-mr] .cform { scroll-margin-block: calc(var(--hdr-h) + 12px); } | |||
| 50 | + | } | |||
| 51 | + | } | |||
M internal/adapters/web/static/js/ermite.js +1 -0
| @@ -183,6 +183,7 @@ | |||
| 183 | 183 | layout(mode) { | |
| 184 | 184 | const el = $('[data-diff]'); | |
| 185 | 185 | if (!el) return; | |
| 186 | + | if (el.hasAttribute('data-mr') && el.dataset.mobile === 'true') return; | |
| 186 | 187 | el.dataset.diff = mode; | |
| 187 | 188 | try { sessionStorage.setItem('diff', mode); } catch { /* storage refused: the layout holds for this page */ } | |
| 188 | 189 | for (const d of $$('[data-file]')) if ($('.dt', d)) d.open = true; | |
| @@ -183,6 +183,7 @@ | |||||
| 183 | layout(mode) { | 183 | layout(mode) { | ||
| 184 | const el = $('[data-diff]'); | 184 | const el = $('[data-diff]'); | ||
| 185 | if (!el) return; | 185 | if (!el) return; | ||
| 186 | + | if (el.hasAttribute('data-mr') && el.dataset.mobile === 'true') return; | |||
| 186 | el.dataset.diff = mode; | 187 | el.dataset.diff = mode; | ||
| 187 | try { sessionStorage.setItem('diff', mode); } catch { /* storage refused: the layout holds for this page */ } | 188 | try { sessionStorage.setItem('diff', mode); } catch { /* storage refused: the layout holds for this page */ } | ||
| 188 | for (const d of $$('[data-file]')) if ($('.dt', d)) d.open = true; | 189 | for (const d of $$('[data-file]')) if ($('.dt', d)) d.open = true; | ||
M internal/adapters/web/static/js/mr.js +79 -1
| @@ -27,6 +27,20 @@ window.ermiteMR = (() => { | |||
| 27 | 27 | b.searchParams.set('on', `${y[1]}:${nums[0]}-${nums[1]}:${y[3]}`); | |
| 28 | 28 | return b.href; | |
| 29 | 29 | }, | |
| 30 | + | // Range mode is explicit: ordinary taps still open single-line forms. | |
| 31 | + | touchRange(previous, current, base) { | |
| 32 | + | if (!previous) return { start: current, url: null }; | |
| 33 | + | const url = core.rangeURL(previous, current, base); | |
| 34 | + | if (url) return { start: null, url }; | |
| 35 | + | if (previous === current) return { start: null, url: current }; | |
| 36 | + | return { start: current, url: null }; // another file/side starts afresh | |
| 37 | + | }, | |
| 38 | + | mobileURL(href, view, narrow) { | |
| 39 | + | if (!narrow || view !== 'split') return null; | |
| 40 | + | const url = new URL(href); | |
| 41 | + | url.searchParams.set('view', 'unified'); | |
| 42 | + | return url.href; | |
| 43 | + | }, | |
| 30 | 44 | quote(button) { | |
| 31 | 45 | const thread = button.closest('.cmt').closest('[data-thread]'); | |
| 32 | 46 | const details = thread?.querySelector(':scope > details[data-reply]'); | |
| @@ -67,6 +81,7 @@ window.ermiteMR = (() => { | |||
| 67 | 81 | const $ = (sel, root = document) => root.querySelector(sel); | |
| 68 | 82 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |
| 69 | 83 | const loading = new Map(); // src -> promise of the swap | |
| 84 | + | let refreshTouchComments = () => {}; | |
| 70 | 85 | let moved = false; | |
| 71 | 86 | let lazyIO = null; // the observer of the placeholders (watch) | |
| 72 | 87 | let revealGen = 0; // the last reveal wins the scroll | |
| @@ -115,6 +130,7 @@ window.ermiteMR = (() => { | |||
| 115 | 130 | window.htmx?.process(f); | |
| 116 | 131 | window.ermite?.watchFile?.(f, old); | |
| 117 | 132 | } | |
| 133 | + | refreshTouchComments(); | |
| 118 | 134 | } | |
| 119 | 135 | ||
| 120 | 136 | // stale: the diff changed under the page. The placeholders say so; no file | |
| @@ -137,7 +153,7 @@ window.ermiteMR = (() => { | |||
| 137 | 153 | // A page with both layouts says which shows in data-diff; a large diff | |
| 138 | 154 | // rendered in one layout only says it in data-view. | |
| 139 | 155 | const diffs = $('#diffs')?.dataset; | |
| 140 | - | if (/^c\d+$/.test(id) && (diffs?.diff || diffs?.view) === 'split') { | |
| 156 | + | if (/^c\d+$/.test(id) && !window.matchMedia?.('(max-width: 720px)').matches && (diffs?.diff || diffs?.view) === 'split') { | |
| 141 | 157 | const split = document.getElementById(id + '-s'); | |
| 142 | 158 | if (split) return split; | |
| 143 | 159 | } | |
| @@ -337,9 +353,71 @@ window.ermiteMR = (() => { | |||
| 337 | 353 | addEventListener('hashchange', hash); | |
| 338 | 354 | } | |
| 339 | 355 | ||
| 356 | + | function touchComments() { | |
| 357 | + | const diffs = $('#diffs'); | |
| 358 | + | const bar = document.createElement('div'); | |
| 359 | + | bar.className = 'mr-touch-range'; | |
| 360 | + | const button = document.createElement('button'); | |
| 361 | + | button.type = 'button'; | |
| 362 | + | button.className = 'btn'; | |
| 363 | + | button.textContent = 'Select line range'; | |
| 364 | + | button.setAttribute('aria-pressed', 'false'); | |
| 365 | + | const status = document.createElement('span'); | |
| 366 | + | status.setAttribute('role', 'status'); | |
| 367 | + | bar.append(button, status); | |
| 368 | + | diffs.prepend(bar); | |
| 369 | + | refreshTouchComments = () => { bar.hidden = !$('.ln a[href*="on="]', diffs); }; | |
| 370 | + | refreshTouchComments(); | |
| 371 | + | let enabled = false, start = null, marker = null; | |
| 372 | + | const clear = () => { marker?.removeAttribute('data-range-start'); marker = null; start = null; }; | |
| 373 | + | button.addEventListener('click', () => { | |
| 374 | + | enabled = !enabled; | |
| 375 | + | clear(); | |
| 376 | + | button.setAttribute('aria-pressed', String(enabled)); | |
| 377 | + | status.textContent = enabled ? 'Tap the first line number, then the last. Tap the same line for one line.' : ''; | |
| 378 | + | }); | |
| 379 | + | diffs.addEventListener('click', (e) => { | |
| 380 | + | const link = e.target.closest?.('.ln a[href*="on="]'); | |
| 381 | + | if (!enabled || !link || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return; | |
| 382 | + | e.preventDefault(); | |
| 383 | + | e.stopPropagation(); // do not alter the desktop shift-click anchor | |
| 384 | + | const next = core.touchRange(start, link.href, location.href); | |
| 385 | + | clear(); | |
| 386 | + | if (next.url) { location.assign(next.url); return; } | |
| 387 | + | start = next.start; | |
| 388 | + | marker = link; | |
| 389 | + | marker.setAttribute('data-range-start', ''); | |
| 390 | + | status.textContent = `Range starts at line ${link.textContent.trim()}. Tap the last line number (same file and side).`; | |
| 391 | + | }); | |
| 392 | + | // The server opens the line form. Scroll after layout without stealing | |
| 393 | + | // focus or forcing a mobile keyboard to appear. | |
| 394 | + | if (new URL(location.href).searchParams.has('on')) requestAnimationFrame(() => { | |
| 395 | + | const form = $$('.fnote .cform', diffs).find((f) => f.getClientRects().length); | |
| 396 | + | form?.scrollIntoView({ block: 'center' }); | |
| 397 | + | }); | |
| 398 | + | } | |
| 399 | + | ||
| 340 | 400 | function start() { | |
| 341 | 401 | if (!$('#diffs[data-mr]')) return; | |
| 342 | 402 | panels(); | |
| 403 | + | // Large diffs may contain only the split table. Ask the existing server | |
| 404 | + | // route for unified rather than writing the saved session preference. | |
| 405 | + | const narrow = window.matchMedia?.('(max-width: 720px)'); | |
| 406 | + | const unify = () => { | |
| 407 | + | $('#diffs').dataset.mobile = String(!!narrow?.matches); | |
| 408 | + | const url = core.mobileURL(location.href, $('#diffs')?.dataset.view, narrow?.matches); | |
| 409 | + | if (url) { location.replace(url); return true; } | |
| 410 | + | return false; | |
| 411 | + | }; | |
| 412 | + | if (unify()) return; | |
| 413 | + | narrow?.addEventListener('change', unify); | |
| 414 | + | addEventListener('click', (e) => { | |
| 415 | + | if (narrow?.matches && e.target.closest?.('.vbtn[data-view="split"]')) { | |
| 416 | + | e.preventDefault(); | |
| 417 | + | e.stopImmediatePropagation(); | |
| 418 | + | } | |
| 419 | + | }, true); | |
| 420 | + | touchComments(); | |
| 343 | 421 | let folded = false; | |
| 344 | 422 | try { folded = localStorage.getItem('mr-files-folded') === '1'; } catch { /* no storage: open */ } | |
| 345 | 423 | if (folded) foldFiles(true); | |
| @@ -27,6 +27,20 @@ window.ermiteMR = (() => { | |||||
| 27 | b.searchParams.set('on', `${y[1]}:${nums[0]}-${nums[1]}:${y[3]}`); | 27 | b.searchParams.set('on', `${y[1]}:${nums[0]}-${nums[1]}:${y[3]}`); | ||
| 28 | return b.href; | 28 | return b.href; | ||
| 29 | }, | 29 | }, | ||
| 30 | + | // Range mode is explicit: ordinary taps still open single-line forms. | |||
| 31 | + | touchRange(previous, current, base) { | |||
| 32 | + | if (!previous) return { start: current, url: null }; | |||
| 33 | + | const url = core.rangeURL(previous, current, base); | |||
| 34 | + | if (url) return { start: null, url }; | |||
| 35 | + | if (previous === current) return { start: null, url: current }; | |||
| 36 | + | return { start: current, url: null }; // another file/side starts afresh | |||
| 37 | + | }, | |||
| 38 | + | mobileURL(href, view, narrow) { | |||
| 39 | + | if (!narrow || view !== 'split') return null; | |||
| 40 | + | const url = new URL(href); | |||
| 41 | + | url.searchParams.set('view', 'unified'); | |||
| 42 | + | return url.href; | |||
| 43 | + | }, | |||
| 30 | quote(button) { | 44 | quote(button) { | ||
| 31 | const thread = button.closest('.cmt').closest('[data-thread]'); | 45 | const thread = button.closest('.cmt').closest('[data-thread]'); | ||
| 32 | const details = thread?.querySelector(':scope > details[data-reply]'); | 46 | const details = thread?.querySelector(':scope > details[data-reply]'); | ||
| @@ -67,6 +81,7 @@ window.ermiteMR = (() => { | |||||
| 67 | const $ = (sel, root = document) => root.querySelector(sel); | 81 | const $ = (sel, root = document) => root.querySelector(sel); | ||
| 68 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | 82 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | ||
| 69 | const loading = new Map(); // src -> promise of the swap | 83 | const loading = new Map(); // src -> promise of the swap | ||
| 84 | + | let refreshTouchComments = () => {}; | |||
| 70 | let moved = false; | 85 | let moved = false; | ||
| 71 | let lazyIO = null; // the observer of the placeholders (watch) | 86 | let lazyIO = null; // the observer of the placeholders (watch) | ||
| 72 | let revealGen = 0; // the last reveal wins the scroll | 87 | let revealGen = 0; // the last reveal wins the scroll | ||
| @@ -115,6 +130,7 @@ window.ermiteMR = (() => { | |||||
| 115 | window.htmx?.process(f); | 130 | window.htmx?.process(f); | ||
| 116 | window.ermite?.watchFile?.(f, old); | 131 | window.ermite?.watchFile?.(f, old); | ||
| 117 | } | 132 | } | ||
| 133 | + | refreshTouchComments(); | |||
| 118 | } | 134 | } | ||
| 119 | 135 | ||||
| 120 | // stale: the diff changed under the page. The placeholders say so; no file | 136 | // stale: the diff changed under the page. The placeholders say so; no file | ||
| @@ -137,7 +153,7 @@ window.ermiteMR = (() => { | |||||
| 137 | // A page with both layouts says which shows in data-diff; a large diff | 153 | // A page with both layouts says which shows in data-diff; a large diff | ||
| 138 | // rendered in one layout only says it in data-view. | 154 | // rendered in one layout only says it in data-view. | ||
| 139 | const diffs = $('#diffs')?.dataset; | 155 | const diffs = $('#diffs')?.dataset; | ||
| 140 | - | if (/^c\d+$/.test(id) && (diffs?.diff || diffs?.view) === 'split') { | 156 | + | if (/^c\d+$/.test(id) && !window.matchMedia?.('(max-width: 720px)').matches && (diffs?.diff || diffs?.view) === 'split') { |
| 141 | const split = document.getElementById(id + '-s'); | 157 | const split = document.getElementById(id + '-s'); | ||
| 142 | if (split) return split; | 158 | if (split) return split; | ||
| 143 | } | 159 | } | ||
| @@ -337,9 +353,71 @@ window.ermiteMR = (() => { | |||||
| 337 | addEventListener('hashchange', hash); | 353 | addEventListener('hashchange', hash); | ||
| 338 | } | 354 | } | ||
| 339 | 355 | ||||
| 356 | + | function touchComments() { | |||
| 357 | + | const diffs = $('#diffs'); | |||
| 358 | + | const bar = document.createElement('div'); | |||
| 359 | + | bar.className = 'mr-touch-range'; | |||
| 360 | + | const button = document.createElement('button'); | |||
| 361 | + | button.type = 'button'; | |||
| 362 | + | button.className = 'btn'; | |||
| 363 | + | button.textContent = 'Select line range'; | |||
| 364 | + | button.setAttribute('aria-pressed', 'false'); | |||
| 365 | + | const status = document.createElement('span'); | |||
| 366 | + | status.setAttribute('role', 'status'); | |||
| 367 | + | bar.append(button, status); | |||
| 368 | + | diffs.prepend(bar); | |||
| 369 | + | refreshTouchComments = () => { bar.hidden = !$('.ln a[href*="on="]', diffs); }; | |||
| 370 | + | refreshTouchComments(); | |||
| 371 | + | let enabled = false, start = null, marker = null; | |||
| 372 | + | const clear = () => { marker?.removeAttribute('data-range-start'); marker = null; start = null; }; | |||
| 373 | + | button.addEventListener('click', () => { | |||
| 374 | + | enabled = !enabled; | |||
| 375 | + | clear(); | |||
| 376 | + | button.setAttribute('aria-pressed', String(enabled)); | |||
| 377 | + | status.textContent = enabled ? 'Tap the first line number, then the last. Tap the same line for one line.' : ''; | |||
| 378 | + | }); | |||
| 379 | + | diffs.addEventListener('click', (e) => { | |||
| 380 | + | const link = e.target.closest?.('.ln a[href*="on="]'); | |||
| 381 | + | if (!enabled || !link || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return; | |||
| 382 | + | e.preventDefault(); | |||
| 383 | + | e.stopPropagation(); // do not alter the desktop shift-click anchor | |||
| 384 | + | const next = core.touchRange(start, link.href, location.href); | |||
| 385 | + | clear(); | |||
| 386 | + | if (next.url) { location.assign(next.url); return; } | |||
| 387 | + | start = next.start; | |||
| 388 | + | marker = link; | |||
| 389 | + | marker.setAttribute('data-range-start', ''); | |||
| 390 | + | status.textContent = `Range starts at line ${link.textContent.trim()}. Tap the last line number (same file and side).`; | |||
| 391 | + | }); | |||
| 392 | + | // The server opens the line form. Scroll after layout without stealing | |||
| 393 | + | // focus or forcing a mobile keyboard to appear. | |||
| 394 | + | if (new URL(location.href).searchParams.has('on')) requestAnimationFrame(() => { | |||
| 395 | + | const form = $$('.fnote .cform', diffs).find((f) => f.getClientRects().length); | |||
| 396 | + | form?.scrollIntoView({ block: 'center' }); | |||
| 397 | + | }); | |||
| 398 | + | } | |||
| 399 | + | ||||
| 340 | function start() { | 400 | function start() { | ||
| 341 | if (!$('#diffs[data-mr]')) return; | 401 | if (!$('#diffs[data-mr]')) return; | ||
| 342 | panels(); | 402 | panels(); | ||
| 403 | + | // Large diffs may contain only the split table. Ask the existing server | |||
| 404 | + | // route for unified rather than writing the saved session preference. | |||
| 405 | + | const narrow = window.matchMedia?.('(max-width: 720px)'); | |||
| 406 | + | const unify = () => { | |||
| 407 | + | $('#diffs').dataset.mobile = String(!!narrow?.matches); | |||
| 408 | + | const url = core.mobileURL(location.href, $('#diffs')?.dataset.view, narrow?.matches); | |||
| 409 | + | if (url) { location.replace(url); return true; } | |||
| 410 | + | return false; | |||
| 411 | + | }; | |||
| 412 | + | if (unify()) return; | |||
| 413 | + | narrow?.addEventListener('change', unify); | |||
| 414 | + | addEventListener('click', (e) => { | |||
| 415 | + | if (narrow?.matches && e.target.closest?.('.vbtn[data-view="split"]')) { | |||
| 416 | + | e.preventDefault(); | |||
| 417 | + | e.stopImmediatePropagation(); | |||
| 418 | + | } | |||
| 419 | + | }, true); | |||
| 420 | + | touchComments(); | |||
| 343 | let folded = false; | 421 | let folded = false; | ||
| 344 | try { folded = localStorage.getItem('mr-files-folded') === '1'; } catch { /* no storage: open */ } | 422 | try { folded = localStorage.getItem('mr-files-folded') === '1'; } catch { /* no storage: open */ } | ||
| 345 | if (folded) foldFiles(true); | 423 | if (folded) foldFiles(true); | ||
M internal/app/testdata/web/commit-big.html +3 -3
| @@ -127,12 +127,12 @@ | |||
| 127 | 127 | </span> | |
| 128 | 128 | <details class="menu vmenu"> | |
| 129 | 129 | <summary class="btn" aria-label="Diff view"> | |
| 130 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 130 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 131 | 131 | </summary> | |
| 132 | 132 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 133 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 133 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 134 | 134 | </a> | |
| 135 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 135 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 136 | 136 | </a> | |
| 137 | 137 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 138 | 138 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -127,12 +127,12 @@ | |||||
| 127 | </span> | 127 | </span> | ||
| 128 | <details class="menu vmenu"> | 128 | <details class="menu vmenu"> | ||
| 129 | <summary class="btn" aria-label="Diff view"> | 129 | <summary class="btn" aria-label="Diff view"> | ||
| 130 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 130 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 131 | </summary> | 131 | </summary> | ||
| 132 | <div class="menu-pop" role="group" aria-label="Diff view"> | 132 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 133 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 133 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 134 | </a> | 134 | </a> | ||
| 135 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 135 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 136 | </a> | 136 | </a> | ||
| 137 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 137 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 138 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 138 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-binary.html +3 -3
| @@ -123,12 +123,12 @@ | |||
| 123 | 123 | </span> | |
| 124 | 124 | <details class="menu vmenu"> | |
| 125 | 125 | <summary class="btn" aria-label="Diff view"> | |
| 126 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 127 | 127 | </summary> | |
| 128 | 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 129 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 129 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 130 | 130 | </a> | |
| 131 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 131 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 132 | 132 | </a> | |
| 133 | 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 134 | 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -123,12 +123,12 @@ | |||||
| 123 | </span> | 123 | </span> | ||
| 124 | <details class="menu vmenu"> | 124 | <details class="menu vmenu"> | ||
| 125 | <summary class="btn" aria-label="Diff view"> | 125 | <summary class="btn" aria-label="Diff view"> | ||
| 126 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 127 | </summary> | 127 | </summary> | ||
| 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 129 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 129 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 130 | </a> | 130 | </a> | ||
| 131 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 131 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 132 | </a> | 132 | </a> | ||
| 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-merge.html +3 -3
| @@ -122,12 +122,12 @@ | |||
| 122 | 122 | </span> | |
| 123 | 123 | <details class="menu vmenu"> | |
| 124 | 124 | <summary class="btn" aria-label="Diff view"> | |
| 125 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 125 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 126 | 126 | </summary> | |
| 127 | 127 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 128 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 128 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 129 | 129 | </a> | |
| 130 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 130 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 131 | 131 | </a> | |
| 132 | 132 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 133 | 133 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -122,12 +122,12 @@ | |||||
| 122 | </span> | 122 | </span> | ||
| 123 | <details class="menu vmenu"> | 123 | <details class="menu vmenu"> | ||
| 124 | <summary class="btn" aria-label="Diff view"> | 124 | <summary class="btn" aria-label="Diff view"> | ||
| 125 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 125 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 126 | </summary> | 126 | </summary> | ||
| 127 | <div class="menu-pop" role="group" aria-label="Diff view"> | 127 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 128 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 128 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 129 | </a> | 129 | </a> | ||
| 130 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 130 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 131 | </a> | 131 | </a> | ||
| 132 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 132 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 133 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 133 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-modify-split.html +3 -3
| @@ -129,12 +129,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 129 | 129 | </span> | |
| 130 | 130 | <details class="menu vmenu"> | |
| 131 | 131 | <summary class="btn" aria-label="Diff view"> | |
| 132 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> | |
| 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> | |
| 133 | 133 | </summary> | |
| 134 | 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 135 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 135 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 136 | 136 | </a> | |
| 137 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 137 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" aria-current="true" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 138 | 138 | </a> | |
| 139 | 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 140 | 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -129,12 +129,12 @@ And <b>html</b> & "quotes".</pre> | |||||
| 129 | </span> | 129 | </span> | ||
| 130 | <details class="menu vmenu"> | 130 | <details class="menu vmenu"> | ||
| 131 | <summary class="btn" aria-label="Diff view"> | 131 | <summary class="btn" aria-label="Diff view"> | ||
| 132 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> | 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> |
| 133 | </summary> | 133 | </summary> | ||
| 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 135 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 135 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 136 | </a> | 136 | </a> | ||
| 137 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 137 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" aria-current="true" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 138 | </a> | 138 | </a> | ||
| 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-modify.html +3 -3
| @@ -129,12 +129,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 129 | 129 | </span> | |
| 130 | 130 | <details class="menu vmenu"> | |
| 131 | 131 | <summary class="btn" aria-label="Diff view"> | |
| 132 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 133 | 133 | </summary> | |
| 134 | 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 135 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 135 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 136 | 136 | </a> | |
| 137 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 137 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 138 | 138 | </a> | |
| 139 | 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 140 | 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -129,12 +129,12 @@ And <b>html</b> & "quotes".</pre> | |||||
| 129 | </span> | 129 | </span> | ||
| 130 | <details class="menu vmenu"> | 130 | <details class="menu vmenu"> | ||
| 131 | <summary class="btn" aria-label="Diff view"> | 131 | <summary class="btn" aria-label="Diff view"> | ||
| 132 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 133 | </summary> | 133 | </summary> | ||
| 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 135 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 135 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 136 | </a> | 136 | </a> | ||
| 137 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 137 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 138 | </a> | 138 | </a> | ||
| 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-rename.html +3 -3
| @@ -123,12 +123,12 @@ | |||
| 123 | 123 | </span> | |
| 124 | 124 | <details class="menu vmenu"> | |
| 125 | 125 | <summary class="btn" aria-label="Diff view"> | |
| 126 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 127 | 127 | </summary> | |
| 128 | 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 129 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 129 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 130 | 130 | </a> | |
| 131 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 131 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 132 | 132 | </a> | |
| 133 | 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 134 | 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -123,12 +123,12 @@ | |||||
| 123 | </span> | 123 | </span> | ||
| 124 | <details class="menu vmenu"> | 124 | <details class="menu vmenu"> | ||
| 125 | <summary class="btn" aria-label="Diff view"> | 125 | <summary class="btn" aria-label="Diff view"> | ||
| 126 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 127 | </summary> | 127 | </summary> | ||
| 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 129 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 129 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 130 | </a> | 130 | </a> | ||
| 131 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 131 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 132 | </a> | 132 | </a> | ||
| 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-root.html +3 -3
| @@ -121,12 +121,12 @@ | |||
| 121 | 121 | </span> | |
| 122 | 122 | <details class="menu vmenu"> | |
| 123 | 123 | <summary class="btn" aria-label="Diff view"> | |
| 124 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 124 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 125 | 125 | </summary> | |
| 126 | 126 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 127 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 127 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 128 | 128 | </a> | |
| 129 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 129 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 130 | 130 | </a> | |
| 131 | 131 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 132 | 132 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -121,12 +121,12 @@ | |||||
| 121 | </span> | 121 | </span> | ||
| 122 | <details class="menu vmenu"> | 122 | <details class="menu vmenu"> | ||
| 123 | <summary class="btn" aria-label="Diff view"> | 123 | <summary class="btn" aria-label="Diff view"> | ||
| 124 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 124 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 125 | </summary> | 125 | </summary> | ||
| 126 | <div class="menu-pop" role="group" aria-label="Diff view"> | 126 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 127 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 127 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 128 | </a> | 128 | </a> | ||
| 129 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 129 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 130 | </a> | 130 | </a> | ||
| 131 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 131 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 132 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 132 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-trojan.html +3 -3
| @@ -124,12 +124,12 @@ | |||
| 124 | 124 | </span> | |
| 125 | 125 | <details class="menu vmenu"> | |
| 126 | 126 | <summary class="btn" aria-label="Diff view"> | |
| 127 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 127 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 128 | 128 | </summary> | |
| 129 | 129 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 130 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 130 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 131 | 131 | </a> | |
| 132 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 132 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 133 | 133 | </a> | |
| 134 | 134 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 135 | 135 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -124,12 +124,12 @@ | |||||
| 124 | </span> | 124 | </span> | ||
| 125 | <details class="menu vmenu"> | 125 | <details class="menu vmenu"> | ||
| 126 | <summary class="btn" aria-label="Diff view"> | 126 | <summary class="btn" aria-label="Diff view"> | ||
| 127 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 127 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 128 | </summary> | 128 | </summary> | ||
| 129 | <div class="menu-pop" role="group" aria-label="Diff view"> | 129 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 130 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 130 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 131 | </a> | 131 | </a> | ||
| 132 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 132 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 133 | </a> | 133 | </a> | ||
| 134 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 134 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 135 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 135 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||