@mindful-web/marko-web-digital-edition 1.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/components/ad.marko +81 -0
- package/components/ad.marko.js +102 -0
- package/components/article-body.marko +134 -0
- package/components/article-body.marko.js +186 -0
- package/components/author.marko +30 -0
- package/components/author.marko.js +59 -0
- package/components/gate.marko +158 -0
- package/components/gate.marko.js +147 -0
- package/components/ld-json.marko +5 -0
- package/components/ld-json.marko.js +29 -0
- package/components/marko.json +78 -0
- package/components/reflow.marko +67 -0
- package/components/reflow.marko.js +106 -0
- package/components/replica.marko +86 -0
- package/components/replica.marko.js +128 -0
- package/components/scroll.marko +134 -0
- package/components/scroll.marko.js +23 -0
- package/components/theme.marko +73 -0
- package/components/theme.marko.js +30 -0
- package/components/toc.marko +28 -0
- package/components/toc.marko.js +53 -0
- package/components/toolbar.marko +47 -0
- package/components/toolbar.marko.js +54 -0
- package/components/tracking.marko +211 -0
- package/components/tracking.marko.js +60 -0
- package/lib/authors.js +125 -0
- package/lib/entities.js +97 -0
- package/lib/index.js +453 -0
- package/marko.json +6 -0
- package/package.json +33 -0
- package/routes/index.js +129 -0
- package/scss/index.scss +679 -0
- package/templates/article.marko +108 -0
- package/templates/article.marko.js +238 -0
- package/templates/hub.marko +94 -0
- package/templates/hub.marko.js +219 -0
- package/templates/partial.marko +17 -0
- package/templates/partial.marko.js +44 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { pageImageUrl, pageSrcset, pageAspect } from "../lib";
|
|
2
|
+
import { adEntity, entityAttr } from "../lib/entities";
|
|
3
|
+
|
|
4
|
+
$ const { manifest, page } = input;
|
|
5
|
+
$ // Hotspot regions carry only id/href/label; the ad item behind each one has the
|
|
6
|
+
$ // advertiser and company the entity needs.
|
|
7
|
+
$ const adItemFor = (r) => manifest.items.find((i) => i.id === r.id) || { id: r.id, advertiserName: r.label, landingUrl: r.href };
|
|
8
|
+
$ const canAccess = input.canAccess !== false;
|
|
9
|
+
$ const previewPages = input.previewPages || 6;
|
|
10
|
+
$ const pageCount = manifest.pdf.pageCount;
|
|
11
|
+
$ const aspect = pageAspect(manifest);
|
|
12
|
+
$ // Unentitled visitors get a cover-and-first-few-pages preview rather than nothing,
|
|
13
|
+
$ // so the "Print view" toggle stays honest for signed-out readers.
|
|
14
|
+
$ const pages = canAccess ? manifest.pages : manifest.pages.slice(0, previewPages);
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
Replica view: imgix-rendered page images with transparent anchors positioned over
|
|
18
|
+
the ad regions the ingest pipeline identified. Fractional bboxes become percentage
|
|
19
|
+
offsets, so a hotspot tracks the image at every breakpoint without JS.
|
|
20
|
+
|
|
21
|
+
Only the current page and its neighbours are eagerly loaded; the rest lazy-load as
|
|
22
|
+
the reader scrolls. The whole issue is 80 pages, so rendering all <img> tags up
|
|
23
|
+
front with loading="lazy" is cheaper than a JS pager and keeps deep links working
|
|
24
|
+
with no client code.
|
|
25
|
+
-->
|
|
26
|
+
<div
|
|
27
|
+
class="edition-replica"
|
|
28
|
+
data-edition-issue-id=manifest.issueId
|
|
29
|
+
data-edition-page-count=pageCount
|
|
30
|
+
data-edition-start-page=page
|
|
31
|
+
>
|
|
32
|
+
<div class="edition-replica__pages">
|
|
33
|
+
<for|p| of=pages>
|
|
34
|
+
$ const near = Math.abs(p.n - page) <= 1;
|
|
35
|
+
$ const hotspots = p.regions.filter((r) => r.href);
|
|
36
|
+
<figure class="edition-replica__page" id=`page-${p.n}` data-page=p.n>
|
|
37
|
+
<div class="edition-replica__frame" style=`aspect-ratio: ${aspect};`>
|
|
38
|
+
<img
|
|
39
|
+
class="edition-replica__img"
|
|
40
|
+
src=pageImageUrl(manifest, p.n, { w: near ? 1200 : 800 })
|
|
41
|
+
srcset=pageSrcset(manifest, p.n)
|
|
42
|
+
sizes="(min-width: 1100px) 1000px, 100vw"
|
|
43
|
+
alt=`${manifest.publication} ${manifest.name}, page ${p.folio}`
|
|
44
|
+
width=manifest.pdf.baseWidth
|
|
45
|
+
height=manifest.pdf.baseHeight
|
|
46
|
+
loading=(near ? "eager" : "lazy")
|
|
47
|
+
decoding="async"
|
|
48
|
+
/>
|
|
49
|
+
<for|r| of=hotspots>
|
|
50
|
+
$ const adItem = adItemFor(r);
|
|
51
|
+
$ const entity = adEntity(manifest, { ...adItem, printPage: p.n }, { placement: "replica" });
|
|
52
|
+
<!--
|
|
53
|
+
The hotspot doubles as the ad's tracking element in the replica: the
|
|
54
|
+
impression / active-view observer in tracking.marko picks up anything
|
|
55
|
+
with data-edition-ad-id, and the p1events link listener reads data-p1-*.
|
|
56
|
+
-->
|
|
57
|
+
<a
|
|
58
|
+
class="edition-replica__hotspot"
|
|
59
|
+
href=r.href
|
|
60
|
+
target="_blank"
|
|
61
|
+
rel="nofollow noopener sponsored"
|
|
62
|
+
title=(r.label || "Advertisement")
|
|
63
|
+
aria-label=`Advertisement: ${r.label || "advertiser"}`
|
|
64
|
+
style=`left:${(r.bbox[0] * 100).toFixed(3)}%;top:${(r.bbox[1] * 100).toFixed(3)}%;width:${(r.bbox[2] * 100).toFixed(3)}%;height:${(r.bbox[3] * 100).toFixed(3)}%;`
|
|
65
|
+
data-edition-ad-id=r.id
|
|
66
|
+
data-edition-page=p.n
|
|
67
|
+
data-edition-placement="replica"
|
|
68
|
+
data-advertiser=(r.label || "")
|
|
69
|
+
data-edition-entity=JSON.stringify(entity)
|
|
70
|
+
data-p1-category="Digital Edition Ad Unit"
|
|
71
|
+
data-p1-action="Click"
|
|
72
|
+
data-p1-label=(r.label || "")
|
|
73
|
+
data-p1-entity=entityAttr(entity)
|
|
74
|
+
></a>
|
|
75
|
+
</for>
|
|
76
|
+
</div>
|
|
77
|
+
<figcaption class="edition-replica__folio">Page ${p.folio}</figcaption>
|
|
78
|
+
</figure>
|
|
79
|
+
</for>
|
|
80
|
+
<if(!canAccess && pageCount > pages.length)>
|
|
81
|
+
<p class="edition-replica__more">
|
|
82
|
+
Showing ${pages.length} of ${pageCount} pages.
|
|
83
|
+
</p>
|
|
84
|
+
</if>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Compiled using marko@4.20.2 - DO NOT EDIT
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/replica.marko",
|
|
6
|
+
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
|
+
module_lib_module = require("../lib"),
|
|
8
|
+
lib_module = module_lib_module.default || module_lib_module,
|
|
9
|
+
pageImageUrl = module_lib_module.pageImageUrl,
|
|
10
|
+
pageSrcset = module_lib_module.pageSrcset,
|
|
11
|
+
pageAspect = module_lib_module.pageAspect,
|
|
12
|
+
module_entities_module = require("../lib/entities"),
|
|
13
|
+
entities_module = module_entities_module.default || module_entities_module,
|
|
14
|
+
adEntity = module_entities_module.adEntity,
|
|
15
|
+
entityAttr = module_entities_module.entityAttr,
|
|
16
|
+
marko_forOf = require("marko/dist/runtime/helpers/for-of"),
|
|
17
|
+
marko_attr = require("marko/dist/runtime/html/helpers/attr"),
|
|
18
|
+
marko_styleAttr = require("marko/dist/runtime/html/helpers/style-attr"),
|
|
19
|
+
helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
|
|
20
|
+
marko_escapeXml = helpers_escape_xml.x;
|
|
21
|
+
|
|
22
|
+
function render(input, out, __component, component, state) {
|
|
23
|
+
var data = input;
|
|
24
|
+
|
|
25
|
+
const { manifest, page } = input;
|
|
26
|
+
|
|
27
|
+
// Hotspot regions carry only id/href/label; the ad item behind each one has the
|
|
28
|
+
|
|
29
|
+
// advertiser and company the entity needs.
|
|
30
|
+
|
|
31
|
+
const adItemFor = (r) => manifest.items.find((i) => i.id === r.id) || { id: r.id, advertiserName: r.label, landingUrl: r.href };
|
|
32
|
+
|
|
33
|
+
const canAccess = input.canAccess !== false;
|
|
34
|
+
|
|
35
|
+
const previewPages = input.previewPages || 6;
|
|
36
|
+
|
|
37
|
+
const pageCount = manifest.pdf.pageCount;
|
|
38
|
+
|
|
39
|
+
const aspect = pageAspect(manifest);
|
|
40
|
+
|
|
41
|
+
// Unentitled visitors get a cover-and-first-few-pages preview rather than nothing,
|
|
42
|
+
|
|
43
|
+
// so the "Print view" toggle stays honest for signed-out readers.
|
|
44
|
+
|
|
45
|
+
const pages = canAccess ? manifest.pages : manifest.pages.slice(0, previewPages);
|
|
46
|
+
|
|
47
|
+
out.w("<div class=\"edition-replica\"" +
|
|
48
|
+
marko_attr("data-edition-issue-id", manifest.issueId) +
|
|
49
|
+
marko_attr("data-edition-page-count", pageCount) +
|
|
50
|
+
marko_attr("data-edition-start-page", page) +
|
|
51
|
+
"><div class=\"edition-replica__pages\">");
|
|
52
|
+
|
|
53
|
+
var $for$0 = 0;
|
|
54
|
+
|
|
55
|
+
marko_forOf(pages, function(p) {
|
|
56
|
+
const near = Math.abs(p.n - page) <= 1;
|
|
57
|
+
|
|
58
|
+
const hotspots = p.regions.filter((r) => r.href);
|
|
59
|
+
|
|
60
|
+
var $keyScope$0 = "[" + (($for$0++) + "]");
|
|
61
|
+
|
|
62
|
+
out.w("<figure class=\"edition-replica__page\"" +
|
|
63
|
+
marko_attr("id", "page-" + p.n) +
|
|
64
|
+
marko_attr("data-page", p.n) +
|
|
65
|
+
"><div class=\"edition-replica__frame\"" +
|
|
66
|
+
marko_styleAttr(("aspect-ratio: " + aspect) + ";") +
|
|
67
|
+
"><img class=\"edition-replica__img\"" +
|
|
68
|
+
marko_attr("src", pageImageUrl(manifest, p.n, {
|
|
69
|
+
w: near ? 1200 : 800
|
|
70
|
+
})) +
|
|
71
|
+
marko_attr("srcset", pageSrcset(manifest, p.n)) +
|
|
72
|
+
" sizes=\"(min-width: 1100px) 1000px, 100vw\"" +
|
|
73
|
+
marko_attr("alt", (((manifest.publication + " ") + manifest.name) + ", page ") + p.folio) +
|
|
74
|
+
marko_attr("width", manifest.pdf.baseWidth) +
|
|
75
|
+
marko_attr("height", manifest.pdf.baseHeight) +
|
|
76
|
+
marko_attr("loading", near ? "eager" : "lazy") +
|
|
77
|
+
" decoding=\"async\">");
|
|
78
|
+
|
|
79
|
+
var $for$1 = 0;
|
|
80
|
+
|
|
81
|
+
marko_forOf(hotspots, function(r) {
|
|
82
|
+
const adItem = adItemFor(r);
|
|
83
|
+
|
|
84
|
+
const entity = adEntity(manifest, { ...adItem, printPage: p.n }, { placement: "replica" });
|
|
85
|
+
|
|
86
|
+
var $keyScope$1 = "[" + ((($for$1++) + $keyScope$0) + "]");
|
|
87
|
+
|
|
88
|
+
out.w("<a class=\"edition-replica__hotspot\"" +
|
|
89
|
+
marko_attr("href", r.href) +
|
|
90
|
+
" target=\"_blank\" rel=\"nofollow noopener sponsored\"" +
|
|
91
|
+
marko_attr("title", r.label || "Advertisement") +
|
|
92
|
+
marko_attr("aria-label", "Advertisement: " + (r.label || "advertiser")) +
|
|
93
|
+
marko_styleAttr(((((((("left:" + (r.bbox[0] * 100).toFixed(3)) + "%;top:") + (r.bbox[1] * 100).toFixed(3)) + "%;width:") + (r.bbox[2] * 100).toFixed(3)) + "%;height:") + (r.bbox[3] * 100).toFixed(3)) + "%;") +
|
|
94
|
+
marko_attr("data-edition-ad-id", r.id) +
|
|
95
|
+
marko_attr("data-edition-page", p.n) +
|
|
96
|
+
" data-edition-placement=\"replica\"" +
|
|
97
|
+
marko_attr("data-advertiser", r.label || "") +
|
|
98
|
+
marko_attr("data-edition-entity", JSON.stringify(entity)) +
|
|
99
|
+
" data-p1-category=\"Digital Edition Ad Unit\" data-p1-action=\"Click\"" +
|
|
100
|
+
marko_attr("data-p1-label", r.label || "") +
|
|
101
|
+
marko_attr("data-p1-entity", entityAttr(entity)) +
|
|
102
|
+
"></a>");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
out.w("</div><figcaption class=\"edition-replica__folio\">Page " +
|
|
106
|
+
marko_escapeXml(p.folio) +
|
|
107
|
+
"</figcaption></figure>");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if ((!canAccess) && (pageCount > pages.length)) {
|
|
111
|
+
out.w("<p class=\"edition-replica__more\">Showing " +
|
|
112
|
+
marko_escapeXml(pages.length) +
|
|
113
|
+
" of " +
|
|
114
|
+
marko_escapeXml(pageCount) +
|
|
115
|
+
" pages.</p>");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
out.w("</div></div>");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
marko_template._ = marko_renderer(render, {
|
|
122
|
+
d_: true,
|
|
123
|
+
e_: marko_componentType
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
marko_template.meta = {
|
|
127
|
+
id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/replica.marko"
|
|
128
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
$ const { manifest } = input;
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
Continuous-scroll reader for the hub.
|
|
5
|
+
|
|
6
|
+
Progressive enhancement only: the hub is complete and fully navigable without this.
|
|
7
|
+
When it runs, it appends the next article as an HTML fragment (`?partial=1`) as the
|
|
8
|
+
reader nears the end, and pushStates to that article's real URL — the same URL that
|
|
9
|
+
serves the article standalone, so what the reader sees and what a crawler indexes
|
|
10
|
+
never diverge.
|
|
11
|
+
|
|
12
|
+
History: each appended article gets one pushState. Scrolling back up replaces the
|
|
13
|
+
URL (no new history entries) so Back leaves the reader rather than stepping through
|
|
14
|
+
every article the reader passed.
|
|
15
|
+
-->
|
|
16
|
+
<script>
|
|
17
|
+
$!{`
|
|
18
|
+
(function () {
|
|
19
|
+
var root = document.querySelector('.edition-reflow');
|
|
20
|
+
if (!root || !window.IntersectionObserver || !window.fetch || !history.pushState) return;
|
|
21
|
+
|
|
22
|
+
var flow = root.querySelector('[data-edition-flow]');
|
|
23
|
+
var queueEl = root.querySelector('[data-edition-queue]');
|
|
24
|
+
if (!flow || !queueEl) return;
|
|
25
|
+
|
|
26
|
+
var queue = [].map.call(
|
|
27
|
+
queueEl.querySelectorAll('[data-edition-queue-slug]'),
|
|
28
|
+
function (a) { return { slug: a.getAttribute('data-edition-queue-slug'), href: a.getAttribute('href') }; }
|
|
29
|
+
);
|
|
30
|
+
if (!queue.length) return;
|
|
31
|
+
|
|
32
|
+
// The queue is the no-JS fallback; once we take over, hide it.
|
|
33
|
+
queueEl.hidden = true;
|
|
34
|
+
|
|
35
|
+
var loading = false;
|
|
36
|
+
var done = false;
|
|
37
|
+
var current = null;
|
|
38
|
+
|
|
39
|
+
// An article entering the reader counts as a View of that article — the same
|
|
40
|
+
// event the standalone article page fires on load (see tracking.marko), with the
|
|
41
|
+
// entity the server stamped on the article element.
|
|
42
|
+
function track(el, href) {
|
|
43
|
+
if (!window.p1events) return;
|
|
44
|
+
var entity = null;
|
|
45
|
+
try { entity = JSON.parse(el.getAttribute('data-edition-entity')); } catch (e) { return; }
|
|
46
|
+
if (!entity) return;
|
|
47
|
+
p1events('track', {
|
|
48
|
+
action: 'View',
|
|
49
|
+
category: 'Digital Edition Article',
|
|
50
|
+
label: el.getAttribute('data-edition-article-slug') || '',
|
|
51
|
+
entity: entity,
|
|
52
|
+
props: { path: href, via: 'scroll' }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setUrl(el, href, slug, push) {
|
|
57
|
+
if (current === slug) return;
|
|
58
|
+
current = slug;
|
|
59
|
+
try { history[push ? 'pushState' : 'replaceState']({ slug: slug }, '', href); } catch (e) { return; }
|
|
60
|
+
if (push) track(el, href);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Report the article currently filling the viewport.
|
|
64
|
+
var seen = Object.create(null);
|
|
65
|
+
var spy = new IntersectionObserver(function (entries) {
|
|
66
|
+
entries.forEach(function (e) {
|
|
67
|
+
if (!e.isIntersecting) return;
|
|
68
|
+
var el = e.target;
|
|
69
|
+
var slug = el.getAttribute('data-edition-article-slug');
|
|
70
|
+
var href = el.getAttribute('data-edition-article-href');
|
|
71
|
+
if (!slug || !href) return;
|
|
72
|
+
var isNew = !seen[slug];
|
|
73
|
+
seen[slug] = 1;
|
|
74
|
+
setUrl(el, href, slug, isNew);
|
|
75
|
+
});
|
|
76
|
+
}, { threshold: 0.25, rootMargin: '-20% 0px -60% 0px' });
|
|
77
|
+
|
|
78
|
+
[].forEach.call(flow.querySelectorAll('[data-edition-article-slug]'), function (el) { spy.observe(el); });
|
|
79
|
+
|
|
80
|
+
function appendNext() {
|
|
81
|
+
if (loading || done) return;
|
|
82
|
+
var next = queue.shift();
|
|
83
|
+
if (!next) { done = true; return; }
|
|
84
|
+
loading = true;
|
|
85
|
+
var url = next.href + (next.href.indexOf('?') === -1 ? '?' : '&') + 'partial=1';
|
|
86
|
+
fetch(url, { credentials: 'same-origin', headers: { 'x-requested-with': 'fetch' } })
|
|
87
|
+
.then(function (r) { return r.ok ? r.text() : Promise.reject(new Error(String(r.status))); })
|
|
88
|
+
.then(function (html) {
|
|
89
|
+
var holder = document.createElement('div');
|
|
90
|
+
holder.innerHTML = html;
|
|
91
|
+
// Appending removes the node from the holder, so iterating a live
|
|
92
|
+
// HTMLCollection with forEach skips every other child — drain instead.
|
|
93
|
+
while (holder.firstElementChild) {
|
|
94
|
+
var child = holder.firstElementChild;
|
|
95
|
+
flow.appendChild(child);
|
|
96
|
+
if (child.getAttribute && child.getAttribute('data-edition-article-slug')) spy.observe(child);
|
|
97
|
+
}
|
|
98
|
+
// Let the impression tracker pick up the ads inside the new article.
|
|
99
|
+
try { document.dispatchEvent(new CustomEvent('edition:content-appended')); } catch (e) { /* older browsers: impressions only */ }
|
|
100
|
+
loading = false;
|
|
101
|
+
// Keep going if the sentinel is still on screen after this insert.
|
|
102
|
+
if (sentinelVisible) appendNext();
|
|
103
|
+
})
|
|
104
|
+
.catch(function () {
|
|
105
|
+
// Put it back and stand down; the visible queue link still works.
|
|
106
|
+
queue.unshift(next);
|
|
107
|
+
loading = false;
|
|
108
|
+
done = true;
|
|
109
|
+
queueEl.hidden = false;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var sentinel = document.createElement('div');
|
|
114
|
+
sentinel.className = 'edition-reflow__sentinel';
|
|
115
|
+
sentinel.setAttribute('aria-hidden', 'true');
|
|
116
|
+
flow.parentNode.insertBefore(sentinel, queueEl);
|
|
117
|
+
|
|
118
|
+
var sentinelVisible = false;
|
|
119
|
+
new IntersectionObserver(function (entries) {
|
|
120
|
+
sentinelVisible = entries[0].isIntersecting;
|
|
121
|
+
if (sentinelVisible) appendNext();
|
|
122
|
+
}, { rootMargin: '600px 0px' }).observe(sentinel);
|
|
123
|
+
|
|
124
|
+
// Clicking a contents or queue link should behave normally (a real navigation) —
|
|
125
|
+
// only the scroll position drives the reader.
|
|
126
|
+
window.addEventListener('popstate', function (e) {
|
|
127
|
+
var slug = e.state && e.state.slug;
|
|
128
|
+
if (!slug) return;
|
|
129
|
+
var el = flow.querySelector('[data-edition-article-slug="' + slug + '"]');
|
|
130
|
+
if (el) { current = slug; el.scrollIntoView(); }
|
|
131
|
+
});
|
|
132
|
+
}());
|
|
133
|
+
`}
|
|
134
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Compiled using marko@4.20.2 - DO NOT EDIT
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/scroll.marko",
|
|
6
|
+
marko_renderer = require("marko/dist/runtime/components/renderer");
|
|
7
|
+
|
|
8
|
+
function render(input, out, __component, component, state) {
|
|
9
|
+
var data = input;
|
|
10
|
+
|
|
11
|
+
const { manifest } = input;
|
|
12
|
+
|
|
13
|
+
out.w("<script>\n \n (function () {\n var root = document.querySelector('.edition-reflow');\n if (!root || !window.IntersectionObserver || !window.fetch || !history.pushState) return;\n\n var flow = root.querySelector('[data-edition-flow]');\n var queueEl = root.querySelector('[data-edition-queue]');\n if (!flow || !queueEl) return;\n\n var queue = [].map.call(\n queueEl.querySelectorAll('[data-edition-queue-slug]'),\n function (a) { return { slug: a.getAttribute('data-edition-queue-slug'), href: a.getAttribute('href') }; }\n );\n if (!queue.length) return;\n\n // The queue is the no-JS fallback; once we take over, hide it.\n queueEl.hidden = true;\n\n var loading = false;\n var done = false;\n var current = null;\n\n // An article entering the reader counts as a View of that article — the same\n // event the standalone article page fires on load (see tracking.marko), with the\n // entity the server stamped on the article element.\n function track(el, href) {\n if (!window.p1events) return;\n var entity = null;\n try { entity = JSON.parse(el.getAttribute('data-edition-entity')); } catch (e) { return; }\n if (!entity) return;\n p1events('track', {\n action: 'View',\n category: 'Digital Edition Article',\n label: el.getAttribute('data-edition-article-slug') || '',\n entity: entity,\n props: { path: href, via: 'scroll' }\n });\n }\n\n function setUrl(el, href, slug, push) {\n if (current === slug) return;\n current = slug;\n try { history[push ? 'pushState' : 'replaceState']({ slug: slug }, '', href); } catch (e) { return; }\n if (push) track(el, href);\n }\n\n // Report the article currently filling the viewport.\n var seen = Object.create(null);\n var spy = new IntersectionObserver(function (entries) {\n entries.forEach(function (e) {\n if (!e.isIntersecting) return;\n var el = e.target;\n var slug = el.getAttribute('data-edition-article-slug');\n var href = el.getAttribute('data-edition-article-href');\n if (!slug || !href) return;\n var isNew = !seen[slug];\n seen[slug] = 1;\n setUrl(el, href, slug, isNew);\n });\n }, { threshold: 0.25, rootMargin: '-20% 0px -60% 0px' });\n\n [].forEach.call(flow.querySelectorAll('[data-edition-article-slug]'), function (el) { spy.observe(el); });\n\n function appendNext() {\n if (loading || done) return;\n var next = queue.shift();\n if (!next) { done = true; return; }\n loading = true;\n var url = next.href + (next.href.indexOf('?') === -1 ? '?' : '&') + 'partial=1';\n fetch(url, { credentials: 'same-origin', headers: { 'x-requested-with': 'fetch' } })\n .then(function (r) { return r.ok ? r.text() : Promise.reject(new Error(String(r.status))); })\n .then(function (html) {\n var holder = document.createElement('div');\n holder.innerHTML = html;\n // Appending removes the node from the holder, so iterating a live\n // HTMLCollection with forEach skips every other child — drain instead.\n while (holder.firstElementChild) {\n var child = holder.firstElementChild;\n flow.appendChild(child);\n if (child.getAttribute && child.getAttribute('data-edition-article-slug')) spy.observe(child);\n }\n // Let the impression tracker pick up the ads inside the new article.\n try { document.dispatchEvent(new CustomEvent('edition:content-appended')); } catch (e) { /* older browsers: impressions only */ }\n loading = false;\n // Keep going if the sentinel is still on screen after this insert.\n if (sentinelVisible) appendNext();\n })\n .catch(function () {\n // Put it back and stand down; the visible queue link still works.\n queue.unshift(next);\n loading = false;\n done = true;\n queueEl.hidden = false;\n });\n }\n\n var sentinel = document.createElement('div');\n sentinel.className = 'edition-reflow__sentinel';\n sentinel.setAttribute('aria-hidden', 'true');\n flow.parentNode.insertBefore(sentinel, queueEl);\n\n var sentinelVisible = false;\n new IntersectionObserver(function (entries) {\n sentinelVisible = entries[0].isIntersecting;\n if (sentinelVisible) appendNext();\n }, { rootMargin: '600px 0px' }).observe(sentinel);\n\n // Clicking a contents or queue link should behave normally (a real navigation) —\n // only the scroll position drives the reader.\n window.addEventListener('popstate', function (e) {\n var slug = e.state && e.state.slug;\n if (!slug) return;\n var el = flow.querySelector('[data-edition-article-slug=\"' + slug + '\"]');\n if (el) { current = slug; el.scrollIntoView(); }\n });\n }());\n \n</script>");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
marko_template._ = marko_renderer(render, {
|
|
17
|
+
d_: true,
|
|
18
|
+
e_: marko_componentType
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
marko_template.meta = {
|
|
22
|
+
id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/scroll.marko"
|
|
23
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
$ const defaultTheme = input.defaultTheme || "dark";
|
|
2
|
+
$ const view = input.view === "print" ? "print" : "read";
|
|
3
|
+
|
|
4
|
+
<!--
|
|
5
|
+
Reader theme.
|
|
6
|
+
|
|
7
|
+
Rendered into the page <head> so the attribute is set before first paint — a
|
|
8
|
+
server-rendered default plus a client-stored override cannot be done without an
|
|
9
|
+
inline script, and doing it in the body would flash the wrong theme.
|
|
10
|
+
|
|
11
|
+
BOTH views default to DARK, deliberately: the toggle is offered in both, and the
|
|
12
|
+
stored preference is shared, so a per-view default would flip the theme on a
|
|
13
|
+
first-time visitor who merely switched Read -> Print without touching the toggle.
|
|
14
|
+
In the replica, dark mode darkens ONLY the canvas the page images float on —
|
|
15
|
+
the chrome keeps the light treatment, which is how a PDF reader behaves. The
|
|
16
|
+
CSS needs to know the view to scope that, so the same pre-paint script also
|
|
17
|
+
stamps `data-edition-view` on the root.
|
|
18
|
+
|
|
19
|
+
An explicit choice is remembered per viewer and wins over the default.
|
|
20
|
+
localStorage is wrapped because it throws outright in some contexts (private
|
|
21
|
+
windows, blocked site data) rather than just returning null.
|
|
22
|
+
|
|
23
|
+
The click handler is delegated from `document`, so it is safe to attach here
|
|
24
|
+
before the toolbar button exists.
|
|
25
|
+
-->
|
|
26
|
+
<script>
|
|
27
|
+
$!{`
|
|
28
|
+
(function () {
|
|
29
|
+
var KEY = 'edition:theme';
|
|
30
|
+
var DEFAULT = ${JSON.stringify(defaultTheme)};
|
|
31
|
+
var root = document.documentElement;
|
|
32
|
+
root.setAttribute('data-edition-view', ${JSON.stringify(view)});
|
|
33
|
+
|
|
34
|
+
function read() {
|
|
35
|
+
try { return window.localStorage.getItem(KEY); } catch (e) { return null; }
|
|
36
|
+
}
|
|
37
|
+
function write(v) {
|
|
38
|
+
try { window.localStorage.setItem(KEY, v); } catch (e) { /* non-persistent is fine */ }
|
|
39
|
+
}
|
|
40
|
+
function apply(theme) {
|
|
41
|
+
root.setAttribute('data-edition-theme', theme);
|
|
42
|
+
var btn = document.querySelector('[data-edition-theme-toggle]');
|
|
43
|
+
if (btn) {
|
|
44
|
+
btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var stored = read();
|
|
49
|
+
apply(stored === 'dark' || stored === 'light' ? stored : DEFAULT);
|
|
50
|
+
|
|
51
|
+
document.addEventListener('click', function (e) {
|
|
52
|
+
var btn = e.target && e.target.closest && e.target.closest('[data-edition-theme-toggle]');
|
|
53
|
+
if (!btn) return;
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
var next = root.getAttribute('data-edition-theme') === 'dark' ? 'light' : 'dark';
|
|
56
|
+
apply(next);
|
|
57
|
+
write(next);
|
|
58
|
+
if (window.p1events) {
|
|
59
|
+
p1events('track', {
|
|
60
|
+
action: 'Theme Toggle',
|
|
61
|
+
category: 'Digital Edition',
|
|
62
|
+
label: next,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Label the button correctly once it exists, even if the theme never changes.
|
|
68
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
69
|
+
apply(root.getAttribute('data-edition-theme') || DEFAULT);
|
|
70
|
+
});
|
|
71
|
+
}());
|
|
72
|
+
`}
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Compiled using marko@4.20.2 - DO NOT EDIT
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/theme.marko",
|
|
6
|
+
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
|
+
helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
|
|
8
|
+
marko_escapeXml = helpers_escape_xml.x,
|
|
9
|
+
marko_escapeScript = require("marko/dist/runtime/html/helpers/escape-script-placeholder");
|
|
10
|
+
|
|
11
|
+
function render(input, out, __component, component, state) {
|
|
12
|
+
var data = input;
|
|
13
|
+
|
|
14
|
+
const defaultTheme = input.defaultTheme || "dark";
|
|
15
|
+
|
|
16
|
+
const view = input.view === "print" ? "print" : "read";
|
|
17
|
+
|
|
18
|
+
out.w("<script>" +
|
|
19
|
+
marko_escapeScript(("\n " + (((("\n (function () {\n var KEY = 'edition:theme';\n var DEFAULT = " + JSON.stringify(defaultTheme)) + ";\n var root = document.documentElement;\n root.setAttribute('data-edition-view', ") + JSON.stringify(view)) + ");\n\n function read() {\n try { return window.localStorage.getItem(KEY); } catch (e) { return null; }\n }\n function write(v) {\n try { window.localStorage.setItem(KEY, v); } catch (e) { /* non-persistent is fine */ }\n }\n function apply(theme) {\n root.setAttribute('data-edition-theme', theme);\n var btn = document.querySelector('[data-edition-theme-toggle]');\n if (btn) {\n btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');\n }\n }\n\n var stored = read();\n apply(stored === 'dark' || stored === 'light' ? stored : DEFAULT);\n\n document.addEventListener('click', function (e) {\n var btn = e.target && e.target.closest && e.target.closest('[data-edition-theme-toggle]');\n if (!btn) return;\n e.preventDefault();\n var next = root.getAttribute('data-edition-theme') === 'dark' ? 'light' : 'dark';\n apply(next);\n write(next);\n if (window.p1events) {\n p1events('track', {\n action: 'Theme Toggle',\n category: 'Digital Edition',\n label: next,\n });\n }\n });\n\n // Label the button correctly once it exists, even if the theme never changes.\n document.addEventListener('DOMContentLoaded', function () {\n apply(root.getAttribute('data-edition-theme') || DEFAULT);\n });\n }());\n ")) + "\n") +
|
|
20
|
+
"</script>");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
marko_template._ = marko_renderer(render, {
|
|
24
|
+
d_: true,
|
|
25
|
+
e_: marko_componentType
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
marko_template.meta = {
|
|
29
|
+
id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/theme.marko"
|
|
30
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { editionPath } from "../lib";
|
|
2
|
+
|
|
3
|
+
$ const { manifest, items, currentSlug } = input;
|
|
4
|
+
|
|
5
|
+
<!--
|
|
6
|
+
Contents. These are real links to per-article URLs, which is what makes each story
|
|
7
|
+
crawlable — the scroll reader intercepts them for a smoother read, but they work
|
|
8
|
+
fully without JS.
|
|
9
|
+
-->
|
|
10
|
+
<nav class="edition-toc" aria-label="In this issue">
|
|
11
|
+
<h2 class="edition-toc__title">In this issue</h2>
|
|
12
|
+
<ol class="edition-toc__list">
|
|
13
|
+
<for|item| of=items>
|
|
14
|
+
$ const href = editionPath(manifest.issueId, { slug: item.slug });
|
|
15
|
+
$ const isCurrent = currentSlug && item.slug === currentSlug;
|
|
16
|
+
<li class="edition-toc__item">
|
|
17
|
+
<a
|
|
18
|
+
class=`edition-toc__link ${isCurrent ? "is-current" : ""}`
|
|
19
|
+
href=href
|
|
20
|
+
aria-current=(isCurrent ? "page" : null)
|
|
21
|
+
>
|
|
22
|
+
<span class="edition-toc__headline">${item.headline || "Untitled"}</span>
|
|
23
|
+
<span class="edition-toc__page">p. ${item.printPage}</span>
|
|
24
|
+
</a>
|
|
25
|
+
</li>
|
|
26
|
+
</for>
|
|
27
|
+
</ol>
|
|
28
|
+
</nav>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Compiled using marko@4.20.2 - DO NOT EDIT
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/toc.marko",
|
|
6
|
+
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
|
+
module_lib_module = require("../lib"),
|
|
8
|
+
lib_module = module_lib_module.default || module_lib_module,
|
|
9
|
+
editionPath = module_lib_module.editionPath,
|
|
10
|
+
marko_forOf = require("marko/dist/runtime/helpers/for-of"),
|
|
11
|
+
helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
|
|
12
|
+
marko_escapeXml = helpers_escape_xml.x,
|
|
13
|
+
marko_classAttr = require("marko/dist/runtime/html/helpers/class-attr"),
|
|
14
|
+
marko_attr = require("marko/dist/runtime/html/helpers/attr");
|
|
15
|
+
|
|
16
|
+
function render(input, out, __component, component, state) {
|
|
17
|
+
var data = input;
|
|
18
|
+
|
|
19
|
+
const { manifest, items, currentSlug } = input;
|
|
20
|
+
|
|
21
|
+
out.w("<nav class=\"edition-toc\" aria-label=\"In this issue\"><h2 class=\"edition-toc__title\">In this issue</h2><ol class=\"edition-toc__list\">");
|
|
22
|
+
|
|
23
|
+
var $for$0 = 0;
|
|
24
|
+
|
|
25
|
+
marko_forOf(items, function(item) {
|
|
26
|
+
const href = editionPath(manifest.issueId, { slug: item.slug });
|
|
27
|
+
|
|
28
|
+
const isCurrent = currentSlug && item.slug === currentSlug;
|
|
29
|
+
|
|
30
|
+
var $keyScope$0 = "[" + (($for$0++) + "]");
|
|
31
|
+
|
|
32
|
+
out.w("<li class=\"edition-toc__item\"><a" +
|
|
33
|
+
marko_classAttr("edition-toc__link " + (isCurrent ? "is-current" : "")) +
|
|
34
|
+
marko_attr("href", href) +
|
|
35
|
+
marko_attr("aria-current", isCurrent ? "page" : null) +
|
|
36
|
+
"><span class=\"edition-toc__headline\">" +
|
|
37
|
+
marko_escapeXml(item.headline || "Untitled") +
|
|
38
|
+
"</span><span class=\"edition-toc__page\">p. " +
|
|
39
|
+
marko_escapeXml(item.printPage) +
|
|
40
|
+
"</span></a></li>");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
out.w("</ol></nav>");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
marko_template._ = marko_renderer(render, {
|
|
47
|
+
d_: true,
|
|
48
|
+
e_: marko_componentType
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
marko_template.meta = {
|
|
52
|
+
id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/toc.marko"
|
|
53
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { editionPath } from "../lib";
|
|
2
|
+
|
|
3
|
+
$ const { manifest, view } = input;
|
|
4
|
+
$ const readHref = editionPath(manifest.issueId);
|
|
5
|
+
$ const printHref = editionPath(manifest.issueId, { view: "print" });
|
|
6
|
+
$ const showThemeToggle = input.showThemeToggle !== false;
|
|
7
|
+
|
|
8
|
+
<div class="edition-toolbar">
|
|
9
|
+
<div class="edition-toolbar__title">
|
|
10
|
+
<span class="edition-toolbar__pub">${manifest.publication}</span>
|
|
11
|
+
<span class="edition-toolbar__issue">${manifest.name}</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="edition-toolbar__actions">
|
|
14
|
+
<if(showThemeToggle)>
|
|
15
|
+
<!--
|
|
16
|
+
Both labels are rendered server-side and CSS shows the one matching the
|
|
17
|
+
active theme, so there is no flash of the wrong label and the button needs
|
|
18
|
+
no state of its own. The click handler lives in theme.marko.
|
|
19
|
+
-->
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="edition-toolbar__theme"
|
|
23
|
+
data-edition-theme-toggle
|
|
24
|
+
aria-live="polite"
|
|
25
|
+
>
|
|
26
|
+
<span class="edition-toolbar__theme__icon" aria-hidden="true">
|
|
27
|
+
<span data-when="dark">☀</span><span data-when="light">☾</span>
|
|
28
|
+
</span>
|
|
29
|
+
<span class="edition-toolbar__theme__label">
|
|
30
|
+
<span data-when="dark">Light</span><span data-when="light">Dark</span>
|
|
31
|
+
</span>
|
|
32
|
+
</button>
|
|
33
|
+
</if>
|
|
34
|
+
<nav class="edition-toolbar__views" aria-label="Reading view">
|
|
35
|
+
<a
|
|
36
|
+
class=`edition-toolbar__view ${view === "read" ? "is-active" : ""}`
|
|
37
|
+
href=readHref
|
|
38
|
+
aria-current=(view === "read" ? "page" : null)
|
|
39
|
+
>Read</a>
|
|
40
|
+
<a
|
|
41
|
+
class=`edition-toolbar__view ${view === "print" ? "is-active" : ""}`
|
|
42
|
+
href=printHref
|
|
43
|
+
aria-current=(view === "print" ? "page" : null)
|
|
44
|
+
>Print view</a>
|
|
45
|
+
</nav>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|