geoblacklight 6.0.0.pre.alpha.7 → 6.0.0.pre.beta.2

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 (231) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +14 -3
  3. data/README.md +2 -2
  4. data/Rakefile +1 -17
  5. data/app/assets/images/geoblacklight/logo-dark.svg +31 -0
  6. data/app/assets/images/geoblacklight/logo.svg +10 -10
  7. data/app/assets/stylesheets/geoblacklight/global.css +44 -7
  8. data/app/assets/stylesheets/geoblacklight/index_maps.css +0 -7
  9. data/app/assets/stylesheets/geoblacklight/record.css +33 -9
  10. data/app/assets/stylesheets/geoblacklight/search.css +66 -17
  11. data/app/assets/stylesheets/geoblacklight/viewers.css +18 -111
  12. data/app/assets/stylesheets/geoblacklight.css +0 -5
  13. data/app/components/blacklight/icons/georeferenced_component.rb +23 -0
  14. data/app/components/geoblacklight/document/download_link_component.html.erb +1 -1
  15. data/app/components/geoblacklight/document/preview_component.html.erb +0 -2
  16. data/app/components/geoblacklight/document_component.html.erb +0 -4
  17. data/app/components/geoblacklight/facets/bbox_field_component.html.erb +2 -1
  18. data/app/components/geoblacklight/georeferenced_header_badge_component.rb +16 -0
  19. data/app/components/geoblacklight/header_badge_component.html.erb +4 -2
  20. data/app/components/geoblacklight/header_badge_component.rb +1 -1
  21. data/app/components/geoblacklight/header_badges_component.html.erb +5 -0
  22. data/app/components/geoblacklight/header_badges_component.rb +34 -0
  23. data/app/components/geoblacklight/iiif_drag_drop_component.html.erb +1 -1
  24. data/app/components/geoblacklight/item_map_viewer_component.html.erb +0 -58
  25. data/app/components/geoblacklight/item_map_viewer_component.rb +19 -70
  26. data/app/components/geoblacklight/locator_map_component.rb +31 -0
  27. data/app/components/geoblacklight/login_link_component.html.erb +1 -1
  28. data/app/components/geoblacklight/login_link_component.rb +17 -1
  29. data/app/components/geoblacklight/metadata_description_markdown_component.rb +3 -9
  30. data/app/components/geoblacklight/overview_map_component.html.erb +2 -0
  31. data/app/components/geoblacklight/overview_map_component.rb +86 -0
  32. data/app/components/geoblacklight/relations/relation_component.html.erb +12 -0
  33. data/app/components/geoblacklight/relations/relation_component.rb +30 -0
  34. data/app/components/geoblacklight/relations/relations_component.html.erb +15 -0
  35. data/app/components/geoblacklight/relations/relations_component.rb +59 -0
  36. data/app/components/geoblacklight/resource_header_badge_component.rb +1 -1
  37. data/app/components/geoblacklight/search_result_component.html.erb +14 -33
  38. data/app/components/geoblacklight/search_result_component.rb +2 -14
  39. data/app/components/geoblacklight/static_map_component.rb +3 -14
  40. data/app/components/geoblacklight/truncatable_metadata_field_layout_component.html.erb +7 -0
  41. data/app/components/geoblacklight/truncatable_metadata_field_layout_component.rb +45 -0
  42. data/app/controllers/{relation_controller.rb → relations_controller.rb} +4 -2
  43. data/app/controllers/viewer_record_controller.rb +28 -0
  44. data/app/helpers/geoblacklight_helper.rb +57 -6
  45. data/app/javascript/geoblacklight/controllers/overview_map_controller.js +143 -0
  46. data/app/javascript/geoblacklight/core.js +8 -10
  47. data/app/javascript/geoblacklight/index.js +9 -12
  48. data/app/javascript/geoblacklight/initializers/field_truncation.js +62 -0
  49. data/app/javascript/geoblacklight/initializers/metadata_download.js +3 -8
  50. data/app/javascript/geoblacklight/initializers/truncation.js +7 -3
  51. data/app/javascript/geoblacklight/initializers/viewer_requests.js +61 -0
  52. data/app/javascript/geoblacklight/initializers/viewer_theme.js +28 -0
  53. data/app/models/concerns/geoblacklight/solr_document/thumbnail.rb +38 -0
  54. data/app/models/concerns/geoblacklight/solr_document.rb +2 -0
  55. data/app/models/concerns/geoblacklight/suppressed_records_search_behavior.rb +1 -1
  56. data/app/views/catalog/_document_split.html.erb +10 -10
  57. data/app/views/catalog/_home_text.html.erb +1 -1
  58. data/app/views/catalog/metadata.html.erb +1 -1
  59. data/app/views/{relation → relations}/index.html.erb +1 -1
  60. data/app/views/relations/index.json.jbuilder +9 -0
  61. data/config/importmap.rb +8 -8
  62. data/config/locales/geoblacklight.en.yml +48 -99
  63. data/geoblacklight.gemspec +3 -3
  64. data/lib/generators/geoblacklight/assets_generator.rb +2 -2
  65. data/lib/generators/geoblacklight/install_generator.rb +3 -6
  66. data/lib/generators/geoblacklight/templates/assets/customizations.css +5 -1
  67. data/lib/generators/geoblacklight/templates/catalog_controller.rb +17 -27
  68. data/lib/generators/geoblacklight/templates/settings.yml +15 -95
  69. data/lib/geoblacklight/configuration/display_note_shown_config.rb +3 -0
  70. data/lib/geoblacklight/configuration/relationship_config.rb +7 -1
  71. data/lib/geoblacklight/configuration/relationships_config.rb +15 -15
  72. data/lib/geoblacklight/configuration/settings_attributes.rb +85 -0
  73. data/lib/geoblacklight/configuration/settings_builder.rb +3 -14
  74. data/lib/geoblacklight/configuration.rb +23 -62
  75. data/lib/geoblacklight/exceptions.rb +4 -0
  76. data/lib/geoblacklight/reference.rb +0 -2
  77. data/lib/geoblacklight/references.rb +17 -5
  78. data/lib/geoblacklight/relations/ancestors.rb +12 -0
  79. data/lib/geoblacklight/relations/descendants.rb +11 -0
  80. data/lib/geoblacklight/relations/query.rb +39 -0
  81. data/lib/geoblacklight/{relation → relations}/relation_response.rb +5 -4
  82. data/lib/geoblacklight/routes/exportable.rb +2 -1
  83. data/lib/geoblacklight/version.rb +1 -1
  84. data/lib/geoblacklight/viewer_record.rb +39 -0
  85. data/lib/geoblacklight.rb +1 -1
  86. data/lib/tasks/geoblacklight.rake +1 -17
  87. data/lib/tasks/solr.rb +37 -0
  88. data/schema/readme.md +1 -1
  89. data/script/wait-for-solr +36 -0
  90. data/solr/conf/solrconfig.xml +10 -16
  91. data/spec/components/geoblacklight/document/sidebar_component_spec.rb +3 -3
  92. data/spec/components/geoblacklight/document_component_spec.rb +8 -34
  93. data/spec/components/geoblacklight/{header_icons_component_spec.rb → header_badges_component_spec.rb} +20 -2
  94. data/spec/components/geoblacklight/item_map_viewer_component_spec.rb +65 -15
  95. data/spec/components/geoblacklight/locator_map_component_spec.rb +54 -0
  96. data/spec/components/geoblacklight/login_link_component_spec.rb +20 -12
  97. data/spec/components/geoblacklight/metadata_description_markdown_component_spec.rb +1 -6
  98. data/spec/components/geoblacklight/overview_map_component_spec.rb +136 -0
  99. data/spec/components/geoblacklight/relations/relation_component_spec.rb +98 -0
  100. data/spec/components/geoblacklight/relations/relations_component_spec.rb +146 -0
  101. data/spec/components/geoblacklight/search_result_component_spec.rb +66 -39
  102. data/spec/components/geoblacklight/static_map_component_spec.rb +8 -2
  103. data/spec/components/geoblacklight/truncatable_metadata_field_layout_component_spec.rb +83 -0
  104. data/spec/controllers/catalog_controller_spec.rb +22 -7
  105. data/spec/controllers/{relation_controller_spec.rb → relations_controller_spec.rb} +1 -1
  106. data/spec/factories/user.rb +6 -1
  107. data/spec/features/axe_spec.rb +5 -3
  108. data/spec/features/home_page_spec.rb +8 -7
  109. data/spec/features/index_view_spec.rb +6 -20
  110. data/spec/features/layer_preview_spec.rb +4 -11
  111. data/spec/features/locator_map_spec.rb +31 -0
  112. data/spec/features/relations_spec.rb +22 -11
  113. data/spec/features/restricted_viewer_spec.rb +14 -0
  114. data/spec/features/search_results_map_spec.rb +89 -13
  115. data/spec/features/search_spec.rb +6 -2
  116. data/spec/features/sign_in_spec.rb +26 -0
  117. data/spec/features/tilejson_spec.rb +1 -3
  118. data/spec/features/tms_spec.rb +2 -2
  119. data/spec/features/truncated_metadata_spec.rb +77 -0
  120. data/spec/features/viewer_requests_spec.rb +40 -0
  121. data/spec/features/viewer_theme_spec.rb +51 -0
  122. data/spec/features/wmts_spec.rb +6 -6
  123. data/spec/features/xyz_spec.rb +3 -3
  124. data/spec/fixtures/solr_documents/README.md +18 -14
  125. data/spec/fixtures/solr_documents/actual-polygon1.json +22 -18
  126. data/spec/fixtures/solr_documents/b1g_wabash_child_15.json +14 -15
  127. data/spec/fixtures/solr_documents/b1g_wabash_child_16.json +14 -15
  128. data/spec/fixtures/solr_documents/b1g_wabash_child_17.json +14 -15
  129. data/spec/fixtures/solr_documents/b1g_wabash_child_18.json +14 -15
  130. data/spec/fixtures/solr_documents/b1g_wabash_parent.json +14 -15
  131. data/spec/fixtures/solr_documents/esri-dynamic-layer-single-layer.json +2 -2
  132. data/spec/fixtures/solr_documents/esri-feature-layer.json +1 -1
  133. data/spec/fixtures/solr_documents/esri-image-map-layer.json +20 -20
  134. data/spec/fixtures/solr_documents/esri-wms-layer.json +22 -24
  135. data/spec/fixtures/solr_documents/index_map_point.json +3 -3
  136. data/spec/fixtures/solr_documents/public_polygon.json +28 -0
  137. data/spec/helpers/geoblacklight_helper_spec.rb +103 -25
  138. data/spec/lib/geoblacklight/bounding_box_spec.rb +16 -0
  139. data/spec/lib/geoblacklight/configuration/relationships_config_spec.rb +121 -0
  140. data/spec/lib/geoblacklight/configuration/settings_attributes_spec.rb +113 -0
  141. data/spec/lib/geoblacklight/configuration/settings_builder_spec.rb +33 -4
  142. data/spec/lib/geoblacklight/references_spec.rb +35 -0
  143. data/spec/lib/geoblacklight/{relation → relations}/ancestors_spec.rb +17 -6
  144. data/spec/lib/geoblacklight/{relation → relations}/descendants_spec.rb +14 -7
  145. data/spec/lib/geoblacklight/{relation → relations}/relation_response_spec.rb +31 -17
  146. data/spec/lib/geoblacklight/viewer_record_spec.rb +69 -0
  147. data/spec/models/concerns/geoblacklight/solr_document/thumbnail_spec.rb +149 -0
  148. data/spec/models/concerns/geoblacklight/solr_document_spec.rb +1 -1
  149. data/spec/requests/bookmarks_spec.rb +2 -1
  150. data/spec/requests/catalog_show_spec.rb +24 -2
  151. data/spec/requests/index_view_spec.rb +50 -2
  152. data/spec/requests/missing_metadata_spec.rb +1 -1
  153. data/spec/requests/relations_spec.rb +9 -2
  154. data/spec/requests/search_results_spec.rb +6 -6
  155. data/spec/requests/viewer_record_spec.rb +37 -0
  156. data/spec/requests/viewer_requests_spec.rb +52 -0
  157. data/spec/solr_config_spec.rb +41 -0
  158. data/spec/solr_fixtures_spec.rb +23 -0
  159. data/spec/spec_helper.rb +13 -3
  160. data/spec/support/authentication.rb +37 -0
  161. data/spec/support/features/map_helpers.rb +68 -0
  162. data/spec/support/features/session_helpers.rb +18 -13
  163. data/spec/support/features/viewer_helpers.rb +20 -0
  164. data/spec/support/features.rb +4 -0
  165. data/spec/test_app_templates/lib/generators/test_app_generator.rb +1 -1
  166. data/template.rb +1 -1
  167. metadata +72 -84
  168. data/app/components/geoblacklight/accordion_component.html.erb +0 -16
  169. data/app/components/geoblacklight/accordion_component.rb +0 -15
  170. data/app/components/geoblacklight/attribute_table_component.html.erb +0 -15
  171. data/app/components/geoblacklight/attribute_table_component.rb +0 -17
  172. data/app/components/geoblacklight/header_icons_component.html.erb +0 -3
  173. data/app/components/geoblacklight/header_icons_component.rb +0 -27
  174. data/app/components/geoblacklight/index_map_inspect_component.html.erb +0 -12
  175. data/app/components/geoblacklight/index_map_inspect_component.rb +0 -15
  176. data/app/components/geoblacklight/index_map_legend_component.html.erb +0 -14
  177. data/app/components/geoblacklight/index_map_legend_component.rb +0 -24
  178. data/app/components/geoblacklight/location_leaflet_map_component.rb +0 -68
  179. data/app/components/geoblacklight/relations_component.html.erb +0 -23
  180. data/app/components/geoblacklight/relations_component.rb +0 -25
  181. data/app/components/geoblacklight/viewer_help_text_component.html.erb +0 -1
  182. data/app/components/geoblacklight/viewer_help_text_component.rb +0 -46
  183. data/app/controllers/wms_controller.rb +0 -17
  184. data/app/javascript/geoblacklight/controllers/leaflet_viewer_controller.js +0 -240
  185. data/app/javascript/geoblacklight/controllers/openlayers_viewer_controller.js +0 -74
  186. data/app/javascript/geoblacklight/controllers/search_results_controller.js +0 -50
  187. data/app/javascript/geoblacklight/initializers/popovers.js +0 -11
  188. data/app/javascript/geoblacklight/initializers/tooltips.js +0 -51
  189. data/app/javascript/geoblacklight/leaflet/basemaps.js +0 -73
  190. data/app/javascript/geoblacklight/leaflet/constants.js +0 -18
  191. data/app/javascript/geoblacklight/leaflet/controls/geosearch.js +0 -168
  192. data/app/javascript/geoblacklight/leaflet/controls/layer_opacity.js +0 -121
  193. data/app/javascript/geoblacklight/leaflet/controls/sidebar.js +0 -49
  194. data/app/javascript/geoblacklight/leaflet/controls/sleep.js +0 -101
  195. data/app/javascript/geoblacklight/leaflet/inspection.js +0 -210
  196. data/app/javascript/geoblacklight/leaflet/layer_index_map.js +0 -128
  197. data/app/javascript/geoblacklight/leaflet/layer_wmts.js +0 -57
  198. data/app/javascript/geoblacklight/leaflet/layers.js +0 -146
  199. data/app/javascript/geoblacklight/leaflet/utils.js +0 -83
  200. data/app/javascript/geoblacklight/openlayers/basemaps.js +0 -55
  201. data/app/javascript/geoblacklight/openlayers/inspection.js +0 -42
  202. data/app/javascript/geoblacklight/openlayers/layers.js +0 -44
  203. data/app/views/relation/index.json.jbuilder +0 -8
  204. data/lib/geoblacklight/configuration/layer_config.rb +0 -28
  205. data/lib/geoblacklight/configuration/leaflet_config.rb +0 -73
  206. data/lib/geoblacklight/configuration/leaflet_layers_config.rb +0 -33
  207. data/lib/geoblacklight/configuration/leaflet_sleep_config.rb +0 -20
  208. data/lib/geoblacklight/relation/ancestors.rb +0 -31
  209. data/lib/geoblacklight/relation/descendants.rb +0 -31
  210. data/lib/geoblacklight/routes/wms.rb +0 -15
  211. data/lib/geoblacklight/wms_layer/feature_info_response.rb +0 -37
  212. data/lib/geoblacklight/wms_layer.rb +0 -34
  213. data/spec/components/geoblacklight/attribute_table_component_spec.rb +0 -36
  214. data/spec/components/geoblacklight/index_map_legend_component_spec.rb +0 -29
  215. data/spec/components/geoblacklight/viewer_help_text_component_spec.rb +0 -27
  216. data/spec/controllers/wms_controller_spec.rb +0 -34
  217. data/spec/features/configurable_basemap_spec.rb +0 -40
  218. data/spec/features/full_screen_control_spec.rb +0 -15
  219. data/spec/features/layer_inspection_spec.rb +0 -12
  220. data/spec/features/layer_opacity_spec.rb +0 -16
  221. data/spec/fixtures/solr_documents/harvard_raster.json +0 -39
  222. data/spec/fixtures/solr_documents/public_polygon_mit.json +0 -23
  223. data/spec/fixtures/solr_documents/uva_slug_colon.json +0 -29
  224. data/spec/javascripts/geoblacklight_spec.js +0 -11
  225. data/spec/lib/geoblacklight/configuration/layer_config_spec.rb +0 -71
  226. data/spec/lib/geoblacklight/configuration/leaflet_config_spec.rb +0 -129
  227. data/spec/lib/geoblacklight/configuration/leaflet_layers_config_spec.rb +0 -63
  228. data/spec/lib/geoblacklight/configuration_spec.rb +0 -48
  229. data/spec/lib/geoblacklight/wms_layer/feature_info_response_spec.rb +0 -54
  230. data/spec/lib/geoblacklight/wms_layer_spec.rb +0 -115
  231. /data/app/components/geoblacklight/{location_leaflet_map_component.html.erb → locator_map_component.html.erb} +0 -0
@@ -19,21 +19,10 @@ module Geoblacklight
19
19
  @label ||= t("geoblacklight.location")
20
20
  end
21
21
 
22
+ # The card is the label and the border; the map inside it is a locator, which is the viewer's own
23
+ # component for where a single record is.
22
24
  def viewer_tag
23
- leaflet_options = Geoblacklight.configuration.leaflet_options.deep_dup
24
- leaflet_options.sleep.sleep = false
25
- tag.div(nil,
26
- id: "static-map",
27
- class: "viewer border",
28
- data: {
29
- "controller" => "leaflet-viewer",
30
- "leaflet-viewer-basemap-value" => Geoblacklight.configuration.basemap_provider,
31
- "leaflet-viewer-dark-basemap-value" => Geoblacklight.configuration.dark_basemap_provider,
32
- "leaflet-viewer-page-value" => "STATIC_MAP",
33
- "leaflet-viewer-map-geom-value" => @document.geometry.geojson,
34
- "leaflet-viewer-options-value" => leaflet_options.to_h,
35
- "leaflet-viewer-draw-initial-bounds-value" => true
36
- })
25
+ render Geoblacklight::LocatorMapComponent.new(document: @document)
37
26
  end
38
27
  end
39
28
  end
@@ -0,0 +1,7 @@
1
+ <%= tag.div class: "truncate-field row", data: truncate_data do %>
2
+ <dt class="blacklight-<%= @key %> <%= @label_class %>"><%= label %></dt>
3
+ <% values.each do |v| %>
4
+ <%= v %>
5
+ <% end %>
6
+ <%= tag.dd class: read_more_slot_class %>
7
+ <% end %>
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geoblacklight
4
+ # Lays out a metadata field so that it shows only the first few of its values, with a
5
+ # button to reveal the rest. The label and the values are wrapped in a div so that
6
+ # app/javascript/geoblacklight/initializers/field_truncation.js can tell which values
7
+ # belong to the field; it reads the data attributes set here.
8
+ #
9
+ # Configure a show field to use it, optionally overriding the limit. Because this is a
10
+ # layout rather than a field component, it composes with whatever component the field
11
+ # already uses to render its values:
12
+ #
13
+ # config.add_show_field field_config.subject, label: "Subject", link_to_facet: true,
14
+ # layout_component: Geoblacklight::TruncatableMetadataFieldLayoutComponent, limit: 3
15
+ class TruncatableMetadataFieldLayoutComponent < Blacklight::MetadataFieldLayoutComponent
16
+ DEFAULT_LIMIT = 5
17
+
18
+ # @param read_more_text [String] label for the button that reveals the rest of the values
19
+ # @param close_text [String] label for the button once the values are revealed
20
+ # @param limit [Integer] number of values to show before hiding the rest
21
+ def initialize(read_more_text: nil, close_text: nil, limit: nil, **kwargs)
22
+ @read_more_text = read_more_text
23
+ @close_text = close_text
24
+ @limit = limit
25
+ super(**kwargs)
26
+ end
27
+
28
+ private
29
+
30
+ def truncate_data
31
+ {
32
+ read_more_text: @read_more_text || t("geoblacklight.truncate.read_more"),
33
+ close_text: @close_text || t("geoblacklight.truncate.close"),
34
+ button_label: t("geoblacklight.truncate.button_label"),
35
+ limit: @limit || @field.field_config[:limit] || DEFAULT_LIMIT
36
+ }
37
+ end
38
+
39
+ # A <dl> cannot hold a bare <button>, so the initializer moves the button into this
40
+ # empty <dd>. It takes the same columns as the values so that it lines up with them.
41
+ def read_more_slot_class
42
+ ["read-more-slot", @offset_class, @value_class]
43
+ end
44
+ end
45
+ end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class RelationController < ApplicationController
3
+ class RelationsController < ApplicationController
4
4
  include Blacklight::Configurable
5
5
 
6
6
  copy_blacklight_config_from(CatalogController)
7
7
 
8
+ allow_unauthenticated_access raise: false if respond_to?(:allow_unauthenticated_access)
9
+
8
10
  def index
9
- @relations = Geoblacklight::Relation::RelationResponse.new(params[:id], repository)
11
+ @relations = Geoblacklight::Relations::RelationResponse.new(params[:id], repository)
10
12
  render layout: !(request.xhr? || request.headers["Turbo-Frame"].present?)
11
13
  end
12
14
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Serves the version of the Solr document that is used by viewers.
4
+ class ViewerRecordController < ApplicationController
5
+ include Blacklight::Configurable
6
+
7
+ copy_blacklight_config_from(CatalogController)
8
+
9
+ allow_unauthenticated_access raise: false if respond_to?(:allow_unauthenticated_access)
10
+
11
+ rescue_from Blacklight::Exceptions::RecordNotFound do
12
+ head :not_found
13
+ end
14
+
15
+ def show
16
+ render json: Geoblacklight::ViewerRecord.new(document, available: helpers.document_available?(document))
17
+ end
18
+
19
+ private
20
+
21
+ def document
22
+ @document ||= repository.find(params[:id]).documents.first
23
+ end
24
+
25
+ def repository
26
+ @repository ||= blacklight_config.repository_class.new(blacklight_config)
27
+ end
28
+ end
@@ -24,15 +24,28 @@ module GeoblacklightHelper
24
24
  end
25
25
 
26
26
  def document_available?(document)
27
- document.public? || (document.same_institution? && user_signed_in?)
27
+ document.public? || (document.same_institution? && current_user_signed_in?)
28
+ end
29
+
30
+ def current_user_signed_in?
31
+ controller.respond_to?(:current_user) && current_user.present?
32
+ end
33
+
34
+ # If you turn off dark mode support, Geoblacklight will display in light mode,
35
+ # but ogm-viewer will still try to honor a system dark mode preference. This
36
+ # ensures that everything stays light until you explicitly enable dark mode.
37
+ def geoblacklight_viewer_theme
38
+ return if blacklight_config.dark_mode_support
39
+
40
+ "light"
28
41
  end
29
42
 
30
43
  ##
31
- # Blacklight catalog controller helper method to truncate field value to 150 chars
32
- # @param [SolrDocument] args
33
- # @return [String]
34
- def snippit(args)
35
- truncate(Array(args[:value]).flatten.join(" "), length: 150)
44
+ # Transform markdown into HTML
45
+ # @param [String] value markdown to transform
46
+ # @return [ActiveSupport::SafeBuffer] rendered HTML
47
+ def markdown_to_html(value)
48
+ Commonmarker.to_html(value).html_safe
36
49
  end
37
50
 
38
51
  ##
@@ -76,4 +89,42 @@ module GeoblacklightHelper
76
89
  "index"
77
90
  end
78
91
  end
92
+
93
+ ##
94
+ # Blacklight view_config.thumbnail_method contract. Renders the document's
95
+ # configured thumbnail_url as an <img>, or nil if unavailable/disabled.
96
+ # @param [SolrDocument] document
97
+ # @param [Hash] image_options
98
+ # @return [String, nil]
99
+ def geoblacklight_thumbnail(document, image_options = {})
100
+ return unless Geoblacklight.configuration.thumbnails_enabled
101
+
102
+ url = document.thumbnail_url
103
+ return if url.blank?
104
+
105
+ image_tag url, {loading: "lazy"}.merge(image_options)
106
+ end
107
+
108
+ ##
109
+ # Blacklight view_config.default_thumbnail (Symbol) contract. Falls back to
110
+ # a placeholder icon based on the document's resource/data type.
111
+ # @param [SolrDocument] document
112
+ # @param [Hash] _image_options unused; icons aren't rendered as <img>
113
+ # @return [String, nil]
114
+ def geoblacklight_default_thumbnail(document, _image_options = {})
115
+ return unless Geoblacklight.configuration.thumbnails_enabled
116
+
117
+ default_thumbnail_icon(document)
118
+ end
119
+
120
+ private
121
+
122
+ ##
123
+ # @param [SolrDocument] document
124
+ # @return [String]
125
+ def default_thumbnail_icon(document)
126
+ dataset_type = document.resource_type&.find { |type| type.include?(" data") }&.gsub(" data", "")
127
+ name = [dataset_type, document.resource_class&.first].compact.first
128
+ geoblacklight_icon(name)
129
+ end
79
130
  end
@@ -0,0 +1,143 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Which row is being hovered
4
+ const RESULTS = "#documents[data-overview-map-results]"
5
+ const ROW = `${RESULTS} [data-document-counter]`
6
+
7
+ // Controller used for the search results map
8
+ // NOTE: the map is turbo-permanent to prevent flashing on reload
9
+ export default class OverviewMapController extends Controller {
10
+ static values = {
11
+ catalogUrl: String,
12
+ }
13
+
14
+ connect() {
15
+ this.element.searchBounds = this.boundsMarker()?.dataset.bounds || undefined
16
+ this.load()
17
+ }
18
+
19
+ disconnect() {
20
+ this.unwatchResults()
21
+ }
22
+
23
+ boundsMarker() {
24
+ return document.getElementById(`${this.element.id}-bounds`)
25
+ }
26
+
27
+ async load() {
28
+ // Imported here rather than at the top of the file: this library re-exports everything the
29
+ // viewer can draw, including the previewers the components themselves load only on demand, so
30
+ // naming it statically would put all of that on every page that loads GeoBlacklight. Here it
31
+ // arrives with the map, and only on the pages that have one.
32
+ const [library] = await Promise.all([import("ogm-viewer/lib"), customElements.whenDefined("ogm-overview")])
33
+
34
+ // Taken off the page while we waited, by a Turbo visit to somewhere else
35
+ if (!this.element.isConnected) return
36
+
37
+ // Build previewable objects from every result's geometry
38
+ this.element.previewers = this.locations(library, this.results())
39
+
40
+ // Bind event handling for highlighting search result rows
41
+ if (this.rows().length) this.watchResults()
42
+ }
43
+
44
+ // Build a resource and previewer (container for geometry) for each search result.
45
+ // NOTE: results with no geometry still have to take up a number, because the
46
+ // numbering has to match the search result list.
47
+ locations({ LocationPreviewer, LocationResource }, results) {
48
+ const places = results.map(({ place }) => Number(place)).filter((place) => place)
49
+ const locations = Array.from({ length: places.length ? Math.max(...places) : 0 })
50
+
51
+ results.forEach(({ id, place, geometry }) => {
52
+ if (!place || !geometry) return
53
+
54
+ locations[place - 1] = new LocationPreviewer(new LocationResource(id, geometry))
55
+ })
56
+
57
+ return locations
58
+ }
59
+
60
+ // Parse data coming in from GBL in the template. This gets reset on each
61
+ // load of search results and doesn't use turbo.
62
+ results() {
63
+ const json = document.querySelector(RESULTS)?.dataset.overviewMapResults
64
+ return json ? JSON.parse(json) : []
65
+ }
66
+
67
+ // Execute the search when you drag a box on the map. The event that gets
68
+ // emitted by the map contains the bounding box as its `detail`.
69
+ search({ detail: bounds }) {
70
+ const url = new URL(this.catalogUrlValue, window.location)
71
+
72
+ // Go back to page 1, and replace the existing query, if there is one
73
+ const params = new URL(window.location).searchParams
74
+ params.delete("page")
75
+ params.set("bbox", bounds.join(" "))
76
+ url.search = params.toString()
77
+
78
+ this.visit(url)
79
+ }
80
+
81
+ // When you hover over a number on the map, also highlight that row in the
82
+ // search results (apply .highlighted class)
83
+ highlight({ detail }) {
84
+ this.rows().forEach((row) => {
85
+ const marked = detail && (this.layerId(row) === detail.id || this.place(row) === detail.place)
86
+ row.classList.toggle("highlighted", Boolean(marked))
87
+ })
88
+ }
89
+
90
+ // Reverse of highlight(): when you hover over a row in the search results,
91
+ // focus that number on the map
92
+ handlePointer = (event) => {
93
+ const row = event.target instanceof Element ? event.target.closest(ROW) : null
94
+ this.element.highlighted = row ? this.layerId(row) : undefined
95
+ }
96
+
97
+ watchResults() {
98
+ document.addEventListener("mouseover", this.handlePointer)
99
+ document.addEventListener("focusin", this.handlePointer)
100
+ }
101
+
102
+ unwatchResults() {
103
+ document.removeEventListener("mouseover", this.handlePointer)
104
+ document.removeEventListener("focusin", this.handlePointer)
105
+ }
106
+
107
+ // Rows are consulted only for the two-way pointer/focus highlight. Geometry, ids and numbering used
108
+ // to build the map all come from results() instead.
109
+ rows() {
110
+ return Array.from(document.querySelectorAll(ROW))
111
+ }
112
+
113
+ // The number shown beside a result, which is the number the map draws for it
114
+ place(row) {
115
+ return Number(row.dataset.documentCounter) || undefined
116
+ }
117
+
118
+ layerId(row) {
119
+ return row.dataset.mapId
120
+ }
121
+
122
+ // Turbo when it's there, and in place when the reader is already on the page they're searching: the
123
+ // page renders again around them, so their scroll position - and the map they are working in - stays
124
+ // where it was. A search from anywhere else is a navigation like any other, and lands at the top.
125
+ visit(url) {
126
+ if (typeof Turbo === "undefined") {
127
+ window.location.href = url
128
+ return
129
+ }
130
+
131
+ if (url.pathname === window.location.pathname) {
132
+ document.addEventListener(
133
+ "turbo:before-render",
134
+ () => {
135
+ if (Turbo.navigator.currentVisit) Turbo.navigator.currentVisit.scrolled = true
136
+ },
137
+ { once: true },
138
+ )
139
+ }
140
+
141
+ Turbo.visit(url)
142
+ }
143
+ }
@@ -1,15 +1,14 @@
1
1
  // Initializers
2
- import initializePopovers from "geoblacklight/initializers/popovers"
3
- import initializeTooltips from "geoblacklight/initializers/tooltips"
4
2
  import initializeTruncation from "geoblacklight/initializers/truncation"
3
+ import initializeFieldTruncation from "geoblacklight/initializers/field_truncation"
5
4
  import initializeMetadataDownload from "geoblacklight/initializers/metadata_download"
5
+ import initializeViewerTheme from "geoblacklight/initializers/viewer_theme"
6
+ import initializeViewerRequests from "geoblacklight/initializers/viewer_requests"
6
7
 
7
8
  // Stimulus controllers
8
9
  import ClipboardController from "geoblacklight/controllers/clipboard_controller"
9
- import LeafletViewerController from "geoblacklight/controllers/leaflet_viewer_controller"
10
10
  import OembedViewerController from "geoblacklight/controllers/oembed_viewer_controller"
11
- import OpenlayersViewerController from "geoblacklight/controllers/openlayers_viewer_controller"
12
- import SearchResultsController from "geoblacklight/controllers/search_results_controller"
11
+ import OverviewMapController from "geoblacklight/controllers/overview_map_controller"
13
12
 
14
13
  // Inspired by Blacklight's javascript/blacklight/core.js
15
14
  const Geoblacklight = (function () {
@@ -41,17 +40,16 @@ Geoblacklight.listeners().forEach((listener) =>
41
40
  )
42
41
 
43
42
  // Register our initializers
44
- Geoblacklight.onLoad(initializePopovers)
45
- Geoblacklight.onLoad(initializeTooltips)
46
43
  Geoblacklight.onLoad(initializeTruncation)
44
+ Geoblacklight.onLoad(initializeFieldTruncation)
47
45
  Geoblacklight.onLoad(initializeMetadataDownload)
46
+ Geoblacklight.onLoad(initializeViewerTheme)
47
+ Geoblacklight.onLoad(initializeViewerRequests)
48
48
 
49
49
  // Register our Stimulus controllers
50
50
  if (typeof Stimulus !== "undefined") {
51
- Stimulus.register("openlayers-viewer", OpenlayersViewerController)
52
51
  Stimulus.register("oembed-viewer", OembedViewerController)
53
- Stimulus.register("leaflet-viewer", LeafletViewerController)
54
- Stimulus.register("search-results", SearchResultsController)
52
+ Stimulus.register("overview-map", OverviewMapController)
55
53
  Stimulus.register("clipboard", ClipboardController)
56
54
  } else {
57
55
  console.error(
@@ -1,25 +1,22 @@
1
1
  // Stimulus controllers
2
- import OpenlayersViewerController from "geoblacklight/controllers/openlayers_viewer_controller"
3
2
  import OembedViewerController from "geoblacklight/controllers/oembed_viewer_controller"
4
- import LeafletViewerController from "geoblacklight/controllers/leaflet_viewer_controller"
5
- import SearchResultsController from "geoblacklight/controllers/search_results_controller"
3
+ import OverviewMapController from "geoblacklight/controllers/overview_map_controller"
6
4
  import ClipboardController from "geoblacklight/controllers/clipboard_controller"
7
5
 
6
+ // OpenGeoMetadata web components (each import registers the custom element it is named for)
7
+ import "ogm-viewer"
8
+ import "ogm-overview"
9
+ import "ogm-locator"
10
+
8
11
  // GBL core
9
12
  import Core from "geoblacklight/core"
10
-
11
- // Other modules
12
- import openLayersBasemaps from "geoblacklight/openlayers/basemaps"
13
- import leafletBasemaps from "geoblacklight/leaflet/basemaps"
13
+ import { onViewerRequest } from "geoblacklight/initializers/viewer_requests"
14
14
 
15
15
  export default {
16
- OpenlayersViewerController,
17
16
  OembedViewerController,
18
- LeafletViewerController,
19
- SearchResultsController,
17
+ OverviewMapController,
20
18
  ClipboardController,
21
- openLayersBasemaps,
22
- leafletBasemaps,
23
19
  Core,
24
20
  onLoad: Core.onLoad,
21
+ onViewerRequest,
25
22
  }
@@ -0,0 +1,62 @@
1
+ // Limit how many values a metadata field shows on the show page
2
+
3
+ // Class that hides a field's values past its limit and adds a button to reveal them.
4
+ // The hidden values are only hidden visually, so assistive technology is given the whole
5
+ // list up front. That leaves the button useful to sighted users alone, so it is marked
6
+ // aria-disabled and its label says why.
7
+ class FieldTruncator {
8
+ constructor(element) {
9
+ this.slot = element.querySelector(":scope > dd.read-more-slot")
10
+ if (!this.slot) return
11
+
12
+ // this field was already truncated on an earlier turbo:load/turbo:frame-load
13
+ if (this.slot.firstElementChild) return
14
+
15
+ // if the field is already short enough, don't truncate it
16
+ const values = element.querySelectorAll(":scope > dd:not(.read-more-slot)")
17
+ const limit = parseInt(element.dataset.limit, 10) || 5
18
+ if (values.length <= limit) return
19
+
20
+ // the value at the limit is left in place but clipped, so that the list visibly runs
21
+ // on past the values it is showing; everything after it is hidden outright
22
+ this.preview = values[limit]
23
+ this.hidden = Array.from(values).slice(limit + 1)
24
+ this.readMoreText = element.dataset.readMoreText
25
+ this.closeText = element.dataset.closeText
26
+
27
+ // add the button
28
+ this.button = document.createElement("button")
29
+ this.button.classList.add("btn", "btn-link", "p-0", "border-0", "read-more")
30
+ this.button.setAttribute("aria-disabled", "true")
31
+ this.button.setAttribute("aria-label", element.dataset.buttonLabel)
32
+ this.button.addEventListener("click", this.toggle.bind(this))
33
+
34
+ // start collapsed
35
+ this.collapse()
36
+ this.slot.append(this.button)
37
+ }
38
+
39
+ toggle() {
40
+ if (this.expanded) this.collapse()
41
+ else this.expand()
42
+ }
43
+
44
+ collapse() {
45
+ this.preview.classList.add("truncate-preview")
46
+ this.hidden.forEach((value) => value.classList.add("visually-hidden"))
47
+ this.button.textContent = this.readMoreText
48
+ this.expanded = false
49
+ }
50
+
51
+ expand() {
52
+ this.preview.classList.remove("truncate-preview")
53
+ this.hidden.forEach((value) => value.classList.remove("visually-hidden"))
54
+ this.button.textContent = this.closeText
55
+ this.expanded = true
56
+ }
57
+ }
58
+
59
+ // Initialize truncation for every metadata field that limits its values
60
+ export default function initializeFieldTruncation() {
61
+ document.querySelectorAll(".truncate-field").forEach((element) => new FieldTruncator(element))
62
+ }
@@ -28,10 +28,8 @@ export class GeoBlacklightMetadataDownloadButton {
28
28
  export default function initializeMetadataDownload() {
29
29
  const modal = document.getElementById("blacklight-modal")
30
30
 
31
- modal.addEventListener("focus", (e) => {
32
- if (!Array.from(e.target.classList).includes("show")) {
33
- return
34
- }
31
+ // Use Blacklight's dialog-based modal dispatches these custom events
32
+ modal.addEventListener("show.blacklight.blacklight-modal", (e) => {
35
33
  e.target.querySelectorAll(".metadata-body").forEach((el) => {
36
34
  el.closest(".modal-content").classList.add("metadata-modal")
37
35
  })
@@ -41,10 +39,7 @@ export default function initializeMetadataDownload() {
41
39
  })
42
40
  })
43
41
 
44
- modal.addEventListener("blur", (e) => {
45
- if (Array.from(e.target.classList).includes("show")) {
46
- return
47
- }
42
+ modal.addEventListener("hide.blacklight.blacklight-modal", (e) => {
48
43
  e.target.querySelectorAll(".metadata-body").forEach((el) => {
49
44
  el.closest(".modal-content").classList.remove("metadata-modal")
50
45
  })
@@ -4,8 +4,11 @@
4
4
  // Uses bootstrap's built-in collapse functionality; CSS is used to do the truncation
5
5
  class Truncator {
6
6
  constructor(element, { lines, id }) {
7
+ // this element was already processed on an earlier turbo:load/turbo:frame-load
8
+ if (element.classList.contains("collapse")) return
9
+
7
10
  // if the element is already small enough, don't truncate it
8
- const minHeight = lines * parseFloat(window.getComputedStyle(element).fontSize)
11
+ const minHeight = lines * parseFloat(window.getComputedStyle(element).lineHeight)
9
12
  if (element.getBoundingClientRect().height <= minHeight) return
10
13
 
11
14
  // set a unique ID for the element if it doesn't have one
@@ -13,10 +16,11 @@ class Truncator {
13
16
  this.element.id ||= id
14
17
  this.readMoreText = element.dataset.readMoreText
15
18
  this.closeText = element.dataset.closeText
19
+ this.element.style.setProperty("--max-lines", lines)
16
20
 
17
21
  // add the button
18
22
  this.button = document.createElement("button")
19
- this.button.classList.add("btn", "btn-link", "p-0", "border-0")
23
+ this.button.classList.add("btn", "btn-link", "p-0", "border-0", "read-more")
20
24
  this.button.dataset.bsToggle = "collapse"
21
25
  this.button.dataset.bsTarget = `#${this.element.id}`
22
26
  this.button.setAttribute("aria-expanded", "false")
@@ -40,7 +44,7 @@ class Truncator {
40
44
  export default function initializeTruncation() {
41
45
  document.querySelectorAll(".truncate-abstract").forEach((element, i) => {
42
46
  new Truncator(element, {
43
- lines: 12,
47
+ lines: parseInt(element.dataset.maxLines, 10) || 8,
44
48
  id: `truncate-${i}`,
45
49
  })
46
50
  })
@@ -0,0 +1,61 @@
1
+ // Initializer for helping the viewer make authenticated requests for restricted data.
2
+
3
+ // List of registered request transform functions
4
+ const registered = []
5
+
6
+ /**
7
+ * Register a transform, from a built GBL app's JavaScript entrypoint:
8
+ *
9
+ * Geoblacklight.onViewerRequest((url, resourceType) => {
10
+ * if (!url.startsWith("https://geo.example.edu/restricted/")) return
11
+ * return { headers: { Authorization: `Bearer ${token}` } }
12
+ * })
13
+ *
14
+ * Answer with `{ headers }`, `{ credentials: "include" }`, or `{ url }` to send the request
15
+ * somewhere else - a proxy on this application's own origin, say, which can authenticate with the
16
+ * service using credentials the browser never sees. Return nothing to leave a request alone.
17
+ *
18
+ * Always check the URL first; the viewer uses the transform for tile and basemap requests too,
19
+ * and you probably don't want to redirect or send cookies to those.
20
+ *
21
+ * Answer synchronously: this runs inline as the reader pans and zooms, so anything it needs - a
22
+ * token, say - has to already be in hand rather than fetched here.
23
+ *
24
+ * @param {(url: string, resourceType: "metadata" | "tile") => object | undefined} transform
25
+ */
26
+ export function onViewerRequest(transform) {
27
+ registered.push(transform)
28
+ }
29
+
30
+ // URLs where we know we need to send credentials. This gets set via GBL's
31
+ // configuration as a data attribute on the viewer.
32
+ function restrictedOrigins(viewer) {
33
+ if (!viewer.dataset.restrictedOrigins) return []
34
+ return JSON.parse(viewer.dataset.restrictedOrigins)
35
+ }
36
+
37
+ // Pick the transform to use for a given request. The first registered transform
38
+ // that returns a value will be used.
39
+ //
40
+ // If there weren't any registered transforms but we know the request is to a
41
+ // restricted origin, use a default transform that sends credentials (cookies).
42
+ function transformFor(origins) {
43
+ return (url, resourceType) => {
44
+ for (const transform of registered) {
45
+ const transformed = transform(url, resourceType)
46
+ if (transformed) return transformed
47
+ }
48
+
49
+ if (origins.some((origin) => url.startsWith(origin))) return { credentials: "include" }
50
+ }
51
+ }
52
+
53
+ // On load, find all the viewers in the page and set up request transforms for them.
54
+ export default function initializeViewerRequests() {
55
+ document.querySelectorAll("ogm-viewer").forEach((viewer) => {
56
+ const origins = restrictedOrigins(viewer)
57
+ if (!registered.length && !origins.length) return
58
+
59
+ viewer.requestTransform = transformFor(origins)
60
+ })
61
+ }
@@ -0,0 +1,28 @@
1
+ // Keep any viewers on the page in the same color mode as the rest of the page.
2
+ // Bootstrap's mode lives in data-bs-theme on <html>, set by Blacklight's theme switcher; the viewer
3
+ // takes a "theme" attribute. Blacklight resolves "auto" against prefers-color-scheme before writing
4
+ // the attribute, so the value we read is always either "light" or "dark".
5
+
6
+ // Apply the current color mode to every viewer and overview on the page
7
+ function syncViewerTheme() {
8
+ const theme = document.documentElement.getAttribute("data-bs-theme")
9
+
10
+ // No attribute means the host app has dark mode support turned off
11
+ if (!theme) return
12
+
13
+ // Every kind of map the viewer offers: the previewer, the map of where a set of records is, and
14
+ // the map of where one of them is. All three take the same attribute.
15
+ document
16
+ .querySelectorAll("ogm-viewer, ogm-overview, ogm-locator")
17
+ .forEach((viewer) => viewer.setAttribute("theme", theme))
18
+ }
19
+
20
+ // Follow the switcher after load. <html> outlives Turbo navigation, so one observer covers every
21
+ // page: re-observing the same node with the same options replaces the registration instead of
22
+ // stacking up another one, which is what makes this safe to call on each turbo:load.
23
+ const observer = new MutationObserver(syncViewerTheme)
24
+
25
+ export default function initializeViewerTheme() {
26
+ syncViewerTheme()
27
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-bs-theme"] })
28
+ }