0

I'm using a script from Tim Down to create a highlight. Now, I'd like the user to click the browser action again to remove it.

I thought I could add another if statement to this snippet:

function makeEditableAndHighlight(colour) {
    var range, sel = window.getSelection();
    if (sel.rangeCount && sel.getRangeAt) {
        range = sel.getRangeAt(0);
    }
    document.designMode = "on";
    if (range) {
        sel.removeAllRanges();
        sel.addRange(range);
    }
    // Use HiliteColor since some browsers apply BackColor to the whole block
    if (!document.execCommand("HiliteColor", false, '#FFFF00')) {
        document.execCommand("BackColor", false, '#FFFF00');
    }
    if (!document.execCommand("HiliteColor", true, '#FFFF00')) {  // Added this logic
        document.execCommand("removeFormat", false, null);
    }
    document.designMode = "off";
}

My thinking was that if "HiliteColor" returned true, it would remove the formatting, but it isn't working. Any thoughts?

Edit After doing more reading, I learned that the boolean in the execCommand doesn't have anything to do with returning a value. How can I improve my logic to reverse the background color? Is it even doable?

Community
  • 1
  • 1
Brian
  • 4,274
  • 2
  • 27
  • 55

1 Answers1

4

You need a way to "serialize" the selected range for later access.
This answer explains how to achieve the serialization/deserialization.

Your code could look like this:

var serializedRange;

/* Serializes and returns the specified range
 * (ignoring it if its length is zero) */
function serializeRange(range) {
    return (!range || ((range.startContainer === range.endContainer)
                       && (range.startOffset === range.endOffset)))
            ? null : {
                startContainer: range.startContainer,
                startOffset:    range.startOffset,
                endContainer:   range.endContainer,
                endOffset:      range.endOffset
            };
}

/* Restores the specified serialized version
 * (removing any ranges currently seleted) */
function restoreRange(serialized) {
    var range = document.createRange();
    range.setStart(serialized.startContainer, serialized.startOffset);
    range.setEnd(serialized.endContainer, serialized.endOffset);

    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
}

/* Hilites the currently selected range or removes the hilite
 * (if there is a previously serialized range) */
function toggleHilite() {
    document.designMode = 'on';

    var sel = window.getSelection();
    if (serializedRange) {
        /* There is a hilited range, let's remove the hilite */
        restoreRange(serializedRange);
        serializedRange = null;
        document.execCommand('removeFormat', false, null);
        sel.removeAllRanges();
    } else {
        /* There is no hilited range, so hilite
         * the currently selected range (if any) */
        if (sel.rangeCount && sel.getRangeAt) {
            document.execCommand('hiliteColor', false, '#FFFF00');
            serializedRange = serializeRange(sel.getRangeAt(0));
            // it is important to serialize the range *after* hiliting,
            // because `execCommand` will change the DOM affecting the
            // range's start-/endContainer and offsets.
        }
    }

    document.designMode = 'off';
}
Community
  • 1
  • 1
gkalpak
  • 47,844
  • 8
  • 105
  • 118
  • This was fantastic! Thank you so much for helping with this. – Brian Jan 31 '14 at 01:42
  • Always glad to help ! (Just a heads-up: I made one minor improvement in `serializeRange()`.) – gkalpak Jan 31 '14 at 01:50
  • Ok, because I'm still learning, I just to make sure I understand the change: the `===` you added is making sure that the starting/ending points for the selection aren't messed up by the DOM restructure, right? In other words, if the values don't return identical, it'll test false ad then **add** a highlight rather than remove? – Brian Jan 31 '14 at 02:01
  • I noticed that sometimes (e.g. when clicking in an area with selectable text) Chrome would report a selected range that started and ended at the same index in the same container (essentially a 0-length selection). Hiliting such a selection would have no visual effect, so it would be harmful to the UX (I can give an example if it is not clear why). So, I had to check if a selection had 0-length and treat it as no selection at all. Initially I was just comparing the start- and endOffsets. – gkalpak Jan 31 '14 at 06:50
  • But I realized I had to compare the containers ss well (because starting at position x in container y and ending at position x a dozen containers further down the DOM does **not** make me a 0-length range). – gkalpak Jan 31 '14 at 06:51
  • Okay, I'm running into a confusing problem. The script seemed to work last night, but I must not have tested it well enough. It's running fine on the first click and `console.log(serializedRange)` returns the selection object. When I highlight a new range and click again, the console displays `null`. Is the serialized object not being stored properly? – Brian Jan 31 '14 at 18:14
  • This is what the script is supposed to do: **1.** Select a range and click button: script hilites and stores range. **2.** Click button again (regardless if you have a range selected or not): script restores the serialized range "un-hilites" it and clears all ranges. **3.** You select a range and click the button: back at step (1). Are you experiencing some other behaviour ? – gkalpak Jan 31 '14 at 20:27
  • OK, I see that now. I was thinking one thing when I posted the question, and asked something different. I'll work on adding the functionality I'm looking for a different way. Thanks again! – Brian Jan 31 '14 at 20:30