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

CKEditor предоставляет API для установки курсора в соответствии с диапазоном:

var ranges = new CKEDITOR.dom.range( editor.document );
editor.getSelection().selectRanges( [ ranges ] );

Поскольку CKEditor предоставляет этот API, проблему можно упростить, убрав это требование и просто найдя способ получить диапазон от координат мыши над div, содержащим различные элементы HTML.

Однако это не то же самое, что преобразование координаты мыши в положение курсора в текстовой области, поскольку текстовые области имеют фиксированную ширину столбца и высоту строки, где CKEditor отображает HTML через iframe.

Основываясь на этом, похоже, что диапазон может применяться к элементам.

Как бы вы определили начальный/конечный диапазон, ближайший к текущей позиции мыши?

Изменить: пример того, как можно использовать API ckeditor для выбора диапазона в событии mouseup.

editor.document.on('mouseup', function(e) {
    this.focus();
    var node = e.data.$.target;

    var range = new CKEDITOR.dom.range( this.document );
    range.setStart(new CKEDITOR.dom.node(node), 0);
    range.collapse();

    var ranges = [];
    ranges.push(range);
    this.getSelection().selectRanges( ranges );
});

Проблема с приведенным выше примером заключается в том, что целевой узел события (e.data.$.target) срабатывает только для таких узлов, как HTML, BODY или IMG, но не для текстовых узлов. Даже если бы это было так, эти узлы представляют фрагменты текста, которые не поддерживают установку курсора в положение мыши в этом фрагменте текста.

18
mrwrk 15 Ноя 2011 в 01:34

4 ответа

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

Обнаружить элемент браузера очень просто: вам нужно либо зарегистрировать свой обработчик для каждого элемента, либо использовать целевое поле события. Существует много информации об этом, задайте более конкретный вопрос о stackoverflow, если у вас возникли проблемы с этим.

Когда у вас есть элемент, вам нужно выяснить, какой символ внутри элемента был нажат, а затем создать соответствующий диапазон, чтобы поместить туда курсор. Как указано в сообщении, на которое вы ссылаетесь, варианты браузера делают это очень сложным. Эта страница немного устарела, но содержит хорошее обсуждение диапазонов: http://www.quirksmode. org/dom/range_intro.html

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

Я никогда не видел полного решения этого в javascript. Несколько лет назад я работал над одним, но не нашел ответа, которым бы я был доволен (некоторые действительно сложные случаи). Подход, который я использовал, был ужасным хаком: вставить интервалы в текст, а затем использовать их для выполнения двоичного поиска, пока не будет найден наименьший возможный диапазон, содержащий щелчок мыши. Пролеты не меняют макет, поэтому вы можете использовать свойства position_x/y промежутка, чтобы узнать, что они содержат щелчок.

Например. предположим, что у вас есть следующий текст в узле:

<p>Here is some paragraph text.</p>

Мы знаем, что щелчок был где-то в этом абзаце. Разделите абзац пополам с интервалом:

<p><span>Here is some p</span>aragraph text.</p>

Если диапазон содержит координаты щелчка, продолжите бинарный поиск в этой половине, в противном случае выполните поиск во второй половине.

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

С тех пор, как я работал над этим, браузеры стали намного быстрее. Вероятно, теперь они достаточно быстры, чтобы добавлять s вокруг каждого символа, затем вокруг каждых двух символов и т. д., чтобы создать двоичное дерево, в котором легко искать. Вы можете попробовать этот подход - это значительно облегчит определение строки, над которой вы работаете.

TL; DR это действительно сложная проблема, и если есть ответ, возможно, не стоит тратить время на его поиск.

2
Stephen Nelson 15 Апр 2012 в 19:21

Есть два способа сделать это, как и любой WYSIWYG.

Во-первых: - вы сдаетесь, потому что это слишком сложно, и в итоге это станет убийцей браузера;

Второе: - вы пытаетесь разобрать текст и поместить его в нужное место в полупрозрачной текстовой области или div над оригиналом, но здесь у нас есть две проблемы:

1) как бы вы анализировали динамические фрагменты данных, чтобы получить только текст и убедиться, что вы сопоставляете его с точным положением фактического содержимого

2) как бы вы решили обновление, чтобы анализировать каждый проклятый символ, который вы печатаете, или каждое действие, которое вы делаете в редакторе.

В конце концов, это всего лишь «Жестокая одиссея на темную сторону дерева DOM», но если вы выберете второй путь, то код из вашего поста будет работать как шарм.

1
khael 28 Мар 2012 в 07:20

Извините, что поднял старую тему, но я хотел опубликовать это здесь на случай, если кто-то еще наткнется на этот вопрос, так как информации по этому вопросу очень мало. Мне просто нужно было написать функцию, которая делает это для пользовательского сценария Outlook для Интернета, потому что они переопределяют функцию перетаскивания по умолчанию и прерывают ее в поле создания. Это решение, которое я придумал:

function rangeFromCoord(x, y) {
    const closest = {
        offset: 0,
        xDistance: Infinity,
        yDistance: Infinity,
    };

    const {
        minOffset,
        maxOffset,
        element,
    } = (() => {
        const range = document.createRange();
        range.selectNodeContents(document.elementFromPoint(x, y));
        return {
            element: range.startContainer,
            minOffset: range.startOffset,
            maxOffset: range.endOffset,
        };
    })();

    for(let i = minOffset; i <= maxOffset; i++) {
        const range = document.createRange();
        range.setStart(element, i);
        range.setEnd(element, i);
        const marker = document.createElement("span");
        marker.style.width = "0";
        marker.style.height = "0";
        marker.style.position = "absolute";
        marker.style.overflow = "hidden";
        range.insertNode(marker);
        const rect = marker.getBoundingClientRect();
        const distX = Math.abs(x - rect.left);
        const distY = Math.abs(y - rect.top);
        marker.remove();
        if(closest.yDistance > distY) {
            closest.offset = i;
            closest.xDistance = distX;
            closest.yDistance = distY;
        } else if(closest.yDistance === distY) {
            if(closest.xDistance > distX) {
                closest.offset = i;
                closest.xDistance = distX;
                closest.yDistance = distY;
            }
        }
    }

    const range = document.createRange();
    range.setStart(element, closest.offset);
    range.setEnd(element, closest.offset);
    return range;
}

Все, что вам нужно сделать, это передать координаты клиента, и функция автоматически выберет наиболее конкретный элемент в этой позиции. Он будет использовать этот выбор, чтобы получить родительский элемент, используемый браузером (в первую очередь элементы contenteditable), а также максимальное и минимальное смещения. Затем он продолжит работу, перебирая смещения, размещая marker элементов span с position: absolute; width: 0; height: 0; overflow: hidden; на каждом смещении, чтобы исследовать их положение, удалять их и проверять расстояние. Как и в большинстве текстовых редакторов, он сначала максимально приблизится по координате Y, а затем переместится по координате X. Как только он найдет ближайшую позицию, он создаст новый выбор и вернет его.

1
sploders101 22 Сен 2020 в 20:45

Я работал над аналогичной задачей, чтобы позволить TinyMCE (встроенный режим) инициализироваться с помощью каретки, помещенной в положение щелчка мыши. Следующий код работает как минимум в последних версиях Firefox и Chrome:

let contentElem  = $('#editorContentRootElem');
let editorConfig = { inline: true, forced_root_block: false };

let onFirstFocus = () => {
  contentElem.off('click focus', onFirstFocus);

  setTimeout(() => {
    let uniqueId = 'uniqueCaretId';
    let range    = document.getSelection().getRangeAt(0);
    let caret    = document.createElement("span");
    range.surroundContents(caret);
    caret.outerHTML = `<span id="${uniqueId}" contenteditable="false"></span>`;

    editorConfig.setup = (editor) => {
      this.editor = editor;

      editor.on('init', () => {
        var caret = $('#' + uniqueId)[0];
        if (!caret) return;

        editor.selection.select(caret);
        editor.selection.collapse(false);
        caret.parentNode.removeChild(caret);
      });
    };

    tinymce.init(editorConfig);         
  }, 0); // after redraw
}; // onFirstFocus

contentElem.on('click focus', onFirstFocus);

Объяснение

Кажется, что после события щелчка/фокуса мыши и перерисовки (setTimeout ms 0) document.getSelection().getRangeAt(0) возвращает допустимый диапазон курсора. Мы можем использовать его для любых целей. TinyMCE перемещает каретку для начала при инициализации, поэтому я создаю специальный элемент «каретки» в начале текущего диапазона, а затем заставляю редактор выбрать его, а затем удалить.

0
Alexander Shostak 15 Фев 2019 в 17:49