在Textarea中的光标位置显示DIV

Arm*_*her 58 javascript dom

对于我的项目,我很乐意为特定的textarea提供自动完成功能.类似于intellisense/omnicomplete的工作原理.然而,我必须找出绝对光标位置,以便我知道DIV应该出现在哪里.

事实证明:那是(几乎我希望)无法实现的.有没有人有一些巧妙的想法如何解决这个问题?

eno*_*rev 35

Version 2 of My Hacky Experiment

This new version works with any font, which can be adjusted on demand, and any textarea size.

After noticing that some of you are still trying to get this to work, I decided to try a new approach. My results are FAR better this time around - at least on google chrome on linux. I no longer have a windows PC available to me, so I can only test on chrome/firefox on Ubuntu. My results work 100% consistently on Chrome, and let's say somewhere around 70 - 80% on Firefox, but I don't imagine it would be incredibly difficult to find the inconsistencies.

This new version relies on a Canvas object. In my example, I actually show that very canvas - just so you can see it in action, but it could very easily be done with a hidden canvas object.

This is most certainly a hack, and I apologize ahead of time for my rather thrown together code. At the very least, in google chrome, it works consistently, no matter what font I set it to, or size of textarea. I used Sam Saffron's example to show cursor coordinates (a gray-background div). I also added a "Randomize" link, so you can see it work in different font/texarea sizes and styles, and watch the cursor position update on the fly. I recommend looking at the full page demo so you can better see the companion canvas play along.

I'll summarize how it works...

The underlying idea is that we're trying to redraw the textarea on a canvas, as closely as possible. Since the browser uses the same font engine for both and texarea, we can use canvas's font measurement functionality to figure out where things are. From there, we can use the canvas methods available to us to figure out our coordinates.

First and foremost, we adjust our canvas to match the dimensions of the textarea. This is entirely for visual purposes since the canvas size doesn't really make a difference in our outcome. Since Canvas doesn't actually provide a means of word wrap, I had to conjure (steal/borrow/munge together) a means of breaking up lines to as-best-as-possible match the textarea. This is where you'll likely find you need to do the most cross-browser tweaking.

After word wrap, everything else is basic math. We split the lines into an array to mimic the word wrap, and now we want to loop through those lines and go all the way down until the point where our current selection ends. In order to do that, we're just counting characters and once we surpass selection.end, we know we have gone down far enough. Multiply the line count up until that point with the line-height and you have a y coordinate.

The x coordinate is very similar, except we're using context.measureText. As long as we're printing out the right number of characters, that will give us the width of the line that's being drawn to Canvas, which happens to end after the last character written out, which is the character before the currentl selection.end position.

When trying to debug this for other browsers, the thing to look for is where the lines don't break properly. You'll see in some places that the last word on a line in canvas may have wrapped over on the textarea or vice-versa. This has to do with how the browser handles word wraps. As long as you get the wrapping in the canvas to match the textarea, your cursor should be correct.

I'll paste the source below. You should be able to copy and paste it, but if you do, I ask that you download your own copy of jquery-fieldselection instead of hitting the one on my server.

I've also upped a new demo as well as a fiddle.

Good luck!

<!DOCTYPE html>
<html lang="en-US">
    <head>
        <meta charset="utf-8" />
        <title>Tooltip 2</title>
        <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
        <script type="text/javascript" src="http://enobrev.info/cursor/js/jquery-fieldselection.js"></script>
        <style type="text/css">
            form {
                float: left;
                margin: 20px;
            }

            #textariffic {
                height: 400px;
                width: 300px;
                font-size: 12px;
                font-family: 'Arial';
                line-height: 12px;
            }

            #tip {
                width:5px;
                height:30px;
                background-color: #777;
                position: absolute;
                z-index:10000
            }

            #mock-text {
                float: left;
                margin: 20px;
                border: 1px inset #ccc;
            }

            /* way the hell off screen */
            .scrollbar-measure {
                width: 100px;
                height: 100px;
                overflow: scroll;
                position: absolute;
                top: -9999px;
            }

            #randomize {
                float: left;
                display: block;
            }
        </style>
        <script type="text/javascript">
            var oCanvas;
            var oTextArea;
            var $oTextArea;
            var iScrollWidth;

            $(function() {
                iScrollWidth = scrollMeasure();
                oCanvas      = document.getElementById('mock-text');
                oTextArea    = document.getElementById('textariffic');
                $oTextArea   = $(oTextArea);

                $oTextArea
                        .keyup(update)
                        .mouseup(update)
                        .scroll(update);

                $('#randomize').bind('click', randomize);

                update();
            });

            function randomize() {
                var aFonts      = ['Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Impact', 'Times New Roman', 'Verdana', 'Webdings'];
                var iFont       = Math.floor(Math.random() * aFonts.length);
                var iWidth      = Math.floor(Math.random() * 500) + 300;
                var iHeight     = Math.floor(Math.random() * 500) + 300;
                var iFontSize   = Math.floor(Math.random() * 18)  + 10;
                var iLineHeight = Math.floor(Math.random() * 18)  + 10;

                var oCSS = {
                    'font-family':  aFonts[iFont],
                    width:          iWidth + 'px',
                    height:         iHeight + 'px',
                    'font-size':    iFontSize + 'px',
                    'line-height':  iLineHeight + 'px'
                };

                console.log(oCSS);

                $oTextArea.css(oCSS);

                update();
                return false;
            }

            function showTip(x, y) {
                $('#tip').css({
                      left: x + 'px',
                      top: y + 'px'
                  });
            }

            // https://stackoverflow.com/a/11124580/14651
            // https://stackoverflow.com/a/3960916/14651

            function wordWrap(oContext, text, maxWidth) {
                var aSplit = text.split(' ');
                var aLines = [];
                var sLine  = "";

                // Split words by newlines
                var aWords = [];
                for (var i in aSplit) {
                    var aWord = aSplit[i].split('\n');
                    if (aWord.length > 1) {
                        for (var j in aWord) {
                            aWords.push(aWord[j]);
                            aWords.push("\n");
                        }

                        aWords.pop();
                    } else {
                        aWords.push(aSplit[i]);
                    }
                }

                while (aWords.length > 0) {
                    var sWord = aWords[0];
                    if (sWord == "\n") {
                        aLines.push(sLine);
                        aWords.shift();
                        sLine = "";
                    } else {
                        // Break up work longer than max width
                        var iItemWidth = oContext.measureText(sWord).width;
                        if (iItemWidth > maxWidth) {
                            var sContinuous = '';
                            var iWidth = 0;
                            while (iWidth <= maxWidth) {
                                var sNextLetter = sWord.substring(0, 1);
                                var iNextWidth  = oContext.measureText(sContinuous + sNextLetter).width;
                                if (iNextWidth <= maxWidth) {
                                    sContinuous += sNextLetter;
                                    sWord = sWord.substring(1);
                                }
                                iWidth = iNextWidth;
                            }
                            aWords.unshift(sContinuous);
                        }

                        // Extra space after word for mozilla and ie
                        var sWithSpace = (jQuery.browser.mozilla || jQuery.browser.msie) ? ' ' : '';
                        var iNewLineWidth = oContext.measureText(sLine + sWord + sWithSpace).width;
                        if (iNewLineWidth <= maxWidth) {  // word fits on current line to add it and carry on
                            sLine += aWords.shift() + " ";
                        } else {
                            aLines.push(sLine);
                            sLine = "";
                        }

                        if (aWords.length === 0) {
                            aLines.push(sLine);
                        }
                    }
                }
                return aLines;
            }

            // http://davidwalsh.name/detect-scrollbar-width
            function scrollMeasure() {
                // Create the measurement node
                var scrollDiv = document.createElement("div");
                scrollDiv.className = "scrollbar-measure";
                document.body.appendChild(scrollDiv);

                // Get the scrollbar width
                var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;

                // Delete the DIV
                document.body.removeChild(scrollDiv);

                return scrollbarWidth;
            }

            function update() {
                var oPosition  = $oTextArea.position();
                var sContent   = $oTextArea.val();
                var oSelection = $oTextArea.getSelection();

                oCanvas.width  = $oTextArea.width();
                oCanvas.height = $oTextArea.height();

                var oContext    = oCanvas.getContext("2d");
                var sFontSize   = $oTextArea.css('font-size');
                var sLineHeight = $oTextArea.css('line-height');
                var fontSize    = parseFloat(sFontSize.replace(/[^0-9.]/g, ''));
                var lineHeight  = parseFloat(sLineHeight.replace(/[^0-9.]/g, ''));
                var sFont       = [$oTextArea.css('font-weight'), sFontSize + '/' + sLineHeight, $oTextArea.css('font-family')].join(' ');

                var iSubtractScrollWidth = oTextArea.clientHeight < oTextArea.scrollHeight ? iScrollWidth : 0;

                oContext.save();
                oContext.clearRect(0, 0, oCanvas.width, oCanvas.height);
                oContext.font = sFont;
                var aLines = wordWrap(oContext, sContent, oCanvas.width - iSubtractScrollWidth);

                var x = 0;
                var y = 0;
                var iGoal = oSelection.end;
                aLines.forEach(function(sLine, i) {
                    if (iGoal > 0) {
                        oContext.fillText(sLine.substring(0, iGoal), 0, (i + 1) * lineHeight);

                        x = oContext.measureText(sLine.substring(0, iGoal + 1)).width;
                        y = i * lineHeight - oTextArea.scrollTop;

                        var iLineLength = sLine.length;
                        if (iLineLength == 0) {
                            iLineLength = 1;
                        }

                        iGoal -= iLineLength;
                    } else {
                        // after
                    }
                });
                oContext.restore();

                showTip(oPosition.left + x, oPosition.top + y);
            }

        </script>
    </head>
    <body>

        <a href="#" id="randomize">Randomize</a>

        <form id="tipper">
            <textarea id="textariffic">Aliquam urna. Nullam augue dolor, tincidunt condimentum, malesuada quis, ultrices at, arcu. Aliquam nunc pede, convallis auctor, sodales eget, aliquam eget, ligula. Proin nisi lacus, scelerisque nec, aliquam vel, dictum mattis, eros. Curabitur et neque. Fusce sollicitudin. Quisque at risus. Suspendisse potenti. Mauris nisi. Sed sed enim nec dui viverra congue. Phasellus velit sapien, porttitor vitae, blandit volutpat, interdum vel, enim. Cras sagittis bibendum neque. Proin eu est. Fusce arcu. Aliquam elit nisi, malesuada eget, dignissim sed, ultricies vel, purus. Maecenas accumsan diam id nisi.

Phasellus et nunc. Vivamus sem felis, dignissim non, lacinia id, accumsan quis, ligula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed scelerisque nulla sit amet mi. Nulla consequat, elit vitae tempus vulputate, sem libero rhoncus leo, vulputate viverra nulla purus nec turpis. Nam turpis sem, tincidunt non, congue lobortis, fermentum a, ipsum. Nulla facilisi. Aenean facilisis. Maecenas a quam eu nibh lacinia ultricies. Morbi malesuada orci quis tellus.

Sed eu leo. Donec in turpis. Donec non neque nec ante tincidunt posuere. Pellentesque blandit. Ut vehicula vestibulum risus. Maecenas commodo placerat est. Integer massa nunc, luctus at, accumsan non, pulvinar sed, odio. Pellentesque eget libero iaculis dui iaculis vehicula. Curabitur quis nulla vel felis ullamcorper varius. Sed suscipit pulvinar lectus.</textarea>

        </form>

        <div id="tip"></div>

        <canvas id="mock-text"></canvas>
    </body>
</html>
Run Code Online (Sandbox Code Playgroud)

Bug

There's one bug I do recall. If you put the cursor before the first letter on a line, it shows the "position" as the last letter on the previous line. This has to do with how selection.end work. I don't think it should be too difficult to look for that case and fix it accordingly.


Version 1

Leaving this here so you can see the progress without having to dig through the edit history.

It's not perfect and it's most Definitely a hack, but I got it to work pretty well on WinXP IE, FF, Safari, Chrome and Opera.

As far as I can tell there's no way to directly find out the x/y of a cursor on any browser. The IE method, mentioned by Adam Bellaire is interesting, but unfortunately not cross-browser. I figured the next best thing would be to use the characters as a grid.

Unfortunately there's no font metric information built into any of the browsers, which means a monospace font is the only font type that's going to have a consistent measurement. Also, there's no reliable means of figuring out a font-width from the font-height. At first I'd tried using a percentage of the height, which worked great. Then I changed the font-size and everything went to hell.

I tried one method to figure out character width, which was to create a temporary textarea and keep adding characters until the scrollHeight (or scrollWidth) changed. It seems plausable, but about halfway down that road, I realized I could just use the cols attribute on the textarea and figured there are enough hacks in this ordeal to add another one. This means you can't set the width of the textarea via css. You HAVE to use the cols for this to work.

The next problem I ran into is that, even when you set the font via css, the browsers report the font differently. When you don't set a font, mozilla uses monospace by default, IE uses Courier New, Opera "Courier New" (with quotes), Safari, 'Lucida Grand' (with single quotes). When you do set the font to monospace, mozilla and ie take what you give them, Safari comes out as -webkit-monospace and Opera stays with "Courier New".

所以现在我们初始化一些变量.确保在css中设置行高.Firefox报告正确的行高,但IE报告"正常",我没有打扰其他浏览器.我只是在我的CSS中设置行高,这解决了差异.我还没有使用ems而不是像素进行测试.字符高度只是字体大小.应该也可以在你的CSS中预先设置它.

另外,在我们开始放置角色之前还有一个预先设置 - 这真让我挠头.对于ie和mozilla,texarea字符是<cols,其他一切都是<=字符.所以Chrome可以容纳50个字符,但是mozilla和ie会打破最后一个字.

Now we're going to create an array of first-character positions for every line. We loop through every char in the textarea. If it's a newline, we add a new position to our line array. If it's a space, we try to figure out if the current "word" will fit on the line we're on or if it's going to get pushed to the next line. Punctuation counts as a part of the "word". I haven't tested with tabs, but there's a line there for adding 4 chars for a tab char.

Once we have an array of line positions, we loop through and try to find which line the cursor is on. We're using hte "End" of the selection as our cursor.

x = (cursor position - first character position of cursor line)*character width

y = ((cursor line + 1)*line height) - scroll position

I'm using jquery 1.2.6, jquery-fieldselection, and jquery-dimensions

The Demo: http://enobrev.info/cursor/

And the code:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Tooltip</title>
        <script type="text/javascript" src="js/jquery-1.2.6.js"></script>
        <script type="text/javascript" src="js/jquery-fieldselection.js"></script>
        <script type="text/javascript" src="js/jquery.dimensions.js"></script>
        <style type="text/css">
            form {
                margin: 20px auto;
                width: 500px;
            }

            #textariffic {
                height: 400px;
                font-size: 12px;
                font-family: monospace;
                line-height: 15px;
            }

            #tip {
                position: absolute;
                z-index: 2;
                padding: 20px;
                border: 1px solid #000;
                background-color: #FFF;
            }
        </style>
        <script type="text/javascript">
            $(function() {
                $('textarea')
                    .keyup(update)
                    .mouseup(update)
                    .scroll(update);
            });

            function showTip(x, y) {                
                y = y + $('#tip').height();

                $('#tip').css({
                    left: x + 'px',
                    top: y + 'px'
                });
            }

            function update() {
                var oPosition = $(this).position();
                var sContent = $(this).val();

                var bGTE = jQuery.browser.mozilla || jQuery.browser.msie;

                if ($(this).css('font-family') == 'monospace'           // mozilla
                ||  $(this).css('font-family') == '-webkit-monospace'   // Safari
                ||  $(this).css('font-family') == '"Courier New"') {    // Opera
                    var lineHeight   = $(this).css('line-height').replace(/[^0-9]/g, '');
                        lineHeight   = parseFloat(lineHeight);
                    var charsPerLine = this.cols;
                    var charWidth    = parseFloat($(this).innerWidth() / charsPerLine);


                    var iChar = 0;
                    var iLines = 1;
                    var sWord = '';

                    var oSelection = $(this).getSelection();
                    var aLetters = sContent.split("");
                    var aLines = [];

                    for (var w in aLetters) {
                        if (aLetters[w] == "\n") {
                            iChar = 0;
                            aLines.push(w);
                            sWord = '';
                        } else if (aLetters[w] == " ") {    
                            var wordLength = parseInt(sWord.length);


                            if ((bGTE && iChar + wordLength >= charsPerLine)
                            || (!bGTE && iChar + wordLength > charsPerLine)) {
                                iChar = wordLength + 1;
                                aLines.push(w - wordLength);
                            } else {                
                                iChar += wordLength + 1; // 1 more char for the space
                            }

                            sWord = '';
                        } else if (aLetters[w] == "\t") {
                            iChar += 4;
                        } else {
                            sWord += aLetters[w];     
                        }
                    }

                    var iLine = 1;
                    for(var i in aLines) {
                        if (oSelection.end < aLines[i]) {
                            iLine = parseInt(i) - 1;
                            break;
                        }
                    }

                    if (iLine > -1) {
                        var x = parseInt(oSelection.end - aLines[iLine]) * charWidth;
                    } else {
                        var x = parseInt(oSelection.end) * charWidth;
                    }
                    var y = (iLine + 1) * lineHeight - this.scrollTop; // below line

                    showTip(oPosition.left + x, oPosition.top + y);
                }
            }

        </script>
    </head>
    <body>
        <form id="tipper">
            <textarea id="textariffic" cols="50">
Aliquam urna. Nullam augue dolor, tincidunt condimentum, malesuada quis, ultrices at, arcu. Aliquam nunc pede, convallis auctor, sodales eget, aliquam eget, ligula. Proin nisi lacus, scelerisque nec, aliquam vel, dictum mattis, eros. Curabitur et neque. Fusce sollicitudin. Quisque at risus. Suspendisse potenti. Mauris nisi. Sed sed enim nec dui viverra congue. Phasellus velit sapien, porttitor vitae, blandit volutpat, interdum vel, enim. Cras sagittis bibendum neque. Proin eu est. Fusce arcu. Aliquam elit nisi, malesuada eget, dignissim sed, ultricies vel, purus. Maecenas accumsan diam id nisi.

Phasellus et nunc. Vivamus sem felis, dignissim non, lacinia id, accumsan quis, ligula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed scelerisque nulla sit amet mi. Nulla consequat, elit vitae tempus vulputate, sem libero rhoncus leo, vulputate viverra nulla purus nec turpis. Nam turpis sem, tincidunt non, congue lobortis, fermentum a, ipsum. Nulla facilisi. Aenean facilisis. Maecenas a quam eu nibh lacinia ultricies. Morbi malesuada orci quis tellus.

Sed eu leo. Donec in turpis. Donec non neque nec ante tincidunt posuere. Pellentesque blandit. Ut vehicula vestibulum risus. Maecenas commodo placerat est. Integer massa nunc, luctus at, accumsan non, pulvinar sed, odio. Pellentesque eget libero iaculis dui iaculis vehicula. Curabitur quis nulla vel felis ullamcorper varius. Sed suscipit pulvinar lectus. 
            </textarea>

        </form>

        <p id="tip">Here I Am!!</p>
    </body>
</html>
Run Code Online (Sandbox Code Playgroud)


Ada*_*ire 0

这篇博文似乎解决了你的问题,但不幸的是作者承认他只在 IE 6 中进行了测试。

IE 中的 DOM 不提供有关字符相对位置的信息;但是,它确实为浏览器呈现的控件提供了边界和偏移值。因此,我使用这些值来确定字符的相对边界。然后,使用 JavaScript TextRange,我创建了一种机制,用于使用此类度量来计算给定 TextArea 内固定宽度字体的行和列位置。

首先,必须根据所使用的固定宽度字体的大小来计算 TextArea 的相对边界。为此,必须将 TextArea 的原始值存储在本地 JavaScript 变量中并清除该值。然后,创建一个 TextRange 以确定 TextArea 的上边界和左边界。


归档时间:

查看次数:

24890 次

最近记录:

11 年,9 月 前