blacklight 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.env +3 -3
  3. data/.github/matrix.json +12 -18
  4. data/.github/workflows/lint.yml +1 -1
  5. data/.github/workflows/test.yml +4 -3
  6. data/.rubocop.yml +72 -7
  7. data/.rubocop_todo.yml +142 -268
  8. data/Gemfile +1 -1
  9. data/README.md +2 -9
  10. data/VERSION +1 -1
  11. data/app/assets/builds/blacklight.css +37 -23
  12. data/app/assets/images/blacklight/logo-dark.svg +184 -0
  13. data/app/assets/images/blacklight/logo.svg +184 -0
  14. data/app/assets/javascripts/blacklight/blacklight.esm.js +128 -45
  15. data/app/assets/javascripts/blacklight/blacklight.esm.js.map +1 -1
  16. data/app/assets/javascripts/blacklight/blacklight.js +128 -45
  17. data/app/assets/javascripts/blacklight/blacklight.js.map +1 -1
  18. data/app/assets/stylesheets/blacklight/_balanced_list.scss +1 -1
  19. data/app/assets/stylesheets/blacklight/_bootstrap_overrides.scss +17 -0
  20. data/app/assets/stylesheets/blacklight/_constraints.scss +2 -2
  21. data/app/assets/stylesheets/blacklight/_facets.scss +14 -10
  22. data/app/assets/stylesheets/blacklight/_header.scss +0 -4
  23. data/app/assets/stylesheets/blacklight/blacklight_defaults.scss +5 -9
  24. data/app/components/blacklight/facets/inclusive_constraint_component.html.erb +1 -1
  25. data/app/components/blacklight/icons/checkmark_component.rb +16 -0
  26. data/app/components/blacklight/icons/circle_half_component.rb +16 -0
  27. data/app/components/blacklight/icons/moon_stars_component.rb +17 -0
  28. data/app/components/blacklight/icons/sun_component.rb +16 -0
  29. data/app/components/blacklight/metadata_field_component.rb +1 -1
  30. data/app/components/blacklight/search_navbar_component.html.erb +1 -1
  31. data/app/components/blacklight/theme_switcher_component.html.erb +31 -0
  32. data/app/components/blacklight/theme_switcher_component.rb +6 -0
  33. data/app/components/blacklight/top_navbar_component.html.erb +1 -1
  34. data/app/controllers/concerns/blacklight/bookmarks.rb +8 -14
  35. data/app/controllers/concerns/blacklight/catalog.rb +9 -10
  36. data/app/controllers/concerns/blacklight/controller.rb +54 -11
  37. data/app/controllers/concerns/blacklight/guest_user.rb +51 -0
  38. data/app/controllers/concerns/blacklight/search_context.rb +1 -1
  39. data/app/controllers/concerns/blacklight/search_history.rb +3 -1
  40. data/app/controllers/concerns/blacklight/searchable.rb +18 -0
  41. data/app/helpers/blacklight/layout_helper_behavior.rb +7 -0
  42. data/app/javascript/blacklight-frontend/color_theme_switcher.js +85 -0
  43. data/app/javascript/blacklight-frontend/index.js +2 -0
  44. data/app/models/concerns/blacklight/document/dublin_core.rb +2 -2
  45. data/app/models/concerns/blacklight/document.rb +1 -1
  46. data/app/models/concerns/blacklight/user.rb +36 -0
  47. data/app/presenters/blacklight/field_presenter.rb +1 -1
  48. data/app/presenters/blacklight/rendering/microdata.rb +1 -0
  49. data/app/services/blacklight/bookmarks_search_builder.rb +4 -4
  50. data/app/services/blacklight/search_service.rb +46 -33
  51. data/app/values/blacklight/types.rb +1 -1
  52. data/app/views/catalog/_email_form.html.erb +3 -3
  53. data/app/views/catalog/_home_text.html.erb +1 -1
  54. data/app/views/catalog/_sms_form.html.erb +4 -4
  55. data/app/views/catalog/index.rss.builder +4 -4
  56. data/app/views/catalog/opensearch.json.jbuilder +3 -0
  57. data/app/views/catalog/opensearch.xml.builder +2 -2
  58. data/app/views/layouts/blacklight/base.html.erb +13 -1
  59. data/app/views/shared/_user_util_links.html.erb +18 -5
  60. data/blacklight.gemspec +3 -3
  61. data/config/locales/blacklight.ar.yml +5 -0
  62. data/config/locales/blacklight.ca.yml +5 -0
  63. data/config/locales/blacklight.de.yml +6 -2
  64. data/config/locales/blacklight.en.yml +5 -0
  65. data/config/locales/blacklight.es.yml +5 -0
  66. data/config/locales/blacklight.fr.yml +5 -0
  67. data/config/locales/blacklight.hu.yml +5 -0
  68. data/config/locales/blacklight.it.yml +6 -2
  69. data/config/locales/blacklight.nl.yml +5 -0
  70. data/config/locales/blacklight.pt-BR.yml +6 -2
  71. data/config/locales/blacklight.sq.yml +5 -0
  72. data/config/locales/blacklight.zh.yml +5 -0
  73. data/lib/blacklight/component.rb +1 -1
  74. data/lib/blacklight/configuration/action_config_map_entry.rb +31 -0
  75. data/lib/blacklight/configuration/display_field.rb +1 -0
  76. data/lib/blacklight/configuration/field.rb +6 -6
  77. data/lib/blacklight/configuration/fields.rb +1 -1
  78. data/lib/blacklight/configuration/sort_field.rb +2 -2
  79. data/lib/blacklight/configuration.rb +30 -19
  80. data/lib/blacklight/parameters.rb +1 -1
  81. data/lib/blacklight/search_builder.rb +58 -19
  82. data/lib/blacklight/search_state.rb +4 -0
  83. data/lib/blacklight/solr/request.rb +8 -1
  84. data/lib/blacklight/solr/response/group_response.rb +1 -1
  85. data/lib/blacklight/solr/response/spelling.rb +8 -8
  86. data/lib/blacklight/solr/response.rb +2 -21
  87. data/lib/blacklight/solr/search_builder_behavior.rb +3 -9
  88. data/lib/blacklight.rb +2 -2
  89. data/lib/generators/blacklight/assets/importmap_generator.rb +2 -2
  90. data/lib/generators/blacklight/assets_generator.rb +1 -1
  91. data/lib/generators/blacklight/controller_generator.rb +1 -1
  92. data/lib/generators/blacklight/document_generator.rb +1 -1
  93. data/lib/generators/blacklight/install_generator.rb +6 -4
  94. data/lib/generators/blacklight/models_generator.rb +1 -1
  95. data/lib/generators/blacklight/search_builder_generator.rb +1 -1
  96. data/lib/generators/blacklight/solr_generator.rb +1 -1
  97. data/lib/generators/blacklight/templates/catalog_controller.rb +20 -1
  98. data/lib/generators/blacklight/templates/solr/conf/schema.xml +264 -178
  99. data/lib/generators/blacklight/templates/solr/conf/solrconfig.xml +6 -14
  100. data/lib/generators/blacklight/test_support_generator.rb +1 -1
  101. data/lib/generators/blacklight/user_generator.rb +66 -5
  102. data/lib/railties/blacklight.rake +4 -4
  103. data/package.json +2 -2
  104. data/spec/components/blacklight/document/action_component_spec.rb +1 -1
  105. data/spec/components/blacklight/document/group_component_spec.rb +1 -1
  106. data/spec/components/blacklight/document_component_spec.rb +9 -9
  107. data/spec/components/blacklight/facet_component_spec.rb +2 -2
  108. data/spec/components/blacklight/facets/index_navigation_component_spec.rb +1 -2
  109. data/spec/components/blacklight/facets/list_component_spec.rb +1 -1
  110. data/spec/components/blacklight/facets/suggest_component_spec.rb +1 -2
  111. data/spec/components/blacklight/metadata_field_component_spec.rb +26 -0
  112. data/spec/components/blacklight/skip_link_component_spec.rb +2 -2
  113. data/spec/controllers/application_controller_spec.rb +71 -0
  114. data/spec/controllers/bookmarks_controller_spec.rb +18 -5
  115. data/spec/controllers/catalog_controller_spec.rb +7 -5
  116. data/spec/features/advanced_search_spec.rb +29 -16
  117. data/spec/features/alternate_controller_spec.rb +4 -4
  118. data/spec/features/axe_spec.rb +34 -22
  119. data/spec/features/bookmarks_spec.rb +13 -13
  120. data/spec/features/dark_mode_spec.rb +42 -0
  121. data/spec/features/did_you_mean_spec.rb +27 -27
  122. data/spec/features/facet_missing_spec.rb +3 -3
  123. data/spec/features/record_view_spec.rb +14 -14
  124. data/spec/features/search_context_spec.rb +8 -8
  125. data/spec/features/search_filters_spec.rb +16 -16
  126. data/spec/features/search_formats_spec.rb +2 -2
  127. data/spec/features/search_history_spec.rb +11 -11
  128. data/spec/features/search_pagination_spec.rb +17 -17
  129. data/spec/features/search_results_spec.rb +3 -3
  130. data/spec/features/search_sort_spec.rb +4 -4
  131. data/spec/features/search_spec.rb +20 -20
  132. data/spec/helpers/blacklight/url_helper_behavior_spec.rb +15 -15
  133. data/spec/helpers/blacklight_helper_spec.rb +1 -1
  134. data/spec/helpers/catalog_helper_spec.rb +7 -7
  135. data/spec/integration/generators/blacklight/user_generator_spec.rb +60 -0
  136. data/spec/lib/blacklight/search_state/pivot_filter_field_spec.rb +1 -1
  137. data/spec/models/blacklight/configurable_spec.rb +1 -1
  138. data/spec/models/blacklight/configuration_spec.rb +0 -19
  139. data/spec/models/blacklight/document/dublin_core_spec.rb +2 -2
  140. data/spec/models/blacklight/facet_paginator_spec.rb +1 -1
  141. data/spec/models/blacklight/search_builder_spec.rb +15 -3
  142. data/spec/models/blacklight/solr/repository_spec.rb +10 -2
  143. data/spec/models/blacklight/solr/search_builder_behavior_spec.rb +3 -5
  144. data/spec/models/blacklight/user_spec.rb +32 -5
  145. data/spec/models/bookmark_spec.rb +1 -1
  146. data/spec/models/record_mailer_spec.rb +8 -8
  147. data/spec/models/search_spec.rb +1 -1
  148. data/spec/models/solr_document_spec.rb +4 -0
  149. data/spec/presenters/blacklight/index_presenter_spec.rb +1 -0
  150. data/spec/presenters/blacklight/rendering/pipeline_spec.rb +1 -0
  151. data/spec/presenters/blacklight/show_presenter_spec.rb +1 -0
  152. data/spec/presenters/blacklight/thumbnail_presenter_spec.rb +1 -0
  153. data/spec/services/blacklight/search_service_spec.rb +7 -7
  154. data/spec/spec_helper.rb +3 -12
  155. data/spec/support/features/session_helpers.rb +13 -4
  156. data/spec/support/features/turbo_helpers.rb +13 -0
  157. data/spec/support/features.rb +2 -0
  158. data/spec/support/user_helpers.rb +24 -0
  159. data/spec/test_app_templates/lib/generators/test_app_generator.rb +1 -1
  160. data/spec/views/catalog/_document.html.erb_spec.rb +4 -4
  161. data/spec/views/catalog/_paginate_compact.html.erb_spec.rb +2 -2
  162. data/spec/views/catalog/_sort_and_per_page.html.erb_spec.rb +5 -5
  163. data/spec/views/catalog/index.html.erb_spec.rb +2 -2
  164. data/spec/views/catalog/show.html.erb_spec.rb +3 -4
  165. data/spec/views/shared/_user_util_links.html.erb_spec.rb +56 -0
  166. data/template.demo.rb +2 -1
  167. metadata +30 -22
  168. data/.hound.yml +0 -4
  169. data/.jshintrc +0 -22
  170. data/app/assets/images/blacklight/logo.png +0 -0
@@ -5,6 +5,8 @@
5
5
  module Blacklight::Controller
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ include Blacklight::GuestUser
9
+
8
10
  included do
9
11
  include ActiveSupport::Callbacks
10
12
 
@@ -17,6 +19,7 @@ module Blacklight::Controller
17
19
 
18
20
  if respond_to? :helper_method
19
21
  helper_method :current_user_session, :current_user, :current_or_guest_user
22
+ helper_method :blacklight_account_path, :blacklight_login_path, :blacklight_login_url, :blacklight_logout_link_options, :blacklight_logout_path
20
23
 
21
24
  # extra head content
22
25
  helper_method :has_user_authentication_provider?
@@ -28,8 +31,7 @@ module Blacklight::Controller
28
31
  # Which class to use for the search state. You can subclass SearchState if you
29
32
  # want to override any of the methods (e.g. SearchState#url_for_document)
30
33
  # TODO: move to Searchable
31
- class_attribute :search_state_class
32
- self.search_state_class = Blacklight::SearchState
34
+ class_attribute :search_state_class, default: Blacklight::SearchState
33
35
  end
34
36
 
35
37
  # @private
@@ -56,7 +58,7 @@ module Blacklight::Controller
56
58
  end
57
59
 
58
60
  # This must be on every controller that uses the layout, because it is used in
59
- # the header to draw Blacklight::SearchNavbarComponent (via the shared/header_navbar template)
61
+ # the header to draw Blacklight::SearchNavbarComponent
60
62
  # @return [Blacklight::SearchState] a memoized instance of the parameter state.
61
63
  def search_state
62
64
  @search_state ||= search_state_class.new(params, blacklight_config, self)
@@ -71,8 +73,8 @@ module Blacklight::Controller
71
73
  end
72
74
 
73
75
  def search_action_path *args
74
- if args.first.is_a? Hash
75
- args.first[:only_path] = true if args.first[:only_path].nil?
76
+ if args.first.is_a?(Hash) && args.first[:only_path].nil?
77
+ args.first[:only_path] = true
76
78
  end
77
79
 
78
80
  search_action_url(*args)
@@ -86,10 +88,10 @@ module Blacklight::Controller
86
88
 
87
89
  # Here's a stub implementation we'll add if it isn't provided for us
88
90
  def current_or_guest_user
89
- if defined? super
90
- super
91
+ if defined?(super) && (user = super)
92
+ user
91
93
  elsif has_user_authentication_provider?
92
- current_user
94
+ current_user || guest_user
93
95
  end
94
96
  end
95
97
  alias blacklight_current_or_guest_user current_or_guest_user
@@ -98,13 +100,13 @@ module Blacklight::Controller
98
100
  #
99
101
  #
100
102
  def has_user_authentication_provider?
101
- respond_to? :current_user
103
+ respond_to?(:current_user) || blacklight_login_path.present? || blacklight_logout_path.present?
102
104
  end
103
105
 
104
106
  ##
105
107
  # When a user logs in, transfer any saved searches or bookmarks to the current_user
106
108
  def transfer_guest_to_user
107
- return unless respond_to?(:current_user) && respond_to?(:guest_user) && current_user && guest_user
109
+ return unless respond_to?(:current_user) && current_user && guest_user
108
110
 
109
111
  current_user_searches = current_user.searches.pluck(:query_params)
110
112
  current_user_bookmarks = current_user.bookmarks.pluck(:document_id)
@@ -134,10 +136,51 @@ module Blacklight::Controller
134
136
 
135
137
  redirect_to(root_url) && return unless has_user_authentication_provider?
136
138
 
137
- redirect_to new_user_session_url(referer: request.fullpath)
139
+ store_location_for_rails_authentication
140
+ redirect_to(root_url) && return unless blacklight_login_url
141
+
142
+ redirect_to blacklight_login_url(referer: request.fullpath)
138
143
  end
139
144
 
140
145
  def determine_layout
141
146
  'blacklight'
142
147
  end
148
+
149
+ def blacklight_login_path(options = {})
150
+ if respond_to?(:new_user_session_path)
151
+ new_user_session_path(options)
152
+ elsif respond_to?(:new_session_path)
153
+ new_session_path(options)
154
+ end
155
+ end
156
+
157
+ def blacklight_login_url(options = {})
158
+ if respond_to?(:new_user_session_url)
159
+ new_user_session_url(options)
160
+ elsif respond_to?(:new_session_url)
161
+ new_session_url(options)
162
+ end
163
+ end
164
+
165
+ def blacklight_logout_path
166
+ if respond_to?(:destroy_user_session_path)
167
+ destroy_user_session_path
168
+ elsif respond_to?(:session_path)
169
+ session_path
170
+ end
171
+ end
172
+
173
+ def blacklight_logout_link_options
174
+ return {} unless respond_to?(:session_path)
175
+
176
+ { data: { turbo_method: :delete } }
177
+ end
178
+
179
+ def blacklight_account_path
180
+ edit_user_registration_path if respond_to?(:edit_user_registration_path)
181
+ end
182
+
183
+ def store_location_for_rails_authentication
184
+ session[:return_to_after_authenticating] = request.url if respond_to?(:new_session_url)
185
+ end
143
186
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blacklight
4
+ # Provides a persisted "guest" user for anonymous visitors so they can
5
+ # accumulate bookmarks and searches that are transferred to a real account
6
+ # upon sign-in (see Blacklight::Controller#transfer_guest_to_user).
7
+ #
8
+ # This is the fallback used by Rails' built-in authentication. When Devise
9
+ # together with the +devise-guests+ gem is used, that pair provides its own
10
+ # +#guest_user+ / +#current_or_guest_user+ (and knows how to build a valid
11
+ # Devise guest, e.g. skipping password validation). They sit higher in the
12
+ # ancestor chain than this concern, so +#guest_user+ defers to +super+ when
13
+ # available and only falls back to the implementation below otherwise. The
14
+ # helper methods below are intentionally named to avoid colliding with
15
+ # +devise-guests+' own +#create_guest_user+.
16
+ module GuestUser
17
+ extend ActiveSupport::Concern
18
+
19
+ # Returns a persisted guest user for the current session, creating one on
20
+ # first access. Keyed off the session so it survives across requests.
21
+ # Returns +nil+ if a guest cannot be created (e.g. no user model).
22
+ def guest_user
23
+ return super if defined?(super)
24
+ return @blacklight_guest_user if defined?(@blacklight_guest_user) && @blacklight_guest_user
25
+
26
+ @blacklight_guest_user = find_blacklight_guest_user || create_blacklight_guest_user
27
+ end
28
+
29
+ private
30
+
31
+ def find_blacklight_guest_user
32
+ return unless (id = session[:blacklight_guest_user_id])
33
+
34
+ blacklight_guest_user_class.find_by(id: id)
35
+ end
36
+
37
+ def create_blacklight_guest_user
38
+ user = blacklight_guest_user_class.create_guest_user!
39
+ session[:blacklight_guest_user_id] = user.id
40
+ user
41
+ rescue ActiveRecord::RecordInvalid
42
+ nil
43
+ end
44
+
45
+ # The model class that guest users are created from. Defaults to +User+,
46
+ # consistent with Blacklight::TokenBasedUser. Override to use another model.
47
+ def blacklight_guest_user_class
48
+ ::User
49
+ end
50
+ end
51
+ end
@@ -25,7 +25,7 @@ module Blacklight::SearchContext
25
25
 
26
26
  # Returns a list of Searches from the ids in the user's history.
27
27
  def searches_from_history
28
- session[:history].blank? ? ::Search.none : ::Search.where(id: session[:history]).order("updated_at desc")
28
+ session[:history].blank? ? ::Search.none : ::Search.where(id: session[:history]).order(updated_at: :desc)
29
29
  end
30
30
 
31
31
  # GET previous and next document json for the document specified by
@@ -7,6 +7,8 @@ module Blacklight
7
7
  include Blacklight::SearchContext
8
8
 
9
9
  included do
10
+ allow_unauthenticated_access raise: false if respond_to?(:allow_unauthenticated_access)
11
+
10
12
  copy_blacklight_config_from(CatalogController)
11
13
  end
12
14
 
@@ -23,7 +25,7 @@ module Blacklight
23
25
  flash[:error] = I18n.t('blacklight.search_history.clear.failure')
24
26
  end
25
27
 
26
- redirect_back fallback_location: blacklight.search_history_path
28
+ redirect_back_or_to(blacklight.search_history_path)
27
29
  end
28
30
  end
29
31
  end
@@ -24,6 +24,24 @@ module Blacklight::Searchable
24
24
  search_service_class.new(config: blacklight_config, search_state: search_state, **search_service_context)
25
25
  end
26
26
 
27
+ # This method may be overridden to customize search behavior.
28
+ # @return [Blacklight::Solr::Response] the solr response object
29
+ def retrieve_search_results
30
+ search_service.search_results
31
+ end
32
+
33
+ # This method may be overridden to customize search behavior.
34
+ # @return [Blacklight::Document] the solr document object
35
+ def retrieve_document(id)
36
+ search_service.fetch(id)
37
+ end
38
+
39
+ # This method may be overridden to customize search behavior.
40
+ # @return [Array<Blacklight::Document>] an array of solr document objects
41
+ def retrieve_documents(ids)
42
+ search_service.fetch(Array(ids))
43
+ end
44
+
27
45
  # Override this method on the class that includes Blacklight::Searchable to provide more context to the search service if necessary.
28
46
  # For example, if your search builder needs to be aware of the current user, override this method to return a hash including the current user.
29
47
  # Then the search builder could use some property about the current user to construct a constraint on the search.
@@ -106,5 +106,12 @@ module Blacklight
106
106
  def extra_body_classes
107
107
  @extra_body_classes ||= ["blacklight-#{controller.controller_name}", "blacklight-#{[controller.controller_name, controller.action_name].join('-')}"]
108
108
  end
109
+
110
+ ##
111
+ # True if dark mode support is enabled in the configuration.
112
+ # @see Blacklight::Configuration#dark_mode_support
113
+ def dark_mode_support?
114
+ blacklight_config.dark_mode_support
115
+ end
109
116
  end
110
117
  end
@@ -0,0 +1,85 @@
1
+ /*!
2
+ * Color mode toggler for Blacklight
3
+ * Based on Bootstrap's color mode toggler (https://getbootstrap.com/docs/5.3/customize/color-modes/#javascript)
4
+ */
5
+
6
+ import Core from 'blacklight-frontend/core'
7
+
8
+ const ColorThemeSwitcher = (() => {
9
+ 'use strict'
10
+
11
+ const getStoredTheme = () => localStorage.getItem('theme')
12
+ const setStoredTheme = theme => localStorage.setItem('theme', theme)
13
+
14
+ const getPreferredTheme = () => {
15
+ const storedTheme = getStoredTheme()
16
+ if (storedTheme) {
17
+ return storedTheme
18
+ }
19
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
20
+ }
21
+
22
+ const setTheme = theme => {
23
+ if (theme === 'auto') {
24
+ document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
25
+ } else {
26
+ document.documentElement.setAttribute('data-bs-theme', theme)
27
+ }
28
+ }
29
+
30
+ const showActiveTheme = (theme, focus = false) => {
31
+ const themeSwitcher = document.querySelector('#bl-theme-switcher')
32
+ if (!themeSwitcher) return
33
+
34
+ // Reset all dropdown items
35
+ document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
36
+ element.classList.remove('active')
37
+ element.setAttribute('aria-pressed', 'false')
38
+ const check = element.querySelector('.bl-theme-check')
39
+ if (check) check.classList.add('d-none')
40
+ })
41
+
42
+ // Activate the selected item
43
+ const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
44
+ if (btnToActive) {
45
+ btnToActive.classList.add('active')
46
+ btnToActive.setAttribute('aria-pressed', 'true')
47
+ const check = btnToActive.querySelector('.bl-theme-check')
48
+ if (check) check.classList.remove('d-none')
49
+ }
50
+
51
+ // Swap the toggle button icon
52
+ themeSwitcher.querySelectorAll('.bl-theme-icon').forEach(icon => icon.classList.add('d-none'))
53
+ const activeIcon = themeSwitcher.querySelector(`.bl-theme-icon[data-bl-theme-icon="${theme}"]`)
54
+ if (activeIcon) activeIcon.classList.remove('d-none')
55
+
56
+ themeSwitcher.setAttribute('aria-label', `Toggle theme (${theme})`)
57
+
58
+ if (focus) {
59
+ themeSwitcher.focus()
60
+ }
61
+ }
62
+
63
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
64
+ const storedTheme = getStoredTheme()
65
+ if (storedTheme !== 'light' && storedTheme !== 'dark') {
66
+ setTheme(getPreferredTheme())
67
+ }
68
+ })
69
+
70
+ document.addEventListener('click', e => {
71
+ const btn = e.target.closest('[data-bs-theme-value]')
72
+ if (!btn) return
73
+
74
+ const theme = btn.getAttribute('data-bs-theme-value')
75
+ setStoredTheme(theme)
76
+ setTheme(theme)
77
+ showActiveTheme(theme, true)
78
+ })
79
+
80
+ Core.onLoad(() => showActiveTheme(getPreferredTheme()))
81
+
82
+ return { setTheme, getPreferredTheme, showActiveTheme }
83
+ })()
84
+
85
+ export default ColorThemeSwitcher
@@ -1,5 +1,6 @@
1
1
  import BookmarkToggle from 'blacklight-frontend/bookmark_toggle'
2
2
  import ButtonFocus from 'blacklight-frontend/button_focus'
3
+ import ColorThemeSwitcher from 'blacklight-frontend/color_theme_switcher'
3
4
  import FacetSuggest from 'blacklight-frontend/facet_suggest'
4
5
  import Modal from 'blacklight-frontend/modal'
5
6
  import SearchContext from 'blacklight-frontend/search_context'
@@ -8,6 +9,7 @@ import Core from 'blacklight-frontend/core'
8
9
  export default {
9
10
  BookmarkToggle,
10
11
  ButtonFocus,
12
+ ColorThemeSwitcher,
11
13
  FacetSuggest,
12
14
  Modal,
13
15
  SearchContext,
@@ -36,8 +36,8 @@ module Blacklight::Document::DublinCore
36
36
  xml.target!
37
37
  end
38
38
 
39
- alias_method :export_as_xml, :export_as_oai_dc_xml
40
- alias_method :export_as_dc_xml, :export_as_oai_dc_xml
39
+ alias export_as_xml export_as_oai_dc_xml
40
+ alias export_as_dc_xml export_as_oai_dc_xml
41
41
 
42
42
  private
43
43
 
@@ -32,7 +32,7 @@ module Blacklight::Document
32
32
  end
33
33
 
34
34
  attr_reader :response, :_source
35
- alias_method :solr_response, :response
35
+ alias solr_response response
36
36
 
37
37
  delegate :[], :key?, :keys, :to_h, :as_json, to: :_source
38
38
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Blacklight::User
4
4
  extend ActiveSupport::Concern
5
+
5
6
  # SEE ALSO: The lib/blacklight/generator/user_generator.rb class for where this
6
7
  # is generated into the hosting application.
7
8
  included do
@@ -11,6 +12,41 @@ module Blacklight::User
11
12
  has_many :searches, dependent: :destroy, as: :user
12
13
  end
13
14
 
15
+ class_methods do
16
+ # The column that holds the user's login/identifier. Rails' authentication
17
+ # generator uses +email_address+, while Devise and others use +email+.
18
+ def blacklight_email_column
19
+ column_names.include?("email_address") ? :email_address : :email
20
+ end
21
+
22
+ # Create a persisted "guest" user for an anonymous visitor, so they can
23
+ # accumulate bookmarks and searches that are transferred to a real account
24
+ # upon sign-in. Works whether the model uses +email+ or +email_address+,
25
+ # and supplies a random password when the model authenticates with a
26
+ # password (either +has_secure_password+'s +password_digest+ column or
27
+ # Devise's +encrypted_password+ column).
28
+ def create_guest_user!
29
+ attributes = { blacklight_email_column => "guest_#{Time.current.to_i}_#{SecureRandom.hex(4)}@example.com" }
30
+
31
+ if authenticates_with_password?
32
+ password = SecureRandom.hex(16)
33
+ attributes[:password] = password
34
+ attributes[:password_confirmation] = password
35
+ end
36
+
37
+ create!(attributes)
38
+ end
39
+
40
+ # True when the model stores a password (Rails' +has_secure_password+ via
41
+ # +password_digest+, or Devise's +database_authenticatable+ via
42
+ # +encrypted_password+). In either case the model exposes a virtual
43
+ # +password+ / +password_confirmation+ attribute that must be populated to
44
+ # satisfy validation.
45
+ def authenticates_with_password?
46
+ column_names.intersect?(%w[password_digest encrypted_password])
47
+ end
48
+ end
49
+
14
50
  def bookmarks_for_documents documents = []
15
51
  if documents.any?
16
52
  bookmarks.where(document_type: documents.first.class.base_class.to_s, document_id: documents.map(&:id))
@@ -31,7 +31,7 @@ module Blacklight
31
31
 
32
32
  attr_reader :view_context, :document, :field_config, :except_operations, :options
33
33
 
34
- delegate :key, :component, to: :field_config
34
+ delegate :key, :component, :layout_component, to: :field_config
35
35
 
36
36
  # @return [Array<String>]
37
37
  def render
@@ -4,6 +4,7 @@ module Blacklight
4
4
  module Rendering
5
5
  class Microdata < AbstractStep
6
6
  include ActionView::Helpers::TagHelper
7
+
7
8
  def render
8
9
  return next_step(values) unless config.itemprop && html?
9
10
 
@@ -6,17 +6,17 @@ module Blacklight
6
6
  ##
7
7
  # Filters the query to only include the bookmarked items
8
8
  #
9
- # @param [Hash] solr_parameters
9
+ # @param [Blacklight::Solr::Request] request_parameters
10
10
  #
11
11
  # @return [void]
12
- def bookmarked(solr_parameters)
13
- solr_parameters[:fq] ||= []
12
+ def bookmarked(request_parameters)
14
13
  bookmarks = @scope.context.fetch(:bookmarks)
15
14
  return unless bookmarks
16
15
 
17
16
  document_ids = bookmarks.collect { |b| b.document_id.to_s }
18
- solr_parameters[:fq] += ["{!terms f=id}#{document_ids.join(',')}"]
17
+ request_parameters.append_ids_filter(blacklight_config.document_model.unique_key, document_ids)
19
18
  end
19
+
20
20
  self.default_processor_chain += [:bookmarked]
21
21
  end
22
22
  end
@@ -3,10 +3,16 @@
3
3
  # SearchService returns search results from the repository
4
4
  module Blacklight
5
5
  class SearchService
6
+ # @params [Blacklight::Configuration] config
7
+ # @params [Blacklight::SearchState] search_state
8
+ # @params [Class] search_builder_class a class that inherits from Blacklight::SearchBuilder
9
+ # @params [Hash] context any data the search builder needs to access. For example, the current user.
6
10
  def initialize(config:, search_state:, search_builder_class: config.search_builder_class, **context)
7
11
  @blacklight_config = config
8
12
  @search_state = search_state
9
- @user_params = @search_state.params
13
+ @user_params = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(@search_state.params,
14
+ 'Use @search_state.params instead of @user_params',
15
+ Blacklight.deprecation)
10
16
  @search_builder_class = search_builder_class
11
17
  @context = context
12
18
  end
@@ -15,11 +21,7 @@ module Blacklight
15
21
  attr_reader :blacklight_config, :context
16
22
 
17
23
  def search_builder
18
- search_builder_class.new(self)
19
- end
20
-
21
- def search_state_class
22
- @search_state.class
24
+ search_builder_class.new(self, blacklight_config: blacklight_config)
23
25
  end
24
26
 
25
27
  # Fetch query results from solr
@@ -71,11 +73,24 @@ module Blacklight
71
73
 
72
74
  # Get the previous and next document from a search result
73
75
  # @return [Blacklight::Solr::Response, Array<Blacklight::SolrDocument>] the solr response and a list of the first and last document
74
- def previous_and_next_documents_for_search(index, request_params, extra_controller_params = {})
75
- p = previous_and_next_document_params(index)
76
- new_state = request_params.is_a?(Blacklight::SearchState) ? request_params : Blacklight::SearchState.new(request_params, blacklight_config)
77
- query = search_builder.with(new_state).start(p.delete(:start)).rows(p.delete(:rows)).merge(extra_controller_params).merge(p)
78
- response = repository.search(params: query)
76
+ def previous_and_next_documents_for_search(index, request_params, params: nil, **extra_controller_params)
77
+ unless params
78
+ new_state = request_params.is_a?(Blacklight::SearchState) ? request_params : Blacklight::SearchState.new(request_params, blacklight_config)
79
+ builder = search_builder.with(new_state)
80
+
81
+ search_service_params = Blacklight.deprecation.silence do
82
+ previous_and_next_document_params(index)
83
+ end
84
+
85
+ builder = if search_service_params.empty?
86
+ builder.for_previous_and_next_documents(index).merge(extra_controller_params)
87
+ else
88
+ Blacklight.deprecation.warn("SearchService#previous_and_next_document_params returned parameters. Implement customizations in SearchBuilder#for_previous_and_next_document instead")
89
+ builder.start(search_service_params.delete(:start)).rows(search_service_params.delete(:rows)).merge(extra_controller_params).merge(search_service_params)
90
+ end
91
+ end
92
+
93
+ response = repository.search(params: params || builder)
79
94
  document_list = response.documents
80
95
 
81
96
  # only get the previous doc if there is one
@@ -90,14 +105,16 @@ module Blacklight
90
105
  # the second item is an other array. This second array contains
91
106
  # all of the field values for each of the documents...
92
107
  # where the field is the "field" argument passed in.
108
+ # @deprecated
93
109
  def opensearch_response(field = nil, extra_controller_params = {})
94
110
  field ||= blacklight_config.view_config(:opensearch).title_field
95
111
 
96
112
  query = search_builder.with(search_state).merge(solr_opensearch_params(field)).merge(extra_controller_params)
97
- response = repository.search(query)
113
+ response = repository.search(params: query)
98
114
 
99
115
  [search_state.query_param, response.documents.flat_map { |doc| doc[field] }.uniq]
100
116
  end
117
+ Blacklight.deprecation.deprecate_methods Blacklight::SearchService, opensearch_response: "The opensearch_response method is deprecated without replacement."
101
118
 
102
119
  private
103
120
 
@@ -113,34 +130,20 @@ module Blacklight
113
130
 
114
131
  ##
115
132
  # Opensearch autocomplete parameters for plucking a field's value from the results
133
+ # @deprecated
116
134
  def solr_opensearch_params(field)
117
135
  solr_params = {}
118
136
  solr_params[:rows] ||= 10
119
137
  solr_params[:fl] = field || blacklight_config.view_config(:opensearch).title_field
120
138
  solr_params
121
139
  end
140
+ Blacklight.deprecation.deprecate_methods Blacklight::SearchService, solr_opensearch_params: "The solr_opensearch_params method is deprecated without replacement."
122
141
 
123
- ##
124
- # Pagination parameters for selecting the previous and next documents
125
- # out of a result set.
126
- def previous_and_next_document_params(index, window = 1)
127
- solr_params = blacklight_config.document_pagination_params.dup
128
-
129
- if solr_params.empty?
130
- solr_params[:fl] = blacklight_config.document_model.unique_key
131
- end
132
-
133
- if index > 0
134
- solr_params[:start] = index - window # get one before
135
- solr_params[:rows] = (2 * window) + 1 # and one after
136
- else
137
- solr_params[:start] = 0 # there is no previous doc
138
- solr_params[:rows] = 2 * window # but there should be one after
139
- end
140
-
141
- solr_params[:facet] = false
142
- solr_params
142
+ def previous_and_next_document_params(*, **)
143
+ {}
143
144
  end
145
+ Blacklight.deprecation.deprecate_methods(Blacklight::SearchService,
146
+ previous_and_next_document_params: "Use SearchBuilder#for_previous_and_next_documents instead of SearchService#previous_and_next_document_params")
144
147
 
145
148
  ##
146
149
  # Retrieve a set of documents by id
@@ -149,11 +152,20 @@ module Blacklight
149
152
  def fetch_many(ids, extra_controller_params)
150
153
  extra_controller_params ||= {}
151
154
 
155
+ requested_rows = extra_controller_params.delete(:rows)
156
+ if requested_rows
157
+ Blacklight.deprecation.warn("Passing :rows to fetch_many is deprecated. Create an issue in blacklight if you see this warning. " \
158
+ "Otherwise, :rows will be ignored in Blackight 10")
159
+ else
160
+ requested_rows = ids.count
161
+ end
162
+
152
163
  query = search_builder
153
164
  .with(search_state)
154
165
  .where(blacklight_config.document_model.unique_key => ids)
155
166
  .merge(blacklight_config.fetch_many_document_params)
156
167
  .merge(extra_controller_params)
168
+ query.rows(requested_rows)
157
169
 
158
170
  # find_many was introduced in Blacklight 8.4. Before that, we used the
159
171
  # regular search method (possibly with a find-many specific `qt` parameter).
@@ -162,7 +174,8 @@ module Blacklight
162
174
  solr_response = if repository.respond_to?(:find_many)
163
175
  repository.find_many(query)
164
176
  else
165
- repository.search(query)
177
+ Blacklight.deprecation.warn("Repository#find_many is not implemented. Falling back to Repository#search.")
178
+ repository.search(params: query)
166
179
  end
167
180
 
168
181
  solr_response.documents
@@ -88,7 +88,7 @@ module Blacklight
88
88
  def cast(input)
89
89
  value = super
90
90
 
91
- return value unless value.is_a?(String)
91
+ return value unless value.is_a?(::String)
92
92
 
93
93
  JSON.parse(value)
94
94
  end
@@ -6,7 +6,7 @@
6
6
 
7
7
  <div class="modal-body">
8
8
  <%= render '/shared/flash_msg' %>
9
- <div class="row">
9
+ <div class="row mb-3">
10
10
  <label class="col-form-label col-sm-2" for="to">
11
11
  <%= t('blacklight.email.form.to') %>
12
12
  </label>
@@ -15,7 +15,7 @@
15
15
  </div>
16
16
  </div>
17
17
 
18
- <div class="row">
18
+ <div class="row mb-3">
19
19
  <label class="col-form-label col-sm-2" for="message">
20
20
  <%= t('blacklight.email.form.message') %>
21
21
  </label>
@@ -25,7 +25,7 @@
25
25
  </div>
26
26
 
27
27
  <% @documents&.each do |doc| %>
28
- <%=hidden_field_tag "id[]", doc.id %>
28
+ <%= hidden_field_tag "id[]", doc.id %>
29
29
  <% end %>
30
30
  <%- if params[:sort] -%>
31
31
  <%= hidden_field_tag "sort", params[:sort] %>
@@ -1,4 +1,4 @@
1
- <div class="text-center p-5 mb-4 bg-light rounded-3">
1
+ <div class="text-center p-5 mb-4 bg-body-tertiary border rounded-3">
2
2
  <h1 class="fw-bold"><%= t('blacklight.welcome') %></h1>
3
3
 
4
4
  <p class="lead">Blacklight is a multi-institutional open-source collaboration building a better discovery platform framework.</p>