From e9e43041f0d8740b9c9ba7a4ec73f848062647ab Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Tue, 21 Jan 2014 18:13:23 -0500 Subject: [PATCH] Add Selection interface --- build.json | 1 + shadowdom.js | 1 + src/wrappers/Document.js | 12 ++- src/wrappers/Range.js | 3 + src/wrappers/Selection.js | 67 +++++++++++++++ src/wrappers/Window.js | 22 +++-- test/js/Range.js | 19 +++++ test/js/Selection.js | 169 ++++++++++++++++++++++++++++++++++++++ test/test.main.js | 1 + 9 files changed, 287 insertions(+), 8 deletions(-) create mode 100644 src/wrappers/Selection.js create mode 100644 test/js/Selection.js diff --git a/build.json b/build.json index a9c08d0..e564454 100644 --- a/build.json +++ b/build.json @@ -32,6 +32,7 @@ "src/wrappers/ShadowRoot.js", "src/ShadowRenderer.js", "src/wrappers/elements-with-form-property.js", + "src/wrappers/Selection.js", "src/wrappers/Document.js", "src/wrappers/Window.js", "src/wrappers/override-constructors.js" diff --git a/shadowdom.js b/shadowdom.js index cce3f91..3b2b8a4 100644 --- a/shadowdom.js +++ b/shadowdom.js @@ -48,6 +48,7 @@ 'src/wrappers/ShadowRoot.js', 'src/ShadowRenderer.js', 'src/wrappers/elements-with-form-property.js', + 'src/wrappers/Selection.js', 'src/wrappers/Document.js', 'src/wrappers/Window.js', 'src/wrappers/override-constructors.js' diff --git a/src/wrappers/Document.js b/src/wrappers/Document.js index cf4ed44..d468d77 100644 --- a/src/wrappers/Document.js +++ b/src/wrappers/Document.js @@ -8,6 +8,7 @@ var GetElementsByInterface = scope.GetElementsByInterface; var Node = scope.wrappers.Node; var ParentNodeInterface = scope.ParentNodeInterface; + var Selection = scope.wrappers.Selection; var SelectorsInterface = scope.SelectorsInterface; var ShadowRoot = scope.wrappers.ShadowRoot; var defineWrapGetter = scope.defineWrapGetter; @@ -16,9 +17,10 @@ var matchesNames = scope.matchesNames; var mixin = scope.mixin; var registerWrapper = scope.registerWrapper; + var renderAllPending = scope.renderAllPending; + var rewrap = scope.rewrap; var unwrap = scope.unwrap; var wrap = scope.wrap; - var rewrap = scope.rewrap; var wrapEventTargetMethods = scope.wrapEventTargetMethods; var wrapNodeList = scope.wrapNodeList; @@ -55,7 +57,7 @@ 'createEventNS', 'createRange', 'createTextNode', - 'getElementById', + 'getElementById' ].forEach(wrapMethod); var originalAdoptNode = document.adoptNode; @@ -82,6 +84,7 @@ } var originalImportNode = document.importNode; + var originalGetSelection = document.getSelection; mixin(Document.prototype, { adoptNode: function(node) { @@ -103,6 +106,10 @@ } } return clone; + }, + getSelection: function() { + renderAllPending(); + return new Selection(originalGetSelection.call(unwrap(this))); } }); @@ -239,6 +246,7 @@ 'createTextNode', 'elementFromPoint', 'getElementById', + 'getSelection', ]); mixin(Document.prototype, GetElementsByInterface); diff --git a/src/wrappers/Range.js b/src/wrappers/Range.js index bcf1de3..7e85aeb 100644 --- a/src/wrappers/Range.js +++ b/src/wrappers/Range.js @@ -75,6 +75,9 @@ }, intersectsNode: function(node) { return this.impl.intersectsNode(unwrapIfNeeded(node)); + }, + toString: function() { + return this.impl.toString(); } }; diff --git a/src/wrappers/Selection.js b/src/wrappers/Selection.js new file mode 100644 index 0000000..44c337c --- /dev/null +++ b/src/wrappers/Selection.js @@ -0,0 +1,67 @@ +// Copyright 2014 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var registerWrapper = scope.registerWrapper; + var unwrap = scope.unwrap; + var unwrapIfNeeded = scope.unwrapIfNeeded; + var wrap = scope.wrap; + + var OriginalSelection = window.Selection; + + function Selection(impl) { + this.impl = impl; + } + Selection.prototype = { + get anchorNode() { + return wrap(this.impl.anchorNode); + }, + get focusNode() { + return wrap(this.impl.focusNode); + }, + addRange: function(range) { + this.impl.addRange(unwrap(range)); + }, + collapse: function(node, index) { + this.impl.collapse(unwrapIfNeeded(node), index); + }, + containsNode: function(node, allowPartial) { + return this.impl.containsNode(unwrapIfNeeded(node), allowPartial); + }, + extend: function(node, offset) { + this.impl.extend(unwrapIfNeeded(node), offset); + }, + getRangeAt: function(index) { + return wrap(this.impl.getRangeAt(index)); + }, + removeRange: function(range) { + this.impl.removeRange(unwrap(range)); + }, + selectAllChildren: function(node) { + this.impl.selectAllChildren(unwrapIfNeeded(node)); + }, + toString: function() { + return this.impl.toString(); + } + }; + + // WebKit extensions. Not implemented. + // readonly attribute Node baseNode; + // readonly attribute long baseOffset; + // readonly attribute Node extentNode; + // readonly attribute long extentOffset; + // [RaisesException] void setBaseAndExtent([Default=Undefined] optional Node baseNode, + // [Default=Undefined] optional long baseOffset, + // [Default=Undefined] optional Node extentNode, + // [Default=Undefined] optional long extentOffset); + // [RaisesException, ImplementedAs=collapse] void setPosition([Default=Undefined] optional Node node, + // [Default=Undefined] optional long offset); + + registerWrapper(window.Selection, Selection, window.getSelection()); + + scope.wrappers.Selection = Selection; + +})(window.ShadowDOMPolyfill); diff --git a/src/wrappers/Window.js b/src/wrappers/Window.js index 4575273..5842540 100644 --- a/src/wrappers/Window.js +++ b/src/wrappers/Window.js @@ -6,29 +6,34 @@ 'use strict'; var EventTarget = scope.wrappers.EventTarget; + var Selection = scope.wrappers.Selection; var mixin = scope.mixin; var registerWrapper = scope.registerWrapper; + var renderAllPending = scope.renderAllPending; var unwrap = scope.unwrap; var unwrapIfNeeded = scope.unwrapIfNeeded; var wrap = scope.wrap; - var renderAllPending = scope.renderAllPending; var OriginalWindow = window.Window; + var originalGetComputedStyle = window.getComputedStyle; + var originalGetSelection = window.getSelection; function Window(impl) { EventTarget.call(this, impl); } Window.prototype = Object.create(EventTarget.prototype); - var originalGetComputedStyle = window.getComputedStyle; OriginalWindow.prototype.getComputedStyle = function(el, pseudo) { - renderAllPending(); - return originalGetComputedStyle.call(this || window, unwrapIfNeeded(el), - pseudo); + return wrap(this || window).getComputedStyle(unwrapIfNeeded(el), pseudo); + }; + + OriginalWindow.prototype.getSelection = function() { + return wrap(this || window).getSelection(); }; // Work around for https://bugzilla.mozilla.org/show_bug.cgi?id=943065 delete window.getComputedStyle; + delete window.getSelection; ['addEventListener', 'removeEventListener', 'dispatchEvent'].forEach( function(name) { @@ -43,9 +48,14 @@ mixin(Window.prototype, { getComputedStyle: function(el, pseudo) { + renderAllPending(); return originalGetComputedStyle.call(unwrap(this), unwrapIfNeeded(el), pseudo); - } + }, + getSelection: function() { + renderAllPending(); + return new Selection(originalGetSelection.call(unwrap(this))); + }, }); registerWrapper(OriginalWindow, Window); diff --git a/test/js/Range.js b/test/js/Range.js index 23d0b49..e00c644 100644 --- a/test/js/Range.js +++ b/test/js/Range.js @@ -8,6 +8,14 @@ suite('Range', function() { var wrap = ShadowDOMPolyfill.wrap; + var div; + + teardown(function() { + if (div && div.parentNode) + div.parentNode.removeChild(div); + div = undefined; + }); + test('instanceof', function() { var range = document.createRange(); assert.instanceOf(range, Range); @@ -55,4 +63,15 @@ suite('Range', function() { assert.isFalse(range.hasOwnProperty('startOffset')); }); + test('toString', function() { + var range = document.createRange(); + div = document.createElement('div'); + document.body.appendChild(div); + div.innerHTML = 'abc'; + var a = div.firstChild; + var b = a.nextSibling; + range.selectNode(b); + assert.equal(range.toString(), 'b'); + }); + }); diff --git a/test/js/Selection.js b/test/js/Selection.js new file mode 100644 index 0000000..151180e --- /dev/null +++ b/test/js/Selection.js @@ -0,0 +1,169 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Selection', function() { + + var wrap = ShadowDOMPolyfill.wrap; + var div, a, b, c; + + teardown(function() { + if (div && div.parentNode) + div.parentNode.removeChild(div); + div = a = b = c = undefined; + }); + + setup(function() { + div = document.createElement('div'); + div.innerHTML = 'abc'; + a = div.firstChild; + b = a.nextSibling; + c = div.lastChild; + document.body.appendChild(div); + }); + + + test('document.getSelection()', function() { + var selection = document.getSelection(); + assert.instanceOf(selection, Selection); + + var doc = wrap(document); + selection = doc.getSelection(); + assert.instanceOf(selection, Selection); + }); + + test('window.getSelection()', function() { + var selection = window.getSelection(); + assert.instanceOf(selection, Selection); + + var win = wrap(window); + selection = win.getSelection(); + assert.instanceOf(selection, Selection); + }); + + test('getSelection()', function() { + var selection = getSelection(); + assert.instanceOf(selection, Selection); + }); + + test('basics', function() { + var selection = window.getSelection(); + selection.selectAllChildren(div); + + assert.equal(selection.toString(), 'abc'); + + assert.isFalse(selection.isCollapsed); + assert.equal(selection.rangeCount, 1); + + // https://code.google.com/p/chromium/issues/detail?id=336821 + if (/WebKit/.test(navigator.userAgent)) + return; + + assert.equal(selection.anchorNode, div); + assert.equal(selection.anchorOffset, 0); + + assert.equal(selection.focusNode, div); + assert.equal(selection.focusOffset, 3); + }); + + test('getRangeAt', function() { + var selection = window.getSelection(); + selection.selectAllChildren(div); + var range = selection.getRangeAt(0); + assert.instanceOf(range, Range); + }); + + test('collapse', function() { + var selection = window.getSelection(); + + for (var i = 0; i < 4; i++) { + selection.selectAllChildren(div); + selection.collapse(div, i); + + assert.isTrue(selection.isCollapsed); + assert.equal(selection.toString(), ''); + + // https://code.google.com/p/chromium/issues/detail?id=336821 + if (/WebKit/.test(navigator.userAgent)) + continue; + + assert.equal(selection.anchorNode, div); + assert.equal(selection.anchorOffset, i); + + assert.equal(selection.focusNode, div); + assert.equal(selection.focusOffset, i); + } + }); + + test('extend', function() { + // IE does not have extend. + if (/Trident/.test(navigator.userAgent)) + return; + + var selection = window.getSelection(); + + for (var i = 0; i < 4; i++) { + selection.selectAllChildren(div); + selection.extend(div, i); + + assert.equal(selection.isCollapsed, i === 0); + assert.equal(selection.toString(), 'abc'.slice(0, i)); + + // https://code.google.com/p/chromium/issues/detail?id=336821 + if (/WebKit/.test(navigator.userAgent)) + continue; + + assert.equal(selection.anchorNode, div); + assert.equal(selection.anchorOffset, 0); + + assert.equal(selection.focusNode, div); + assert.equal(selection.focusOffset, i); + } + }); + + test('addRange', function() { + var selection = window.getSelection(); + var range = document.createRange(); + range.selectNode(b); + selection.addRange(range); + + // Uncertain why this fails in Blink. The same test passes without the + // shadow dom polyfill in Blink. + if (/WebKit/.test(navigator.userAgent)) + return; + + assert.equal(selection.toString(), 'b'); + }); + + test('removeRange', function() { + // Not implemented in Blink. + if (/WebKit/.test(navigator.userAgent)) + return; + + var selection = window.getSelection(); + selection.selectAllChildren(div); + var range = selection.getRangeAt(0); + selection.removeRange(range); + assert.equal(selection.toString(), ''); + }); + + test('containsNode', function() { + // IE does not have containsNode. + if (/Trident/.test(navigator.userAgent)) + return; + + var selection = window.getSelection(); + selection.selectAllChildren(div); + + assert.isFalse(selection.containsNode(div)); + assert.isFalse(selection.containsNode(document)); + assert.isFalse(selection.containsNode(document.body)); + + assert.isTrue(selection.containsNode(a, true)); + assert.isTrue(selection.containsNode(b, true)); + assert.isTrue(selection.containsNode(c, true)); + }); + +}); diff --git a/test/test.main.js b/test/test.main.js index 49b685f..393eaac 100644 --- a/test/test.main.js +++ b/test/test.main.js @@ -112,6 +112,7 @@ var modules = [ 'Range.js', 'SVGElement.js', 'SVGElementInstance.js', + 'Selection.js', 'ShadowRoot.js', 'Text.js', 'Window.js',