├── CNAME ├── .gitignore ├── src ├── templates │ ├── facet.html │ ├── form.html │ ├── input.html │ ├── facetControls.html │ ├── medicationAssistedTreatment.html │ ├── extraResources.html │ ├── twelveStepPrograms.html │ ├── medicalDetoxPrograms.html │ ├── assessment.html │ └── welcome.html ├── data │ ├── config.js │ ├── typeahead.js │ ├── search.js │ ├── analytics.js │ ├── geojson.js │ └── facet.js ├── ui │ ├── back-to-top.js │ ├── scroll.js │ ├── filtering.js │ ├── tabs.js │ ├── project.js │ ├── loading.js │ ├── select_county.js │ ├── info.js │ ├── search.js │ ├── feedback_widget.js │ ├── search_results.js │ ├── list.js │ ├── facet.js │ └── map.js ├── timed_with_object.js ├── script.js └── infotemplates.js ├── .editorconfig ├── img ├── logo.png ├── spinner.gif ├── GetHelpLextype.png ├── comment-alt-solid.svg ├── map-marker-alt-solid.svg ├── search-solid.svg ├── info-circle-solid.svg ├── print-solid.svg ├── home-solid.svg └── bullhorn-solid.svg ├── get-help-lex-geocode.gif ├── styles ├── images │ ├── loader.gif │ ├── search-icon.png │ └── search-icon-mobile.png ├── properties.css ├── navbar_header.css └── style.css ├── lib ├── leaflet │ ├── images │ │ ├── layers.png │ │ ├── locate.png │ │ ├── spinner.gif │ │ ├── layers-2x.png │ │ ├── locate@2x.png │ │ ├── marker-icon.png │ │ ├── spinner@2x.gif │ │ ├── locate_touch.png │ │ ├── marker-shadow.png │ │ ├── marker-icon-2x.png │ │ └── marker-icon-gray.png │ ├── L.Control.Locate.css │ └── L.Control.Locate.js ├── leaflet.markercluster │ ├── MarkerCluster.css │ └── MarkerCluster.Default.css ├── es5-sham.min.js ├── fuse.min.js └── es5-shim.min.js ├── rules-engine-questions.docx ├── kentucky substance abuse referral list.csv ├── .travis.yml ├── civic.json ├── script ├── build.js └── copyfiles.js ├── test ├── spec │ ├── ui │ │ ├── tab_spec.js │ │ ├── project_spec.js │ │ ├── info_spec.js │ │ ├── loading_spec.js │ │ ├── search_spec.js │ │ ├── list_spec.js │ │ ├── facet_spec.js │ │ └── map_spec.js │ ├── data │ │ ├── search_spec.js │ │ ├── geojson_spec.js │ │ ├── analytics_spec.js │ │ └── facet_spec.js │ └── infotemplates_spec.js ├── runner.js └── mock.js ├── .jshintrc ├── license.md ├── data.geojson ├── package.json ├── karma.conf.js ├── google-apps-feedback-script.js ├── readme.md └── config.json /CNAME: -------------------------------------------------------------------------------- 1 | gethelplex.org 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | dist/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /src/templates/facet.html: -------------------------------------------------------------------------------- 1 |

{{{title}}}

2 | {{{form}}} 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/img/logo.png -------------------------------------------------------------------------------- /img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/img/spinner.gif -------------------------------------------------------------------------------- /get-help-lex-geocode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/get-help-lex-geocode.gif -------------------------------------------------------------------------------- /img/GetHelpLextype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/img/GetHelpLextype.png -------------------------------------------------------------------------------- /styles/images/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/styles/images/loader.gif -------------------------------------------------------------------------------- /lib/leaflet/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/layers.png -------------------------------------------------------------------------------- /lib/leaflet/images/locate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/locate.png -------------------------------------------------------------------------------- /rules-engine-questions.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/rules-engine-questions.docx -------------------------------------------------------------------------------- /styles/images/search-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/styles/images/search-icon.png -------------------------------------------------------------------------------- /lib/leaflet/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/spinner.gif -------------------------------------------------------------------------------- /lib/leaflet/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/layers-2x.png -------------------------------------------------------------------------------- /lib/leaflet/images/locate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/locate@2x.png -------------------------------------------------------------------------------- /lib/leaflet/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/marker-icon.png -------------------------------------------------------------------------------- /lib/leaflet/images/spinner@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/spinner@2x.gif -------------------------------------------------------------------------------- /lib/leaflet/images/locate_touch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/locate_touch.png -------------------------------------------------------------------------------- /lib/leaflet/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/marker-shadow.png -------------------------------------------------------------------------------- /styles/images/search-icon-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/styles/images/search-icon-mobile.png -------------------------------------------------------------------------------- /lib/leaflet/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/marker-icon-2x.png -------------------------------------------------------------------------------- /lib/leaflet/images/marker-icon-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/lib/leaflet/images/marker-icon-gray.png -------------------------------------------------------------------------------- /kentucky substance abuse referral list.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlexington/gethelplex/HEAD/kentucky substance abuse referral list.csv -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | # explicitly enable building gh-pages branch 5 | branches: 6 | only: 7 | - gh-pages 8 | - /^.*$/ 9 | -------------------------------------------------------------------------------- /src/templates/form.html: -------------------------------------------------------------------------------- 1 |
2 | {{#inputs}} 3 | {{{this}}} 4 | {{/inputs}} 5 |
6 | clear 7 | -------------------------------------------------------------------------------- /civic.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "Alpha", 3 | "tags": [ 4 | "lexington", 5 | "kentucky", 6 | "ky", 7 | "finda", 8 | "health", 9 | "human services", 10 | "treatment", 11 | "substance abuse", 12 | "gis" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/templates/input.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | -------------------------------------------------------------------------------- /script/build.js: -------------------------------------------------------------------------------- 1 | ({ 2 | baseUrl: '../src', 3 | mainConfigFile: '../src/script.js', 4 | preserveLicenseComments: true, 5 | wrap: false, 6 | name: '../node_modules/almond/almond', 7 | include:'script', 8 | insertRequire:['script'], 9 | out: '../dist/src/script.js', 10 | optimize: 'uglify2' 11 | }) 12 | -------------------------------------------------------------------------------- /img/comment-alt-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /styles/properties.css: -------------------------------------------------------------------------------- 1 | /* Styles for each individual property, as defined in config.json */ 2 | 3 | .feature-phone_numbers { 4 | font-style: italic; 5 | } 6 | .feature-address { 7 | font-style: italic; 8 | } 9 | .feature-address a { /* directions */ 10 | font-style: normal; 11 | } 12 | .feature-organization_name { 13 | font-weight: bold; 14 | font-size: 20px; 15 | color: #2a6496; 16 | } 17 | -------------------------------------------------------------------------------- /lib/leaflet.markercluster/MarkerCluster.css: -------------------------------------------------------------------------------- 1 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { 2 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; 3 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; 4 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; 5 | transition: transform 0.3s ease-out, opacity 0.3s ease-in; 6 | } 7 | -------------------------------------------------------------------------------- /src/data/config.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var $ = require('jquery'); 5 | module.exports = flight.component(function loader() { 6 | this.after('initialize', function() { 7 | // load the data 8 | $.getJSON('config.json', function(config) { 9 | this.trigger('config', config); 10 | }.bind(this)); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/ui/back-to-top.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var $ = require('jquery'); 5 | 6 | module.exports = flight.component(function() { 7 | this.onClick = function() { 8 | $('body,html').animate({ 9 | scrollTop: 0 10 | }, 1000); 11 | return false; 12 | }; 13 | 14 | this.after('initialize', function() { 15 | this.on('click', this.onClick); 16 | }); 17 | }); 18 | }); -------------------------------------------------------------------------------- /img/map-marker-alt-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/scroll.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var $ = require('jquery'); 5 | 6 | module.exports = flight.component(function() { 7 | 8 | this.onScroll = function() { 9 | if (this.$node.scrollTop() >= 500) { 10 | $('#back-to-top').fadeIn(500); 11 | } else { 12 | $('#back-to-top').fadeOut(500); 13 | } 14 | }; 15 | 16 | this.after('initialize', function() { 17 | this.on('scroll', this.onScroll); 18 | }); 19 | }); 20 | }); -------------------------------------------------------------------------------- /src/ui/filtering.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | require('bootstrap'); 5 | 6 | return flight.component(function filtering() { 7 | this.attributes({ 8 | contentSelector: '#message' 9 | }); 10 | 11 | this.toggle = function() { 12 | this.$node.toggle(); 13 | }; 14 | 15 | this.after('initialize', function() { 16 | this.on(document, 'dataFilteringStarted', this.toggle); 17 | this.on(document, 'dataFilteringFinished', this.toggle); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /img/search-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/facetControls.html: -------------------------------------------------------------------------------- 1 |
2 | {{#if showResults}} 3 | « Back to Questions 4 | {{else}} 5 | 8 | 11 | 12 | Skip to Facilities » 13 | 14 | {{/if}} 15 |
16 | -------------------------------------------------------------------------------- /img/info-circle-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/print-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/spec/ui/tab_spec.js: -------------------------------------------------------------------------------- 1 | define( 2 | ['test/mock'], 3 | function(mock) { 4 | 'use strict'; 5 | describeComponent('ui/tabs', function() { 6 | beforeEach(function() { 7 | setupComponent(); 8 | }); 9 | 10 | describe('on results-tab click', function() { 11 | it("Removes survey-tabs class", function() { 12 | this.$node.html('
'); 13 | this.component.setupClickHandlers(); 14 | 15 | this.$node.find('#results-tab').click(); 16 | expect(this.$node.find('.survey-tabs').length).toBe(0); 17 | }); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /img/home-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/medicationAssistedTreatment.html: -------------------------------------------------------------------------------- 1 |

Medication Assisted Treatments (MAT)

2 | 3 |

MAT combines behavioural therapy and medications to treat substance use disorders. These medications may include: buprenorphine (ex: subutex, suboxone, zubsolv), methadone, and naltrexone (Vivitrol). 4 | These national locators will help you find providers near you:

5 | 10 | -------------------------------------------------------------------------------- /src/ui/tabs.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | 5 | module.exports = flight.component(function sidebar() { 6 | this.onShowResults = function(event, opts) { 7 | if (!opts) { opts = {}; } 8 | 9 | if (!opts.dontClickTab) { 10 | this.$node.find('#results-tab').click(); 11 | } 12 | this.$node.find('.survey-tabs').removeClass('survey-tabs'); 13 | }; 14 | 15 | this.setupClickHandlers = function() { 16 | this.$node.find('#results-tab').on('click', function() { 17 | this.trigger('uiShowResults', {dontClickTab: true}); 18 | }.bind(this)); 19 | }; 20 | 21 | this.after('initialize', function() { 22 | this.on(document, 'uiShowResults', this.onShowResults); 23 | this.setupClickHandlers(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /img/bullhorn-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "curly": true, 5 | "debug": false, 6 | "eqeqeq": true, 7 | "es3": true, 8 | "forin": true, 9 | "immed": true, 10 | "iterator": false, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonew": true, 16 | "smarttabs": false, 17 | "strict": true, 18 | "trailing": true, 19 | "undef": true, 20 | "unused": true, 21 | "indent": 2, 22 | "globals": { 23 | "define": false, 24 | "require": false, 25 | // jasmine 26 | "jasmine": false, 27 | "describeComponent": false, 28 | "setupComponent": false, 29 | "spyOn": false, 30 | "spyOnEvent": false, 31 | "setFixtures": false, 32 | "describe": false, 33 | "beforeEach": false, 34 | "afterEach": false, 35 | "it": false, 36 | "expect": false, 37 | "waits": false, 38 | "runs": false 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ui/project.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var $ = require('jquery'); 5 | var _ = require('lodash'); 6 | 7 | var stripHtml = function(html) { 8 | return $("
").html(html).text(); 9 | }; 10 | 11 | module.exports = flight.component(function project() { 12 | this.configureProject = function(ev, config) { 13 | _.mapValues( 14 | config.project, 15 | function(value, key) { 16 | // find everything with data-project="key", and replace the HTML 17 | // with what's in the configuration 18 | $("*[data-project=" + key + "]").html(value); 19 | // set meta fields to the text value 20 | $("meta[name=" + key + "]").attr( 21 | 'content', stripHtml(value)); 22 | }); 23 | }; 24 | 25 | this.after('initialize', function() { 26 | this.on(document, 'config', this.configureProject); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/spec/ui/project_spec.js: -------------------------------------------------------------------------------- 1 | define(['test/mock', 'jquery'], function(mock, $) { 2 | 'use strict'; 3 | describeComponent('ui/project', function() { 4 | beforeEach(function() { 5 | setupComponent(); 6 | }); 7 | 8 | describe('on config', function() { 9 | it('updates elements with data-project', function() { 10 | setFixtures("
"); 11 | $(document).trigger('config', mock.config); 12 | expect($("#name").html()).toEqual(mock.config.project.name); 13 | expect($("#desc").html()).toEqual(mock.config.project.description); 14 | }); 15 | 16 | it('updates content of meta fields', function() { 17 | setFixtures(""); 18 | $(document).trigger('config', mock.config); 19 | expect($("meta[name=description]").attr('content')).toEqual( 20 | $("
").html(mock.config.project.description).text()); 21 | }); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/spec/ui/info_spec.js: -------------------------------------------------------------------------------- 1 | define(['infotemplates', 'jquery'], function(templates, $) { 2 | 'use strict'; 3 | describeComponent('ui/info', function() { 4 | beforeEach(function() { 5 | setupComponent('
'); 6 | }); 7 | 8 | describe('on selectFeature', function() { 9 | it('creates a popup with the properties of the feature and config', function() { 10 | var config = {properties: 'config properties'}, 11 | feature = {properties: 'feature properties'}; 12 | spyOn(templates, 'popup'); 13 | $(document).trigger('config', config); 14 | $(document).trigger('selectFeature', feature); 15 | expect(templates.popup).toHaveBeenCalledWith( 16 | config.properties, feature.properties); 17 | }); 18 | }); 19 | 20 | describe('on close click', function() { 21 | it('hides the popup', function() { 22 | spyOn(this.$node, 'hide'); 23 | this.$node.find('.close').click(); 24 | expect(this.$node.hide).toHaveBeenCalledWith(); 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /script/copyfiles.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var glob = require('glob'); 6 | var mkdirp = require('mkdirp'); 7 | var input = [ 8 | 'index.html', 9 | 'data.geojson', 10 | 'config.json', 11 | 'styles/properties.css', 12 | 'img/logo.png' 13 | ]; 14 | var inGlobs = [ 15 | '*.md', 16 | 'lib/leaflet/images/marker-*' 17 | ]; 18 | var outDir = 'dist'; 19 | 20 | function move(infile, outpath) { 21 | fs.createReadStream(infile).pipe(fs.createWriteStream(path.join(outpath, infile))); 22 | } 23 | 24 | function moveGlob (inGlob, outpath) { 25 | glob(inGlob, function (err, files) { 26 | if (err) { 27 | console.log(err); 28 | } 29 | files.forEach(function (file) { 30 | move(file, outpath); 31 | }); 32 | }); 33 | } 34 | 35 | mkdirp.sync(path.join(outDir,'lib/leaflet/images')); 36 | mkdirp.sync(path.join(outDir,'styles')); 37 | mkdirp.sync(path.join(outDir,'img')); 38 | input.forEach(function (file) { 39 | move(file, outDir); 40 | }); 41 | inGlobs.forEach(function (file) { 42 | moveGlob(file, outDir); 43 | }); 44 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Code For Boston 2 | ===== 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | **THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE.** 21 | -------------------------------------------------------------------------------- /data.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "geometry": { 7 | "type": "Point", 8 | "coordinates": [ 9 | -84.4934789, 10 | 38.082537 11 | ] 12 | }, 13 | "properties": { 14 | "address": "1351 Newtown Pike Building 5", 15 | "organization_name": "Bluegrass.org Pride Program", 16 | "city": "Lexington", 17 | "web_url": "http://www.firstchurchuu.org/outreach.html#glbt", 18 | "phone_numbers": [ 19 | "859-425-1210" 20 | ], 21 | "contact_names": [], 22 | "contact_emails": [ 23 | "test@gmail.com" 24 | ], 25 | "facility_type": [ 26 | "out_patient" 27 | ], 28 | "service_class_level_1": [ 29 | "Para-professional Support Services" 30 | ], 31 | "service_class_level_2": [ 32 | "Para-professional Counseling, Therapy and Support" 33 | ], 34 | "target_populations": [ 35 | "LGBTQ" 36 | ], 37 | "age_range": "", 38 | "additional_notes": "" 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/ui/loading.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | require('bootstrap'); 5 | 6 | return flight.component(function loading() { 7 | this.attributes({ 8 | contentSelector: 'h4', 9 | loadingText: 'Loading...' 10 | }); 11 | 12 | var showCount = 0; 13 | 14 | this.showLoading = function() { 15 | this.show(this.attr.loadingText); 16 | }; 17 | 18 | this.show = function(content) { 19 | showCount = showCount + 1; 20 | if (showCount === 1) { // first show 21 | this.select('contentSelector').text(content); 22 | this.$node.modal({ 23 | keyboard: false 24 | }); 25 | } 26 | }; 27 | 28 | this.hide = function() { 29 | showCount = showCount - 1; 30 | if (showCount === 0) { 31 | this.$node.modal('hide'); 32 | } 33 | }; 34 | 35 | this.after('initialize', function() { 36 | showCount = 0; 37 | this.on(document, 'mapStarted', this.showLoading); 38 | this.on(document, 'mapFinished', this.hide); 39 | 40 | this.on(document, 'listStarted', this.showLoading); 41 | this.on(document, 'listFinished', this.hide); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /lib/leaflet/L.Control.Locate.css: -------------------------------------------------------------------------------- 1 | /* Compatible with Leaflet 0.7 */ 2 | 3 | .leaflet-touch .leaflet-bar-part-single { 4 | -webkit-border-radius: 7px 7px 7px 7px; 5 | border-radius: 7px 7px 7px 7px; 6 | border-bottom: none; 7 | } 8 | 9 | .leaflet-control-locate a { 10 | background-image: url(images/locate.png); 11 | background-size:90px 30px; 12 | background-position: -2px -2px; 13 | } 14 | 15 | .leaflet-retina .leaflet-control-locate a { 16 | background-image: url(images/locate@2x.png); 17 | } 18 | 19 | .leaflet-touch .leaflet-control-locate a { 20 | background-image: url(images/locate_touch.png); 21 | } 22 | 23 | .leaflet-control-locate.requesting a { 24 | background-size:12px 12px; 25 | background-image: url(images/spinner.gif); 26 | background-position: 50% 50%; 27 | } 28 | 29 | .leaflet-retina .leaflet-control-locate.requesting a { 30 | background-image: url(images/spinner@2x.gif); 31 | } 32 | 33 | .leaflet-control-locate.active a { 34 | background-position: -32px -2px; 35 | } 36 | 37 | .leaflet-control-locate.active.following a { 38 | background-position: -62px -2px; 39 | } 40 | 41 | .leaflet-touch .leaflet-control-locate { 42 | box-shadow: none; 43 | border: 2px solid rgba(0,0,0,0.2); 44 | background-clip: padding-box; 45 | } 46 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var tests = []; 3 | for (var file in window.__karma__.files) { 4 | if (window.__karma__.files.hasOwnProperty(file)) { 5 | if (/_spec\.js$/.test(file)) { 6 | tests.push(file); 7 | } 8 | } 9 | } 10 | 11 | require.config({ 12 | baseUrl: '/base/src/', 13 | paths: { 14 | 'test': '../test', 15 | 'jquery': '../lib/jquery-1.11.1.min', 16 | 'leaflet': '../lib/leaflet/leaflet-src', 17 | 'L.Control.Locate': '../lib/leaflet/L.Control.Locate', 18 | 'leaflet.markercluster': '../lib/leaflet.markercluster/leaflet.markercluster-src', 19 | 'handlebars': '../lib/handlebars', 20 | 'lodash': '../lib/lodash.min', 21 | 'flight': '../lib/flight.min', 22 | 'd3': '../lib/d3.min', 23 | 'Tabletop': '../lib/tabletop', 24 | 'bootstrap': '../lib/bootstrap.min', 25 | 'text': '../lib/text' 26 | }, 27 | shim: { 28 | 'handlebars': { 29 | exports: 'Handlebars' 30 | }, 31 | 'underscore': { 32 | exports: '_' 33 | }, 34 | 'flight': { 35 | exports: 'flight' 36 | }, 37 | leaflet: { 38 | exports: 'L' 39 | }, 40 | bootstrap: ['jquery'], 41 | 'L.Control.Locate': ['leaflet'], 42 | 'leaflet.markercluster': ['leaflet'] 43 | }, 44 | 45 | deps: tests, 46 | callback: window.__karma__.start 47 | }); 48 | -------------------------------------------------------------------------------- /src/ui/select_county.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var $ = require('jquery'); 5 | var _ = require('lodash'); 6 | 7 | module.exports = flight.component(function selectCounty() { 8 | this.loadData = function(ev, data) { 9 | var counties = _.map(data.features, function(feature) { 10 | if (_.isString(feature.properties.county)) { 11 | return feature.properties.county.trim(); 12 | } 13 | }); 14 | counties = _.uniq(counties); 15 | counties = _.compact(counties); 16 | counties = _.sortBy(counties); 17 | 18 | counties.forEach(function(county) { 19 | this.$node.append($("
").addClass(this.attr.contentClass). 26 | appendTo(this.$node); 27 | } 28 | content.html(popup); 29 | this.$node.show(); 30 | }; 31 | 32 | this.hide = function() { 33 | this.$node.hide(); 34 | this.trigger(document, 'deselectFeature', this.attr.currentFeature); 35 | this.attr.currentFeature = null; 36 | }; 37 | 38 | this.after('initialize', function() { 39 | this.on(document, 'config', this.configureInfo); 40 | this.on(document, 'selectFeature', this.update); 41 | this.on('click', { 42 | closeSelector: this.hide 43 | }); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/leaflet.markercluster/MarkerCluster.Default.css: -------------------------------------------------------------------------------- 1 | .marker-cluster-small { 2 | background-color: rgba(181, 226, 140, 0.6); 3 | } 4 | .marker-cluster-small div { 5 | background-color: rgba(110, 204, 57, 0.6); 6 | } 7 | 8 | .marker-cluster-medium { 9 | background-color: rgba(241, 211, 87, 0.6); 10 | } 11 | .marker-cluster-medium div { 12 | background-color: rgba(240, 194, 12, 0.6); 13 | } 14 | 15 | .marker-cluster-large { 16 | background-color: rgba(253, 156, 115, 0.6); 17 | } 18 | .marker-cluster-large div { 19 | background-color: rgba(241, 128, 23, 0.6); 20 | } 21 | 22 | /* IE 6-8 fallback colors */ 23 | .leaflet-oldie .marker-cluster-small { 24 | background-color: rgb(181, 226, 140); 25 | } 26 | .leaflet-oldie .marker-cluster-small div { 27 | background-color: rgb(110, 204, 57); 28 | } 29 | 30 | .leaflet-oldie .marker-cluster-medium { 31 | background-color: rgb(241, 211, 87); 32 | } 33 | .leaflet-oldie .marker-cluster-medium div { 34 | background-color: rgb(240, 194, 12); 35 | } 36 | 37 | .leaflet-oldie .marker-cluster-large { 38 | background-color: rgb(253, 156, 115); 39 | } 40 | .leaflet-oldie .marker-cluster-large div { 41 | background-color: rgb(241, 128, 23); 42 | } 43 | 44 | .marker-cluster { 45 | background-clip: padding-box; 46 | border-radius: 20px; 47 | } 48 | .marker-cluster div { 49 | width: 30px; 50 | height: 30px; 51 | margin-left: 5px; 52 | margin-top: 5px; 53 | 54 | text-align: center; 55 | border-radius: 15px; 56 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 57 | } 58 | .marker-cluster span { 59 | line-height: 30px; 60 | } -------------------------------------------------------------------------------- /src/timed_with_object.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | var $ = require('jquery'); 4 | 5 | var chunkLength = 50, 6 | tickLength = 25; 7 | 8 | function nextTick(f) { 9 | window.setTimeout(f, tickLength); 10 | } 11 | 12 | return function timedWithObject(array, handler, object, newThis) { 13 | // Given an array and a handler function, calls the handler function with 14 | // each element of the array, along with arbitrary object, returning a 15 | // promise. The value returned from the previous call to the handler is 16 | // used as the object for the next call, and the final object is used to 17 | // resolve the returned promise. If processing takes longer than 50ms, 18 | // further chunks will be done in other interations of the event loop to 19 | // avoid blocking the UI thread. Based on Nicholas C. Zakas' 20 | // timedChunk(). 21 | var deferred = $.Deferred(); 22 | if (array.length === 0) { 23 | deferred.resolve(object); 24 | return deferred.promise(); 25 | } 26 | 27 | array = array.slice(); 28 | 29 | nextTick(function internalLoop() { 30 | var start = +new Date(), 31 | offset; 32 | 33 | do { 34 | object = handler.call(newThis, array.shift(), object); 35 | offset = +new Date() - start; 36 | } while (array.length > 0 && offset < chunkLength); 37 | 38 | if (array.length > 0) { 39 | nextTick(internalLoop); 40 | } else { 41 | deferred.resolve(object); 42 | } 43 | 44 | }); 45 | 46 | return deferred.promise(); 47 | }; 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "finda", 3 | "version": "0.0.0", 4 | "description": "Generic 'find-a' app for geographic datasets", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "devDependencies": { 9 | "karma": "~0.13.22", 10 | "jshint": "~2.4.4", 11 | "karma-requirejs": "~0.2.6", 12 | "karma-jasmine": "~0.1.5", 13 | "karma-cli": "0.0.3", 14 | "karma-phantomjs-launcher": "~0.1.2", 15 | "almond": "^0.2.9", 16 | "clean-css": "^2.1.6", 17 | "requirejs": "^2.1.11", 18 | "rimraf": "^2.2.6", 19 | "replace": "^0.2.9", 20 | "glob": "^3.2.9", 21 | "mkdirp": "^0.3.5", 22 | "copyfiles": "0.0.1" 23 | }, 24 | "scripts": { 25 | "start": "http-server", 26 | "test": "jshint src && karma start --single-run --browsers PhantomJS", 27 | "test-server": "jshint src && karma start --browsers PhantomJS", 28 | "test-client": "jshint src && karma run", 29 | "build": "npm run clean && npm run copy && npm run cssmin && npm run requirejs && npm run processhtml && npm run index", 30 | "clean": "rimraf dist", 31 | "copy": "copyfiles index.html data.geojson config.json *.md styles/properties.css img/* lib/leaflet/images/marker-* dist", 32 | "cssmin": "cleancss --s1 -o dist/styles/style.css styles/style.css", 33 | "requirejs": "r.js -o script/build.js", 34 | "index": "replace -s 'data-main=\"(src/script.js)\" src=\"lib/require.js\"' 'src=\"$1\"' dist/index.html" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/codeforboston/finda.git" 39 | }, 40 | "license": "MIT", 41 | "private": true 42 | } 43 | -------------------------------------------------------------------------------- /test/spec/ui/loading_spec.js: -------------------------------------------------------------------------------- 1 | define(['jquery'], function($) { 2 | 'use strict'; 3 | describeComponent('ui/loading', function() { 4 | beforeEach(function() { 5 | setupComponent('', { 6 | contentSelector: 'h4' 7 | }); 8 | spyOn(this.$node, 'modal'); 9 | }); 10 | 11 | describe('loading', function() { 12 | it("displays a loading message on loading events", function() { 13 | $(document).trigger('mapStarted', {}); 14 | expect(this.component.select('contentSelector').text()).toEqual( 15 | this.component.attr.loadingText); 16 | }); 17 | }); 18 | 19 | describe("the modal", function() { 20 | beforeEach(function() { 21 | $(document).trigger('mapStarted', {}); 22 | }); 23 | it("is shown loading events", function() { 24 | expect(this.$node.modal).toHaveBeenCalledWith({ 25 | keyboard: false 26 | }); 27 | }); 28 | it("isn't re-shown a second time", function() { 29 | $(document).trigger('listStarted', {}); 30 | expect(this.$node.modal.callCount).toEqual(1); 31 | }); 32 | it("doesn't hide the modal if things are still loading", function() { 33 | $(document).trigger('listStarted', {}); 34 | $(document).trigger('mapFinished', {}); 35 | expect(this.$node.modal).not.toHaveBeenCalledWith('hide'); 36 | }); 37 | it("hides when all finish events is triggered", function() { 38 | $(document).trigger('listStarted', {}); 39 | $(document).trigger('mapFinished', {}); 40 | $(document).trigger('listFinished', {}); 41 | expect(this.$node.modal).toHaveBeenCalledWith('hide'); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /styles/navbar_header.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ghl-navbar { 4 | height:50px !important; 5 | } 6 | 7 | #ghl-navbar ul.nav { 8 | font-size: 12px; 9 | } 10 | 11 | .navbar-header .navbar-nav>li, .navbar-header .navbar-nav{ 12 | float: left !important; 13 | } 14 | 15 | 16 | 17 | .navbar-header .navbar-right{ 18 | float: right !important; 19 | } 20 | 21 | .navbar-header { 22 | width:100%; 23 | height:55px; 24 | } 25 | 26 | .navbar-header .navbar-nav>li { 27 | text-align: center; 28 | position: relative; 29 | } 30 | 31 | .navbar-header .header-nav-icon { 32 | width:18px;opacity: 0.7;position:absolute;top:-7px;left:50%;margin-left:-10px; 33 | } 34 | 35 | .navbar-header .navbar-nav>li>a { 36 | /*line-height: 24px;*/ 37 | padding-top:13px; 38 | padding-bottom:7px; 39 | padding-left:10px; 40 | padding-right:10px; 41 | min-width:45px; 42 | } 43 | 44 | .navbar-header .home-nav-icon { 45 | width:20px; 46 | top:-6px; 47 | } 48 | 49 | @media (min-width: 768px) { 50 | .navbar-header .navbar-nav { 51 | margin-top:7px; 52 | } 53 | .navbar-header .navbar-nav>li>a { 54 | min-width:60px; 55 | } 56 | } 57 | 58 | @media (max-width: 498px) { 59 | .navbar-header .home-nav-entry { display:none; } 60 | .navbar-header .brand_image { margin-left:-10px; margin-left:-10px; } 61 | .navbar-header { margin-left:0px; !important; margin-right:0px; !important; } 62 | .navbar-header .navbar-nav>li>a { min-width:30px; padding-right:5px; padding-right:5px; } 63 | .navbar-header .info-nav-icon { margin-left:-8px; } 64 | } -------------------------------------------------------------------------------- /src/ui/search.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var _ = require('lodash'); 5 | 6 | module.exports = flight.component(function search() { 7 | this.defaultAttrs({ 8 | searchSelector: 'input', 9 | searchResultsSelector: '#search-results', 10 | resultTemplate: '{{ organization_name }} ({{ address }})' 11 | }); 12 | 13 | this.configureSearch = function(ev, config) { 14 | if (config.search && config.search.geosearch) { 15 | this.$node.show(); 16 | } else { 17 | this.$node.hide(); 18 | } 19 | }; 20 | 21 | this.inProgressSearch = _.debounce(function(ev) { 22 | ev.preventDefault(); 23 | if (ev.keyCode === 13) { // Enter 24 | return; 25 | } 26 | var query = this.select('searchSelector').val(); 27 | this.trigger(document, 'uiInProgressSearch', { 28 | query: query 29 | }); 30 | }, 100); 31 | 32 | this.search = function(ev) { 33 | ev.preventDefault(); 34 | var address = this.select('searchSelector').val(); 35 | if (address) { 36 | this.trigger(document, 'uiSearch', {query: address}); 37 | } 38 | this.select('searchResultsSelector').trigger('uiHideSearchResults'); 39 | }; 40 | 41 | this.onSearchResult = function(ev, result) { 42 | this.select('searchSelector').attr('placeholder', 43 | result.name).val(''); 44 | }; 45 | 46 | this.after('initialize', function() { 47 | this.on(this.attr.searchSelector, 'keydown', this.inProgressSearch); 48 | this.on('submit', this.search); 49 | this.on(document, 'config', this.configureSearch); 50 | this.on(document, 'dataSearchResult', this.onSearchResult); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/spec/data/search_spec.js: -------------------------------------------------------------------------------- 1 | define(['test/mock', 'jquery'], function(mock, $) { 2 | 'use strict'; 3 | describeComponent('data/search', function() { 4 | beforeEach(function() { 5 | setupComponent(); 6 | spyOn($, 'getJSON'); 7 | spyOnEvent(document, 'dataSearchResult'); 8 | }); 9 | 10 | describe('on config', function() { 11 | beforeEach(function() { 12 | this.component.trigger('config', mock.config); 13 | }); 14 | it('records the bounds of the search', function() { 15 | expect(this.component.maxBounds).toEqual(mock.config.map.maxBounds); 16 | }); 17 | }); 18 | 19 | describe('on uiSearch', function() { 20 | beforeEach(function() { 21 | this.component.trigger('config', mock.config); 22 | }); 23 | it('searches for the given query', function() { 24 | this.component.trigger('uiSearch', { 25 | query: 'search query' 26 | }); 27 | expect($.getJSON).toHaveBeenCalledWith( 28 | this.component.attr.searchUrl, 29 | { 30 | format: 'json', 31 | addressdetails: 1, 32 | q: 'search query', 33 | viewbox: '39.2,-78,44.5,-65' 34 | }, 35 | jasmine.any(Function) 36 | ); 37 | }); 38 | }); 39 | 40 | describe("#onSearchResult", function() { 41 | it("does nothing if there's no result", function() { 42 | this.component.searchResults([]); 43 | expect('dataSearchResult').not.toHaveBeenTriggered(); 44 | }); 45 | 46 | it("triggers dataSearchResult if there's a result", function() { 47 | this.component.searchResults([mock.openSearchResult]); 48 | expect('dataSearchResult').toHaveBeenTriggeredOnAndWith( 49 | document, 50 | mock.parsedSearchResult); 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/data/search.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var _ = require('lodash'); 5 | var $ = require('jquery'); 6 | 7 | return flight.component(function search() { 8 | this.defaultAttrs({ 9 | searchUrl: '//nominatim.openstreetmap.org/search' 10 | }); 11 | this.configureSearch = function(ev, config) { 12 | if (config.search && config.search.geosearch) { 13 | this.maxBounds = config.map.maxBounds; 14 | } else { 15 | this.teardown(); 16 | } 17 | }; 18 | 19 | this.onSearch = function(ev, options) { 20 | ev.preventDefault(); 21 | var parameters = { 22 | format: "json", 23 | addressdetails: 1, 24 | q: options.query 25 | }; 26 | if (this.maxBounds) { 27 | parameters.viewbox = [ 28 | this.maxBounds[0][0], this.maxBounds[0][1], 29 | this.maxBounds[1][0], this.maxBounds[1][1] 30 | ].join(','); 31 | } 32 | $.getJSON(this.attr.searchUrl, 33 | parameters, 34 | this.searchResults.bind(this)); 35 | }; 36 | 37 | this.searchResults = function(results) { 38 | if (results.length) { 39 | var location = results[0], 40 | displayName = _.compact([location.address.road, 41 | location.address.city, 42 | location.address.state 43 | ]).join(', '); 44 | this.trigger('dataSearchResult', { 45 | name: displayName, 46 | lat: location.lat, 47 | lng: location.lon 48 | }); 49 | } 50 | }; 51 | 52 | this.after('initialize', function() { 53 | this.on(document, 'config', this.configureSearch); 54 | this.on(document, 'uiSearch', this.onSearch); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/script.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | baseUrl: 'src/', 3 | paths: { 4 | 'jquery': '../lib/jquery-1.11.1.min', 5 | 'bootstrap': '../lib/bootstrap.min', 6 | 'leaflet': '../lib/leaflet/leaflet', 7 | 'L.Control.Locate': '../lib/leaflet/L.Control.Locate', 8 | 'leaflet.markercluster': '../lib/leaflet.markercluster/leaflet.markercluster', 9 | 'handlebars': '../lib/handlebars', 10 | 'lodash': '../lib/lodash.min', 11 | 'flight': '../lib/flight.min', 12 | 'fuse': '../lib/fuse.min', 13 | 'Tabletop': '../lib/tabletop', 14 | 'd3': '../lib/d3.min', 15 | 'StateMachine': '../lib/state-machine', 16 | 'text': '../lib/text' 17 | }, 18 | shim: { 19 | 'handlebars': { 20 | exports: 'Handlebars' 21 | }, 22 | 'lodash': { 23 | exports: '_' 24 | }, 25 | 'flight': { 26 | exports: 'flight' 27 | }, 28 | 'bootstrap': ['jquery'], 29 | leaflet: { 30 | exports: 'L' 31 | }, 32 | 'L.Control.Locate': ['leaflet'], 33 | 'leaflet.markercluster': ['leaflet'] 34 | } 35 | }); 36 | 37 | define(function(require) { 38 | 'use strict'; 39 | require('bootstrap'); 40 | // attach components to the DOM 41 | require('ui/map').attachTo('#map'); 42 | require('ui/search').attachTo('#search'); 43 | require('ui/search_results').attachTo('#search-results'); 44 | require('ui/info').attachTo('#info'); 45 | require('ui/list').attachTo('#list'); 46 | require('ui/tabs').attachTo('#finda-tabs'); 47 | require('ui/facet').attachTo('#facets'); 48 | require('ui/loading').attachTo('#loading'); 49 | require('ui/filtering').attachTo('#message'); 50 | require('ui/feedback_widget').attachTo('#feedback-modal'); 51 | require('ui/back-to-top').attachTo('#back-to-top'); 52 | require('ui/select_county').attachTo('#select_county'); 53 | require('ui/scroll').attachTo(document); 54 | require('ui/project').attachTo(document); 55 | require('data/facet').attachTo(document); 56 | require('data/search').attachTo(document); 57 | require('data/typeahead').attachTo(document); 58 | require('data/geojson').attachTo(document); 59 | require('data/config').attachTo(document); 60 | }); 61 | -------------------------------------------------------------------------------- /test/spec/ui/search_spec.js: -------------------------------------------------------------------------------- 1 | define(['test/mock', 'jquery'], function(mock, $) { 2 | 'use strict'; 3 | describeComponent('ui/search', function() { 4 | beforeEach(function() { 5 | setupComponent('
', 6 | {searchSelector: 'input', 7 | mapSelector: 'div'}); 8 | spyOnEvent('div', 'panTo'); 9 | spyOnEvent(document, 'uiSearch'); 10 | spyOn($, 'getJSON'); 11 | this.config = {search: {geosearch: true}, 12 | map: {maxBounds: 'maxBounds'}}; 13 | }); 14 | 15 | describe('configuration sets up local values', function() { 16 | beforeEach(function() { 17 | $(document).trigger('config', this.config); 18 | }); 19 | it('hides the widget if it is not requested', function() { 20 | this.config.search.geosearch = false; 21 | $(document).trigger('config', this.config); 22 | expect(this.$node).not.toBeVisible(); 23 | }); 24 | }); 25 | 26 | describe('form submission', function() { 27 | beforeEach(function() { 28 | this.component.maxBounds = [[1, 2], [3, 4]]; 29 | spyOnEvent(this.component.node, 'uiSearch'); 30 | }); 31 | it('does nothing if the input is empty', function() { 32 | this.$node.find('input').submit(); 33 | expect('uiSearch').not.toHaveBeenTriggered(); 34 | }); 35 | it('emits a uiSearch event with the search query', function() { 36 | this.$node.find('input').val('address').submit(); 37 | expect('uiSearch').toHaveBeenTriggeredOnAndWith(document, 38 | {query: 'address'}); 39 | }); 40 | }); 41 | 42 | describe('dataSearchResult', function() { 43 | beforeEach(function() { 44 | $(document).trigger('dataSearchResult', mock.parsedSearchResult); 45 | }); 46 | it('sets the placeholder display to the city', function() { 47 | var input = this.component.select('searchSelector'); 48 | expect(input.attr('placeholder')).toEqual( 49 | mock.parsedSearchResult.name); 50 | expect(input.val()).toEqual(''); 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/templates/extraResources.html: -------------------------------------------------------------------------------- 1 |

Additional Resources

2 | 3 |

SAMHSA's National Helpline

4 | 5 |

SAMHSA website

6 |

SAMHSA Buprenorphine Treatment Physician Locator

7 |

1-800-662-HELP (4357)

8 |

1-800-662-9832 (Español) 9 |

TTY: 1-800-487-4889

10 | 11 |

Also known as, the Treatment Referral Routing Service, this Helpline provides 24-hour free and confidential treatment referral and information about mental and/or substance use disorders, prevention, and recovery in English and Spanish.

12 | 13 |

Suicide Prevention Lifeline

14 | 15 |

1-800-273-TALK (8255)

16 |

TTY: 1-800-799-4889

17 | 18 |

Suicide Prevention Lifelife website

19 | 20 |

This is a 24-hour, toll-free, confidential suicide prevention hotline available to anyone in suicidal crisis or emotional distress. Your call is routed to the nearest crisis center in the national network of more than 150 crisis centers.

21 | 22 |

Community Mental Health Centers’ Crisis Lines

23 | 24 |

No matter where you live in Kentucky, there is a community mental health center to serve you.  The community mental health centers cover all 120 counties.  The link below will provide you with the crisis line for each county in Kentucky.  However, if you are experiencing an emergency situation, please call 911.

25 | 26 |

Community Mental Health Centers Crisis Lines website

27 | 28 |

Alcoholics Anonymous

29 | 30 |

1-800-467-8019

31 | 32 |

Alcoholics Anonymous website

33 | 34 |

Narcotics Anonymous

35 | 36 |

1-859-253-4673 (Lexington)

37 | 38 |

Narcotics Anonymous website

39 | 40 | 41 | -------------------------------------------------------------------------------- /test/spec/infotemplates_spec.js: -------------------------------------------------------------------------------- 1 | define( 2 | ['infotemplates', 'jquery', 'test/mock'], 3 | function(templates, $, mock) { 4 | 'use strict'; 5 | describe('infotemplates', function() { 6 | var config = mock.config.properties, 7 | feature = mock.data.features[0].properties; 8 | describe('#popup', function() { 9 | var rendered = templates.popup(config, feature), 10 | $rendered = $(rendered); 11 | it('urls are rendered as links', function () { 12 | var link = $rendered.find('.feature-web_url a'); 13 | expect(link.length).toEqual(1); 14 | expect(link.attr('href')).toEqual(feature.web_url); 15 | expect(link.text()).toEqual(config[3].title); 16 | }); 17 | it('directions are rendered as links to Google Maps', function() { 18 | var $directions = $rendered.find('.feature-address a'); 19 | expect($directions.text()).toEqual(config[2].title); 20 | expect($directions.attr('href')).toMatch('maps.google.com'); 21 | expect($directions.attr('href')).toMatch('q=' + encodeURIComponent( 22 | feature.address.replace('\n', ' '))); 23 | }); 24 | it('images are rendered as images', function() { 25 | var image = $rendered.find('.feature-image img'); 26 | expect(image.attr('src')).toEqual( 27 | feature.image); 28 | }); 29 | it('titles are rendered as h4s', function () { 30 | var title = $rendered.find('.feature-contact_names h4'); 31 | expect(title.length).toEqual(1); 32 | expect(title.text()).toEqual(config[4].title); 33 | }); 34 | it('lists are rendered as unordered lists', function() { 35 | var services_offered = $rendered.find( 36 | '.feature-services_offered ul'); 37 | expect(services_offered.length).toEqual(1); 38 | expect(services_offered.find('li').length).toEqual(2); 39 | }); 40 | it('plain text is rendered as-is, with \n ->
', function () { 41 | var address = $rendered.find('.feature-address'); 42 | expect(address.length).toEqual(2); 43 | expect(address.html()).toEqual( 44 | feature.address.replace(/\n/g, '
')); 45 | }); 46 | it('empty attributes are not rendered', function() { 47 | var additional_notes = $rendered.find('.feature-additional_notes'); 48 | expect(additional_notes.length).toEqual(0); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/spec/data/geojson_spec.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 'use strict'; 3 | describeComponent('data/geojson', function() { 4 | beforeEach(function() { 5 | setupComponent(); 6 | }); 7 | 8 | describe("#processData", function() { 9 | it("gives an ID to features", function() { 10 | var data = { 11 | features: [{}] 12 | }; 13 | var processed = this.component.processData(data); 14 | expect(processed.features[0].id).not.toBeUndefined(); 15 | }); 16 | 17 | it("keeps an existing ID if present", function() { 18 | var data = { 19 | features: [{ 20 | id: 1 21 | }] 22 | }; 23 | var processed = this.component.processData(data); 24 | expect(processed.features[0].id).toEqual('1'); 25 | }); 26 | }); 27 | 28 | describe('ETL CSV to GeoJSON', function() { 29 | it('groups facet values into facets', function() { 30 | 31 | var csvRow = { 32 | organization_name: 'My org', 33 | outpatient_offered: "1", 34 | residential_offered: "1" 35 | }; 36 | 37 | var facetValues = { 38 | outpatient_offered: "facility_type", 39 | residential_offered: "facility_type", 40 | } 41 | 42 | var properties = { 43 | "organization_name": "My org", 44 | "facility_type": [ 45 | "outpatient_offered", 46 | "residential_offered" 47 | ] 48 | }; 49 | 50 | var processed = this.component.csvRowToProperties(csvRow, facetValues); 51 | expect(processed.organization_name).toEqual(properties.organization_name); 52 | expect(processed.facility_type).toEqual(properties.facility_type); 53 | }); 54 | 55 | it('it only includes the search values an org offers', function() { 56 | 57 | var csvRow = { 58 | organization_name: 'My org', 59 | outpatient_offered: "1", 60 | residential_offered: "0" 61 | }; 62 | 63 | var facetValues = { 64 | outpatient_offered: "facility_type", 65 | residential_offered: "facility_type", 66 | } 67 | 68 | var properties = { 69 | "organization_name": "My org", 70 | "facility_type": [ 71 | "outpatient_offered" 72 | ] 73 | }; 74 | 75 | var processed = this.component.csvRowToProperties(csvRow, facetValues); 76 | expect(processed.facility_type).toEqual(properties.facility_type); 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Jan 25 2014 18:43:51 GMT-0500 (EST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | frameworks: ['jasmine'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | 'lib/es5-shim.min.js', 18 | 'lib/es5-sham.min.js', 19 | 'lib/jquery-1.11.1.min.js', 20 | 21 | 'test/lib/jasmine-jquery.js', 22 | 'test/lib/jasmine-flight.js', 23 | 24 | // hack to load RequireJS after the shim libs 25 | 'lib/require.js', 26 | 'node_modules/karma-requirejs/lib/adapter.js', 27 | 28 | {pattern: 'src/**/*.js', included: false}, 29 | {pattern: 'src/templates/*.html', included: false}, 30 | {pattern: 'lib/**/*.js', included: false}, 31 | {pattern: 'test/spec/**/*_spec.js', included: false}, 32 | {pattern: 'test/mock.js', included: false}, 33 | {pattern: 'lib/leaflet/images/*', included: false}, 34 | 'test/runner.js' 35 | ], 36 | 37 | 38 | // list of files to exclude 39 | exclude: [ 40 | 'src/script.js' 41 | ], 42 | 43 | 44 | // test results reporter to use 45 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 46 | reporters: ['progress'], 47 | 48 | 49 | // web server port 50 | port: 9876, 51 | 52 | 53 | // enable / disable colors in the output (reporters and logs) 54 | colors: true, 55 | 56 | 57 | // level of logging 58 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 59 | logLevel: config.LOG_INFO, 60 | 61 | 62 | // enable / disable watching file and executing tests whenever any file changes 63 | autoWatch: true, 64 | 65 | 66 | // Start these browsers, currently available: 67 | // - Chrome 68 | // - ChromeCanary 69 | // - Firefox 70 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 71 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 72 | // - PhantomJS 73 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 74 | browsers: ['PhantomJS'], 75 | 76 | 77 | // If browser does not capture in given timeout [ms], kill it 78 | captureTimeout: 60000, 79 | 80 | 81 | // Continuous Integration mode 82 | // if true, it capture browsers, run tests and exit 83 | singleRun: false 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /lib/es5-sham.min.js: -------------------------------------------------------------------------------- 1 | (function(d){"function"==typeof define?define(d):"function"==typeof YUI?YUI.add("es5-sham",d):d()})(function(){function d(a){try{return Object.defineProperty(a,"sentinel",{}),"sentinel"in a}catch(c){}}Object.getPrototypeOf||(Object.getPrototypeOf=function(a){return a.__proto__||(a.constructor?a.constructor.prototype:prototypeOfObject)});Object.getOwnPropertyDescriptor||(Object.getOwnPropertyDescriptor=function(a,c){if(typeof a!="object"&&typeof a!="function"||a===null)throw new TypeError("Object.getOwnPropertyDescriptor called on a non-object: "+ 2 | a);if(owns(a,c)){var b={enumerable:true,configurable:true};if(supportsAccessors){var d=a.__proto__;a.__proto__=prototypeOfObject;var f=lookupGetter(a,c),e=lookupSetter(a,c);a.__proto__=d;if(f||e){if(f)b.get=f;if(e)b.set=e;return b}}b.value=a[c];return b}});Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(a){return Object.keys(a)});Object.create||(Object.create=function(a,c){var b;if(a===null)b={__proto__:null};else{if(typeof a!="object")throw new TypeError("typeof prototype["+typeof a+ 3 | "] != 'object'");b=function(){};b.prototype=a;b=new b;b.__proto__=a}c!==void 0&&Object.defineProperties(b,c);return b});if(Object.defineProperty){var g=d({}),h="undefined"==typeof document||d(document.createElement("div"));if(!g||!h)var e=Object.defineProperty}if(!Object.defineProperty||e)Object.defineProperty=function(a,c,b){if(typeof a!="object"&&typeof a!="function"||a===null)throw new TypeError("Object.defineProperty called on non-object: "+a);if(typeof b!="object"&&typeof b!="function"||b=== 4 | null)throw new TypeError("Property description must be an object: "+b);if(e)try{return e.call(Object,a,c,b)}catch(d){}if(owns(b,"value"))if(supportsAccessors&&(lookupGetter(a,c)||lookupSetter(a,c))){var f=a.__proto__;a.__proto__=prototypeOfObject;delete a[c];a[c]=b.value;a.__proto__=f}else a[c]=b.value;else{if(!supportsAccessors)throw new TypeError("getters & setters can not be defined on this javascript engine");owns(b,"get")&&defineGetter(a,c,b.get);owns(b,"set")&&defineSetter(a,c,b.set)}return a}; 5 | Object.defineProperties||(Object.defineProperties=function(a,c){for(var b in c)owns(c,b)&&b!="__proto__"&&Object.defineProperty(a,b,c[b]);return a});Object.seal||(Object.seal=function(a){return a});Object.freeze||(Object.freeze=function(a){return a});try{Object.freeze(function(){})}catch(j){var i=Object.freeze;Object.freeze=function(a){return typeof a=="function"?a:i(a)}}Object.preventExtensions||(Object.preventExtensions=function(a){return a});Object.isSealed||(Object.isSealed=function(){return false}); 6 | Object.isFrozen||(Object.isFrozen=function(){return false});Object.isExtensible||(Object.isExtensible=function(a){if(Object(a)!==a)throw new TypeError;for(var c="";owns(a,c);)c=c+"?";a[c]=true;var b=owns(a,c);delete a[c];return b})}); 7 | -------------------------------------------------------------------------------- /src/ui/feedback_widget.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var $ = require('jquery'); 5 | 6 | module.exports = flight.component(function analytics() { 7 | this.feedback = function() { 8 | return { 9 | feedback: this.$node.find('#feedback-text').val(), 10 | email: this.$node.find('#feedback-email').val() 11 | }; 12 | }; 13 | 14 | this.submitBtn = function() { 15 | return this.$node.find('.js-submit'); 16 | }; 17 | 18 | this.resetSubmitBtn = function() { 19 | this.submitBtn().html('Send'); 20 | }; 21 | 22 | this.addFeedbackToTagManager = function() { 23 | // track feedback in Google Tag Manager as a backup 24 | window.dataLayer.push({ 25 | 'eventLabel': this.feedback() 26 | }); 27 | }; 28 | 29 | this.handleSubmission = function(e) { 30 | e.preventDefault(); 31 | this.submitBtn().html('Sending...'); 32 | this.addFeedbackToTagManager(); 33 | 34 | // see google-apps-feedback-script.js 35 | $.ajax({ 36 | url: 'https://script.google.com/macros/s/AKfycbzziKocYO7ZmbLvRaSI_OEFSHTVwnCFrTfQT-OzoqAVQvpg1ZE/exec', 37 | type: 'POST', 38 | data: this.feedback(), 39 | success: this.success.bind(this), 40 | error: this.error.bind(this) 41 | }); 42 | }; 43 | 44 | this.errorAfterSuccessfulMobileSubmit = function(response) { 45 | this.success(); 46 | throw('Error function called after successful mobile submission. Response: ' + 47 | JSON.stringify(response) + 48 | ', feedback ' + this.feedback()); 49 | }; 50 | 51 | this.error = function(response) { 52 | if (response.status === 0 && response.responseText === "") { 53 | return this.errorAfterSuccessfulMobileSubmit(response); 54 | } 55 | this.resetSubmitBtn(); 56 | var error = this.$node.find('.js-error'); 57 | var body = this.feedback() + " \n\n(Error details: " + JSON.stringify(response) + ")"; 58 | error.find('.js-error-email').prop('href', 'mailto:gethelplex@lexingtonky.gov?subject=[GetHelpLex feedback]&body=' + body); 59 | error.show(); 60 | }; 61 | 62 | this.success = function() { 63 | this.resetSubmitBtn(); 64 | this.$node.find('.js-thank-you').show(); 65 | setTimeout(function() { 66 | this.$node.modal('hide'); 67 | this.$node.find('.js-thank-you').hide(); 68 | }.bind(this), 2000); 69 | }; 70 | 71 | this.handleCancel = function() { 72 | this.$node.modal('hide'); 73 | }; 74 | 75 | this.after('initialize', function() { 76 | this.on('#feedback-form', 'submit', this.handleSubmission); 77 | this.on('.btn-cancel', 'click', this.handleCancel); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/spec/ui/list_spec.js: -------------------------------------------------------------------------------- 1 | define( 2 | ['test/mock', 'infotemplates', 'jquery'], 3 | function(mock, templates, $) { 4 | 'use strict'; 5 | describeComponent('ui/list', function() { 6 | beforeEach(function() { 7 | setupComponent(); 8 | }); 9 | 10 | describe('on config', function() { 11 | it("calls teardown if there's no list configuration", function() { 12 | spyOn(this.component, 'teardown').andCallThrough(); 13 | $(document).trigger('config', {}); 14 | expect(this.component.teardown).toHaveBeenCalledWith(); 15 | }); 16 | }); 17 | 18 | describe('on data', function() { 19 | beforeEach(function() { 20 | $(document).trigger('config', mock.config); 21 | $(document).trigger('data', mock.data); 22 | waits(25); 23 | }); 24 | 25 | it('creates a list item for each feature', function() { 26 | expect(this.$node.find('li').length).toEqual( 27 | mock.data.features.length); 28 | }); 29 | 30 | it('renders the list config into the list items', function() { 31 | var $li = this.$node.find('li:eq(0)'); 32 | var feature = $li.data('feature'); 33 | expect($li.html()).toEqual( 34 | templates.popup(mock.config.list, feature.properties, feature.id)); 35 | }); 36 | 37 | it('sorts the list items by their text', function() { 38 | var texts = this.$node.find('li').map(function() { 39 | return this.innerText; 40 | }).get(); 41 | var sorted = texts.slice(); 42 | sorted = sorted.sort(); 43 | expect(texts).toEqual(sorted); 44 | }); 45 | }); 46 | 47 | describe('on item click', function() { 48 | beforeEach(function() { 49 | $(document).trigger('config', mock.config); 50 | $(document).trigger('data', mock.data); 51 | waits(25); 52 | }); 53 | it('triggers a selectFeature event', function() { 54 | var spy = spyOnEvent(document, 'selectFeature'); 55 | var $li = this.$node.find('li:eq(1)'); 56 | 57 | $li.click(); 58 | expect(spy).toHaveBeenTriggeredOnAndWith(document, 59 | $li.data('feature')); 60 | }); 61 | }); 62 | 63 | describe('on selectFeature', function() { 64 | beforeEach(function() { 65 | $(document).trigger('config', mock.config); 66 | $(document).trigger('data', mock.data); 67 | waits(25); 68 | }); 69 | 70 | it('scrolls to the selected feature', function() { 71 | var $li = this.$node.find('li:eq(1)'); 72 | var feature = $li.data('feature'); 73 | 74 | $(document).trigger('selectFeature', feature); 75 | expect(window.location.hash).toBe('#' + feature.id); 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/ui/search_results.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 'use strict'; 3 | var flight = require('flight'); 4 | var $ = require('jquery'); 5 | var _ = require('lodash'); 6 | var Handlebars = require('handlebars'); 7 | 8 | module.exports = flight.component(function searchResults() { 9 | this.defaultAttrs({ 10 | searchSelector: "#search", 11 | helpSelector: ".help", 12 | helpTemplate: "Press Enter to find '{{query}}' on the map.", 13 | suggestedSelector: ".suggested", 14 | resultContainerSelector: "ul", 15 | resultSelector: "li", 16 | resultTemplate: '
  • {{ organization_name }} ({{ address }})
  • ' 17 | }); 18 | 19 | this.showHelp = function(ev, options) { 20 | if (options.query) { 21 | this.select('helpSelector').html( 22 | Handlebars.compile(this.attr.helpTemplate)(options)); 23 | this.trigger('uiShowSearchResults'); 24 | } else { 25 | this.trigger('uiHideSearchResults'); 26 | } 27 | }; 28 | 29 | this.searchResults = function(ev, options) { 30 | var results = options.results, 31 | resultTemplate = Handlebars.compile(this.attr.resultTemplate), 32 | $container = this.select('resultContainerSelector'); 33 | 34 | $container.empty(); 35 | if (results.length) { 36 | _.each(results.slice(0, 5), function(result) { 37 | var html = resultTemplate(result.properties); 38 | $(html).data('result', result).appendTo($container); 39 | }, this); 40 | this.select('suggestedSelector').show(); 41 | $container.show(); 42 | this.trigger('uiShowSearchResults'); 43 | } else { 44 | this.select('suggestedSelector').hide(); 45 | $container.hide(); 46 | } 47 | }; 48 | 49 | this.selectedResult = function(ev) { 50 | ev.preventDefault(); 51 | var $target = $(ev.target).closest(this.attr.resultSelector), 52 | display = $target.text(), 53 | result = $target.data('result'); 54 | 55 | this.trigger('uiHideSearchResults'); 56 | this.trigger(this.attr.searchSelector, 57 | 'uiShowingSearchResult', 58 | {display: display}); 59 | this.trigger(document, 'selectFeature', result); 60 | }; 61 | 62 | this.showSearchResults = function(ev) { 63 | ev.preventDefault(); 64 | this.$node.show(); 65 | }; 66 | 67 | this.hideSearchResults = function(ev) { 68 | ev.preventDefault(); 69 | this.$node.hide(); 70 | }; 71 | 72 | this.after('initialize', function() { 73 | this.on(document, 'dataTypeaheadResults', this.searchResults); 74 | this.on(document, 'uiInProgressSearch', this.showHelp); 75 | this.on('uiShowSearchResults', this.showSearchResults); 76 | this.on('uiHideSearchResults', this.hideSearchResults); 77 | this.on('click', { 78 | resultSelector: this.selectedResult 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/templates/twelveStepPrograms.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |