country_state_select 3.2.0 → 4.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +56 -0
- data/README.md +196 -111
- data/Rakefile +26 -0
- data/app/controllers/country_state_select/cscs_controller.rb +31 -8
- data/app/javascript/country_state_select/controller.js +66 -0
- data/app/javascript/country_state_select/country_state_select.js +245 -0
- data/config/importmap.rb +4 -0
- data/config/routes.rb +14 -4
- data/country_state_select.gemspec +14 -4
- data/docs/cities.md +35 -0
- data/docs/images/cascading-select-demo.png +0 -0
- data/docs/json-api.md +41 -0
- data/docs/migration-v4.md +32 -0
- data/gemfiles/rails_7.0.gemfile +10 -0
- data/gemfiles/rails_7.1.gemfile +7 -0
- data/gemfiles/rails_7.2.gemfile +7 -0
- data/gemfiles/rails_8.0.gemfile +7 -0
- data/lib/country_state_select/configuration.rb +89 -0
- data/lib/country_state_select/country_metadata.rb +29 -0
- data/lib/country_state_select/data/dial_codes.yml +250 -0
- data/lib/country_state_select/data_sources/base.rb +27 -0
- data/lib/country_state_select/data_sources/city_state.rb +28 -0
- data/lib/country_state_select/engine.rb +31 -0
- data/lib/country_state_select/form_builder.rb +48 -0
- data/lib/country_state_select/localization.rb +41 -0
- data/lib/country_state_select/version.rb +1 -1
- data/lib/country_state_select.rb +83 -23
- data/lib/generators/country_state_select/install/install_generator.rb +22 -0
- data/lib/generators/country_state_select/install/templates/README +34 -0
- data/lib/generators/country_state_select/install/templates/initializer.rb +29 -0
- data/package.json +40 -0
- data/spec/country_state_select_spec.rb +2 -2
- data/spec/lib/country_state_select/configuration_spec.rb +63 -0
- data/spec/lib/country_state_select/country_metadata_spec.rb +42 -0
- data/spec/lib/country_state_select/filtering_spec.rb +68 -0
- data/spec/lib/country_state_select/localization_spec.rb +30 -0
- data/spec/requests/business_form_spec.rb +38 -0
- data/spec/requests/lookups_spec.rb +72 -0
- data/spec/spec_helper.rb +12 -4
- data/vendor/assets/javascript/country_state_select.js.erb +35 -21
- data/vendor/assets/javascript/country_state_select_vanilla.js +252 -0
- metadata +147 -17
- data/.circleci/config.yml +0 -15
- data/.travis.yml +0 -14
- data/lib/country_state_select/engine3.rb +0 -10
- data/lib/country_state_select/railtie.rb +0 -10
data/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "country_state_select",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Dependency-free Country/State/City cascading select fields, with an optional Stimulus controller. JS companion to the country_state_select Ruby gem.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "app/javascript/country_state_select/country_state_select.js",
|
|
7
|
+
"module": "app/javascript/country_state_select/country_state_select.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./app/javascript/country_state_select/country_state_select.js",
|
|
10
|
+
"./controller": "./app/javascript/country_state_select/controller.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"app/javascript"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/arvindvyas/Country-State-Select.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/arvindvyas/Country-State-Select",
|
|
20
|
+
"bugs": "https://github.com/arvindvyas/Country-State-Select/issues",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"rails",
|
|
23
|
+
"country",
|
|
24
|
+
"state",
|
|
25
|
+
"city",
|
|
26
|
+
"select",
|
|
27
|
+
"stimulus",
|
|
28
|
+
"hotwire"
|
|
29
|
+
],
|
|
30
|
+
"author": "Arvind Vyas",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@hotwired/stimulus": ">=3.0.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"@hotwired/stimulus": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -164,9 +164,9 @@ describe CountryStateSelect do
|
|
|
164
164
|
expect(city_without_country).to eq(city_with_country)
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
-
it 'returns an empty array if there are no
|
|
167
|
+
it 'returns an empty array if there are no cities for that state/country' do
|
|
168
168
|
method_call = CountryStateSelect.collect_cities('', '')
|
|
169
|
-
expect(method_call).to eq(
|
|
169
|
+
expect(method_call).to eq([])
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe CountryStateSelect::Configuration do
|
|
6
|
+
describe '#data_source_instance' do
|
|
7
|
+
it 'defaults to the CityState adapter' do
|
|
8
|
+
expect(CountryStateSelect.configuration.data_source_instance).to be_a(CountryStateSelect::DataSources::CityState)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'memoizes the instance across calls' do
|
|
12
|
+
config = CountryStateSelect::Configuration.new
|
|
13
|
+
|
|
14
|
+
expect(config.data_source_instance).to equal(config.data_source_instance)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'rebuilds the instance when the data source is reassigned' do
|
|
18
|
+
config = CountryStateSelect::Configuration.new
|
|
19
|
+
first = config.data_source_instance
|
|
20
|
+
|
|
21
|
+
config.data_source = :city_state
|
|
22
|
+
expect(config.data_source_instance).not_to equal(first)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'raises for an unknown symbol data source' do
|
|
26
|
+
config = CountryStateSelect::Configuration.new
|
|
27
|
+
config.data_source = :not_a_real_source
|
|
28
|
+
|
|
29
|
+
expect { config.data_source_instance }.to raise_error(ArgumentError)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'accepts a custom adapter object directly' do
|
|
33
|
+
custom = Class.new(CountryStateSelect::DataSources::Base) do
|
|
34
|
+
def countries = { US: 'United States' }
|
|
35
|
+
end.new
|
|
36
|
+
config = CountryStateSelect::Configuration.new
|
|
37
|
+
config.data_source = custom
|
|
38
|
+
|
|
39
|
+
expect(config.data_source_instance).to equal(custom)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe 'defaults' do
|
|
44
|
+
it 'has sane defaults for a fresh configuration' do
|
|
45
|
+
config = CountryStateSelect::Configuration.new
|
|
46
|
+
|
|
47
|
+
expect(config.priority_countries).to eq([])
|
|
48
|
+
expect(config.flags).to be(false)
|
|
49
|
+
expect(config.dial_codes).to be(false)
|
|
50
|
+
expect(config.localize_names).to be(false)
|
|
51
|
+
expect(config.cache_duration).to eq(86_400)
|
|
52
|
+
expect(config.draw_routes).to be(true)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '.configure' do
|
|
57
|
+
it 'yields the shared configuration and persists changes' do
|
|
58
|
+
CountryStateSelect.configure { |c| c.priority_countries = %w[US] }
|
|
59
|
+
|
|
60
|
+
expect(CountryStateSelect.configuration.priority_countries).to eq(%w[US])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe CountryStateSelect::CountryMetadata do
|
|
6
|
+
describe '.flag' do
|
|
7
|
+
it 'converts a two-letter ISO code into its regional indicator flag emoji' do
|
|
8
|
+
expect(described_class.flag(:US)).to eq("\u{1F1FA}\u{1F1F8}")
|
|
9
|
+
expect(described_class.flag('IN')).to eq("\u{1F1EE}\u{1F1F3}")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'returns nil for a non two-letter code' do
|
|
13
|
+
expect(described_class.flag('USA')).to be_nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '.decorate' do
|
|
18
|
+
after { CountryStateSelect.reset_configuration! }
|
|
19
|
+
|
|
20
|
+
it 'returns the plain name when flags and dial_codes are both off' do
|
|
21
|
+
expect(described_class.decorate(:US, 'United States')).to eq('United States')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'prepends the flag when configured' do
|
|
25
|
+
CountryStateSelect.configure { |c| c.flags = true }
|
|
26
|
+
|
|
27
|
+
expect(described_class.decorate(:US, 'United States')).to start_with("\u{1F1FA}\u{1F1F8}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'appends the dial code when configured' do
|
|
31
|
+
CountryStateSelect.configure { |c| c.dial_codes = true }
|
|
32
|
+
|
|
33
|
+
expect(described_class.decorate(:US, 'United States')).to eq('United States (+1)')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'combines flag and dial code when both are configured' do
|
|
37
|
+
CountryStateSelect.configure { |c| c.flags = true; c.dial_codes = true }
|
|
38
|
+
|
|
39
|
+
expect(described_class.decorate(:US, 'United States')).to eq("\u{1F1FA}\u{1F1F8} United States (+1)")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe 'CountryStateSelect only/except/priority filtering' do
|
|
6
|
+
after { CountryStateSelect.reset_configuration! }
|
|
7
|
+
|
|
8
|
+
describe '.countries_collection' do
|
|
9
|
+
it 'restricts to only the given codes when passed explicitly' do
|
|
10
|
+
result = CountryStateSelect.countries_collection(only: %w[US IN])
|
|
11
|
+
|
|
12
|
+
expect(result.map(&:last)).to contain_exactly(:US, :IN)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'excludes the given codes when passed explicitly' do
|
|
16
|
+
result = CountryStateSelect.countries_collection(except: %w[US])
|
|
17
|
+
|
|
18
|
+
expect(result.map(&:last)).not_to include(:US)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'moves priority codes to the front, in the given order' do
|
|
22
|
+
result = CountryStateSelect.countries_collection(priority: %w[IN US])
|
|
23
|
+
|
|
24
|
+
expect(result.first(2).map(&:last)).to eq(%i[IN US])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'reads only/except/priority from global configuration when not passed explicitly' do
|
|
28
|
+
CountryStateSelect.configure do |c|
|
|
29
|
+
c.only_countries = %w[US IN GB]
|
|
30
|
+
c.priority_countries = %w[GB]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result = CountryStateSelect.countries_collection
|
|
34
|
+
|
|
35
|
+
expect(result.first.last).to eq(:GB)
|
|
36
|
+
expect(result.map(&:last)).to contain_exactly(:US, :IN, :GB)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'lets a per-call override win over global configuration' do
|
|
40
|
+
CountryStateSelect.configure { |c| c.only_countries = %w[US] }
|
|
41
|
+
|
|
42
|
+
result = CountryStateSelect.countries_collection(only: %w[IN])
|
|
43
|
+
|
|
44
|
+
expect(result.map(&:last)).to contain_exactly(:IN)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '.collect_states / .raw_states' do
|
|
49
|
+
it 'returns [name, code] pairs from collect_states and [code, name] from raw_states' do
|
|
50
|
+
labeled = CountryStateSelect.collect_states('US')
|
|
51
|
+
raw = CountryStateSelect.raw_states('US')
|
|
52
|
+
|
|
53
|
+
expect(labeled.first).to eq(raw.first.reverse)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'applies only/except/priority the same way as countries' do
|
|
57
|
+
result = CountryStateSelect.collect_states('US', only: %w[CA NY])
|
|
58
|
+
|
|
59
|
+
expect(result.map(&:last)).to contain_exactly(:CA, :NY)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '.countries_except' do
|
|
64
|
+
it 'is equivalent to countries_collection(except: ...)' do
|
|
65
|
+
expect(CountryStateSelect.countries_except('US')).to eq(CountryStateSelect.countries_collection(except: ['US']))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe CountryStateSelect::Localization do
|
|
6
|
+
after do
|
|
7
|
+
CountryStateSelect.reset_configuration!
|
|
8
|
+
I18n.backend.reload!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe '.country_name' do
|
|
12
|
+
it 'returns the English name unchanged when localize_names is off' do
|
|
13
|
+
expect(described_class.country_name(:FR, 'France')).to eq('France')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context 'when localize_names is on' do
|
|
17
|
+
before { CountryStateSelect.configure { |c| c.localize_names = true } }
|
|
18
|
+
|
|
19
|
+
it 'falls back to the English name when no translation is available' do
|
|
20
|
+
expect(described_class.country_name(:FR, 'France')).to eq('France')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'uses a country_state_select.countries.<CODE> I18n key when present' do
|
|
24
|
+
I18n.backend.store_translations(:en, country_state_select: { countries: { FR: 'La France' } })
|
|
25
|
+
|
|
26
|
+
expect(described_class.country_name(:FR, 'France')).to eq('La France')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe 'FormBuilder integration (via the dummy Business form)', type: :request do
|
|
6
|
+
describe 'GET /businesses/new' do
|
|
7
|
+
it 'renders a country select and blank state/city text fields' do
|
|
8
|
+
get '/businesses/new'
|
|
9
|
+
|
|
10
|
+
expect(response.body).to match(/<select[^>]*id="business_country"/)
|
|
11
|
+
expect(response.body).to include('United States')
|
|
12
|
+
expect(response.body).to match(/<input[^>]*type="text"[^>]*id="business_state"/)
|
|
13
|
+
expect(response.body).to match(/<input[^>]*type="text"[^>]*id="business_city"/)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'stamps the Stimulus target data attributes' do
|
|
17
|
+
get '/businesses/new'
|
|
18
|
+
|
|
19
|
+
expect(response.body).to include('data-country-state-select-target="country"')
|
|
20
|
+
expect(response.body).to include('data-country-state-select-target="state"')
|
|
21
|
+
expect(response.body).to include('data-country-state-select-target="city"')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe 'GET /businesses/:id/edit' do
|
|
26
|
+
let(:business) { Business.create!(name: 'Golden Gate Cafe', country: 'US', state: 'CA', city: 'San Francisco') }
|
|
27
|
+
|
|
28
|
+
it 'renders state and city as populated selects with the saved value selected' do
|
|
29
|
+
get "/businesses/#{business.id}/edit"
|
|
30
|
+
|
|
31
|
+
expect(response.body).to match(/<select[^>]*id="business_state"/)
|
|
32
|
+
expect(response.body).to match(/<option selected="selected" value="CA">California<\/option>/)
|
|
33
|
+
|
|
34
|
+
expect(response.body).to match(/<select[^>]*id="business_city"/)
|
|
35
|
+
expect(response.body).to match(/<option selected="selected" value="San Francisco">San Francisco<\/option>/)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe 'CountryStateSelect lookup endpoints', type: :request do
|
|
6
|
+
describe 'GET /find_states' do
|
|
7
|
+
it 'returns raw [code, name] pairs for the given country' do
|
|
8
|
+
get '/find_states', params: { country_id: 'US' }, as: :json
|
|
9
|
+
|
|
10
|
+
expect(response).to have_http_status(:ok)
|
|
11
|
+
body = JSON.parse(response.body)
|
|
12
|
+
expect(body).to include(%w[CA California])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'returns an empty array for a country with no states' do
|
|
16
|
+
get '/find_states', params: { country_id: 'MF' }, as: :json
|
|
17
|
+
|
|
18
|
+
expect(JSON.parse(response.body)).to eq([])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'sets Cache-Control based on the configured cache_duration' do
|
|
22
|
+
CountryStateSelect.configure { |c| c.cache_duration = 3600 }
|
|
23
|
+
|
|
24
|
+
get '/find_states', params: { country_id: 'US' }, as: :json
|
|
25
|
+
|
|
26
|
+
expect(response.headers['Cache-Control']).to include('max-age=3600')
|
|
27
|
+
expect(response.headers['ETag']).to be_present
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'does not mark the response public when cache_duration is disabled' do
|
|
31
|
+
CountryStateSelect.configure { |c| c.cache_duration = nil }
|
|
32
|
+
|
|
33
|
+
get '/find_states', params: { country_id: 'US' }, as: :json
|
|
34
|
+
|
|
35
|
+
expect(response.headers['Cache-Control']).not_to include('public')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns 304 on a conditional request with a matching ETag' do
|
|
39
|
+
get '/find_states', params: { country_id: 'US' }, as: :json
|
|
40
|
+
etag = response.headers['ETag']
|
|
41
|
+
|
|
42
|
+
get '/find_states', params: { country_id: 'US' }, headers: { 'If-None-Match' => etag }, as: :json
|
|
43
|
+
|
|
44
|
+
expect(response).to have_http_status(:not_modified)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'applies only/except/priority filtering configured on the data source' do
|
|
48
|
+
CountryStateSelect.configure { |c| c.priority_countries = [] }
|
|
49
|
+
|
|
50
|
+
get '/find_states', params: { country_id: 'US' }, as: :json
|
|
51
|
+
codes = JSON.parse(response.body).map(&:first)
|
|
52
|
+
|
|
53
|
+
expect(codes).to include('CA', 'NY')
|
|
54
|
+
expect(codes).not_to include('COUNTRY_ISO_CODE')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe 'GET /find_cities' do
|
|
59
|
+
it 'returns city names for the given state and country' do
|
|
60
|
+
get '/find_cities', params: { state_id: 'mp', country_id: 'in' }, as: :json
|
|
61
|
+
|
|
62
|
+
expect(response).to have_http_status(:ok)
|
|
63
|
+
expect(JSON.parse(response.body)).to be_an(Array)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'returns an empty array when state_id is blank' do
|
|
67
|
+
get '/find_cities', params: { state_id: '', country_id: 'us' }, as: :json
|
|
68
|
+
|
|
69
|
+
expect(JSON.parse(response.body)).to eq([])
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require '
|
|
5
|
-
require '
|
|
3
|
+
ENV['RAILS_ENV'] ||= 'test'
|
|
4
|
+
require File.expand_path('dummy/config/environment', __dir__)
|
|
5
|
+
require 'rspec/rails'
|
|
6
|
+
|
|
7
|
+
ActiveRecord::Migration.maintain_test_schema!
|
|
6
8
|
|
|
7
9
|
RSpec.configure do |config|
|
|
8
|
-
|
|
10
|
+
config.use_transactional_fixtures = true
|
|
11
|
+
config.infer_spec_type_from_file_location!
|
|
12
|
+
config.filter_rails_from_backtrace!
|
|
13
|
+
|
|
14
|
+
config.after do
|
|
15
|
+
CountryStateSelect.reset_configuration!
|
|
16
|
+
end
|
|
9
17
|
end
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// DEPRECATED (jQuery + Chosen): kept for backward compatibility with apps
|
|
2
|
+
// already requiring this file. New apps should use the dependency-free
|
|
3
|
+
// build instead — see vendor/assets/javascript/country_state_select_vanilla.js
|
|
4
|
+
// (Sprockets) or the npm package / Stimulus controller (importmap, esbuild,
|
|
5
|
+
// webpack). This file will be removed in a future major version.
|
|
1
6
|
function CountryStateSelect(options) {
|
|
2
7
|
|
|
3
8
|
var state_id = options['state_id'];
|
|
@@ -10,6 +15,8 @@ function CountryStateSelect(options) {
|
|
|
10
15
|
var city_name = $('#' + city_id).attr('name');
|
|
11
16
|
var city_class = $('#' + city_id).attr('class');
|
|
12
17
|
|
|
18
|
+
var hasCityField = typeof city_name !== "undefined";
|
|
19
|
+
|
|
13
20
|
return statesDropdown();
|
|
14
21
|
|
|
15
22
|
// ====== ***** METHODS ***** ===================================================================== //
|
|
@@ -23,11 +30,18 @@ function CountryStateSelect(options) {
|
|
|
23
30
|
return findStates($(this).val());
|
|
24
31
|
});
|
|
25
32
|
|
|
33
|
+
// Bind the city lookup ONCE via event delegation. The state <select> is
|
|
34
|
+
// swapped out by replaceWith() on every country change, so a directly-bound
|
|
35
|
+
// handler would be lost (the old code re-bound it on every build, stacking
|
|
36
|
+
// duplicate listeners and firing multiple AJAX calls per change).
|
|
37
|
+
if (hasCityField) {
|
|
38
|
+
citiesDropdown();
|
|
39
|
+
}
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
function citiesDropdown() {
|
|
29
|
-
|
|
30
|
-
return findCities($("#" + state_id).val()
|
|
43
|
+
$(document).on('change', '#' + state_id, function () {
|
|
44
|
+
return findCities($("#" + state_id).val(), $("#" + country_id).val());
|
|
31
45
|
});
|
|
32
46
|
}
|
|
33
47
|
|
|
@@ -74,6 +88,12 @@ function CountryStateSelect(options) {
|
|
|
74
88
|
return options.hasOwnProperty("chosen_ui") && options['chosen_ui'];
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
// Escape values before interpolating them into markup so names containing
|
|
92
|
+
// quotes, angle brackets or ampersands can't break the option or inject HTML.
|
|
93
|
+
function escapeHtml(value){
|
|
94
|
+
return $('<div>').text(value == null ? '' : value).html();
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
function findStates(id) {
|
|
78
98
|
|
|
79
99
|
//Remove all Chosen from existing fields
|
|
@@ -83,7 +103,7 @@ function CountryStateSelect(options) {
|
|
|
83
103
|
//Perform AJAX request to get the data; on success, build the dropdown
|
|
84
104
|
$.ajax({
|
|
85
105
|
url: "/find_states",
|
|
86
|
-
type: '
|
|
106
|
+
type: 'get',
|
|
87
107
|
dataType: 'json',
|
|
88
108
|
cache: false,
|
|
89
109
|
data: {country_id: id},
|
|
@@ -99,8 +119,7 @@ function CountryStateSelect(options) {
|
|
|
99
119
|
//Perform AJAX request to get the data; on success, build the dropdown
|
|
100
120
|
$.ajax({
|
|
101
121
|
url: "/find_cities",
|
|
102
|
-
type: '
|
|
103
|
-
|
|
122
|
+
type: 'get',
|
|
104
123
|
dataType: 'json',
|
|
105
124
|
cache: false,
|
|
106
125
|
data: {
|
|
@@ -113,33 +132,27 @@ function CountryStateSelect(options) {
|
|
|
113
132
|
|
|
114
133
|
//Build the HTML for our dropdown menus
|
|
115
134
|
function buildStatesDropdown(data) {
|
|
135
|
+
var html;
|
|
116
136
|
|
|
117
137
|
if (data.length === 0) {
|
|
118
|
-
html = '<input id="' + state_id + '" name="' + state_name + '" class="' + state_class + '" type="text"
|
|
138
|
+
html = '<input id="' + state_id + '" name="' + state_name + '" class="' + state_class + '" type="text" value="" >';
|
|
119
139
|
} else {
|
|
120
140
|
html = '<select id="' + state_id + '" name="' + state_name + '" class="' + state_class + '" >';
|
|
121
|
-
html += '<option>'+ find_select_option_text('state') +'</option>'
|
|
141
|
+
html += '<option>' + escapeHtml(find_select_option_text('state')) + '</option>';
|
|
122
142
|
|
|
123
|
-
for (i = 0; i < data.length; i++) {
|
|
124
|
-
html += '<option value='+data[i][0]+'>' + data[i][1] + '</option>';
|
|
143
|
+
for (var i = 0; i < data.length; i++) {
|
|
144
|
+
html += '<option value="' + escapeHtml(data[i][0]) + '">' + escapeHtml(data[i][1]) + '</option>';
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
html += '</select>';
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
|
|
131
150
|
$('#' + state_id).replaceWith(html);
|
|
132
151
|
|
|
133
152
|
//This has to happen AFTER we've replaced the dropdown or text
|
|
134
153
|
if (data.length > 0) {
|
|
135
154
|
addChosenToState();
|
|
136
155
|
}
|
|
137
|
-
|
|
138
|
-
// [142] FIXME # Is there any other way to call city method , it is adding change method in every state change
|
|
139
|
-
if(typeof city_name !== "undefined" ){
|
|
140
|
-
citiesDropdown();
|
|
141
|
-
};
|
|
142
|
-
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
function find_select_option_text(type){
|
|
@@ -152,14 +165,15 @@ function CountryStateSelect(options) {
|
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
function buildCitiesDropdown(data) {
|
|
168
|
+
var html;
|
|
155
169
|
|
|
156
170
|
if (data.length === 0) {
|
|
157
|
-
html = '<input id="' + city_id + '" name="' + city_name + '" class="' + city_class + '" type="text"
|
|
171
|
+
html = '<input id="' + city_id + '" name="' + city_name + '" class="' + city_class + '" type="text" value="" >';
|
|
158
172
|
} else {
|
|
159
173
|
html = '<select id="' + city_id + '" name="' + city_name + '" class="' + city_class + '" >';
|
|
160
|
-
html += '<option>'+ find_select_option_text('city') +'</option>'
|
|
161
|
-
for (i = 0; i < data.length; i++) {
|
|
162
|
-
html += '<option>' + data[i] + '</option>';
|
|
174
|
+
html += '<option>' + escapeHtml(find_select_option_text('city')) + '</option>';
|
|
175
|
+
for (var i = 0; i < data.length; i++) {
|
|
176
|
+
html += '<option>' + escapeHtml(data[i]) + '</option>';
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
html += '</select>';
|
|
@@ -174,4 +188,4 @@ function CountryStateSelect(options) {
|
|
|
174
188
|
|
|
175
189
|
}
|
|
176
190
|
|
|
177
|
-
}
|
|
191
|
+
}
|