Подтвердить что ты не робот

Извлечение текста из contentEditable div

У меня есть div, установленный в contentEditable и стилизованный под "white-space:pre", поэтому он сохраняет такие вещи, как linebreaks. В Safari, FF и IE, div в значительной степени выглядит и работает одинаково. Все хорошо. Я хочу извлечь текст из этого div, но таким образом, чтобы не потерять форматирование - в частности, разрыв строки.

Мы используем jQuery, чья функция text() в основном выполняет предварительную настройку DFS и склеивает все содержимое в этой ветке DOM в один кусок. Это теряет форматирование.

Я посмотрел на функцию html(), но кажется, что все три браузера делают разные вещи с фактическим HTML, который создается за кулисами в моем div contentEditable. Предполагая, что я набираю это в свой div:

1
2
3

Вот результаты:

Safari 4:

1
<div>2</div>
<div>3</div>

Firefox 3.6:

1
<br _moz_dirty="">
2
<br _moz_dirty="">
3
<br _moz_dirty="">
<br _moz_dirty="" type="_moz">

IE 8:

<P>1</P><P>2</P><P>3</P>

Тьфу. Здесь нет ничего очень последовательного. Удивительно, что MSIE выглядит наиболее разумно! (Заглавный тег P и все)

У div будет динамически задаваться стиль (шрифт, цвет, размер и выравнивание), который выполняется с помощью CSS, поэтому я не уверен, могу ли я использовать тег pre (на который ссылались некоторые страницы я найденный с помощью Google).

Кто-нибудь знает какой-либо код JavaScript и/или jQuery-плагин или что-то, что будет извлекать текст из contentEditable div таким образом, чтобы сохранить разрывы строк? Я бы предпочел не изобретать синтаксический анализ если мне не нужно.

Обновление: я вырезал функцию getText из jQuery 1.4.2 и изменил ее, чтобы извлечь ее с незаполненным пробелом (я только chnaged одна строка, где я добавляю новую строку);

function extractTextWithWhitespace( elems ) {
    var ret = "", elem;

    for ( var i = 0; elems[i]; i++ ) {
        elem = elems[i];

        // Get the text from text nodes and CDATA nodes
        if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
            ret += elem.nodeValue + "\n";

        // Traverse everything else, except comment nodes
        } else if ( elem.nodeType !== 8 ) {
            ret += extractTextWithWhitespace2( elem.childNodes );
        }
    }

    return ret;
}

Я вызываю эту функцию и использую ее вывод, чтобы назначить ее XML node с jQuery, что-то вроде:

var extractedText = extractTextWithWhitespace($(this));
var $someXmlNode = $('<someXmlNode/>');
$someXmlNode.text(extractedText);

Полученный XML в конечном итоге отправляется на сервер через вызов AJAX.

Это хорошо работает в Safari и Firefox.

В IE кажется, что только первая "\n" сохраняется. Заглядывая в него больше, похоже, что jQuery устанавливает такой текст (строка 4004 jQuery-1.4.2.js):

return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );

Чтение на createTextNode, похоже, что реализация IE может размыть пробелы. Это правда или я делаю что-то неправильно?

4b9b3361

Ответ 1

Я забыл об этом вопросе до сих пор, когда Нико ударил его щедростью.

Я решил проблему, написав ту функцию, в которой я нуждался, и вырезал функцию из существующей кодовой базы jQuery и модифицировал ее для работы по мере необходимости.

Я тестировал эту функцию с помощью Safari (WebKit), IE, Firefox и Opera. Я не стал проверять какие-либо другие браузеры, так как вся информация для контента нестандартная. Также возможно, что обновление для любого браузера может нарушить эту функцию, если они изменят способ реализации contentEditable. Поэтому программисты остерегайтесь.

function extractTextWithWhitespace(elems)
{
    var lineBreakNodeName = "BR"; // Use <br> as a default
    if ($.browser.webkit)
    {
        lineBreakNodeName = "DIV";
    }
    else if ($.browser.msie)
    {
        lineBreakNodeName = "P";
    }
    else if ($.browser.mozilla)
    {
        lineBreakNodeName = "BR";
    }
    else if ($.browser.opera)
    {
        lineBreakNodeName = "P";
    }
    var extractedText = extractTextWithWhitespaceWorker(elems, lineBreakNodeName);

    return extractedText;
}

// Cribbed from jQuery 1.4.2 (getText) and modified to retain whitespace
function extractTextWithWhitespaceWorker(elems, lineBreakNodeName)
{
    var ret = "";
    var elem;

    for (var i = 0; elems[i]; i++)
    {
        elem = elems[i];

        if (elem.nodeType === 3     // text node
            || elem.nodeType === 4) // CDATA node
        {
            ret += elem.nodeValue;
        }

        if (elem.nodeName === lineBreakNodeName)
        {
            ret += "\n";
        }

        if (elem.nodeType !== 8) // comment node
        {
            ret += extractTextWithWhitespace(elem.childNodes, lineBreakNodeName);
        }
    }

    return ret;
}

Ответ 2

К сожалению, вам все равно придется обрабатывать это для случая pre отдельно для каждого браузера (во многих случаях я не допускаю обнаружения браузера, используйте обнаружение функции... но в этом случае это необходимо), но, к счастью, вам может позаботиться о них все довольно кратко, например:

var ce = $("<pre />").html($("#edit").html());
if($.browser.webkit) 
  ce.find("div").replaceWith(function() { return "\n" + this.innerHTML; });    
if($.browser.msie) 
  ce.find("p").replaceWith(function() { return this.innerHTML  +  "<br>"; });
if($.browser.mozilla || $.browser.opera ||$.browser.msie )
  ce.find("br").replaceWith("\n");

var textWithWhiteSpaceIntact = ce.text();

Здесь вы можете проверить это. IE, в частности, является сложной задачей из-за того, что путь &nbsp; и новые строки в преобразовании текста, поэтому он обрабатывает выше <br>, чтобы сделать его согласованным, поэтому ему нужно 2 прохода для правильной обработки.

В приведенном выше #edit есть идентификатор компонента contentEditable, поэтому просто измените это или сделайте это функцией, например:

function getContentEditableText(id) {
    var ce = $("<pre />").html($("#" + id).html());
    if ($.browser.webkit)
      ce.find("div").replaceWith(function() { return "\n" + this.innerHTML; });
    if ($.browser.msie)
      ce.find("p").replaceWith(function() { return this.innerHTML + "<br>"; });
    if ($.browser.mozilla || $.browser.opera || $.browser.msie)
      ce.find("br").replaceWith("\n");

    return ce.text();
}

Вы можете проверить это здесь. Или, поскольку это построено на методах jQuery в любом случае, сделайте его плагином, например:

$.fn.getPreText = function () {
    var ce = $("<pre />").html(this.html());
    if ($.browser.webkit)
      ce.find("div").replaceWith(function() { return "\n" + this.innerHTML; });
    if ($.browser.msie)
      ce.find("p").replaceWith(function() { return this.innerHTML + "<br>"; });
    if ($.browser.mozilla || $.browser.opera || $.browser.msie)
      ce.find("br").replaceWith("\n");

    return ce.text();
};

Тогда вы можете просто называть его $("#edit").getPreText(), чтобы проверить эту версию здесь.

Ответ 4

Я обнаружил это сегодня в Firefox:

Я передаю contenteditable div, для которого белое пространство задано "pre" для этой функции, и оно работает резко.

Я добавил строку, чтобы показать, сколько узлов есть, и кнопку, которая помещает вывод в другой PRE, чтобы доказать, что разрывы строк не повреждены.

В основном это говорит:

For each child node of the DIV,
   if it contains the 'data' property,
      add the data value to the output
   otherwise
      add an LF (or a CRLF for Windows)
}
and return the result.

Есть проблема, tho. Когда вы нажимаете enter в конце любой строки исходного текста, вместо того, чтобы вставлять LF, он помещает "Â" внутрь. Вы можете снова нажать Enter, и он помещает LF туда, но не в первый раз. И вам нужно удалить "Â" (это выглядит как пробел). Перейти фигуры - я думаю, что ошибка.

Это не происходит в IE8. (измените textContent на innerText) Там есть другая ошибка, tho. Когда вы нажимаете enter, он разбивает node на 2 узла, как это происходит в Firefox, но свойство "data" каждого из этих узлов становится "undefined".

Я уверен, что здесь происходит гораздо больше, чем кажется на первый взгляд, поэтому любой вклад в этом вопросе будет просвещен.

<!DOCTYPE html>
<html>
<HEAD>
<SCRIPT type="text/javascript">
    function htmlToText(elem) {
        var outText="";
        for(var x=0; x<elem.childNodes.length; x++){
            if(elem.childNodes[x].data){
                outText+=elem.childNodes[x].data;
            }else{
                outText+="\n";
            }
        }
        alert(elem.childNodes.length + " Nodes: \r\n\r\n" + outText);
        return(outText);
    }
</SCRIPT>
</HEAD>
<body>

<div style="white-space:pre;" contenteditable=true id=test>Text in a pre element
is displayed in a fixed-width
font, and it preserves
both      spaces and
line breaks
</DIV>
<INPUT type=button value="submit" onclick="document.getElementById('test2').textContent=htmlToText(document.getElementById('test'))">
<PRE id=test2>
</PRE>
</body>
</html>

Ответ 5

здесь решение (с использованием подчеркивания и jquery), которое работает в iOS Safari (iOS 7 и 8), Safari 8, Chrome 43 и Firefox 36 в OS X и IE6-11 в Windows:

_.reduce($editable.contents(), function(text, node) {
    return text + (node.nodeValue || '\n' +
        (_.isString(node.textContent) ? node.textContent : node.innerHTML));
}, '')

см. тестовую страницу здесь: http://brokendisk.com/code/contenteditable.html

хотя я считаю, что реальный ответ заключается в том, что если вы не заинтересованы в разметке, предоставляемой браузером, вы не должны использовать атрибут contenteditable - текстовое поле будет подходящим инструментом для задания.

Ответ 6

this.editableVal = function(cont, opts) 
{
  if (!cont) return '';
  var el = cont.firstChild;
  var v = '';
  var contTag = new RegExp('^(DIV|P|LI|OL|TR|TD|BLOCKQUOTE)$');
  while (el) {
    switch (el.nodeType) {
      case 3:
        var str = el.data.replace(/^\n|\n$/g, ' ').replace(/[\n\xa0]/g, ' ').replace(/[ ]+/g, ' ');
        v += str;
        break;
      case 1:
        var str = this.editableVal(el);
        if (el.tagName && el.tagName.match(contTag) && str) {
          if (str.substr(-1) != '\n') {
            str += '\n';
          }

          var prev = el.previousSibling;
          while (prev && prev.nodeType == 3 && PHP.trim(prev.nodeValue) == '') {
            prev = prev.previousSibling;
          }
          if (prev && !(prev.tagName && (prev.tagName.match(contTag) || prev.tagName == 'BR'))) {
            str = '\n' + str;
          }

        }else if (el.tagName == 'BR') {
          str += '\n';
        }
        v += str;
        break;
    }
    el = el.nextSibling;
  }
  return v;
}