6
\$\begingroup\$

To keep it succinct, I created a simple way to input dates and times into an input box without worrying about formatting. As an alternative to date time pickers, dropdowns and all the other various datetime libraries.


    /**
 * Loosetime.js
 *
 * Author: Bruce Taylor
 * URL: brucetaylor.xyz
 * Date: 31/12/2015
 * License: MIT License, Open source
 */
/*global document, console, window  */
var loosetime = (function () {
    "use strict";

    /**
     * looseconfig provides rules and placeholder values to allow loosetime to work with missing arguments
     * @type {{format: string, value: string, class: string, delims: string[]}}
     */
    var looseconfig = {
        "format": "DD/MM/YY HH24:MM:SS.s",
        "value": "DD/MM/YY 00:00.00",
        "class": "input",
        "delims": ['/', '-', '_', '.', '|', ',', ' ', ':']
    };

    /**
     * resetCursorPos sets the caret to position zero allowing the user to easily type in dates
     * @param element
     */
    function resetCursorPos(element) {
        element.selectionStart = 0;
        element.selectionEnd = 0;
    }

    /**
     * parseDateTime creates a string to validate against for the given DateTime format
     * @param dateTime
     * @returns {string}
     */
    function parseDateTime(dateTime) {
        var validString = "",
            splitDateTime = dateTime.split(/[\/\/:.\s]/g);
        splitDateTime.forEach(function (stringvalidationSegment) {
            switch (stringvalidationSegment) {
                case "DD":
                    validString += "31";
                    break;
                case "MM":
                    validString += "12";
                    break;
                case "YY":
                    validString += "99";
                    break;
                case "YYYY":
                    validString += "2999";
                    break;
                case "HH24":
                    validString += "23";
                    break;
                case "HH12":
                    validString += "11";
                    break;
                case "mm":
                    validString += "59";
                    break;
                case "SS":
                    validString += "59";
                    break;
                case "s":
                    validString += "99";
                    break;
            }
            validString += "/";
        });
        return validString;
    }

    /**
     * dateTimeRules is where the magic happens, it intercepts the keypress and validates it against the current input
     * @param e event
     * @param target
     */
    function dateTimeRules(e, target) {
        var event = window.event || e,
            input = target,
            delims,
            validationSegment,
            inputvalidationSegment,
            newSel,
            thisChar,
            explode,
            char,
            sel,
            val;
        if (event.charCode >= 48 && event.charCode <= 57) { //user enters a number
            delims = looseconfig.delims;
            sel = input.selectionStart;
            val = input.value;
            explode = val.split("");
            char = event.charCode - 48;
            if (sel <= input.maxLength + 1) {
                thisChar = input.getAttribute('data-loosetime').charAt(sel);
                if (!Number(thisChar)) { //if we hit a delimeter check the validation char after it
                    thisChar = input.getAttribute('data-loosetime').charAt(sel + 1);
                }
                if (delims.indexOf(explode[sel]) !== -1) {
                    validationSegment = input.getAttribute('data-loosetime').substring(sel + 1, sel + 3);
                    if (char <= thisChar && !thisChar.isNaN) {
                        //need to evaluate the number validationSegment in the delimeter
                        explode = val.split("");
                        explode[sel + 1] = event.charCode - 48;
                        val = explode.join("");
                        newSel = sel + 2;
                    } else {
                        newSel = sel;
                    }
                } else {
                    validationSegment = input.getAttribute('data-loosetime').substring(sel === 0 ? 0 : sel - 1, sel === 0 ? sel + 2 : sel + 1);
                    inputvalidationSegment = input.value.charAt(sel === 0 ? 0 : sel - 1);
                    if (sel > 0) {
                        inputvalidationSegment = inputvalidationSegment.concat(char);
                        if (inputvalidationSegment <= validationSegment && !thisChar.isNaN) {
                            explode = val.split("");
                            explode[sel] = event.charCode - 48;
                            val = explode.join("");
                            newSel = sel + 1;
                        } else {
                            newSel = sel;
                        }
                    }
                    else {
                        if (char <= input.getAttribute('data-loosetime').charAt(sel)) {
                            explode = val.split("");
                            explode[sel] = event.charCode - 48;
                            val = explode.join("");
                            newSel = sel + 1;
                        }
                        else {
                            newSel = sel;
                        }
                    }
                }
                input.value = val;
                input.selectionStart = newSel;
                input.selectionEnd = newSel;
            }
        }
    }

    /**
     * constructor for loosetime
     * @param format
     * @param target
     * @param inputVal
     * @param inputName
     * @param inputClass
     */
    function init(format, target, inputVal, inputName, inputClass) {
        var inputLength,
            element,
            input;
        try {
            if (format === "undefined") {
                format = looseconfig.format;
            } else {
                format = parseDateTime(format);
            }
            try {
                if (inputVal === "undefined") {
                    inputVal = looseconfig.value;
                }
                inputLength = inputVal.length - 2;
                input = document.createElement("input");
                input.setAttribute("name", inputName);
                input.setAttribute("maxlength", inputLength);
                input.setAttribute("size", inputLength);
                input.setAttribute("value", inputVal);
                input.setAttribute("type", "input");
                input.setAttribute("class", inputClass);
                input.setAttribute("data-loosetime", format);
                input.addEventListener("click", function () {
                    resetCursorPos(this);
                });
                input.addEventListener("keypress", function (e) {
                    dateTimeRules(e, this);
                });
                try {
                    element = document.getElementById(target);
                    element.appendChild(input);
                } catch (e) {
                    console.error("Error, no element given to append loosetime to.");
                }
            } catch (e) {
                console.error("Error, value is invalid." + e.toString());
            }
        } catch (e) {
            console.error("Error, date format missing or invalid.");
        }
    }

    return init; //return constructor
})();

Things I have done:

  • Implemented module pattern
  • Used all vanilla JS
  • Enumerated a config inside the LooseTime class

To do:

  • The code can be a little more DRY in some parts
  • Implement a jQuery version (if there is any point in doing that?)
\$\endgroup\$
  • \$\begingroup\$ Its just one library with 200 lines, shall I paste the whole thing? \$\endgroup\$ – tomaytotomato Dec 31 '15 at 17:08
  • \$\begingroup\$ I don't really have the energy to write a full review right now, but two things I want to mention: You are comparing with the string literal "undefined", while you seem to want to know if the variable is actually undefined (the token). Use of var a, b, c, d; etc is error-prone, because a typo (either missing , or a ; instead of a , will drop some variables into the global scope. \$\endgroup\$ – Sumurai8 Dec 31 '15 at 17:27
  • \$\begingroup\$ Thanks, are you looking at the right version? I removed that a few commits back. \$\endgroup\$ – tomaytotomato Dec 31 '15 at 17:32
  • \$\begingroup\$ I cannot access github from this network. I look at the code as pasted in this question. \$\endgroup\$ – Sumurai8 Dec 31 '15 at 17:33
  • \$\begingroup\$ It would be best if you show a demo here (snippet, jsfiddle etc.). \$\endgroup\$ – Joseph Dec 31 '15 at 20:24
5
\$\begingroup\$

Note: There is quite some code, and I have not looked at all of it. Using this review you can probably find quite some loose ends yourself.

Styling and readability

Single quote and double quote strings behave identical in javascript. I recommend to choose one of the two, and stick with it. Either escape the single or double quote when you need to use it instead of switching to the other type string.

You are declaring variables using the comma syntax. I recommend to instead use var for every variable instead, because the comma syntax is prone to errors. If you forget a comma, or have a semicolon instead, you drop some of your variables into the global scope. This can happen quite easily when modifying the variables in a particular part of your program.

You sometimes put else on a new line, and you sometimes put it on the same line as the previous }. I would recommend choosing one style, and sticking to it.

You are using some magic numbers, mainly "48" and "57". I would recommend replacing them with constants, and initializing those constants somewhere at the top. For example with "0".charCodeAt(0);.

Logic errors / bugs

You are strictly comparing parameters with the string literal "undefined". You likely meant to use typeof parameter === "undefined" or parameter === undefined.

Your script does not catch the backspace or delete keys. Using these keys will result in the input to be broken until the page reloads. You probably should replace deleted characters with their placeholder counterparts.

Your script allows for invalid dates, such as "39" for "DD" if you first enter "19", then go back and press "3". You probably have to reset the next character to "D" when you type the first.

Other possible improvements

Optional parameters

You can use the ternary operator for conditional assignment to condense some of the code, such as in the following case:

format = (typeof format === "undefined") ? looseconfig.format : parseDateTime(format);

parseDateTime(..) does not have a default case, and the split regex does not account for multiple delimiters. This will cause multiple delimiters to be put in the validation string. This does not seem to play well with the dateTimeRules(..) function, where you "just take the next char" if you hit a delimiter.

NaN

You are checking if a character in the validation string is a delimited by doing !Number(thisChar). Your validation string can currently not contain a 0, but this can change. I recommend changing this to Number.isNaN(Number(thisChar)) to better show what you want to accomplish, and to avoid a bug when your validation string for some reason can contain a 0. Alternatively, because your validation string seems to be always a /, why don't you just compare that?

Later, you are using !thisChar.isNaN, but I am not aware of such a property on a String. It thus will always be true.

Double character in regex

You have the / character twice in your regex in parseDateTime(..).

dateTime.split(/[\/\/:.\s]/g);

Duplicated code

In dateTimeRules(..) you do explode = val.split("");. In the if-statement, you do this again. I suppose you got halfway moving duplicated code out of the if-statement, but forgot to remove it from the if-statement.

Date validation

Your script happily accepts dates such as "31-03", even though there is no March 31rd. I am unsure how you would deal with this, as you allow multiple placeholders of the same type, and one would normally input the day before the month.

\$\endgroup\$
  • \$\begingroup\$ Thanks, a few of those points you mentioned are due to JSHint or JSLint complaining. So I changed my code to conform to it. I think I will ignore JSHint for the most part. \$\endgroup\$ – tomaytotomato Jan 2 '16 at 11:38
  • 1
    \$\begingroup\$ JSLint is great, but it can sometimes be a bit pedantic. I would recommend taking a look at it, looking at your code if it is consistent, looking at the warning it gives to find out if it would make your code more readable and otherwise just ignore it. The things I put under styling is things I notice, even if I don't actively look for them. \$\endgroup\$ – Sumurai8 Jan 2 '16 at 12:01

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.