PageSourceSearch

https://reader.exacteditions.com/assets/reader/core/ReaderController-96e747d4.js

js exacteditions.com collected 2026-10-01 10:19:00 UTC 37,597 bytes, 1,341 lines download raw bytes

1/* eslint-disable no-console */
2import Issue from 'reader/core/models/Issue';
3import ArticleViewController from 'reader/core/ArticleViewController';
4import CollectionViewController from 'reader/core/CollectionViewController';
5import IssueViewController from 'reader/core/IssueViewController';
6import MagazineViewController from 'reader/core/MagazineViewController';
7import PageViewController from 'reader/core/PageViewController';
8import ResultsViewController from 'reader/core/ResultsViewController';
9import CelebritiesController from 'reader/core/CelebritiesController';
10import ThumbnailViewController from 'reader/core/ThumbnailViewController';
11import BookmarksController from 'reader/core/BookmarksController';
12import Magazine from 'reader/core/models/Magazine';
13import AutoComplete from 'reader/lib/AutoComplete';
14import PageService from 'reader/lib/PageService';
15import KeyStroke from 'reader/KeyStroke';
16import Bookmark from 'reader/lib/Bookmark';
17import Notifier from 'reader/Notifier';
18import Request from 'reader/Request';
19import asyncTimeout from 'reader/helpers/asyncTimeout';
20import Title from 'reader/lib/Title';
21import CurrentPolicy from 'reader/lib/CurrentPolicy';
22import PurchaseLinks from 'reader/lib/PurchaseLinks';
23import QueryParams from 'reader/QueryParams';
24import NavigationRegistry from 'reader/NavigationRegistry';
25import User from 'reader/core/models/User';
26import {
27  find, click, isVisible, setAttribute,
28  hideAll, showAll, hide, show,
29} from 'reader/helpers/dom-utils';
30
31const VIEW_AUTO = 'auto';
32const VIEW_PAGE = 'page';
33const VIEW_SPREAD = 'spread';
34const VIEW_THUMBNAILS = 'thumbs';
35
36export default class ReaderController {
37  constructor(reader) {
38    // Don't accummulate history during initial setup
39    this.handlePopState = this.handlePopState.bind(this);
40    this.reader = reader;
41    this.processingHistory = true;
42
43    this.analyticsTrackers = new Map();
44    this.analyticsMagazineId = null;
45
46    this.magazine = null;
47    this.issue = null;
48    this.initialStackId = '';
49    this.collections = [];
50
51    this.phrases = {};
52
53    this.prepareIssue();
54
55    this.homepage = this.reader[0].dataset.homepage;
56
57    this.issueViewController = new IssueViewController(this.magazine, this, this.initialStackId);
58
59    this.magazineViewController = new MagazineViewController(
60      this, this.allPublicContent, this.titlesetId, this.publicationType);
61
62    this.articleViewController = new ArticleViewController(this);
63    this.collectionViewController = new CollectionViewController(this, this.allPublicContent);
64    this.pageViewController = new PageViewController(this.issue, this);
65    this.thumbnailViewController = new ThumbnailViewController(this.issue, this);
66    this.resultsViewController = new ResultsViewController(this);
67    this.celebritiesController = new CelebritiesController(this);
68    this.bookmarksController = new BookmarksController(this);
69
70    this.viewControllers = [
71      this.articleViewController,
72      this.issueViewController,
73      this.magazineViewController,
74      this.collectionViewController,
75      this.pageViewController,
76      this.thumbnailViewController,
77      this.resultsViewController,
78      this.celebritiesController,
79      this.bookmarksController,
80    ];
81
82    this.hookupControls();
83    this.setup();
84  }
85
86  async setup() {
87    window.onpopstate = this.handlePopState;
88
89    let page = await PageService.startPage(this.issue);
90
91    page = page || Number(this.reader.data('page'));
92
93    if (!Number.isNaN(page)) {
94      this.thumbnailViewController.setPage(page);
95      this.pageViewController.setPage(page, this.exceptionalAccess);
96    }
97
98    this.pageViewController.setTerm(this.reader.data('term') || '');
99    this.updateToolbar();
100    this.recordGA4();
101
102    // Can we improve this?
103    if (this.bookmarksController.shouldDisplay()) {
104      this.displayBookmarks();
105    }
106
107    // Now ready to accummulate history based on user interation
108    this.processingHistory = false;
109
110    this.showOrHideLoginLink(this.issue);
111  }
112
113  updateInstitutional(institutional, displayName) {
114    this.institutional = institutional;
115
116    if (this.institutional && displayName) {
117      const loginBtn = find('#readerLogin');
118
119      if (loginBtn) {
120        loginBtn.textContent = displayName;
121      }
122    }
123  }
124
125  hookupControls() {
126    const wsLink = find('#ws-link');
127
128    if (wsLink) {
129      this.wsLink = wsLink;
130    }
131
132    click('#btnThumbs', () => {
133      this.displayThumbs(this.currentPage());
134    });
135
136    click('#btnContents', () => {
137      this.showViewController(this.pageViewController);
138    });
139
140    click('#btnSpread', () => {
141      this.showViewController(this.pageViewController);
142    });
143
144    click('#btnTitles', () => {
145      this.goHome();
146    });
147
148    click('#btnBookmarks', () => {
149      this.displayBookmarks();
150    });
151
152    document.addEventListener('nav:bookmarks', () => this.displayBookmarks());
153    document.addEventListener('nav:magazine', ({ detail }) => {
154      this.displayIssues(detail.magazineId);
155    });
156    document.addEventListener('nav:issue', ({ detail }) => {
157      this.displaySpread(null, detail.issueId);
158    });
159    document.addEventListener('nav:bookmark', ({ detail }) => {
160      this.displaySpread(Number(detail.page), detail.issueId);
161    });
162    document.addEventListener('nav:home', () => this.goHome());
163    document.addEventListener('nav:preferences', () => { window.location.href = '/preferences'; });
164
165    KeyStroke.register(['KeyK'], async (e) => {
166      if (!User.current().id && !find('#bookmarkIndex')) {
167        return;
168      }
169
170      if (e.metaKey || e.ctrlKey) {
171        if (find('jump-to')) {
172          find('jump-to').remove();
173          return;
174        }
175
176        await customElements.whenDefined('jump-to');
177
178        const jumpTo = document.createElement('jump-to');
179        document.body.appendChild(jumpTo);
180        setAttribute('jump-to', 'results', '[]');
181        setAttribute('jump-to', 'isLoading', 'false');
182
183        e.preventDefault();
184      }
185    }, { meta: false, prevent: false, active: false });
186
187    const controller = this;
188
189    const input = find('#search-term');
190    const url = (term) => `/api/magazines/filtered?q=${term}
190${controller.defaultUrlParams('&')}`;
191    const onClick = (data) => {
192      const isTitleset = data.isTitleset === 'true';
193      const value = Number(data.id);
194
195      if (data.type === 'celebrity') {
196        window.location.href = `/celebrities/${data.id}`;
197        return;
198      }
199
200      if (value) {
201        if (isTitleset) {
202          let showPublic = controller.offerPublicTitles;
203          if (showPublic) {
204            controller.collections.m.every((c) => {
205              if (c.id === value) {
206                showPublic = false;
207                return false;
208              }
209
210              return true;
211            });
212          }
213
214          controller.displayMagazines(showPublic, value);
215        } else {
216          controller.displayIssues(value);
217        }
218
219        const searchTermElement = find('#search-term');
220        searchTermElement.setAttribute('value', '');
221        searchTermElement.blur();
222      }
223    };
224
225    new AutoComplete(input, {
226      url, onClick, minLength: 3, maxEntries: 7,
227    }, this);
228
229    let locked = false;
230
231    KeyStroke.register(['KeyB'], async () => {
232      if (locked) return;
233      if (!this.showingIssue()) return;
234      if (!this.pageViewController.bookmarksVisible) return;
235
236      const pages = document.querySelector('#pages');
237      if (!isVisible(pages)) return;
238
239      locked = true;
240
241      const currentPage = this.currentPage();
242
243      const created = await Bookmark.toggle(this.issue.id, {
244        label: String(this.issue.labelForPage(currentPage)),
245        index: String(currentPage),
246      });
247
248      Notifier.notify('bookmark:update');
249
250      const shareMenu = document.querySelector('.sharePopover');
251      if (isVisible(shareMenu)) {
252        locked = false;
253        return;
254      }
255
256      let title = 'Removed bookmark.';
257
258      if (created) title = 'Boomarked.';
259      /**
260       * @todo Change this to a toast when on Bootstrap 5.
261       */
262      $('#btnShare').tooltip({ trigger: 'manual', title });
263      $('#btnShare').tooltip('show');
264
265      await asyncTimeout(() => {
266        $('#btnShare').tooltip('destroy');
267      }, 1000);
268
269      locked = false;
270    });
271
272    KeyStroke.register(['KeyH'], this.goHome.bind(this));
273    KeyStroke.register(['Slash'], this.focusSearch.bind(this), { prevent: true });
274
275    KeyStroke.register(['ArrowLeft'], () => {
276      if (!this.respondToIssueNavigationKeys()) return;
277      this.pageViewController.pagePrev();
278    });
279
280    KeyStroke.register(['ArrowRight'], () => {
281      if (!this.respondToIssueNavigationKeys()) return;
282      this.pageViewController.pageNext();
283    });
284
285    KeyStroke.register(['KeyC'], () => {
286      if (!this.respondToIssueNavigationKeys()) return;
287      this.displaySpread(this.issue.contentsPage);
288    });
289
290    KeyStroke.register(['KeyP'], () => {
291      if (!this.respondToIssueNavigationKeys()) return;
292      this.displayPage(this.currentPage());
293    });
294
295    KeyStroke.register(['KeyS'], () => {
296      if (!this.respondToIssueNavigationKeys()) return;
297      this.displaySpread(this.currentPage());
298    });
299
300    KeyStroke.register(['KeyT'], () => {
301      if (!this.respondToIssueNavigationKeys()) return;
302      this.displayThumbs(this.currentPage());
303    });
304
305    window.addEventListener('resize', () => controller.updateAfterResize());
306
307    $('#content').on('scroll', () => $('body,html').trigger('scroll'));
308  }
309
310  respondToIssueNavigationKeys() {
311    if (!this.showingIssue()) return false;
312
313    if ((document.querySelector('page-flow') && (document.querySelector('page-flow').contains(document.activeElement)))) return false;
314
315    return true;
316  }
317
318  goHome() {
319    if (this.analyticsMagazineId) {
320      window.location.href = '/';
321      return;
322    }
323
324    // Prevent redundant navigation.
325    if (this.homepage === 'titles') {
326      if (['/', '/magazines'].includes(window.location.pathname) && !QueryParams.current().find('ts')) return;
327
328      this.displayMagazines();
329      return;
330    }
331
332    if (['/', '/collections'].includes(window.location.pathname)) return;
333
334    this.displayCollections();
335  }
336
337  focusSearch() {
338    const search = find('#search-term');
339    search.focus();
340    search.select();
341  }
342
343  showViewController(viewController) {
344    this.viewControllers.forEach((vc) => {
345      if (vc !== viewController) vc.hide();
346    });
347
348    viewController.show();
349
350    if (viewController.showClientLogo) {
351      this.showClientLogo();
352    } else {
353      this.hideClientLogo();
354    }
355
356    this.updateToolbar();
357
358    new NavigationRegistry().away();
359  }
360
361  prepareIssue() {
362    const data = this.reader.data();
363    const purchaseLinks = data['purchase-links'] || {};
364
365    if (data.purchaseLinksExternal) {
366      purchaseLinks.external = data.purchaseLinksExternal;
367    }
368
369    if (data.purchaseLinksIndividual) {
370      purchaseLinks.individual = data.purchaseLinksIndividual;
371    }
372
373    this.institutional = (data.institutional === 1);
374    this.exceptionalAccess = (data.exceptionalAccess === 1);
375    this.allPublicContent = (data.allPublicContent === 1);
376    this.publicationType = data.publicationType;
377    this.multiplePublicationTypes = (data.multiplePublicationTypes === 1);
378    this.titlesetId = data.ts;
379    this.offerPublicTitles = (data.offerPublicTitles === 1);
380    this.offerMaps = (data.offerMaps === 1);
381
382    this.collections = data.collections;
383
384    this.issue = null;
385
386    if (data.magazineId) {
387      this.magazine = new Magazine(
388        data.magazineId,
389        data.magazineName,
390        data['magazine-short-name'],
391        data.singleIssue,
392        data.analyticsId,
393        data.magazineAuthor || '',
394        data.magazineFirstName,
395        data.magazinePublicationType,
396      );
397
398      this.initialStackId = data.stackId;
399    }
400
401    if (data.issueId) {
402      this.issue = new Issue(
403        data.issueId,
404        data.name,
405        data.numPages,
406        data.contentsPage,
407        data.mediaRoot,
408        data.publicationDateForUrl,
409        data.imageCloudfrontPolicy,
410        data.webpCloudfrontPolicy,
411        data.labellers,
412        data.offset,
413        this.magazine,
414        data.visiblePageRanges,
415        (data.fullyVisible === 1),
416        data.visiblePages,
417        data.visiblePagesWebp,
418        purchaseLinks,
419        (data['is-promote-universals'] === 1),
420        data.completePdf,
421        {
422          ofc: this.phraseFor('ofc'), ifc: this.phraseFor('ifc'), ibc: this.phraseFor('ibc'), obc: this.phraseFor('obc'),
423        },
424        data.hideLogin,
425        data.hasContentsPage === 1,
426        data.webp,
427        data.displayIndividual,
428        data.isbn,
429        data.isBook,
430        data.highDefinition,
431        data.coverPage,
432        data.singleSpread,
433      );
434
435      this.setCurrentPolicies();
436    }
437  }
438
439  urlForPage(page, size) {
440    let pagePadded = `${page}`;
441
442    while (pagePadded.length < 6) {
443      pagePadded = `0${pagePadded}`;
444    }
445
446    const policy = CurrentPolicy.fromCache().get();
447
448    return `${this.issue.mediaRoot}/${this.issue.publicationDateForURL}/images/${size}/page-${pagePadded}.jpg${policy.url}`;
449  }
450
451  // Refactor me when we have moved to Webp.
452  webpImageForPage(page, size) {
453    let pagePadded = `${page}`;
454
455    while (pagePadded.length < 6) {
456      pagePadded = `0${pagePadded}`;
457    }
458
459    const policy = CurrentPolicy.fromCache().get('webp');
460
461    return `${this.issue.mediaRoot}/${this.issue.publicationDateForURL}/webp/${size}/page-${pagePadded}.webp${policy.url}`;
462  }
463
464  urlForPageInfo(page) {
465    // "#{@issue.mediaRoot}/wordmap/links#{page}.txt#{@issue.wordmap_cloudfront_policy}"
466    return `/api/issues/${this.issue.id}/pageinfo/${page}.json${this.defaultUrlParams('?')}`; // Access via our own server for now, go directly from JS once we have CORS set up.
467  }
468
469  urlForHighlights(page, term) {
470    return `/issues/${this.issue.id}/highlights/${page}.json?term=${encodeURIComponent(term)}${this.defaultUrlParams('&')}`;
471  }
472
473  selectIssue(issueId, view = VIEW_AUTO, page = null) {
474    this.issue = null;
475    this.magazine = null;
476    const controller = this;
477    PageService.startPage(issueId).then((pageServicePage) => {
478      new Request(`/issues/${issueId}.json${controller.defaultUrlParams('?')}`)
479        .json()
480        .then((data) => controller.processIssue(data, view, pageServicePage || page))
481        .catch((error) => controller.reportError(error));
482    });
483  }
484
485  selectMagazine(magazineId, stackId) {
486    this.issue = null;
487    this.magazine = null;
488
489    const controller = this;
490
491    new Request(`/magazines/${magazineId}.json${controller.defaultUrlParams('?')}`)
492      .json()
493      .then((data) => controller.processMagazine(data, stackId))
494      .catch((error) => controller.reportError(error));
495  }
496
497  displayCollections(allPublicContent = false) {
498    this.issue = null;
499    this.magazine = null;
500
501    Title.set(this.phraseFor('collections'));
502
503    this.collectionViewController.allPublicContent = allPublicContent;
504    this.collectionViewController.updateUI();
505
506    this.showViewController(this.collectionViewController);
507  }
508
509  displayBookmarks() {
510    this.issue = null;
511    this.magazine = null;
512
513    Title.set(this.phraseFor('bookmarks'));
514    this.showViewController(this.bookmarksController);
515  }
516
517  displayMagazines(allPublicContent = false, titlesetId = null, publicationType = 'a') {
518    this.issue = null;
519    this.magazine = null;
520
521    Title.set(this.phraseFor('titles'));
522
523    this.showViewController(this.magazineViewController);
524
525    this.magazineViewController.allPublicContent = allPublicContent;
526    this.magazineViewController.titlesetId = titlesetId;
527    this.magazineViewController.publicationType = publicationType;
528    this.magazineViewController.updateUI();
529  }
530
531  displayIssues(magazineId = null, stackId = '') {
532    this.issue = null;
533
534    if (magazineId && (!this.magazine || (magazineId !== this.magazine.id))) {
535      this.selectMagazine(magazineId, stackId);
536      return;
537    }
538
539    this.showViewController(this.issueViewController);
540
541    this.issueViewController.selectStack(stackId);
542  }
543
544  displayThumbs(page, issueId = null) {
545    if (issueId && (!this.issue || (issueId !== this.issue.id))) {
546      this.selectIssue(issueId, VIEW_THUMBNAILS);
547      return;
548    }
549
550    Title.set(this.issue.displayName());
551
552    this.showViewController(this.thumbnailViewController);
553
554    this.thumbnailViewController.setPage(page);
555  }
556
557  displayPage(page, issueId = null) {
558    if (issueId && (!this.issue || (issueId !== this.issue.id))) {
559      this.selectIssue(issueId, VIEW_PAGE, page);
560      return;
561    }
562
563    this.showViewController(this.pageViewController);
564
565    this.pageViewController.displayPage(page);
566  }
567
568  // Pass null as the page parameter to choose the issue's cover page (which
569  // isn't necessarily 1).
570  displaySpread(page, issueId = null) {
571    if (issueId && (!this.issue || (issueId !== this.issue.id))) {
572      this.selectIssue(issueId, VIEW_SPREAD, page);
573      return;
574    }
575
576    page = page || this.issue.coverPage;
577
578    this.showViewController(this.pageViewController);
579    this.pageViewController.displaySpread(page);
580  }
581
582  pushHistoryViewPage(issue, page) {
583    if (this.processingHistory) { return; }
584    if (ResultsViewController.onSearchResults()) return;
585
586    if (this.lastHistoryState
587      && (this.lastHistoryState.action === 'page')
588      && (this.lastHistoryState.page === page)
589      && (this.lastHistoryState.issue === issue.id)) { return; }
590
591    this.lastHistoryState = {
592      action: 'page',
593      page,
594      issue: issue.id,
595    };
596
597    this.pushHistory(
598      this.lastHistoryState,
599      `Page ${page}`,
600      `/issues/${issue.id}/page/${page}${this.defaultUrlParams('?')}`,
601      issue.magazine.id,
602    );
603
604    this.logPage(issue);
605  }
606
607  pushHistoryViewSpread(issue, page) {
608    if (this.processingHistory) { return; }
609
610    if (this.lastHistoryState
611      && (this.lastHistoryState.action === 'spread')
612      && (this.lastHistoryState.page === page)
613      && (this.lastHistoryState.issue === issue.id)) { return; }
614
615    this.lastHistoryState = {
616      action: 'spread',
617      page,
618      issue: issue.id,
619    };
620
621    this.pushHistory(
622      this.lastHistoryState,
623      `Spread ${page}`,
624      `/issues/${issue.id}/spread/${page}${this.defaultUrlParams('?')}`,
625      issue.magazine.id,
626    );
627
628    Notifier.notify('view:spread');
629
630    this.logPage(issue);
631  }
632
633  pushHistoryViewThumbs(issue, page) {
634    if (this.processingHistory) { return; }
635
636    if (this.lastHistoryState
637      && (this.lastHistoryState.action === 'thumbs')
638      && (this.lastHistoryState.page === page)
639      && (this.lastHistoryState.issue === issue.id)) { return; }
640
641    this.lastHistoryState = {
642      action: 'thumbs',
643      page,
644      issue: issue.id,
645    };
646
647    const historyStatePage = this.lastHistoryState.page > 1 ? `/${this.lastHistoryState.page}` : '';
648
649    this.pushHistory(
650      this.lastHistoryState,
651      'Thumbnails',
652      `/issues/${issue.id}/thumbs${historyStatePage}${this.defaultUrlParams('?')}`,
653      issue.magazine.id,
654    );
655
656    Notifier.notify('view:thumbs');
657
658    this.logPage(issue);
659  }
660
661  pushHistoryViewIssues(magazine, stackId) {
662    if (this.processingHistory) { return; }
663
664    if (this.lastHistoryState
665      && (this.lastHistoryState.action === 'issues')
666      && (this.lastHistoryState.magazine === magazine.id)
667      && (this.lastHistoryState.stack === stackId)) { return; }
668
669    this.lastHistoryState = {
670      action: 'issues',
671      magazine: magazine.id,
672      stack: stackId,
673    };
674
675    this.pushHistory(
676      this.lastHistoryState,
677      'Issues',
678      `/magazines/${magazine.id}/issues/${stackId}${this.defaultUrlParams('?')}`,
679      magazine.id,
680    );
681
682    Notifier.notify('view:issues');
683  }
684
685  pushHistoryViewBookmarks() {
686    if (this.processingHistory) { return; }
687
688    if (this.lastHistoryState
689      && this.lastHistoryState.action === 'bookmarks') { return; }
690
691    this.lastHistoryState = {
692      action: 'bookmarks',
693    };
694
695    this.updateToolbar();
696
697    let target = '/bookmarks';
698
699    const separator = '?';
700
701    target += this.defaultUrlParams(separator);
702
703    let pageSeparator = '?';
704    if (target.includes('?')) {
705      pageSeparator = '&';
706    }
707
708    const page = QueryParams.current().find('page');
709    if (page) {
710      target += `${pageSeparator}page=${page}`;
711    }
712
713    this.pushHistory(
714      this.lastHistoryState,
715      'Bookmarks',
716      target,
717      null,
718    );
719  }
720
721  pushHistoryViewMagazines(allPublicContent, titlesetId, publicationType) {
722    if (this.processingHistory) { return; }
723
724    if (this.lastHistoryState
725      && (this.lastHistoryState.action === 'magazines')
726      && (this.lastHistoryState.allPublicContent === allPublicContent)
727      && (this.lastHistoryState.titlesetId === titlesetId)
728      && (this.lastHistoryState.publicationType === publicationType)) { return; }
729
730    this.lastHistoryState = {
731      action: 'magazines',
732      allPublicContent,
733      titlesetId,
734      publicationType,
735    };
736
737    this.updateToolbar();
738
739    let target = '/magazines';
740
741    let separator = '?';
742
743    if (allPublicContent) {
744      target += `${separator}public=1`;
745      separator = '&';
746    }
747
748    if (titlesetId) {
749      target += `${separator}ts=${titlesetId}`;
750      separator = '&';
751    }
752
753    if (publicationType !== 'a') {
754      target += `${separator}pt=${publicationType}`;
755      separator = '&';
756    }
757
758    target += this.defaultUrlParams(separator);
759
760    this.pushHistory(
761      this.lastHistoryState,
762      'Titles',
763      target,
764      null,
765    );
766  }
767
768  pushHistoryViewCollections(allPublicContent) {
769    if (this.processingHistory) { return; }
770
771    if (this.lastHistoryState
772      && (this.lastHistoryState.action === 'collections')
773      && (this.lastHistoryState.allPublicContent === allPublicContent)) { return; }
774
775    this.lastHistoryState = {
776      action: 'collections',
777      allPublicContent,
778    };
779
780    this.updateToolbar();
781
782    let target = '/collections';
783
784    let separator = '?';
785
786    if (allPublicContent) {
787      target += `${separator}public=1`;
788      separator = '&';
789    }
790
791    target += this.defaultUrlParams(separator);
792
793    this.pushHistory(
794      this.lastHistoryState,
795      'Collections',
796      target,
797      null,
798    );
799  }
800
801  pushHistory(state, name, path, magazineId) {
802    const newVersion = document.querySelector('meta[name="eev_current"]');
803
804    if (newVersion || (this.analyticsMagazineId && (this.analyticsMagazineId !== magazineId))) {
805      window.location.href = path;
806      return;
807    }
808
809    window.history.pushState(
810      state,
811      name,
812      path,
813    );
814
815    this.updateAnlyticsConsentLinks(path);
816    this.recordGA4();
817  }
818
819  handlePopState(event) {
820    if (!event.state || !event.state.action) {
821      document.location.reload();
822      return;
823    }
824
825    this.lastHistoryState = null;
826
827    this.processingHistory = true;
828
829    switch (event.state.action) {
830      case 'page': this.displayPage(event.state.page, event.state.issue); break;
831      case 'spread': this.displaySpread(event.state.page, event.state.issue); break;
832      case 'thumbs': this.displayThumbs(event.state.page, event.state.issue); break;
833      case 'issues': this.displayIssues(event.state.magazine, event.state.stack); break;
834      case 'magazines': this.displayMagazines(event.state.allPublicContent, event.state.t
834itlesetId, event.state.publicationType); break;
835      case 'collections': this.displayCollections(event.state.allPublicContent); break;
836      case 'bookmarks': this.displayBookmarks(); break;
837      default: // do nothing;
838    }
839
840    this.processingHistory = false;
841  }
842
843  currentPage() {
844    return this.pageViewController.page;
845  }
846
847  reportError(textStatus) {
848    console.error(`Failed to retrieve data: ${textStatus}`);
849  }
850
851  processIssue(data, view, page) {
852    this.magazine = new Magazine(
853      data['magazine-id'],
854      data['magazine-name'],
855      data['magazine-short-name'],
856      data['single-issue'],
857      data['analytics-id'],
858      data['magazine-author'] || '',
859      data['magazine-first-name'],
860      data['magazine-publication-type'],
861    );
862
863    this.issue = new Issue(
864      data['issue-id'],
865      data.name,
866      data['num-pages'],
867      data['contents-page'],
868      data['media-root'],
869      data['publication-date-for-url'],
870      data['image-cloudfront-policy'],
871      data['webp-cloudfront-policy'],
872      data.labellers,
873      data.offset,
874      this.magazine,
875      data['visible-page-ranges'],
876      (data['fully-visible'] === 1),
877      JSON.parse(data['visible-pages'] || '{}'),
878      JSON.parse(data['visible-pages-webp'] || '{}'),
879      data['purchase-links'],
880      (data['is-promote-universals'] === 1),
881      data['complete-pdf'],
882      {
883        ofc: this.phraseFor('ofc'), ifc: this.phraseFor('ifc'), ibc: this.phraseFor('ibc'), obc: this.phraseFor('obc'),
884      },
885      data['hide-login'],
886      data['has-contents-page'] === 1,
887      data.webp,
888      data['display-individual'],
889      data.isbn,
890      data['is-book'],
891      data['high-definition'],
892      data['cover-page'],
893      (data['single-spread'] === 1),
894    );
895
896    this.thumbnailViewController.setIssue(this.issue);
897    this.pageViewController.setIssue(this.issue);
898    this.showOrHideLoginLink(this.issue);
899
900    Title.set(this.issue.displayName());
901
902    this.setCurrentPolicies();
903
904    if (view === VIEW_AUTO) {
905      view = this.appropriateView();
906    }
907
908    page = page || this.issue.coverPage;
909
910    switch (view) {
911      case VIEW_PAGE: return this.displayPage(page);
912      case VIEW_SPREAD: return this.displaySpread(page);
913      case VIEW_THUMBNAILS: return this.displayThumbs(page);
914      default: return console.log(`Unexpected view type request: ${view}`);
915    }
916  }
917
918  appropriateView() {
919    if (this.reader.width() > this.reader.height()) {
920      return VIEW_SPREAD;
921    }
922
923    return VIEW_PAGE;
924  }
925
926  setCurrentPolicies() {
927    const policyCache = CurrentPolicy.fromCache();
928
929    policyCache.replace(this.issue.policies.jpeg);
930    policyCache.replace(this.issue.policies.webp, 'webp');
931  }
932
933  processMagazine(data, stackId) {
934    this.magazine = new Magazine(
935      data.id,
936      data.name,
937      data['magazine-short-name'],
938      data['single-issue'],
939      data.analytics_account_id,
940      data['magazine-author'] || '',
941      data['magazine-first-name'],
942      data['magazine-publication-type'],
943    );
944
945    this.issueViewController.setMagazine(this.magazine, stackId);
946
947    Title.set(this.magazine.name);
948
949    return this.displayIssues();
950  }
951
952  updateToolbar() {
953    let allPublicContent;
954    setAttribute('#search-from', 'value', '');
955    setAttribute('#search-to', 'value', '');
956    setAttribute('#search-stack', 'value', '');
957
958    const params = QueryParams.current();
959
960    if (params.find('from')) {
961      setAttribute('#search-from', 'value', params.find('from'));
962    }
963
964    if (params.find('to')) {
965      setAttribute('#search-to', 'value', params.find('to'));
966    }
967
968    if (params.find('stack')) {
969      setAttribute('#search-stack', 'value', params.find('stack'));
970    }
971
972    const rc = params.roomContext();
973
974    if (rc) {
975      setAttribute('#search-rc', 'value', rc);
976    }
977
978    if (this.issue) {
979      setAttribute('#search-form', 'action', `/issues/${this.issue.id}/search`);
980      setAttribute('#search-term', 'placeholder', `${this.phraseFor('searchplaceholder')} ${this.issue.name}`);
981
982      if (this.issue.isBook && this.issue.externalPurchaseLink()) {
983        showAll('.external-link-toolbar');
984        $('.external-link-toolbar').attr('href', this.issue.externalPurchaseLink());
985        $('.external-link-toolbar').attr('target', '_blank');
986      } else if (
987        !this.issue.externalPurchaseLink() && this.issue.isbn
988      ) {
989        showAll('.external-link-toolbar');
990        $('.external-link-toolbar').removeAttr('target');
991        $('.external-link-toolbar').removeAttr('href');
992
993        document.querySelector('.external-link-toolbar').addEventListener('click', async () => {
994          let html = '';
995
996          (await new PurchaseLinks().toArray()).forEach((link) => {
997            html += `
998              <li><a href="${link.url.replace('{ISBN}', this.issue.isbn)}">${link.name}</a></li>
999            `;
1000          });
1001
1002          html = `<ul class="external-link-list">${html}</ul>`;
1003          $('.external-link-toolbar').popover({
1004            animation: false,
1005            container: 'body',
1006            html: true,
1007            placement: 'top',
1008            trigger: 'manual',
1009            content: html,
1010          });
1011
1012          $('.external-link-toolbar').popover('show');
1013        });
1014
1015        document.addEventListener('click', this.clickaway);
1016
1017        // Cover case where user presses h to go home.
1018        KeyStroke.register(['KeyH'], () => {
1019          this.clickaway({ target: document.querySelector('body') });
1020        });
1021      } else {
1022        hideAll('.external-link-toolbar');
1023      }
1024    } else if (this.magazine) {
1025      $('#search-form').attr('action', `/magazines/${this.magazine.id}/search`);
1026      $('#search-term').attr('placeholder', `${this.phraseFor('searchplaceholder')} ${this.magazine.name}`);
1027
1028      if (this.issueViewController) {
1029        const restriction = this.issueViewController.searchRestriction();
1030
1031        if (restriction) {
1032          $('#search-from').val(restriction.dateFrom);
1033          $('#search-to').val(restriction.dateTo);
1034          $('#search-stack').val(restriction.scope);
1035          $('#search-term').attr('placeholder', `${this.phraseFor('searchplaceholder')} ${restriction.scope}`);
1036        }
1037      }
1038    } else if (this.collectionViewController.visible()) {
1039      $('#search-form').attr('action', '/search');
1040      this.collectionViewController.updateSearchForm();
1041    } else {
1042      $('#search-form').attr('action', '/search');
1043      this.magazineViewController.updateSearchForm();
1044    }
1045
1046    if (this.wsLink) {
1047      this.wsLink
1048        .setAttribute('href', this.issue ? `https://webstamp.exacteditions.com/issues/${this.issue.id}/edit` : 'https://webstamp.exacteditions.com');
1049    }
1050
1051    const controller = this;
1052
1053    // TODO: tidy this up
1054    const breadnav = $('.breadnav');
1055
1056    $('.breadcrumb-item', breadnav).not('.home').remove();
1057
1058    let link, li;
1059
1060    if (
1061      !this.magazine
1062        && this.magazineViewController.visible() && this.magazineViewController.titlesetName()) {
1063      const { titlesetId } = this.magazineViewController;
1064
1065      allPublicContent = this.magazineViewController.allPublicContent ? '1' : '0';
1066
1067      link = $(`<a href='/magazines?ts=${titlesetId}&public=${allPublicContent}${this.defaultUrlParams('&')}'>${this.magazineViewController.titlesetName()}</a>`);
1068
1069      link.data('titlesetId', titlesetId);
1070      link.data('allPublicContent', allPublicContent);
1071
1072      link.on('click', function (e) {
1073        e.preventDefault();
1074        return controller.displayMagazines($(this).data('allPublicContent'), $(this).data('titlesetId'));
1075      });
1076
1077      li = $("<li class='breadcrumb-item'></li>");
1078
1079      li.append(link);
1080
1081      breadnav.append(li);
1082    }
1083
1084    if (this.magazine && !this.magazine.singleIssue) {
1085      let linkText = this.phraseFor('titleoverride') || this.magazine.name;
1086
1087      link = $(`<a href='/magazines/${this.magazine.id}/issues${this.defaultUrlParams('?')}'>${linkText}</a>`);
1088
1089      link.data('magazineId', this.magazine.id);
1090
1091      link.on('click', function (e) {
1092        e.preventDefault();
1093        return controller.displayIssues($(this).data('magazineId'));
1094      });
1095
1096      li = $("<li class='breadcrumb-item'></li>");
1097
1098      li.append(link);
1099
1100      breadnav.append(li);
1101
1102      if (this.issueViewController) {
1103        const stackPath = this.issueViewController.stackPath(this.issue);
1104
1105        stackPath.forEach((stack) => {
1106          link = $(`<a href='/magazines/${this.magazine.id}/issues/${stack.id}${this.defaultUrlParams('?')}'>${stack.label}</a>`);
1107
1108          link.data('magazineId', this.magazine.id);
1109          link.data('stackId', stack.id);
1110
1111          link.on('click', function (e) {
1112            e.preventDefault();
1113            return controller.displayIssues($(this).data('magazineId'), $(this).data('stackId'));
1114          });
1115
1116          li = $("<li class='breadcrumb-item stack-label'></li>");
1117
1118          li.append(link);
1119
1120          breadnav.append(li);
1121        });
1122      }
1123    }
1124
1125    if (this.issue) {
1126      link = $(`<a href='/issues/${this.issue.id}${this.defaultUrlParams('?')}'>${this.issue.name}</a>`);
1127
1128      link.data('issueId', this.issue.id);
1129      const issueId = this.issue.id;
1130
1131      link.on('click', async (e) => {
1132        e.preventDefault();
1133        const page = await PageService.startPage(issueId);
1134        controller.displaySpread(page, issueId);
1135      });
1136
1137      li = $("<li class='breadcrumb-item'></li>");
1138
1139      li.append(link);
1140
1141      breadnav.append(li);
1142    }
1143
1144    if (this.collectionViewController.visible()) {
1145      allPublicContent = this.collectionViewController.allPublicContent ? '1' : '0';
1146
1147      link = $(`<a href='/collections?public=${allPublicContent}${this.defaultUrlParams('&')}'>${this.phraseFor('collections')}</a>`);
1148
1149      link.data('allPublicContent', allPublicContent);
1150
1151      link.on('click', function (e) {
1152        e.preventDefault();
1153        return controller.displayCollections($(this).data('allPublicContent'));
1154      });
1155
1156      li = $("<li class='breadcrumb-item'></li>");
1157
1158      li.append(link);
1159
1160      breadnav.append(li);
1161    }
1162
1163    this.updateAfterResize();
1164  }
1165
1166  recordGA4() {
1167    if (!window.gtag) {
1168      return;
1169    }
1170
1171    let eventName;
1172    let issue;
1173    let magazine;
1174    let pageIndex;
1175
1176    if (this.pageViewController.visible()) {
1177      pageIndex = this.pageViewController.page;
1178      issue = this.pageViewController.issue;
1179
1180      if (!(issue.hasFullVisibility(pageIndex))) {
1181        eventName = 'ee_view_paywall';
1182      } else if (this.pageViewController.displayingSpread()) {
1183        eventName = 'ee_view_spread';
1184      } else {
1185        eventName = 'ee_view_page';
1186      }
1187    } else if (this.thumbnailViewController.visible()) {
1188      eventName = 'ee_view_thumbnails';
1189      issue = this.thumbnailViewController.issue;
1190    } else if (this.issueViewController.visible()) {
1191      eventName = 'ee_view_issues';
1192      magazine = this.issueViewController.magazine;
1193    }
1194
1195    const params = {};
1196
1197    if (pageIndex) {
1198      params.ee_page_index = pageIndex;
1199    }
1200
1201    if (issue) {
1202      params.ee_issue_id = issue.id;
1203      params.ee_issue_name = issue.name;
1204      magazine = issue.magazine;
1205    }
1206
1207    if (magazine) {
1208      params.ee_title_id = magazine.id;
1209      params.ee_title_name = magazine.name;
1210    }
1211
1212    const targets = ['G-VCC34BJ1S3'];
1213
1214    if (magazine && magazine.analyticsId) {
1215      if (magazine.analyticsId.startsWith('G-')) {
1216        targets.push(magazine.analyticsId);
1217
1218        this.analyticsMagazineId = magazine.id;
1219      }
1220    }
1221
1222    targets.forEach((target) => {
1223      window.gtag('config', target);
1224    });
1225
1226    if (!eventName) {
1227      return;
1228    }
1229
1230    window.gtag('event', eventName, {
1231      send_to: targets,
1232      ...params,
1233    });
1234  }
1235
1236  updateAnlyticsConsentLinks(path) {
1237    const params = new URLSearchParams({
1238      redirect: path,
1239    });
1240
1241    const href = `/analytics_consent?${params.toString()}`;
1242
1243    const accept = find('#analytics_consent_accept');
1244
1245    if (accept) {
1246      accept.href = `${href}&consent=1`;
1247    }
1248
1249    const reject = find('#analytics_consent_reject');
1250
1251    if (reject) {
1252      reject.href = `${href}&consent=0`;
1253    }
1254  }
1255
1256  hideClientLogo() {
1257    hide('#client-logo');
1258  }
1259
1260  showClientLogo() {
1261    show('#client-logo');
1262  }
1263
1264  clickaway(e) {
1265    if (e.target === find('.external-link-toolbar')) return;
1266
1267    let popover = null;
1268
1269    const ell = find('.external-link-list');
1270
1271    if (ell) {
1272      if (ell.parentElement) {
1273        popover = ell.parentElement.parentElement;
1274      }
1275    }
1276
1277    if (!popover || popover.contains(e.target)) {
1278      return;
1279    }
1280
1281    $('.external-link-toolbar').popover('destroy');
1282  }
1283
1284  // True if we're showing a particular issue at the moment and it would make sense
1285  // to offer page-by-page navigation, thumbnails, etc.
1286  showingIssue() {
1287    return this.pageViewController.visible() || this.thumbnailViewController.visible();
1288  }
1289
1290  phraseFor(key) {
1291    if (!this.phrases[key]) { this.phrases[key] = this.reader.data(`phrase.${key}`); }
1292
1293
1294    if (!this.phrases[key]) { console.log(`Missing phrase for key ${key}`); }
1295
1296    return this.phrases[key] || '';
1297  }
1298
1299  logPage(issue) {
1300    if (!this.institutional) { return; }
1301
1302    new Request(`/issues/${issue.id}/logpage${this.defaultUrlParams('?')}`)
1303      .html(); // Not really a html endpoint, we just don't care about the
1304    // response and @html doesn't attempt a parse.
1305  }
1306
1307  updateAfterResize() {
1308    // explicitly match window height as 100vh is unreliable on mobile browsers.
1309    const reader = find('#reader');
1310    reader.style.height = `${$(window).height()}px`;
1311
1312    const navbarHeight = $('.navbar')[0].offsetHeight;
1313
1314    $('#reader').css({ 'padding-top': `${navbarHeight}px` });
1315  }
1316
1317  defaultUrlParams(separator) {
1318    const rc = QueryParams.current().roomContext();
1319
1320    if (rc) {
1321      return `${separator}rc=${rc}`;
1322    }
1323    return '';
1324  }
1325
1326  showOrHideLoginLink(issue) {
1327    if (issue && this.convertStringToBoolean(issue.hideLogin)) {
1328      hideAll('.login-to-see');
1329      $('.access-options').css('border', 'none');
1330      return;
1331    }
1332    showAll('.login-to-see');
1333  }
1334
1335  convertStringToBoolean(string) {
1336    if ((string === 'true') || (string === true)) {
1337      return true;
1338    }
1339    return false;
1340  }
1341}

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.