resize font to fit in a div (on one line)

2019-01-02 17:40发布

问题:

There have been similar questions asked, but the solutions do mesh with what I'm trying to do. Basically, I have an article with a title (<h1>). I don't want to control the length of the title, but I also don't want the title to appear on multiple lines. Is there a way with css or jQuery to resize text based on the width of a <div> tag?

I know what I would do with pseudocode if I could detect the overlap of the text to the edge of the <div>:

var fontize = $("#title").css("font-size");
var i = /*remove unit from integer*/
while( /*text overlaps div*/ ){
    $("#title").css("font-size", --i+"pt");
}

If there's a CSS attribute I can set that would be even nicer, but I can't seem to find one (overflow wouldn't work in this situation).

回答1:

CSS no, Javascript yes

There's no way you could do this using CSS, but you can do it in javascript/jQuery. To help you with your pseudo code because you already know what to do. It's just that you don't know how to detect excess width.

The best way would be to have a DIV with following (at least) style:

position: absolute;
visibility: hidden;
white-space: nowrap;
font-family: /* same as your title's */

then copy your text to it and set some starting font size. Then you can iterate through your while loop and stop when div's width is appropriate. Then set calculated font size to your original title.

This way this checking will be hidden from user's eyes and it will therefore work faster as well.

BTW: This is the usual way how auto growing textarea scripts work. They use dummy divs with same style settings as the original text area and adjust area's height as the user types in text. So Text area can be quite small at first but if user types in lots of text it will auto grow to accommodate content.

While loop optimization

You could optimize your while loop to decrease the number of iterations considerably by doing this:

  1. Set a starting font size.
  2. get test DIV's width.
  3. calculate width factor between orig_div and test_div.
  4. adjust font size by this factor rather than increase/decrease by one unit
  5. test test_div width


回答2:

I had a similar issue, which made me write my own plugin for this. Have a look at jquery-quickfit (which is quite similar to Robert Koritnik's solution, which I really like).

In order to prevent the headline to span multiple lines, just add a css style of:

white-space:nowrap;

to the element.

After including jquery and quickfit in the header. You can trigger quickfit with:

$('h1').quickfit();

It meassures and calculates a size invariant meassure for each letter of the text to fit and uses this to calculate the next best font-size which fits the text into the container.

The calculations are cached, which makes it very fast when dealing having to fit multiple text or having to fit a text multiple times, like e.g., on window resize (there is almost no performance penalty on resize).

Demo for a similar situation as yours

Further documentation, examples and source code are on the project page.



回答3:

Here are 3 functions I use frequently to get the text width, height and adjust the size to the container's width.

  • getTextWidth() will return you the actual width of the text contained in the initiator.
  • getTextHeight(width) will return the actual height of the wrapped text contained in the initiator with a certain specified width.
  • autoTextSize(minSize, maxSize, truncate) will size the text within the container so it fits, considering a minimum and maximum size.
  • autoTruncateText() will only show the characters the user can actually see and end the text with '...'.
(function ($) {
  $.fn.getTextWidth = function() {
    var spanText = $("BODY #spanCalculateTextWidth");

    if (spanText.size() <= 0) {
      spanText = $("<span id='spanCalculateTextWidth' style='filter: alpha(0);'></span>");
      spanText.appendTo("BODY");
    }

    var valu = this.val();
    if (!valu) valu = this.text();

    spanText.text(valu);

    spanText.css({
      "fontSize": this.css('fontSize'),
      "fontWeight": this.css('fontWeight'),
      "fontFamily": this.css('fontFamily'),
      "position": "absolute",
      "top": 0,
      "opacity": 0,
      "left": -2000
    });

    return spanText.outerWidth() + parseInt(this.css('paddingLeft')) + 'px';
  };

  $.fn.getTextHeight = function(width) {
    var spanText = $("BODY #spanCalculateTextHeight");

    if (spanText.size() <= 0) {
      spanText = $("<span id='spanCalculateTextHeight'></span>");
      spanText.appendTo("BODY");
    }

    var valu = this.val();
    if (!valu) valu = this.text();

    spanText.text(valu);

    spanText.css({
      "fontSize": this.css('fontSize'),
      "fontWeight": this.css('fontWeight'),
      "fontFamily": this.css('fontFamily'),
      "top": 0,
      "left": -1 * parseInt(width) + 'px',
      "position": 'absolute',
      "display": "inline-block",
      "width": width
    });

    return spanText.innerHeight() + 'px';
  };

  /**
   * Adjust the font-size of the text so it fits the container.
   *
   * @param minSize     Minimum font size?
   * @param maxSize     Maximum font size?
   * @param truncate    Truncate text after sizing to make sure it fits?
   */
  $.fn.autoTextSize = function(minSize, maxSize, truncate) {
    var _self = this,
        _width = _self.innerWidth(),
        _textWidth = parseInt(_self.getTextWidth()),
        _fontSize = parseInt(_self.css('font-size'));

    while (_width < _textWidth || (maxSize && _fontSize > parseInt(maxSize))) {
      if (minSize && _fontSize <= parseInt(minSize)) break;

      _fontSize--;
      _self.css('font-size', _fontSize + 'px');

      _textWidth = parseInt(_self.getTextWidth());
    }

    if (truncate) _self.autoTruncateText();
  };

  /**
   * Function that truncates the text inside a container according to the
   * width and height of that container. In other words, makes it fit by chopping
   * off characters and adding '...'.
   */
  $.fn.autoTruncateText = function() {
    var _self = this,
        _width = _self.outerWidth(),
        _textHeight = parseInt(_self.getTextHeight(_width)),
        _text = _self.text();

    // As long as the height of the text is higher than that
    // of the container, we'll keep removing a character.
    while (_textHeight > _self.outerHeight()) {
      _text = _text.slice(0,-1);
      _self.text(_text);
      _textHeight = parseInt(_self.getTextHeight(_width));
      _truncated = true;
    }

    // When we actually truncated the text, we'll remove the last
    // 3 characters and replace it with '...'.
    if (!_truncated) return;
    _text = _text.slice(0, -3);

    // Make sure there is no dot or space right in front of '...'.
    var lastChar = _text[_text.length - 1];
    if (lastChar == ' ' || lastChar == '.') _text = _text.slice(0, -1);
    _self.text(_text + '...');
  };
})(jQuery);


回答4:

@Clovis Six thank for your answer. It prove very usefull to me. A pity I cannot thanks you more than just a vote up.

Note: I have to change the "$J(" for "$(" for it to work on my config.

evendo, this out of the scope of this question and not in the use of SO, I extended your code to make it work for multi-line box with max-height.

  /**
   * Adjust the font-size of the text so it fits the container
   *
   * support multi-line, based on css 'max-height'.
   * 
   * @param minSize     Minimum font size?
   * @param maxSize     Maximum font size?
   */
  $.fn.autoTextSize_UseMaxHeight = function(minSize, maxSize) {
    var _self = this,
        _width = _self.innerWidth(),
        _boxHeight = parseInt(_self.css('max-height')),
        _textHeight = parseInt(_self.getTextHeight(_width)),
        _fontSize = parseInt(_self.css('font-size'));

    while (_boxHeight < _textHeight || (maxSize && _fontSize > parseInt(maxSize))) {
      if (minSize && _fontSize <= parseInt(minSize)) break;

      _fontSize--;
      _self.css('font-size', _fontSize + 'px');

      _textHeight = parseInt(_self.getTextHeight(_width));
    }
  };

PS: I know this should be a comment, but comments don't let me post code properly.



回答5:

Today I created a jQuery plugin called jQuery Responsive Headlines that does exactly what you want: it adjusts the font size of a headline to fit inside its containing element (a div, the body or whatever). There are some nice options that you can set to customize the behavior of the plugin and I have also taken the precaution to add support for Ben Altmans Throttle/Debounce functionality to increase performance and ease the load on the browser/computer when the user resizes the window. In it's most simple use case the code would look like this:

$('h1').responsiveHeadlines();

...which would turn all h1 on the page elements into responsive one-line headlines that adjust their sizes to the parent/container element.

You'll find the source code and a longer description of the plugin on this GitHub page.

Good luck!



回答6:

Very simple. Create a span for the text, get the width and reduce font-size until the span has the same width of the div container:

while($("#container").find("span").width() > $("#container").width()) {
    var currentFontSize = parseInt($("#container").find("span").css("font-size")); 
    $("#container").find("span").css("font-size",currentFontSize-1); 
}


回答7:

This may be overkill for what you require, but I found this library to be very helpful:

http://fittextjs.com/

It's only good for single lines though, so I'm not certain if that fits your requirement.



回答8:

I wasn't in love with the other solutions out there, so, here's another one. It requires the tag you're resizing the text inside of:

  1. Be fixed height

  2. Not be so long that it'd overrun the boundaries at 10px - the idea being, you don't want it to shoot below that and resize text to become unreadable. Not an issue in our use case, but if it's possible in yours you'd want to give some thoughts to separately truncating before running this.

It uses a binary search to find the best size so I suspect it outperforms a lot of the other solutions here and elsewhere that just step the font-size down by a pixel over and over. Most browsers today support decimals in font sizes as well, and this script has some benefits there since it will get to within .1px of the best answer, whatever that is, even if it's a relatively long decimal. You could easily change the max - fontSize < .1 to some other value than .1 to get less precision for less CPU usage.

Usage:

$('div.event').fitText();

Code:

(function() {
    function resizeLoop(testTag, checkSize) {
        var fontSize = 10;
        var min = 10;
        var max = 0;
        var exceeded = false;

        for(var i = 0; i < 30; i++) {
            testTag.css('font-size', fontSize);
            if (checkSize(testTag)) {
                max = fontSize;
                fontSize = (fontSize + min) / 2;
            } else {
                if (max == 0) {
                    // Start by growing exponentially
                    min = fontSize;
                    fontSize *= 2;
                } else {
                    // If we're within .1px of max anyway, call it a day
                    if (max - fontSize < .1)
                        break;

                    // If we've seen a max, move half way to it
                    min = fontSize;
                    fontSize = (fontSize + max) / 2;
                }
            }
        }

        return fontSize;
    }

    function sizeText(tag) {
        var width = tag.width();
        var height = tag.height();

        // Clone original tag and append to the same place so we keep its original styles, especially font
        var testTag = tag.clone(true)
        .appendTo(tag.parent())
        .css({
            position: 'absolute',
            left: 0, top: 0,
            width: 'auto', height: 'auto'
        });

        var fontSize;

        // TODO: This decision of 10 characters is arbitrary. Come up
        // with a smarter decision basis.
        if (tag.text().length < 10) {
            fontSize = resizeLoop(testTag, function(t) {
                return t.width() > width || t.height() > height;
            });
        } else {
            testTag.css('width', width);
            fontSize = resizeLoop(testTag, function(t) {
                return t.height() > height;
            });
        }

        testTag.remove();
        tag.css('font-size', fontSize);
    };

    $.fn.fitText = function() {
        this.each(function(i, tag) {
            sizeText($(tag));
        });
    };
})();

http://jsfiddle.net/b9chris/et6N6/1/



回答9:

There is a jquery plugin available on github that probably just do what you want. It is called jquery-quickfit. It uses Jquery to provide a quick and dirty approach to fitting text into its surrounding container.

HTML:

<div id="quickfit">Text to fit*</div>

Javascript:

<script src=".../jquery.min.js" type="text/javascript" />
<script src="../script/jquery.quickfit.js" type="text/javascript" />

<script type="text/javascript">
  $(function() {
    $('#quickfit').quickfit();
  });
</script>

More information: https://github.com/chunksnbits/jquery-quickfit



回答10:

My solution, as a jQuery extension based on Robert Koritnik's answer:

$.fn.fitToWidth=function(){
    $(this).wrapInner("<span style='display:inline;font:inherit;white-space:inherit;'></span>").each(function(){
        var $t=$(this);
        var a=$t.outerWidth(),
            $s=$t.children("span"),
            f=parseFloat($t.css("font-size"));
        while($t.children("span").outerWidth() > a) $t.css("font-size",--f);
        $t.html($s.html());
    });
}

This actually creates a temporary span inheriting important properties, and compares the width difference. Granted, the while loop needs to be optimised (reduce by a percentage difference calculated between the two sizes).

Example usage:

$(function(){
    $("h1").fitToWidth();
});


回答11:

I've make a very little jQuery stuff to adjust the font size of your div. It even works with multiple lines, but will only increase font's size (you can simply set a default font size of 1px if you want). I increases the font size until having a line break, set decreases it 1px. No other fancy stuff, hidden div or so. Simply add the quickfit class to your div.

$(".quickfit").each(function() {
    var jThis=$(this);
    var fontSize=parseInt(jThis.css("font-size"));
    var originalLines=jThis.height()/fontSize;

    for(var i=0;originalLines>=jThis.height()/fontSize && i<30;i++)
    { jThis.css("font-size",""+(++fontSize)+"px"); }
    jThis.css("font-size",""+(fontSize-1)+"px");
});


回答12:

I understand that this question seems to have been answered fairly thoroughly, but there were some instances where solutions here would may cause other issues. For example, tkahn's library looked to be very useful, but it changed the display of the element it was attached to, which could prove to be a problem. In my case, it prevented me from centering the text both vertically and horizontally. After some messing around and experimenting, I have come up with a simple method involving jQuery to fit the text on one line without needing to modify any attributes of the parent element. Note that in this code, I have used Robert Koritnik's suggestion for optimizing the while loop. To use this code, simply add the "font_fix" class to any divs containing text needing to be fit to it in one line. For a header, this may require an extra div around the header. Then, either call this function once for a fixed size div, or set it to a resize and/or orientation listener for varying sizes.

function fixFontSize(minimumSize){
  var x = document.getElementsByClassName("font_fix");
  for(var i = 0; i < x.length; i++){
    x[i].innerHTML = '<div class="font_fix_inner" style="white-space: nowrap; display: inline-block;">' + x[i].innerHTML + '</div>';

    var y = x[i].getElementsByClassName("font_fix_inner")[0];
    var size = parseInt($("#" + x[i].id).css("font-size"));

    size *= x[i].clientWidth / y.clientWidth;
    while(y.clientWidth > x[i].clientWidth){
      size--;
      if(size <= minimumSize){
        size = minimumSize;
        y.style.maxWidth = "100%";
        y.style.overflow = "hidden";
        y.style.textOverflow = "ellipsis";
        $("#" + x[i].id).css("font-size", size + "px");
        break;
      }
      $("#" + x[i].id).css("font-size", size + "px");
    }
  }
}

Now, I've added an additional case where the text is chopped off if it gets too small (below the minimum threshold passed into the function) for convenience. Another thing that happens once that threshold is reached that may or may not be desired is the changing of the max width to 100%. This should be changed for each user's scenario. Finally, the whole purpose of posting this answer as an alternate is for its abilities to center the content within the parent div. That can be easily done by adding css attributes to the inner div class as follows:

.font_fix_inner {
  position: relative;
  float: center;
  top: 50%;
  -webkit-transform: translateY(-50%);
  transform: translateY(-50%);
}

Hope this helped someone!



回答13:

Check out this example here http://codepen.io/LukeXF/pen/yOQWNb, you can use a simple function on page load and page resize to make the magic happen. Though be careful not lag out the client from too many function calls. (The example has a delay).

function resize() {  
    $('.resize').each(function(i, obj) {
        $(this).css('font-size', '8em');

        while ($(this).width() > $(this).parent().width()) {
            $(this).css('font-size', (parseInt($(this).css('font-size')) - 1) + "px");
        }
    });
}


标签: