-
-
Notifications
You must be signed in to change notification settings - Fork 25
/
reveal-link.ts
100 lines (80 loc) · 2.98 KB
/
reveal-link.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import {BodyContent} from '../../display/custom-elements/body-content';
import {MarginalContent} from '../../display/custom-elements/marginal-content';
import {passageNamed} from '../../story';
import {render} from '../render';
import {InlineButton} from './inline-button';
/**
* A link that changes its contents when clicked, either to inline source
* (specified by the `text` attribute) or the source of a passage in the story
* (specified by `passage`). If the resulting content includes paragraph breaks,
* it will try to stitch those into the surrounding content appropriately.
*
* Available as `<reveal-link>`.
*/
export class RevealLink extends InlineButton {
constructor() {
super();
this.addEventListener('click', () => {
let source = this.getAttribute('text');
const passageAttribute = this.getAttribute('passage');
if (passageAttribute) {
const passage = passageNamed(passageAttribute);
if (passage) {
source = passage.source;
}
}
if (source) {
const output = document.createElement('div');
// Need to trim() this to avoid spurious empty text nodes at the end.
output.innerHTML = render(source).trim();
// Output contains only block-level elements as its children. We know this
// because when we render source, it always comes wrapped in block
// elements; a bare link in a paragraphy by itself, for example, gets
// rendered with a <p> container by marked.
const parent: BodyContent | MarginalContent | null = this.closest(
'body-content, marginal-content'
);
if (!parent) {
throw new Error(
"Couldn't find suitable parent element to do a reveal link transition on."
);
}
parent.changeContent(() => {
const toInsert = output.children.length;
if (toInsert > 0 && this.parentNode) {
// Put the first child where the link was in the DOM. Set its display
// as inline to make it imitate the link's layout.
const firstInsert = document.createElement('span');
firstInsert.innerHTML = output.firstElementChild?.innerHTML ?? '';
this.parentNode.insertBefore(firstInsert, this);
output.firstElementChild?.remove();
if (
toInsert > 1 &&
this.parentNode.parentNode &&
output.lastChild
) {
// If there are other block elements, place them as siblings of the
// parent.
const lastInsert = output.lastChild;
while (output.lastChild) {
this.parentNode.parentNode.insertBefore(
output.lastChild,
this.parentNode.nextSibling
);
}
// Move any inline elements after the link we just expanded to the
// end of the last newly-inserted block element. Otherwise, they'll
// be sandwiched in by the new insert and order of content won't be
// preserved.
while (this.nextSibling) {
lastInsert.insertBefore(this.nextSibling, null);
}
}
}
// Remove ourselves from the DOM.
this.remove();
});
}
});
}
}