在mouseup上替换所选文本

时间:2017-03-04 18:35:57

标签: javascript jquery html css window

我正在构建一个实时HTML突出显示器,以便当用户选择一个文本范围时,文本被具有背景属性的span元素包围。

这是小提琴:https://jsfiddle.net/4hd2vrex/

问题是,当用户进行多次选择时,这会变得非常混乱,跨度会嵌套,我会得到这样的内容:

<span style="background-color: rgb(255, 255, 131);">
    r
    <span style="background-color: rgb(255, 255, 131);">
        <span style="background-color: rgb(255, 255, 131);">
            e
        </span>
        p
    </span>
    r
    <span style="background-color: rgb(255, 255, 131);">
        e
    </span>
    h
    <span style="background-color: rgb(255, 255, 131);">
        end
    </span>
    e
    <span style="background-color: rgb(255, 255, 131);">
        rit
    </span>
</span>
神圣的大奖蝙蝠侠!为了解决这个问题,我有以下想法:

在添加任何跨度之前,只需使用原始选定文本window.getSelection()替换所有选定文本,范围标记和所有内容。

因此,例如,如果我选择了上面那些混乱的跨度,在用更多跨度包装我选择的文本之前,我会用window.getSelection()替换那些跨度,这只是文本 reprehenderit 我会得到。

<span style="background-color: rgb(255, 255, 131);">reprehenderit</span>

问: 如何将选择替换为选定的文字?

1 个答案:

答案 0 :(得分:2)

我用我的方式完成了整个高亮文本,而不是使用window.Selection API,但使用:select(start,end).then(merge).then(filter).then(highlight)。最有趣的是它可以突出显示复杂元素,即使只是文本也是如此。我发现select api也可以编写一个wysiwyg html编辑器,所以我把它分享给所有对选择问题感兴趣的人,并希望对你有所帮助,好问题!

(function (context, factory) {
    if (typeof module != 'undefined' && typeof module.exports == 'object') {
        module.exports = factory(context);
    } else {
        factory(context, true);
    }
})(window || this, function (context, bind) {
    function promise(executor) {
        return new Promise(executor);
    }

    var $TYPE = 'nodeType', $TEXT = 'textContent', $PARENT = 'parentNode', $NEXT = 'nextSibling', $FIRST = 'firstChild', NIL = {};

    function leaf(node) {
        return node[$TYPE] == 3;
    }

    function next(node, tree) {
        var it = tree ? node[$FIRST] || node[$NEXT] : node[$NEXT];
        if (it) {
            if (leaf(it)) return it;
            return next(it, true);
        }
        var parent = node[$PARENT];
        return parent && next(parent);
    }

    function parent(node) {
        return node[$PARENT];
    }

    function wrap(node, start, end) {
        if (!node) throw 'node is null';
        if (!leaf(node)) throw 'node is not a leaf:' + node.tagName;
        var rawText = node[$TEXT];
        var rawLength = rawText.length;
        var self = {
            node: node,
            text: function (text) {
                if (text !== undefined) {
                    node.textContent = text;
                    return wrap(node, 0, text.length);
                }
                return rawText.substring(self.start(), self.end());
            },
            is: function (other) {
                return node == other.node;
            },
            start: function () {
                return start === NIL || !start ? 0 : start;
            },
            end: function () {
                return end === NIL || !end ? rawLength : end;
            },
            length: function () {
                return self.end() - self.start();
            },
            to: function (end) {
                return wrap(node, self.start(), end.end());
            },
            toLast: function () {
                return wrap(node, start, rawLength);
            },
            next: function () {
                var it = next(node);
                return it && wrap(it);
            },
            split: function () {
                if (self.length() >= rawLength) return self;
                var stack = [0].concat(self.start() || []).concat(self.end()).concat(self.end() != rawLength ? rawLength : []);
                var start = stack.shift();
                var separated = [];
                while (stack.length) {
                    var end = stack.shift();
                    var text = document.createTextNode(rawText.substring(start, end));
                    self.after(text);
                    separated.push(wrap(text));
                    start = end;
                }
                self.remove();
                return !self.start() ? separated[0] : separated[1];
            },
            remove: function (optimized) {
                var parent = node[$PARENT];
                if (optimized && parent.childNodes.length == 1) {
                    parent[$PARENT].removeChild(parent);
                }
                parent.removeChild(node);
                return this;
            },
            merge: function (other) {
                var it = self.split();
                return it.text(other.split().remove(true).text() + it.text());
            },
            after: function (e) {
                node[$PARENT].insertBefore(e, node);
                return this;
            },
            wrap: function (e) {
                e.appendChild(self.split().after(e).node);
            }
        };

        return self;
    }


    function select(start, end) {
        return promise(function (resolve) {
            start = wrap(start.text, start.offset, NIL), end = wrap(end.text, NIL, end.offset);
            var selected = [];
            while (start) {
                if (start.is(end)) {
                    selected.push(start.to(end));
                    break;
                }
                selected.push(start.toLast());
                start = start.next();
            }
            resolve(selected);
        });
    }

    function merge(filter) {
        return function (parts) {
            var result = [parts.shift()];
            while (parts.length) {
                var prev = result.pop();
                var next = parts.shift();
                if (filter(prev.node, next.node)) {
                    result.push(next.merge(prev));
                } else {
                    result.push(prev);
                    result.push(next);
                }
            }
            return result;
        }
    }

    function filter(test) {
        return function (parts) {
            return parts.filter(function (part) {
                return test(part.node);
            });
        }
    }

    function apply(consume) {
        return function (parts) {
            return parts.forEach(function (part) {
                return consume(part);
            });
        }
    }

    var exports = {
        __esModule: true,
        default: select,
        select: select,
        merge: merge,
        filter: filter,
        apply: apply
    };
    if (bind)for (var name in exports)context[name] = exports[name];
    return exports;
});


(function () {
    var COMPONENT_ID = 'highlight-' + +new Date;
    var highlighter = {
        init: function () {
            this.bindEvents();
        },
        /**
         *
         */
        bindEvents: function () {
            var self = this;
            $('.swatch').on('click', function () {
                $('.swatch').removeClass('active');
                $(this).addClass('active');
            });
            $('.content').mouseup(function () {
                var current = self.actived();
                if (current.hasClass('clear')) {
                    self.clear();
                } else {
                    self.highlight();
                }
            });

        },
        actived: function () {
            return $('.swatch.active');
        },
        color: function () {
            return this.actived().css('background-color');
        },
        /**
         *
         */
        highlight: function () {
            var self = this;
            var selection = self.getSelection();
            if (selection) {
                self.select(selection.getRangeAt(0)).//
                then(merge(function (left, right) {
                    var p1 = left.parentNode;
                    var p2 = right.parentNode;

                    var a1 = self.compare(left);
                    var a2 = self.compare(right);
                    return (a1 && a2 && p1.parentNode == p2.parentNode) ||
                        (!a1 && !a2 && p1 == p2) ||
                        (a1 && !a2 && p1.parentNode == p2) ||
                        (!a1 && a2 && p2.parentNode == p1);
                })).then(filter(function (part) {
                    return !self.compare(part);
                })).then(function (parts) {
                    parts.map(function (node) {
                        node.wrap(self.component());
                    });
                }).catch(function (e) {
                    console.log(e);
                });
                selection.removeAllRanges();
            }
        },
        component: function () {
            return $('<span data-toggle="' + COMPONENT_ID + '">').css('background-color', this.color()).get(0);
        },
        compare: function (text) {
            var self = this;
            var parent = $(text).parent();
            var highlighted = parent.is(self.selector());
            var color = parent.css('background-color');
            return highlighted && color == self.color();
        },
        selector: function () {
            return '[data-toggle="?"]'.replace(/\?/, COMPONENT_ID);
        },
        clear: function () {
            var self = this;
            var selection = self.getSelection();
            if (selection) {
                self.select(selection.getRangeAt(0)).then(apply(function (part) {
                    var text = $(part.split().node);
                    while (true) {
                        var comp = text.closest(self.selector());
                        if (!comp || !comp.length) {
                            break;
                        }
                        var children = comp.contents();
                        var first = children[0], last = children[children.length - 1];
                        if (text.is(last)) {
                            comp.after(text);
                        } else if (text.is(first)) {
                            comp.before(text);
                        } else {
                            var heading = comp.clone().empty();
                            for (var i = 0; i < children.length; i++) {
                                if (text.is(children[i])) {
                                    break;
                                }
                                heading.append(children[i]);
                            }
                            comp.before(heading).before(text);
                        }

                        if (first == last) comp.remove();
                    }
                }));
                selection.removeAllRanges();
            }
        },
        select: function (range) {
            return select(
                {text: range.startContainer, offset: range.startOffset},
                {text: range.endContainer, offset: range.endOffset}
            );
        },
        getSelection: function () {
            var sel = window.getSelection();
            return /^\s*$/.test(self && sel.toString()) ? null : sel;
        }
    };

    highlighter.init();

})();
body {
    margin: 0;
    background: #fafafa;
    box-shadow: 0 0 5rem rgba(0, 0, 0, 0.25) inset;
}

::-moz-selection {
    background-color: rgba(0, 0, 0, 0.2);
}

::selection {
    background-color: rgba(0, 0, 0, 0.2);
}

.content {
    padding: 100px;
}

.footer {
    padding: 0 100px 0 100px;
    flex-basis: 100%;
    height: 60px;
    background: #292B2C;
    position:fixed;top:0;width:100%;
}

.footer .items-left {
    float: left;
}

.footer-item {
    line-height: 60px;
}

#colors {
    padding: 12px;
}

.swatch {
    width: 30px;
    height: 30px;
    border-radius: 15px;
    box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.5), 0px 2px 2px rgba(0, 0, 0, 0.5);
    display: inline-block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="content">
    <span style="color:red;"><b>Content</b> <i>Lorem</i> <font size='7'>ipsum</font> dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
    dolore magna aliqua. Ut enim ad minim</span> veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
    commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
    pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
    laborum.
    Content Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
    commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
    pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
    laborum.
    Content Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
    commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
    pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
    laborum.
    Content Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
    commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
    pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
    laborum.
</div>
<div class="footer">
    <div class="items-left">
        <div id="colors">
            <div class="swatch active" style="background-color: rgba(255,255,131,.5);"></div>
            <div class="swatch" style="background-color: rgba(255,140,218,.5);"></div>
            <div class="swatch" style="background-color: rgba(144,255,184,.5);"></div>
            <div class="swatch clear"></div>
        </div>
    </div>
</div>