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 | 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 | 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 | 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 |
4 |
Alcoholics Anonymous
5 |
To find a meeting anywhere in Kentucky:
6 | http://www.aa.org
7 |
8 |
Bluegrass Intergroup
9 |
Lexington, KY
10 | Phone/FAX: 859-225-1212
11 | www.bluegrassintergroup.org
12 |
13 |
Bowling Green Central Office
14 |
Bowling Green, KY
15 | Main: 270-782-5267
16 |
17 |
Northern Kentucky Central Office
18 |
Covington, KY
19 | Main: 859-491-7181
20 | www.home.fuse.net/nkyaa
21 |
22 |
Western Kentucky Intergroup
23 |
Gilbertsville, KY
24 | Toll Free: 800-606-6047
25 | Main: 270-362-7141
26 |
27 |
Greater Louisville Intergroup, Inc.
28 |
Louisville, KY
29 | Main: 502-582-1849
30 | www.louisvilleaa.org
31 |
32 |
Intergroup 17
33 |
Owensboro, KY
34 | Main: 270-683-0371
35 |
36 |
Narcotics Anonymous
37 |
https://www.na.org/
38 | To find a meeting anywhere in Kentucky-Kentuckiana Region:
39 | http://www.krscna.org
40 |
41 |
Pennyrile Area (Paducah & Hopkinsville)
42 |
http://www.na-pana.org
43 |
44 |
Southern Zonal Forum
45 |
http://www.szfna.org
46 |
47 |
Across the River Area (Henderson)
48 |
Phone: 877-642-5831
49 | http://www.atrana.org/
50 |
51 |
South Central Kentucky Area (Bowling Green, Glasgow, Scottsville, Franklin)
52 |
Phone: 866-901-2849
53 | http://sckana.net
54 |
55 |
Louisville Area
56 |
Phone: 502-499-4423
57 | http://nalouisville.org/
58 |
59 |
Grassroots Area (Campton, Inez, Salyersville)
60 |
Phone: 855-319-8869
61 | http://www.grassrootsna.org
62 |
63 |
Bluegrass-Appalachian Region (Central Kentucky)
64 |
Phone: 859-253-4673
65 | http://www.barcna.com
66 |
67 |
Kentucky Survivors Area (Lexington)
68 |
Phone: 859-253-4673
69 | http://www.kentuckysurvivors.com
70 |
--------------------------------------------------------------------------------
/src/templates/welcome.html:
--------------------------------------------------------------------------------
1 |
Welcome to GetHelpLex
2 |
3 |
GetHelpLex is a resource for people seeking facilities and services for substance use disorder (substance abuse/addiction) in or around Lexington, Kentucky.
4 |
5 |
6 |
7 | What Services Are You Interested In?
8 |
9 |
10 |
11 |
12 |
13 | Assessments
14 |
15 |
16 |
17 |
18 | Medical Detox
19 |
20 |
21 |
22 |
23 | Residential
24 |
25 |
26 |
27 |
28 | Outpatient
29 |
30 |
31 |
32 |
33 |
34 | Twelve Step Programs
35 |
36 |
37 |
38 |
39 |
40 | Guided Search
41 |
42 |
43 |
45 |
46 |
47 |
50 |
51 |
GetHelpLex is a tool to help you find a substance abuse treatment program for yourself or others. It's an informational tool ONLY. If you are experiencing a medical emergency, please call 911.
52 |
53 |
Please remember that it is:
54 |
55 |
56 | NOT a diagnostic tool
57 | NOT an assessment tool
58 | NOT a substitute for substance use treatment services
59 |
60 |
61 |
No identifying information is being collected from your search.
62 |
63 |
64 |
67 |
68 |
69 | The Lexington Needle Exchange Program is located at the Lexington-Fayette County Health Department
70 | (650 Newtown Pike, Lexington, KY.). The exchange is available on Wednesdays from 3:00 p.m. to 6:00 p.m.
71 | and Fridays from 11:00 a.m. to 4:00 p.m. and is anonymous.
72 |
73 |
74 | For additional information, please call 859-252-2371
75 |
76 |
77 |
78 |
81 |
82 |
83 | No cost naloxone is offered each Friday at the Lexington-Fayette County Health Department
84 | (650 Newtown Pike, Lexington, KY.) every Friday from 11:00 a.m. to 4:00 p.m. In order to receive
85 | naloxone a person must bring photo identification and complete a 10 minute class on how to use it.
86 |
87 |
88 | For additional information, please call 859-252-2371
89 |
90 |
91 | Free naloxone is subject to availability
92 |
93 |
94 |
95 |
98 |
99 |
100 | The Matthew Casey Wethington Act for Substance Abuse Intervention is named for Matthew Casey Wethington,
101 | who died in 2002 from a heroin overdose at the age of 23. Casey was an energetic young man who enjoyed
102 | life until it was “taken” by drugs. Casey never intended to become addicted to drugs when he used the first time.
103 | What he did not realize was that his using would progress from abusing to dependence and then to the disease of addiction.
104 | Although his parents tried to get him help, there was no law that could force someone into treatment because he was an
105 | adult. After Casey’s death his parents lobbied for a change. “Casey’s Law” is an involuntary treatment act for
106 | those who suffer from the disease of addiction.
107 |
108 |
109 | For more information on Casey’s Law, please visit the Kentucky Office of Drug Control Policy’s Casey Law page at the link below:
110 | https://odcp.ky.gov/Stop-Overdoses/Pages/Caseys-Law.aspx .
111 |
112 |
113 |
114 |
115 |
116 |
117 | There are many types of substance abuse treatment available.
118 | No single type of treatment is appropriate for all individuals.
119 |
120 |
121 |
122 |
123 | Assessment
124 |
125 | ⇗
126 |
127 |
128 | According to SAMHSA’s Treatment Improvement Protocol Series, No.51, assessment is a process for defining the nature of that problem, determining a diagnosis, and developing specific treatment recommendations for addressing the problem or diagnosis.
129 |
130 |
131 | Medical Detox
132 |
133 | ⇗
134 |
135 |
136 | According to the National Institute on Drug Abuse, medical detox manages the acute physical symptoms of withdrawal associated with stopping drug use.
137 |
138 |
139 | Outpatient
140 |
141 | ⇗
142 |
143 |
144 | Outpatient treatment programs provide treatment at a variety of sites, but the person lives elsewhere. Many meet in the evenings and on weekends so participants can go to school or work. Out-patient treatment programs have different requirements for attendance. Some programs require daily attendance; others meet only one to three times per week (What is Substance Abuse Treatment? A Booklet for Families, SAMHSA).
145 |
146 | Intensive outpatient program (IOP)
147 | Intensive outpatient programs typically meet 3-5 days a week for 2-4 hours a day or more. Relapse prevention is often a major focus of IOP. Typically outpatient programs are often scheduled around work or school.
148 |
149 |
150 | Residential
151 |
152 | ⇗
153 |
154 |
155 | Residential programs provide a living environment with treatment services. Several models of residential treatment (such as the therapeutic community) exist, and treatment in these programs lasts from a month to a year or more, according to SAMHSA’s, “What is Substance Abuse Treatment? A Booklet for Families.
156 |
157 | Medication Assisted Treatment
158 | The text should read: Medications are used in combination with counseling and behavioral therapies to provide a whole-patient approach to the treatment of substance use disorders (SAMHSA).
159 |
160 |
161 |
162 | For additional information in substance abuse treatment, please see the Substance Abuse and Mental Health Services Administration’s
163 |
164 | What is Substance Abuse Treatment? A Booklet for Families.
165 |
166 |
167 |
168 |
169 |
170 |
171 | LFUCG Office of Substance Abuse and Violence Intervention (SAVI) voluntarily provides the data on this website as a service to the public. While SAVI attempts to keep the web information accurate and timely, this information is made available on an “as-is” basis. Please report any revisions or corrections to
gethelplex@lexingtonky.gov or
859-258-3834 . LFUCG SAVI explicitly disclaims any representations and warranties, including but not limited to, sponsoring and/or guaranteeing the services provided by any agency or service provider on this website. If you are experiencing a medical emergency or suicidal, please call
911.
172 |
173 |
174 |
175 |
176 |
177 | To keep our information current, we verify program details once a year . But please verify treatment details with the program before making any treatment decisions.
178 |
179 |
180 |
181 | Please submit feedback or call 859-258-3834 to report troubleshooting problems with the locator. Please note, this feedback is not always monitored and should be used ONLY to report information that needs to be updated and/or revised on the website.
182 |
183 |
184 |
DO NOT send treatment, assessment or referral questions as feedback.
185 |
186 |
187 |
188 |
189 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | });
--------------------------------------------------------------------------------
/src/ui/facet.js:
--------------------------------------------------------------------------------
1 | define(function(require, exports, module) {
2 | 'use strict';
3 | // compile all the templates
4 | var flight = require('flight');
5 | var Handlebars = require('handlebars');
6 | var _ = require('lodash');
7 | var $ = require('jquery');
8 | var welcomeTemplate = require('text!templates/welcome.html');
9 | var inputTemplate = require('text!templates/input.html');
10 | var formTemplate = require('text!templates/form.html');
11 | var facetTemplate = require('text!templates/facet.html');
12 | var facetControlsTemplate = require('text!templates/facetControls.html');
13 | var extraResourcesTemplate = require('text!templates/extraResources.html');
14 | var assessmentTemplate = require('text!templates/assessment.html');
15 |
16 | var templates = {
17 | welcome: Handlebars.compile(welcomeTemplate),
18 | input: Handlebars.compile(inputTemplate),
19 | form: Handlebars.compile(formTemplate),
20 | facet: Handlebars.compile(facetTemplate),
21 | facetControls: Handlebars.compile(facetControlsTemplate),
22 | extraResources: Handlebars.compile(extraResourcesTemplate),
23 | assessment: Handlebars.compile(assessmentTemplate)
24 | };
25 |
26 | module.exports = flight.component(function () {
27 | this.configureFacets = function(ev, config) {
28 | this.facetConfig = config.facets;
29 | };
30 |
31 | // -1 is start with intro question that is not a facet
32 | this.facetOffset = -1;
33 |
34 | this.meetsFacetDependency = function(facetData, key, dependency) {
35 | return _.find(facetData, function(facets) {
36 | return _.find(facets, function(facet) {
37 | return facet.value === dependency && facet.selected;
38 | });
39 | });
40 | };
41 |
42 | this.getFacetConfig = function(key, attr) {
43 | if (this.facetConfig[key]) {
44 | return this.facetConfig[key][attr];
45 | }
46 | };
47 |
48 | this.displayFacets = function(ev, facetData) {
49 | // cache facet data so that you can call it internally instead of waiting
50 | // for event from data facet
51 | if (facetData) {
52 | this.facetData = facetData;
53 | } else {
54 | facetData = this.facetData;
55 | }
56 |
57 | // Remove search facets that are not part of the survey
58 | delete facetData.county;
59 |
60 | this.noSelectionsAvailable = false;
61 |
62 | // show first question if you're looking for a treatment facility
63 | if (this.facetOffset === -1) {
64 | this.$node.html(templates.welcome());
65 | this.on('.js-next-prev', 'click', this.nextPrevHandler);
66 | this.on('.js-no-treatment', 'click', this.showNoTreatment);
67 | this.on('.js-not-sure-treatment', 'click', this.showNoTreatment);
68 | return;
69 | }
70 |
71 | var facets = _.keys(facetData);
72 | var key = facets[this.facetOffset];
73 | if (!this.showAllFacets) {
74 | if (this.facetOffset >= facets.length) {
75 | this.$node.find('.js-offer-results[data-offer-results=true]').click();
76 | return;
77 | }
78 |
79 | // does the facet have a dependency?
80 | if (key) {
81 | var dependency = this.getFacetConfig(key, 'dependency');
82 | if (dependency) {
83 | if (!this.meetsFacetDependency(facetData, key, dependency)) {
84 | this.setFacetOffset(this.facetOffset + 1);
85 | return;
86 | }
87 | }
88 | }
89 |
90 | var newFacetData = {};
91 | newFacetData[key] = facetData[key];
92 | facetData = newFacetData;
93 | }
94 |
95 | var facet = _.chain(facetData).map(
96 | _.bind(function(values, key) {
97 | // only one facet available that has no facilities
98 | if (values.length === 1 && values[0].count === 0) {
99 | this.noSelectionsAvailable = true;
100 | }
101 | var hasSelected = _.some(values, 'selected');
102 | var configKey = this.showAllFacets ? 'title' : 'survey_title';
103 | // render a template for each facet
104 | return templates.facet({
105 | title: this.getFacetConfig(key, configKey),
106 | key: key,
107 | // render the form for each value of the facet
108 | form: templates.form({
109 | key: key,
110 | has_selected: hasSelected,
111 | inputs: _.chain(values).filter('count').map(templates.input).
112 | value()
113 | })
114 | });
115 | }, this)).value().join('');
116 |
117 | var previousOffset;
118 | if (typeof this.facetHistory === 'object') {
119 | previousOffset = this.facetHistory[this.facetHistory.length - 1] || -1;
120 | } else {
121 | previousOffset = -1;
122 | }
123 | this.$node.html(
124 | facet +
125 | templates.facetControls({
126 | showResults: this.showAllFacets,
127 | facetOffset: this.facetOffset + 1,
128 | previousFacetOffset: previousOffset
129 | })
130 | );
131 |
132 | this.on('.js-next-prev', 'click', this.nextPrevHandler);
133 | this.on('.js-offer-results', 'click', this.showResultsHandler);
134 |
135 | if (this.noSelectionsAvailable === true) {
136 | // click button to advance to the next facet.
137 | // NOTE(chaserx): I couldn't find a way to use `facetOffset` without
138 | // creating infinite loop.
139 | this.$node.find('button.btn-next').trigger('click');
140 | this.facetHistory.pop();
141 | this.noSelectionsAvailable = false;
142 | return;
143 | }
144 | };
145 |
146 | this.showResultsHandler = function(ev) {
147 | // can be true or false
148 | var offerResults = $(ev.target).data('offerResults');
149 | this.showAllFacets = offerResults;
150 | if (this.showAllFacets) {
151 | this.showResults();
152 | $(document).trigger('uiShowResults', {});
153 | } else {
154 | $('#facets').removeClass('control-sidebar');
155 | $('#facets').addClass('control-survey');
156 | $(document).trigger('uiHideResults', {});
157 | }
158 | this.displayFacets();
159 | };
160 |
161 | this.showResults = function() {
162 | $('#facets').addClass('control-sidebar');
163 | $('#facets').removeClass('control-survey');
164 | };
165 |
166 | this.nextPrevHandler = function(ev) {
167 | if (typeof this.facetHistory === 'undefined') {
168 | this.facetHistory = [];
169 | }
170 | var clickedEl = $(ev.target);
171 | var facility_type = $(ev.target).data('facility-type');
172 | var jump_to_results = $(ev.target).data('jump-to-results');
173 | var offset = parseInt(clickedEl.data('nextFacetOffset'), 10);
174 | if (clickedEl.is('.previous')) {
175 | var facet = _.keys(this.facetData)[offset];
176 | $(document).trigger('uiClearFacets', {facet: facet});
177 | this.setFacetOffset(this.facetHistory.pop());
178 | } else {
179 | if (facility_type) {
180 | $(document).trigger('uiClearFacets', {facet: 'facility_type'});
181 | $(document).trigger('uiClearFacets', {facet: 'out_patient'});
182 | $(document).trigger('uiClearFacets', {facet: 'gender'});
183 | $(document).trigger('uiClearFacets', {facet: 'pregnancy'});
184 | $(document).trigger('uiClearFacets', {facet: 'age'});
185 | $(document).trigger('uiClearFacets', {facet: 'insurance'});
186 | $(document).trigger('uiFilterFacet', {
187 | facet: 'facility_type',
188 | selected: [facility_type]
189 | });
190 | if (facility_type == 'outpatient_offered') {
191 | // outpatient offered settings appear to be injected based on selecting things in the first panel
192 | // so we'll give it 1/10 of a second to do what it needs do and then trigger another jump
193 | var that=this;
194 | window.setTimeout((function(){that.setFacetOffsetWithObject(that)}),100);
195 | }
196 | }
197 | var lastItem = this.facetHistory[this.facetHistory.length - 1];
198 | if (lastItem !== this.facetOffset) {
199 | this.facetHistory.push(this.facetOffset);
200 | }
201 | this.setFacetOffset(offset);
202 | if (jump_to_results) {
203 | $(document).trigger('uiShowResults', {});
204 | }
205 | }
206 | };
207 |
208 | this.setFacetOffset = function(offset) {
209 | this.facetOffset = offset;
210 | this.displayFacets();
211 | };
212 |
213 | this.setFacetOffsetWithObject = function(obj,offset) { obj.setFacetOffset(1); }
214 |
215 | this.clearFacets = function(ev) {
216 | ev.preventDefault();
217 | var facet = $(ev.target).data('facet');
218 | $(document).trigger('uiClearFacets', {facet: facet});
219 | };
220 |
221 | this.selectFacet = function(ev) {
222 | var $form = $(ev.target).parents('form'),
223 | facet = $form.data('facet'),
224 | selected = _.map($form.serializeArray(),
225 | 'name');
226 | window.setTimeout(function() {
227 | $(document).trigger('uiFilterFacet', {
228 | facet: facet,
229 | selected: selected
230 | });
231 | }, 0);
232 | };
233 |
234 | this.showNoTreatment = function() {
235 | this.$node.html(templates.assessment());
236 | };
237 |
238 | // defaultAttrs is now deprecated in favor of 'attributes', but our
239 | // version of flight still uses this.
240 | this.defaultAttrs({
241 | clearFacetsSelector : ".clear-facets"
242 | });
243 |
244 | this.after('initialize', function() {
245 | this.on('change', this.selectFacet);
246 | this.on(document, 'config', this.configureFacets);
247 | this.on(document, 'dataFacets', this.displayFacets);
248 | this.on(document, 'uiShowResults', this.showResults);
249 | this.on(document, 'uiFacetChangeRequest', function(ev, facet) {
250 | var input = $('input[name=' + facet.name + ']');
251 | input.prop('checked', true);
252 | input.trigger('change');
253 | });
254 | this.on('click', { clearFacetsSelector : this.clearFacets });
255 | });
256 | });
257 | });
258 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/ui/info.js:
--------------------------------------------------------------------------------
1 | define(function(require, exports, module) {
2 | 'use strict';
3 | var flight = require('flight');
4 | var $ = require('jquery');
5 | var templates = require('infotemplates');
6 | module.exports = flight.component(function info() {
7 | this.defaultAttrs({
8 | "contentClass": "content",
9 | "closeSelector": ".close"
10 | });
11 |
12 | this.configureInfo = function(ev, config) {
13 | this.infoConfig = config.properties;
14 | };
15 |
16 | this.update = function(ev, feature) {
17 | if (!feature) {
18 | return;
19 | }
20 | this.attr.currentFeature = feature;
21 | var popup = templates.popup(this.infoConfig,
22 | feature.properties);
23 | var content = this.$node.find("div." + this.attr.contentClass);
24 | if (!content.length) {
25 | content = $("
").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 |
--------------------------------------------------------------------------------
/src/ui/list.js:
--------------------------------------------------------------------------------
1 | define(function(require, exports, module) {
2 | 'use strict';
3 | var flight = require('flight');
4 | var _ = require('lodash');
5 | var $ = require('jquery');
6 | var templates = require('infotemplates');
7 | var timedWithObject = require('timed_with_object');
8 |
9 | module.exports = flight.component(function list() {
10 | this.defaultAttrs({
11 | listItemSelector: 'li'
12 | });
13 |
14 | function $elementForFeature(feature) {
15 | var liArray = this.$node.find('li').filter(function() {
16 | var nodeFeature = $(this).data('feature');
17 | if (!nodeFeature) {
18 | return false;
19 | }
20 | return _.isEqual(nodeFeature, feature);
21 | });
22 | return $(liArray[0]);
23 | }
24 |
25 | function compareListItems(a, b) {
26 | a = a._text;
27 | b = b._text;
28 | if (a < b) {
29 | return -1;
30 | } else if (a > b) {
31 | return 1;
32 | } else {
33 | return 0;
34 | }
35 | }
36 |
37 | this.configureList = function(ev, config) {
38 | var listConfig = config.list;
39 | if (!listConfig) {
40 | this.teardown();
41 | return;
42 | }
43 | this.trigger('listStarted', {});
44 | this.render = _.partial(templates.popup, config.list);
45 | this.renderFull = _.partial(templates.popup, config.properties);
46 | };
47 |
48 | this.loadData = function(ev, data) {
49 | var $ul = this.$node.empty().html('
').find('ul');
50 | timedWithObject(
51 | data.features,
52 | function(feature, l) {
53 | var $li = $("
").html(this.render(feature.properties, feature.id))
54 | .addClass('item')
55 | .data('feature', feature);
56 | $li._text = $li.text();
57 | l.push($li);
58 | return l;
59 | },
60 | [],
61 | this).then(function(l) {
62 | l.sort(compareListItems);
63 | $ul.append(l);
64 | this.trigger('listFinished', {});
65 | }.bind(this));
66 | };
67 |
68 | this.filterData = function(ev, data) {
69 | this.trigger('listFilteringStarted', {});
70 | this.$node.find('li.item').hide().filter(function() {
71 | var $li = $(this);
72 | return _.contains(data.featureIds, $li.data('feature').id);
73 | }).show();
74 | this.trigger('listFinished', {});
75 | };
76 |
77 | this.onFeatureClick = function onFeatureClick(ev) {
78 | var $li = $(ev.target).closest('li.item');
79 | var feature = $li.data('feature');
80 | this.selectedLi = $li;
81 | this.trigger('selectFeature', feature);
82 | };
83 |
84 | this.onFeatureSelected = function onFeatureSelected(ev, feature) {
85 | var $selectedItem = $elementForFeature.call(this, feature);
86 | var propsWithTitles = this.addFacetTitles(feature.properties, this.facetTitles);
87 |
88 | // set url to blah.com#finda-17
89 | window.location.hash = feature.id;
90 | $selectedItem.html(this.renderFull(propsWithTitles, feature.id));
91 |
92 | // does not clear previous selections so they remain findable later
93 | $selectedItem.addClass('selected-facility');
94 | };
95 |
96 | this.addFacetTitles = function(featureProperties, facetTitles) {
97 | var propsWithTitles = _.clone(featureProperties);
98 | // can probably use map rather than each
99 | _.each(propsWithTitles, function(values, key) {
100 | if (facetTitles && _.isArray(values)) {
101 | propsWithTitles[key] = _.map(values, function(value) {
102 | return facetTitles[value];
103 | }.bind(this));
104 | }
105 | }.bind(this));
106 | return propsWithTitles;
107 | };
108 |
109 | this.after('initialize', function() {
110 | this.on(document, 'config', this.configureList);
111 | this.on(document, 'data', this.loadData);
112 | this.on(document, 'dataFiltered', this.filterData);
113 | this.on(document, 'selectFeature', this.onFeatureSelected);
114 | this.on('click', {
115 | listItemSelector: this.onFeatureClick
116 | });
117 | this.on(document, 'uiHideResults', function() {
118 | this.$node.hide();
119 | });
120 | this.on(document, 'facetTitles', function(ev, facetTitles) {
121 | this.facetTitles = facetTitles;
122 | });
123 | });
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/ui/map.js:
--------------------------------------------------------------------------------
1 | define(function(require, exports, module) {
2 | 'use strict';
3 | var flight = require('flight');
4 | var L = require('leaflet');
5 | var _ = require('lodash');
6 | var timedWithObject = require('timed_with_object');
7 |
8 | require('L.Control.Locate');
9 | require('leaflet.markercluster');
10 |
11 | module.exports = flight.component(function map() {
12 | this.attributes({
13 | tileUrl: 'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
14 | tileAttribution: '©
OpenStreetMap ©
CartoDB ',
15 | tileSubdomains: 'abcd',
16 | tileMinZoom: 2,
17 | tileMaxZoom: 19
18 | });
19 |
20 | this.defineIconStyles = function() {
21 | // define icon styles
22 | var path = L.Icon.Default.imagePath;
23 | if (!path) {
24 | path = L.Icon.Default.imagePath = 'lib/leaflet/images';
25 | }
26 | this.grayIcon = L.icon({
27 | iconUrl: path + '/marker-icon-gray.png',
28 | shadowUrl: path + '/marker-shadow.png'
29 | });
30 |
31 | this.defaultIcon = L.icon({
32 | iconUrl: path + '/marker-icon.png',
33 | shadowUrl: path + '/marker-shadow.png'
34 | });
35 | };
36 |
37 | this.configureMap = function(ev, config) {
38 | this.trigger('mapStarted', {});
39 | // if list or facets are enabled, give the map less space
40 | var addition = 0;
41 | if (config.facets) {
42 | addition += 300;
43 | }
44 | if (config.list) {
45 | addition += 300;
46 | }
47 |
48 | if (addition > 0) {
49 | window.setTimeout(function() {
50 | if (this.map) {
51 | this.$node.css('left', '+=' + addition);
52 | this.map.invalidateSize();
53 | }
54 | }.bind(this), 50);
55 | }
56 |
57 | var mapConfig = config.map;
58 |
59 | if (mapConfig.maxZoom){
60 | this.map.options.maxZoom = mapConfig.maxZoom;
61 | this.cluster.options.disableClusteringAtZoom = 1;
62 | this.cluster._maxZoom = mapConfig.maxZoom - 1;
63 | }
64 | if (mapConfig.maxBounds){
65 | this.map.setMaxBounds(mapConfig.maxBounds);
66 | }
67 |
68 | // set feature attribute to be used as preview text to config
69 | this.featurePreviewAttr = config.map.preview_attribute;
70 |
71 | // setup the center after we're done moving around
72 | this.map.setView(mapConfig.center, mapConfig.zoom);
73 | };
74 |
75 | this.loadData = function(ev, data) {
76 | this.defineIconStyles();
77 |
78 | var setupFeature = function(feature, layer) {
79 | this.layers[feature.id] = layer;
80 |
81 | // bind popup to feature with specified preview attribute
82 | this.bindPopupToFeature(
83 | layer,
84 | feature.properties[this.featurePreviewAttr]);
85 |
86 | layer.on({
87 | click: this.emitClick.bind(this),
88 | mouseover: this.emitHover.bind(this),
89 | mouseout: this.clearHover.bind(this)
90 | });
91 | }.bind(this);
92 |
93 | this.layers = {};
94 |
95 | var geojson = L.geoJson(data, {onEachFeature: setupFeature});
96 | geojson.addTo(this.cluster);
97 | };
98 |
99 | this.filterData = function(e, data) {
100 | var object = {
101 | keepLayers: [],
102 | addLayers: [],
103 | removeLayers: []
104 | };
105 | this.trigger('mapFilteringStarted', {});
106 | timedWithObject(
107 | _.pairs(this.layers),
108 | function(pair, object) {
109 | var featureId = pair[0],
110 | layer = pair[1],
111 | selected = _.contains(data.featureIds, featureId),
112 | hasLayer = this.cluster.hasLayer(layer);
113 | if (selected) {
114 | object.keepLayers.push(layer);
115 | }
116 | if (hasLayer && !selected) {
117 | object.removeLayers.push(layer);
118 | } else if (!hasLayer && selected) {
119 | object.addLayers.push(layer);
120 | }
121 | return object;
122 | },
123 | object,
124 | this).then(function(object) {
125 | // rough level at which it's faster to remove all the layers and just
126 | // add the ones we want
127 | if (object.removeLayers.length > 1000) {
128 | this.cluster.clearLayers();
129 | this.cluster.addLayers(object.keepLayers);
130 | } else {
131 | if (object.removeLayers.length) {
132 | this.cluster.removeLayers(object.removeLayers);
133 | }
134 | if (object.addLayers.length) {
135 | this.cluster.addLayers(object.addLayers);
136 | } else {
137 | // add layers will trigger mapFinished, but if we don't add any
138 | // layers then we'll need to do it manually
139 | this.trigger('mapFinished', {});
140 | }
141 | }
142 | }.bind(this));
143 | };
144 |
145 | this.emitClick = function(e) {
146 | this.trigger(document, 'selectFeature', e.target.feature);
147 | };
148 |
149 | this.emitHover = function(e) {
150 | this.trigger(document, 'hoverFeature', e.target.feature);
151 | };
152 |
153 | this.clearHover = function(e) {
154 | this.trigger(document, 'clearHoverFeature', e.target.feature);
155 | };
156 |
157 | this.selectFeature = function(ev, feature) {
158 | if (this.previouslyClicked) {
159 | this.previouslyClicked.setIcon(this.defaultIcon);
160 | this.trigger(document, 'deselectFeature', this.currentFeature);
161 | }
162 | if (feature) {
163 | this.currentFeature = feature;
164 | var layer = this.layers[feature.id];
165 | layer.setIcon(this.grayIcon);
166 | this.previouslyClicked = layer;
167 |
168 | // re-bind popup to feature with specified preview attribute
169 | this.bindPopupToFeature(
170 | layer,
171 | feature.properties[this.featurePreviewAttr]);
172 |
173 | this.trigger('panTo', {lng: feature.geometry.coordinates[0],
174 | lat: feature.geometry.coordinates[1]});
175 | } else {
176 | this.previouslyClicked = null;
177 | }
178 | };
179 |
180 | this.deselectFeature = function(ev, feature) {
181 | if (this.previouslyClicked) {
182 | this.previouslyClicked.setIcon(this.defaultIcon);
183 | }
184 | var layer = this.layers[feature.id];
185 | // re-bind popup to feature with specified preview attribute
186 | this.bindPopupToFeature(
187 | layer,
188 | feature.properties[this.featurePreviewAttr]);
189 | this.previouslyClicked = null;
190 | };
191 |
192 | this.bindPopupToFeature = function(layer, feature){
193 | layer.bindPopup(
194 | feature,
195 | {
196 | closeButton: false,
197 | offset: L.point(0, -40)
198 | });
199 | };
200 |
201 | this.hoverFeature = function(ev, feature) {
202 | if (feature) {
203 | var layer = this.layers[feature.id];
204 | layer.openPopup();
205 | }
206 | };
207 |
208 | this.clearHoverFeature = function(ev, feature) {
209 | if (feature) {
210 | var layer = this.layers[feature.id];
211 | layer.closePopup();
212 | }
213 | };
214 |
215 | this.panTo = function(ev, latlng) {
216 | this.map.panTo(latlng);
217 | };
218 |
219 | this.onSearchResult = function(ev, result) {
220 | if (!this.searchMarker) {
221 | this.searchMarker = L.marker(result, {
222 | icon: L.divIcon({className: 'search-result-marker'})
223 | });
224 | this.searchMarker.addTo(this.map);
225 | } else {
226 | this.searchMarker.setLatLng(result);
227 | }
228 |
229 | this.trigger('panTo',
230 | {lat: result.lat,
231 | lng: result.lng});
232 | };
233 |
234 | this.onBoundsChanged = function onBoundsChanged(e) {
235 | var map = e.target;
236 | var currentBounds = map.getBounds(),
237 | southWest = currentBounds.getSouthWest(),
238 | northEast = currentBounds.getNorthEast();
239 | this.trigger('mapBounds', {
240 | southWest: [southWest.lat, southWest.lng],
241 | northEast: [northEast.lat, northEast.lng]
242 | });
243 | };
244 |
245 | this.after('initialize', function() {
246 | this.on(document, 'uiHideResults', function() {
247 | this.$node.hide();
248 | });
249 |
250 | this.on(document, 'uiShowResults', function() {
251 | this.map._onResize();
252 | this.$node.show();
253 | });
254 |
255 | this.map = L.map(this.node, {});
256 |
257 | this.cluster = new L.MarkerClusterGroup({
258 | chunkedLoading: true,
259 | chunkProgress: function(processed, total) {
260 | if (processed === total) {
261 | this.trigger('mapFinished', {});
262 | }
263 | }.bind(this)
264 | });
265 | this.cluster.addTo(this.map);
266 |
267 | L.control.scale().addTo(this.map);
268 | // Add the location control which will zoom to current
269 | // location
270 | L.control.locate().addTo(this.map);
271 |
272 |
273 | this.layers = {};
274 |
275 | L.tileLayer(this.attr.tileUrl, {
276 | attribution: this.attr.tileAttribution,
277 | subdomains: this.attr.tileSubdomains,
278 | minZoom: this.attr.tileMinZoom,
279 | maxZoom: this.attr.tileMaxZoom
280 | }).addTo(this.map);
281 |
282 | this.map.on('moveend', this.onBoundsChanged.bind(this));
283 |
284 | this.on(document, 'config', this.configureMap);
285 | this.on(document, 'data', this.loadData);
286 | this.on(document, 'dataFiltered', this.filterData);
287 |
288 | this.on(document, 'selectFeature', this.selectFeature);
289 | this.on(document, 'deselectFeature', this.deselectFeature);
290 | this.on(document, 'hoverFeature', this.hoverFeature);
291 | this.on(document, 'clearHoverFeature', this.clearHoverFeature);
292 | this.on(document, 'dataSearchResult', this.onSearchResult);
293 | this.on('panTo', this.panTo);
294 | });
295 |
296 | this.before('teardown', function() {
297 | if (this.map) {
298 | this.map.remove();
299 | this.map = undefined;
300 | }
301 | });
302 | });
303 | });
304 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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($("
").html(county).attr('value', county));
20 | }.bind(this));
21 | };
22 |
23 | this.onCountySelected = function onCountySelected() {
24 | var county = $(this.$node).val();
25 | var selected = [];
26 | if (county) {
27 | selected.push(county);
28 | }
29 | $(document).trigger('uiFilterFacet', {
30 | facet: "county",
31 | selected: selected
32 | });
33 | };
34 |
35 | this.after('initialize', function() {
36 | this.on(document, 'data', this.loadData);
37 | this.on('change', this.onCountySelected);
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/styles/images/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openlexington/gethelplex/8a6d101d3ed36fd481b4bb7fbd276a9518439bbe/styles/images/loader.gif
--------------------------------------------------------------------------------
/styles/images/search-icon-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openlexington/gethelplex/8a6d101d3ed36fd481b4bb7fbd276a9518439bbe/styles/images/search-icon-mobile.png
--------------------------------------------------------------------------------
/styles/images/search-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openlexington/gethelplex/8a6d101d3ed36fd481b4bb7fbd276a9518439bbe/styles/images/search-icon.png
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/styles/style.css:
--------------------------------------------------------------------------------
1 | @import url("../lib/bootstrap.min.css");
2 | @import url("../lib/leaflet/leaflet.css");
3 | @import url("../lib/leaflet/L.Control.Locate.css");
4 | @import url("../lib/leaflet.markercluster/MarkerCluster.css");
5 | @import url("../lib/leaflet.markercluster/MarkerCluster.Default.css");
6 |
7 | a {
8 | color: #1262b3; /* #428bca */
9 | }
10 |
11 | html {
12 | height: 100%;
13 | }
14 |
15 | body {
16 | height: 100%;
17 | margin: 0;
18 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
19 | font-weight: 300;
20 | display: flex;
21 | flex-direction: column;
22 | overflow: hidden;
23 | color: #111;
24 | }
25 |
26 | body > div.container {
27 | display: flex;
28 | flex-direction: row;
29 | position: relative;
30 | width: 100%;
31 | height: 100%;
32 | margin: 0;
33 | padding: 0;
34 | justify-content: center;
35 | background-color: #f6f6f6;
36 | }
37 |
38 | .left-column, .right-column {
39 | width: 100%;
40 | height: 100%;
41 | overflow: hidden;
42 | }
43 |
44 | .left-column {
45 | width: 600px;
46 | display: flex;
47 | flex-direction: column;
48 | }
49 |
50 | .right-column {
51 | border-left: 1px solid #aaa;
52 | }
53 |
54 | .navbar {
55 | background: #e9e9e9; /*This should be the brand primary color*/
56 | border-bottom: 2px solid #25AAE1; /*This should be the brand accent color*/
57 | margin-bottom: 0;
58 | }
59 |
60 | .navbar .navbar-header {
61 | margin-top: 13px;
62 | margin-left: 0;
63 | }
64 |
65 | .navbar .navbar-header .navbar-toggle {
66 | margin-top: 0px;
67 | }
68 |
69 | .navbar .navbar-brand {
70 | float: none;
71 | vertical-align: middle;
72 | font-weight: 300;
73 | font-size: 27px;
74 | color: #25AAE1;
75 | }
76 |
77 | /* Extra top padding for nav menu - Not Small devices (tablets, 768px and up) */
78 | @media (min-width: 768px) {
79 | #finda-navbar-collapse {
80 | padding-top: 4px;
81 | }
82 | }
83 |
84 | /* Extra bottom margin for nav menu - Small devices (phones, 768px and lower) */
85 | @media (max-width: 767px) {
86 | #finda-navbar-collapse {
87 | margin-bottom: 30px;
88 | }
89 | }
90 |
91 | .navbar-form .btn {
92 | border: none;
93 | outline: none;
94 | background-color: #25AAE1; /*This should be the brand accent color*/
95 | color: #fff;
96 | font-weight: 400;
97 | vertical-align: middle;
98 | }
99 |
100 | .navbar-form .btn:hover {
101 | box-shadow: none;
102 | outline: none;
103 | border: none;
104 | background-color: #4F4175; /*This should be a 25% darken of the brand accent color*/
105 | }
106 |
107 | .navbar-form .btn:active {
108 | box-shadow: none;
109 | outline: none;
110 | border: none;
111 | background-color: #342B4E; /*This should be a 50% darken of the brand accent color*/
112 | }
113 |
114 | .js-offer-results {
115 | padding-left: 1em;
116 | }
117 |
118 | #search-results {
119 | left: 142px;
120 | }
121 |
122 | #search-results .help {
123 | padding: 3px 20px;
124 | }
125 |
126 | #search-results .suggested {
127 | border-top: 1px solid #eee;
128 | color: #ccc;
129 | padding: 6px 20px 3px;
130 | margin-bottom: 0;
131 | }
132 |
133 | #search-results ul {
134 | padding: 0
135 | }
136 |
137 | #search-results li {
138 | padding: 3px 20px;
139 | cursor: pointer;
140 | list-style: none;
141 | }
142 |
143 | #map {
144 | z-index: 1;
145 | }
146 |
147 | .control {
148 | z-index: 2;
149 | font: 14px/16px;
150 | background: #fff;
151 | display: none;
152 | }
153 |
154 | .leaflet-control-locate {
155 | border: 1px solid rgba(0,0,0,0.4)
156 | }
157 |
158 | .leaflet-control-locate a {
159 | background-color: #fff;
160 | background-position: -3px, -2px;
161 | }
162 |
163 | .leaflet-control-locate.active a {
164 | background-position: -33px -2px
165 | }
166 |
167 | .control h4 {
168 | margin: 10px 0;
169 | color: #777;
170 | }
171 |
172 | .search-result-marker {
173 | height: 13px;
174 | width: 13px;
175 | border-radius: 50%;
176 | background-color: #69579c; /*This should be the brand accent color*/
177 | }
178 |
179 | #info {
180 | position: absolute;
181 | border: 1px solid #AFAFAF;
182 | padding: 10px 14px;
183 | height: auto;
184 | width: 275px;
185 | top: 15px;
186 | right: 15px;
187 | overflow-y: auto;
188 | box-shadow: 0 3px 5px rgba(0,0,0,.25);
189 | border-radius: 5px;
190 | }
191 |
192 | .control-sidebar {
193 | float: left;
194 | box-sizing: border-box;
195 | width: 100%;
196 | height: 100%;
197 | overflow-y: auto;
198 | }
199 |
200 | #finda-tabs {
201 | margin-top: 10px;
202 | }
203 |
204 | .tab-content {
205 | height: 100%;
206 | display: flex;
207 | }
208 |
209 | #facets {
210 | padding: 20px;
211 | }
212 |
213 | #facets .checkbox.selected label {
214 | font-weight: bold
215 | }
216 |
217 | #facets form.facet-form {
218 | display: inline-block;
219 | }
220 |
221 | #facets .clear-facets {
222 | display: inline-block;
223 | font-style: italic;
224 | margin: 10px 0 0 3em;
225 | vertical-align: top;
226 | }
227 |
228 | #facets .clear-facets:hover {
229 | color: red; /* TODO: red is probably a horrible color. suggestions? */;;
230 | cursor: pointer;
231 | }
232 |
233 | #listTab.active {
234 | display: flex;
235 | flex-direction: column;
236 | }
237 |
238 | #listTab .list-filters {
239 | display: flex;
240 | flex-direction: row;
241 | padding: 20px 10px;
242 | padding-bottom: 20px;
243 | border-bottom: 1px solid #aaa;
244 | }
245 |
246 | .list-filters .btn-print {
247 | height: 100%;
248 | margin-left: 10px;
249 | }
250 |
251 | #list {
252 | height: 100%;
253 | overflow-y: scroll;
254 | }
255 |
256 | #list .item {
257 | cursor: pointer;
258 | padding: 10px;
259 | border-bottom: 1px solid #aaa;
260 | }
261 |
262 | #list ul {
263 | padding-bottom: 20px;
264 | padding-left: 0;
265 | }
266 |
267 | #list ul ul {
268 | margin: 0 0 0 3em;
269 | padding-bottom: 0;
270 | }
271 |
272 | #list ul li.item > div > div h4 {
273 | margin: 0 0 5px 0;
274 | color: #666;
275 | font-weight: 600;
276 | font-size: 1em;
277 | }
278 |
279 | #list ul li.item > div > div + div {
280 | border-bottom: 1px solid #dedede;
281 | padding: 0.5em 0.3em;
282 | margin-top: 0;
283 | }
284 |
285 | #list ul li.item > div > div + div + div {
286 | border-top: 1px solid #fff;
287 | }
288 |
289 | #list ul li.item > div > div:last-child {
290 | border-bottom: none;
291 | }
292 |
293 | #list ul li.item:nth-child(even) {
294 | background-color: #F1F9FF;
295 | }
296 |
297 | #list.control>ul>li {
298 | border-top: 1px solid #fff;
299 | border-bottom: 1px solid #dedede;
300 | padding: 1em;
301 | cursor: pointer;
302 | border-left: 5px solid #fcfcfc;
303 | list-style: none;
304 | }
305 |
306 | #list .feature-gender ul li,
307 | #list .feature-age ul li {
308 | display: inline;
309 | }
310 |
311 | #list .feature-gender ul,
312 | #list .feature-age ul {
313 | margin-left: 0;
314 | }
315 |
316 | #list .feature-gender ul li + li:before,
317 | #list .feature-age ul li + li:before {
318 | content: "\b7\a0";
319 | }
320 |
321 | /* clear the navbar when anchored to in url */
322 | #list .selected-facility {
323 | border-right: 20px solid #69579c;
324 | }
325 |
326 | #about .modal-footer img {
327 | height: 2.5em;
328 | float: left;
329 | }
330 |
331 | #loading h4 {
332 | text-align: center
333 | }
334 |
335 | .control-survey {
336 | height: 100%;
337 | border-right: 1px solid #aaa;
338 | border-left: 1px solid #aaa;
339 | overflow-y: auto;
340 | width: 100%;
341 | }
342 |
343 | .control-survey .btn {
344 | margin-bottom: 5px
345 | }
346 |
347 | .survey-tabs {
348 | margin: auto;
349 | }
350 |
351 | .nav-tabs {
352 | position: relative;
353 | z-index: 1;
354 | border-bottom: 1px solid #aaa;
355 | }
356 |
357 | .nav-tabs.control-tabs>li.active>a {
358 | background-color: #fff;
359 | border-top: 1px solid #aaa;
360 | border-left: 1px solid #aaa;
361 | border-right: 1px solid #aaa;
362 | }
363 |
364 | .treatment-definition {
365 | margin-top: 1rem;
366 | margin-bottom: 1rem;
367 | }
368 |
369 | .grey-font {
370 | color: #6d6e71;
371 | }
372 |
373 | .mainline {
374 | font-weight: 700 !important;
375 | }
376 |
377 | .tagline {
378 | font-size: 70%;
379 | font-style: italic;
380 | }
381 |
382 | /* proudly borrowed from https://github.com/luckyshot/ga-feedback */
383 | #gaf-button {
384 | position: fixed;
385 | bottom: 0;
386 | right: 1em;
387 | background: rgba(61, 194, 85, 0.8);
388 | color: #fff;
389 | padding: 4px 7px;
390 | font-size: 1.3em;
391 | border-top-left-radius: 5px;
392 | border-top-right-radius: 5px;
393 | z-index: 999999999;
394 | }
395 |
396 | #back-to-top {
397 | text-decoration: none;
398 | position: fixed;
399 | left: 0;
400 | bottom: 0;
401 | display: none;
402 | background: rgba(37, 170, 225, 0.8);
403 | color: #fff;
404 | padding: 4px 7px;
405 | font-size: 1.3em;
406 | border-top-left-radius: 5px;
407 | border-top-right-radius: 5px;
408 | z-index: 999999999;
409 | }
410 |
411 | #feedback-form textarea {
412 | margin-bottom: 20px;
413 | }
414 |
415 | .well {
416 | text-align: center;
417 | }
418 |
419 | .welcome-options {
420 | padding: 0;
421 | list-style-type: none;
422 | margin-right: auto;
423 | margin-left: auto;
424 | }
425 |
426 | /* was 768 - but made it smaller to allow some display on smaller screens */
427 | @media only screen and (max-width : 498px) {
428 | body > div.container {
429 | flex-direction: column;
430 | }
431 |
432 | .left-column {
433 | width: 100%;
434 | }
435 |
436 | .right-column {
437 | display: none !important;
438 | }
439 | }
440 |
441 | @media print {
442 | body.modal-open {
443 | visibility: hidden;
444 | }
445 |
446 | body.modal-open .modal .modal-header,
447 | body.modal-open .modal .modal-body {
448 | visibility: visible;
449 | }
450 |
451 | body.modal-open .modal .modal-header::before, .list-filters::before {
452 | content: "GetHelpLex - gethelplex.org \A";
453 | white-space: pre;
454 | font-weight: 900;
455 | }
456 |
457 | body > div.container {
458 | top: 0;
459 | }
460 |
461 | body, .left-column, #info, .control-sidebar, #list, .control-survey {
462 | overflow: visible !important;
463 | }
464 |
465 | #list {
466 | width: 100%;
467 | border: 0;
468 | }
469 |
470 | #list > ul > li {
471 | border: 0;
472 | padding: 0;
473 | list-style: none !important;
474 | }
475 |
476 | #list ul li.item > div > div + div, #list ul li.item > div > div + div + div {
477 | border: 0;
478 | }
479 |
480 | .right-column, #map, #finda-tabs, #back-to-top, #gaf-button {
481 | display: none !important;
482 | }
483 |
484 | #select_county {
485 | display: none;
486 | }
487 |
488 | .btn-print {
489 | display: none;
490 | }
491 | }
492 |
493 | .btn-service-title {
494 | height:20px;
495 | width:20px;
496 | color:#1262b3;
497 | margin:0px;
498 | margin-left:10px;
499 | margin-bottom:4px;
500 | padding:0px;
501 | padding-bottom:2px;
502 | font-size:18px;
503 | line-height:0px;
504 | }
505 |
506 | .btn-print {
507 | min-height: 34px;
508 | }
509 |
510 | .print-icon {
511 | height:14px;
512 | opacity:0.7;
513 | margin-top:-4px;
514 | }
515 |
--------------------------------------------------------------------------------
/test/mock.js:
--------------------------------------------------------------------------------
1 | define({
2 | "config": {
3 | "project": {
4 | "name": "Finda",
5 | "description": "
Finda is a generic \"find-a\" app for geographic datasets.
",
6 | "contact": "
Please send feedback, ideas, and bug reports! "
7 | },
8 | "map": {
9 | "center":[42.3725, -71.1266],
10 | "zoom":13,
11 | "maxZoom":16,
12 | "maxBounds":[
13 | [39.2, -78.0],
14 | [44.5, -65.0]
15 | ]
16 | },
17 | "properties":[
18 | "organization_name",
19 | "address",
20 | {"name": "address", "title": "directions", "directions": true },
21 | {"name": "web_url", "title": "website", "url": true },
22 | {"name": "contact_names", "title": "Contact Information" },
23 | "contact_emails",
24 | "phone_numbers",
25 |
26 | {"name": "services_offered", "title": "Services" },
27 | {"name": "youth_category", "title": "Type of Organization" },
28 | {"name": "target_populations", "title": "Populations Served" },
29 |
30 | {"name": "additional_notes", "title": "Information"}
31 | ],
32 | "list": [
33 | "organization_name",
34 | "address"
35 | ],
36 | "search": {
37 | "geosearch": true
38 | },
39 | "facets": {
40 | services_offered: {
41 | title: "Services",
42 | survey_title: "What kind of services do you want?",
43 | type: "list"
44 | }
45 | }
46 | },
47 |
48 | "data": {
49 | "type": "FeatureCollection",
50 | "features": [
51 | {
52 | "id": 'finda-1',
53 | "type": "Feature",
54 | "geometry": {
55 | "type": "Point",
56 | "coordinates": [
57 | -72.6411923,
58 | 42.3250492
59 | ]
60 | },
61 | "properties": {
62 | "address": "2 Conz Street\nMaplewood Shops #34\nFranklin, MA 01060",
63 | "organization_name": "Generation Q South: Community Action Youth Programs",
64 | "community": "Northampton",
65 | "services_offered": [
66 | "support group",
67 | "social group"
68 | ],
69 | "web_url": "http://www.communityaction.us/our-groups-programs.html",
70 | "phone_numbers": [
71 | "413-774-7028"
72 | ],
73 | "contact_names": [
74 | "GenQ@communityaction.us"
75 | ],
76 | "contact_emails": [],
77 | "youth_category": "Community Group",
78 | "service_class_level_1": "Support Services",
79 | "service_class_level_2": "Para-professional Counseling, Therapy, and Support",
80 | "service_classes": [
81 | "Clubhouse"
82 | ],
83 | "target_populations": [
84 | "LGBTQ youth 12-21"
85 | ],
86 | "age_range": "",
87 | "additional_notes": []
88 | }
89 | },
90 | {
91 | "id": 'finda-2',
92 | "type": "Feature",
93 | "geometry": {
94 | "type": "Point",
95 | "coordinates": [
96 | -72.59839699999999,
97 | 42.593369
98 | ]
99 | },
100 | "properties": {
101 | "address": "154 Federal Street\nFranklin, MA 01301",
102 | "organization_name": "Generation Q North: Community Action Youth Programs",
103 | "community": "Greenfield",
104 | "services_offered": [
105 | "support group",
106 | "social group"
107 | ],
108 | "web_url": "http://www.communityaction.us/our-groups-programs.html",
109 | "phone_numbers": [
110 | "413-774-7028"
111 | ],
112 | "contact_names": [
113 | "GenQ@communityaction.us"
114 | ],
115 | "contact_emails": [],
116 | "youth_category": "Community Group",
117 | "service_class_level_1": "Support Services",
118 | "service_class_level_2": "Para-professional Counseling, Therapy, and Support",
119 | "service_classes": [
120 | "Clubhouse"
121 | ],
122 | "target_populations": [],
123 | "age_range": "",
124 | "additional_notes": []
125 | }
126 | },
127 | {
128 | "id": 'finda-3',
129 | "type": "Feature",
130 | "geometry": {
131 | "type": "Point",
132 | "coordinates": [
133 | -72.73988969999999,
134 | 42.5964078
135 | ]
136 | },
137 | "properties": {
138 | "address": "53 Elm Street\nFranklin, MA 01370",
139 | "organization_name": "PFLAG Franklin-Hampshire",
140 | "community": "Shellbourne Falls",
141 | "services_offered": [
142 | "support group",
143 | "public education"
144 | ],
145 | "web_url": "http://community.pflag.org/page.aspx?pid=803",
146 | "phone_numbers": [
147 | "(413)-625-6636"
148 | ],
149 | "contact_names": [
150 | "jcmalinski48@gmail.com"
151 | ],
152 | "contact_emails": [],
153 | "youth_category": "Community Group",
154 | "service_class_level_1": "Support Services",
155 | "service_class_level_2": "Para-professional Counseling, Therapy, and Support",
156 | "service_classes": [
157 | "Community Prevention, Education and Outreach"
158 | ],
159 | "target_populations": [
160 | "parent of LGBTQ youth"
161 | ],
162 | "age_range": "",
163 | "additional_notes": []
164 | }
165 | }
166 | ]
167 | },
168 |
169 | openSearchResult: {
170 | "place_id":"98244943",
171 | "licence":"Data \u00a9 OpenStreetMap contributors, ODbL 1.0. http:\/\/www.openstreetmap.org\/copyright",
172 | "osm_type":"relation",
173 | "osm_id":"2315704",
174 | "boundingbox": "box",
175 | "lat":"42.3604823",
176 | "lon":"-71.0595678",
177 | "display_name":"display name",
178 | "class":"place",
179 | "type":"city",
180 | "importance":1.0299782170989,
181 | "icon":"http:\/\/nominatim.openstreetmap.org\/images\/mapicons\/poi_place_city.p.20.png",
182 | "address":{
183 | "city":"Boston",
184 | "county":"Suffolk County",
185 | "state":"Massachusetts",
186 | "country":"United States of America",
187 | "country_code":"us"}},
188 |
189 | parsedSearchResult: {
190 | name: "Boston, Massachusetts",
191 | lat: "42.3604823",
192 | lng: "-71.0595678"
193 | }
194 | });
195 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/spec/data/analytics_spec.js:
--------------------------------------------------------------------------------
1 | define(function() {
2 | 'use strict';
3 | describeComponent('data/analytics', function() {
4 | var config;
5 | beforeEach(function() {
6 | config = {analytics: {}};
7 | setupComponent();
8 | spyOn(this.component, 'track');
9 | spyOn(this.component, 'trackAll');
10 | });
11 |
12 | afterEach(function() {
13 | window.ga = undefined;
14 | });
15 |
16 | describe('track', function() {
17 | beforeEach(function() {
18 | this.component.track.andCallThrough();
19 | });
20 | it('calls window.ga if present', function() {
21 | window.ga = function() {};
22 | spyOn(window, 'ga');
23 | this.component.track(1, 2, 3);
24 | expect(window.ga).toHaveBeenCalledWith(1, 2, 3);
25 | });
26 | it('creates ga object if not present', function() {
27 | window.ga = undefined;
28 | this.component.track(1, 2, 3);
29 | expect(window.ga).toEqual({q: [[1, 2, 3]], l: jasmine.any(Number)});
30 | });
31 | it('uses an existing ga object', function() {
32 | window.ga = {q: []};
33 | this.component.track(1, 2, 3);
34 | expect(window.ga).toEqual({q: [[1, 2, 3]]});
35 | });
36 | });
37 |
38 | it("enabled: false sends no events", function() {
39 | this.component.trigger('config', config);
40 | expect(this.component.track).not.toHaveBeenCalled();
41 | });
42 |
43 | describe("enabled: true", function() {
44 | beforeEach(function() {
45 | config.analytics.enabled = true;
46 | });
47 | it('configures the CodeForBoston tracker (private: false)', function() {
48 | this.component.trigger('config', config);
49 | expect(this.component.track).toHaveBeenCalledWith(
50 | 'create', this.component.attr.codeForBostonTracker, 'localhost',
51 | {name: 'cfb'});
52 | });
53 | it('does not configure the CodeForBoston tracker (private: true)', function() {
54 | config.analytics['private'] = true;
55 | this.component.trigger('config', config);
56 | expect(this.component.track).not.toHaveBeenCalled();
57 | });
58 | it('uses a provided hostname', function() {
59 | config.analytics.hostname = 'auto';
60 | this.component.trigger('config', config);
61 | expect(this.component.track).toHaveBeenCalledWith(
62 | 'create', this.component.attr.codeForBostonTracker, 'auto',
63 | {name: 'cfb'});
64 | });
65 | it('also configures a provided tracker', function() {
66 | config.analytics.google_tracker = 'TRACKER';
67 | this.component.trigger('config', config);
68 | expect(this.component.track).toHaveBeenCalledWith(
69 | 'create', this.component.attr.codeForBostonTracker, 'localhost',
70 | {name: 'cfb'});
71 | expect(this.component.track).toHaveBeenCalledWith(
72 | 'create', 'TRACKER', 'localhost',
73 | {name: 'user'});
74 | });
75 | it('trackAll send the message to each configured tracker', function () {
76 | config.analytics.google_tracker = 'tracker';
77 | this.component.trackAll.andCallThrough();
78 | this.component.trigger('config', config);
79 | expect(this.component.track).toHaveBeenCalledWith(
80 | 'cfb.send', 'pageview');
81 | expect(this.component.track).toHaveBeenCalledWith(
82 | 'user.send', 'pageview');
83 | });
84 | it('sends a pageview event', function() {
85 | this.component.trigger('config', config);
86 | expect(this.component.trackAll).toHaveBeenCalledWith(
87 | 'send', 'pageview');
88 | });
89 |
90 | describe('events (detail: false)', function() {
91 | beforeEach(function() {
92 | this.component.trigger('config', config);
93 | });
94 | it('sends a click event on facet selection', function() {
95 | this.component.trigger('uiFilterFacet', {facet: 'data'});
96 | expect(this.component.trackAll).toHaveBeenCalledWith(
97 | 'send', 'event', 'click', 'facets', null);
98 | });
99 | it('sends a click event on feature selection', function() {
100 | this.component.trigger('selectFeature', {geometry: {
101 | coordinates: [-1, +2]}});
102 | expect(this.component.trackAll).toHaveBeenCalledWith(
103 | 'send', 'event', 'click', 'feature', null);
104 | });
105 | it('sends a click event on search', function() {
106 | this.component.trigger('uiSearch', {search: 'data'});
107 | expect(this.component.trackAll).toHaveBeenCalledWith(
108 | 'send', 'event', 'click', 'search', null);
109 | });
110 | });
111 |
112 | describe('events (detail: true)', function() {
113 | beforeEach(function() {
114 | config.analytics.detail_enabled = true;
115 | this.component.trigger('config', config);
116 | });
117 | it('sends a click event on facet selection', function() {
118 | this.component.trigger('uiFilterFacet', {facet: 'data'});
119 | expect(this.component.trackAll).toHaveBeenCalledWith(
120 | 'send', 'event', 'click', 'facets',
121 | JSON.stringify({facet: 'data'}));
122 | });
123 | it('sends a click event on feature selection', function() {
124 | this.component.trigger('selectFeature', {geometry: {
125 | coordinates: [-1, +2]}});
126 | expect(this.component.trackAll).toHaveBeenCalledWith(
127 | 'send', 'event', 'click', 'feature', '-1,2');
128 | });
129 | it('sends a click event on search', function() {
130 | this.component.trigger('uiSearch', {query: 'data'});
131 | expect(this.component.trackAll).toHaveBeenCalledWith(
132 | 'send', 'event', 'click', 'search', 'data');
133 | });
134 | });
135 | });
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/test/spec/data/facet_spec.js:
--------------------------------------------------------------------------------
1 | define(['test/mock', 'lodash'], function(mock, _) {
2 | 'use strict';
3 | describeComponent('data/facet', function() {
4 | beforeEach(function() {
5 | setupComponent();
6 | spyOnEvent(document, 'dataFacets');
7 | spyOnEvent(document, 'dataFiltered');
8 | });
9 |
10 | describe('on config', function() {
11 | beforeEach(function() {
12 | this.component.trigger('config', mock.config);
13 | });
14 | it('records the facets to display', function() {
15 | expect(this.component.config).toEqual(mock.config.facets);
16 | });
17 | });
18 |
19 | describe('on data', function() {
20 | beforeEach(function() {
21 | this.component.trigger('config', mock.config);
22 | this.component.trigger('data', mock.data);
23 | waits(100);
24 | });
25 | it('emits a "dataFacets" event with the values for each facet', function() {
26 | expect('dataFacets').toHaveBeenTriggeredOnAndWith(
27 | document,
28 | {
29 | services_offered: [
30 | {value: 'public education', count: 1, selected: false},
31 | {value: 'social group', count: 2, selected: false},
32 | {value: 'support group', count: 3, selected: false}
33 | ]
34 | });
35 | });
36 | });
37 |
38 | describe('on uiFilterFacet', function() {
39 | describe("single value facet", function() {
40 | beforeEach(function() {
41 | var config = _.clone(mock.config);
42 | config.facets = {
43 | community: {
44 | title: "Community",
45 | type: 'single'
46 | }
47 | };
48 | this.component.trigger('config', config);
49 | this.component.trigger('data', mock.data);
50 | this.component.trigger('uiFilterFacet', {
51 | facet: 'community',
52 | selected: ['Northampton']
53 | });
54 | waits(100);
55 | });
56 | it('emits a "dataFiltered" event with the filtered data', function() {
57 | expect('dataFiltered').toHaveBeenTriggeredOnAndWith(
58 | document,
59 | {
60 | featureIds: [mock.data.features[0].id]
61 | });
62 | });
63 | it('emits a "dataFacets" event with the filtered values for each facet', function() {
64 | expect('dataFacets').toHaveBeenTriggeredOnAndWith(
65 | document,
66 | {community: [
67 | {value: 'Greenfield', count: 1, selected: false},
68 | {value: 'Northampton', count: 1, selected: true},
69 | {value: 'Shellbourne Falls', count: 1, selected: false}
70 | ]
71 | });
72 | });
73 | });
74 | describe("list value facet", function() {
75 | beforeEach(function() {
76 | this.component.trigger('config', mock.config);
77 | this.component.trigger('data', mock.data);
78 | this.component.trigger('uiFilterFacet', {
79 | facet: 'services_offered',
80 | selected: ['social group']
81 | });
82 | waits(100);
83 | });
84 | it('emits a "dataFiltered" event with the filtered data', function() {
85 | expect('dataFiltered').toHaveBeenTriggeredOnAndWith(
86 | document,
87 | {featureIds: [mock.data.features[0].id,
88 | mock.data.features[1].id]
89 | });
90 | });
91 | it('emits a "dataFacets" event with the filtered values for each facet', function() {
92 | expect('dataFacets').toHaveBeenTriggeredOnAndWith(
93 | document,
94 | {services_offered: [
95 | {value: 'public education', count: 0, selected: false},
96 | {value: 'social group', count: 2, selected: true},
97 | {value: 'support group', count: 2, selected: false}
98 | ]
99 | });
100 | });
101 | });
102 | describe("map facet", function() {
103 | describe('value not provided', function() {
104 | beforeEach(function() {
105 | var config = _.clone(mock.config);
106 | config.facets = {
107 | map: {
108 | title: "Map",
109 | text: "Limit results",
110 | type: 'map'
111 | }
112 | };
113 | this.component.trigger('config', config);
114 | this.component.trigger('data', mock.data);
115 | this.component.trigger('mapBounds', {
116 | southWest: [42.3251, -71.6411],
117 | northEast: [42.3250, -72.6412]
118 | });
119 | this.component.trigger('uiFilterFacet', {
120 | facet: 'map',
121 | selected: ['Limit results']
122 | });
123 | waits(50);
124 | });
125 | it('emits a "dataFiltered" event with the filtered data', function() {
126 | expect('dataFiltered').toHaveBeenTriggeredOnAndWith(
127 | document,
128 | {
129 | featureIds: [mock.data.features[0].id]
130 | });
131 | });
132 | it('emits a "dataFacets" event with the filtered values for each facet', function() {
133 | expect('dataFacets').toHaveBeenTriggeredOnAndWith(
134 | document,
135 | {map: [
136 | {value: 'Limit results', count: 1, selected: true}
137 | ]
138 | });
139 | });
140 | });
141 | describe('if value: true is configured', function() {
142 | beforeEach(function() {
143 | var config = _.clone(mock.config);
144 | config.facets = {
145 | map: {
146 | title: "Map",
147 | text: "Limit results",
148 | type: 'map',
149 | value: true
150 | }
151 | };
152 | this.component.trigger('config', config);
153 | this.component.trigger('data', mock.data);
154 | waits(50);
155 | });
156 | it('the facet defaults to selected', function() {
157 | expect('dataFacets').toHaveBeenTriggeredOnAndWith(
158 | document,
159 | {map: [
160 | {value: 'Limit results', count: 3, selected: true}
161 | ]
162 | });
163 | });
164 | });
165 | });
166 | describe("multiple facets", function() {
167 | beforeEach(function() {
168 | var config = _.clone(mock.config);
169 | config.facets.community = {
170 | title: "Community",
171 | type: 'single'
172 | };
173 | this.component.trigger('config', config);
174 | this.component.trigger('data', mock.data);
175 | this.component.trigger('uiFilterFacet', {
176 | facet: 'services_offered',
177 | selected: ['social group']
178 | });
179 | waits(100);
180 | });
181 | it('emits a "dataFiltered" event with the filtered data', function() {
182 | expect('dataFiltered').toHaveBeenTriggeredOnAndWith(
183 | document,
184 | {featureIds: [mock.data.features[0].id,
185 | mock.data.features[1].id]
186 | });
187 | this.component.trigger('uiFilterFacet', {
188 | facet: 'community',
189 | selected: ['Northampton']
190 | });
191 | waits(25);
192 | runs(function() {
193 | expect('dataFiltered').toHaveBeenTriggeredOnAndWith(
194 | document,
195 | {featureIds: [mock.data.features[0].id]
196 | });
197 | });
198 | });
199 | it('emits a "dataFacets" event with the filtered values for each facet', function() {
200 | expect('dataFacets').toHaveBeenTriggeredOnAndWith(
201 | document,
202 | {
203 | community: [
204 | {value: 'Greenfield', count: 1, selected: false},
205 | {value: 'Northampton', count: 1, selected: false},
206 | {value: 'Shellbourne Falls', count: 0, selected: false}
207 | ],
208 | services_offered: [
209 | {value: 'public education', count: 0, selected: false},
210 | {value: 'social group', count: 2, selected: true},
211 | {value: 'support group', count: 2, selected: false}
212 | ]
213 | });
214 | this.component.trigger('uiFilterFacet', {
215 | facet: 'community',
216 | selected: ['Northampton']
217 | });
218 | waits(100);
219 | runs(function() {
220 | expect('dataFacets').toHaveBeenTriggeredOnAndWith(
221 | document,
222 | {
223 | community: [
224 | {value: 'Greenfield', count: 1, selected: false},
225 | {value: 'Northampton', count: 1, selected: true},
226 | {value: 'Shellbourne Falls', count: 0, selected: false}
227 | ],
228 | services_offered: [
229 | {value: 'public education', count: 0, selected: false},
230 | {value: 'social group', count: 1, selected: true},
231 | {value: 'support group', count: 1, selected: false}
232 | ]
233 | });
234 | });
235 | });
236 | it('does not filter away matching simple facets', function() {
237 | this.component.trigger('uiFilterFacet', {
238 | facet: 'services_offered',
239 | selected: ['public education']
240 | });
241 | waits(100);
242 | runs(function() {
243 | expect('dataFacets').toHaveBeenTriggeredOnAndWith(
244 | document,
245 | {
246 | community: [
247 | {value: 'Greenfield', count: 0, selected: false},
248 | {value: 'Northampton', count: 0, selected: false},
249 | {value: 'Shellbourne Falls', count: 1, selected: false}
250 | ],
251 | services_offered: [
252 | {value: 'public education', count: 1, selected: true},
253 | {value: 'social group', count: 0, selected: false},
254 | {value: 'support group', count: 1, selected: false}
255 | ]
256 | });
257 | });
258 | });
259 | });
260 | });
261 | describe('on uiClearFacets', function() {
262 | beforeEach(function() {
263 | this.component.trigger('config', mock.config);
264 | this.component.trigger('data', mock.data);
265 | this.component.trigger('uiFilterFacet', {
266 | facet: 'community',
267 | selected: ['Northampton']
268 | });
269 | });
270 | it('emits a "dataFacets" event with no facets selected', function() {
271 | this.component.trigger('uiClearFacets', {"facet" : "community"});
272 | waits(100);
273 | runs(function() {
274 | expect('dataFacets').toHaveBeenTriggeredOnAndWithFuzzy(
275 | document,
276 | {community: [
277 | {selected: false},
278 | {selected: false},
279 | {selected: false}]
280 | });
281 | });
282 | });
283 | });
284 | });
285 | });
286 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/ui/facet_spec.js:
--------------------------------------------------------------------------------
1 | define(['test/mock', 'jquery'], function(mock, $) {
2 | 'use strict';
3 | describeComponent('ui/facet', function() {
4 | var mockFacets = {
5 | 'services_offered': [{value: 'first', count: 1},
6 | {value: 'second', count: 2, selected: true},
7 | {value: 'third', count: 0}]
8 | };
9 |
10 | beforeEach(function() {
11 | setupComponent();
12 | spyOnEvent(document, 'uiFilterFacet');
13 | spyOnEvent(document, 'uiClearFacets');
14 | });
15 | describe('on config', function() {
16 | it('stores the facet config', function() {
17 | $(document).trigger('config', mock.config);
18 | expect(this.component.facetConfig).toEqual(
19 | mock.config.facets);
20 | });
21 | });
22 | describe('on dataFacets', function() {
23 | beforeEach(function() {
24 | this.component.trigger('config', mock.config);
25 | this.component.trigger('dataFacets', mockFacets);
26 | // skip past intro question to first facet
27 | this.component.setFacetOffset(0);
28 | });
29 | it('renders the name of the facet in an h4', function() {
30 | expect(this.$node.find('h4').text()).toEqual('What kind of services do you want?');
31 | });
32 | it('renders each facet value as a checkbox', function() {
33 | expect(this.$node.find('input').length).toEqual(2);
34 | expect(this.$node.find('input:first').attr('name')).toEqual('first');
35 | });
36 | it('renders the label with the facet value', function() {
37 | var text = this.$node.find("label:first").text();
38 | expect(text).toContain('first');
39 | });
40 | it('renders a checked checkbox if selected is true', function() {
41 | expect(this.$node.find("input[name=second]").val()).toEqual('on');
42 | });
43 | it('does not render labels with a 0 count', function() {
44 | expect(this.$node.find("input[name=third]").length).toEqual(0);
45 | });
46 | });
47 |
48 | describe('on click', function() {
49 | // NB: "second" is already selected, per the mockFacets above
50 | beforeEach(function() {
51 | this.component.trigger('config', mock.config);
52 | this.component.trigger('dataFacets', mockFacets);
53 | // skip past intro question to first facet
54 | this.component.setFacetOffset(0);
55 | });
56 | it('sends a "uiFilterFacet" event with the selected facets', function () {
57 | this.component.$node.find('input:first').click();
58 | waits(1);
59 | runs(function() {
60 | expect('uiFilterFacet').toHaveBeenTriggeredOnAndWith(
61 | document,
62 | {facet: 'services_offered',
63 | selected: ['first', 'second']
64 | });
65 | });
66 | });
67 | it('sends a "uiFilterFacet" event with no facets', function () {
68 | this.component.$node.find('input:eq(1)').click();
69 | waits(1);
70 | runs(function() {
71 | expect('uiFilterFacet').toHaveBeenTriggeredOnAndWith(
72 | document,
73 | {facet: 'services_offered',
74 | selected: []
75 | });
76 | });
77 | });
78 | });
79 |
80 | describe('on click clear facet', function() {
81 | beforeEach(function() {
82 | this.component.trigger('config', mock.config);
83 | this.component.trigger('dataFacets', mockFacets);
84 | // skip past intro question to first facet
85 | this.component.setFacetOffset(0);
86 | });
87 | it('sends a "uiClearFacets" event', function () {
88 | this.component.$node.find('.clear-facets').click();
89 | waits(1);
90 | runs(function() {
91 | expect('uiClearFacets').toHaveBeenTriggeredOn(document);
92 | });
93 | });
94 | });
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/spec/ui/map_spec.js:
--------------------------------------------------------------------------------
1 | define(
2 | ['leaflet', 'test/mock', 'jquery', 'lodash'], function(L, mock, $, _) {
3 | 'use strict';
4 | describeComponent('ui/map', function() {
5 | beforeEach(function() {
6 | L.Icon.Default.imagePath = '/base/lib/leaflet/images';
7 | spyOn(L.control, 'locate').andReturn(
8 | jasmine.createSpyObj('Locate', ['addTo']));
9 | spyOn(L.control, 'scale').andReturn(
10 | jasmine.createSpyObj('Scale', ['addTo']));
11 | setupComponent();
12 | });
13 |
14 | describe('initialize', function() {
15 | it('sets up the map', function() {
16 | expect(this.component.map).toBeDefined();
17 | });
18 | it('sets up the scale control', function() {
19 | expect(L.control.scale).toHaveBeenCalledWith();
20 | expect(L.control.scale().addTo).toHaveBeenCalledWith(
21 | this.component.map);
22 | });
23 | it('sets up the locate control', function() {
24 | expect(L.control.locate).toHaveBeenCalledWith();
25 | expect(L.control.locate().addTo).toHaveBeenCalledWith(
26 | this.component.map);
27 | });
28 | });
29 | describe('loading data', function () {
30 | it('config sets up the map object', function() {
31 | this.component.map = jasmine.createSpyObj('Map',
32 | ['setView',
33 | 'setMaxBounds',
34 | 'invalidateSize',
35 | 'addLayer',
36 | 'remove'
37 | ]);
38 | this.component.map.options = {};
39 | this.component.trigger('config', mock.config);
40 | expect(this.component.map.options.maxZoom, mock.config.map.maxZoom);
41 | expect(this.component.map.setView).toHaveBeenCalledWith(
42 | mock.config.map.center, mock.config.map.zoom);
43 | expect(this.component.map.setMaxBounds).toHaveBeenCalledWith(
44 | mock.config.map.maxBounds);
45 | });
46 |
47 | it('data sets up the features', function() {
48 | this.component.trigger('data', mock.data);
49 | waits(25);
50 | runs(function() {
51 | expect(_.size(this.component.layers)).toEqual(3);
52 | });
53 | });
54 |
55 | it('data a second time resets the data', function() {
56 | this.component.trigger('data', {type: 'FeatureCollection',
57 | features: []});
58 | waits(25);
59 | runs(function() {
60 | expect(_.size(this.component.layers)).toEqual(0);
61 | });
62 | });
63 | });
64 |
65 | describe('panning', function() {
66 | beforeEach(function() {
67 | spyOn(this.component.map, 'panTo');
68 | });
69 | it('panTo goes to the lat/lng with maximum zoom', function() {
70 | var latlng = {lat: 1, lng: 2};
71 | this.component.trigger('config', mock.config);
72 | this.component.trigger('panTo', latlng);
73 | expect(this.component.map.panTo).toHaveBeenCalledWith(
74 | latlng);
75 | });
76 | });
77 |
78 | describe('on map move', function() {
79 | it('triggers a mapBounds event with the corners of the map', function() {
80 | var map = this.component.map;
81 |
82 | spyOnEvent(this.component.node, 'mapBounds');
83 |
84 | map.setView([0, 0], 11);
85 |
86 | expect('mapBounds').toHaveBeenTriggeredOn(this.component.node);
87 | expect('mapBounds').toHaveBeenTriggeredOnAndWith(
88 | this.component.node,
89 | {
90 | southWest: [map.getBounds().getSouthWest().lat,
91 | map.getBounds().getSouthWest().lng],
92 | northEast: [map.getBounds().getNorthEast().lat,
93 | map.getBounds().getNorthEast().lng]
94 | }
95 | );
96 | });
97 | });
98 |
99 | describe('clicking an icon', function() {
100 | var layer;
101 | beforeEach(function() {
102 | spyOnEvent(document, 'selectFeature');
103 | this.component.trigger('config', mock.config);
104 | this.component.trigger('data', mock.data);
105 |
106 | waits(25);
107 | runs(function() {
108 | // fake the click event
109 | layer = this.component.layers[mock.data.features[0].id];
110 | layer.fireEvent('click', {
111 | latlng: layer._latlng
112 | });
113 | });
114 | });
115 |
116 | it('sends a selectFeature event', function() {
117 | expect('selectFeature').toHaveBeenTriggeredOnAndWith(
118 | document, layer.feature);
119 | });
120 | });
121 |
122 | describe('selectFeature', function() {
123 | beforeEach(function() {
124 | this.component.trigger('config', mock.config);
125 | this.component.trigger('data', mock.data);
126 | waits(25);
127 | runs(function() {
128 | this.component.trigger(document,
129 | 'selectFeature', mock.data.features[0]);
130 | });
131 | });
132 | it('turns the icon gray', function() {
133 | var icon = this.component.$node.find('.leaflet-marker-icon:first');
134 | expect(icon.attr('src')).toMatch(/marker-icon-gray\.png$/);
135 | });
136 |
137 | it('turns the previously clicked icon back to the default', function() {
138 | this.component.trigger(document, 'selectFeature', null);
139 | var icon = this.component.$node.find('.leaflet-marker-icon:first');
140 | expect(icon.attr('src')).toMatch(/marker-icon\.png$/);
141 | });
142 | });
143 |
144 | describe('deselectFeature', function() {
145 | beforeEach(function() {
146 | this.component.trigger('config', mock.config);
147 | this.component.trigger('data', mock.data);
148 | waits(25);
149 | runs(function() {
150 | this.component.trigger(document,
151 | 'selectFeature', mock.data.features[0]);
152 | });
153 | });
154 | it('turns the icon back to default', function() {
155 | this.component.trigger(document, 'deselectFeature', mock.data.features[0]);
156 | var icon = this.component.$node.find('.leaflet-marker-icon:first');
157 | expect(icon.attr('src')).toMatch(/marker-icon\.png$/);
158 | });
159 | });
160 |
161 | describe("dataSearchResult", function() {
162 | beforeEach(function() {
163 | this.component.trigger('config', mock.config);
164 | this.component.trigger('data', mock.data);
165 | spyOnEvent('.component-root', 'panTo');
166 | this.component.trigger(
167 | document,
168 | 'dataSearchResult',
169 | {
170 | lat: 41,
171 | lng: -71
172 | }
173 | );
174 | });
175 | it('puts a marker on the map', function() {
176 | expect(this.$node.find('.search-result-marker').length).toEqual(1);
177 | });
178 | it('puts the marker at the given lat/lng', function() {
179 | expect(this.component.searchMarker._latlng.lat).toEqual(41);
180 | expect(this.component.searchMarker._latlng.lng).toEqual(-71);
181 | });
182 | it('pans to the lat/long if present', function() {
183 | expect('panTo').toHaveBeenTriggeredOnAndWith(
184 | '.component-root',
185 | {lat: 41,
186 | lng: -71
187 | });
188 | });
189 | });
190 | });
191 | });
192 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------