/* Javascript for Meetings Archive, Single Meeting, and Single Location pages a) procedural logic b) event handlers c) functions */ jQuery(function($){ //a) procedural logic var $body = $('body'); var typeaheadEnabled = false; if (typeof tsml_map !== 'object') { //main meetings page //search typeahead var tsml_regions = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.nonword('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, prefetch: { url: tsml.ajaxurl + '?action=tsml_regions', cache: false } }); var tsml_groups = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, prefetch: { url: tsml.ajaxurl + '?action=tsml_groups', cache: false } }); var tsml_locations = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, prefetch: { url: tsml.ajaxurl + '?action=tsml_locations', cache: false } }); //show/hide upcoming menu option toggleUpcoming(); //if already searching, mark results var $search_field = $('#meetings #search input[name=query]'); if ($search_field.size() && $search_field.val().length) { $('#tsml td').not('.time').mark($search_field.val()); } var mode = $('#search li.active a').attr('data-id'); if (mode == 'search') { typeaheadEnable(); //list/map page (only create if we're on the map view) if ($('a.toggle-view[data-id="map"]').hasClass('active')) { createMap(true, locations); } } else if ((mode == 'location') && $search_field.val().length) { doSearch(); } else if (mode == 'me') { doSearch(); } } else { //meeting or location detail page var location_link = (typeof tsml_map.location_url === 'undefined') ? tsml_map.location : formatLink(tsml_map.location_url, tsml_map.location, 'tsml_meeting'); locations = {}; locations[tsml_map.location_id] = { latitude: tsml_map.latitude, longitude: tsml_map.longitude, formatted_address: tsml_map.formatted_address, name: tsml_map.location, meetings: [], directions: tsml_map.directions, directions_url: tsml_map.directions_url, url: tsml_map.location_url, }; createMap(false, locations); } //b) jQuery event handlers //handle directions links; send to Apple Maps (iOS), or Google Maps (everything else) var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); $body.on('click', 'a.tsml-directions', function(e){ e.preventDefault(); var directions = (iOS ? 'maps://?' : 'https://maps.google.com/?') + $.param({ daddr: $(this).attr('data-latitude') + ',' + $(this).attr('data-longitude'), saddr: 'Current Location', q: $(this).attr('data-location') }); window.open(directions); }); //expand region select $('.panel-expandable').on('click', '.panel-heading', function(e){ $(this).closest('.panel-expandable').toggleClass('expanded'); if (tsml.debug) console.log('.panel-expandable toggling'); }) //single meeting page feedback form $('#meeting #feedback').validate({ onfocusout: false, onkeyup: function(element) { }, highlight: function(element, errorClass, validClass) { $(element).parent().addClass('has-error'); }, unhighlight: function(element, errorClass, validClass) { $(element).parent().removeClass('has-error'); }, errorPlacement: function(error, element) { return; //don't show message on page, simply highlight }, submitHandler: function(form){ var $form = $(form), $feedback = $form.closest('#feedback'); if (!$form.hasClass('running')); $.post(tsml.ajaxurl, $form.serialize(), function(data) { $form.removeClass('running'); $feedback.find('.list-group').html('
  • ' + data + '
  • '); }).fail(function(response) { $form.removeClass('running'); $feedback.find('.list-group').html('
  • ' + tsml.strings.email_not_sent + '
  • '); }); $form.addClass('running'); return false; } }); //table sorting $('#meetings table thead').on('click', 'th', function(){ var sort = $(this).attr('class'); var order; //update header if ($(this).attr('data-sort')) { order = ($(this).attr('data-sort') == 'asc') ? 'desc' : 'asc'; } else { order = 'asc'; } $('#meetings table thead th').removeAttr('data-sort'); $('#meetings table thead th.' + sort).attr('data-sort', order); sortMeetings(); }); //controls changes $('#meetings .controls').on('submit', '#search', function(){ //capture submit event trackAnalytics('search', $search_field.val()) doSearch(); return false; }).on('click', 'div.expand', function(e){ //expand or contract regions submenu e.preventDefault(); e.stopPropagation(); $(this).next('ul.children').toggleClass('expanded'); $(this).toggleClass('expanded'); }).on('click', '.dropdown-menu a', function(e){ //these are live hrefs now e.preventDefault(); //dropdown menu click var param = $(this).closest('div').attr('id'); if (param == 'mode') { if (tsml.debug) console.log('dropdown click search mode'); //only one search mode $('#mode li').removeClass('active'); //remove meeting results $('#meetings').addClass('empty'); //change icon & enable or disable if ($(this).attr('data-id') == 'search') { $search_field.prop('disabled', false); $('#search button i').removeClass().addClass('glyphicon glyphicon-search'); } else if ($(this).attr('data-id') == 'location') { $search_field.prop('disabled', false); $('#search button i').removeClass().addClass('glyphicon glyphicon-map-marker'); setAlert('loc_thinking'); } else if ($(this).attr('data-id') == 'me') { $search_field.prop('disabled', true); $('#search button i').removeClass().addClass('glyphicon glyphicon-user'); setAlert('geo_thinking'); } //change placeholder text $search_field.attr('placeholder', $(this).text()); } else if (param == 'distance') { //distance only one if (tsml.debug) console.log('dropdown click distance'); $('#distance li').removeClass('active'); $('#distance span.selected').html($(this).html()); trackAnalytics('distance', $(this).text()); } else if (param == 'region') { //switch between region and district mode if ($(this).hasClass('switch')) { if (tsml.debug) console.log('dropdown click switching between region and district'); var mode = $(this).parent().hasClass('region') ? 'district' : 'region'; $(this).closest('#meetings').attr('tax-mode', mode); e.stopPropagation(); return; } //region only one if (tsml.debug) console.log('dropdown click region or district'); $('#region li').removeClass('active'); $('#region span.selected').html($(this).html()); trackAnalytics('region', $(this).text()); } else if (param == 'day') { //day only one selected if (tsml.debug) console.log('dropdown click day'); $('#day li').removeClass('active'); $('#day span.selected').html($(this).html()); trackAnalytics('day', $(this).text()); } else if (param == 'time') { //time only one if (tsml.debug) console.log('dropdown click time'); $('#time li').removeClass('active'); $('#time span.selected').html($(this).html()); trackAnalytics('time', $(this).text()); } else if (param == 'type') { //type can be multiple if (tsml.debug) console.log('dropdown click type'); if (!e.metaKey) $('#type li').removeClass('active'); trackAnalytics('type', $(this).text()); } $(this).parent().toggleClass('active'); //wait to set label on type until we have a complete count if (param == 'type') { if ($('#type li.active a[data-id]').size()) { if (tsml.debug) console.log('dropdown click ' + $('#type li.active a[data-id]').size() + ' types selected'); var types = []; $('#type li.active a[data-id]').each(function(){ types.push($(this).text()); }); $('#type span.selected').html(types.join(' + ')); } else { if (tsml.debug) console.log('dropdown click no types selected'); $('#type span.selected').html($(this).text()); } } //set page title var string = ''; if ($('#meetings #day li.active').index()) { string += $('#meetings #day span.selected').text(); } if ($('#meetings #time li.active').index()) { string += ' ' + $('#meetings #time span.selected').text(); } if ($('#meetings #type li.active').index()) { string += ' ' + $('#meetings #type span.selected').text(); } string += ' ' + tsml.program + ' Meetings'; if ($('#meetings #region li.active').index()) { string += ' in ' + $('#meetings #region span.selected').text(); } document.title = string; $('#tsml #meetings .title h1').text(string); //show/hide upcoming menu option toggleUpcoming(); doSearch(); }); //toggle between list and map $('#meetings #action .toggle-view').click(function(e){ //these are live hrefs now e.preventDefault(); //what's going on var action = $(this).attr('data-id'); var previous = $('#meetings').attr('data-view'); //don't do anything if (action == previous) return; //toggle control, set meetings div $('#meetings #action .toggle-view').toggleClass('active'); $('#meetings').attr('data-view', action); //init map if it doesn't exist yet if (action == 'map') { createMap(true, locations, searchLocation); } //save the query in the query string, if the browser is up to it if (history.pushState) { if (action == tsml.defaults.view) { var url = updateQueryString('tsml-view'); } else { var url = updateQueryString('tsml-view', action); } window.history.pushState({path:url}, '', url); } }); //resize fullscreen on resize $(window).resize(function(e){ if ($('#meetings').hasClass('tsml_fullscreen')) { var center = map.getCenter(); var height = $(window).height() - 79; if ($body.hasClass('admin-bar')) height -= 32; $('#map').css('height', height); google.maps.event.trigger(map, 'resize'); map.setCenter(center); } }); //c) functions //run search (triggered by dropdown toggle or form submit) function doSearch() { //types can be multiple var types = []; $('#type li.active a').each(function(){ if ($(this).attr('data-id')) { types.push($(this).attr('data-id')); } }); //prepare query for ajax var controls = { action: 'meetings', query: $('#meetings #search input[name=query]').val().trim(), mode: $('#search li.active a').attr('data-id'), region: $('#region li.region.active a').attr('data-id'), district: $('#region li.district.active a').attr('data-id'), day: $('#day li.active a').attr('data-id'), time: $('#time li.active a').attr('data-id'), type: types.length ? types.join(',') : undefined, distance: $('#distance li.active a').attr('data-id'), view: $('#meetings .toggle-view.active').attr('data-id'), } //reset search location searchLocation = null; //get current query string for history and appending to links var query_string = {}; query_string['tsml-day'] = controls.day ? controls.day : 'any'; if ((controls.mode != 'search') && (controls.distance != tsml.defaults.distance)) { query_string['tsml-distance'] = controls.distance; } if (controls.mode && (controls.mode != tsml.defaults.mode)) query_string['tsml-mode'] = controls.mode; if (controls.query && (controls.query != tsml.defaults.query)) query_string['tsml-query'] = controls.query; if (controls.mode == 'search') { if (controls.region != tsml.defaults.region) { query_string['tsml-region'] = controls.region; } else if (controls.district != tsml.defaults.district) { query_string['tsml-district'] = controls.district; } } if (controls.time && (controls.time != tsml.defaults.time)) query_string['tsml-time'] = controls.time; if (controls.type && (controls.type != tsml.defaults.type)) query_string['tsml-type'] = controls.type; if (controls.view && (controls.view != tsml.defaults.view)) query_string['tsml-view'] = controls.view; query_string = $.param(query_string); //save the query in the query string, if the browser is up to it if (history.pushState) { var url = window.location.protocol + '//' + window.location.host + window.location.pathname; if (query_string.length) url = url + '?' + query_string; if (location.search.indexOf('post_type=tsml_meeting') > -1) { url = url + ((url.indexOf('?') > -1) ? '&' : '?') + 'post_type=tsml_meeting'; } window.history.pushState({path:url}, '', url); } //set the mode on the parent object $('#meetings').attr('data-mode', controls.mode); if (controls.mode == 'search') { typeaheadEnable(); removeSearchMarker(); //clear search marker if it exists getMeetings(controls); } else if (controls.mode == 'location') { typeaheadDisable(); if (controls.query) { //start spinner $('#search button i').removeClass().addClass('glyphicon glyphicon-refresh spinning'); //geocode the address $.getJSON(tsml.ajaxurl, { action: 'tsml_geocode', address: controls.query, nonce: tsml.nonce, }, function(geocoded) { if (tsml.debug) console.log('doSearch() location geocoded', geocoded); $('#search button i').removeClass().addClass('glyphicon glyphicon-map-marker'); if (geocoded.status == 'error') { //show error message removeSearchMarker(); //clear marker if it exists setAlert('loc_error'); } else { $search_field.val(geocoded.formatted_address); controls.latitude = geocoded.latitude; controls.longitude = geocoded.longitude; controls.query = ''; //don't actually keyword search this searchLocation = { latitude: controls.latitude, longitude: controls.longitude, }; getMeetings(controls); } }); } else { setAlert('loc_empty'); } } else if (controls.mode == 'me') { if (controls.query) { $('#meetings #search input[name=query]').val(''); controls.query = ''; } //start spinner $('#search button i').removeClass().addClass('glyphicon glyphicon-refresh spinning'); if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(function(pos) { $('#search button i').removeClass().addClass('glyphicon glyphicon-user'); controls.latitude = pos.coords.latitude; controls.longitude = pos.coords.longitude; searchLocation = { latitude: controls.latitude, longitude: controls.longitude, }; getMeetings(controls); }, function() { //browser supports but can't get geolocation if (tsml.debug) console.log('doSearch() didnt get location'); $('#search button i').removeClass().addClass('glyphicon glyphicon-user'); //todo switch to location removeSearchMarker(); setAlert('geo_error'); }, { enableHighAccuracy: true, timeout: 10000, //10 seconds maximumAge: 600000, //10 minutes }); } else { //browser doesn't support geolocation if (tsml.debug) console.log('doSearch() no browser support for geo'); $('#search button i').removeClass().addClass('glyphicon glyphicon-user'); //todo switch to location removeSearchMarker(); setAlert('geo_error_browser'); } } } //actually get the meetings from the JSON resource and output them function getMeetings(controls) { //request new meetings result controls.distance_units = tsml.distance_units; controls.nonce = tsml.nonce; //make url more readable for (var property in controls) { if (controls[property] === null || controls[property] === undefined || !controls[property].toString().length) { delete controls[property]; } } if (tsml.debug) console.log('getMeetings()', tsml.ajaxurl + '?' + $.param(controls)); $.post(tsml.ajaxurl, controls, function(response){ if (tsml.debug) console.log('getMeetings() received', response); if (typeof response != 'object' || response == null) { //there was a problem with the data source $('#meetings').addClass('empty'); setAlert('data_error'); } else if (!response.length) { //if keyword and no results, clear other parameters and search again if (controls.query && (typeof controls.day !== 'undefined' || typeof controls.region !== 'undefined' || typeof controls.time !== 'undefined' || typeof controls.type !== 'undefined')) { $('#day li').removeClass('active').first().addClass('active'); $('#time li').removeClass('active').first().addClass('active'); $('#region li').removeClass('active').first().addClass('active'); $('#type li').removeClass('active').first().addClass('active'); //set selected text $('#day span.selected').html($('#day li:first-child a').html()); $('#time span.selected').html($('#time li:first-child a').html()); $('#region span.selected').html($('#region li:first-child a').html()); $('#type span.selected').html($('#type li:first-child a').html()); return doSearch(); } $('#meetings').addClass('empty'); setAlert('no_meetings'); } else { $('#meetings').removeClass('empty'); setAlert(); locations = []; var tbody = $('#meetings_tbody').html(''); //loop through JSON meetings $.each(response, function(index, obj){ //types could be undefined if (!obj.types) obj.types = []; //add type 'flags' if (typeof tsml.flags == 'object') { for (var i = 0; i < tsml.flags.length; i++) { if (obj.types.indexOf(tsml.flags[i]) !== -1) { obj.name += ' ' + tsml.types[tsml.flags[i]] + ''; } } } //decode types (for hidden type column) for (var i = 0; i < obj.types.length; i++) { obj.types[i] = tsml.types[obj.types[i]]; } obj.types.sort(); obj.types = obj.types.join(', '); //save location info for map view if (!locations[obj.location_id]) { locations[obj.location_id] = { name: obj.location, formatted_address: obj.formatted_address, latitude: obj.latitude, longitude: obj.longitude, url: obj.location_url, meetings: [] }; } //push meeting on to location locations[obj.location_id].meetings[locations[obj.location_id].meetings.length] = { name : obj.name, time : obj.time_formatted, day : obj.day, notes : obj.notes, url : obj.url }; //add new table row var row = '' + (typeof controls.day !== 'undefined' || typeof obj.day === 'undefined' ? obj.time_formatted : tsml.days[obj.day] + '' + obj.time_formatted) + ''; break; case 'distance': row += '' + obj.distance + ' ' + tsml.distance_units + ''; break; case 'name': row += '' + formatLink(obj.url, obj.name, 'post_type') + ''; break; case 'location': row += '' + obj.location + ''; break; case 'address': row += '' + formatAddress(obj.formatted_address, tsml.street_only) + ''; break; case 'region': row += '' + (obj.sub_region || obj.region || '') + ''; break; case 'district': row += '' + (obj.sub_district || obj.district || '') + ''; break; case 'types': row += '' + obj.types + ''; break; } } tbody.append(row + ''); }); sortMeetings(); //highlight search results if (controls.query && controls.mode == 'search') { $('#tsml td').not('.time').mark(controls.query); } //build map if (controls.view == 'map') { createMap(true, locations, searchLocation); } } }, 'json'); } //slugify a string, like WordPress's sanitize_title() function sanitizeTitle(str) { if (str == null) return ''; str = str.replace(/^\s+|\s+$/g, ''); // trim str = str.toLowerCase(); // remove accents, swap ñ for n, etc var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;"; var to = "aaaaeeeeiiiioooouuuunc------"; for (var i=0, l=from.length ; i y[0]) return (order == 'asc') ? 1 : -1; if (x[0] < y[0]) return (order == 'asc') ? -1 : 1; return 0; }); for (var i = 0, len = store.length; i < len; i++){ tbody.appendChild(store[i][1]); } } //if day is today, show 'upcoming' time option, otherwise hide it function toggleUpcoming() { var current_day = new Date().getDay(); var selected_day = $('#day li.active a').first().attr('data-id'); var selected_time = $('#time li.active a').first().attr('data-id'); if (current_day != selected_day) { $('#time li.upcoming').addClass('hidden'); if (selected_time == 'upcoming') { $('#time li.active').removeClass('active'); $('#time li').first().addClass('active'); $('#time span.selected').html($('#time li a').first().text()); } } else { $('#time li.upcoming').removeClass('hidden'); } } //send event to google analytics, if loaded function trackAnalytics(action, label) { if (typeof ga === 'function') { if (tsml.debug) console.log('trackAnalytics() sending ' + action + ': ' + label + ' to google analytics'); ga('send', 'event', '12 Step Meeting List', action, label); } } //disable the typeahead (if you switched to a different search mode) function typeaheadDisable() { if (!typeaheadEnabled) return; typeaheadEnabled = false; $('#meetings #search input[name="query"]').typeahead('destroy'); } //enable the typeahead (if you switch back to search) function typeaheadEnable() { if (typeaheadEnabled) return; if (tsml.debug) console.log('typeaheadEnable()'); typeaheadEnabled = true; $('#meetings #search input[name="query"]').typeahead({ highlight: true }, { name: 'tsml_regions', display: 'value', source: tsml_regions, templates: { header: '

    ' + tsml.strings.regions + '

    ', } }, { name: 'tsml_groups', display: 'value', source: tsml_groups, templates: { header: '

    ' + tsml.strings.groups + '

    ', } }, { name: 'tsml_locations', display: 'value', source: tsml_locations, templates: { header: '

    ' + tsml.strings.locations + '

    ', } }).on('typeahead:selected', function($e, item){ if (item.type == 'region') { $('#region li').removeClass('active'); var active = $('#region li a[data-id="' + item.id + '"]'); active.parent().addClass('active'); $('#region span.selected').html(active.html()); $('#search input[name="query"]').val('').typeahead('val', ''); trackAnalytics('region', active.text()); doSearch(); } else if (item.type == 'location') { trackAnalytics('location', item.value); location.href = item.url; } else if (item.type == 'group') { trackAnalytics('group', item.value); doSearch(); } }); } //set a param on the query string function updateQueryString(key, value, url) { if (!url) url = window.location.href; var re = new RegExp("([?&])" + key + "=.*?(&|#|$)(.*)", "gi"), hash; if (re.test(url)) { if (typeof value !== 'undefined' && value !== null) { return url.replace(re, '$1' + key + "=" + value + '$2$3'); } else { hash = url.split('#'); url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, ''); if (typeof hash[1] !== 'undefined' && hash[1] !== null) url += '#' + hash[1]; return url; } } else { if (typeof value !== 'undefined' && value !== null) { var separator = url.indexOf('?') !== -1 ? '&' : '?'; hash = url.split('#'); url = hash[0] + separator + key + '=' + value; if (typeof hash[1] !== 'undefined' && hash[1] !== null) url += '#' + hash[1]; return url; } else { return url; } } } });