handy_location_inputs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 278e0cc94cffc6ccfa881af0d79356dbd4b898e84de67dfbab4d02a68c5d849b
4
+ data.tar.gz: ec2af4700802febe3495a11ce38f005cdb00a0ad3658b267ead66449293868b3
5
+ SHA512:
6
+ metadata.gz: 32252ebe82b061b901ddd8570e1926b5c6789f376bd159434eee3c1d3354216ef9e8f35781550cb2f251a900eaeedac9cfd54d2b1e1fd80be06919cedb7e8617
7
+ data.tar.gz: 52cc17252396bb125ab63dcfe46bb70ebec013c58e7b9dd37f81c75dc937f7968beccd476156303ec2284f9d0dfa1c323dda1304bb6da738eeeb2a5bd4ba21e2
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019 handofthecode
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # HandyLocationInputs
2
+ HandyLocationInputs is a rails gem/engine for adding powerful location inputs to your forms. It allows you to guarentee your Country, State, and City names will be uniform without the UX penalties of Select Boxes.
3
+
4
+ ## Requirements
5
+ Rails with turbolinks and bulma css. (Future versions will remove the dependency on Bulma.)
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'handy_location_inputs'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install handy_location_inputs
22
+ ```
23
+
24
+ ## Setup
25
+
26
+ First, add this line to your app/assets/javascripts/application.js manifest file.
27
+
28
+ ```javascript
29
+ // = require handy_location_inputs/input_controllers
30
+ ```
31
+
32
+ To use handy_location_inputs in a view, ```include HandyLocationInputs::Locatable``` to that view's controller.
33
+
34
+ If the associated model is new, or has no location data yet, call ```country_list_to_client``` to the controller action. This will pass the country list to the client.
35
+
36
+ If the model already has state and city data that you would like to pass to the client as well, call ```location_lists_to_client(your_model)```.
37
+
38
+ ### In The View
39
+
40
+ To trigger functionality in a form, include the class ```handy-location-inputs``` somewhere on the page.
41
+
42
+ Each input needs an id of ```country-input```, ```state-input```, or ```city-input``` respectively.
43
+
44
+ The dropdown for each input should look like this.
45
+
46
+ ```html
47
+ <div class="dropdown country-dropdown">
48
+ <div class="dropdown-menu" role="menu">
49
+ <div class="country-dropdown-content">
50
+ </div>
51
+ </div>
52
+ </div>
53
+ ```
54
+
55
+ ## License
56
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'HandyLocationInputs'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts/handy_location_inputs .js
2
+ //= link_directory ../stylesheets/handy_location_inputs .css
@@ -0,0 +1,180 @@
1
+ document.addEventListener("turbolinks:load", function(){
2
+ if(!document.querySelector('.handy-location-inputs')) return;
3
+
4
+ var countryInput = document.getElementById("country-input");
5
+ var countryDropdown = document.querySelector('.country-dropdown');
6
+ var countryDropdownContent = document.querySelector('.country-dropdown-content')
7
+
8
+ var stateInput = document.getElementById("state-input");
9
+ var stateDropdown = document.querySelector('.state-dropdown');
10
+ var stateDropdownContent = document.querySelector('.state-dropdown-content')
11
+
12
+ var cityInput = document.getElementById("city-input");
13
+ var cityDropdown = document.querySelector('.city-dropdown');
14
+ var cityDropdownContent = document.querySelector('.city-dropdown-content')
15
+
16
+ class InputController {
17
+ constructor(type, input, dropdown, dropdownContent) {
18
+ this.type = type;
19
+ this.input = input;
20
+ this.dropdown = dropdown;
21
+ this.dropdownContent = dropdownContent;
22
+ this.setPlaces();
23
+ this.addAllEventListeners();
24
+ }
25
+
26
+ addAllEventListeners() {
27
+ this.addInputListener();
28
+ this.addClickListener();
29
+ this.addBlurInputListener();
30
+ this.addFocusListener();
31
+ this.addClearSubordinateInputsListener();
32
+ }
33
+
34
+ addClearSubordinateInputsListener() {
35
+ this.input.addEventListener('change', () => this.clearSubordinateInputs());
36
+ }
37
+
38
+ addClickListener() {
39
+ this.dropdown.addEventListener('click', e => {
40
+ this.input.value = e.target.innerText;
41
+ var nextInput = this.subordinateInputs()[0];
42
+ if(nextInput) {
43
+ this.ajaxCallForSubordinateInput();
44
+ nextInput.focus();
45
+ }
46
+ });
47
+ }
48
+
49
+ subordinateInputs() {
50
+ var result;
51
+ if(this.type === 'countries') {
52
+ result = [stateInput, cityInput];
53
+ } else if(this.type === 'states') {
54
+ result = [cityInput];
55
+ } else {
56
+ result = [];
57
+ }
58
+ return result
59
+ }
60
+
61
+ subordinateInputType() {
62
+ if(this.type === 'countries') {
63
+ return 'states';
64
+ } else if(this.type === 'states') {
65
+ return 'cities';
66
+ } else return null;
67
+ }
68
+
69
+ superiorController() {
70
+ if(this.type === 'states') return countryInputController;
71
+ if(this.type === 'cities') return stateInputController;
72
+ }
73
+
74
+ clearSubordinateInputs() {
75
+ this.subordinateInputs().forEach(input => input.value = '');
76
+ }
77
+
78
+ inputIsValid() {
79
+ return this.input.value &&
80
+ gon[this.type] &&
81
+ this.inputWithProperCase();
82
+ }
83
+
84
+ inputWithProperCase() {
85
+ return this.places.find(place => place.toLowerCase() === this.input.value.toLowerCase())
86
+ }
87
+
88
+ fixCase() {
89
+ if(this.inputWithProperCase()) {
90
+ this.input.value = this.inputWithProperCase();
91
+ }
92
+ }
93
+
94
+ ajaxCallForSubordinateInput() {
95
+ if(!this.inputIsValid()) return;
96
+ if(this.type === 'cities') return;
97
+ var url;
98
+ var data;
99
+ if(this.type === 'countries') {
100
+ url = "/HandyLocationInputs/states.json";
101
+ data = `country=${this.inputWithProperCase()}`;
102
+ } else if(this.type === 'states') {
103
+ url = "/HandyLocationInputs/cities.json";
104
+ data = `country=${countryInputController.inputWithProperCase()}&state=${this.inputWithProperCase()}`
105
+ }
106
+ Rails.ajax({
107
+ dataType: "json",
108
+ url: url,
109
+ type: "POST",
110
+ data: data,
111
+ beforeSend: () => true,
112
+ success: (data) => {
113
+ gon[this.subordinateInputType()] = data[this.subordinateInputType()];
114
+ },
115
+ });
116
+ }
117
+
118
+ addBlurInputListener() {
119
+ this.input.addEventListener('blur', () => {
120
+ this.fixCase();
121
+ this.ajaxCallForSubordinateInput();
122
+ setTimeout(() => {
123
+ this.dropdown.classList.remove('is-active');
124
+ }, 300)
125
+ });
126
+ }
127
+
128
+ addFocusListener() {
129
+ this.input.addEventListener('focus', () => {
130
+ if(!this.superiorController()) {
131
+ this.input.dispatchEvent(new Event('keyup'));
132
+ } else if(this.superiorController().inputIsValid()) {
133
+ setTimeout(() => {
134
+ this.input.dispatchEvent(new Event('keyup'))
135
+ }, 1000)
136
+ } else {
137
+ this.superiorController().input.focus()
138
+ }
139
+ });
140
+ }
141
+
142
+ setPlaces() {
143
+ this.places;
144
+ if(gon) {
145
+ if(this.type === 'countries') {
146
+ this.places = gon.countries;
147
+ } else if(this.type === 'states') {
148
+ this.places = gon.states;
149
+ } else if(this.type === 'cities') {
150
+ this.places = gon.cities;
151
+ }
152
+ }
153
+ if(!this.places || !this.places.length) return;
154
+ }
155
+
156
+ addInputListener() {
157
+ this.input.addEventListener("keyup", () => {
158
+ var inputValLower = this.input.value.toLowerCase();
159
+ this.dropdown.classList.add('is-active');
160
+ this.setPlaces();
161
+ this.matching = this.places.filter(c => c.toLowerCase().slice(0, inputValLower.length) == (inputValLower)).slice(0, 5);
162
+ if(this.dropdownContent.firstChild) {
163
+ while (this.dropdownContent.firstChild) {
164
+ this.dropdownContent.removeChild(this.dropdownContent.firstChild);
165
+ }
166
+ }
167
+ this.matching.forEach((c) => {
168
+ let item = document.createElement("div");
169
+ item.className = 'dropdown-item';
170
+ item.innerText = c;
171
+ this.dropdownContent.appendChild(item)
172
+ });
173
+ });
174
+ }
175
+ }
176
+
177
+ countryInputController = new InputController('countries', countryInput, countryDropdown, countryDropdownContent)
178
+ stateInputController = new InputController('states', stateInput, stateDropdown, stateDropdownContent)
179
+ cityInputController = new InputController('cities', cityInput, cityDropdown, cityDropdownContent)
180
+ });
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,40 @@
1
+ module HandyLocationInputs
2
+ module Locatable
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ def country_list_to_client
8
+ countries = CS.countries.map { |c| c[1] }.sort
9
+ countries.delete("country_name")
10
+ countries.delete("United States")
11
+ countries.unshift("United States")
12
+ gon.countries = countries
13
+ end
14
+
15
+ def location_lists_to_client(model)
16
+ country_list_to_client
17
+ gon.states = CS.states(country_key model).values if !model[:country].blank?
18
+ gon.cities = CS.cities(state_key(model), country_key(model)) if !model[:state].blank?
19
+ end
20
+
21
+ def valid_location?(model)
22
+ return false if state_key(model).blank?
23
+ return false if !CS.cities(state_key(model), country_key(model)).blank? &&
24
+ CS.cities(state_key(model), country_key(model)).map(&:downcase).exclude?(model[:city].downcase)
25
+ true
26
+ end
27
+
28
+ def country_key(model)
29
+ @country_key ||= CS.countries.key(model[:country])
30
+ end
31
+
32
+ def state_key(model)
33
+ @state_key ||= return_key(CS.states(country_key(model)), model[:state])
34
+ end
35
+
36
+ def return_key(obj, match)
37
+ obj.each {|key, val| break key if key.to_s == match || val == match}
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,30 @@
1
+ module HandyLocationInputs
2
+ class LocationsController < ApplicationController
3
+ protect_from_forgery with: :exception
4
+
5
+ include HandyLocationInputs::Locatable
6
+
7
+ skip_around_action :set_time_zone
8
+ skip_before_action :require_user, :require_village
9
+
10
+ def states
11
+ respond_to do |format|
12
+ format.json {render json: { states: CS.states(country_key).values }}
13
+ end
14
+ end
15
+
16
+ def cities
17
+ respond_to do |format|
18
+ format.json {render json: { cities: CS.cities(state_key, country_key)}}
19
+ end
20
+ end
21
+
22
+ def country_key
23
+ CS.countries.key(params[:country])
24
+ end
25
+
26
+ def state_key
27
+ return_key(CS.states(country_key), params[:state])
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ module HandyLocationInputs
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module HandyLocationInputs
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module HandyLocationInputs
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module HandyLocationInputs
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Handy location inputs</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "handy_location_inputs/application", media: "all" %>
9
+ <%= javascript_include_tag "handy_location_inputs/application" %>
10
+ </head>
11
+ <body>
12
+
13
+ <%= yield %>
14
+
15
+ </body>
16
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ HandyLocationInputs::Engine.routes.draw do
2
+ post 'states', to: 'locations#states'
3
+ post 'cities', to: 'locations#cities'
4
+ end
@@ -0,0 +1,5 @@
1
+ require "handy_location_inputs/engine"
2
+
3
+ module HandyLocationInputs
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,5 @@
1
+ module HandyLocationInputs
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace HandyLocationInputs
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module HandyLocationInputs
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :handy_location_inputs do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: handy_location_inputs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - handofthecode
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-07-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.2.3
27
+ description: HandyLocationInputs is an easy way to get uniform location input from
28
+ a user without the UX compromise of select boxes.
29
+ email:
30
+ - deartovi@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - app/assets/config/handy_location_inputs_manifest.js
39
+ - app/assets/javascripts/handy_location_inputs/input_controllers.js
40
+ - app/assets/stylesheets/handy_location_inputs/application.css
41
+ - app/controllers/concerns/handy_location_inputs/locatable.rb
42
+ - app/controllers/handy_location_inputs/locations_controller.rb
43
+ - app/helpers/handy_location_inputs/application_helper.rb
44
+ - app/jobs/handy_location_inputs/application_job.rb
45
+ - app/mailers/handy_location_inputs/application_mailer.rb
46
+ - app/models/handy_location_inputs/application_record.rb
47
+ - app/views/layouts/handy_location_inputs/application.html.erb
48
+ - config/routes.rb
49
+ - lib/handy_location_inputs.rb
50
+ - lib/handy_location_inputs/engine.rb
51
+ - lib/handy_location_inputs/version.rb
52
+ - lib/tasks/handy_location_inputs_tasks.rake
53
+ homepage: https://github.com/handofthecode/handy_location_inputs
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ allowed_push_host: https://rubygems.org
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.0.1
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: HandyLocationInputs auto suggests locations as the user types. It prevents
77
+ them from continuing until a valid location is selected. Subordinate input possibilities
78
+ are requested from the server via ajax as soon as approved locations are entered.
79
+ Cases are fixed.
80
+ test_files: []