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
|
||||
|
||||
- **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
|
||||
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,
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
Reading only: in the editor a tick would be an edit.
|
||||
-->
|
||||
<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"
|
||||
:checked="isTicked(block.id)"
|
||||
:aria-label="'Mark this section done'"
|
||||
@@ -21,10 +22,11 @@
|
||||
</h2>
|
||||
|
||||
<p v-else-if="block.type === 'paragraph'" class="gb-para"
|
||||
:class="{ 'gb-sectiondone': inDoneSection(index) }"
|
||||
v-html="inline(block.text)"></p>
|
||||
|
||||
<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">
|
||||
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
|
||||
:checked="isTicked(block.id)"
|
||||
@@ -41,7 +43,8 @@
|
||||
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".
|
||||
-->
|
||||
<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">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
@@ -54,21 +57,25 @@
|
||||
</li>
|
||||
</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>
|
||||
</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>
|
||||
</ol>
|
||||
|
||||
<div v-else-if="block.type === 'callout'"
|
||||
class="gb-callout" :class="'gb-' + (block.variant || 'info')">
|
||||
<div v-else-if="block.type === 'callout'" class="gb-callout"
|
||||
:class="['gb-' + (block.variant || 'info'),
|
||||
{ 'gb-sectiondone': inDoneSection(index) }]">
|
||||
<strong v-if="block.title">{{ block.title }}</strong>
|
||||
<div v-html="inline(block.text)"></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">
|
||||
<span class="gb-lang">{{ block.language || 'text' }}</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
|
||||
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"
|
||||
:alt="block.alt || ''">
|
||||
<p v-else-if="imageState(block).failed" class="gb-imagefailed">
|
||||
@@ -118,7 +126,8 @@
|
||||
<figcaption v-if="block.caption">{{ block.caption }}</figcaption>
|
||||
</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">
|
||||
<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>
|
||||
@@ -179,6 +188,42 @@ function 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
|
||||
// between two of them does not renumber anything by hand.
|
||||
const stepIndex = computed(() => {
|
||||
@@ -215,11 +260,24 @@ function select (block) {
|
||||
dark mode work with no dark-mode code. */
|
||||
.gb-blocks { line-height: 1.65; }
|
||||
.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
|
||||
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 .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 {
|
||||
/* Touch-sized: a guide is read on a shopfloor PC, often with gloves on. */
|
||||
width: 1.1rem;
|
||||
|
||||
@@ -244,7 +244,12 @@ class _Builder:
|
||||
if tag in ('h2', 'h3', 'h4', 'h5'):
|
||||
text = _inline(node)
|
||||
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
|
||||
|
||||
if tag == 'p':
|
||||
|
||||
@@ -212,3 +212,14 @@ def test_an_empty_document_produces_a_named_guide_and_no_blocks():
|
||||
parsed = parse('')
|
||||
assert parsed['title'] == 'Untitled'
|
||||
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