blacklight-spotlight 1.4.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +5 -1
  3. data/app/assets/javascripts/spotlight/blocks/resources_block.js +3 -2
  4. data/app/assets/javascripts/spotlight/crop.es6 +2 -0
  5. data/app/assets/javascripts/spotlight/multi_image_selector.js +5 -4
  6. data/app/assets/javascripts/spotlight/reindex_monitor.js +16 -3
  7. data/app/controllers/concerns/spotlight/controller.rb +6 -1
  8. data/app/controllers/spotlight/browse_controller.rb +27 -0
  9. data/app/controllers/spotlight/catalog_controller.rb +2 -2
  10. data/app/helpers/spotlight/browse_helper.rb +0 -29
  11. data/app/helpers/spotlight/title_helper.rb +2 -2
  12. data/app/models/concerns/spotlight/custom_translation_extension.rb +23 -0
  13. data/app/models/concerns/spotlight/solr_document/atomic_updates.rb +4 -2
  14. data/app/models/spotlight/blacklight_configuration.rb +2 -2
  15. data/app/models/spotlight/exhibit.rb +1 -1
  16. data/app/views/layouts/spotlight/spotlight.html.erb +1 -0
  17. data/app/views/spotlight/dashboards/_analytics.html.erb +4 -4
  18. data/app/views/spotlight/search_configurations/_search_fields.html.erb +1 -1
  19. data/db/migrate/20180306142612_create_translations.rb +18 -0
  20. data/lib/generators/spotlight/install_generator.rb +4 -0
  21. data/lib/generators/spotlight/templates/config/initializers/translation.rb +17 -0
  22. data/lib/spotlight/engine.rb +1 -0
  23. data/lib/spotlight/version.rb +1 -1
  24. data/spec/controllers/spotlight/browse_controller_spec.rb +32 -0
  25. data/spec/controllers/spotlight/confirmations_controller_spec.rb +1 -1
  26. data/spec/controllers/spotlight/filters_controller_spec.rb +0 -1
  27. data/spec/controllers/spotlight/roles_controller_spec.rb +17 -0
  28. data/spec/controllers/spotlight/tags_controller_spec.rb +6 -1
  29. data/spec/examples.txt +1280 -1167
  30. data/spec/factories/translation.rb +6 -0
  31. data/spec/features/add_iiif_manifest_spec.rb +2 -0
  32. data/spec/features/browse_category_spec.rb +28 -0
  33. data/spec/features/edit_search_fields_spec.rb +1 -2
  34. data/spec/features/exhibit_masthead_spec.rb +2 -2
  35. data/spec/features/exhibit_themes_spec.rb +1 -1
  36. data/spec/features/exhibits/add_tags_spec.rb +1 -1
  37. data/spec/features/item_admin_spec.rb +1 -1
  38. data/spec/features/javascript/blocks/solr_documents_block_spec.rb +41 -28
  39. data/spec/features/javascript/multi_image_select_spec.rb +1 -1
  40. data/spec/features/javascript/reindex_monitor_spec.rb +1 -1
  41. data/spec/features/javascript/search_config_admin_spec.rb +16 -29
  42. data/spec/features/site_masthead_spec.rb +1 -1
  43. data/spec/features/slideshow_spec.rb +1 -2
  44. data/spec/features/translation_scope_spec.rb +24 -0
  45. data/spec/models/spotlight/ability_spec.rb +7 -10
  46. data/spec/models/spotlight/contact_email_spec.rb +1 -1
  47. data/spec/models/spotlight/contact_form_spec.rb +1 -1
  48. data/spec/models/spotlight/resources/iiif_harvester_spec.rb +3 -0
  49. data/spec/models/spotlight/solr_document/atomic_updates_spec.rb +2 -2
  50. data/spec/spec_helper.rb +12 -4
  51. data/spec/support/disable_friendly_id_deprecation_warnings.rb +8 -0
  52. data/spec/support/features/capybara_default_max_wait_metadata_helper.rb +16 -0
  53. data/spec/support/features/test_features_helpers.rb +12 -2
  54. data/spec/views/spotlight/browse/show.html.erb_spec.rb +0 -7
  55. data/spec/views/spotlight/roles/index.html.erb_spec.rb +2 -0
  56. data/spec/views/spotlight/tags/index.html.erb_spec.rb +1 -0
  57. data/vendor/assets/javascripts/Leaflet.Editable.js +22 -11
  58. data/vendor/assets/javascripts/leaflet-iiif.js +91 -23
  59. metadata +246 -227
  60. data/spec/features/curator_items.rb +0 -10
  61. data/spec/features/tags_admin_spec.rb +0 -27
  62. data/spec/features/user_admin_spec.rb +0 -29
@@ -0,0 +1,24 @@
1
+ describe 'Translations scope setting', type: :feature do
2
+ let(:exhibit) { FactoryBot.create(:exhibit) }
3
+ let(:other_exhibit) { FactoryBot.create(:exhibit) }
4
+ let(:exhibit_curator) { FactoryBot.create(:exhibit_curator, exhibit: exhibit) }
5
+
6
+ describe 'exhibit route set' do
7
+ before do
8
+ login_as exhibit_curator
9
+ FactoryBot.create(:translation, exhibit: exhibit)
10
+ FactoryBot.create(:translation, exhibit: other_exhibit)
11
+ end
12
+ it 'default scope of Translation should be limited to current exhibit' do
13
+ visit spotlight.exhibit_path(exhibit)
14
+ expect(Translation.all.count).to eq 1
15
+ end
16
+ end
17
+
18
+ describe 'without the context of an exhibit' do
19
+ it 'renders page ok' do
20
+ visit root_path
21
+ expect(page).to have_css '.site-title', text: 'Blacklight'
22
+ end
23
+ end
24
+ end
@@ -4,10 +4,10 @@ describe Spotlight::Ability, type: :model do
4
4
  before do
5
5
  allow_any_instance_of(Spotlight::Search).to receive(:set_default_featured_image)
6
6
  end
7
- let(:exhibit) { FactoryBot.create(:exhibit) }
8
- let(:search) { FactoryBot.create(:published_search, exhibit: exhibit) }
9
- let(:unpublished_search) { FactoryBot.create(:search, exhibit: exhibit) }
10
- let(:page) { FactoryBot.create(:feature_page, exhibit: exhibit) }
7
+ let(:exhibit) { FactoryBot.build_stubbed(:exhibit) }
8
+ let(:search) { FactoryBot.build_stubbed(:published_search, exhibit: exhibit) }
9
+ let(:unpublished_search) { FactoryBot.build_stubbed(:search, exhibit: exhibit) }
10
+ let(:page) { FactoryBot.build_stubbed(:feature_page, exhibit: exhibit) }
11
11
  subject { Ability.new(user) }
12
12
 
13
13
  describe 'a user with no roles' do
@@ -30,6 +30,7 @@ describe Spotlight::Ability, type: :model do
30
30
  describe 'a user with admin role' do
31
31
  let(:user) { FactoryBot.create(:exhibit_admin, exhibit: exhibit) }
32
32
  let(:role) { FactoryBot.create(:role, resource: exhibit) }
33
+ let(:blacklight_config) { exhibit.blacklight_configuration }
33
34
 
34
35
  it { is_expected.to be_able_to(:update, exhibit) }
35
36
 
@@ -41,12 +42,12 @@ describe Spotlight::Ability, type: :model do
41
42
  it { is_expected.to be_able_to(:import, exhibit) }
42
43
  it { is_expected.to be_able_to(:process_import, exhibit) }
43
44
  it { is_expected.to be_able_to(:destroy, exhibit) }
44
-
45
- let(:blacklight_config) { exhibit.blacklight_configuration }
46
45
  end
47
46
 
48
47
  describe 'a user with curate role' do
49
48
  let(:user) { FactoryBot.create(:exhibit_curator, exhibit: exhibit) }
49
+ let(:contact) { FactoryBot.build_stubbed(:contact, exhibit: exhibit) }
50
+ let(:blacklight_config) { exhibit.blacklight_configuration }
50
51
 
51
52
  it { is_expected.not_to be_able_to(:update, exhibit) }
52
53
  it { is_expected.to be_able_to(:curate, exhibit) }
@@ -64,13 +65,9 @@ describe Spotlight::Ability, type: :model do
64
65
 
65
66
  it { is_expected.to be_able_to(:tag, exhibit) }
66
67
 
67
- let(:contact) { FactoryBot.create(:contact, exhibit: exhibit) }
68
-
69
68
  it { is_expected.to be_able_to(:edit, contact) }
70
69
  it { is_expected.to be_able_to(:new, contact) }
71
70
  it { is_expected.to be_able_to(:create, contact) }
72
71
  it { is_expected.to be_able_to(:destroy, contact) }
73
-
74
- let(:blacklight_config) { exhibit.blacklight_configuration }
75
72
  end
76
73
  end
@@ -1,5 +1,5 @@
1
1
  describe Spotlight::ContactEmail, type: :model do
2
- let(:exhibit) { FactoryBot.create(:exhibit) }
2
+ let(:exhibit) { FactoryBot.build_stubbed(:exhibit) }
3
3
  subject { described_class.new(exhibit: exhibit) }
4
4
 
5
5
  it { is_expected.not_to be_valid }
@@ -1,6 +1,6 @@
1
1
  describe Spotlight::ContactForm do
2
2
  subject { described_class.new(name: 'Root', email: 'user@example.com').tap { |c| c.current_exhibit = exhibit } }
3
- let(:exhibit) { FactoryBot.create(:exhibit) }
3
+ let(:exhibit) { FactoryBot.build_stubbed(:exhibit) }
4
4
  let(:honeypot_field_name) { Spotlight::Engine.config.spambot_honeypot_email_field }
5
5
 
6
6
  context 'with a site-wide contact email' do
@@ -7,6 +7,9 @@ describe Spotlight::Resources::IiifHarvester do
7
7
  describe 'Validation' do
8
8
  subject { harvester }
9
9
  context 'when given an invalid URL' do
10
+ before do
11
+ stub_request(:head, 'http://example.com').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
12
+ end
10
13
  let(:url) { 'http://example.com' }
11
14
 
12
15
  it 'errors when the URL is not a IIIF URL' do
@@ -28,7 +28,7 @@ describe Spotlight::SolrDocument::AtomicUpdates, type: :model do
28
28
  it 'sends an atomic update request' do
29
29
  expected = {
30
30
  params: { commitWithin: 500 },
31
- data: [{ id: 'doc_id', a: { set: 1 }, b: { set: 2 } }].to_json,
31
+ data: [{ id: 'doc_id', a: { set: 1 }, b: { set: 2 }, timestamp: { set: nil } }].to_json,
32
32
  headers: { 'Content-Type' => 'application/json' }
33
33
  }
34
34
  expect(blacklight_solr).to receive(:update).with(expected)
@@ -36,7 +36,7 @@ describe Spotlight::SolrDocument::AtomicUpdates, type: :model do
36
36
  end
37
37
 
38
38
  it 'cowardlies refuse to index a document if the only value is an id' do
39
- allow(subject).to receive_messages(to_solr: { id: 'doc_id' })
39
+ allow(subject).to receive_messages(to_solr: { id: 'doc_id' }, timestamp: { set: nil })
40
40
  expect(blacklight_solr).not_to receive(:update)
41
41
  subject.reindex
42
42
  end
@@ -7,7 +7,7 @@ EngineCart.load_application!
7
7
 
8
8
  Internal::Application.config.active_job.queue_adapter = :inline
9
9
 
10
- require 'rails-controller-testing' if Rails::VERSION::MAJOR >= 5
10
+ require 'rails-controller-testing'
11
11
  require 'rspec/collection_matchers'
12
12
  require 'rspec/its'
13
13
  require 'rspec/rails'
@@ -15,19 +15,24 @@ require 'rspec/active_model/mocks'
15
15
  require 'paper_trail/frameworks/rspec'
16
16
 
17
17
  require 'selenium-webdriver'
18
+ require 'webmock/rspec'
18
19
 
19
20
  Capybara.javascript_driver = :headless_chrome
20
21
 
22
+ # @note In January 2018, TravisCI disabled Chrome sandboxing in its Linux
23
+ # container build environments to mitigate Meltdown/Spectre
24
+ # vulnerabilities, at which point Spotlight needs to use the --no-sandbox
25
+ # flag. https://github.com/travis-ci/docs-travis-ci-com/blob/c1da4af0b7ee5de35fa4490fa8e0fc4b44881089/user/chrome.md
26
+ # h/t @mjgiarlo
21
27
  Capybara.register_driver :headless_chrome do |app|
22
28
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
23
- chromeOptions: { args: %w[headless disable-gpu] }
29
+ chromeOptions: { args: %w[headless disable-gpu no-sandbox window-size=1280,1696] }
24
30
  )
25
31
 
26
32
  Capybara::Selenium::Driver.new(app,
27
33
  browser: :chrome,
28
34
  desired_capabilities: capabilities)
29
35
  end
30
- Capybara.default_max_wait_time = 10
31
36
 
32
37
  if ENV['COVERAGE'] || ENV['CI']
33
38
  require 'simplecov'
@@ -56,7 +61,9 @@ RSpec.configure do |config|
56
61
  config.filter_rails_from_backtrace!
57
62
 
58
63
  config.use_transactional_fixtures = false
59
-
64
+ config.before :all do
65
+ WebMock.disable_net_connect!(allow_localhost: true)
66
+ end
60
67
  config.before :each do
61
68
  DatabaseCleaner.strategy = if Capybara.current_driver == :rack_test
62
69
  :transaction
@@ -96,6 +103,7 @@ RSpec.configure do |config|
96
103
  config.include ::Rails.application.routes.url_helpers
97
104
  config.include ::Rails.application.routes.mounted_helpers
98
105
  config.include Spotlight::TestFeaturesHelpers, type: :feature
106
+ config.include CapybaraDefaultMaxWaitMetadataHelper, type: :feature
99
107
 
100
108
  config.expect_with :rspec do |expectations|
101
109
  expectations.include_chain_clauses_in_custom_matcher_descriptions = true
@@ -0,0 +1,8 @@
1
+ default_deprecation_behaviours = ActiveSupport::Deprecation.behavior
2
+ ActiveSupport::Deprecation.behavior = lambda do |message, callstack|
3
+ raise 'Remove friendly_id deprecation silencing patch!' if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR > 1
4
+ unless callstack.find { |l| l.path =~ %r{gems/friendly_id} } &&
5
+ message =~ /The behavior of .* inside of after callbacks will be changing/
6
+ default_deprecation_behaviours.each { |b| b.call(message, callstack) }
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ module CapybaraDefaultMaxWaitMetadataHelper
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before do |example|
6
+ next unless example.metadata[:default_max_wait_time]
7
+ @previous_wait_time = Capybara.default_max_wait_time
8
+ Capybara.default_max_wait_time = example.metadata[:default_max_wait_time]
9
+ end
10
+
11
+ after do |example|
12
+ next unless example.metadata[:default_max_wait_time]
13
+ Capybara.default_max_wait_time = @previous_wait_time
14
+ end
15
+ end
16
+ end
@@ -13,6 +13,13 @@ module Spotlight
13
13
  find('.tt-suggestion', text: opts[:with], match: :first).click
14
14
  end
15
15
 
16
+ # just like #fill_in_typeahead_field, but wait for the
17
+ # form fields to show up on the page too
18
+ def fill_in_solr_document_block_typeahead_field(opts)
19
+ fill_in_typeahead_field(opts)
20
+ expect(page).to have_css('li[data-resource-id="' + opts[:with] + '"]')
21
+ end
22
+
16
23
  def add_widget(type)
17
24
  click_add_widget
18
25
 
@@ -31,10 +38,13 @@ module Spotlight
31
38
  end
32
39
 
33
40
  def save_page
34
- sleep 1
41
+ page.execute_script <<-EOF
42
+ SirTrevor.getInstance().onFormSubmit();
43
+ EOF
35
44
  click_button('Save changes')
36
45
  # verify that the page was created
37
- expect(page).to have_content('page was successfully updated')
46
+ expect(page).to_not have_selector('.alert-danger')
47
+ expect(page).to have_selector('.alert-info', text: 'page was successfully updated')
38
48
  end
39
49
 
40
50
  RSpec::Matchers.define :have_breadcrumbs do |*expected|
@@ -6,7 +6,6 @@ describe 'spotlight/browse/show', type: :view do
6
6
  before do
7
7
  allow(view).to receive_messages(resource_masthead?: false)
8
8
  allow(view).to receive_messages(blacklight_config: Blacklight::Configuration.new)
9
- view.blacklight_config.view.gallery = true
10
9
  allow(search).to receive_messages(documents: double(size: 15))
11
10
  allow(view).to receive_messages(render_document_index_with_view: '')
12
11
  stub_template('_results_pagination.html.erb' => '')
@@ -62,10 +61,4 @@ describe 'spotlight/browse/show', type: :view do
62
61
  render
63
62
  expect(response).to have_content 'Sort and Per Page actions'
64
63
  end
65
-
66
- it 'displays the search results' do
67
- expect(view).to receive(:render_document_index_with_view).with(:gallery, anything, anything).and_return 'Gallery View'
68
- render
69
- expect(response).to include 'Gallery View'
70
- end
71
64
  end
@@ -17,6 +17,8 @@ describe 'spotlight/roles/index', type: :view do
17
17
  assert_select "form[action='#{action}'][method='post']" do
18
18
  assert_select "tr[data-show-for='#{admin_role.id}']"
19
19
  assert_select "tr[data-edit-for='#{admin_role.id}']"
20
+ assert_select 'td', /jane@example.com/
21
+ assert_select 'td', 'Admin'
20
22
  assert_select "input[type='submit'][data-behavior='destroy-user'][data-target='#{admin_role.id}']"
21
23
  assert_select "input[type='hidden'][data-destroy-for='#{admin_role.id}']"
22
24
  assert_select "a[data-behavior='cancel-edit']"
@@ -15,6 +15,7 @@ describe 'spotlight/tags/index.html.erb', type: :view do
15
15
  render
16
16
  [tag1.tag.name, tag2.tag.name].each do |name|
17
17
  expect(rendered).to have_css('td', text: name)
18
+ expect(rendered).to have_link(name, href: 't')
18
19
  end
19
20
  end
20
21
  end
@@ -716,6 +716,7 @@
716
716
  this.editor.refresh();
717
717
  var icon = this._icon;
718
718
  var marker = this.editor.addVertexMarker(e.latlng, this.latlngs);
719
+ this.editor.onNewVertex(marker);
719
720
  /* Hack to workaround browser not firing touchend when element is no more on DOM */
720
721
  var parent = marker._icon.parentNode;
721
722
  parent.removeChild(marker._icon);
@@ -1018,7 +1019,7 @@
1018
1019
  initVertexMarkers: function (latlngs) {
1019
1020
  if (!this.enabled()) return;
1020
1021
  latlngs = latlngs || this.getLatLngs();
1021
- if (L.Polyline._flat(latlngs)) this.addVertexMarkers(latlngs);
1022
+ if (isFlat(latlngs)) this.addVertexMarkers(latlngs);
1022
1023
  else for (var i = 0; i < latlngs.length; i++) this.initVertexMarkers(latlngs[i]);
1023
1024
  },
1024
1025
 
@@ -1037,6 +1038,14 @@
1037
1038
  return new this.tools.options.vertexMarkerClass(latlng, latlngs, this);
1038
1039
  },
1039
1040
 
1041
+ onNewVertex: function (vertex) {
1042
+ // 🍂namespace Editable
1043
+ // 🍂section Vertex events
1044
+ // 🍂event editable:vertex:new: VertexEvent
1045
+ // Fired when a new vertex is created.
1046
+ this.fireAndForward('editable:vertex:new', {latlng: vertex.latlng, vertex: vertex});
1047
+ },
1048
+
1040
1049
  addVertexMarkers: function (latlngs) {
1041
1050
  for (var i = 0; i < latlngs.length; i++) {
1042
1051
  this.addVertexMarker(latlngs[i], latlngs);
@@ -1219,7 +1228,8 @@
1219
1228
  if (this._drawing === L.Editable.FORWARD) this._drawnLatLngs.push(latlng);
1220
1229
  else this._drawnLatLngs.unshift(latlng);
1221
1230
  this.feature._bounds.extend(latlng);
1222
- this.addVertexMarker(latlng, this._drawnLatLngs);
1231
+ var vertex = this.addVertexMarker(latlng, this._drawnLatLngs);
1232
+ this.onNewVertex(vertex);
1223
1233
  this.refresh();
1224
1234
  },
1225
1235
 
@@ -1431,7 +1441,7 @@
1431
1441
  },
1432
1442
 
1433
1443
  ensureMulti: function () {
1434
- if (this.feature._latlngs.length && L.Polyline._flat(this.feature._latlngs)) {
1444
+ if (this.feature._latlngs.length && isFlat(this.feature._latlngs)) {
1435
1445
  this.feature._latlngs = [this.feature._latlngs];
1436
1446
  }
1437
1447
  },
@@ -1447,7 +1457,7 @@
1447
1457
  },
1448
1458
 
1449
1459
  formatShape: function (shape) {
1450
- if (L.Polyline._flat(shape)) return shape;
1460
+ if (isFlat(shape)) return shape;
1451
1461
  else if (shape[0]) return this.formatShape(shape[0]);
1452
1462
  },
1453
1463
 
@@ -1512,13 +1522,13 @@
1512
1522
  },
1513
1523
 
1514
1524
  ensureMulti: function () {
1515
- if (this.feature._latlngs.length && L.Polyline._flat(this.feature._latlngs[0])) {
1525
+ if (this.feature._latlngs.length && isFlat(this.feature._latlngs[0])) {
1516
1526
  this.feature._latlngs = [this.feature._latlngs];
1517
1527
  }
1518
1528
  },
1519
1529
 
1520
1530
  ensureNotFlat: function () {
1521
- if (!this.feature._latlngs.length || L.Polyline._flat(this.feature._latlngs)) this.feature._latlngs = [this.feature._latlngs];
1531
+ if (!this.feature._latlngs.length || isFlat(this.feature._latlngs)) this.feature._latlngs = [this.feature._latlngs];
1522
1532
  },
1523
1533
 
1524
1534
  vertexCanBeDeleted: function (vertex) {
@@ -1537,7 +1547,7 @@
1537
1547
  // [[1, 2], [3, 4]] => must be nested
1538
1548
  // [] => must be nested
1539
1549
  // [[]] => is already nested
1540
- if (L.Polyline._flat(shape) && (!shape[0] || shape[0].length !== 0)) return [shape];
1550
+ if (isFlat(shape) && (!shape[0] || shape[0].length !== 0)) return [shape];
1541
1551
  else return shape;
1542
1552
  }
1543
1553
 
@@ -1706,7 +1716,7 @@
1706
1716
 
1707
1717
  // 🍂namespace Editable; 🍂class EditableMixin
1708
1718
  // `EditableMixin` is included to `L.Polyline`, `L.Polygon`, `L.Rectangle`, `L.Circle`
1709
- // and `L.Marker`. It adds some methods to them.
1719
+ // and `L.Marker`. It adds some methods to them.
1710
1720
  // *When editing is enabled, the editor is accessible on the instance with the
1711
1721
  // `editor` property.*
1712
1722
  var EditableMixin = {
@@ -1768,7 +1778,7 @@
1768
1778
  var shape = null;
1769
1779
  latlngs = latlngs || this._latlngs;
1770
1780
  if (!latlngs.length) return shape;
1771
- else if (L.Polyline._flat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs;
1781
+ else if (isFlat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs;
1772
1782
  else for (var i = 0; i < latlngs.length; i++) if (this.isInLatLngs(latlng, latlngs[i])) return latlngs[i];
1773
1783
  return shape;
1774
1784
  },
@@ -1807,8 +1817,8 @@
1807
1817
  var shape = null;
1808
1818
  latlngs = latlngs || this._latlngs;
1809
1819
  if (!latlngs.length) return shape;
1810
- else if (L.Polyline._flat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs;
1811
- else if (L.Polyline._flat(latlngs[0]) && this.isInLatLngs(latlng, latlngs[0])) shape = latlngs;
1820
+ else if (isFlat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs;
1821
+ else if (isFlat(latlngs[0]) && this.isInLatLngs(latlng, latlngs[0])) shape = latlngs;
1812
1822
  else for (var i = 0; i < latlngs.length; i++) if (this.isInLatLngs(latlng, latlngs[i][0])) return latlngs[i];
1813
1823
  return shape;
1814
1824
  },
@@ -1872,6 +1882,7 @@
1872
1882
  this.on('add', this._onEditableAdd);
1873
1883
  };
1874
1884
 
1885
+ var isFlat = L.LineUtil.isFlat || L.LineUtil._flat || L.Polyline._flat; // <=> 1.1 compat.
1875
1886
 
1876
1887
 
1877
1888
  if (L.Polyline) {
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Leaflet-IIIF 1.0.2
2
+ * Leaflet-IIIF 2.0.0
3
3
  * IIIF Viewer for Leaflet
4
4
  * by Jack Reed, @mejackreed
5
5
  */
@@ -10,7 +10,8 @@ L.TileLayer.Iiif = L.TileLayer.extend({
10
10
  tileSize: 256,
11
11
  updateWhenIdle: true,
12
12
  tileFormat: 'jpg',
13
- fitBounds: true
13
+ fitBounds: true,
14
+ setMaxBounds: false
14
15
  },
15
16
 
16
17
  initialize: function(url, options) {
@@ -25,6 +26,11 @@ L.TileLayer.Iiif = L.TileLayer.extend({
25
26
  this._explicitTileSize = true;
26
27
  }
27
28
 
29
+ // Check for an explicit quality
30
+ if (options.quality) {
31
+ this._explicitQuality = true;
32
+ }
33
+
28
34
  options = L.setOptions(this, options);
29
35
  this._infoDeferred = new $.Deferred();
30
36
  this._infoUrl = url;
@@ -42,7 +48,7 @@ L.TileLayer.Iiif = L.TileLayer.extend({
42
48
  miny = (y * tileBaseSize),
43
49
  maxx = Math.min(minx + tileBaseSize, _this.x),
44
50
  maxy = Math.min(miny + tileBaseSize, _this.y);
45
-
51
+
46
52
  var xDiff = (maxx - minx);
47
53
  var yDiff = (maxy - miny);
48
54
 
@@ -63,6 +69,25 @@ L.TileLayer.Iiif = L.TileLayer.extend({
63
69
  // Set maxZoom for map
64
70
  map._layersMaxZoom = _this.maxZoom;
65
71
 
72
+ // Set minZoom and minNativeZoom based on how the imageSizes match up
73
+ var smallestImage = _this._imageSizes[0];
74
+ var mapSize = _this._map.getSize();
75
+ var newMinZoom = 0;
76
+ // Loop back through 5 times to see if a better fit can be found.
77
+ for (var i = 1; i <= 5; i++) {
78
+ if (smallestImage.x > mapSize.x || smallestImage.y > mapSize.y) {
79
+ smallestImage = smallestImage.divideBy(2);
80
+ _this._imageSizes.unshift(smallestImage);
81
+ newMinZoom = -i;
82
+ } else {
83
+ break;
84
+ }
85
+ }
86
+ _this.options.minZoom = newMinZoom;
87
+ _this.options.minNativeZoom = newMinZoom;
88
+ _this._prev_map_layersMinZoom = _this._map._layersMinZoom;
89
+ _this._map._layersMinZoom = newMinZoom;
90
+
66
91
  // Call add TileLayer
67
92
  L.TileLayer.prototype.onAdd.call(_this, map);
68
93
 
@@ -70,6 +95,10 @@ L.TileLayer.Iiif = L.TileLayer.extend({
70
95
  _this._fitBounds();
71
96
  }
72
97
 
98
+ if(_this.options.setMaxBounds) {
99
+ _this._setMaxBounds();
100
+ }
101
+
73
102
  // Reset tile sizes to handle non 256x256 IIIF tiles
74
103
  _this.on('tileload', function(tile, url) {
75
104
 
@@ -85,18 +114,45 @@ L.TileLayer.Iiif = L.TileLayer.extend({
85
114
  });
86
115
  });
87
116
  },
117
+ onRemove: function(map) {
118
+ var _this = this;
119
+
120
+ map._layersMinZoom = _this._prev_map_layersMinZoom;
121
+
122
+ // Remove maxBounds set for this image
123
+ if(_this.options.setMaxBounds) {
124
+ map.setMaxBounds(null);
125
+ }
126
+
127
+ // Call remove TileLayer
128
+ L.TileLayer.prototype.onRemove.call(_this, map);
129
+
130
+ },
88
131
  _fitBounds: function() {
89
132
  var _this = this;
90
133
 
91
134
  // Find best zoom level and center map
92
135
  var initialZoom = _this._getInitialZoom(_this._map.getSize());
93
- var imageSize = _this._imageSizes[initialZoom];
136
+ var offset = _this._imageSizes.length - 1 - _this.options.maxNativeZoom;
137
+ var imageSize = _this._imageSizes[initialZoom + offset];
94
138
  var sw = _this._map.options.crs.pointToLatLng(L.point(0, imageSize.y), initialZoom);
95
139
  var ne = _this._map.options.crs.pointToLatLng(L.point(imageSize.x, 0), initialZoom);
96
140
  var bounds = L.latLngBounds(sw, ne);
97
141
 
98
142
  _this._map.fitBounds(bounds, true);
99
143
  },
144
+ _setMaxBounds: function() {
145
+ var _this = this;
146
+
147
+ // Find best zoom level, center map, and constrain viewer
148
+ var initialZoom = _this._getInitialZoom(_this._map.getSize());
149
+ var imageSize = _this._imageSizes[initialZoom];
150
+ var sw = _this._map.options.crs.pointToLatLng(L.point(0, imageSize.y), initialZoom);
151
+ var ne = _this._map.options.crs.pointToLatLng(L.point(imageSize.x, 0), initialZoom);
152
+ var bounds = L.latLngBounds(sw, ne);
153
+
154
+ _this._map.setMaxBounds(bounds, true);
155
+ },
100
156
  _getInfo: function() {
101
157
  var _this = this;
102
158
 
@@ -136,14 +192,15 @@ L.TileLayer.Iiif = L.TileLayer.extend({
136
192
  }
137
193
  }
138
194
 
139
- ceilLog2 = function(x) {
195
+ function ceilLog2(x) {
140
196
  return Math.ceil(Math.log(x) / Math.LN2);
141
197
  };
142
198
 
143
199
  // Calculates maximum native zoom for the layer
144
200
  _this.maxNativeZoom = Math.max(ceilLog2(_this.x / _this.options.tileSize),
145
201
  ceilLog2(_this.y / _this.options.tileSize));
146
-
202
+ _this.options.maxNativeZoom = _this.maxNativeZoom;
203
+
147
204
  // Enable zooming further than native if maxZoom option supplied
148
205
  if (_this._customMaxZoom && _this.options.maxZoom > _this.maxNativeZoom) {
149
206
  _this.maxZoom = _this.options.maxZoom;
@@ -151,7 +208,7 @@ L.TileLayer.Iiif = L.TileLayer.extend({
151
208
  else {
152
209
  _this.maxZoom = _this.maxNativeZoom;
153
210
  }
154
-
211
+
155
212
  for (var i = 0; i <= _this.maxZoom; i++) {
156
213
  scale = Math.pow(2, _this.maxNativeZoom - i);
157
214
  width_ = Math.ceil(_this.x / scale);
@@ -172,18 +229,24 @@ L.TileLayer.Iiif = L.TileLayer.extend({
172
229
 
173
230
  _setQuality: function() {
174
231
  var _this = this;
232
+ var profileToCheck = _this.profile;
175
233
 
176
- // Quality already specified by consumer
177
- if (_this.options.quality) {
234
+ if (_this._explicitQuality) {
178
235
  return;
179
236
  }
180
237
 
238
+ // If profile is an object
239
+ if (typeof(profileToCheck) === 'object') {
240
+ profileToCheck = profileToCheck['@id'];
241
+ }
242
+
181
243
  // Set the quality based on the IIIF compliance level
182
244
  switch (true) {
183
- case /^http:\/\/library.stanford.edu\/iiif\/image-api\/1.1\/compliance.html.*$/.test(_this.profile):
245
+ case /^http:\/\/library.stanford.edu\/iiif\/image-api\/1.1\/compliance.html.*$/.test(profileToCheck):
184
246
  _this.options.quality = 'native';
185
247
  break;
186
- case /^http:\/\/iiif.io\/api\/image\/2.*$/.test(_this.profile):
248
+ // Assume later profiles and set to default
249
+ default:
187
250
  _this.options.quality = 'default';
188
251
  break;
189
252
  }
@@ -196,11 +259,15 @@ L.TileLayer.Iiif = L.TileLayer.extend({
196
259
  return this._infoToBaseUrl() + '{region}/{size}/{rotation}/{quality}.{format}';
197
260
  },
198
261
  _isValidTile: function(coords) {
199
- var _this = this,
200
- zoom = _this._getZoomForUrl(),
201
- sizes = _this._tierSizes[zoom],
202
- x = coords.x,
203
- y = (coords.y);
262
+ var tileBounds = this._tileCoordsToBounds(coords);
263
+ var _this = this;
264
+ var zoom = _this._getZoomForUrl();
265
+ var sizes = _this._tierSizes[zoom];
266
+ var x = coords.x;
267
+ var y = coords.y;
268
+ if (zoom < 0 && x >= 0 && y >= 0) {
269
+ return true;
270
+ }
204
271
 
205
272
  if (!sizes) return false;
206
273
  if (x < 0 || sizes[0] <= x || y < 0 || sizes[1] <= y) {
@@ -210,14 +277,15 @@ L.TileLayer.Iiif = L.TileLayer.extend({
210
277
  }
211
278
  },
212
279
  _getInitialZoom: function (mapSize) {
213
- var _this = this,
214
- tolerance = 0.8,
215
- imageSize;
216
-
217
- for (var i = _this.maxNativeZoom; i >= 0; i--) {
218
- imageSize = this._imageSizes[i];
280
+ var _this = this;
281
+ var tolerance = 0.8;
282
+ var imageSize;
283
+ // Calculate an offset between the zoom levels and the array accessors
284
+ var offset = _this._imageSizes.length - 1 - _this.options.maxNativeZoom;
285
+ for (var i = _this._imageSizes.length - 1; i >= 0; i--) {
286
+ imageSize = _this._imageSizes[i];
219
287
  if (imageSize.x * tolerance < mapSize.x && imageSize.y * tolerance < mapSize.y) {
220
- return i;
288
+ return i - offset;
221
289
  }
222
290
  }
223
291
  // return a default zoom