Dax89 / WebPirate

A Tabbed, WebKit based Browser Web for SailfishOS
GNU General Public License v3.0
15 stars 7 forks source link

[WIP] Android Like Text Selection #81

Closed Dax89 closed 8 years ago

Dax89 commented 8 years ago

@llelectronics: you might find this one very useful for Webcat too :)

Pure JS implementation of Android like text selection, currenty I'm testing it directly on Chromium (I can debug it easily), markers still doesn't follow SailfishOS' ambience, but it's pretty easy to implement.

Text selection il very good atm, but it still needs some polish because sometimes it goes crazy. Any improvement is welcome!

JS Code

var __wp_textselection__ = {
    SELECTION_MARKER: "__wp_selection_marker__",
    SELECTION_START_MARKER: "__wp_selection_start_marker__",
    SELECTION_END_MARKER: "__wp_selection_end_marker__",
    SELECTION_WIDTH: 25,
    SELECTION_HEIGHT: 40,
    SELECTION_PADDING: 20,
    moving: false,

    isStartMarker: function(target) {
        return target.className.indexOf(__wp_textselection__.SELECTION_START_MARKER) !== -1;
    },

    isEndMarker: function(target) {
        return target.className.indexOf(__wp_textselection__.SELECTION_END_MARKER) !== -1;
    },

    isMarker: function(target) {
      return __wp_textselection__.isStartMarker(target) || __wp_textselection__.isEndMarker(target);
    },

    onTouchStart: function(touchevent) {
        touchevent.preventDefault();
    },

    onTouchMove: function(touchevent) {
        if(touchevent.touches.length <= 0)
            return;

        touchevent.preventDefault();

        var target = touchevent.target;
        var touch = touchevent.touches[0];

        var range = document.caretRangeFromPoint(touch.clientX, touch.clientY - __wp_textselection__.SELECTION_PADDING);
        __wp_textselection__.updateSelection(target, range);
    },

    onTouchEnd: function(touchevent) {
        touchevent.preventDefault();
    },

    isReversed: function(newrange, oldrange, displaystart) {
      var newrect = newrange.getBoundingClientRect();
      var oldrect = oldrange.getBoundingClientRect();
      var reversed = false; 

      if(displaystart)
        reversed = (newrange.startOffset >= oldrange.endOffset) && (newrect.top >= oldrect.bottom);
      else
        reversed = (newrange.startOffset <= oldrange.startOffset) && (newrect.top <= oldrect.top);

      return reversed;
    },

    swapMarkers: function() {
        var startmarker = document.querySelector("." + __wp_textselection__.SELECTION_START_MARKER);
        var endmarker = document.querySelector("." + __wp_textselection__.SELECTION_END_MARKER);

        startmarker.className = __wp_textselection__.SELECTION_MARKER + " " + __wp_textselection__.SELECTION_END_MARKER;
        endmarker.className = __wp_textselection__.SELECTION_MARKER + " " + __wp_textselection__.SELECTION_START_MARKER;
    },

    updateSelection: function(target, newrange) {
        var selection = window.getSelection();
        var oldrange = selection.getRangeAt(0);
        var displaystart = __wp_textselection__.isStartMarker(target)
        var displayend = __wp_textselection__.isEndMarker(target)
        var reversed = __wp_textselection__.isReversed(newrange, oldrange, displaystart);
        var range = document.createRange();

        if(displaystart) {
          if(reversed) {
            range.setStart(oldrange.endContainer, oldrange.endOffset);
            range.setEnd(newrange.startContainer, newrange.startOffset);
          }
          else {
            range.setStart(newrange.startContainer, newrange.startOffset);
            range.setEnd(oldrange.endContainer, oldrange.endOffset);
          }
        }
        else if(displayend) {
          if(reversed) {
            range.setStart(newrange.startContainer, newrange.startOffset);
            range.setEnd(oldrange.endContainer, oldrange.endOffset);
          }
          else {
            range.setStart(oldrange.startContainer, oldrange.startOffset);
            range.setEnd(newrange.endContainer, newrange.endOffset);
          }
        }
        else
          return;

        console.log(range);

        selection.removeAllRanges();
        selection.addRange(range);

        if(reversed)
          __wp_textselection__.swapMarkers();

        __wp_textselection__.displayMarkers(selection, displaystart, displayend);
    },

    createMarkerStyle: function() {
        var head = document.getElementsByTagName("HEAD")[0];

        // General Style
        var markerstyle = document.createElement("STYLE");
        markerstyle.id = __wp_textselection__.SELECTION_MARKER;

        markerstyle.innerHTML = "." + __wp_textselection__.SELECTION_MARKER + " {\n" +
                "background: linear-gradient(#34a727 0, darkgreen 18px);\n" +
                "background-color: darkgreen;\n" +
                "box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);\n" +
                "content: \"\";\n" +
                "display: block;\n" +
                "height: " + __wp_textselection__.SELECTION_HEIGHT + "px;\n" +
                "opacity: 0.95;\n" +
                "position: absolute;\n" +
                "width: " + __wp_textselection__.SELECTION_WIDTH + "px;\n" +
                "}";

        head.appendChild(markerstyle);

        // Start Marker Style
        markerstyle = document.createElement("STYLE");
        markerstyle.id = __wp_textselection__.SELECTION_START_MARKER;

        markerstyle.innerHTML = "." + __wp_textselection__.SELECTION_START_MARKER + "{\n" +
                "margin-left: -25px;\n" +
                "border-radius: 25px 0 0 0;\n" +
                "}";

        head.appendChild(markerstyle);

        // End Marker Style
        markerstyle = document.createElement("STYLE");
        markerstyle.id = __wp_textselection__.SELECTION_END_MARKER;

        markerstyle.innerHTML = "." + __wp_textselection__.SELECTION_END_MARKER + "{\n" +
                "border-radius: 0 25px 0 0;\n" +
                "}";

        head.appendChild(markerstyle);
    },

    createMarker: function(style) {
        var marker = document.createElement("DIV");
        marker.className = __wp_textselection__.SELECTION_MARKER + " " + style;
        marker.setAttribute("style", "visibility: hidden; pointer-events: auto; z-index: 1200;");

        marker.addEventListener("touchstart", __wp_textselection__.onTouchStart, true);
        marker.addEventListener("touchmove", __wp_textselection__.onTouchMove, true);
        marker.addEventListener("touchend", __wp_textselection__.onTouchEnd, true);

        var body = document.getElementsByTagName("BODY")[0];
        body.appendChild(marker);
    },

    displayMarkers: function(selection, displaystart, displayend) {
        if(selection.rangeCount <= 0) {
            __wp_textselection__.hideMarkers();
            return;
        }

        var r = selection.getRangeAt(0);
        var rects = r.getClientRects();
        var firstrect = rects[0], lastrect = rects[rects.length - 1];

        if((displaystart === undefined) || displaystart === true) {
          var startmarker = document.querySelector("." + __wp_textselection__.SELECTION_START_MARKER);
          startmarker.style.top = (window.scrollY + firstrect.bottom + __wp_textselection__.SELECTION_PADDING) + "px";
          startmarker.style.left = (window.scrollX + firstrect.left) + "px";
          startmarker.style.visibility = "visible";
        }

        if((displayend === undefined) || displayend === true) {
          var endmarker = document.querySelector("." + __wp_textselection__.SELECTION_END_MARKER);
          endmarker.style.top = (window.scrollY + lastrect.bottom + __wp_textselection__.SELECTION_PADDING) + "px";
          endmarker.style.left = (window.scrollX + lastrect.right) + "px";
          endmarker.style.visibility = "visible";
        }
    },

    hideMarkers: function() {
        var startmarker = document.querySelector("." + __wp_textselection__.SELECTION_START_MARKER);
        var endmarker = document.querySelector("." + __wp_textselection__.SELECTION_END_MARKER);

        startmarker.style.visibility = "hidden";
        endmarker.style.visibility = "hidden";
    },

    wordRange: function(clientx, clienty) {
        var range = document.caretRangeFromPoint(clientx, clienty);
        var selstart = range.startContainer;
        var i = range.startOffset;

        while((i > 0) && !/\s/.test(selstart.textContent[i]))
            i--;

        i++; // Stay in bounds with the selected word

        var word = /\w+/.exec(selstart.textContent.substr(i));

        if(!word || !word[0]) { // FIXME: Fallback to node selection
            console.log("word is null");
            range.selectNodeContents(selstart);
            return range;
        }

        range.setStart(selstart, i);
        range.setEnd(selstart, i + word[0].length);
        return range;
    },

    select: function(clientx, clienty) {
        if(!document.getElementById(__wp_textselection__.SELECTION_MARKER)) {
            __wp_textselection__.createMarkerStyle();
            __wp_textselection__.createMarker(__wp_textselection__.SELECTION_START_MARKER);
            __wp_textselection__.createMarker(__wp_textselection__.SELECTION_END_MARKER);
        }

        var range = __wp_textselection__.wordRange(clientx, clienty);
        var selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
        __wp_textselection__.displayMarkers(selection);
    }
};

document.addEventListener("touchstart", function(touchevent) {
  touchevent.preventDefault();

  if(touchevent.target.className.indexOf(__wp_textselection__.SELECTION_MARKER) !== -1)
    return;

  var touch = touchevent.touches[0];
  __wp_textselection__.select(touch.clientX, touch.clientY);
});

/*
var selection = window.getSelection();

document.addEventListener("touchmove", function(touchevent) {
  touchevent.preventDefault();

  var touch = touchevent.touches[0];
  var range = document.caretRangeFromPoint(touch.clientX, touch.clientY);

  if(selection.rangeCount <= 0) {
    selection.addRange(range);
    return;
  }

  var docrange = selection.getRangeAt(0);
  docrange.setEnd(range.endContainer, range.endOffset);
  //console.log(docrange);
  selection.removeAllRanges();
  selection.addRange(docrange);
});
*/

Screenshot Text Selection

llelectronics commented 8 years ago

Looks promising. I had that also on my todo list. Though never found time starting implementing it.

Dax89 commented 8 years ago

Now the selection is calibrated and very precise!

var __wp_textselection__ = {
    SELECTION_MARKER: "__wp_selection_marker__",
    SELECTION_START_MARKER: "__wp_selection_start_marker__",
    SELECTION_END_MARKER: "__wp_selection_end_marker__",
    SELECTION_WIDTH: 25,
    SELECTION_HEIGHT: 40,
    SELECTION_PADDING: 20,
    moving: false,

    isStartMarker: function(target) {
        return target.className.indexOf(__wp_textselection__.SELECTION_START_MARKER) !== -1;
    },

    isEndMarker: function(target) {
        return target.className.indexOf(__wp_textselection__.SELECTION_END_MARKER) !== -1;
    },

    isMarker: function(target) {
      return __wp_textselection__.isStartMarker(target) || __wp_textselection__.isEndMarker(target);
    },

    onTouchStart: function(touchevent) {
        touchevent.preventDefault();
    },

    onTouchMove: function(touchevent) {
        if(touchevent.touches.length <= 0)
            return;

        touchevent.preventDefault();

        var target = touchevent.target;
        var touch = touchevent.touches[0];

        var range = document.caretRangeFromPoint(touch.clientX, touch.clientY - (__wp_textselection__.SELECTION_HEIGHT + __wp_textselection__.SELECTION_PADDING))
        __wp_textselection__.updateSelection(target, range);
    },

    onTouchEnd: function(touchevent) {
        touchevent.preventDefault();
    },

    isReversed: function(oldrange) {
        if(oldrange.collapsed)
          return true;

        return false;
    },

    swapMarkers: function() {
        var startmarker = document.querySelector("." + __wp_textselection__.SELECTION_START_MARKER);
        var endmarker = document.querySelector("." + __wp_textselection__.SELECTION_END_MARKER);

        startmarker.className = __wp_textselection__.SELECTION_MARKER + " " + __wp_textselection__.SELECTION_END_MARKER;
        endmarker.className = __wp_textselection__.SELECTION_MARKER + " " + __wp_textselection__.SELECTION_START_MARKER;
    },

    updateSelection: function(target, newrange) {
        var selection = window.getSelection();
        var oldrange = selection.getRangeAt(0);
        var displaystart = __wp_textselection__.isStartMarker(target)
        var displayend = __wp_textselection__.isEndMarker(target)
        var reversed = __wp_textselection__.isReversed(oldrange);
        var range = document.createRange();

        if(displaystart) {
          if(reversed) {
            range.setStart(oldrange.endContainer, oldrange.endOffset);
            range.setEnd(newrange.startContainer, newrange.startOffset);
          }
          else {
            range.setStart(newrange.startContainer, newrange.startOffset);
            range.setEnd(oldrange.endContainer, oldrange.endOffset);
          }
        }
        else if(displayend) {
          if(reversed) {
            range.setStart(newrange.startContainer, newrange.startOffset);
            range.setEnd(oldrange.endContainer, oldrange.endOffset);
          }
          else {
            range.setStart(oldrange.startContainer, oldrange.startOffset);
            range.setEnd(newrange.endContainer, newrange.endOffset);
          }
        }
        else
          return;

        selection.removeAllRanges();
        selection.addRange(range);

        __wp_textselection__.displayMarkers(selection, displaystart, displayend);

        if(reversed)
          __wp_textselection__.swapMarkers();
    },

    createMarkerStyle: function() {
        var head = document.getElementsByTagName("HEAD")[0];

        // General Style
        var markerstyle = document.createElement("STYLE");
        markerstyle.id = __wp_textselection__.SELECTION_MARKER;

        markerstyle.innerHTML = "." + __wp_textselection__.SELECTION_MARKER + " {\n" +
                "background: linear-gradient(#34a727 0, darkgreen 18px);\n" +
                "background-color: darkgreen;\n" +
                "box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);\n" +
                "content: \"\";\n" +
                "display: block;\n" +
                "height: " + __wp_textselection__.SELECTION_HEIGHT + "px;\n" +
                "opacity: 0.95;\n" +
                "position: absolute;\n" +
                "width: " + __wp_textselection__.SELECTION_WIDTH + "px;\n" +
                "}";

        head.appendChild(markerstyle);

        // Start Marker Style
        markerstyle = document.createElement("STYLE");
        markerstyle.id = __wp_textselection__.SELECTION_START_MARKER;

        markerstyle.innerHTML = "." + __wp_textselection__.SELECTION_START_MARKER + "{\n" +
                "margin-left: -25px;\n" +
                "border-radius: 25px 0 0 0;\n" +
                "}";

        head.appendChild(markerstyle);

        // End Marker Style
        markerstyle = document.createElement("STYLE");
        markerstyle.id = __wp_textselection__.SELECTION_END_MARKER;

        markerstyle.innerHTML = "." + __wp_textselection__.SELECTION_END_MARKER + "{\n" +
                "border-radius: 0 25px 0 0;\n" +
                "}";

        head.appendChild(markerstyle);
    },

    createMarker: function(style) {
        var marker = document.createElement("DIV");
        marker.className = __wp_textselection__.SELECTION_MARKER + " " + style;
        marker.setAttribute("style", "visibility: hidden; pointer-events: auto; z-index: 1200;");

        marker.addEventListener("touchstart", __wp_textselection__.onTouchStart, true);
        marker.addEventListener("touchmove", __wp_textselection__.onTouchMove, true);
        marker.addEventListener("touchend", __wp_textselection__.onTouchEnd, true);

        var body = document.getElementsByTagName("BODY")[0];
        body.appendChild(marker);
    },

    displayMarkers: function(selection, displaystart, displayend) {
        if(selection.rangeCount <= 0) {
            __wp_textselection__.hideMarkers();
            return;
        }

        var bodyrect = document.body.getBoundingClientRect();
        var r = selection.getRangeAt(0);
        var rects = r.getClientRects();
        var firstrect = rects[0], lastrect = rects[rects.length - 1];

        if((displaystart === undefined) || displaystart === true) {
          var startmarker = document.querySelector("." + __wp_textselection__.SELECTION_START_MARKER);
          startmarker.style.top = ((firstrect.bottom  - bodyrect.top) + __wp_textselection__.SELECTION_PADDING) + "px";
          startmarker.style.left = (firstrect.left - bodyrect.left) + "px";
          startmarker.style.visibility = "visible";
        }

        if((displayend === undefined) || displayend === true) {
          var endmarker = document.querySelector("." + __wp_textselection__.SELECTION_END_MARKER);
          endmarker.style.top = ((lastrect.bottom - bodyrect.top) + __wp_textselection__.SELECTION_PADDING) + "px";
          endmarker.style.left = (lastrect.right - bodyrect.left) + "px";
          endmarker.style.visibility = "visible";
        }
    },

    hideMarkers: function() {
        var startmarker = document.querySelector("." + __wp_textselection__.SELECTION_START_MARKER);
        var endmarker = document.querySelector("." + __wp_textselection__.SELECTION_END_MARKER);

        startmarker.style.visibility = "hidden";
        endmarker.style.visibility = "hidden";
    },

    wordRange: function(clientx, clienty) {
        var range = document.caretRangeFromPoint(clientx, clienty);
        var selstart = range.startContainer;
        var i = range.startOffset;

        while((i > 0) && !/\s/.test(selstart.textContent[i]))
            i--;

        i++; // Stay in bounds with the selected word

        var word = /\w+/.exec(selstart.textContent.substr(i));

        if(!word || !word[0]) { // FIXME: Fallback to node selection
            console.log("word is null");
            range.selectNodeContents(selstart);
            return range;
        }

        range.setStart(selstart, i);
        range.setEnd(selstart, i + word[0].length);
        return range;
    },

    select: function(clientx, clienty) {
        if(!document.getElementById(__wp_textselection__.SELECTION_MARKER)) {
            __wp_textselection__.createMarkerStyle();
            __wp_textselection__.createMarker(__wp_textselection__.SELECTION_START_MARKER);
            __wp_textselection__.createMarker(__wp_textselection__.SELECTION_END_MARKER);
        }

        var range = __wp_textselection__.wordRange(clientx, clienty);
        var selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
        __wp_textselection__.displayMarkers(selection);
    }
};

document.addEventListener("touchstart", function(touchevent) {
  touchevent.preventDefault();

  if(touchevent.target.className.indexOf(__wp_textselection__.SELECTION_MARKER) !== -1)
    return;

  var touch = touchevent.touches[0];
  __wp_textselection__.select(touch.clientX, touch.clientY);
});

/*
var selection = window.getSelection();

document.addEventListener("touchmove", function(touchevent) {
  touchevent.preventDefault();

  var touch = touchevent.touches[0];
  var range = document.caretRangeFromPoint(touch.clientX, touch.clientY);

  if(selection.rangeCount <= 0) {
    selection.addRange(range);
    return;
  }

  var docrange = selection.getRangeAt(0);
  docrange.setEnd(range.endContainer, range.endOffset);
  //console.log(docrange);
  selection.removeAllRanges();
  selection.addRange(docrange);
});
*/

EDIT: The (basic) implementation is now complete!