5
\$\begingroup\$

I have this jQuery function I wrote up to set the size of an input based off of its data. In this case I have already figured out the widths for each character based off of a 12px font size for Helvetica.

It's not super flexible or anything, but I would like to know if there is any good way to take what I have and maybe make it more elegant (or fewer lines of code).

HTML:

<input value="Some really long string about nothing important"" />

CSS:

body {
    font-family:Helvetica;
    font-size:12px;
}
span {
    padding:0;
    margin:0;
}
input {
    border:0;
}

JavaScript / jQuery:

// Helvetica, 12px or 1.2em at 62.5%
var str = $('input').val();
var length = str.length;

var charWidth = 0;
for ( var i = 0; i < length; i++ )
{
    console.log(str[i]);

    if ( str[i] == "f" || 
         str[i] == "i" || 
         str[i] == "I" || 
         str[i] == "j" || 
         str[i] == "l" || 
         str[i] == "t" 
       )
    {
        charWidth += 3;
    }
    if ( str[i] == "r" )
    {
        charWidth += 4;
    }
    if ( str[i] == "v" || 
         str[i] == "x" || 
         str[i] == "y" || 
         str[i] == "z" 
       )
    {
        charWidth += 5;
    }
    if ( str[i] == "c" || 
         str[i] == "k" ||
         str[i] == " "
       )
    {
       charWidth += 6;
    }
    if ( str[i] == "a" || 
         str[i] == "A" || 
         str[i] == "b" || 
         str[i] == "d" || 
         str[i] == "e" || 
         str[i] == "F" || 
         str[i] == "g" || 
         str[i] == "h" || 
         str[i] == "J" || 
         str[i] == "L" || 
         str[i] == "n" || 
         str[i] == "o" || 
         str[i] == "p" || 
         str[i] == "q" || 
         str[i] == "s" || 
         str[i] == "T" || 
         str[i] == "u" || 
         str[i] == "V" || 
         str[i] == "X" || 
         str[i] == "Y" || 
         str[i] == "Z" ||
         str[i] == "0" ||
         str[i] == "1" ||
         str[i] == "2" ||
         str[i] == "3" ||
         str[i] == "4" ||
         str[i] == "5" ||
         str[i] == "6" ||
         str[i] == "7" ||
         str[i] == "8" ||
         str[i] == "9" 
       )
    {
        charWidth += 7;
    }
    if ( str[i] == "B" ||
         str[i] == "E" || 
         str[i] == "K" || 
         str[i] == "P" || 
         str[i] == "S" 
       )
    {
        charWidth += 8;
    }
    if ( str[i] == "C" || 
         str[i] == "D" || 
         str[i] == "G" || 
         str[i] == "H" || 
         str[i] == "M" || 
         str[i] == "N" || 
         str[i] == "O" || 
         str[i] == "Q" || 
         str[i] == "R" || 
         str[i] == "U" || 
         str[i] == "w" 
       )
    {
        charWidth += 9;
    }

    if ( str[i] == "m" || str == "W")
    {
        charWidth += 11;
    }
}

$('input').css({'width': charWidth});

jsFiddle

\$\endgroup\$
  • 1
    \$\begingroup\$ I'd suggest an array of widths so you can just get the charcode of the character and look it's width up in the array. This would be a lot faster and more elegant than the giant if/else. You could also make this function a jQuery plugin so you could just so $('input').autoSize(); \$\endgroup\$ – jfriend00 Nov 20 '13 at 22:15
  • \$\begingroup\$ Yeah I had thought about using an array, but wasn't quite sure how to implement it. \$\endgroup\$ – Kris Hollenbeck Nov 20 '13 at 22:21
5
\$\begingroup\$

Here's how you could use a table array for the widths. I didn't fill in all the character widths (I just did lowercase) because it's tedious to build, but you can extend it to contain all the typeable characters that you want to support. I also made it a jQuery method that you can call like this:

$("input").autoSize();

Here's the code (also in a jsFiddle):

(function() {
    var widths = [
     // a, b, c, d, e, f, g
        7, 7, 6, 7, 7, 3, 7,
     // h, i, j, k, l, m, n
        7, 3, 3, 6, 3, 11, 7,
     // o, p, q, r, s, t, u
        7, 7, 7, 4, 7, 3, 7,
     // v, w, x, y, z
        5, 9, 5, 5, 5
    ];

    // character code our table starts with
    var lowWidth = 97;

    $.fn.autoSize = function() {
        return this.each(function() {
            var val = this.value;
            var totalWidth = 0, charIndex, ch;
            for (var i = 0, len = val.length; i < len; i++) {
                // get char code and see if it's in our width table
                charIndex = val.charCodeAt(i) - lowWidth;
                ch = val.charAt(i);
                if (charIndex >= 0 && charIndex < widths.length) {
                    totalWidth += widths[charIndex];
                } else if (ch == ' ') {
                    // special case for space char 
                    // until the table contains all codes we need
                    totalWidth += 6;
                }
            }
            $(this).css("width", totalWidth + "px");
        });
    }
})();

FYI, jQuery will also measure the natural width of any DOM element for you so you could also have a span that is styled with the right font that you insert this text into and then ask jQuery what it's width is. jQuery will temporarily make it position absolute (so it will layout to it's natural width) and then let the browser tell you how wide it is. This would be much, much more accurate than what you are doing.

Letting jQuery measure it for you would look like this:

$.fn.autoSize = function() {
    var testItem = $("#testWidth");
    return this.each(function() {
        // put the text into our test span
        testItem.text(this.value);
        $(this).width(testItem.width());
    });
}

And, you'd have appropriate CSS for a testItem span (see the jsFiddle demo for details):

jsFiddle demo: http://jsfiddle.net/jfriend00/jQ93f/

\$\endgroup\$
  • \$\begingroup\$ @KrisHollenbeck - Added simpler way that uses the system to measure the text for you. \$\endgroup\$ – jfriend00 Nov 21 '13 at 0:28
  • \$\begingroup\$ I appreciate the time you put into this answer and you gave me some good ideas. But I ended up doing it slightly different. Feel free to add comments to my answer if you see areas of improvement. I will leave this open for a while before I choose an answer. \$\endgroup\$ – Kris Hollenbeck Nov 21 '13 at 16:43
3
\$\begingroup\$

jfriend Got me thinking about arrays / objects as an option. End the end this is where I went with it. I am not sure if it is any better or more elegant than jfriend's answer. But I like the idea of an object so I will have the ability to expand to other fonts and sizes. Also this seems like less code, but it may not be faster. Which is also important. So anyways I am open to criticism / ideas.

HTML:

<input value="Some really long string about nothing. And some other characters !@#$%&*():;/" />

JS:

 // Helvetica, 12px or 1.2em at 62.5%

 var characters = {
     "a" : 7, "A" : 7, "b" : 7, "B" : 8, "c" : 6, "C" : 9,
     "d" : 7, "D" : 9, "e" : 7, "E" : 8, "F" : 7, "f" : 3,
     "g" : 7, "G" : 9, "h" : 7, "H" : 9, "i" : 3, "I" : 3,
     "j" : 3, "J" : 7, "k" : 5, "K" : 8, "l" : 3, "L" : 7, 
     "m" : 11, "M" : 9,"n" : 7, "N" : 9, "o" : 7, "O" : 9,
     "p" : 7, "P" : 8, "q" : 7, "Q" : 9, "r" : 4, "R" : 9,
     "s" : 7, "S" : 8, "t" : 3, "T" : 7, "u" : 7, "U" : 9,
     "v" : 5, "V" : 7, "w" : 9, "W" : 11,"x" : 5, "X" : 7,
     "y" : 5, "Y" : 7, "z" : 5, "Z" : 7, "0" : 7, "1" : 7,
     "2" : 7, "3" : 7, "4" : 7, "5" : 7, "6" : 7, "7" : 7,
     "8" : 7, "9" : 7, " " : 5, "." : 3, "," : 3, ";" : 3,
     "'" : 2, "\"" : 4, "!" : 3, "@" : 12, "\/" : 3, "$" : 7,
     "%" : 11, "&" : 8, "*" : 5, "(" : 4, ")" : 4, "-" : 4,
     "_" : 7, "+" : 7, "=" : 7, "?" : 7, "|" : 3, "#" : 7,
     ":" : 3
 };

$.fn.dataWidth = function() {
    var length = str.length;
    var charWidth = 0;
    for ( var i = 0; i < length; i++ )
    {
       var elWidth = characters[str[i]];
       charWidth += elWidth;
    }

    $(this).css('width', charWidth);
}

var str = $('input').val();
$('input').dataWidth(str);

FIDDLE:

http://jsfiddle.net/krishollenbeck/z9hhvhe3/1/

\$\endgroup\$
  • \$\begingroup\$ I would at least put the initialization of the object outside of the function, init it once. Also your object misses some punctuation chars (!?,-'") \$\endgroup\$ – konijn Nov 21 '13 at 19:28
  • \$\begingroup\$ Good point. And yeah I figured I was going to have to add some more characters. I have updated my answer to reflect that change. \$\endgroup\$ – Kris Hollenbeck Nov 21 '13 at 23:00
1
\$\begingroup\$

I would agree with jonijin's answer, as there are many things, zoom level, font-size overrides, browser vendor and even OS that could skew the actual widths of a string. Not to mention, the other non DOM+Browser methods, won't work for all font-families or other non Latin characters.

Also, based on all the answers above, i don't see any checks to prevent "over sizing" the input fields...

I've solved this type of thing with CSS in the past here is a simple jsbin demonstrating the basic concept https://jsbin.com/wakabadeni/edit?html,css,output

Of course I'm not sure that would work for your use case, just an idea.

\$\endgroup\$
  • \$\begingroup\$ You can embed executable JavaScript in your post (Ctrl+M), instead of having it off-site behind an eventual rotted link. As answers must relate to the OP's code, please take the time to explain how that independent solution solves "this type of thing". Code-only answers that consist of independent solutions, are subject to deletion... and that's with the code embedded in the post... \$\endgroup\$ – Mathieu Guindon Apr 3 '17 at 14:28
0
\$\begingroup\$

It seems to me that the approach is wrong, you really should let the browser calculate the width, it's what is is good at.

From an SO answer, I would use this:

function textWidth(font)
{
  var f = font || '12px arial',
      o = $('<div>' + this + '</div>')
            .css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden', 'font': f})
            .appendTo($('body')),
      w = o.width();
  o.remove();
  return w;
}
\$\endgroup\$

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.