PageSourceSearch

https://reader.exacteditions.com/assets/reader/core/IssueViewController-7ce634f1.js

js exacteditions.com collected 2026-10-01 10:18:51 UTC 11,465 bytes, 397 lines download raw bytes

1import StackConstants from 'reader/core/stackers/StackConstants';
2import MonthStacker from 'reader/core/stackers/MonthStacker';
3import IssueStackKey from 'reader/core/stackers/IssueStackKey';
4import TitleStacker from 'reader/core/stackers/TitleStacker';
5import IssueStack from 'reader/core/stackers/IssueStack';
6import Request from 'reader/Request';
7import ApplicationController from 'reader/core/ApplicationController';
8import Title from 'reader/lib/Title';
9import Toolbar from 'reader/lib/Toolbar';
10import API from 'reader/api/API';
11import Routes from 'reader/Routes';
12import { findAll, addClass } from 'reader/helpers/dom-utils';
13import generateUUID from 'reader/helpers/generateUUID';
14/*
15 * decaffeinate suggestions:
16 * DS102: Remove unnecessary code created because of implicit returns
17 * DS205: Consider reworking code to avoid use of IIFEs
18 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
19 */
20
21const ISSUES_PER_PAGE = 1000;
22
23export default class IssueViewController extends ApplicationController {
24  constructor(magazine, readerController, targetStackId) {
25    super('#issues');
26    this.magazine = magazine;
27    this.readerController = readerController;
28    MonthStacker.setMonthNames(this.readerController.phraseFor('months').split(/,/));
29
30    this.refreshPending = false;
31
32    // For progressive rendering, we will maintain our own (growing) list of
33    // available issues. Each time we retrieve another page of issues, we
34    // append them to the list, regenerate our stacks and adjust the visible
35    // issue-stack boxes to reflect what's changed.
36    this.issues = [];
37
38    this.rootStack = null;
39
40    this.currentStack = null;
41
42    this.targetStackId = `${targetStackId}`;
43
44    this.page = 1;
45
46    this.boxesByStackKey = new Map();
47
48    if (this.visible()) {
49      this.refreshContent();
50      this.readerController.showClientLogo();
51    }
52
53    this.showClientLogo = true;
54  }
55
56  setMagazine(magazine, stackId) {
57    this.magazine = magazine;
58
59    this.targetStackId = stackId;
60
61    this.rootStack = null;
62
63    this.empty();
64  }
65
66  show() {
67    super.show();
68
69    Toolbar.hide();
70
71    if (!this.rootStack) { this.refreshContent(); }
72  }
73
74  refreshContent() {
75    if (!this.magazine) { return; }
76
77    if (this.refreshPending) { return; }
78
79    this.refreshPending = true;
80
81    this.page = 1;
82
83    this.issues = [];
84    this.boxesByStackKey = new Map();
85
86    this.requestPageOfIssues();
87  }
88
89  requestPageOfIssues() {
90    const controller = this;
91    const endpoint = `/magazines/${this.magazine.id}/issues/list.json?page=${this.page}&per_page=${ISSUES_PER_PAGE}${controller.readerController.defaultUrlParams('&')}`;
92
93    new Request(endpoint).json()
94      .then((data) => {
95        controller.refreshPending = false;
96        controller.processIssues(data);
97      })
98      .catch((error) => {
99        controller.refreshPending = false;
100        controller.readerController.reportError(error);
101      });
102  }
103
104  processIssues({
105    issues, policy, policyWebp, magazine: magazineId, publisher: publisherId,
106  }) {
107    if (this.page === 1) {
108      if (issues.length === 1) {
109        this.readerController.selectIssue(issues[0].i);
110
111        return;
112      }
113    }
114
115    issues.forEach((issue) => {
116      const coverSpec = `000000${issue.cp}`.slice(-6);
117
118      this.issues.push({
119        id: issue.i,
120        name: issue.n,
121        coverDate: new Date(issue.c * 1000),
122        publicationDate: issue.p,
123        coverThumbnailWebpUrl: issue.w ? `https://d1zfca9r0ctlm4.cloudfront.net/${publisherId}/${magazineId}/${issue.i}/${issue.p}/webp/2/page-${coverSpec}.webp${policyWebp}` : null,
124        coverThumbnailUrl: `https://d1zfca9r0ctlm4.cloudfront.net/${publisherId}/${magazineId}/${issue.i}/${issue.p}/images/2/page-${coverSpec}.jpg${policy}`,
125        supplementGroup: issue.sg || 0,
126        supplementIndex: issue.si || 0,
127        publicationStatus: issue.ps,
128      });
129    });
130
131    this.prepareNewRootStack();
132
133    this.issues.forEach((issue) => {
134      this.rootStack.addIssue(issue);
135    });
136
137    // Render what we've got.
138    this.rootStack.condense(StackConstants.MINIMUM_VIABLE_SUBSTACKS);
139
140    const stack = this.rootStack.stackForId(this.targetStackId) || this.rootStack;
141    this.renderStack(stack, this.page > 1);
142
143    if (issues.length < ISSUES_PER_PAGE) {
144      this.targetStackId = '';
145      return;
146    }
147
148    this.page += 1;
149
150    this.requestPageOfIssues();
151  }
152
153  selectStack(stackId) {
154    if (this.rootStack) {
155      this.renderStack(this.rootStack.stackForId(stackId) || this.rootStack);
156    }
157
158    this.readerController.pushHistoryViewIssues(this.magazine, stackId);
159
160    this.targetStackId = stackId;
161
162    if (!this.rootStack) {
163      this.refreshContent();
164    }
165  }
166
167  renderStack(stack, progressive = false) {
168    if (!progressive) {
169      this.empty();
170
171      this.boxesByStackKey = new Map();
172
173      const container = document.createElement('nav');
174      addClass(container, 'issueGrid');
175      container.setAttribute('aria-label', this.readerController.phraseFor('aria.cho
175oseissue'));
176
177      this.div.appendChild(container);
178
179      const row = document.createElement('div');
180      addClass(row, 'issueGrid-grid');
181
182      container.appendChild(row);
183
184      window.scrollTo({ top: 0, behavior: 'smooth' });
185    }
186
187    this.currentStack = stack;
188
189    let pageTitle = this.magazine.name;
190
191    this.stackPath().forEach((pathElement) => {
192      pageTitle = `${pageTitle} / ${pathElement.label}`;
193    });
194
195    Title.set(pageTitle);
196
197    this.readerController.updateToolbar();
198
199    if (progressive) { this.removeRedundantBoxes(stack); }
200
201    return stack.substacks().map((substack, index) => (
202      this.renderSubstack(
203        substack,
204        index,
205        this.div.querySelector('.issueGrid-grid'),
206      )));
207  }
208
209  // During progressive rendering, we may create boxes for stacks which are no
210  // longer relevant when the next page of results comes in (e.g. single-issue
211  // stacks may turn into year stacks once more issues have arrived). Remove them
212  // by comparing their stack keys to the set of keys for current substacks.
213  removeRedundantBoxes(stack) {
214    const currentSubstackKeys = new Set(stack.substacks().map((s) => s.issueStackKey.encode()));
215
216    findAll('.issueGrid-issue', this.div).forEach((box) => {
217      const key = box.getAttribute('data-stack-key');
218
219      if (!currentSubstackKeys.has(key)) { this.removeBox(box); }
220    });
221  }
222
223  // Removes the specified box
224  removeBox(box) {
225    box.remove();
226    this.boxesByStackKey.delete(box.getAttribute('data-stack-key'));
227  }
228
229  renderSubstack(substack, index, row) {
230    const encodedKey = substack.issueStackKey.encode();
231
232    // Are we already showing a box for this stack? Retain it if so, but update
233    // the associated stack as it may now contain more substacks than it did
234    // when we last rendered it.
235    const existingBox = this.boxesByStackKey.get(encodedKey);
236
237    if (existingBox) {
238      existingBox.issueStack = substack;
239
240      return;
241    }
242
243    const controller = this;
244
245    const cover = document.createElement('magazine-cover');
246    cover.classList.add('issueGrid-issue');
247    const id = `cover-${generateUUID()}`;
248    cover.setAttribute('id', id);
249    let path;
250    let stack;
251
252    if (substack.singleIssue()) {
253      path = Routes.issueSpreadPath(substack.posterIssue().id);
254    } else {
255      stack = substack.substacks()[1].posterIssue();
256      path = Routes.magazineIssuesPath(this.magazine.id, substack.idForStack());
257    }
258
259    cover.setAttribute('path', path);
260
261    let { issue: apiIssue, coverUrls } = API.MagazineCover(substack.posterIssue(), stack);
262
263    if (!substack.singleIssue()) {
264      apiIssue.name = substack.label();
265    }
266
267    cover.setAttribute('role', 'link');
268    cover.setAttribute('aria-label', apiIssue.name);
269
270    apiIssue = JSON.stringify(apiIssue);
271    coverUrls = JSON.stringify(coverUrls);
272
273    cover.setAttribute('issue', apiIssue);
274    cover.setAttribute('coverUrls', coverUrls);
275
276    if (substack.posterIssue().supplementGroup) {
277      cover.setAttribute('data-supplement', substack.posterIssue().supplementGroup);
278    }
279
280    row.appendChild(cover);
281
282    const box = this.div.querySelector(`#${id}`);
283
284    box.setAttribute('data-stack-key', encodedKey);
285    box.issueStack = substack;
286
287    this.boxesByStackKey.set(encodedKey, box);
288
289    const findSupplements = () => Array.from(
290      document.querySelectorAll(`[data-supplement="${substack.posterIssue().supplementGroup}"]`),
291    );
292
293    const highlightSupplements = () => {
294      const supplements = findSupplements();
295
296      if (supplements.length) {
297        supplements.forEach((supplement) => {
298          supplement.classList.add('supplement-highlight');
299        });
300      }
301    };
302
303    const unHighlightSupplements = () => {
304      const supplements = findSupplements();
305
306      if (supplements.length) {
307        supplements.forEach((supplement) => {
308          supplement.classList.remove('supplement-highlight');
309        });
310      }
311    };
312
313    if (substack.singleIssue()) {
314      box.addEventListener('mouseover', highlightSupplements);
315      box.addEventListener('mouseleave', unHighlightSupplements);
316
317      box.addEventListener('focus', highlightSupplements);
318      box.addEventListener('blur', unHighlightSupplements);
319    }
320
321    const onClick = (e) => {
322      if (e.metaKey || e.ctrlKey) { return; }
323      e.preventDefault();
324
325      // Retrieve current stack - it may have changed since box was first
326      // rendered.
327      const myStack = box.issueStack;
328
329      if (myStack.singleIssue()) {
330        controller.readerController.selectIssue(myStack.posterIssue().id);
331        return;
332      }
333
334      const stackId = myStack.idForStack();
335
336      // Set our target stack id so that any progressive updates don't switch
337      // back to the root stack
338      controller.targetStackId = stackId;
339
340      controller.renderStack(myStack);
341
342      controller.readerController.pushHistoryViewIssues(controller.magazine, stackId);
343    };
344
345    box.addEventListener('click', onClick);
346    box.addEventListener('keydown', (e) => {
347      if (e.code === 'Enter') onClick(e);
348    });
349  }
350
351  prepareNewRootStack() {
352    const key = new IssueStackKey(null, 0, 0, 0, StackConstants.STACK_SCALE_TITLE, false);
353
354    const stacker = new TitleStacker();
355
356    this.rootStack = new IssueStack(key, stacker, null);
357
358    this.currentStack = null;
359  }
360
361  /**
362   * Returns an array of the non-root stacks which lead to the supplied issue.
363   * If issue is null then we return an array of non-root stacks which
364   * lead to the last-rendered stack.
365   */
366  stackPath(issue = null) {
367    const ret = [];
368
369    if (this.rootStack) {
370      if (issue) {
371        if (this.rootStack) { this.rootStack.tracePath(issue, ret); }
372      } else if (this.currentStack) {
373        let stack = this.currentStack;
374
375        while (stack) {
376          if (stack.idForStack()) { ret.unshift({ label: stack.label(), id: stack.idForStack() }); }
377
378          stack = stack.parentStack;
379        }
380      }
381    }
382
383    return ret;
384  }
385
386  searchRestriction() {
387    // Only stacks with IDs provide useful filters (not root stack or single-issue)
388    if (!this.currentStack || !this.currentStack.idForStack()) { return null; }
389
390    const issues = this.currentStack.issues();
391
392    const from = issues[issues.length - 1].publicationDate;
393    const to = issues[0].publicationDate;
394
395    return { dateFrom: from, dateTo: to, scope: this.currentStack.label() };
396  }
397}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.