Move text selection out of gr-diff.

Change-Id: I0734653066a1bb78f95c141aa8202fad315b13c0
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index f647957..d9d6c8a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -62,6 +62,26 @@
           this._renderDiff();
         },
 
+        getLineElByChild: function(node) {
+          while (node) {
+            if (node instanceof Element) {
+              if (node.classList.contains('lineNum')) {
+                return node;
+              }
+              if (node.classList.contains('section')) {
+                return null;
+              }
+            }
+            node = node.previousSibling || node.parentElement;
+          };
+          return null;
+        },
+
+        getSideByLineEl: function(lineEl) {
+          return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
+            GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
+        },
+
         createCommentThread: function(changeNum, patchNum, path, side,
             projectConfig) {
           return this._builder.createCommentThread(changeNum, patchNum, path,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
new file mode 100644
index 0000000..09cab0b
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
@@ -0,0 +1,43 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-diff-selection">
+  <template>
+    <style>
+      .contentWrapper ::content .content {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+      }
+
+      :host.selected-right .contentWrapper ::content .right + .content,
+      :host.selected-left .contentWrapper ::content .left + .content,
+      :host.selected-right .contentWrapper ::content .unified .right ~ .content,
+      :host.selected-left .contentWrapper ::content .unified .left ~ .content {
+        -webkit-user-select: text;
+        -moz-user-select: text;
+        -ms-user-select: text;
+        user-select: text;
+      }
+    </style>
+    <div class="contentWrapper">
+      <content></content>
+    </div>
+  </template>
+  <script src="gr-diff-selection.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
new file mode 100644
index 0000000..6160c9d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -0,0 +1,84 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff-selection',
+
+    properties: {
+      _cachedDiffBuilder: Object,
+    },
+
+    listeners: {
+      'copy': '_handleCopy',
+      'down': '_handleDown',
+    },
+
+    get diffBuilder() {
+      if (!this._cachedDiffBuilder) {
+        this._cachedDiffBuilder =
+            Polymer.dom(this).querySelector('gr-diff-builder');
+      }
+      return this._cachedDiffBuilder;
+    },
+
+    _handleDown: function(e) {
+      var lineEl = this.diffBuilder.getLineElByChild(e.target);
+      if (!lineEl) {
+        return;
+      }
+      var side = this.diffBuilder.getSideByLineEl(lineEl);
+      this.classList.remove('selected-right', 'selected-left');
+      this.classList.add('selected-' + side);
+    },
+
+    _handleCopy: function(e) {
+      if (!e.target.classList.contains('content')) {
+        return;
+      }
+      var lineEl = this.diffBuilder.getLineElByChild(e.target);
+      if (!lineEl) {
+        return;
+      }
+      var side = this.diffBuilder.getSideByLineEl(lineEl);
+      var text = this._getSelectedText(side);
+      e.clipboardData.setData('Text', text);
+      e.preventDefault();
+    },
+
+    _getSelectedText: function(opt_side) {
+      var sel = window.getSelection();
+      if (sel.rangeCount != 1) {
+        return; // No multi-select support yet.
+      }
+      var range = sel.getRangeAt(0);
+      var fragment = range.cloneContents();
+      var selector = '.content,td.content:nth-of-type(1)';
+      if (opt_side) {
+        selector = '.' + opt_side + ' + ' + selector;
+      }
+      var contentEls = Polymer.dom(fragment).querySelectorAll(selector);
+      if (contentEls.length === 0) {
+        return fragment.textContent;
+      }
+
+      var text = '';
+      for (var i = 0; i < contentEls.length; i++) {
+        text += contentEls[i].textContent + '\n';
+      }
+      return text;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
new file mode 100644
index 0000000..3719de9
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff-selection</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-selection.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-diff-selection>
+      <table>
+        <tr>
+          <td class="lineNum left">1</td>
+          <td class="content">ba ba</td>
+          <td class="lineNum right">1</td>
+          <td class="other">some other text</td>
+        </tr>
+        <tr>
+          <td class="lineNum left">2</td>
+          <td class="content">zin</td>
+          <td class="lineNum right">2</td>
+          <td class="content">more more more</td>
+        </tr>
+        <tr>
+          <td class="lineNum left">2</td>
+          <td class="content">ga ga</td>
+          <td class="lineNum right">3</td>
+          <td class="other">some other text</td>
+        </tr>
+      </table>
+    </gr-diff-selection>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-diff-selection', function() {
+    var element;
+
+    var emulateCopyOn = function(element) {
+      var event = new CustomEvent('copy', {bubbles: true});
+      event.clipboardData = {
+        setData: sinon.stub(),
+      };
+      element.dispatchEvent(event);
+      return event;
+    };
+
+    setup(function() {
+      element = fixture('basic');
+      element._cachedDiffBuilder = {
+        getLineElByChild: sinon.stub().returns({}),
+        getSideByLineEl: sinon.stub(),
+      };
+    });
+
+    test('applies selected-left on left side click', function() {
+      element.classList.add('selected-right');
+      element._cachedDiffBuilder.getSideByLineEl.returns('left');
+      MockInteractions.down(element);
+      assert.isTrue(
+          element.classList.contains('selected-left'), 'adds selected-left');
+      assert.isFalse(
+          element.classList.contains('selected-right'),
+          'removes selected-right');
+    });
+
+    test('applies selected-right on right side click', function() {
+      element.classList.add('selected-left');
+      element._cachedDiffBuilder.getSideByLineEl.returns('right');
+      MockInteractions.down(element);
+      assert.isTrue(
+          element.classList.contains('selected-right'), 'adds selected-right');
+      assert.isFalse(
+          element.classList.contains('selected-left'), 'removes selected-left');
+    });
+
+    test('ignores copy for non-content Element', function() {
+      sinon.stub(element, '_getSelectedText');
+      emulateCopyOn(element.querySelector('.other'));
+      assert.isFalse(element._getSelectedText.called);
+    });
+
+    test('asks for text for right side Elements', function() {
+      element._cachedDiffBuilder.getSideByLineEl.returns('left');
+      sinon.stub(element, '_getSelectedText');
+      emulateCopyOn(element.querySelector('td.content'));
+      assert.deepEqual(['left'], element._getSelectedText.lastCall.args);
+    });
+
+    test('reacts to copy for content Elements', function() {
+      sinon.stub(element, '_getSelectedText');
+      emulateCopyOn(element.querySelector('td.content'));
+      assert.isTrue(element._getSelectedText.called);
+    });
+
+    test('inserts text into clipboard on copy', function() {
+      sinon.stub(element, '_getSelectedText').returns('the text');
+      var event = emulateCopyOn(element.querySelector('td.content'));
+      assert.deepEqual(
+          ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
+    });
+
+    test('copies content correctly', function() {
+      var selection = window.getSelection();
+      var range = document.createRange();
+      range.setStart(element.querySelector('td.content').firstChild, 3);
+      range.setEnd(
+          element.querySelectorAll('td.content')[3].firstChild, 2);
+      selection.addRange(range);
+      assert.equal('ba\nzin\nga\n', element._getSelectedText('left'));
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index cfe5d66..7c66bf1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -17,8 +17,9 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
 <link rel="import" href="../gr-diff-builder/gr-diff-builder.html">
+<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
+<link rel="import" href="../gr-diff-selection/gr-diff-selection.html">
 
 <dom-module id="gr-diff">
   <template>
@@ -41,7 +42,7 @@
         border-collapse: collapse;
         border-right: 1px solid #ddd;
       }
-      .section {
+      .lineNum {
         background-color: #eee;
       }
       .image-diff .gr-diff {
@@ -99,18 +100,6 @@
         max-width: var(--content-width, 80ch);
         min-width: var(--content-width, 80ch);
       }
-      .content.left {
-        -webkit-user-select: var(--left-user-select, text);
-        -moz-user-select: var(--left-user-select, text);
-        -ms-user-select: var(--left-user-select, text);
-        user-select: var(--left-user-select, text);
-      }
-      .content.right {
-        -webkit-user-select: var(--right-user-select, text);
-        -moz-user-select: var(--right-user-select, text);
-        -ms-user-select: var(--right-user-select, text);
-        user-select: var(--right-user-select, text);
-      }
       .content.add hl,
       .content.add.darkHighlight {
         background-color: var(--dark-add-highlight-color);
@@ -151,17 +140,17 @@
       }
     </style>
     <div class$="[[_computeContainerClass(_loggedIn, viewMode)]]"
-        on-tap="_handleTap"
-        on-mousedown="_handleMouseDown"
-        on-copy="_handleCopy">
-      <gr-diff-builder
-         id="diffBuilder"
-         view-mode="[[viewMode]]"
-         is-image-diff="[[isImageDiff]]"
-         base-image="[[_baseImage]]"
-         revision-image="[[_revisionImage]]">
-        <table id="diffTable"></table>
-      </gr-diff-builder>
+        on-tap="_handleTap">
+      <gr-diff-selection>
+        <gr-diff-builder
+            id="diffBuilder"
+            view-mode="[[viewMode]]"
+            is-image-diff="[[isImageDiff]]"
+            base-image="[[_baseImage]]"
+            revision-image="[[_revisionImage]]">
+          <table id="diffTable"></table>
+        </gr-diff-builder>
+      </gr-diff-selection>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index f89d1e8..f3a353b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -54,10 +54,6 @@
         value: DiffViewMode.SIDE_BY_SIDE,
       },
       _diff: Object,
-      _selectionSide: {
-        type: String,
-        observer: '_selectionSideChanged',
-      },
       _comments: Object,
     },
 
@@ -297,66 +293,6 @@
       });
     },
 
-    _handleMouseDown: function(e) {
-      var el = Polymer.dom(e).rootTarget;
-      var side;
-      for (var node = el; node != null; node = node.parentNode) {
-        if (!node.classList) { continue; }
-
-        if (node.classList.contains(DiffSide.LEFT)) {
-          side = DiffSide.LEFT;
-          break;
-        } else if (node.classList.contains(DiffSide.RIGHT)) {
-          side = DiffSide.RIGHT;
-          break;
-        }
-      }
-      this._selectionSide = side;
-    },
-
-    _selectionSideChanged: function(side) {
-      if (side) {
-        var oppositeSide = side === DiffSide.RIGHT ?
-            DiffSide.LEFT : DiffSide.RIGHT;
-        this.customStyle['--' + side + '-user-select'] = 'text';
-        this.customStyle['--' + oppositeSide + '-user-select'] = 'none';
-      } else {
-        this.customStyle['--left-user-select'] = 'text';
-        this.customStyle['--right-user-select'] = 'text';
-      }
-      this.updateStyles();
-    },
-
-    _handleCopy: function(e) {
-      if (!e.target.classList.contains('content')) {
-        return;
-      }
-      var text = this._getSelectedText(this._selectionSide);
-      e.clipboardData.setData('Text', text);
-      e.preventDefault();
-    },
-
-    _getSelectedText: function(opt_side) {
-      var sel = window.getSelection();
-      var range = sel.getRangeAt(0);
-      var doc = range.cloneContents();
-      var selector = '.content';
-      if (opt_side) {
-        selector += '.' + opt_side;
-      }
-      var contentEls = Polymer.dom(doc).querySelectorAll(selector);
-
-      if (contentEls.length === 0) {
-        return doc.textContent;
-      }
-
-      var text = '';
-      for (var i = 0; i < contentEls.length; i++) {
-        text += contentEls[i].textContent + '\n';
-      }
-      return text;
-    },
-
     _prefsChanged: function(prefsChangeRecord) {
       var prefs = prefsChangeRecord.base;
       this.customStyle['--content-width'] = prefs.line_length + 'ch';
@@ -473,7 +409,6 @@
           this.changeNum, this._diff, this.patchRange);
     },
 
-
     _projectConfigChanged: function(projectConfig) {
       var threadEls = this._getCommentThreads();
       for (var i = 0; i < threadEls.length; i++) {