├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .scss-lint.yml ├── Capfile ├── Gemfile ├── Gemfile.lock ├── ISSUE_TEMPLATE.md ├── Jenkinsfile ├── LICENSE ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── fonts │ │ ├── sul-icons.eot │ │ ├── sul-icons.svg │ │ ├── sul-icons.ttf │ │ └── sul-icons.woff │ ├── images │ │ ├── .keep │ │ ├── by.png │ │ ├── by.svg │ │ ├── iiif-drag-icon.png │ │ ├── images │ │ │ ├── layers-2x.png │ │ │ ├── layers.png │ │ │ ├── marker-icon-2x.png │ │ │ ├── marker-icon.png │ │ │ └── marker-shadow.png │ │ ├── locked-media-poster.svg │ │ ├── pdm.png │ │ ├── pdm.svg │ │ ├── stanford_red_s.png │ │ ├── stanford_s.png │ │ ├── stanford_s.svg │ │ ├── sul_stacked_color.svg │ │ └── waveform-audio-poster.svg │ └── stylesheets │ │ ├── common.css │ │ ├── companion_window.css │ │ ├── content_list.css │ │ ├── document.css │ │ ├── file.css │ │ ├── geo.css │ │ ├── leaflet.css │ │ ├── locked_status.css │ │ ├── m3.css │ │ ├── media.css │ │ ├── model.css │ │ └── webarchive.css ├── components │ ├── companion_windows │ │ ├── authorization_messages_component.html.erb │ │ ├── authorization_messages_component.rb │ │ ├── content_list_component.html.erb │ │ ├── content_list_component.rb │ │ ├── content_not_available_component.html.erb │ │ ├── content_not_available_component.rb │ │ ├── control_button_component.rb │ │ ├── external_link_component.html.erb │ │ ├── external_link_component.rb │ │ ├── fullscreen_button_component.html.erb │ │ ├── fullscreen_button_component.rb │ │ ├── rights_component.html.erb │ │ ├── rights_component.rb │ │ ├── share_button_component.html.erb │ │ └── share_button_component.rb │ ├── companion_windows_component.html.erb │ ├── companion_windows_component.rb │ ├── dialog_component.html.erb │ ├── dialog_component.rb │ ├── document │ │ ├── content_list_item_component.html.erb │ │ └── content_list_item_component.rb │ ├── download │ │ ├── all_files_component.html.erb │ │ ├── all_files_component.rb │ │ ├── file_list_component.html.erb │ │ ├── file_list_component.rb │ │ ├── file_list_item_component.html.erb │ │ └── file_list_item_component.rb │ ├── download_access_component.html.erb │ ├── download_access_component.rb │ ├── embed │ │ └── file │ │ │ ├── auth_messages_component.html.erb │ │ │ ├── auth_messages_component.rb │ │ │ ├── dir_row_component.html.erb │ │ │ ├── dir_row_component.rb │ │ │ ├── file_row_component.html.erb │ │ │ └── file_row_component.rb │ ├── embed_this_form_component.html.erb │ ├── embed_this_form_component.rb │ ├── file_component.html.erb │ ├── file_component.rb │ ├── geo_component.html.erb │ ├── geo_component.rb │ ├── icons │ │ ├── audio_file_component.html.erb │ │ ├── audio_file_component.rb │ │ ├── description_component.html.erb │ │ ├── description_component.rb │ │ ├── download_component.html.erb │ │ ├── download_component.rb │ │ ├── file_present_component.html.erb │ │ ├── file_present_component.rb │ │ ├── folder_component.html.erb │ │ ├── folder_component.rb │ │ ├── folder_zip_component.html.erb │ │ ├── folder_zip_component.rb │ │ ├── image_component.html.erb │ │ ├── image_component.rb │ │ ├── insert_chart_component.html.erb │ │ ├── insert_chart_component.rb │ │ ├── insert_drive_file_component.html.erb │ │ ├── insert_drive_file_component.rb │ │ ├── lock_clock_component.html.erb │ │ ├── lock_clock_component.rb │ │ ├── lock_component.html.erb │ │ ├── lock_component.rb │ │ ├── lock_globe_component.html.erb │ │ ├── lock_globe_component.rb │ │ ├── picture_as_pdf_component.html.erb │ │ ├── picture_as_pdf_component.rb │ │ ├── stanford_only_component.html.erb │ │ ├── stanford_only_component.rb │ │ ├── terminal_component.html.erb │ │ ├── terminal_component.rb │ │ ├── video_file_component.html.erb │ │ ├── video_file_component.rb │ │ ├── web_archive_component.html.erb │ │ └── web_archive_component.rb │ ├── iframe_component.rb │ ├── locked_status_component.rb │ ├── login_component.html.erb │ ├── login_component.rb │ ├── m3_component.html.erb │ ├── m3_component.rb │ ├── media │ │ ├── prev_next_component.html.erb │ │ ├── prev_next_component.rb │ │ ├── preview_image_component.rb │ │ ├── tag_component.rb │ │ └── wrapper_component.rb │ ├── media_component.html.erb │ ├── media_component.rb │ ├── model_component.html.erb │ ├── model_component.rb │ ├── pdf_component.html.erb │ ├── pdf_component.rb │ ├── restricted_message_component.html.erb │ ├── restricted_message_component.rb │ ├── tabpanel_component.html.erb │ ├── tabpanel_component.rb │ ├── view_access_component.html.erb │ ├── view_access_component.rb │ ├── web_archive_component.html.erb │ └── web_archive_component.rb ├── controllers │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ ├── embed_controller.rb │ └── pages_controller.rb ├── javascript │ ├── controllers │ │ ├── application.js │ │ ├── clipboard_controller.js │ │ ├── companion_window_controller.js │ │ ├── content_list_controller.js │ │ ├── cue_controller.js │ │ ├── embed_this_controller.js │ │ ├── file_auth_controller.js │ │ ├── fullscreen_controller.js │ │ ├── geo_controller.js │ │ ├── iiif_auth_restriction_controller.js │ │ ├── iiif_manifest_loader_controller.js │ │ ├── iiif_metadata_controller.js │ │ ├── image_controller.js │ │ ├── index.js │ │ ├── locked_media_poster_controller.js │ │ ├── locked_poster_controller.js │ │ ├── media_player_controller.js │ │ ├── media_tag_controller.js │ │ ├── media_wrapper_controller.js │ │ ├── meta_check_controller.js │ │ ├── model_controller.js │ │ ├── osd_controller.js │ │ ├── pdf_controller.js │ │ ├── prev_next_controller.js │ │ ├── tab_controller.js │ │ ├── tooltip_controller.js │ │ └── transcript_controller.js │ ├── document.js │ ├── file.js │ ├── file_controllers │ │ ├── application.js │ │ ├── download_all_controller.js │ │ ├── index.js │ │ └── tree_controller.js │ ├── geo.js │ ├── geo │ │ └── leaflet_opacity.js │ ├── media.js │ ├── model.js │ ├── packs │ │ └── m3.js │ ├── src │ │ ├── components │ │ │ ├── CdlAuthenticationControl.jsx │ │ │ ├── CdlCopyright.jsx │ │ │ ├── CdlCountdown.jsx │ │ │ ├── CdlLoginWindowSizing.jsx │ │ │ ├── CdlLogout.jsx │ │ │ ├── DueDate.jsx │ │ │ └── embedMode.jsx │ │ ├── modules │ │ │ ├── m3_viewer.js │ │ │ ├── metrics.js │ │ │ └── thumbnail.js │ │ └── plugins │ │ │ ├── analyticsPlugin.js │ │ │ ├── cdlAuthPlugin.js │ │ │ ├── embedModePlugin.js │ │ │ ├── miradorZoomBugPlugin.jsx │ │ │ ├── shareMenuPlugin.jsx │ │ │ └── xywhPlugin.js │ └── webarchive.js ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ └── embed │ │ ├── envelope.rb │ │ ├── hierarchical_contents.rb │ │ ├── purl.rb │ │ ├── purl │ │ ├── file_json_deserializer.rb │ │ ├── media_file.rb │ │ ├── resource.rb │ │ ├── resource_dir.rb │ │ ├── resource_file.rb │ │ └── resource_json_deserializer.rb │ │ ├── purl_json_loader.rb │ │ ├── response.rb │ │ └── was_time_map.rb ├── viewers │ └── embed │ │ ├── viewer │ │ ├── authorization.rb │ │ ├── common_viewer.rb │ │ ├── document_viewer.rb │ │ ├── file.rb │ │ ├── geo.rb │ │ ├── m3_viewer.rb │ │ ├── media.rb │ │ ├── model_viewer.rb │ │ └── was_seed.rb │ │ └── viewer_factory.rb └── views │ ├── embed │ ├── _analytics.html.erb │ ├── iframe.html.erb │ └── iiif.html.erb │ ├── layouts │ └── preview │ │ ├── file.html.erb │ │ ├── geo.html.erb │ │ ├── media.html.erb │ │ ├── model.html.erb │ │ └── pdf.html.erb │ └── pages │ ├── home.html.erb │ └── sandbox.html.erb ├── babel.config.js ├── bin ├── brakeman ├── bundle ├── dev ├── importmap ├── rails ├── rake ├── rdbg ├── rubocop ├── setup ├── shakapacker ├── shakapacker-dev-server ├── thrust ├── update └── yarn ├── compose.yaml ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── deploy.rb ├── deploy │ ├── prod.rb │ ├── stage.rb │ └── uat.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── importmap.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── config.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── high_voltage.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── okcomputer.rb │ ├── permissions_policy.rb │ └── wrap_parameters.rb ├── licenses.yml ├── locales │ ├── en.yml │ └── okcomputer_en.yml ├── puma.rb ├── routes.rb ├── settings.yml ├── settings │ ├── development.yml │ ├── production.yml │ └── test.yml ├── shakapacker.yml ├── spring.rb └── webpack │ ├── development.js │ ├── production.js │ └── webpack.config.js ├── lib ├── assets │ └── .keep ├── bcp47.rb ├── bcp47 │ ├── parser.rb │ ├── record.rb │ └── registry.rb ├── constants.rb ├── embed.rb ├── embed │ ├── mimetypes.rb │ ├── pretty_filesize.rb │ ├── request.rb │ ├── response.rb │ └── stacks_image.rb ├── size_converter.rb └── tasks │ ├── .keep │ └── precompile.rake ├── log └── .keep ├── package.json ├── postcss.config.js ├── public ├── 400.html ├── 404.html ├── 406-unsupported-browser.html ├── 422.html ├── 500.html ├── favicon.ico └── robots.txt ├── spec ├── components │ ├── companion_windows │ │ ├── content_list_component_spec.rb │ │ ├── control_button_component_spec.rb │ │ ├── fullscreen_button_component_spec.rb │ │ ├── rights_component_spec.rb │ │ └── share_button_component_spec.rb │ ├── companion_windows_component_spec.rb │ ├── dialog_component_spec.rb │ ├── download │ │ ├── all_files_component_spec.rb │ │ └── file_list_component_spec.rb │ ├── embed │ │ └── file │ │ │ └── auth_messages_component_spec.rb │ ├── embed_this_form_component_spec.rb │ ├── file_component_spec.rb │ ├── geo_component_spec.rb │ ├── icons │ │ └── stanford_only_component_spec.rb │ ├── iframe_component_spec.rb │ ├── m3_component_spec.rb │ ├── media │ │ ├── preview_image_component_spec.rb │ │ ├── tag_component_spec.rb │ │ └── wrapper_component_spec.rb │ ├── media_component_spec.rb │ ├── model_component_spec.rb │ ├── pdf_component_spec.rb │ ├── restricted_message_component_spec.rb │ └── web_archive_component_spec.rb ├── factories │ ├── files.rb │ ├── purls.rb │ └── resources.rb ├── features │ ├── 3d_viewer_spec.rb │ ├── document_viewer_spec.rb │ ├── embed_this_panel_spec.rb │ ├── file_hierarchy_spec.rb │ ├── file_search_spec.rb │ ├── file_viewer_spec.rb │ ├── geo_viewer_spec.rb │ ├── iiif_embed_spec.rb │ ├── media_viewer_spec.rb │ ├── metrics_spec.rb │ ├── sandbox_spec.rb │ ├── status_spec.rb │ └── was_seed_viewer_spec.rb ├── fixtures │ ├── purl_fixtures.rb │ └── was_time_map_fixtures.rb ├── lib │ ├── bcp47 │ │ ├── parser_spec.rb │ │ ├── record_spec.rb │ │ └── registry_spec.rb │ └── embed │ │ ├── mimetypes_spec.rb │ │ ├── pretty_filesize_spec.rb │ │ ├── request_spec.rb │ │ ├── response_spec.rb │ │ ├── stacks_image_spec.rb │ │ ├── viewer │ │ ├── common_viewer_spec.rb │ │ ├── file_spec.rb │ │ ├── geo_spec.rb │ │ ├── m3_viewer_spec.rb │ │ ├── media_spec.rb │ │ ├── model_viewer_spec.rb │ │ └── was_seed_spec.rb │ │ └── viewer_factory_spec.rb ├── models │ └── embed │ │ ├── envelope_spec.rb │ │ ├── hierarchical_contents_spec.rb │ │ ├── purl │ │ ├── media_file_spec.rb │ │ ├── resource_file_spec.rb │ │ └── resource_spec.rb │ │ ├── purl_json_loader_spec.rb │ │ ├── purl_spec.rb │ │ └── was_time_map_spec.rb ├── rails_helper.rb ├── requests │ └── embed_spec.rb ├── spec_helper.rb └── support │ ├── metrics_helper.rb │ └── stub_apps │ └── stub_metrics_api.rb ├── test └── components │ └── previews │ └── embed │ ├── companion_window_component_preview.rb │ ├── file_component_preview.rb │ ├── geo_component_preview.rb │ ├── legacy │ ├── geo_component_preview.rb │ └── model_component_preview.rb │ ├── media_component_preview.rb │ ├── model_component_preview.rb │ └── pdf_component_preview.rb ├── vendor ├── data │ └── language-subtag-registry └── javascript │ ├── .keep │ ├── Leaflet.Control.Custom.js │ └── leaflet.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest/globals": true 4 | }, 5 | "extends": ["airbnb","react-app"], 6 | "globals": { 7 | "page": true, 8 | "document": true 9 | }, 10 | "parser": "babel-eslint", 11 | "plugins": ["jest"], 12 | "rules": { 13 | "import/prefer-default-export": "off", 14 | "no-console": "off", 15 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 16 | "require-jsdoc": ["error", { 17 | "require": { 18 | "FunctionDeclaration": true, 19 | "MethodDefinition": true, 20 | "ClassDeclaration": true, 21 | "ArrowFunctionExpression": true, 22 | "FunctionExpression": true 23 | } 24 | }], 25 | "no-underscore-dangle": "off", 26 | "react/prefer-stateless-function": "off", 27 | "sort-keys": ["error", "asc", { 28 | "caseSensitive": false, 29 | "natural": false 30 | }], 31 | "react/jsx-props-no-spreading": "off", 32 | "arrow-parens": "off", 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby: ["3.4"] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ${{ matrix.ruby }} 21 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 18 25 | - run: bin/yarn install 26 | - name: Run tests 27 | run: bin/rake 28 | env: 29 | RAILS_ENV: test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore capistrano config. 11 | /.capistrano 12 | 13 | # Ignore ruby version config. 14 | /.ruby-version 15 | 16 | # Ignore the default SQLite database. 17 | /db/*.sqlite3 18 | /db/*.sqlite3-journal 19 | 20 | # Ignore all logfiles and tempfiles. 21 | /log/*.log 22 | /tmp 23 | 24 | config/settings.local.yml 25 | config/settings/*.local.yml 26 | config/environments/*.local.yml 27 | 28 | /node_modules 29 | coverage 30 | 31 | .pry_history 32 | 33 | /public/assets 34 | /public/packs 35 | /public/packs-test 36 | /public/file 37 | /node_modules 38 | /yarn-error.log 39 | yarn-debug.log* 40 | .yarn-integrity 41 | 42 | /app/assets/builds/* 43 | !/app/assets/builds/.keep 44 | 45 | .rspec_status 46 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load DSL and Setup Up Stages 4 | require 'capistrano/setup' 5 | 6 | # Includes default deployment tasks 7 | require 'capistrano/deploy' 8 | require 'capistrano/scm/git' 9 | install_plugin Capistrano::SCM::Git 10 | 11 | # Includes tasks from other gems included in your Gemfile 12 | # 13 | # For documentation on these, see for example: 14 | # 15 | # https://github.com/capistrano/rvm 16 | # https://github.com/capistrano/rbenv 17 | # https://github.com/capistrano/chruby 18 | # https://github.com/capistrano/bundler 19 | # https://github.com/capistrano/rails 20 | # 21 | # require 'capistrano/rvm' 22 | # require 'capistrano/rbenv' 23 | # require 'capistrano/chruby' 24 | # require 'capistrano/bundler' 25 | # require 'capistrano/rails/assets' 26 | # require 'capistrano/rails/migrations' 27 | 28 | require 'capistrano/bundler' 29 | require 'capistrano/rails/assets' 30 | require 'capistrano/honeybadger' 31 | require 'capistrano/passenger' 32 | require 'capistrano/shared_configs' 33 | require 'dlss/capistrano' 34 | 35 | # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. 36 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 37 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | gem 'rails', '~> 8.0.0' 7 | 8 | gem 'propshaft' 9 | 10 | # Use Puma as the app server 11 | gem 'puma', '~> 6.0' 12 | 13 | # Reduces boot times through caching; required in config/boot.rb 14 | gem 'bootsnap', '>= 1.1.0', require: false 15 | gem 'faraday', '~> 2' 16 | gem 'faraday-follow_redirects' 17 | 18 | gem 'config' 19 | gem 'dor-rights-auth' 20 | gem 'nokogiri', '>= 1.7.1' 21 | 22 | group :development, :test do 23 | gem 'capybara' 24 | gem 'debug', platforms: %i[mri] 25 | gem 'druid-tools' 26 | gem 'factory_bot_rails', '~> 6.4' 27 | gem 'high_voltage' 28 | gem 'rspec' 29 | gem 'rspec-rails' 30 | 31 | gem 'selenium-webdriver', '~> 4.2' 32 | 33 | gem 'webmock', '~> 3.19' 34 | 35 | # Linting/Styleguide Enforcement 36 | gem 'rubocop', '~> 1.53' 37 | gem 'rubocop-capybara', require: false 38 | gem 'rubocop-factory_bot', require: false 39 | gem 'rubocop-performance', require: false 40 | gem 'rubocop-rails', require: false 41 | gem 'rubocop-rspec', require: false 42 | gem 'rubocop-rspec_rails', require: false 43 | end 44 | 45 | group :deployment do 46 | gem 'capistrano', '~> 3.0' 47 | gem 'capistrano-bundler' 48 | gem 'capistrano-passenger' 49 | gem 'capistrano-rails' 50 | gem 'capistrano-shared_configs' 51 | gem 'dlss-capistrano' 52 | end 53 | 54 | # Use honeybadger for exception handling 55 | gem 'honeybadger' 56 | 57 | gem 'newrelic_rpm', group: :production 58 | 59 | # Use okcomputer to monitor the application 60 | gem 'okcomputer' 61 | 62 | gem 'shakapacker', '~> 7.0' 63 | 64 | gem 'view_component', '~> 3.10' 65 | 66 | gem 'importmap-rails', '~> 2.0' 67 | 68 | gem 'stimulus-rails', '~> 1.3' 69 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | 3 | ### Actual behavior 4 | 5 | ### Steps to reproduce the behavior 6 | 7 | ### Browser / Environment 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 The Board of Trustees of the Leland Stanford Junior University. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: unset PORT && env RUBY_DEBUG_OPEN=true bin/rails server 2 | js: bin/shakapacker-dev-server 3 | stacks: docker compose up --abort-on-container-exit --build 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | 8 | begin 9 | require 'rubocop/rake_task' 10 | RuboCop::RakeTask.new 11 | rescue LoadError 12 | # should only be here when gem group development and test aren't installed 13 | end 14 | 15 | # remove default rspec task 16 | task(:default).clear 17 | 18 | task default: ['test:prepare', :spec, :rubocop] 19 | 20 | task asset_paths: [:environment] do 21 | puts Rails.application.config.assets.paths 22 | end 23 | 24 | task :update_language_tags do 25 | File.open(Settings.language_subtag_registry.path, 'w') do |file| 26 | file.write(Faraday.get(Settings.language_subtag_registry.url).body) 27 | end 28 | end 29 | 30 | task :stackify, [:bare_druid] => :environment do |_task, args| 31 | exit(1) unless Rails.env.development? 32 | 33 | bare_druid = args[:bare_druid] 34 | druid_path = DruidTools::StacksDruid.new(bare_druid, Settings.stacks_storage_root).path 35 | `mkdir -p #{druid_path}` 36 | Embed::Purl.find(bare_druid).downloadable_files.each do |resource_file| 37 | puts "Downloading #{resource_file.filename} to #{druid_path}/#{resource_file.filename}" 38 | `curl https://stacks.stanford.edu/file/#{bare_druid}/#{resource_file.filename} -o #{druid_path}/#{resource_file.filename}` 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/builds/.keep -------------------------------------------------------------------------------- /app/assets/fonts/sul-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/fonts/sul-icons.eot -------------------------------------------------------------------------------- /app/assets/fonts/sul-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/fonts/sul-icons.ttf -------------------------------------------------------------------------------- /app/assets/fonts/sul-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/fonts/sul-icons.woff -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/by.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/by.png -------------------------------------------------------------------------------- /app/assets/images/iiif-drag-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/iiif-drag-icon.png -------------------------------------------------------------------------------- /app/assets/images/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/images/layers-2x.png -------------------------------------------------------------------------------- /app/assets/images/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/images/layers.png -------------------------------------------------------------------------------- /app/assets/images/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/images/marker-icon-2x.png -------------------------------------------------------------------------------- /app/assets/images/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/images/marker-icon.png -------------------------------------------------------------------------------- /app/assets/images/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/images/marker-shadow.png -------------------------------------------------------------------------------- /app/assets/images/pdm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/pdm.png -------------------------------------------------------------------------------- /app/assets/images/stanford_red_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/stanford_red_s.png -------------------------------------------------------------------------------- /app/assets/images/stanford_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/assets/images/stanford_s.png -------------------------------------------------------------------------------- /app/assets/images/stanford_s.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/common.css: -------------------------------------------------------------------------------- 1 | /* For styles shared between the legacy and modern viewers */ 2 | :root { 3 | --link-color: #006cb8; 4 | --stanford-40-black: #ababa9; 5 | } 6 | /* From Bootstrap 5.3 */ 7 | .visually-hidden-focusable:not(:focus):not(:focus-within):not(caption), 8 | .visually-hidden:not(caption) { 9 | position: absolute !important; 10 | } 11 | 12 | .visually-hidden, 13 | .visually-hidden-focusable:not(:focus):not(:focus-within) { 14 | width: 1px !important; 15 | height: 1px !important; 16 | padding: 0 !important; 17 | margin: -1px !important; 18 | overflow: hidden !important; 19 | clip: rect(0, 0, 0, 0) !important; 20 | white-space: nowrap !important; 21 | border: 0 !important; 22 | } 23 | 24 | a, 25 | a:hover, 26 | a:focus { 27 | color: var(--link-color); 28 | } 29 | 30 | /* TODO: Remove this class and replace with Icons::StanfordOnlyComponent when we remove legacy file viewer */ 31 | .sul-embed-stanford-only-text { 32 | --vertical-align-position: top; 33 | background: 34 | url("stanford_s.svg") no-repeat left, 35 | none; 36 | display: inline-block; 37 | height: 15px; 38 | width: 15px; 39 | vertical-align: var(--vertical-align-position); 40 | } 41 | 42 | .left-drawer .sul-embed-stanford-only-text { 43 | --vertical-align-position: sub; 44 | } 45 | 46 | .sul-embed-location-restricted-text, .stanford-digital-red { 47 | color: var(--stanford-digital-red); 48 | } 49 | 50 | .su-underline { 51 | text-decoration: underline dotted var(--stanford-40-black) 1px; 52 | text-underline-position: under; 53 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/document.css: -------------------------------------------------------------------------------- 1 | @import url("companion_window.css"); 2 | @import url("content_list.css"); 3 | @import url("common.css"); 4 | @import url("locked_status.css"); 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/locked_status.css: -------------------------------------------------------------------------------- 1 | .locked-status { 2 | display: block; 3 | overflow: hidden; 4 | height: 100%; 5 | text-align: center; 6 | 7 | img { 8 | height: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/assets/stylesheets/m3.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --gray-80: #ccc; 3 | } 4 | 5 | .mosaic-root { 6 | bottom: 0 !important; 7 | left: 0 !important; 8 | right: 0 !important; 9 | top: 0 !important; 10 | } 11 | 12 | .mosaic-tile { 13 | margin: 0 !important; 14 | } 15 | 16 | .mosaic-split.-row, 17 | .mosaic-split.-column { 18 | background-color: #aaa; 19 | } 20 | 21 | .sul-embed-container { 22 | border: 0; 23 | border-left: 1px solid var(--gray-80); 24 | box-shadow: 25 | 0 1px 3px 0 rgba(0, 0, 0, 0.2), 26 | 0 1px 1px 0 rgba(0, 0, 0, 0.2), 27 | 0 2px 1px -1px rgba(0, 0, 0, 0.2); 28 | height: calc(100vh - 3px); /* Give a little space for the box shadow */ 29 | margin: 0 1px 3px 0; 30 | position: relative; 31 | 32 | /* For single-window M3 embeds, this will be overriden by Mirador 3's 33 | selected window top-bar styling, but if there are mulitple 34 | windows, it will give the unselected windows a visible border. */ 35 | .mirador-window-top-bar { 36 | border-top-color: var(--gray-80); 37 | } 38 | } 39 | 40 | .sul-embed-container { 41 | .mirador-window-sidebar-annotation-panel { 42 | .MuiMenuItem-root { 43 | white-space: normal; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/assets/stylesheets/model.css: -------------------------------------------------------------------------------- 1 | @import url("companion_window.css"); 2 | @import url("common.css"); 3 | @import url("locked_status.css"); 4 | 5 | /* stops scroll for lock icon which is caused by sul-embed-3d position: relative */ 6 | #main-display { 7 | overflow: hidden; 8 | } 9 | 10 | .sul-embed-3d { 11 | display: inline-block; 12 | position: relative; 13 | height: 100%; 14 | width: 100%; 15 | 16 | .buttons { 17 | left: 10px; 18 | position: absolute; 19 | top: 10px; 20 | z-index: 1; 21 | } 22 | 23 | .zoom-in, 24 | .zoom-out { 25 | background: transparent; 26 | border: none; 27 | color: white; 28 | cursor: pointer; 29 | font-size: 2em; 30 | padding: 5px 13px; 31 | } 32 | 33 | .model-viewer-container, model-viewer { 34 | height: 100%; 35 | width: 100%; 36 | } 37 | 38 | model-viewer { 39 | background-color: black; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/assets/stylesheets/webarchive.css: -------------------------------------------------------------------------------- 1 | @import url("companion_window.css"); 2 | @import url("common.css"); 3 | 4 | .sul-embed-was-seed-container { 5 | overflow-y: scroll; 6 | } 7 | 8 | .sul-embed-was-seed-content { 9 | padding: 10px; 10 | } 11 | 12 | .sul-embed-was-seed-row { 13 | display: flex; 14 | flex-direction: row; 15 | flex-wrap: wrap; 16 | } 17 | 18 | .sul-embed-was-seed-column { 19 | display: flex; 20 | flex-direction: column; 21 | flex: 1; 22 | padding: 0 12px 12px 12px; 23 | } 24 | 25 | .sul-embed-was-seed-preview { 26 | img { 27 | max-height: 200px; 28 | } 29 | } 30 | 31 | .sul-embed-was-seed-captures { 32 | padding-top: 6px; 33 | h1 { 34 | font-size: 1.5rem; 35 | font-weight: 400; 36 | } 37 | } 38 | 39 | .sul-embed-was-seed-info { 40 | flex-basis: 200px; 41 | max-width: 226px; 42 | padding-top: 12px; 43 | position: relative; 44 | 45 | a { 46 | border: 1px solid var(--border-color); 47 | text-align: center; 48 | } 49 | } 50 | 51 | .sul-embed-was-seed-list { 52 | display: flex; 53 | flex-wrap: wrap; 54 | list-style-type: none; 55 | padding-top: 12px; 56 | } 57 | 58 | .sul-embed-was-seed-list-item { 59 | border: 1px solid var(--border-color); 60 | margin: 0 15px 15px 0; 61 | min-width: 225px; 62 | padding: 10px; 63 | position: relative; 64 | text-align: center; 65 | color: var(--button-color); 66 | 67 | a { 68 | vertical-align: sub; 69 | } 70 | 71 | svg { 72 | vertical-align: middle; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/components/companion_windows/authorization_messages_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render LoginComponent.new %> 3 | <%= render RestrictedMessageComponent.new %> 4 |
5 | -------------------------------------------------------------------------------- /app/components/companion_windows/authorization_messages_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CompanionWindows 4 | class AuthorizationMessagesComponent < ViewComponent::Base 5 | # bindings to stimulus actions 6 | def authorization_actions 7 | %w[auth-denied@window->iiif-auth-restriction#displayMessage 8 | needs-login@window->iiif-auth-restriction#displayLoginPrompt 9 | auth-success@window->iiif-auth-restriction#hideLoginPrompt 10 | show-message-panel@window->iiif-auth-restriction#showMessagePanel 11 | thumbnail-clicked@window->iiif-auth-restriction#clearRestrictedMessage].join(' ') 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/components/companion_windows/content_list_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render TabpanelComponent.new(id: 'object-content', title: 'Contents') do |component| %> 2 | <% component.with_body do %> 3 | 9 | <% end %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/components/companion_windows/content_list_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CompanionWindows 4 | class ContentListComponent < ViewComponent::Base 5 | def initialize(viewer:) 6 | @viewer = viewer 7 | end 8 | 9 | attr_reader :viewer 10 | 11 | def document_viewer? 12 | viewer.instance_of?(::Embed::Viewer::DocumentViewer) 13 | end 14 | 15 | def resource_files_collection 16 | viewer.purl_object.resource_files 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/components/companion_windows/content_not_available_component.html.erb: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/components/companion_windows/content_not_available_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CompanionWindows 4 | class ContentNotAvailableComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/companion_windows/control_button_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CompanionWindows 4 | class ControlButtonComponent < ViewComponent::Base 5 | def initialize(aria:, action:, controller: '', html_class: '', role: nil, data: {}, hidden: nil, disabled: nil) # rubocop:disable Metrics/ParameterLists 6 | @aria = aria 7 | @data = data 8 | @data[:controller] = [controller, 'tooltip'].compact_blank.join(' ') 9 | @data[:action] = 10 | "#{action} mouseenter->tooltip#show focus->tooltip#show mouseleave->tooltip#hide blur->tooltip#hide" 11 | @html_class = html_class 12 | @role = role 13 | @hidden = hidden 14 | @disabled = disabled 15 | super 16 | end 17 | attr_reader :data, :aria, :html_class, :role, :hidden, :disabled 18 | 19 | def call 20 | tag.button(class: html_class, data:, aria:, role:, hidden:, disabled:) do 21 | content 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/components/companion_windows/external_link_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to external_url, class: 'modal-component-button', target: '_parent', data: stimulus_attributes do %> 2 | 5 | <%= label %> 6 | <% end %> -------------------------------------------------------------------------------- /app/components/companion_windows/external_link_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CompanionWindows 4 | class ExternalLinkComponent < ViewComponent::Base 5 | def initialize(viewer:, label:) 6 | @viewer = viewer 7 | @label = label 8 | end 9 | 10 | attr_reader :viewer, :label 11 | 12 | delegate :external_url, to: :viewer 13 | 14 | def stimulus_attributes 15 | return unless viewer.instance_of?(::Embed::Viewer::Geo) 16 | 17 | { controller: 'meta-check', meta_check_url_value: viewer.purl_object.meta_json_url } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/components/companion_windows/fullscreen_button_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render CompanionWindows::ControlButtonComponent.new(action: 'click->fullscreen#toggle fullscreenchange@window->fullscreen#updateButton', aria: { label: 'Full screen' }, data: { fullscreen_target: 'button' }) do %> 2 | 3 | 4 | 5 | 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/components/companion_windows/fullscreen_button_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CompanionWindows 4 | class FullscreenButtonComponent < ViewComponent::Base 5 | def render? 6 | # Currently the fullscreen api does not support fullscreen on WebView on iOS 7 | # https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#api.document.fullscreenenabled 8 | request.user_agent !~ /#{Settings.fullscreen_hide}/ 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/components/companion_windows/rights_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render TabpanelComponent.new(id: 'rights', hidden: true, title: 'Rights') do |component| %> 2 | <% component.with_body do %> 3 |
4 | <% if use_and_reproduction.present? %> 5 |
Use and reproduction
6 |
<%= use_and_reproduction %>
7 | <% end %> 8 | <% if copyright.present? %> 9 |
Copyright
10 |
<%= copyright %>
11 | <% end %> 12 | <% if license.present? %> 13 |
License
14 |
<%= license %>
15 | <% end %> 16 | <% if no_rights_information_present? %> 17 |
Attribution
18 |
<%= default_attribution %>
19 | <% end %> 20 |
21 |
22 | <%= image_tag 'sul_stacked_color.svg', alt: "Stanford Libraries logo", role: "presentation", class: "attributionLogo" %> 23 |
24 | <% end %> 25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/components/companion_windows/rights_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CompanionWindows 4 | class RightsComponent < ViewComponent::Base 5 | def initialize(viewer:) 6 | @viewer = viewer 7 | end 8 | 9 | attr_reader :viewer 10 | 11 | delegate :purl_object, to: :viewer 12 | delegate :use_and_reproduction, :copyright, :license, to: :purl_object 13 | 14 | def no_rights_information_present? 15 | use_and_reproduction.blank? && 16 | copyright.blank? && 17 | license.blank? 18 | end 19 | 20 | def default_attribution 21 | 'Provided by the Stanford University Libraries' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/components/companion_windows/share_button_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render CompanionWindows::ControlButtonComponent.new(action: 'click->companion-window#openModal', aria: { label: 'Share & download', controls: 'share_and_download' }, data: { companion_window_target: 'shareButton', target: 'modalComponentsPopover' }) do %> 2 | 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/components/companion_windows/share_button_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CompanionWindows 4 | class ShareButtonComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/dialog_component.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/components/dialog_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DialogComponent < ViewComponent::Base 4 | def initialize(dialog_id:, stimulus_target:) 5 | @dialog_id = dialog_id 6 | @stimulus_target = stimulus_target 7 | end 8 | 9 | renders_one :header 10 | renders_one :main 11 | renders_one :footer 12 | 13 | attr_reader :dialog_id, :stimulus_target 14 | 15 | def header_id 16 | "#{dialog_id}-modal-header" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/components/document/content_list_item_component.html.erb: -------------------------------------------------------------------------------- 1 |
  • data-action="click->content-list#<%= show_method %> keydown.enter->content-list#<%= show_method %>" data-url="<%= url %>" role="tab" tabindex="0"> 2 | <%= render file_type_icon.new %> 3 | 4 | <%= download_label %> 5 | 6 | <%# TODO, we should be using thumbnail.js instead of ViewAccessComponent. Currently only media is. %> 7 | <%= render ViewAccessComponent.new(file:) %> 8 |
  • 9 | -------------------------------------------------------------------------------- /app/components/document/content_list_item_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Document 4 | # Draws the content list for the Document viewer 5 | class ContentListItemComponent < ViewComponent::Base 6 | def initialize(content_list_item:, viewer:, content_list_item_counter:) 7 | @file = content_list_item 8 | @viewer = viewer 9 | @index = content_list_item_counter 10 | end 11 | attr_reader :file, :viewer, :index 12 | 13 | def url 14 | file.file_url 15 | end 16 | 17 | def download_label 18 | file.label_or_filename 19 | end 20 | 21 | def classes 22 | default_classes = 'file-thumb' 23 | return default_classes unless index.zero? 24 | 25 | "#{default_classes} active" 26 | end 27 | 28 | # NOTE: when other content types make use of this component, add other JS methods to the content_list_controller.js 29 | # to define how to swap new content into the main panel, and then set the method below name below. The JS method 30 | # defined below in content_list_controller.js will be trigged when the user clicks on the file, and it will be 31 | # passed the URL 32 | def show_method 33 | 'showPdf' if viewer.instance_of?(::Embed::Viewer::DocumentViewer) 34 | end 35 | 36 | def file_type_icon 37 | viewer.file_type_icon(file.mimetype) 38 | end 39 | 40 | # Only PDF content is rendered in this panel for now, can be extended later to other content types 41 | # Media content does not need this component since it uses custom javascript to render thumbnails and link to 42 | # content 43 | def render? 44 | # do not render any file that is not a PDF 45 | url.ends_with?('.pdf') 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/components/download/all_files_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if display_download_all? %> 2 | 3 | Download zip of all files 4 | <%= render Icons::StanfordOnlyComponent.new if any_stanford_only_files? %> 5 | (<%= pretty_filesize %> uncompressed) 6 | <% else %> 7 |

    Bulk download not available. Please <%= mail_to "sdr-contact@lists.stanford.edu", "contact us", subject: "Download SDR object #{purl_object.druid}" %> if you need help downloading. Total files in this item: <%= downloadable_files.length %>, total size: <%= pretty_filesize %>. 8 |

    9 | <% end %> 10 | -------------------------------------------------------------------------------- /app/components/download/all_files_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Download 4 | class AllFilesComponent < ViewComponent::Base 5 | # @param [#purl_object] viewer 6 | def initialize(viewer:) 7 | @viewer = viewer 8 | end 9 | 10 | attr_reader :viewer 11 | 12 | delegate :purl_object, :download_url, :any_stanford_only_files?, to: :viewer 13 | delegate :downloadable_files, to: :purl_object 14 | 15 | def render? 16 | !viewer.is_a?(Embed::Viewer::Media) 17 | end 18 | 19 | # Returns true or false whether the viewer should display the Download All 20 | # link. The limits were determined in testing and may need to be adjusted 21 | # based on experience with download performance and any changes in the 22 | # Stacks API. It returns false when there is just one file because the 23 | # file download link will suffice for that. 24 | def display_download_all? 25 | purl_object.size < 10_737_418_240 && downloadable_files.length < 3000 26 | end 27 | 28 | def pretty_filesize 29 | viewer.pretty_filesize(purl_object.size) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/components/download/file_list_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if downloadable_files.present? %> 2 | 5 | <%= render Download::AllFilesComponent.new(viewer:) %> 6 | 7 | <% if single_file_download? %> 8 | <% if grouped_files? %> 9 | <% grouped_downloadable_files.each do |content| %> 10 | <% if content.files.any? %> 11 |

    <%= content.label %>

    12 | 15 | <% end %> 16 | <% end %> 17 | <% else %> 18 | 21 | <% end %> 22 | <% end %> 23 | <% else %> 24 |

    No downloads available

    25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/components/download/file_list_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Download 4 | class FileListComponent < ViewComponent::Base 5 | # @param [#purl_object] viewer 6 | def initialize(viewer:) 7 | @viewer = viewer 8 | end 9 | 10 | attr_reader :viewer 11 | 12 | delegate :purl_object, to: :viewer 13 | delegate :downloadable_files, to: :purl_object 14 | 15 | def grouped_downloadable_files 16 | purl_object.contents.map do |item| 17 | item.dup.tap { |obj| obj.files = obj.files.select(&:downloadable?) } 18 | end 19 | end 20 | 21 | # Determine if we need to group files. 22 | # For example, if a media file has a caption or transcript 23 | # we will want to group the caption with the media file. 24 | def grouped_files? 25 | viewer.is_a?(Embed::Viewer::DocumentViewer) || 26 | viewer.is_a?(Embed::Viewer::Geo) || 27 | viewer.is_a?(Embed::Viewer::ModelViewer) || 28 | downloadable_files.any? { |file| file.caption? || file.transcript? } 29 | end 30 | 31 | # File viewer does not show single file download links because it has these links in the main panel 32 | def single_file_download? 33 | !viewer.is_a?(Embed::Viewer::File) 34 | end 35 | 36 | def prefer_filename 37 | viewer.is_a?(Embed::Viewer::Geo) || viewer.is_a?(Embed::Viewer::ModelViewer) 38 | end 39 | 40 | def pretty_filesize 41 | viewer.pretty_filesize(purl_object.size) 42 | end 43 | 44 | def version 45 | purl_object.version_id 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/components/download/file_list_item_component.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | Download <%= download_label %> 4 | 5 | <%= render DownloadAccessComponent.new(file:) %> 6 | <%= file_size %> 7 |
  • 8 | -------------------------------------------------------------------------------- /app/components/download/file_list_item_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Download 4 | class FileListItemComponent < ViewComponent::Base 5 | include Embed::PrettyFilesize 6 | def initialize(file_list_item:, prefer_filename: false, version: nil) 7 | @file = file_list_item 8 | @prefer_filename = prefer_filename 9 | @version = version 10 | end 11 | attr_reader :file, :version 12 | 13 | def file_size 14 | "(#{pretty_filesize(file.size)})" if file.size 15 | end 16 | 17 | def url 18 | file.file_url(download: true, version:) 19 | end 20 | 21 | def download_label 22 | return file.filename if @prefer_filename 23 | 24 | file.label_or_filename 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/components/download_access_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if file.location_restricted? %> 2 | (Restricted) 3 | <% elsif file.stanford_only_downloadable? %> 4 | 5 | Stanford only 6 | 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/components/download_access_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DownloadAccessComponent < ViewComponent::Base 4 | def initialize(file:) 5 | @file = file 6 | end 7 | 8 | attr_reader :file 9 | end 10 | -------------------------------------------------------------------------------- /app/components/embed/file/auth_messages_component.html.erb: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/components/embed/file/auth_messages_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Embed 4 | module File 5 | class AuthMessagesComponent < ViewComponent::Base 6 | def initialize(message:) 7 | @message = message 8 | end 9 | 10 | attr_reader :message 11 | 12 | def icon 13 | MESSAGE_ICONS[message[:type]] 14 | end 15 | 16 | MESSAGE_ICONS = { 'embargo' => Icons::LockClockComponent, 17 | 'stanford' => Icons::LockComponent, 18 | 'location-restricted' => Icons::LockGlobeComponent }.freeze 19 | 20 | def render? 21 | message.present? 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/components/embed/file/dir_row_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if title.present? %> 2 | 3 | 4 | <%= render Icons::FolderComponent.new %><%= title %> 5 | 6 | 7 | 8 | 9 | <% end %> 10 | <% dirs.each_with_index do |dir, index| %> 11 | <%= render Embed::File::DirRowComponent.new(viewer: viewer, dir: dir, pos_in_set: index + 1, set_size: child_set_size, level: level + 1)%> 12 | <% end %> 13 | <% files.each_with_index do |file, index| %> 14 | <%= render Embed::File::FileRowComponent.new(viewer: viewer, file: file, pos_in_set: dirs.size + index + 1, set_size: child_set_size, level: level + 1)%> 15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/components/embed/file/dir_row_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Embed 4 | module File 5 | class DirRowComponent < ViewComponent::Base 6 | def initialize(viewer:, dir:, pos_in_set: nil, set_size: nil, level: 0) 7 | @viewer = viewer 8 | @dir = dir 9 | @pos_in_set = pos_in_set 10 | @set_size = set_size 11 | @level = level 12 | end 13 | 14 | attr_reader :viewer, :dir, :pos_in_set, :set_size, :level 15 | 16 | delegate :title, :files, :dirs, to: :dir 17 | 18 | def child_set_size 19 | @child_set_size ||= dirs.size + files.size 20 | end 21 | 22 | def first_td_style 23 | "padding-left: #{(level - 1) * 3}ch;" 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/components/embed/file/file_row_component.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | <%= render file_type_icon.new %> 6 |
    7 |
    8 | <%= title %> 9 | <% if label %> 10 |

    <%= label %>

    11 | <% end %> 12 |
    13 | 14 | <%= file_size_text%> 15 | 16 | <% unless no_download? %> 17 | 18 | <%= render Icons::DownloadComponent.new(classes: 'download-icon') %> 19 | Download 20 | <%= title %> 21 | 22 | <%= render DownloadAccessComponent.new(file:) %> 23 | <% else %> 24 | (Download unavailable) 25 | <% end %> 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/components/embed/file/file_row_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Embed 4 | module File 5 | class FileRowComponent < ViewComponent::Base 6 | # @param [Embed::Viewer::File] 7 | # @param [Embed::Purl::ResourceFile] file 8 | def initialize(viewer:, file:, pos_in_set: nil, set_size: nil, level: 0) 9 | @viewer = viewer 10 | @file = file 11 | @pos_in_set = pos_in_set 12 | @set_size = set_size 13 | @level = level 14 | end 15 | 16 | attr_reader :viewer, :file, :pos_in_set, :set_size, :level 17 | 18 | delegate :label, :no_download?, to: :file 19 | 20 | def file_size_text 21 | viewer.file_size_text(file.size) 22 | end 23 | 24 | def title 25 | file.hierarchical_title 26 | end 27 | 28 | def filepath 29 | file.title.downcase 30 | end 31 | 32 | def first_td_style 33 | "padding-left: #{((level - 1) * 3) + 3.5}ch;" 34 | end 35 | 36 | def version 37 | viewer.purl_object.version_id 38 | end 39 | 40 | def url 41 | file.file_url(download: false, version:) 42 | end 43 | 44 | def file_type_icon 45 | viewer.file_type_icon(file.mimetype) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/components/embed_this_form_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EmbedThisFormComponent < ViewComponent::Base 4 | def initialize(viewer:) 5 | @viewer = viewer 6 | end 7 | 8 | delegate :embed_request, :purl_object, to: :viewer 9 | delegate :title, to: :purl_object, prefix: true 10 | delegate :purl_url, to: :purl_object 11 | 12 | attr_reader :viewer 13 | 14 | def file? 15 | viewer.is_a?(Embed::Viewer::File) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/components/file_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FileComponent < ViewComponent::Base 4 | def initialize(viewer:) 5 | @viewer = viewer 6 | end 7 | 8 | attr_reader :viewer 9 | 10 | delegate :purl_object, to: :viewer 11 | delegate :citation_only?, to: :purl_object 12 | 13 | def message 14 | viewer.authorization.message 15 | end 16 | 17 | # If this method is false, then display the "content not available" banner 18 | # We exclude the embargoed state, because we prefer to show the embargo banner and we don't want two banners 19 | def display_not_available_banner? 20 | citation_only? && message.blank? 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/components/geo_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render ::CompanionWindowsComponent.new(viewer:, stimulus_controller: 'file-auth tree') do |component| %> 2 | <% component.with_header_button do %> 3 | <%= render CompanionWindows::ShareButtonComponent.new %> 4 | <% end %> 5 | 6 | <% component.with_header_button do %> 7 | <%= render CompanionWindows::FullscreenButtonComponent.new %> 8 | <% end %> 9 | 10 | <% component.with_authorization_messages do %> 11 | <%= render CompanionWindows::AuthorizationMessagesComponent.new %> 12 | <% end %> 13 | 14 | <% component.with_share_menu_button do %> 15 | <%= render CompanionWindows::ExternalLinkComponent.new(viewer:, label: 'EarthWorks') %> 16 | <% end %> 17 | 18 | <% component.with_body do %> 19 | <%= content_tag :div, class: 'sul-embed-geo', data: {controller: 'geo', action: data_actions} do %> 20 | 22 | <%= content_tag :div, viewer.map_element_options do %> 23 | 24 | <% end %> 25 | <% end %> 26 | <% end %> 27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/components/geo_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class GeoComponent < ViewComponent::Base 4 | def initialize(viewer:) 5 | @viewer = viewer 6 | end 7 | 8 | attr_reader :viewer 9 | 10 | delegate :purl_object, to: :viewer 11 | delegate :druid, :public?, to: :purl_object 12 | 13 | def data_actions 14 | return if public? 15 | 16 | 'iiif-manifest-received@window->file-auth#parseFiles auth-success@window->geo#show' 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/components/icons/audio_file_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/audio_file_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class AudioFileComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/description_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/description_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class DescriptionComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/download_component.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/icons/download_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class DownloadComponent < ViewComponent::Base 5 | # We allow passing classes because the download shows up in a modal and the file table 6 | # we don't want to apply MuiSvgIcon-root or modal-component-icon to the table icon (file view) 7 | # and we special css for the table icon in the file view 8 | def initialize(classes: 'MuiSvgIcon-root modal-component-icon') 9 | @classes = classes 10 | end 11 | 12 | attr_reader :classes 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/components/icons/file_present_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/components/icons/file_present_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class FilePresentComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/folder_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/folder_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class FolderComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/folder_zip_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/folder_zip_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class FolderZipComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/image_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/image_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class ImageComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/insert_chart_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/insert_chart_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class InsertChartComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/insert_drive_file_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/insert_drive_file_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class InsertDriveFileComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/lock_clock_component.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/icons/lock_clock_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class LockClockComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/lock_component.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/icons/lock_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class LockComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/lock_globe_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/lock_globe_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class LockGlobeComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/picture_as_pdf_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/picture_as_pdf_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class PictureAsPdfComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/stanford_only_component.html.erb: -------------------------------------------------------------------------------- 1 | 2 | Stanford only 3 | -------------------------------------------------------------------------------- /app/components/icons/stanford_only_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class StanfordOnlyComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/terminal_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/terminal_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class TerminalComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/video_file_component.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/icons/video_file_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class VideoFileComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/icons/web_archive_component.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/icons/web_archive_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Icons 4 | class WebArchiveComponent < ViewComponent::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/components/iframe_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class IframeComponent < ViewComponent::Base 4 | def initialize(viewer:, version: nil) 5 | @viewer = viewer 6 | @version = version 7 | end 8 | 9 | delegate :height, :width, :embed_request, :iframe_title, :purl_object, to: :@viewer 10 | delegate :druid, :version_id, to: :purl_object 11 | attr_reader :version 12 | 13 | def width_style 14 | width || '100%' 15 | end 16 | 17 | def src 18 | query_params = embed_request.as_url_params.merge(version ? { _v: version } : {}).to_query 19 | "#{iframe_url}?url=#{Settings.purl_url}/#{path_segments}&#{query_params}" 20 | end 21 | 22 | def path_segments 23 | segments = [druid] 24 | segments += ['version', version_id] if version_id 25 | segments.join('/') 26 | end 27 | 28 | def style 29 | "height: #{height}; width: #{width_style};" 30 | end 31 | 32 | def call 33 | tag.iframe(src:, style:, title: iframe_title, 34 | frameborder: 0, marginwidth: 0, marginheight: 0, scrolling: 'no', allowfullscreen: true, 35 | allow: 'clipboard-write') 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/components/locked_status_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class LockedStatusComponent < ViewComponent::Base 4 | def call 5 | tag.picture class: 'locked-status' do 6 | # The explicitly empty alt is preferred here, since this image merely adds visual decoration. 7 | image_tag 'locked-media-poster.svg', loading: 'lazy', alt: '' 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/components/login_component.html.erb: -------------------------------------------------------------------------------- 1 | 9 | 20 | -------------------------------------------------------------------------------- /app/components/login_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This is only used by the PdfComponent 4 | class LoginComponent < ViewComponent::Base 5 | end 6 | -------------------------------------------------------------------------------- /app/components/m3_component.html.erb: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /app/components/m3_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class M3Component < ViewComponent::Base 4 | def initialize(viewer:) 5 | @viewer = viewer 6 | end 7 | 8 | attr_reader :viewer 9 | end 10 | -------------------------------------------------------------------------------- /app/components/media/prev_next_component.html.erb: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /app/components/media/prev_next_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Media 4 | class PrevNextComponent < ViewComponent::Base 5 | # @param [Embed::Purl::MediaFile] file 6 | def initialize(file:, resource_index:, size:) 7 | @file = file 8 | @resource_index = resource_index 9 | @size = size 10 | super 11 | end 12 | 13 | def arrow 14 | tag.svg(viewBox: '0 0 24 24') do 15 | tag.path(d: 'm10 16.5 6-4.5-6-4.5zM12 2C6.48 2 2 6.48 2 12s4.48 10 16 | 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8') 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/components/media_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MediaComponent < ViewComponent::Base 4 | def initialize(viewer:) 5 | @viewer = viewer 6 | end 7 | 8 | attr_reader :viewer 9 | 10 | delegate :purl_object, to: :viewer 11 | delegate :druid, :downloadable_transcript_files?, :downloadable_caption_files?, :downloadable_files, to: :purl_object 12 | 13 | def transcript_message 14 | return unless downloadable_caption_files? 15 | 16 | return unless downloadable_files.any? { it.caption? && it.stanford_only? } 17 | 18 | 'Login in to view transcript' 19 | end 20 | 21 | def resources_with_primary_file 22 | @resources_with_primary_file ||= purl_object.contents.select do |purl_resource| 23 | purl_resource.primary_file.present? 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/components/model_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render ::CompanionWindowsComponent.new(viewer:, stimulus_controller: 'file-auth tree') do |component| %> 2 | <% component.with_header_button do %> 3 | <%= render CompanionWindows::ShareButtonComponent.new %> 4 | <% end %> 5 | 6 | <% component.with_header_button do %> 7 | <%= render CompanionWindows::FullscreenButtonComponent.new %> 8 | <% end %> 9 | 10 | <% component.with_authorization_messages do %> 11 | <%= render CompanionWindows::AuthorizationMessagesComponent.new %> 12 | <% end %> 13 | 14 | <% component.with_body do %> 15 | 18 |
    19 |
    20 | 21 | 22 |
    23 |
    24 |
    25 |
    26 | <% end %> 27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/components/model_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ModelComponent < ViewComponent::Base 4 | def initialize(viewer:) 5 | @viewer = viewer 6 | end 7 | 8 | attr_reader :viewer 9 | 10 | delegate :purl_object, to: :viewer 11 | end 12 | -------------------------------------------------------------------------------- /app/components/pdf_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render ::CompanionWindowsComponent.new(viewer:, stimulus_controller: 'file-auth') do |component| %> 2 | <% component.with_header_button do %> 3 | <%= render CompanionWindows::ShareButtonComponent.new %> 4 | <% end %> 5 | 6 | <% component.with_header_button do %> 7 | <%= render CompanionWindows::FullscreenButtonComponent.new %> 8 | <% end %> 9 | 10 | <% component.with_authorization_messages do %> 11 | <%= render CompanionWindows::AuthorizationMessagesComponent.new %> 12 | <% end %> 13 | 14 | <% component.with_body do %> 15 | 18 |
    19 |
    20 |
    23 |
    24 |
    25 |
    26 | <% end %> 27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/components/pdf_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PdfComponent < ViewComponent::Base 4 | def initialize(viewer:) 5 | @viewer = viewer 6 | end 7 | 8 | attr_reader :viewer 9 | 10 | delegate :purl_object, to: :viewer 11 | delegate :druid, to: :purl_object 12 | end 13 | -------------------------------------------------------------------------------- /app/components/restricted_message_component.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/restricted_message_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RestrictedMessageComponent < ViewComponent::Base 4 | end 5 | -------------------------------------------------------------------------------- /app/components/tabpanel_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= tag.section id: id, role: 'tabpanel', hidden: hidden?, data: data do %> 2 |
    3 |

    <%= title %>

    4 |
    5 | <%= subheading %> 6 |
    7 | <%= body %> 8 |
    9 | <% end %> 10 | -------------------------------------------------------------------------------- /app/components/tabpanel_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TabpanelComponent < ViewComponent::Base 4 | renders_one :body 5 | renders_one :subheading 6 | 7 | def initialize(id:, title:, hidden: false, data: {}) 8 | @id = id 9 | @hidden = hidden 10 | @title = title 11 | @data = data 12 | end 13 | 14 | attr_reader :id, :title, :data 15 | 16 | def hidden? 17 | @hidden 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/components/view_access_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if file.location_restricted? %> 2 | (Restricted) 3 | <% elsif file.stanford_only? %> 4 | 5 | Stanford only 6 | 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/components/view_access_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ViewAccessComponent < ViewComponent::Base 4 | def initialize(file:) 5 | @file = file 6 | end 7 | 8 | attr_reader :file 9 | end 10 | -------------------------------------------------------------------------------- /app/components/web_archive_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class WebArchiveComponent < ViewComponent::Base 4 | def initialize(viewer:) 5 | @viewer = viewer 6 | end 7 | 8 | attr_reader :viewer 9 | 10 | delegate :purl_object, to: :viewer 11 | delegate :druid, to: :purl_object 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PagesController < ApplicationController 4 | include HighVoltage::StaticPage unless Rails.env.production? 5 | 6 | layout false 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } -------------------------------------------------------------------------------- /app/javascript/controllers/clipboard_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus' 2 | 3 | export default class extends Controller { 4 | static targets = ['value'] 5 | 6 | copy () { 7 | navigator.clipboard.writeText(this.valueTarget.value) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/javascript/controllers/cue_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | // Changes clicks on cues to events that the player listens for 4 | export default class extends Controller { 5 | static values = { 6 | start: String, 7 | end: String, 8 | } 9 | jump(evt) { 10 | const event = new CustomEvent('media-seek', { detail: this.startValue }) 11 | window.dispatchEvent(event) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/javascript/controllers/embed_this_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | // handles Embed This form interaction 4 | export default class extends Controller { 5 | static targets = ['title', 'search', 'searchMinFiles', 'embed', 'output'] 6 | connect() { 7 | const [prefix, url, ...rest] = this.outputTarget.value.split(/"/) 8 | this.prefix = prefix 9 | const [urlPrefix] = url.split("&", 1) // remove all of the attributes represented on the embed this form. 10 | this.url = urlPrefix 11 | this.suffix = rest.join() 12 | } 13 | 14 | change() { 15 | this.outputTarget.value = `${this.prefix}"${this.url}&${this.#buildParams()}"${this.suffix}` 16 | } 17 | 18 | #buildParams() { 19 | const params = [] 20 | if (!this.titleTarget.checked) 21 | params.push('hide_title=true') 22 | if (!this.embedTarget.checked) 23 | params.push('hide_embed=true') 24 | if (this.hasSearchTarget) { // only for file type 25 | if (!this.searchTarget.checked) { 26 | params.push('hide_search=true') 27 | } else { 28 | params.push(`min_files_to_search=${this.searchMinFilesTarget.value}`) 29 | } 30 | } 31 | return params.join('&') 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/javascript/controllers/fullscreen_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ['area', 'enterIcon', 'exitIcon', 'button'] 5 | static values = { 6 | close: String 7 | } 8 | 9 | connect() { 10 | this.openValue = this.buttonTarget.getAttribute('aria-label') 11 | } 12 | 13 | toggle() { 14 | if (!document.fullscreenElement) { 15 | this.areaTarget.requestFullscreen().catch((err) => { 16 | alert( 17 | `Error attempting to enable fullscreen mode: ${err.message} (${err.name})`, 18 | ) 19 | }) 20 | } else { 21 | document.exitFullscreen() 22 | } 23 | } 24 | 25 | updateButton() { 26 | if (document.fullscreenElement !== null) { 27 | // The hidden attribute doesn't appear to work on SVG in Firefox 28 | this.buttonTarget.setAttribute('aria-label', this.closeValue) 29 | this.enterIconTarget.style.display = 'none' 30 | this.exitIconTarget.style.display = 'inline-block' 31 | } else { 32 | this.buttonTarget.setAttribute('aria-label', this.openValue) 33 | this.enterIconTarget.style.display = 'inline-block' 34 | this.exitIconTarget.style.display = 'none' 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/javascript/controllers/iiif_manifest_loader_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static values = { 5 | iiifManifest: String 6 | } 7 | 8 | connect() { 9 | this.fetchIiifManifest() 10 | } 11 | 12 | fetchIiifManifest() { 13 | fetch(this.iiifManifestValue) 14 | .then((response) => response.json()) 15 | .then((json) => this.dispatchManifestEvent(json)) 16 | .catch((err) => console.error(err)) 17 | } 18 | 19 | dispatchManifestEvent(json) { 20 | const event = new CustomEvent('iiif-manifest-received', { detail: json }) 21 | window.dispatchEvent(event) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/javascript/controllers/iiif_metadata_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | 5 | drawMetadata(event) { 6 | const json = event.detail 7 | const html = json.metadata.map((record) => `
    ${record.label.en[0]}
    ${this.valueToDefinition(record.value.en)}`).join('') 8 | this.element.innerHTML = html 9 | } 10 | 11 | valueToDefinition(defs) { 12 | return defs.map(record => { 13 | // Make sure all links open in a new window 14 | const node = new DOMParser().parseFromString(record, "text/html").body.firstChild 15 | if ('target' in node) { 16 | // Set all elements owning target to target=_blank 17 | node.setAttribute('target', '_blank') 18 | // Prevent https://www.owasp.org/index.php/Reverse_Tabnabbing 19 | node.setAttribute('rel', 'noopener noreferrer') 20 | node.classList.add('su-underline') 21 | return `
    ${node.outerHTML}
    ` 22 | } else { 23 | return `
    ${record}
    ` 24 | } 25 | }).join('') 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/javascript/controllers/image_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | handleError(event) { 5 | event.target.classList.add('image-thumbnail-icon') 6 | event.target.classList.add('default-thumbnail-icon') 7 | } 8 | 9 | updateImage() { 10 | document.querySelectorAll('.image-thumbnail-icon').forEach(element => { 11 | element.classList.remove('image-thumbnail-icon', 'default-thumbnail-icon') 12 | element.src = `${element.src}?${new Date().getTime()}`; 13 | }); 14 | } 15 | } -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | import { application } from "controllers/application" 2 | 3 | // Eager load all controllers defined in the import map under controllers/**/*_controller 4 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 5 | eagerLoadControllersFrom("controllers", application) -------------------------------------------------------------------------------- /app/javascript/controllers/locked_media_poster_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | hide() { 5 | document.querySelectorAll('video').forEach(video => { 6 | if(video.getAttribute("poster").includes('locked')) 7 | video.removeAttribute("poster") 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/javascript/controllers/locked_poster_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | // Display the lock 5 | show() { 6 | this.element.hidden = false 7 | } 8 | 9 | hide() { 10 | this.element.hidden = true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/javascript/controllers/media_tag_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = [ "mediaWrapper" ] 5 | 6 | connect() { 7 | this.findThumbnails() 8 | } 9 | 10 | // Currently this finds certain data-* properties on the media wrapper which we can make thumbnails with. 11 | // Once these properties are found we emit a thumbnails-found event. The content_list_controller.js 12 | // can then receive this event and draw the content of the thubmnail list. 13 | // TODO: in the future, we should drive the thumbnail list from the data in the IIIF manifest. 14 | findThumbnails() { 15 | const thumbnails = this.mediaWrapperTargets. 16 | map((mediaDiv) => { 17 | const dataset = mediaDiv.dataset 18 | return { fileUri: dataset.fileUri, 19 | isStanfordOnly: dataset.stanfordOnly === "true", 20 | thumbnailUrl: dataset.thumbnailUrl, 21 | defaultIcon: dataset.defaultIcon, 22 | isLocationRestricted: dataset.locationRestricted === "true", 23 | fileLabel: dataset.fileLabel || '' } 24 | }) 25 | 26 | // Timeout is set because when the page is cached, the event fires before the content_list_controller is mounted. 27 | // This causes the sidebar not to load: https://github.com/sul-dlss/sul-embed/issues/2175 28 | setTimeout(() => { 29 | window.dispatchEvent(new CustomEvent('thumbnails-found', { detail: thumbnails })) 30 | }, "100"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/javascript/controllers/media_wrapper_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | import videojs from 'video.js' 3 | 4 | export default class extends Controller { 5 | static targets = [ ] 6 | static values = { 7 | index: Number 8 | } 9 | 10 | 11 | toggleVisibility(event) { 12 | const index = event.detail.index 13 | this.element.hidden = this.indexValue !== index 14 | this.pauseAllMedia(index) 15 | } 16 | 17 | // switch transcript if media object index is the same as the visible index. 18 | pauseAllMedia(index) { 19 | const mediaObject = this.element.querySelector('.video-js') 20 | if (mediaObject) { 21 | const playerObject = videojs(mediaObject.id); 22 | playerObject.pause() 23 | if (mediaObject.dataset.index == index){ 24 | window.dispatchEvent(new CustomEvent('switch-transcript', { detail: playerObject })) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/javascript/controllers/meta_check_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static values = { 5 | url: String 6 | } 7 | 8 | connect() { 9 | fetch(this.urlValue) 10 | .then((response) => response.json()) 11 | .then((json) => this.hideLink(json)) 12 | } 13 | 14 | hideLink(meta_json) { 15 | if (!meta_json['earthworks']) this.element.remove(); 16 | } 17 | } -------------------------------------------------------------------------------- /app/javascript/controllers/model_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["modelViewerContainer"] 5 | show(evt) { 6 | const fileUri = evt.detail.fileUri; 7 | // with-credentials breaks things for local development 8 | // we should figure out how to set up auth to not check if the file is open, then we can fix this 9 | this.modelViewerContainerTarget.innerHTML = ` 10 | 11 | ` 12 | this.modelViewer = this.modelViewerContainerTarget.getElementsByTagName('model-viewer')[0] 13 | } 14 | 15 | zoomIn() { 16 | this.modelViewer?.zoom(1) 17 | } 18 | 19 | zoomOut() { 20 | this.modelViewer?.zoom(-1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/javascript/controllers/osd_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | import OpenSeadragon from 'openseadragon' 3 | 4 | export default class extends Controller { 5 | static values = { 6 | url: String, 7 | navImages: Object, 8 | } 9 | 10 | initializeViewer(evt) { 11 | // Customize the error message displayed to users: 12 | OpenSeadragon.setString('Errors.OpenFailed', 'Restricted') 13 | 14 | // only load viewer if the image has been clicked in the sidebar 15 | // and the viewer has never been initialized. 16 | if (this.viewer || this.element.dataset.index != String(evt.detail.index)) return; 17 | this.viewer = OpenSeadragon({ 18 | id: this.element.id, 19 | tileSources: [this.urlValue], 20 | prefixUrl: '', 21 | navImages: this.navImagesValue, 22 | }) 23 | } 24 | } -------------------------------------------------------------------------------- /app/javascript/controllers/pdf_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | 5 | show(evt) { 6 | const fileUri = evt.detail.fileUri 7 | this.element.innerHTML = ` 8 | 9 | 15 | 16 | ` 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/javascript/controllers/prev_next_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static outlets = [ "content-list" ] 5 | 6 | prevNextMedia(evt) { 7 | const index = evt.currentTarget.dataset.prevNextIndexParam 8 | const thumbnail = this.contentListOutlet.element.querySelector(`[data-content-list-index-param="${index}"]`) 9 | thumbnail.click() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/javascript/controllers/tab_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | // make the object-content tab active by default if it exists 6 | if (this.element.attributes['aria-controls'].value === 'object-content') { 7 | this.switch() 8 | } 9 | } 10 | 11 | switch() { 12 | this.#deactivate(this.#currentlyActiveButton()) 13 | this.#makeActive(this.element) 14 | } 15 | 16 | #currentlyActiveButton() { 17 | return this.#tabList.querySelector('[role=tab].active') 18 | } 19 | 20 | get #tabList() { 21 | return this.element.closest('[role=tablist]') 22 | } 23 | 24 | #makeActive(button) { 25 | button.classList.add("active") 26 | button.setAttribute('aria-selected', true) 27 | this.#tabPanelFor(button).hidden = false 28 | } 29 | 30 | #deactivate(button) { 31 | button.classList.remove("active") 32 | button.setAttribute('aria-selected', false) 33 | this.#tabPanelFor(button).hidden = true 34 | } 35 | 36 | #tabPanelFor(button) { 37 | const id = button.getAttribute('aria-controls') 38 | return document.getElementById(id) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/javascript/controllers/tooltip_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | import { createPopper } from 'popper' 3 | 4 | export default class extends Controller { 5 | connect () { 6 | this.tooltip = document.createElement("div") 7 | this.tooltip.classList.add('tooltip') 8 | this.tooltip.innerHTML = this.element.getAttribute('aria-label') 9 | this.tooltip.ariaHidden = 'true' 10 | // tooltip must be appended to a parent of any part of the viewer 11 | // that might otherwise cover it and be within the area shown in 12 | // fullscreen mode. 13 | this.element.closest('#sul-embed-object').appendChild(this.tooltip) 14 | 15 | this.popperInstance = createPopper(this.element, this.tooltip) 16 | } 17 | show() { 18 | this.tooltip.classList.add('show') 19 | 20 | this.popperInstance.update() 21 | } 22 | 23 | hide() { 24 | this.tooltip.classList.remove('show') 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/javascript/document.js: -------------------------------------------------------------------------------- 1 | import "controllers" 2 | import { trackView } from "src/modules/metrics" 3 | 4 | document.addEventListener("DOMContentLoaded", () => { 5 | trackView() 6 | }) 7 | -------------------------------------------------------------------------------- /app/javascript/file.js: -------------------------------------------------------------------------------- 1 | import "controllers" 2 | import "file_controllers" 3 | import { trackView } from "src/modules/metrics" 4 | 5 | document.addEventListener("DOMContentLoaded", () => { 6 | trackView() 7 | }) 8 | -------------------------------------------------------------------------------- /app/javascript/file_controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } -------------------------------------------------------------------------------- /app/javascript/file_controllers/download_all_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | // This controller ensures Download All button doesn't get clicked repeatedly. The first 4 | // click on Download button will cause the button text to be changed to 5 | // "Initializing download" and disabled, then the download will start. 6 | // 7 | // An element is used to enclose the ' 20 | end 21 | end 22 | end 23 | 24 | it { expect(page).to have_content 'This Dialog' } 25 | it { expect(page).to have_content 'Some information' } 26 | it { expect(page).to have_content 'Close' } 27 | it { expect(page).to have_css '#this-dialog' } 28 | it { expect(page).to have_css '#this-dialog-modal-header' } 29 | it { expect(page).to have_css '[data-companion-window-target="thisDialog"]' } 30 | it { expect(page).to have_css '[aria-labelledby="this-dialog-modal-header"]' } 31 | it { expect(page).to have_css '[data-action="click->companion-window#handleBackdropClicks"]' } 32 | end 33 | -------------------------------------------------------------------------------- /spec/components/download/all_files_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Download::AllFilesComponent, type: :component do 6 | subject(:component) { described_class.new(viewer:) } 7 | 8 | before do 9 | allow(embed_request).to receive(:purl_object).and_return(purl_object) 10 | render_inline(component) 11 | end 12 | 13 | let(:purl_object) { build(:purl, contents:) } 14 | let(:embed_request) { Embed::Request.new({}) } 15 | let(:viewer) do 16 | Embed::Viewer::File.new(embed_request) 17 | end 18 | 19 | context 'when there are two files available for download' do 20 | let(:contents) { [build(:resource, :image, files: [build(:resource_file, :world_downloadable), build(:resource_file, :world_downloadable)])] } 21 | 22 | it 'shows the count' do 23 | expect(page).to have_link 'Download zip of all files', href: 'https://stacks.stanford.edu/object/abc123' 24 | end 25 | end 26 | 27 | context 'when the count exceeds the threshold' do 28 | let(:contents) { [build(:resource, :image, files: files)] } 29 | let(:files) { Array.new(3001) { build(:resource_file, :world_downloadable) } } 30 | 31 | it 'shows the not available message' do 32 | expect(page).to have_content 'Bulk download not available. Please contact us if you need help downloading. Total files in this item: 3001, total size: 0 Bytes.' 33 | end 34 | end 35 | 36 | context 'when the purl includes a version id' do 37 | let(:purl_object) { build(:purl, contents:, druid: 'bc123df4567', version_id: '1') } 38 | let(:contents) { [build(:resource, :file, druid: 'bc123df4567', files: [build(:resource_file, :world_downloadable), build(:resource_file, :world_downloadable)])] } 39 | 40 | it 'returns a versioned stacks url for download all' do 41 | expect(page).to have_link href: 'https://stacks.stanford.edu/object/bc123df4567/version/1' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/components/embed/file/auth_messages_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::File::AuthMessagesComponent, type: :component do 6 | subject(:component) { described_class.new(message:) } 7 | 8 | before do 9 | render_inline(component) 10 | end 11 | 12 | context 'when the message is an embargo message' do 13 | let(:message) { { type: 'embargo', message: 'Embargo message' } } 14 | 15 | it 'has svg and message' do 16 | expect(page).to have_css('svg') 17 | expect(page).to have_css('.authLinkWrapper') 18 | expect(page).to have_content('Embargo message') 19 | end 20 | end 21 | 22 | context 'when the message is an location restricted message' do 23 | let(:message) { { type: 'location-restricted', message: 'Location restricted message' } } 24 | 25 | it 'has svg and message' do 26 | expect(page).to have_css('svg') 27 | expect(page).to have_css('.authLinkWrapper') 28 | expect(page).to have_content('Location restricted message') 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/components/embed_this_form_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe EmbedThisFormComponent, type: :component do 6 | let(:request) do 7 | Embed::Request.new(url: 'http://purl.stanford.edu/abc123') 8 | end 9 | let(:object) do 10 | instance_double(Embed::Purl, title: '', druid: '', version_id: nil, resource_files: [], embargoed?: false, purl_url: 'https://stanford.edu/') 11 | end 12 | let(:viewer) { Embed::Viewer::CommonViewer.new(request) } 13 | 14 | before do 15 | allow(request).to receive(:purl_object).and_return(object) 16 | render_inline(described_class.new(viewer:)) 17 | end 18 | 19 | it 'has the form elements for updating the embed code' do 20 | expect(page).to have_content('Select options:') 21 | expect(page).to have_css('input#sul-embed-embed-title[type="checkbox"]') 22 | expect(page).to have_css('input#sul-embed-embed[type="checkbox"]') 23 | expect(page).to have_css('textarea#sul-embed-iframe-code') 24 | expect(page).to have_css('button.copy-to-clipboard') 25 | end 26 | 27 | context 'with a file viewer' do 28 | let(:viewer) { Embed::Viewer::File.new(request) } 29 | 30 | it 'has the form elements for updating the embed code' do 31 | expect(page).to have_checked_field('add search box', visible: :all) 32 | expect(page).to have_field('sul-embed-min_files_to_search', with: '10', visible: :all) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/components/geo_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe GeoComponent, type: :component do 6 | let(:request) do 7 | Embed::Request.new(url: 'http://purl.stanford.edu/abc123') 8 | end 9 | let(:viewer) { Embed::Viewer::Geo.new(request) } 10 | let(:purl) { build(:purl, :geo) } 11 | 12 | before do 13 | allow(Embed::Purl).to receive(:find).and_return(purl) 14 | 15 | render_inline(described_class.new(viewer:)) 16 | end 17 | 18 | it 'draws geo html body for public resources' do 19 | # visible false because we display:none the container until we've loaded the CSS. 20 | expect(page).to have_css '.sul-embed-geo', visible: :all 21 | expect(page).to have_css '#sul-embed-geo-map', visible: :all 22 | expect(page).to have_css('#sul-embed-geo-map[style="flex: 1"]', visible: :all) 23 | expect(page).to have_css('#sul-embed-geo-map[data-bounding-box=\'[["-1.478794", "29.572742"], ["4.234077", "35.000308"]]\']', visible: :all) 24 | expect(page).to have_css('#sul-embed-geo-map[data-wms-url="https://geowebservices.stanford.edu/geoserver/wms/"]', visible: :all) 25 | expect(page).to have_css('#sul-embed-geo-map[data-layers="druid:cz128vq0535"]', visible: :all) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/components/icons/stanford_only_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Icons::StanfordOnlyComponent, type: :component do 6 | pending "add some examples to (or delete) #{__FILE__}" 7 | 8 | # it "renders something useful" do 9 | # expect( 10 | # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html 11 | # ).to include( 12 | # "Hello, components!" 13 | # ) 14 | # end 15 | end 16 | -------------------------------------------------------------------------------- /spec/components/m3_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe M3Component, type: :component do 6 | let(:request) { Embed::Request.new(url: 'http://purl.stanford.edu/abc123', canvas_index: 3, search: 'xyz', suggested_search: 'abc') } 7 | let(:viewer) { Embed::Viewer::M3Viewer.new(request) } 8 | let(:purl) { build(:purl) } 9 | 10 | before do 11 | allow(Embed::Purl).to receive(:find).and_return(purl) 12 | render_inline(described_class.new(viewer:)) 13 | end 14 | 15 | it 'adds m3 html body for resources' do 16 | expect(page).to have_css '#sul-embed-m3', visible: :all 17 | end 18 | 19 | it 'passes along canvas index' do 20 | expect(page).to have_css '[data-canvas-index="3"]', visible: :all 21 | end 22 | 23 | it 'passes along seeded search queries' do 24 | expect(page).to have_css '[data-search="xyz"]', visible: :all 25 | expect(page).to have_css '[data-suggested-search="abc"]', visible: :all 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/components/media/preview_image_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Media::PreviewImageComponent, type: :component do 6 | pending "add some examples to (or delete) #{__FILE__}" 7 | 8 | # it "renders something useful" do 9 | # expect( 10 | # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html 11 | # ).to include( 12 | # "Hello, components!" 13 | # ) 14 | # end 15 | end 16 | -------------------------------------------------------------------------------- /spec/components/model_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe ModelComponent, type: :component do 6 | let(:request) do 7 | Embed::Request.new(url: 'http://purl.stanford.edu/abc123') 8 | end 9 | let(:viewer) { Embed::Viewer::ModelViewer.new(request) } 10 | let(:purl) { build(:purl, :model_3d) } 11 | 12 | before do 13 | allow(Embed::Purl).to receive(:find).and_return(purl) 14 | 15 | render_inline(described_class.new(viewer:)) 16 | end 17 | 18 | it 'draws model-viewer' do 19 | # visible false because we display:none the container until we've loaded the CSS. 20 | expect(page).to have_button '+', visible: :all 21 | expect(page).to have_button '-', visible: :all 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/components/restricted_message_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe RestrictedMessageComponent, type: :component do 6 | before do 7 | render_inline(described_class.new) 8 | end 9 | 10 | # The location restriction banner is hidden so search with visible: :all 11 | it 'renders banner target for the file auth controller' do 12 | expect(page).to have_css('div[data-iiif-auth-restriction-target="restrictedContainer"]', visible: :all) 13 | end 14 | 15 | it 'renders message target for the file auth controller' do 16 | expect(page).to have_css('p[data-iiif-auth-restriction-target="restrictedMessage"]', visible: :all) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/factories/resources.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :resource, class: 'Embed::Purl::Resource' do 5 | druid { 'abc123' } 6 | 7 | trait :file do 8 | type { 'file' } 9 | description { 'File1 Label' } 10 | end 11 | 12 | trait :model_3d do 13 | type { '3d' } 14 | description { 'File1 Label' } 15 | end 16 | 17 | trait :document do 18 | type { 'document' } 19 | description { 'File1 Label' } 20 | files { [build(:resource_file, :document, :world_downloadable)] } 21 | end 22 | 23 | trait :document_no_download do 24 | type { 'document' } 25 | description { 'File1 Label' } 26 | files { [build(:resource_file, :document, :no_download)] } 27 | end 28 | 29 | trait :video do 30 | type { 'video' } 31 | description { 'First Video' } 32 | files { [build(:media_file, :video, :world_downloadable), build(:media_file, :caption, :world_downloadable), build(:media_file, :image, :world_downloadable, filename: 'video_1.jp2')] } 33 | end 34 | 35 | trait :audio do 36 | type { 'audio' } 37 | description { 'First Audio' } 38 | files { [build(:media_file, :audio), build(:media_file, :caption), build(:media_file, :image, filename: 'audio_1.jp2')] } 39 | end 40 | 41 | trait :image do 42 | type { 'image' } 43 | description { 'Image of media (1 of 1)' } 44 | files { [build(:resource_file, :image)] } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/features/3d_viewer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe '3D Viewer', :js do 6 | let(:purl) { build(:purl, :model_3d) } 7 | 8 | before do 9 | allow(Embed::Purl).to receive(:find).and_return(purl) 10 | visit_iframe_response 11 | end 12 | 13 | it 'renders the 3D viewer for 3D objects' do 14 | expect(page).to have_css('.sul-embed-3d model-viewer') 15 | end 16 | 17 | it 'has working panels' do 18 | expect(page).to have_no_content('About this item') 19 | page.find('[aria-controls="left-drawer"]').click 20 | expect(page).to have_content('About this item') 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/features/document_viewer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'PDF Viewer', :js do 6 | before do 7 | allow(Embed::Purl).to receive(:find).and_return(purl) 8 | visit_iframe_response 9 | end 10 | 11 | context 'when world visible' do 12 | let(:purl) { build(:purl, :document, download: 'world') } 13 | 14 | it 'renders the PDF viewer for documents' do 15 | expect(page).to have_css('.sul-embed-pdf') 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/features/file_hierarchy_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'file viewer with hierarchy', :js do 6 | let(:contents) do 7 | [ 8 | build(:resource, files: [build(:resource_file, filename: 'Title_of_the_PDF.pdf')]), 9 | build(:resource, files: [build(:resource_file, filename: 'dir1/dir2/Title_of_2_PDF.pdf')]) 10 | ] 11 | end 12 | let(:purl) { build(:purl, :file, contents:) } 13 | 14 | before do 15 | allow(Embed::Purl).to receive(:find).and_return(purl) 16 | end 17 | 18 | it 'renders hierarchy' do 19 | visit_iframe_response 20 | expect(page).to have_content('2 files') 21 | # There are 2 files 22 | expect(page).to have_css('tr[data-tree-role="leaf"]', count: 2) 23 | # There are 2 directories 24 | expect(page).to have_css('tr[data-tree-role="branch"]', count: 2) 25 | # One of the files is nested 26 | expect(page).to have_css('tr[data-tree-role="branch"] + tr[data-tree-role="branch"] + tr[data-tree-role="leaf"]', count: 1) 27 | 28 | expect(page).to have_css('tr[data-tree-role="leaf"]', count: 2) 29 | all('tr[aria-expanded="true"]').last.click 30 | expect(page).to have_css('tr[data-tree-role="leaf"]', count: 1) 31 | all('tr[data-tree-role="branch"][aria-expanded="false"]').first.click 32 | expect(page).to have_css('tr[data-tree-role="leaf"]', count: 2) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/features/file_viewer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'File viewer', :js do 6 | let(:purl) do 7 | build(:purl, :file, contents: [build(:resource, files: [build(:resource_file, :document, label: 'File1 Label')])]) 8 | end 9 | 10 | before do 11 | allow(Embed::Purl).to receive(:find).and_return(purl) 12 | end 13 | 14 | context 'with no options' do 15 | before do 16 | visit_iframe_response 17 | end 18 | 19 | it 'makes purl embed request and embed' do 20 | expect(page).to have_css('.sul-embed-container') 21 | expect(page).to have_css('h2') 22 | expect(page).to have_css('#main-display') 23 | 24 | expect(page).to have_css('tr[data-tree-role="leaf"] a', text: 'Download') 25 | expect(page).to have_css('*[data-tree-role="label"]', text: 'File1 Label') 26 | expect(page).to have_css('td', text: '12.35 kB') 27 | end 28 | 29 | context 'when the object has multiple files' do 30 | let(:purl) do 31 | build(:purl, :file, contents: [ 32 | build(:resource, :file, files: [build(:resource_file), build(:resource_file)]), 33 | build(:resource, :image), 34 | build(:resource, :file, files: [build(:resource_file)]) 35 | ]) 36 | end 37 | 38 | it 'contains 4 files in file list' do 39 | expect(page).to have_css('tr[data-tree-role="leaf"]', count: 4) 40 | end 41 | end 42 | end 43 | 44 | context 'when hide_title is requested' do 45 | before do 46 | visit_iframe_response('abc123', hide_title: true) 47 | end 48 | 49 | it 'hides the title' do 50 | expect(page).to have_no_css('h2') 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/features/iiif_embed_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'IIIF Embed', :js do 6 | before do 7 | stub_request(:get, 'https://purl.stanford.edu/fr426cg9537.xml') 8 | .to_return(status: 200, body: '', headers: {}) 9 | end 10 | 11 | it 'renders a Mirador3 Viewer' do 12 | visit iiif_path(url: 'https://purl.stanford.edu/fr426cg9537/iiif/manifest') 13 | 14 | expect(page).to have_css('.sul-embed-container', visible: :visible) 15 | expect(page).to have_css('main.mirador-viewer', visible: :visible) 16 | end 17 | 18 | it 'sets the enable-comparison data attribute when enable_comparison is requested' do 19 | visit iiif_path(url: 'https://purl.stanford.edu/fr426cg9537/iiif/manifest', enable_comparison: 'true') 20 | 21 | expect(page).to have_css('[data-enable-comparison="true"]') 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/features/sandbox_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'embed sandbox page', :js do 6 | let(:purl) do 7 | build(:purl, :file, contents: [build(:resource, files: [build(:resource_file, :document, label: 'File1 Label')])]) 8 | end 9 | 10 | before do 11 | allow(Embed::Purl).to receive(:find).and_return(purl) 12 | visit page_path(id: 'sandbox') 13 | end 14 | 15 | it 'passes the customization URL parameters down to the iframe successfully' do 16 | expect(page).to have_no_css('iframe') 17 | check('hide-title') 18 | check('hide-search') 19 | fill_in 'api-endpoint', with: embed_path 20 | fill_in 'url-scheme', with: 'http://purl.stanford.edu/abc123' 21 | click_on 'Embed' 22 | iframe_src = page.find('iframe')['src'] 23 | expect(iframe_src).to match(/hide_title=true/) 24 | expect(iframe_src).to match(/hide_search=true/) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/lib/bcp47/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Bcp47::Parser do 6 | subject(:parser) { described_class.new(input) } 7 | 8 | describe '#records' do 9 | subject(:records) { parser.records } 10 | 11 | context 'with an invalid record-jar string' do 12 | let(:input) { 'foobar' } 13 | 14 | it 'returns a list with an empty record' do 15 | expect(records).to include(instance_of(Bcp47::Record)) 16 | end 17 | end 18 | 19 | context 'with a non-string' do 20 | let(:input) { ['foobar'] } 21 | 22 | it 'returns nil' do 23 | expect(records).to be_nil 24 | end 25 | end 26 | 27 | context 'with a legit record-jar string' do 28 | let(:input) { "File-Date: 2023-11-17\nOther-Field: foobar%%\nType: Language\nAdded: 2023-10-31" } 29 | 30 | it 'returns a list with two records' do 31 | expect(records.count).to eq(2) 32 | expect(records).to all(be_a(Bcp47::Record)) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/lib/embed/mimetypes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::Mimetypes do 6 | let(:dummy_class) { Class.new { include Embed::Mimetypes }.new } 7 | 8 | describe '.pretty_mime' do 9 | it 'for a known mimetype' do 10 | expect(dummy_class.pretty_mime('application/pdf')).to eq 'pdf' 11 | end 12 | 13 | it 'for an unknown mimetype' do 14 | expect(dummy_class.pretty_mime('text/yolo')).to eq 'text/yolo' 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/lib/embed/pretty_filesize_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::PrettyFilesize do 6 | let(:dummy_class) { Class.new { include Embed::PrettyFilesize }.new } 7 | 8 | describe '.pretty_file_size' do 9 | subject { dummy_class.pretty_filesize(size) } 10 | 11 | context 'kilobyte scale' do 12 | let(:size) { 12_345 } 13 | 14 | it { is_expected.to eq '12.35 kB' } 15 | end 16 | 17 | context 'megabyte scale' do 18 | let(:size) { 4_761_613 } 19 | 20 | it { is_expected.to eq '4.76 MB' } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/lib/embed/viewer/geo_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::Viewer::Geo do 6 | let(:request) { Embed::Request.new(url: 'http://purl.stanford.edu/abc123') } 7 | let(:geo_viewer) { described_class.new(request) } 8 | let(:purl) { build(:purl) } 9 | 10 | before do 11 | allow(Embed::Purl).to receive(:find).and_return(purl) 12 | end 13 | 14 | describe '.external_url' do 15 | it 'builds the external url based on settings and druid value' do 16 | expect(geo_viewer.external_url).to eq('https://earthworks.stanford.edu/catalog/stanford-abc123') 17 | end 18 | end 19 | 20 | describe '#map_element_options' do 21 | subject(:options) { geo_viewer.map_element_options } 22 | 23 | context 'with public content' do 24 | let(:purl) { build(:purl, bounding_box: [['-1.478794', '29.572742'], ['4.234077', '35.000308']], download: 'world') } 25 | 26 | it 'has the required options' do 27 | expect(options).to include({ style: 'flex: 1', id: 'sul-embed-geo-map', 28 | 'data-bounding-box' => '[["-1.478794", "29.572742"], ["4.234077", "35.000308"]]', 29 | 'data-wms-url' => 'https://geowebservices.stanford.edu/geoserver/wms/', 30 | 'data-layers' => 'druid:abc123' }) 31 | end 32 | end 33 | 34 | context 'with restricted content' do 35 | let(:purl) { build(:purl, bounding_box: [['38.298673', '-123.387626'], ['39.399103', '-122.528843']], download: 'stanford') } 36 | 37 | it 'has the required options' do 38 | expect(options).to include({ style: 'flex: 1', id: 'sul-embed-geo-map', 39 | 'data-bounding-box' => '[["38.298673", "-123.387626"], ["39.399103", "-122.528843"]]' }) 40 | end 41 | 42 | it { is_expected.not_to include('data-wms-url') } 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/lib/embed/viewer/media_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::Viewer::Media do 6 | let(:request) { instance_double(Embed::Request, purl_object: instance_double(Embed::Purl)) } 7 | let(:media_viewer) { described_class.new(request) } 8 | 9 | describe '#importmap' do 10 | subject { media_viewer.importmap } 11 | 12 | it { is_expected.to eq 'media' } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/lib/embed/viewer/model_viewer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::Viewer::ModelViewer do 6 | subject(:viewer) { described_class.new(request) } 7 | 8 | let(:purl) do 9 | instance_double( 10 | Embed::Purl, 11 | contents: [ 12 | instance_double(Embed::Purl::Resource, three_dimensional?: true, 13 | files: [instance_double(Embed::Purl::ResourceFile, 14 | file_url: '//file/druid:abc123/version/1/obj1.glb', 15 | no_download?: false)]) 16 | ], 17 | druid: 'abc123', 18 | version_id: 1 19 | ) 20 | end 21 | let(:request) { instance_double(Embed::Request, purl_object: purl) } 22 | 23 | describe '#three_dimensional_files' do 24 | it 'returns the full file URL for the PDFs in an object' do 25 | expect(viewer.three_dimensional_files.length).to eq 1 26 | expect(viewer.three_dimensional_files.first).to match(%r{/file/druid:abc123/version/1/obj1\.glb$}) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/lib/embed/viewer_factory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::ViewerFactory do 6 | let(:request) { instance_double(Embed::Request, purl_object: purl) } 7 | let(:instance) { described_class.new(request) } 8 | 9 | describe 'initialization' do 10 | subject { instance } 11 | 12 | context 'invalid Purl object' do 13 | let(:purl) { Embed::Purl.new(type: 'image', contents: [], constituents: []) } 14 | 15 | it 'raises an error' do 16 | expect { subject }.to raise_error(Embed::Purl::ResourceNotEmbeddable) 17 | end 18 | end 19 | 20 | context 'valid Purl object' do 21 | let(:purl) { Embed::Purl.new(type: 'file', contents: [build(:resource)]) } 22 | 23 | it 'initializes successfully' do 24 | expect { subject }.not_to raise_error 25 | end 26 | end 27 | end 28 | 29 | describe '#viewer' do 30 | subject { instance.viewer } 31 | 32 | let(:purl) { Embed::Purl.new(type: 'image', contents: [build(:resource)]) } 33 | 34 | context 'when the request has a type' do 35 | it { is_expected.to be_a Embed::Viewer::M3Viewer } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/models/embed/hierarchical_contents_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::HierarchicalContents do 6 | describe '#contents' do 7 | let(:root_dir) { described_class.contents(resources) } 8 | let(:resources) do 9 | [ 10 | build(:resource, files: [build(:resource_file, filename: 'Title_of_the_PDF.pdf')]), 11 | build(:resource, files: [build(:resource_file, filename: 'dir1/dir2/Title_of_2_PDF.pdf')]) 12 | ] 13 | end 14 | 15 | it 'returns root ResourceDir' do 16 | expect(root_dir).to be_an Embed::Purl::ResourceDir 17 | expect(root_dir.title).to eq '' 18 | expect(root_dir.files.count).to eq 1 19 | file1 = root_dir.files.first 20 | expect(file1).to be_an Embed::Purl::ResourceFile 21 | expect(file1.title).to eq 'Title_of_the_PDF.pdf' 22 | expect(root_dir.dirs.count).to eq 1 23 | dir1 = root_dir.dirs.first 24 | expect(dir1.title).to eq 'dir1' 25 | expect(dir1.files.count).to eq 0 26 | expect(dir1.dirs.count).to eq 1 27 | dir2 = dir1.dirs.first 28 | expect(dir2.title).to eq 'dir2' 29 | expect(dir2.dirs.count).to eq 0 30 | expect(dir2.files.count).to eq 1 31 | file2 = dir2.files.first 32 | expect(file2.title).to eq 'dir1/dir2/Title_of_2_PDF.pdf' 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/models/embed/purl/media_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Embed::Purl::MediaFile do 6 | describe '#label_or_filename' do 7 | subject { resource_file.label_or_filename } 8 | 9 | context 'with audio caption' do 10 | let(:resource_file) { build(:media_file, :caption) } 11 | 12 | it { is_expected.to eq 'English captions' } 13 | end 14 | 15 | context 'with auto generated caption' do 16 | let(:resource_file) { build(:media_file, :autogenerated_media_transcript) } 17 | 18 | it { is_expected.to eq 'English captions (auto-generated)' } 19 | end 20 | 21 | context 'with audio transcript' do 22 | let(:resource_file) { build(:media_file, :media_transcript) } 23 | 24 | it { is_expected.to eq 'English transcript' } 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'webmock/rspec' 4 | WebMock.disable_net_connect!(allow_localhost: true) 5 | 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = '.rspec_status' 10 | 11 | # Many RSpec users commonly either run the entire suite or an individual 12 | # file, and it's useful to allow more verbose output when running an 13 | # individual spec file. 14 | if config.files_to_run.one? 15 | # Use the documentation formatter for detailed output, 16 | # unless a formatter has already been configured 17 | # (e.g. via a command-line flag). 18 | config.default_formatter = 'doc' 19 | end 20 | 21 | # Print the 10 slowest examples and example groups at the 22 | # end of the spec run, to help surface which specs are running 23 | # particularly slow. 24 | config.profile_examples = 10 25 | 26 | # Run specs in random order to surface order dependencies. If you find an 27 | # order dependency and want to debug it, you can fix the order by providing 28 | # the seed, which is printed after each run. 29 | # --seed 1234 30 | config.order = :random 31 | 32 | # Seed global randomization in this process using the `--seed` CLI option. 33 | # Setting this allows you to use `--seed` to deterministically reproduce 34 | # test failures related to randomization by passing the same `--seed` value 35 | # as the one that triggered the failure. 36 | Kernel.srand config.seed 37 | 38 | # Disable RSpec exposing methods globally on `Module` and `main` 39 | config.disable_monkey_patching! 40 | end 41 | -------------------------------------------------------------------------------- /spec/support/metrics_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Helpers for working with events sent via ahoy.js 4 | 5 | def event_with_properties(name, properties) 6 | a_hash_including({ 'name' => name, 'properties' => a_hash_including(properties) }) 7 | end 8 | 9 | def view_with_properties(properties) 10 | event_with_properties('$view', properties) 11 | end 12 | 13 | def wait_for_event(name) 14 | Timeout.timeout(Capybara.default_max_wait_time) do 15 | loop until StubMetricsApi.last_event&.dig('name') == name 16 | end 17 | end 18 | 19 | def wait_for_view 20 | wait_for_event('$view') 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/stub_apps/stub_metrics_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Stub for the SDR metrics API: 4 | # https://github.com/sul-dlss/sdr-metrics-api 5 | # rubocop:disable Style/ClassVars 6 | class StubMetricsApi 7 | cattr_accessor :events 8 | 9 | @@events = [] 10 | 11 | def self.last_event 12 | events.last 13 | end 14 | 15 | def self.reset! 16 | @@events = [] 17 | end 18 | 19 | def call(env) 20 | req = Rack::Request.new(env) 21 | case req.path 22 | when %r{/visits$} 23 | track_visit 24 | when %r{/events$} 25 | track_events(req.params['events_json']) 26 | else 27 | [404, {}, ['Not Found']] 28 | end 29 | end 30 | 31 | def track_visit 32 | [200, {}, []] 33 | end 34 | 35 | def track_events(params) 36 | JSON.parse(params).each { |event| @@events << event } 37 | [200, {}, []] 38 | end 39 | end 40 | # rubocop:enable Style/ClassVars 41 | -------------------------------------------------------------------------------- /test/components/previews/embed/companion_window_component_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Access these previews at: 4 | # http://localhost:3000/rails/view_components/embed/companion_window_component 5 | module Embed 6 | class CompanionWindowComponentPreview < ViewComponent::Preview 7 | layout 'preview/media' 8 | 9 | def public_pdf 10 | render_viewer_for(url: 'https://purl.stanford.edu/sq929fn8035') 11 | end 12 | 13 | private 14 | 15 | def render_viewer_for(url:) 16 | embed_request = Embed::Request.new(url:) 17 | viewer = Embed::ViewerFactory.new(embed_request).viewer 18 | render(CompanionWindowsComponent.new(viewer:)) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/components/previews/embed/file_component_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Access these previews at: 4 | # http://localhost:3000/rails/view_components/embed/file_component 5 | module Embed 6 | class FileComponentPreview < ViewComponent::Preview 7 | layout 'preview/file' 8 | 9 | def hierarchy 10 | render_viewer_for(url: 'https://purl.stanford.edu/fg478vy8624') 11 | end 12 | 13 | private 14 | 15 | def render_viewer_for(url:) 16 | embed_request = Embed::Request.new(url:) 17 | viewer = Embed::Viewer::File.new(embed_request) 18 | render(viewer.component.new(viewer:)) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/components/previews/embed/geo_component_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Access these previews at: 4 | # http://localhost:3000/rails/view_components/embed/geo_component 5 | module Embed 6 | class GeoComponentPreview < ViewComponent::Preview 7 | layout 'preview/geo' 8 | 9 | def public_raster 10 | render_viewer_for(url: 'https://purl.stanford.edu/tg926kp6619') 11 | end 12 | 13 | def public_vector 14 | render_viewer_for(url: 'https://purl.stanford.edu/cz128vq0535') 15 | end 16 | 17 | private 18 | 19 | def render_viewer_for(url:) 20 | embed_request = Embed::Request.new(url:, new_viewer: 'true') 21 | viewer = Embed::Viewer::Geo.new(embed_request) 22 | render(viewer.component.new(viewer:)) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/components/previews/embed/legacy/geo_component_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Access these previews at: 4 | # http://localhost:3000/rails/view_components/embed/legacy/geo_component 5 | module Embed 6 | module Legacy 7 | class GeoComponentPreview < ViewComponent::Preview 8 | layout 'preview/legacy/geo' 9 | 10 | def public_raster 11 | render_viewer_for(url: 'https://purl.stanford.edu/tg926kp6619') 12 | end 13 | 14 | def public_vector 15 | render_viewer_for(url: 'https://purl.stanford.edu/cz128vq0535') 16 | end 17 | 18 | private 19 | 20 | def render_viewer_for(url:) 21 | embed_request = Embed::Request.new(url:) 22 | viewer = Embed::Viewer::Geo.new(embed_request) 23 | render(viewer.component.new(viewer:)) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/components/previews/embed/legacy/model_component_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Access these previews at: 4 | # http://localhost:3000/rails/view_components/embed/model_component 5 | module Embed 6 | module Legacy 7 | class ModelComponentPreview < ViewComponent::Preview 8 | layout 'preview/legacy/3d' 9 | 10 | def public 11 | render_viewer_for(url: 'https://purl.stanford.edu/bb648mk7250') 12 | end 13 | 14 | private 15 | 16 | def render_viewer_for(url:) 17 | embed_request = Embed::Request.new(url:) 18 | viewer = Embed::Viewer::ModelViewer.new(embed_request) 19 | render(viewer.component.new(viewer:)) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/components/previews/embed/media_component_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Access these previews at: 4 | # http://localhost:3000/rails/view_components/embed/media_component 5 | module Embed 6 | class MediaComponentPreview < ViewComponent::Preview 7 | layout 'preview/media' 8 | 9 | def with_audio 10 | render_media_viewer_for(url: 'https://purl.stanford.edu/gj753wr1198') 11 | end 12 | 13 | def with_public_video 14 | render_media_viewer_for(url: 'https://purl.stanford.edu/gt507vy5436') 15 | end 16 | 17 | def with_stanford_only_video 18 | render_media_viewer_for(url: 'https://purl.stanford.edu/bb142ws0723') 19 | end 20 | 21 | def with_embargo 22 | render_media_viewer_for(url: 'https://purl.stanford.edu/hy056tp9463') 23 | end 24 | 25 | def with_lots_of_files 26 | render_media_viewer_for(url: 'https://purl.stanford.edu/wz015vw6759') 27 | end 28 | 29 | def with_multilingual_captions 30 | render_media_viewer_for(url: 'https://purl.stanford.edu/dq301jn4140') 31 | end 32 | 33 | def without_captions 34 | render_media_viewer_for(url: 'https://purl.stanford.edu/dp324gw4986') 35 | end 36 | 37 | def citation_only 38 | render_media_viewer_for(url: 'https://purl.stanford.edu/bc285ff3003') 39 | end 40 | 41 | def hide_title 42 | render_media_viewer_for(url: 'https://purl.stanford.edu/wz015vw6759', hide_title: 'true') 43 | end 44 | 45 | private 46 | 47 | def render_media_viewer_for(**params) 48 | embed_request = Embed::Request.new(**params) 49 | viewer = Embed::Viewer::Media.new(embed_request) 50 | render(MediaComponent.new(viewer:)) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/components/previews/embed/model_component_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Access these previews at: 4 | # http://localhost:3000/rails/view_components/embed/model_component 5 | module Embed 6 | class ModelComponentPreview < ViewComponent::Preview 7 | layout 'preview/model' 8 | 9 | def public 10 | render_viewer_for(url: 'https://purl.stanford.edu/bb648mk7250') 11 | end 12 | 13 | private 14 | 15 | def render_viewer_for(url:) 16 | embed_request = Embed::Request.new(url:, new_viewer: 'true') 17 | viewer = Embed::Viewer::ModelViewer.new(embed_request) 18 | render(viewer.component.new(viewer:)) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/components/previews/embed/pdf_component_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Access these previews at: 4 | # http://localhost:3000/rails/view_components/embed/pdf_component 5 | module Embed 6 | class PdfComponentPreview < ViewComponent::Preview 7 | layout 'preview/pdf' 8 | 9 | def public 10 | render_viewer_for(url: 'https://purl.stanford.edu/sq929fn8035') 11 | end 12 | 13 | def stanford_only 14 | render_viewer_for(url: 'https://purl.stanford.edu/jr789rw2402') 15 | end 16 | 17 | def citation_only 18 | render_viewer_for(url: 'https://purl.stanford.edu/bz673hm0344') 19 | end 20 | 21 | def with_multiple_files 22 | render_viewer_for(url: 'https://purl.stanford.edu/ds777pr3860') 23 | end 24 | 25 | private 26 | 27 | def render_viewer_for(url:) 28 | embed_request = Embed::Request.new(url:) 29 | viewer = Embed::ViewerFactory.new(embed_request).viewer 30 | render(PdfComponent.new(viewer:)) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sul-dlss/sul-embed/1ab7cc56b68132ae08de850597d241971565691f/vendor/javascript/.keep --------------------------------------------------------------------------------