2
\$\begingroup\$

I made my first jQuery plugin, and was hoping to get a review. I'm especially interested in comments on whether I went about building up my html and applying the css in a reasonable manner (I chose to do it all in jQuery to keep the dependencies limited).

I welcome most of all comments regarding ways to improve the code structure, cleanliness and clarity - is it something you'd feel comfortable working on and customizing if you were asked?

(function ($) {

    'use strict';

    $.fn.geoInput = function (options) {

        var GI, helper, settings, t;

        GI = this;

        GI.map        = null;
        GI.mapOptions = {};
        GI.marker     = new google.maps.Marker();

        settings = $.extend({
            apikey : "<APIKEY>",
            width : "300px",
            height : "150px",
            bgColor : "#A3C3FF",
            precision : 6,
            zoomLevel : 2,
            zoom: '',
            mapCenter : '0,0'
        }, options);

        /** 
         * HELPERS
         */
        helper = {

            /**
             * google maps requires a distinct id, so if someone wants more than one on a page, we'll need to generate random id
             */
            getMapId : function () {
                return 'dmgig_' + (Math.random() + 1).toString(36).substring(2, 7);
            },

            /**
             * latLng string to to google obj. '0,0'
             */
            latLngStringToGoogle : function (latLngString) {
                var center;
                center = latLngString.split(',');
                return new google.maps.LatLng(center[0], center[1]);
            },

            latLngGoogleToString : function (latLngGoogle) {
                return latLngGoogle.lat().toFixed(settings.precision) + ',' + latLngGoogle.lng().toFixed(settings.precision);
            }
        };

        /**
         * PREFERENCES
         */
        GI.prefs = {

            storeZoomLevel : function () {
                sessionStorage.dmgig_zoomLevel = GI.map.getZoom();
                console.log(sessionStorage);
            },

            storeMapCenter : function () {
                sessionStorage.dmgig_mapCenter = helper.latLngGoogleToString(GI.map.getCenter());
                console.log(sessionStorage);
            },

            //storeMarkerCount : function () {}, // todo

            getStoredZoom : function () {
                return parseInt(sessionStorage.dmgig_zoomLevel, 10);
            },

            getStoredMapCenter : function () {
                return helper.latLngStringToGoogle(sessionStorage.dmgig_mapCenter);
            },

            //getMarkerCount : function () {}, // todo

            clearPreferences : function () {
                sessionStorage.removeItem('dmgig_zoomLevel');
                sessionStorage.removeItem('dmgig_mapCenter');
                sessionStorage.removeItem('dmgig_markerCount');
                console.log(sessionStorage);
            }
        };

        // set prefs options
        settings.mapCenter = helper.latLngStringToGoogle(settings.mapCenter);
        if (sessionStorage.getItem('dmgig_zoomLevel') !== null) { settings.zoomLevel = GI.prefs.getStoredZoom(); }
        if (sessionStorage.getItem('dmgig_mapCenter') !== null) { settings.mapCenter = GI.prefs.getStoredMapCenter(); }

        // wipe
        this.empty();

        /**
         * layout html */
        t = {};
        // main template
        t.mapid = helper.getMapId();

        t.main         = $('<div/>');
        t.map          = $('    <div/>', { id : t.mapid }).appendTo(t.main);
        t.mapControls  = $('       <div/>').appendTo(t.main);
        t.geoControls  = $('       <div/>').appendTo(t.main);
        t.clear        = $('           <div style="clear:both"></div>').appendTo(t.geoControls);
        t.rGeoResults  = $('       <div/>').appendTo(t.main);
        t.rgcTable     = $('           <table>').appendTo(t.rGeoControls);
        t.rgctHead     = $('               <thead>').appendTo(t.rgcTable);
        t.rgcTr        = $('                   <tr>').appendTo(t.rgcHead);
        t.rgcTh1       = $('                       <th>&nbsp;&nbsp;&nbsp;Result Sets Found:</th>').appendTo(t.rgcTr);
        t.rgcTh2       = $('                       <th></th>').appendTo(t.rgcTr);
        this.append(t.main); // append main template

        /**
         * CREATE interactive and other elements */
        // display
        t.latLngDisplay         = $('<div/>', {});
        t.revGeoCodeResultsBody = $('<tbody>', {});
        t.revGeoCodeFoundCount  = $('<span/>', {});
        t.prefsPanel            = $('<div/>', {});
        // buttons
        t.geoCode               = $('<div/>', { title : 'geocode from text' });
        t.revGeoCode            = $('<div/>', { title : 'reverse geocode marker location' });
        t.markerToCenter        = $('<div/>', { title : 'bring marker(s) to center' });
        t.centerOnMarker        = $('<div/>', { title : 'center map on marker(s)' });
        t.revGeoCodeResultsHide = $('<span/>', { title : 'hide results' });
        // prefs buttons
        t.togglePrefs        = $('<div/>', { title : 'toggle preferences' });
        t.storeZoomLevel     = $('<div/>', { title : 'store zoom level' });
        t.storeMapCenter     = $('<div/>', { title : 'store map center' });
        t.storeMarkerCount   = $('<div/>', { title : 'store marker count' });
        t.clearPreferences   = $('<div/>', { title : 'clear data storage' });
        // inputs, and input containers
        t.geoCodeInput       = $('<input/>', {});
        t.latInput           = $('<input/>', { type : 'hidden', name : 'lat' });
        t.lngInput           = $('<input/>', { type : 'hidden', name : 'lng' });
        t.hiddenInputs       = $('<div/>', {}); // div to contain hidden inputs from geocoding results
        // other elements
        t.spacer             = $('<div/>', {}); // layout spacer

        /**
         * ATTACH interactive elements */
        $(t.mapControls).append(t.latLngDisplay);

        $(t.geoControls).prepend(t.geoCode, t.geoCodeInput, t.revGeoCode, t.togglePrefs, t.spacer, t.markerToCenter, t.centerOnMarker);

        $(t.rgcTh1).prepend(t.revGeoCodeResultsHide);
        $(t.rgcTh1).append(t.revGeoCodeFoundCount);

        $(t.rGeoResults).append(t.revGeoCodeResultsBody);
        $(t.rGeoResults).after(t.latInput, t.lngInput, t.hiddenInputs);

        t.prefsPanel.append(t.storeZoomLevel, t.storeMapCenter, t.clearPreferences); // todo: t.storeMarkerCount
        $(t.geoControls).after(t.prefsPanel);

        /**
         * CSS */
        t.color = '#000';
        t.bgcolor = '#DDD';

        function button(selector, content, float) {
            selector.css('float', float)
                    .css('height', '12px')
                    .css('width', '12px')
                    .css('padding', '1px')
                    .css('margin', '1px')
                    .css('border', '1px solid #999')
                    .css('border-radius', '4px')
                    .css('color', t.color)
                    .css('background-color', t.bgcolor)
                    .css('text-align', 'center')
                    .css('font-size', '12px')
                    .css('font-family', 'console')
                    .css('cursor', 'pointer')
                    .html(content);
            selector.hover(function () {
                $(this).css('color', t.bgcolor)
                       .css('background-color', t.color);
            }, function () {
                $(this).css('color', t.color)
                       .css('background-color', t.bgcolor);
            });
            return selector;
        }

        function prefsButton(selector, content) {
            selector.css('height', '12px')
                    .css('padding', '1px')
                    .css('margin', '1px')
                    .css('border', '1px solid #999')
                    .css('border-radius', '4px')
                    .css('color', t.color)
                    .css('background-color', t.bgcolor)
                    .css('text-align', 'center')
                    .css('font-size', '12px')
                    .css('font-family', 'console')
                    .css('cursor', 'pointer')
                    .html(content);
            selector.hover(function () {
                $(this).css('color', t.bgcolor)
                       .css('background-color', t.color);
            }, function () {
                $(this).css('color', t.color)
                       .css('background-color', t.bgcolor);
            });
            return selector;
        }

        t.main.css('width', settings.width)
              .css('position', 'relative')
              .css('border', '1px solid #AAA');

        t.map.css('width', settings.width)
             .css('height', settings.height)
             .css('border', '1px solid #CCC')
             .css('background-color', settings.bgColor)
             .css('text-align', 'center')
             .html('<br /><br />loading...');

        t.rGeoResults.css('clear', 'both')
                     .css('display', 'none')
                     .css('left', '-1px')
                     .css('width', 'calc(' + settings.width + ' - 8px)')
                     .css('background-color', '#FFF')
                     .css('border', '2px solid #CCC')
                     .css('position', 'absolute')
                     .css('z-index', '5555')
                     .css('font-size', '12px')
                     .css('font-family', 'Arial')
                     .css('padding', '3px');

        t.prefsPanel.css('clear', 'both')
                    .css('display', 'none')
                    .css('left', '-1px')
                    .css('width', 'calc(' + settings.width + ' - 2px)')
                    .css('background-color', '#DDD')
                    .css('border', '2px solid #CCC')
                    .css('position', 'absolute')
                    .css('z-index', '9999');

        t.latLngDisplay.css('font-size', '12px')
                       .css('text-align', 'center')
                       .css('background-color', t.color)
                       .css('color', t.bgcolor)
                       .css('margin-bottom', '2px')
                       .html('0,0');

        button(t.geoCode, '&#9992;', 'left');
        button(t.revGeoCode, '?', 'left');
        button(t.markerToCenter, '&#9873;', 'right');
        button(t.centerOnMarker, '&#x2750;', 'right');
        button(t.togglePrefs, '&#9881;', 'right');
        t.revGeoCodeResultsHide.html('&times;').css('cursor', 'pointer');

        prefsButton(t.storeZoomLevel, 'store zoom level');
        prefsButton(t.storeMapCenter, 'store map center position');
        prefsButton(t.storeMarkerCount, 'store markers count');
        prefsButton(t.clearPreferences, 'clear storage data');

        t.geoCodeInput.css('float', 'left');

        // other elements
        t.spacer.css('width', '12px').css('height', '12px').css('padding', '1px').css('margin', '1px').css('float', 'right');

        /**
         * attach events
         */
        t.geoCode.on('click', function () {
            GI.geocodeTextLocation();
        });

        t.revGeoCode.on('click', function () {
            GI.reverseGeocodeMarkerPosition();
        });

        t.centerOnMarker.on('click', function () {
            GI.centerToMarker();
        });

        t.markerToCenter.on('click', function () {
            GI.markerToCenter();
        });

        t.storeZoomLevel.on('click', function () { GI.prefs.storeZoomLevel(); });
        t.storeMapCenter.on('click', function () { GI.prefs.storeMapCenter(); });
        // t.storeMarkerCount.on('click',function () { GI.prefs.storeMarkerCount() }); // todo
        t.clearPreferences.on('click', function () { GI.prefs.clearPreferences(); });

        // rev geocode result set actions
        t.revGeoCodeResultsHide.on('click', function () { t.rGeoResults.hide(); });

        t.togglePrefs.click(function () {
            var clicks = $(this).data('clicks');
            if (clicks) {
                t.prefsPanel.slideToggle();
            } else {
                t.prefsPanel.slideToggle();
            }
            $(this).data("clicks", !clicks);
        });

        /**
         * MAP INITILIZATION
         */
        this.initialize = function () {
            GI.mapOptions = {
                zoom : settings.zoomLevel,
                center : settings.mapCenter
            };

            GI.map = new google.maps.Map(document.getElementById(t.mapid), GI.mapOptions);

            GI.marker.setPosition(settings.mapCenter);
            GI.marker.setMap(GI.map);
            GI.marker.setDraggable(true);

            GI.setLatLngInputs();
        };

        /**
         * MARKER FUNCTIONS
         */

        /**
         * marker event listener - drag end */
        GI.watchMarkerMove = google.maps.event.addListener(GI.marker, 'dragend', function () {
            GI.setLatLngInputs();
            t.hiddenInputs.empty(); // clear out the rev geocode inputs, just in case they exist
        });

        /**
         * sets the hidden lat/lng inputs with the current marker position, and updates the lat/lng display as well. */
        GI.setLatLngInputs = function () {

            var latLng, s_latLng;

            latLng = GI.marker.getPosition();
            s_latLng = latLng.lat().toFixed(settings.precision) + ',' + latLng.lng().toFixed(settings.precision);
            t.latLngDisplay.html(s_latLng);
            t.latInput.val(latLng.lat().toFixed(settings.precision));
            t.lngInput.val(latLng.lng().toFixed(settings.precision));
        };

        /**
         * sets the map center to the current marker position */
        GI.centerToMarker = function () {
            GI.map.setCenter(GI.marker.getPosition());
            GI.setLatLngInputs();
        };

        /**
         * sets the marker position to the map center */
        GI.markerToCenter = function () {
            GI.marker.setPosition(GI.map.getCenter());
            GI.setLatLngInputs();
        };

        /**
         * GEOCODE FUNCTIONS
         */

        /**
         * attempts geocode a location based on user input in the geocode search field. If successful, the map is centered on the lat/lng */
        GI.geocodeTextLocation = function () {

            var uriEncodedLocation, center_to, search_input;

            search_input = t.geoCodeInput.val();
            uriEncodedLocation = encodeURIComponent(t.geoCodeInput.val());
            $.get('https://maps.googleapis.com/maps/api/geocode/json?address=' + uriEncodedLocation, function (data) {
                center_to = new google.maps.LatLng(data.results[0].geometry.location.lat, data.results[0].geometry.location.lng);
                GI.map.setCenter(center_to);
                GI.markerToCenter();
            }).fail(function () { alert('Failed to geocode ' + search_input); });
        };

        /**
         * REVERSE GEOCODE FUNCTIONS
         */

        /**
         * reverse geocodes based on marker position, displays the results, if any, in a table below the input display */
        GI.reverseGeocodeMarkerPosition = function () {

            var latLng, s_latLng, uriEncodedLatLng;

            latLng = GI.marker.getPosition();
            s_latLng = latLng.lat().toString() + ',' + latLng.lng().toString();
            uriEncodedLatLng = encodeURIComponent(s_latLng);

            $.get('https://maps.googleapis.com/maps/api/geocode/json?latlng=' + uriEncodedLatLng + '&location_type=ROOFTOP&result_type=street_address&key=' + settings.apikey, function (data) {

                var components, i;

                t.hiddenInputs.empty();
                t.revGeoCodeResultsBody.empty();
                t.revGeoCodeFoundCount.html(data.results.length);
                t.rGeoResults.show();
                if (data.results.length === 0) { return; }

                components = data.results[0].address_components;
                for (i in components) {
                    $(GI.revGeocodeResultsMakeRow(components[i].long_name, components[i].types)).appendTo(t.revGeoCodeResultsBody);
                    $(GI.revGeocodeResultsAddHiddenInputs(components[i].short_name, components[i].types));
                }

            }).fail(function () { alert('Failed to reverse geocode ' + s_latLng); });
        };

        /**
         *  creates table row for with long_name and type */
        this.revGeocodeResultsMakeRow = function (long_name, types) {
            return $('<tr><td><b>' + long_name + '</b> <i>' + types.join(', ') + '</i></td><td>&square;</td></tr>');
        };

        /**
         * appends hidden input to parent div, uses first type element as name, value with short name */
        this.revGeocodeResultsAddHiddenInputs = function (short_name, types) {
            $('<input/>', { type : "hidden", name : types[0], value : short_name }).appendTo(t.hiddenInputs);
        };

        /**
         * PARENT FORM CONTROL
         *
         * on submit, disable the extra inputs so they aren't serialized into the form data. 
         * because disabled elements in some browsers won't respond to jQuery, we just wait for the serialization to happen
         * and then re-enable the fields
         */
        GI.parent_form = $(this).closest('form');
        GI.parent_form.on('submit', function () {
            t.geoCodeInput.prop('disabled', true); // disable the search input so it doesn't appear in results
            setTimeout(function () { // re-enable the search_input field after serialize has executed
                t.geoCodeInput.prop('disabled', false);
            }, 300);
            return false;
        });

        /** INITIALIZE */
        this.initialize();

    };

}(jQuery));
\$\endgroup\$
1
\$\begingroup\$

Take a look at jQuery's cssHooks. It is to my understanding that this Object is used to map CSS properties and it's vendor prefixes to the animate() and css() functions.

In that sense these functions take in an object literal, which in itself is a key:value mapper. In plain english: don't repeat .css() for each property you want to set.

The .css() function has a way to pass one property, or one object literal:

myObject.css('width', 100); // one prop
myObject.css({ width: 100, height: 100 }); // object literal

Another note on this part: I see a lot of people using strings as value when it comes down to integers. By default, jQuery uses pixel values for CSS properties. In my opinion, the less strings you need inside your code, the better it's readability and elegance.

For example:

var cssGeoResults = {
    clear: 'both',
    display: 'none',
    left: -1,
    width: settings.width - 8,
    backgroundColor: '#FFF',
    'border': '2px solid #CCC',
    position: 'absolute',
    zIndex: 5555,
    fontSize: 12,
    fontFamily: 'Arial',
    'padding': 3
};

t.rGeoResults.css(cssGeoResults);

Not all properties are mapped in the cssHooks, hence it's possibility to extend this object. The shorthand properties like padding and border will only work with quotes. You can split those up and then you don't need quotes.

The naming convention used for these properties comes from the HTML DOM Style object.

With that beying said, I wonder if you want to keep dependency to a minimum or the amount of dependency files? Only the important css properties regarding to layout (float, overflowX, ...) can be in your code. Everything else regarding look 'n feel (font, color, ...) should go in an external css file. That way you'll make it easier for people to simply change the look 'n feel. Since that's not really a dependency.

\$\endgroup\$
  • \$\begingroup\$ Great, thanks for all of this. I will def follow the advice on setting up the css. Re: the dependencies - my main goal was to keep dependent files to a minimum, just for the sake of simple installation. I thought also, by keeping the css inline, it was less likely to get overridden by other properties the developers might add. \$\endgroup\$ – dgig Jan 24 '15 at 16:22
  • \$\begingroup\$ It's a good idea to keep dependant files to a minimum but you should allow people to easily change it's look 'n feel as well. Preferably an external css file, not through JavaScript settings => separation of concerns. Then, as a developer, I can choose to take only the script. This will improve the flexibility. \$\endgroup\$ – Tim Vermaelen Jan 24 '15 at 16:43
  • \$\begingroup\$ Sounds wise. Do you have any recommendations on name spaces for css? I have just found this: smacss.com/book/type-modules \$\endgroup\$ – dgig Jan 24 '15 at 17:49
  • \$\begingroup\$ Hmm no, sorry. No experience in using these. \$\endgroup\$ – Tim Vermaelen Jan 24 '15 at 21:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.