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.