插入元素 - 必要时拆分其他元素

时间:2017-02-21 22:03:54

标签: javascript jquery html

给定一个父元素,文本偏移量和长度..我希望能够在偏移量和偏移量+长度之间的文本周围包裹一个元素。如果这个文本跨越我们的子元素,我希望它们被拆分,如果元素是跨度...并且取消(没有做出更改)除了跨度之外的任何其他内容或者如果我们的父节点空间不足。

例如,给定:

<div id='parent'>Aa bb <span class='child'>cc dd</span> ee ff</div>

如果偏移和长度为4和5(这意味着“bb cc”),我最终会得到:

<div id='parent'>Aa <span class='new'>bb <span class='child'>cc</span></span><span class='child'> dd</span> ee ff</div>

请注意,.child元素已被拆分,因此'bb'和'cc'仍位于.child个元素内,尽管只有'bb'被添加到.new

类似于“dd ee”,或者如果存在进一步(更复杂)的子跨度嵌套,则进行各种选择。

我在试图解决如何做到这一点时遇到了一些麻烦,我遇到的唯一分裂就是头痛欲绝。

我认为一个好的函数签名就像splitInsert(parentElement, textOffset, length)

2 个答案:

答案 0 :(得分:0)

看起来你想要在一个跨度的给定位置包装一长串字符。

这是实现此目的的伪代码过程:

  1. 在DOM中查找文本的开始和结束位置,忽略任何元素标记。
  2. 拆分仅部分包含在范围内的所有元素。
  3. 为范围创建包含元素。
  4. 以下是此方法的代码示例的答案。它通过正则表达式匹配来选择范围,因此您需要将其更改为索引和长度,但这应该足以让您前进。

    How to wrap part of a text in a node with JavaScript

答案 1 :(得分:0)

我设法把东西放在一起......比我原先想的要多一些。

我创建了以下功能:

/**
 * Find the text node and the index within that given a parent node and the index within that.
 *
 * @param parentNode
 * @param index
 * @returns {*} - object with 'target' property set to the text node at the index parameter within the
 *  parentNode parameter and 'index' property set to the index of that point within 'target'
 */
findStartPoint = function(parentNode, index) {
    var nodeRight = 0;
    var nodeLeft = 0;
    var node = null;
    for(var i = 0; i < parentNode.childNodes.length; i++){
        node = parentNode.childNodes.item(i);
        if(node.nodeType !== 7 && node.nodeType !== 8){ //not processing instruction or comment
            if(nodeRight <= index){
                nodeLeft = nodeRight;
                nodeRight += node.text.length;
                if(nodeRight > index){
                    if (node.nodeType === 3) {
                        return { target: node, index: index-nodeLeft };
                    } else {
                        return this.findStartPoint( node, index-nodeLeft );
                    }
                }
            }
        }
    }
    return { target: null, index: null };
};

/**
 *
 * Inserts an element within a givin range, will split tags if necessary
 *
 * xx <bold>xx foo <italic> bar</italic></bold> baz xx
 *
 * If I selected 'foo bar baz' in the above:
 * - startPoint would be { target: the text node containing 'xx foo ', index: 4 }
 * - length would be 'foo bar baz'.length
 * - splittableEles could be ['BOLD', 'ITALIC']
 * - insert ele could be <hello>
 *
 * Output would be:
 * xx <bold>xx </bold><hello><bold>foo <italic> bar</italic></bold> baz</hello> xx
 *
 * @param startPoint - an object containing target (text node at beginning of split) and index (index of beginning within this text node)
 * @param length - length of selection in characters
 * @param splittableEles - elements that we allow to be split
 * @param insertEle - element that we will wrap the split within and insert
 * @returns {*}
 */
splitInsert = function(startPoint, length, splittableEles, insertEle) {
    var target = startPoint.target;
    var index = startPoint.index;

   if (index == 0 && $(target.parentNode).text().length <= length) {
        //consume entire target parent
        target.parentNode.parentNode.insertBefore(insertEle, target.parentNode);
        insertEle.appendChild(target.parentNode);
    } else {
       //split and add right of index to insertEle
       var content = target.splitText(index);
       content.parentNode.insertBefore(insertEle, content);
       if (content.length > length) {
           //split off the end if content longer than selection
           content.splitText(length);
       }
       insertEle.appendChild(content);
   }

    while ( insertEle.text.length < length ) {
        if (insertEle.nextSibling) {
            if ( !this.consumeElementForInsert(insertEle, insertEle.nextSibling, length) ) {
                if ( insertEle.nextSibling.nodeType === 3 ) {
                    this.splitTextForInsert(insertEle, insertEle.nextSibling, length)
                } else {
                    this.splitElementForInsert(insertEle, insertEle.nextSibling, length, splittableEles)
                }
            }
        } else {
            //no next sibling... need to split parent. this would make parents next sibling for next iteration
            var parent = insertEle.parentNode;
            if (-1 == $.inArray(parent.nodeName.toUpperCase(), splittableEles)) {
                //selection would require splitting non-splittable element
                return { success: false };
            }
            //wrap insertEle with empty clone of parent, then place after parent
            var clone = parent.cloneNode(false);
            while (insertEle.firstChild) {
                clone.appendChild(insertEle.firstChild);
            }
            insertEle.appendChild(clone);
            parent.parentNode.insertBefore(insertEle, parent.nextSibling);
        }
    }
    return { success: true, newElement: insertEle };
};

/**
 * Splits a textnode ('node'), text on the left will be appended to 'container' to make 'container' have
 * as many 'characters' as specified
 *
 * @param container
 * @param node
 * @param characters
 */
splitTextForInsert = function (container, node, characters) {
    var containerLength = $(container).text().length;
    if ( node.nodeValue.length + containerLength > characters ) {
        node.splitText(characters - containerLength);
    }
    container.appendChild(node);
};

/**
 * Puts 'node' into 'container' as long as it can fit given that 'container' can only have so many 'characters'
 *
 * @param container
 * @param node
 * @param characters
 *
 * @returns {boolean} - true if can consume, false if can't. can't consume if element has more text than needed.
 */
consumeElementForInsert = function (container, node, characters) {
    if ( characters - $(container).text().length > $(node).text().length ) {
        container.appendChild(node);
        return true;
    }
    return false;
}

/**
 * Splits 'node' (recursively if necessary) the amount of 'characters' specified, adds left side into 'container'
 *
 * @param container - parent/container of node we are splitting
 * @param node - node we are splitting
 * @param characters - number of characters in markman selection
 * @param splittableEles - array of nodeTypes that can be split, upper case
 * @param originalContainer - original container (before recursive calls)
 * @returns {boolean} - true if we successfully split element or there is nothing to split, false otherwise. false will happen if we try to split
 *  something not in splittableEles or if we run out of characters
 */
splitElementForInsert = function (container, node, characters, splittableEles, originalContainer) {
    originalContainer = originalContainer || container;
    if (-1 == $.inArray(node.nodeName.toUpperCase(), splittableEles)) {
        return false;
    }
    node.normalize();
    var child = node.firstChild;
    if (!child) {
        return true;
    }
    else if (child.nodeType === 3) {
        var $container = $(originalContainer);
        if (characters - $container.text().length - child.nodeValue.length < 1 ) {
            //this portion is enough for the selected range
            var clone = node.cloneNode(false);
            child.splitText(characters - $container.text().length);
            clone.appendChild(child);
            container.appendChild(clone);
            return true;
        } else {
            //throw this text in the container and go on to the next as we still need more
            if (child.nextSibling) {
                var next = child.nextSibling;
                container.appendChild(child);
                return this.splitElementForInsert( container, next, characters, splittableEles, originalContainer );
            } else {
                return true;
            }
        }
    }
    else if (child.nodeType === 1) {
        //child is an element, split that element
        var clone = node.cloneNode(false);
        container.appendChild(clone);
        return this.splitElementForInsert(clone, child, characters, splittableEles, originalContainer);
    }
};

然后我可以用这样的东西打电话......

var left = this.selectionInfo.left - paraIdOffset;
var right = this.selectionInfo.right - paraIdOffset;
var parentNode = this.selectionInfo.parentXmlNode;
var someElement = this.xml.createElement(...);
var splittableEles = ['SPAN'];
var createHappened = false;

var startPoint = findStartPoint(parentNode, left);
var insert = splitInsert(startPoint, right-left, splittableEles, someElement );
if (insert.success) {
    createHappened = true;
}