section ticking covers the section
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -115,6 +115,17 @@ ADR-007 and ADR-002.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **Ticking a section off crossed out its title and nothing else.** The strike
|
||||||
|
now covers everything under that heading, including its sub-sections, which
|
||||||
|
is what "this section is done" means to somebody who ticked the thing that
|
||||||
|
contained them. It needed the heading RANK, which the HTML importer was
|
||||||
|
discarding: every heading arrived as a sibling, so a parent had no children
|
||||||
|
to cross out. Headings now carry their level, render at their rank, and a
|
||||||
|
guide written before this reads as flat, exactly as it did. Code blocks,
|
||||||
|
tables and images inside a finished section are faded but not struck - a line
|
||||||
|
through a command makes it unreadable, and re-reading the command is usually
|
||||||
|
why somebody looks back at a step they have already done.
|
||||||
|
|
||||||
- **Tech Tools was invisible to anyone without an account**, even though every
|
- **Tech Tools was invisible to anyone without an account**, even though every
|
||||||
tool on the page is deliberately usable without one and Secure Send is the
|
tool on the page is deliberately usable without one and Secure Send is the
|
||||||
single write this product offers a guest. The nav item named no permission,
|
single write this product offers a guest. The nav item named no permission,
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
Reading only: in the editor a tick would be an edit.
|
Reading only: in the editor a tick would be an edit.
|
||||||
-->
|
-->
|
||||||
<h2 v-if="block.type === 'heading'" :id="anchorFor(block)" class="gb-heading"
|
<h2 v-if="block.type === 'heading'" :id="anchorFor(block)" class="gb-heading"
|
||||||
:class="{ 'gb-sectiondone': isTicked(block.id) }">
|
:class="['gb-h' + levelOf(block),
|
||||||
|
{ 'gb-sectiondone': isTicked(block.id) || inDoneSection(index) }]">
|
||||||
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
|
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
|
||||||
:checked="isTicked(block.id)"
|
:checked="isTicked(block.id)"
|
||||||
:aria-label="'Mark this section done'"
|
:aria-label="'Mark this section done'"
|
||||||
@@ -21,10 +22,11 @@
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p v-else-if="block.type === 'paragraph'" class="gb-para"
|
<p v-else-if="block.type === 'paragraph'" class="gb-para"
|
||||||
|
:class="{ 'gb-sectiondone': inDoneSection(index) }"
|
||||||
v-html="inline(block.text)"></p>
|
v-html="inline(block.text)"></p>
|
||||||
|
|
||||||
<div v-else-if="block.type === 'step'" class="gb-step"
|
<div v-else-if="block.type === 'step'" class="gb-step"
|
||||||
:class="{ 'gb-sectiondone': isTicked(block.id) }">
|
:class="{ 'gb-sectiondone': isTicked(block.id) || inDoneSection(index) }">
|
||||||
<label class="gb-stepnum">
|
<label class="gb-stepnum">
|
||||||
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
|
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
|
||||||
:checked="isTicked(block.id)"
|
:checked="isTicked(block.id)"
|
||||||
@@ -41,7 +43,8 @@
|
|||||||
editor a tick would be an edit, and an author clicking around their own
|
editor a tick would be an edit, and an author clicking around their own
|
||||||
draft would quietly change what the next reader sees as "done".
|
draft would quietly change what the next reader sees as "done".
|
||||||
-->
|
-->
|
||||||
<ul v-else-if="block.type === 'checklist'" class="gb-checklist">
|
<ul v-else-if="block.type === 'checklist'" class="gb-checklist"
|
||||||
|
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||||
<li v-for="item in itemsOf(block)" :key="item.id" class="gb-check">
|
<li v-for="item in itemsOf(block)" :key="item.id" class="gb-check">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
@@ -54,21 +57,25 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul v-else-if="block.type === 'bullets'" class="gb-list">
|
<ul v-else-if="block.type === 'bullets'" class="gb-list"
|
||||||
|
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||||
<li v-for="item in itemsOf(block)" :key="item.id" v-html="inline(item.text)"></li>
|
<li v-for="item in itemsOf(block)" :key="item.id" v-html="inline(item.text)"></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ol v-else-if="block.type === 'numbers'" class="gb-list">
|
<ol v-else-if="block.type === 'numbers'" class="gb-list"
|
||||||
|
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||||
<li v-for="item in itemsOf(block)" :key="item.id" v-html="inline(item.text)"></li>
|
<li v-for="item in itemsOf(block)" :key="item.id" v-html="inline(item.text)"></li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div v-else-if="block.type === 'callout'"
|
<div v-else-if="block.type === 'callout'" class="gb-callout"
|
||||||
class="gb-callout" :class="'gb-' + (block.variant || 'info')">
|
:class="['gb-' + (block.variant || 'info'),
|
||||||
|
{ 'gb-sectiondone': inDoneSection(index) }]">
|
||||||
<strong v-if="block.title">{{ block.title }}</strong>
|
<strong v-if="block.title">{{ block.title }}</strong>
|
||||||
<div v-html="inline(block.text)"></div>
|
<div v-html="inline(block.text)"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="block.type === 'code'" class="gb-code">
|
<div v-else-if="block.type === 'code'" class="gb-code"
|
||||||
|
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||||
<div class="gb-codehead">
|
<div class="gb-codehead">
|
||||||
<span class="gb-lang">{{ block.language || 'text' }}</span>
|
<span class="gb-lang">{{ block.language || 'text' }}</span>
|
||||||
<span v-if="block.caption" class="gb-caption">{{ block.caption }}</span>
|
<span v-if="block.caption" class="gb-caption">{{ block.caption }}</span>
|
||||||
@@ -108,7 +115,8 @@
|
|||||||
A failure says so in words. A guide whose screenshot is missing must not
|
A failure says so in words. A guide whose screenshot is missing must not
|
||||||
look like a guide that never had one.
|
look like a guide that never had one.
|
||||||
-->
|
-->
|
||||||
<figure v-else-if="block.type === 'image'" class="gb-figure">
|
<figure v-else-if="block.type === 'image'" class="gb-figure"
|
||||||
|
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||||
<img v-if="imageState(block).src" :src="imageState(block).src"
|
<img v-if="imageState(block).src" :src="imageState(block).src"
|
||||||
:alt="block.alt || ''">
|
:alt="block.alt || ''">
|
||||||
<p v-else-if="imageState(block).failed" class="gb-imagefailed">
|
<p v-else-if="imageState(block).failed" class="gb-imagefailed">
|
||||||
@@ -118,7 +126,8 @@
|
|||||||
<figcaption v-if="block.caption">{{ block.caption }}</figcaption>
|
<figcaption v-if="block.caption">{{ block.caption }}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<div v-else-if="block.type === 'table'" class="gb-tablewrap">
|
<div v-else-if="block.type === 'table'" class="gb-tablewrap"
|
||||||
|
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||||
<table class="gb-table">
|
<table class="gb-table">
|
||||||
<thead v-if="block.header && block.header.length">
|
<thead v-if="block.header && block.header.length">
|
||||||
<tr><th v-for="(cell, c) in block.header" :key="c" v-html="inline(cell)"></th></tr>
|
<tr><th v-for="(cell, c) in block.header" :key="c" v-html="inline(cell)"></th></tr>
|
||||||
@@ -179,6 +188,42 @@ function toggle (itemid, checked) {
|
|||||||
emit('toggle', itemid, checked)
|
emit('toggle', itemid, checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which heading governs each block: the nearest one above it. Ticking a
|
||||||
|
// heading crosses off everything under it down to the next heading, because
|
||||||
|
// that is what "this section is done" means to somebody reading - striking the
|
||||||
|
// title alone leaves the wall of text it was introducing looking untouched.
|
||||||
|
// Headings nest. A block belongs to every heading above it that has not been
|
||||||
|
// closed by a heading of the same or higher rank, so ticking a top-level
|
||||||
|
// section crosses off its sub-sections too - which is what "this section is
|
||||||
|
// done" means to somebody who ticked the thing that contained them.
|
||||||
|
//
|
||||||
|
// A guide written before levels existed has none; those all read as level 2,
|
||||||
|
// which is the flat behaviour they already had.
|
||||||
|
function levelOf (block) {
|
||||||
|
const level = Number(block.level)
|
||||||
|
return Number.isFinite(level) && level >= 2 && level <= 6 ? level : 2
|
||||||
|
}
|
||||||
|
|
||||||
|
const ancestors = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
const stack = []
|
||||||
|
props.blocks.forEach((block, index) => {
|
||||||
|
if (block.type === 'heading') {
|
||||||
|
const level = levelOf(block)
|
||||||
|
while (stack.length && stack[stack.length - 1].level >= level) stack.pop()
|
||||||
|
map[index] = stack.map(entry => entry.id)
|
||||||
|
stack.push({ id: block.id, level })
|
||||||
|
} else {
|
||||||
|
map[index] = stack.map(entry => entry.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
function inDoneSection (index) {
|
||||||
|
return (ancestors.value[index] || []).some(id => isTicked(id))
|
||||||
|
}
|
||||||
|
|
||||||
// Steps number themselves by position among steps, so inserting a paragraph
|
// Steps number themselves by position among steps, so inserting a paragraph
|
||||||
// between two of them does not renumber anything by hand.
|
// between two of them does not renumber anything by hand.
|
||||||
const stepIndex = computed(() => {
|
const stepIndex = computed(() => {
|
||||||
@@ -215,11 +260,24 @@ function select (block) {
|
|||||||
dark mode work with no dark-mode code. */
|
dark mode work with no dark-mode code. */
|
||||||
.gb-blocks { line-height: 1.65; }
|
.gb-blocks { line-height: 1.65; }
|
||||||
.gb-heading { margin: 1.6rem 0 0.6rem; font-size: 1.35rem; color: var(--text); }
|
.gb-heading { margin: 1.6rem 0 0.6rem; font-size: 1.35rem; color: var(--text); }
|
||||||
|
/* Rank is visible, so a reader can see what contains what - and so ticking a
|
||||||
|
section off looks like it covered the right amount of page. */
|
||||||
|
.gb-h3 { font-size: 1.15rem; margin-top: 1.2rem; }
|
||||||
|
.gb-h4, .gb-h5, .gb-h6 { font-size: 1.02rem; margin-top: 1rem; }
|
||||||
|
|
||||||
/* A ticked section is struck through and faded, not hidden: somebody scanning
|
/* A ticked section is struck through and faded, not hidden: somebody scanning
|
||||||
back up the page has to be able to read what they already did. */
|
back up the page has to be able to read what they already did.
|
||||||
|
|
||||||
|
The strike lands on the heading and on everything under it. A code block is
|
||||||
|
faded but NOT struck - a line through a command makes it unreadable, and the
|
||||||
|
reason to look back at a finished step is usually to re-read the command. */
|
||||||
.gb-sectiondone { text-decoration: line-through; opacity: 0.55; }
|
.gb-sectiondone { text-decoration: line-through; opacity: 0.55; }
|
||||||
.gb-sectiondone .gb-stepnum { text-decoration: none; }
|
.gb-sectiondone .gb-stepnum { text-decoration: none; }
|
||||||
|
.gb-code.gb-sectiondone,
|
||||||
|
.gb-sectiondone pre,
|
||||||
|
.gb-sectiondone code,
|
||||||
|
.gb-figure.gb-sectiondone,
|
||||||
|
.gb-tablewrap.gb-sectiondone { text-decoration: none; }
|
||||||
.gb-sectiontick {
|
.gb-sectiontick {
|
||||||
/* Touch-sized: a guide is read on a shopfloor PC, often with gloves on. */
|
/* Touch-sized: a guide is read on a shopfloor PC, often with gloves on. */
|
||||||
width: 1.1rem;
|
width: 1.1rem;
|
||||||
|
|||||||
@@ -244,7 +244,12 @@ class _Builder:
|
|||||||
if tag in ('h2', 'h3', 'h4', 'h5'):
|
if tag in ('h2', 'h3', 'h4', 'h5'):
|
||||||
text = _inline(node)
|
text = _inline(node)
|
||||||
if text:
|
if text:
|
||||||
self._add({'type': 'heading', 'text': text})
|
# The LEVEL travels with it. Without it every heading is a
|
||||||
|
# sibling, and a reader who ticks a section off crosses out its
|
||||||
|
# title and nothing underneath - the sub-headings that belong to
|
||||||
|
# it look untouched, because nothing recorded that they belong.
|
||||||
|
self._add({'type': 'heading', 'level': int(tag[1]),
|
||||||
|
'text': text})
|
||||||
return
|
return
|
||||||
|
|
||||||
if tag == 'p':
|
if tag == 'p':
|
||||||
|
|||||||
@@ -212,3 +212,14 @@ def test_an_empty_document_produces_a_named_guide_and_no_blocks():
|
|||||||
parsed = parse('')
|
parsed = parse('')
|
||||||
assert parsed['title'] == 'Untitled'
|
assert parsed['title'] == 'Untitled'
|
||||||
assert parsed['blocks'] == []
|
assert parsed['blocks'] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_heading_carries_its_rank():
|
||||||
|
"""Ticking a section off has to cross out what belongs to it, and nothing
|
||||||
|
records that a sub-heading belongs to the heading above unless the level
|
||||||
|
travels with them."""
|
||||||
|
parsed = parse('<h1>T</h1><h2>Credentials</h2><p>Intro.</p>'
|
||||||
|
'<h3>SFLD share</h3><p>Detail.</p><h2>Next</h2>')
|
||||||
|
headings = [(b['text'], b.get('level'))
|
||||||
|
for b in parsed['blocks'] if b['type'] == 'heading']
|
||||||
|
assert headings == [('Credentials', 2), ('SFLD share', 3), ('Next', 2)]
|
||||||
|
|||||||
Reference in New Issue
Block a user