关于javascript:插入元素 – 必要时拆分其他元素

Insert element - splitting others if necessary

给定父元素、文本偏移量和长度。我希望能够将一个元素环绕在以偏移量开始的文本周围,并转到偏移量+长度。如果此文本跨越我们的out-of-child元素,我希望在元素跨越时拆分它们…如果除了跨度或父级空间不足之外还有其他内容,则取消(未做任何更改)。

例如,给定:

1
Aa bb <span class='child'>cc dd</span> ee ff

如果偏移量和长度是4和5(这意味着"bb cc"),我想以以下方式结束:

1
Aa <span class='new'>bb <span class='child'>cc</span></span><span class='child'> dd</span> ee ff

注意,.child元素被拆分,因此"bb"和"cc"仍在.child元素中,尽管.new中只添加了"bb"。

类似于"dd ee",或者如果有更多(更复杂)子跨度的嵌套,则有各种选择。

我在尝试如何解决这个问题时遇到了一些困难,唯一的分裂就是头痛。

我认为一个好的函数签名应该类似于splitInsert(parentElement, textOffset, length)


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

我创建了以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/**
 * 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);
    }
};

我可以这样打电话…

1
2
3
4
5
6
7
8
9
10
11
12
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;
}


看起来您希望在一个范围内的给定位置换行一段字符。

下面是一个实现这一点的伪代码过程:

  • 在DOM中查找文本开始和结束位置,忽略任何元素标记。
  • 拆分仅部分包含在范围中的任何元素。
  • 为范围创建一个包含元素。
  • 下面是这个方法的代码示例的答案。它通过regex匹配来选择范围,因此您需要将其更改为按索引和长度,但这应该足以让您继续前进。

    如何使用javascript在节点中包装部分文本