import { Controller } from '@hotwired/stimulus'; export default class extends Controller { static targets = ['container', 'zoomValue', 'pageNo', 'pageCount']; static values = { visibleThreshold: { type: Number, default: 0.5 }, extraPagesToLoad: { type: Number, default: 3 }, pageClass: { type: String, default: 'pdf-page' }, contentClass: { type: String, default: 'pdf-content-wrapper' }, zoomValues: { type: Array, default: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 4, 8] }, zoomFillArea: { type: Number, default: 0.95 }, renderingScale: { type: Number, default: 1.5 }, pdfUrl: String, initialZoom: { type: String, default: 'fit' }, workerSrc: String, }; connect() { this.pages = []; this.pdf = null; this._pagesLoading = []; this._loading = false; this._visibles = []; this._activePage = null; this._documentReady = false; this._rotation = 0; this.currentZoom = 1; // Default zoom if (this.workerSrcValue && this.pdfUrlValue && this.pdfUrlValue.length> 0) { import('pdfjs-dist').then((pdfjs) => { this.pdfjs = pdfjs; // PDF.js의 workerSrc 설정 this.pdfjs.GlobalWorkerOptions.workerSrc = this.workerSrcValue; // PDF 문서를 로드 this.loadDocument().then(() => { this.setZoom(this.initialZoomValue); }); // 스크롤 이벤트 핸들러 바인딩 this._setScrollListener(); }); } } disconnect() { this.containerTarget.removeEventListener('scroll', this.__scrollHandler); } // Public API methods async loadDocument() { this._documentReady = false; this.pages = []; this.clearPages(); this.pdf = null; let loader = document.createElement('div'); loader.classList.add('pdf-loader'); this.containerTarget.appendChild(loader); let loadingTask = this.pdfjs.getDocument(this.pdfUrlValue); return loadingTask.promise .then((pdf) => { this.pdf = pdf; this.pageCount = pdf.numPages; this._rotation = 0; return this.forceViewerInitialization(); }) .then(() => { this.onDocumentReady(); this.dispatch('documentready', { detail: { document: this.pdf } }); this._setActivePage(0); this._documentReady = true; this._setActivePage(1); }); } forceViewerInitialization() { this.pages = []; this.clearPages(); this._pagesLoading = []; this._loading = false; this._visibles = []; this._activePage = null; return this.pdf.getPage(1).then((page) => { this._createSkeletons(page); this._visiblePages(); this._setActivePage(1); }); } onZoomChange(zoom) { this.zoomValueTarget.innerHTML = `${parseInt(zoom * 10000) / 100}%`; } onActivePageChanged(page) { let pageno = page.dataset.page; let pagetotal = this.getPageCount(); if (!page.classList.contains('hidden')) { this.pageNoTarget.innerHTML = pageno; this.pageNoTarget['max'] = pagetotal; this.pageCountTarget.innerHTML = pagetotal; } } onDocumentReady() { let loader = this.containerTarget.querySelector('.pdf-loader'); if (loader) loader.remove(); this.setZoom(this.initialZoomValue); } handleScroll() { this.scrollToPage(this.pageNoTarget.value); } handleZoom(event) { this.setZoom(event.params['zoom']); } setZoom(zoom) { const container = this.containerTarget; const prevzoom = this.currentZoom; const prevScroll = { top: container.scrollTop, left: container.scrollLeft, }; this._zoomPages(zoom); container.scrollLeft = (prevScroll.left * this.currentZoom) / prevzoom; container.scrollTop = (prevScroll.top * this.currentZoom) / prevzoom; this._visiblePages(true); if (this._documentReady) { this.onZoomChange(this.currentZoom); this.dispatch('zoomchange', { detail: { zoom: this.currentZoom } }); } return this.currentZoom; } getZoom() { return this.currentZoom; } // Navigation methods next() { if (this._activePage < this.pdf.numPages) { this.scrollToPage(this._activePage + 1); } } prev() { if (this._activePage> 1) { this.scrollToPage(this._activePage - 1); } } first() { if (this._activePage !== 1) { this.scrollToPage(1); } } last() { if (this.pdf === null) return; if (this._activePage !== this.pdf.numPages) { this.scrollToPage(this.pdf.numPages); } } scrollToPage(i) { if (this.pages.length === 0 || this.pages[i] === undefined) { return; } const page = this.pages[i].div; if (!page) { console.warn(`Page ${i} not found`); return; } const position = { top: page.offsetTop, left: page.offsetLeft, }; if (position) { this.containerTarget.scrollTop = position.top; this.containerTarget.scrollLeft = position.left; } this._setActivePage(i); } rotate(deg, accumulate = false) { if (accumulate) { deg = deg + this._rotation; } this._rotation = deg; const container = this.containerTarget; const prevScroll = { top: container.scrollTop, left: container.scrollLeft, height: container.scrollHeight, width: container.scrollWidth, }; return this.forceViewerInitialization().then(() => { const newScroll = { top: container.scrollTop, left: container.scrollLeft, height: container.scrollHeight, width: container.scrollWidth, }; container.scrollTop = prevScroll.top * (newScroll.height / prevScroll.height); container.scrollLeft = prevScroll.left * (newScroll.width / prevScroll.width); }); } refreshAll() { this._visiblePages(true); } // Private methods _setScrollListener() { let scrollLock = false; let scrollPos = { top: 0, left: 0, }; this.__scrollHandler = (e) => { if (this._activePage) { this.pageNoTarget.value = this._activePage; } if (scrollLock === true) { return; } scrollLock = true; const container = this.containerTarget; if ( Math.abs(container.scrollTop - scrollPos.top)> container.clientHeight * 0.2 * this.currentZoom || Math.abs(container.scrollLeft - scrollPos.left)> container.clientWidth * 0.2 * this.currentZoom ) { scrollPos = { top: container.scrollTop, left: container.scrollLeft, }; this._visiblePages(); } scrollLock = false; }; this.containerTarget.addEventListener('scroll', this.__scrollHandler); } _createSkeleton(page, i) { const pageinfo = { div: null, width: 0, height: 0, loaded: false, }; if (page.getViewport !== undefined) { const viewport = page.getViewport({ rotation: this._rotation, scale: 1, }); pageinfo.width = viewport.width; pageinfo.height = viewport.height; pageinfo.loaded = true; } else { pageinfo.width = page.width; pageinfo.height = page.height; } console.assert(pageinfo.width> 0 && pageinfo.height> 0, 'Page width and height must be greater than 0'); // Create page div pageinfo.div = document.createElement('div'); pageinfo.div.id = `page-${i}`; pageinfo.div.dataset.page = i; pageinfo.div.dataset.width = pageinfo.width; pageinfo.div.dataset.height = pageinfo.height; pageinfo.div.dataset.zoom = this.currentZoom; pageinfo.div.classList.add(this.pageClassValue); pageinfo.div.style.width = `${pageinfo.width * this.currentZoom}px`; pageinfo.div.style.height = `${pageinfo.height * this.currentZoom}px`; // Create content wrapper const content = document.createElement('div'); content.classList.add(this.contentClassValue); content.style.width = `${pageinfo.width}px`; content.style.height = `${pageinfo.height}px`; pageinfo.div.appendChild(content); this._cleanPage(pageinfo.div); return pageinfo; } _placeSkeleton(pageinfo, i) { let prevpage = i - 1; let prevpageEl = null; while ( prevpage> 0 && !(prevpageEl = this.containerTarget.querySelector(`.${this.pageClassValue}[data-page="${prevpage}"]`)) ) { prevpage--; } if (prevpage === 0) { this.containerTarget.appendChild(pageinfo.div); } else { prevpageEl.after(pageinfo.div); } } _createSkeletons(pageinfo) { for (let i = 1; i <= this.pageCount; i++) { if (this.pages[i] === undefined) { pageinfo = this._createSkeleton(pageinfo, i); this.pages[i] = pageinfo; this._placeSkeleton(pageinfo, i); this.dispatch('newpage', { detail: { pageNumber: i, page: pageinfo.div, }, }); } } } _setActivePage(i) { if (this._activePage !== i) { this._activePage = i; const activePage = this.getActivePage(); if (this._documentReady) { this.onActivePageChanged(activePage); this.dispatch('activepagechanged', { detail: { activePageNumber: i, activePage: activePage, }, }); } } } _areaOfPageVisible(page) { if (!page) { return 0; } const containerRect = this.containerTarget.getBoundingClientRect(); const pageRect = page.getBoundingClientRect(); // Calculate relative position const position = { top: pageRect.top - containerRect.top, left: pageRect.left - containerRect.left, bottom: pageRect.bottom - containerRect.top, right: pageRect.right - containerRect.left, }; const c_height = this.containerTarget.clientHeight; const c_width = this.containerTarget.clientWidth; const page_y0 = Math.min(Math.max(position.top, 0), c_height); const page_y1 = Math.min(Math.max(position.bottom, 0), c_height); const page_x0 = Math.min(Math.max(position.left, 0), c_width); const page_x1 = Math.min(Math.max(position.right, 0), c_width); const vis_x = page_x1 - page_x0; const vis_y = page_y1 - page_y0; return vis_x * vis_y; } isPageVisible(i) { if (this.pdf === null || i === undefined || i === null || i < 1 || i> this.pdf.numPages) { return false; } if (typeof i === 'string') { i = parseInt(i); } let page = i; if (typeof i === 'number') { if (this.pages[i] === undefined) return false; page = this.pages[i].div; } const pageArea = page.offsetWidth * page.offsetHeight; return this._areaOfPageVisible(page)> pageArea * this.visibleThresholdValue; } _visiblePages(forceRedraw = false) { let max_area = 0; let i_page = null; if (this.pages.length === 0) { this._visibles = []; this._setActivePage(0); return; } // Find visible pages const visiblePages = []; const visiblePageNumbers = []; for (let i = 1; i < this.pages.length; i++) { const pageinfo = this.pages[i]; if (!pageinfo) continue; const areaVisible = this._areaOfPageVisible(pageinfo.div); if (areaVisible> 0) { visiblePages.push(pageinfo.div); visiblePageNumbers.push(i); if (areaVisible> max_area) { max_area = areaVisible; i_page = parseInt(pageinfo.div.dataset.page); } } } this._setActivePage(i_page); if (visiblePageNumbers.length> 0) { const minVisible = Math.min(...visiblePageNumbers); const maxVisible = Math.max(...visiblePageNumbers); for (let i = Math.max(1, minVisible - this.extraPagesToLoadValue); i < minVisible; i++) { if (!visiblePageNumbers.includes(i)) visiblePageNumbers.push(i); } for (let i = maxVisible + 1; i <= Math.min(maxVisible + this.extraPagesToLoadValue, this.pdf.numPages); i++) { if (!visiblePageNumbers.includes(i)) visiblePageNumbers.push(i); } } let nowVisibles = visiblePageNumbers; if (!forceRedraw) { nowVisibles = visiblePageNumbers.filter((x) => !this._visibles.includes(x)); } // Clean pages that are no longer visible this._visibles .filter((x) => !visiblePageNumbers.includes(x)) .forEach((i) => { this._cleanPage(this.pages[i].div); }); this._visibles = visiblePageNumbers; this.loadPages(...nowVisibles); } loadPages(...pages) { this._pagesLoading.push(...pages); if (this._loading) { return; } this._loadingTask(); } _loadingTask() { this._loading = true; if (this._pagesLoading.length> 0) { const pagei = this._pagesLoading.shift(); this.pdf .getPage(pagei) .then((page) => { return this._renderPage(page, pagei); }) .then((pageinfo) => { if (this._pagesLoading.length> 0) { this._loadingTask(); } }); } this._loading = false; } _renderPage(page, i) { const pageinfo = this.pages[i]; const scale = this.renderingScaleValue; const pixelRatio = window.devicePixelRatio || 1; const viewport = page.getViewport({ rotation: this._rotation, scale: this.currentZoom * scale, }); pageinfo.width = viewport.width / this.currentZoom / scale; pageinfo.height = viewport.height / this.currentZoom / scale; pageinfo.div.dataset.width = pageinfo.width; pageinfo.div.dataset.height = pageinfo.height; pageinfo.div.style.width = `${pageinfo.width * this.currentZoom}px`; pageinfo.div.style.height = `${pageinfo.height * this.currentZoom}px`; pageinfo.loaded = true; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.height = viewport.height * pixelRatio; canvas.width = viewport.width * pixelRatio; canvas.getContext('2d'); const transform = pixelRatio !== 1 ? [pixelRatio, 0, 0, pixelRatio, 0, 0] : null; const renderContext = { canvasContext: context, viewport: viewport, transform: transform, }; return page.render(renderContext).promise.then(() => { this._setPageContent(pageinfo.div, canvas); if (this._documentReady) { this.dispatch('pagerender', { detail: { pageNumber: i, page: pageinfo.div, }, }); } return pageinfo; }); } _cleanPage(page) { const content = page.querySelector(`.${this.contentClassValue}`); if (content) { content.innerHTML = ''; // Add loader const loader = document.createElement('div'); loader.classList.add('pdf-loader'); content.appendChild(loader); } } _setPageContent(page, content) { const wrapper = page.querySelector(`.${this.contentClassValue}`); if (wrapper) { wrapper.style.width = page.style.width; wrapper.style.height = page.style.height; wrapper.innerHTML = ''; wrapper.appendChild(content); } } getActivePage() { if (this._activePage === null || this.pdf === null) { return null; } if (this._activePage < 1 || this._activePage> this.pdf.numPages) { return null; } return this.pages[this._activePage].div; } getPages() { return this.pages; } getPageCount() { if (this.pdf === null) { return 0; } return this.pdf.numPages; } // Zoom functionality _getZoomValue(zoom = null) { if (zoom === null) { return this.currentZoom; } if (parseFloat(zoom) == zoom) { return zoom; } const activePage = this.getActivePage(); let zoomValues = []; switch (zoom) { case 'in': zoom = this.currentZoom; zoomValues = this.zoomValuesValue.filter((x) => x> zoom); if (zoomValues.length> 0) { zoom = Math.min(...zoomValues); } break; case 'out': zoom = this.currentZoom; zoomValues = this.zoomValuesValue.filter((x) => x < zoom); if (zoomValues.length> 0) { zoom = Math.max(...zoomValues); } break; case 'fit': zoom = Math.min(this._getZoomValue('width'), this._getZoomValue('height')); break; case 'width': zoom = (this.zoomFillAreaValue * this.containerTarget.clientWidth) / parseFloat(activePage.dataset.width); break; case 'height': zoom = (this.zoomFillAreaValue * this.containerTarget.clientHeight) / parseFloat(activePage.dataset.height); break; default: zoom = this.currentZoom; break; } return zoom; } _zoomPages(zoom) { zoom = this._getZoomValue(zoom); this.pages.forEach((pageinfo) => { if (!pageinfo || !pageinfo.div) return; const c_width = parseFloat(pageinfo.div.dataset.width); const c_height = parseFloat(pageinfo.div.dataset.height); pageinfo.div.style.width = `${c_width * zoom}px`; pageinfo.div.style.height = `${c_height * zoom}px`; pageinfo.div.dataset.zoom = zoom; const content = pageinfo.div.querySelector(`.${this.contentClassValue}`); if (content) { content.style.width = `${c_width * zoom}px`; content.style.height = `${c_height * zoom}px`; } }); this.currentZoom = zoom; } // Helper methods clearPages() { const pages = this.containerTarget.querySelectorAll(`.${this.pageClassValue}`); pages.forEach((page) => page.remove()); } // Custom event dispatcher dispatch(eventName, detail = {}) { const event = new CustomEvent(eventName, detail); this.containerTarget.dispatchEvent(event); } }

AltStyle によって変換されたページ (->オリジナル) /