Search code examples
javascriptjqueryhtmlcsswindow

Replacing selected text on mouseup


I am building a real-time HTML highlighter so that when a user selects a range of text that text is surrounded with span elements that have a background property.

Here is the fiddle: https://jsfiddle.net/4hd2vrex/

Problem is that this can get quite messy when users do multiple selects, the spans get nested and I get content like this:

<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>

Holy Jackpot Batman! To remedy this I have the following idea:

Before adding any spans just replace all the selected text, span tags and all, with the original selected text window.getSelection().

So for example, if I selected that mess of spans above, before wrapping my selected text with more spans I would replace those spans with window.getSelection() which is just the text reprehenderit and I would get.

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

Q: How do I replace my selection with the selected text?


Solution

  • I have done the whole highlight text with my ways,not use window.Selection API,but use:select(start,end).then(merge).then(filter).then(highlight).and the most interesting things is that it can be highlight complex element,not even if text only.I found that the select api can also write a wysiwyg html editor,so I shared it to everyone who interesting on selection problem,and wish to help you,good question!

    (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>