diff --git a/app/assets/images/pdf_js/blocked.svg b/app/assets/images/pdf_js/blocked.svg new file mode 100644 index 000000000..dafa8ad3f --- /dev/null +++ b/app/assets/images/pdf_js/blocked.svg @@ -0,0 +1,78 @@ + diff --git a/app/assets/javascripts/protocols/index.js b/app/assets/javascripts/protocols/index.js index 341b0afa6..0f0f659fd 100644 --- a/app/assets/javascripts/protocols/index.js +++ b/app/assets/javascripts/protocols/index.js @@ -1,5 +1,5 @@ //= require protocols/import_export/import -/* global ProtocolRepositoryHeader */ +/* global ProtocolRepositoryHeader PdfPreview */ // Global variables var rowsSelected = []; @@ -223,6 +223,7 @@ function initProtocolPreviewModal() { modal.modal("show"); ProtocolRepositoryHeader.init(); initHandsOnTable(modalBody); + PdfPreview.initCanvas(); }, error: function (error) { // TODO diff --git a/app/assets/javascripts/protocols/steps.js.erb b/app/assets/javascripts/protocols/steps.js.erb index d6b98b723..681a7de7a 100644 --- a/app/assets/javascripts/protocols/steps.js.erb +++ b/app/assets/javascripts/protocols/steps.js.erb @@ -552,6 +552,7 @@ $new_step.find('.attachments').trigger('reorder'); tinyMCE.editors.step_description_textarea.remove(); MarvinJsEditor.initNewButton('.new-marvinjs-upload-button'); + PdfPreview.initCanvas(); //Rerender tables $new_step.find("div.step-result-hot-table").each(function() { @@ -684,7 +685,8 @@ }, function(result) { viewModeBtn.closest('.dropdown-menu').find('.attachments-view-mode').removeClass('selected'); viewModeBtn.addClass('selected'); - viewModeBtn.closest('.step').find('.attachments').html(result.html) + viewModeBtn.closest('.step').find('.attachments').html(result.html); + PdfPreview.initCanvas(); }) }) } diff --git a/app/assets/javascripts/sitewide/assets.js b/app/assets/javascripts/sitewide/assets.js index f5dd206ff..20d89295e 100644 --- a/app/assets/javascripts/sitewide/assets.js +++ b/app/assets/javascripts/sitewide/assets.js @@ -1,4 +1,4 @@ -/* global windowScrollEvents HelperModule I18n */ +/* global windowScrollEvents HelperModule I18n PdfPreview */ $(document).on('click', '.asset-context-menu .change-preview-type', function(e) { var viewModeBtn = $(this); var viewMode = viewModeBtn.data('preview-type'); @@ -15,6 +15,7 @@ $(document).on('click', '.asset-context-menu .change-preview-type', function(e) viewModeBtn.closest('.dropdown-menu').find('.change-preview-type').removeClass('selected'); viewModeBtn.addClass('selected'); $(`.asset[data-asset-id=${assetId}]`).replaceWith(data.html); + PdfPreview.initCanvas(); } }); }); diff --git a/app/assets/javascripts/sitewide/dropdown_selector.js b/app/assets/javascripts/sitewide/dropdown_selector.js index 2660b50c2..ceb45ff66 100644 --- a/app/assets/javascripts/sitewide/dropdown_selector.js +++ b/app/assets/javascripts/sitewide/dropdown_selector.js @@ -874,6 +874,7 @@ var dropdownSelector = (function() { valuesArray.forEach(function(value) { option = $selector.find(`option[value="${value}"]`)[0]; + option.selected = 'selected'; options.push(convertOptionToJson(option)); }); setData($selector, options); diff --git a/app/assets/javascripts/sitewide/file_preview.js b/app/assets/javascripts/sitewide/file_preview.js index cd0673017..dee82ba68 100644 --- a/app/assets/javascripts/sitewide/file_preview.js +++ b/app/assets/javascripts/sitewide/file_preview.js @@ -1,6 +1,7 @@ /* eslint no-underscore-dangle: ["error", { "allowAfterThis": true }]*/ /* eslint no-use-before-define: ["error", { "functions": false }]*/ /* eslint-disable no-underscore-dangle */ +/* global PdfPreview */ var FilePreviewModal = (function() { 'use strict'; @@ -18,6 +19,7 @@ var FilePreviewModal = (function() { $('#filePreviewModal .modal-content').html(result.html); $('#filePreviewModal').modal('show'); $('.modal-backdrop').last().css('z-index', $('#filePreviewModal').css('z-index') - 1); + PdfPreview.initCanvas(); }); }); diff --git a/app/assets/javascripts/sitewide/pdf_preview.js b/app/assets/javascripts/sitewide/pdf_preview.js new file mode 100644 index 000000000..80b17c019 --- /dev/null +++ b/app/assets/javascripts/sitewide/pdf_preview.js @@ -0,0 +1,198 @@ +/* global pdfjsLib animateSpinner dropdownSelector pdfjsLibUtils PerfectScrollbar */ +/* eslint-disable no-param-reassign, no-use-before-define */ + +var PdfPreview = (function() { + const MIN_ZOOM = 0.25; + const MAX_ZOOM = 3; + const DEFAULT_ZOOM = 1; + const ZOOM_STEP = 0.25; + + var pageRendering = false; + + function initActionButtons() { + $(document) + // Next page + .on('click', '.pdf-viewer .next-page', function() { + var $canvas = $(this).closest('.pdf-viewer').find('.pdf-canvas'); + renderPdfPage($canvas[0], $canvas.data('current-page') + 1); + }) + // Previous field + .on('click', '.pdf-viewer .prev-page', function() { + var $canvas = $(this).closest('.pdf-viewer').find('.pdf-canvas'); + renderPdfPage($canvas[0], $canvas.data('current-page') - 1); + }) + // Page change field + .on('change', '.pdf-viewer .current-page', function() { + var page = parseInt($(this).val(), 10) || 1; + var $canvas = $(this).closest('.pdf-viewer').find('.pdf-canvas'); + var totalPage = $canvas.data('total-page'); + if (page < 1) page = 1; + if (page > totalPage) page = totalPage; + renderPdfPage($canvas[0], page); + }) + // Zoom out button + .on('click', '.pdf-viewer .zoom-out', function() { + var zoomSelector = $(this).closest('.pdf-viewer').find('.zoom-page-selector'); + var $canvas = $(this).closest('.pdf-viewer').find('.pdf-canvas'); + if (zoomSelector.val() === 'auto') { + dropdownSelector.selectValues(zoomSelector, DEFAULT_ZOOM); + } else { + dropdownSelector.selectValues(zoomSelector, parseFloat(zoomSelector.val()) - ZOOM_STEP); + } + renderPdfPage($canvas[0], $canvas.data('current-page')); + }) + // Zoom in button + .on('click', '.pdf-viewer .zoom-in', function() { + var zoomSelector = $(this).closest('.pdf-viewer').find('.zoom-page-selector'); + var $canvas = $(this).closest('.pdf-viewer').find('.pdf-canvas'); + if (zoomSelector.val() === 'auto') { + dropdownSelector.selectValues(zoomSelector, DEFAULT_ZOOM); + } else { + dropdownSelector.selectValues(zoomSelector, parseFloat(zoomSelector.val()) + ZOOM_STEP); + } + renderPdfPage($canvas[0], $canvas.data('current-page')); + }) + // Zoom dropdown + .on('change', '.pdf-viewer .zoom-page-selector', function() { + var $canvas = $(this).closest('.pdf-viewer').find('.pdf-canvas'); + renderPdfPage($canvas[0], $canvas.data('current-page')); + }) + // Load big pdf + .on('click', '.pdf-viewer .load-blocked-pdf', function() { + var $viewer = $(this).closest('.pdf-viewer'); + $viewer.removeClass('blocked'); + $viewer.find('.pdf-canvas').addClass('ready'); + PdfPreview.initCanvas(); + }); + } + + function initZoomDropdown($canvas) { + var zoomSelector = $canvas.closest('.pdf-viewer').find('.zoom-page-selector'); + dropdownSelector.init(zoomSelector, { + noEmptyOption: true, + singleSelect: true, + closeOnSelect: true, + selectAppearance: 'simple', + disableSearch: true + }); + } + + function refreshPageCounter(canvas) { + var $canvas = $(canvas); + var currentPage = $canvas.data('current-page'); + var totalPage = $canvas.data('total-page'); + var counterContainer = $canvas.closest('.pdf-viewer').find('.page-counter'); + counterContainer.find('.current-page').val(currentPage); + counterContainer.find('.total-page').text(totalPage); + $canvas.closest('.pdf-viewer').find('.prev-page') + .attr('disabled', currentPage === 1); + $canvas.closest('.pdf-viewer').find('.next-page') + .attr('disabled', currentPage === totalPage); + } + + function refreshZoomButtons(canvas) { + var $viewer = $(canvas).closest('.pdf-viewer'); + var zoomSelector = $viewer.find('.zoom-page-selector'); + $viewer.find('.zoom-out').attr('disabled', parseFloat(zoomSelector.val()) === MIN_ZOOM); + $viewer.find('.zoom-in').attr('disabled', parseFloat(zoomSelector.val()) === MAX_ZOOM); + } + + function renderPdfPreview(canvas) { + $(canvas).removeClass('ready'); + initZoomDropdown($(canvas)); + animateSpinner($(canvas).closest('.pdf-viewer'), true); + $(canvas).data( + 'custom-scrollbar', + new PerfectScrollbar($(canvas).closest('.page-container')[0]) + ); + pdfjsLib.GlobalWorkerOptions.workerSrc = canvas.dataset.pdfWorkerUrl; + loadPdfDocument(canvas); + } + + + function loadPdfDocument(canvas, page = 1) { + var loadingPdf = pdfjsLib.getDocument(canvas.dataset.pdfUrl); + loadingPdf.promise + .then(function(pdfDocument) { + $(canvas).data('pdfDocument', pdfDocument); + return renderPdfPage(canvas, page); + }) + .catch(function(reason) { + pageRendering = false; + if (reason.status === 202) { + setTimeout(function() { + loadPdfDocument(canvas, page); + }, 5000); + } + }); + } + + function renderPdfPage(canvas, page = 1) { + var pdfDocument = $(canvas).data('pdfDocument'); + if (pageRendering) return false; + pageRendering = true; + pdfDocument.getPage(page).then(function(pdfPage) { + var ctx; + var renderTask; + var userScale = $(canvas).closest('.pdf-viewer').find('.zoom-page-selector').val(); + var $layersContainer = $(canvas).closest('.pdf-viewer').find('.layers-container'); + var scale = userScale === 'auto' ? DEFAULT_ZOOM : userScale; + var viewport = pdfPage.getViewport({ scale: scale }); + var previewContainer = $(canvas).closest('.page-container')[0]; + var $textLayer = $(canvas).closest('.pdf-viewer').find('.textLayer'); + if (previewContainer.clientHeight < viewport.height && userScale === 'auto') { + scale = previewContainer.clientHeight / viewport.height; + } + viewport = pdfPage.getViewport({ scale: scale }); + canvas.width = viewport.width; + canvas.height = viewport.height; + ctx = canvas.getContext('2d'); + renderTask = pdfPage.render({ + canvasContext: ctx, + viewport: viewport + }); + + // Text layer draw + $layersContainer.css({ + height: viewport.height + 'px', + width: viewport.width + 'px' + }); + + pdfPage.getTextContent().then(function(textContent) { + var textLayer = new pdfjsLibUtils.TextLayerBuilder({ + textLayerDiv: $textLayer[0], + pageIndex: page - 1, + viewport: viewport + }); + textLayer.eventBus = new pdfjsLibUtils.EventBus(); + textLayer.setTextContent(textContent); + textLayer.render(); + }); + + $(canvas) + .data('current-page', page) + .data('total-page', pdfDocument.numPages); + $(canvas).data('custom-scrollbar').update(); + animateSpinner($(canvas).closest('.pdf-viewer'), false); + refreshPageCounter(canvas); + refreshZoomButtons(canvas); + pageRendering = false; + return renderTask.promise; + }); + + return true; + } + + return { + initCanvas: function() { + $.each($('.pdf-canvas.ready'), function(i, canvas) { + renderPdfPreview(canvas); + }); + }, + init: function() { + initActionButtons(); + } + }; +}()); + +PdfPreview.init(); diff --git a/app/assets/stylesheets/extend/perfect-scrollbar.scss b/app/assets/stylesheets/extend/perfect-scrollbar.scss index 6407a74e2..056146ae5 100644 --- a/app/assets/stylesheets/extend/perfect-scrollbar.scss +++ b/app/assets/stylesheets/extend/perfect-scrollbar.scss @@ -67,7 +67,7 @@ * Scrollbar thumb styles */ .ps__thumb-x { - background-color: $color-silver-chalice; + background-color: $color-black; border-radius: 4px; transition: background-color .2s linear, height .2s ease-in-out; -webkit-transition: background-color .2s linear, height .2s ease-in-out; @@ -75,11 +75,12 @@ /* there must be 'bottom' for ps__thumb-x */ bottom: 2px; /* please don't change 'position' */ + opacity: .5; position: absolute; } .ps__thumb-y { - background-color: $color-silver-chalice; + background-color: $color-black; border-radius: 3px; transition: background-color .2s linear, width .2s ease-in-out; -webkit-transition: background-color .2s linear, width .2s ease-in-out; @@ -87,6 +88,7 @@ /* there must be 'right' for ps__thumb-y */ right: 2px; /* please don't change 'position' */ + opacity: .5; position: absolute; } diff --git a/app/assets/stylesheets/global_activities.scss b/app/assets/stylesheets/global_activities.scss index fc99ed398..107b23798 100644 --- a/app/assets/stylesheets/global_activities.scss +++ b/app/assets/stylesheets/global_activities.scss @@ -123,6 +123,7 @@ margin-top: 1px; max-width: 500px; overflow: hidden; + text-align: left; text-overflow: ellipsis; white-space: nowrap; width: auto; diff --git a/app/assets/stylesheets/shared/assets.scss b/app/assets/stylesheets/shared/assets.scss index c4408b119..7db26b879 100644 --- a/app/assets/stylesheets/shared/assets.scss +++ b/app/assets/stylesheets/shared/assets.scss @@ -187,6 +187,15 @@ } } + .pdf-viewer { + align-items: center; + background: $color-silver-chalice; + display: flex; + height: calc(100% - 4em); + justify-content: center; + width: 100%; + } + .header { align-items: center; display: flex; diff --git a/app/assets/stylesheets/shared/dropdown_selector.scss b/app/assets/stylesheets/shared/dropdown_selector.scss index 13930d610..3eedf0d7d 100644 --- a/app/assets/stylesheets/shared/dropdown_selector.scss +++ b/app/assets/stylesheets/shared/dropdown_selector.scss @@ -93,6 +93,7 @@ margin-top: 1px; max-width: 240px; overflow: hidden; + text-align: left; text-overflow: ellipsis; white-space: nowrap; width: auto; @@ -276,6 +277,7 @@ .ds-simple { .tag-label { overflow: hidden; + text-align: left; text-overflow: ellipsis; white-space: nowrap; diff --git a/app/assets/stylesheets/shared/file_preview.scss b/app/assets/stylesheets/shared/file_preview.scss index 3b5fe353d..908e2eada 100644 --- a/app/assets/stylesheets/shared/file_preview.scss +++ b/app/assets/stylesheets/shared/file_preview.scss @@ -145,3 +145,7 @@ } } } + +.modal-backdrop.in { + opacity: .4; +} diff --git a/app/assets/stylesheets/shared/pdf_preview.scss b/app/assets/stylesheets/shared/pdf_preview.scss new file mode 100644 index 000000000..4b0772a74 --- /dev/null +++ b/app/assets/stylesheets/shared/pdf_preview.scss @@ -0,0 +1,96 @@ +.pdf-viewer { + display: flex; + flex-direction: column; + height: 100%; + position: relative; + width: 100%; + + .page-container { + display: flex; + flex-grow: 1; + overflow: auto; + padding: 1em; + position: relative; + width: 100%; + + .layers-container { + margin: 0 auto; + position: relative; + } + } + + .pdf-toolbar { + align-items: center; + background: $color-concrete; + display: inline-flex; + flex-wrap: wrap; + justify-content: center; + padding: .5em; + width: 100%; + + .page-counter { + padding: 0 .25em; + width: auto; + } + + .current-page { + margin-right: .25em; + max-width: 3em; + } + + .total-page { + margin-left: .25em; + } + + .divider { + background: $color-alto; + height: 2em; + margin: 0 1em; + width: 2px; + } + + .zoom-page { + margin-right: .5em; + width: 10em; + } + + .btn:disabled { + background: $color-concrete; + } + } + + .blocked-screen { + align-items: center; + color: $color-white; + display: none; + height: 100%; + justify-content: center; + position: absolute; + width: 100%; + + .title { + @include font-h1; + } + + .description { + @include font-main; + margin-bottom: 2em; + } + + .image { + background: unset; + margin-bottom: 2em; + } + } + + &.blocked { + .pdf-toolbar { + display: none; + } + + .blocked-screen { + display: flex; + flex-direction: column; + } + } +} diff --git a/app/controllers/result_assets_controller.rb b/app/controllers/result_assets_controller.rb index c9ad9347b..61c567f13 100644 --- a/app/controllers/result_assets_controller.rb +++ b/app/controllers/result_assets_controller.rb @@ -29,7 +29,6 @@ class ResultAssetsController < ApplicationController if obj.fetch(:status) flash[:success] = t('result_assets.create.success_flash', module: @my_module.name) - p params.as_json redirect_to results_my_module_path(@my_module, page: params[:page], order: params[:order]) else flash[:error] = t('result_assets.error_flash') diff --git a/app/javascript/packs/pdfjs/images/loading-icon.gif b/app/javascript/packs/pdfjs/images/loading-icon.gif new file mode 100644 index 000000000..1c72ebb55 Binary files /dev/null and b/app/javascript/packs/pdfjs/images/loading-icon.gif differ diff --git a/app/javascript/packs/pdfjs/images/shadow.png b/app/javascript/packs/pdfjs/images/shadow.png new file mode 100644 index 000000000..31d3bdb14 Binary files /dev/null and b/app/javascript/packs/pdfjs/images/shadow.png differ diff --git a/app/javascript/packs/pdfjs/pdf_js.js b/app/javascript/packs/pdfjs/pdf_js.js new file mode 100644 index 000000000..b0daf3a66 --- /dev/null +++ b/app/javascript/packs/pdfjs/pdf_js.js @@ -0,0 +1,4 @@ +global.pdfjsLib = require('pdfjs-dist'); +global.pdfjsLibUtils = require('pdfjs-dist/web/pdf_viewer.js'); + +PdfPreview.initCanvas(); diff --git a/app/javascript/packs/pdfjs/pdf_js_styles.scss b/app/javascript/packs/pdfjs/pdf_js_styles.scss new file mode 100644 index 000000000..1c80c3289 --- /dev/null +++ b/app/javascript/packs/pdfjs/pdf_js_styles.scss @@ -0,0 +1 @@ +@import "~pdfjs-dist/web/pdf_viewer"; diff --git a/app/javascript/packs/pdfjs/pdf_js_worker.js b/app/javascript/packs/pdfjs/pdf_js_worker.js new file mode 100644 index 000000000..15189207a --- /dev/null +++ b/app/javascript/packs/pdfjs/pdf_js_worker.js @@ -0,0 +1 @@ +require('pdfjs-dist/build/pdf.worker.js'); diff --git a/app/models/asset.rb b/app/models/asset.rb index 237e0e345..125c918d5 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -246,6 +246,14 @@ class Asset < ApplicationRecord false end + def pdf? + content_type == 'application/pdf' + end + + def pdf_previewable? + pdf? || (previewable_document?(blob) && Rails.application.config.x.enable_pdf_previews) + end + def post_process_file(team = nil) # Extract asset text if it's of correct type if text? diff --git a/app/views/assets/_asset_inline.html.erb b/app/views/assets/_asset_inline.html.erb index 33d6ece79..123522786 100644 --- a/app/views/assets/_asset_inline.html.erb +++ b/app/views/assets/_asset_inline.html.erb @@ -26,6 +26,8 @@ <% if wopi_enabled? && wopi_file?(asset) %>
+ <% elsif asset.pdf_previewable? %> + <%= render partial: 'shared/pdf_viewer.html.erb', locals: { asset: asset } %> <% elsif asset.previewable? %>