diff --git a/package.json b/package.json index 3b5df96f..b05a6313 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "devDependencies": { "@types/glob": "^7.1.1", "@types/qunit": "^2.5.4", + "bowser": "^2.3.0", "glob": "^7.1.3", "log4javascript": "^1.4.15", "npm-run-all": "^4.1.5", diff --git a/src/core/api.ts b/src/core/api.ts index bffb5228..fe97f2f7 100644 --- a/src/core/api.ts +++ b/src/core/api.ts @@ -16,13 +16,16 @@ export interface Features { * Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried * rangy2 don't support IE < 9. I have tested in browserstack.com with updated IE => not crash */ crashyTextNodes: false; - /** Always use window.getSelection */ + /** @deprecated Always use window.getSelection */ implementsWinGetSelection: true; - /** document.selection should only be used for IE < 9 which rangy2 don't support */ + /** @deprecated document.selection should only be used for IE < 9 which rangy2 don't support */ implementsDocSelection: false; - selectionHasAnchorAndFocus?: boolean; - selectionHasExtend?: boolean; - selectionHasRangeCount?: boolean; + /** @deprecated Always true */ + selectionHasAnchorAndFocus: true; + /** @deprecated Always true */ + selectionHasExtend: true; + /** @deprecated Always true */ + selectionHasRangeCount: true; selectionSupportsMultipleRanges: boolean; implementsControlRange: false; collapsedNonEditableSelectionsSupported: boolean; @@ -34,6 +37,9 @@ export const features: Features = { crashyTextNodes: false, implementsWinGetSelection: true, implementsDocSelection: false, + selectionHasAnchorAndFocus: true, + selectionHasExtend: true, + selectionHasRangeCount: true, selectionSupportsMultipleRanges: window.navigator.userAgent.indexOf("Firefox") > -1, implementsControlRange: false, collapsedNonEditableSelectionsSupported: true, diff --git a/src/core/internal/wrappedselection.ts b/src/core/internal/wrappedselection.ts index d26a8864..9bf0f75e 100644 --- a/src/core/internal/wrappedselection.ts +++ b/src/core/internal/wrappedselection.ts @@ -72,39 +72,19 @@ export function isSelectionValid() { const testRange = createNativeRange(document); // Obtaining a range from a selection -const selectionHasAnchorAndFocus = - features.selectionHasAnchorAndFocus = - util.areHostProperties(testSelection, ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]); - - // Test for existence of native selection extend() method -const selectionHasExtend = - features.selectionHasExtend = - isHostMethod(testSelection, "extend"); - - // Test if rangeCount exists -const selectionHasRangeCount = - features.selectionHasRangeCount = - (typeof testSelection.rangeCount == NUMBER); - -const addRangeBackwardToNative = selectionHasExtend - ? function(nativeSelection: Selection, range: AbstractRange) { + +function addRangeBackwardToNative(nativeSelection: Selection, range: AbstractRange) { var doc = DomRange.getRangeDocument(range); var endRange = createRange(doc); endRange.collapseToPoint(range.endContainer, range.endOffset); nativeSelection.addRange(getNativeRange(endRange)); nativeSelection.extend(range.startContainer, range.startOffset); } - : null; // Selection collapsedness -let selectionIsCollapsed = - selectionHasAnchorAndFocus - ? function(sel) { +function selectionIsCollapsed(sel) { return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset; } - : function(sel) { - return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false; - }; function updateAnchorAndFocusFromRange(sel: WrappedSelection, range, backward) { const anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end"; @@ -145,21 +125,20 @@ let selectionIsCollapsed = return nativeRange; } - let getSelectionRangeAt: undefined | ((sel: Selection|RangySel, index: number) => RangyRange | Range | null); + const getSelectionRangeAt: ((sel: Selection|RangySel, index: number) => RangyRange | Range | null) = - if (isHostMethod(testSelection, "getRangeAt")) { + isHostMethod(testSelection, "getRangeAt") // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation. // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a // lesson to us all, especially me. - getSelectionRangeAt = function(sel, index) { + ? function(sel, index) { try { return sel.getRangeAt(index); } catch (ex) { return null; } - }; - } else if (selectionHasAnchorAndFocus) { - getSelectionRangeAt = function(sel, index) { + } + : function(sel, index) { var doc = getDocument(sel.anchorNode); var range = createRange(doc); range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset); @@ -172,7 +151,6 @@ let selectionIsCollapsed = return range; }; - } function deleteProperties(sel: WrappedSelection) { sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null; @@ -247,7 +225,7 @@ let selectionIsCollapsed = updateEmptySelection(sel); } }; - } else if (selectionHasAnchorAndFocus && + } else if ( typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN) { @@ -269,62 +247,6 @@ let selectionIsCollapsed = } // function createWrappedSelection>(Base: TBase) { - function addRangeBackward(sel: WrappedSelection, range: AbstractRange) { - addRangeBackwardToNative(sel.nativeSelection, range); - sel.refresh(); - }; - const addRange = selectionHasRangeCount - ? function(this: WrappedSelection, range: RangyRangeEx, direction?: string|boolean): void { - if (isDirectionBackward(direction) && selectionHasExtend) { - addRangeBackward(this, range); - } else { - const previousRangeCount = this.rangeCount; - if (! features.selectionSupportsMultipleRanges && previousRangeCount > 0) { - // https://www.chromestatus.com/features/6680566019653632 - return; - } - // Clone the native range so that changing the selected range does not affect the selection. - // This is contrary to the spec but is the only way to achieve consistency between browsers. See - // issue 80. - var clonedNativeRange = getNativeRange(range).cloneRange(); - try { - this.nativeSelection.addRange(clonedNativeRange); - } catch (ex) { - log.error("Native addRange threw error '" + ex + "' with range " + DomRange.inspect(clonedNativeRange), ex); - } - - // Check whether adding the range was successful - this.rangeCount = this.nativeSelection.rangeCount; - - if (this.rangeCount == previousRangeCount + 1) { - // The range was added successfully - - // Check whether the range that we added to the selection is reflected in the last range extracted from - // the selection - if (config.checkSelectionRanges) { - var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1) as Range; - if (nativeRange && !rangesEqual(nativeRange, range)) { - // Happens in WebKit with, for example, a selection placed at the start of a text node - range = new WrappedRange(nativeRange); - } - } - this._ranges[this.rangeCount - 1] = range; - updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection)); - this.isCollapsed = selectionIsCollapsed(this); - } else { - // The range was not added successfully. The simplest thing is to refresh - this.refresh(); - } - } - } - : function(this: WrappedSelection, range: RangyRangeEx, direction?: string|boolean): void { - if (isDirectionBackward(direction) && selectionHasExtend) { - addRangeBackward(this, range); - } else { - this.nativeSelection.addRange(getNativeRange(range)); - this.refresh(); - } - }; // Removal of a single range function removeRangeManually(sel: WrappedSelection, range) { @@ -409,7 +331,50 @@ let selectionIsCollapsed = empty = this.removeAllRanges; //if (selectionHasRangeCount) { - addRange = addRange.bind(this); + addRange(range: RangyRangeEx, direction?: string|boolean): void { + if (isDirectionBackward(direction)) { + addRangeBackwardToNative(this.nativeSelection, range); + this.refresh(); + } else { + const previousRangeCount = this.rangeCount; + if (! features.selectionSupportsMultipleRanges && previousRangeCount > 0) { + // https://www.chromestatus.com/features/6680566019653632 + return; + } + // Clone the native range so that changing the selected range does not affect the selection. + // This is contrary to the spec but is the only way to achieve consistency between browsers. See + // issue 80. + var clonedNativeRange = getNativeRange(range).cloneRange(); + try { + this.nativeSelection.addRange(clonedNativeRange); + } catch (ex) { + log.error("Native addRange threw error '" + ex + "' with range " + DomRange.inspect(clonedNativeRange), ex); + } + + // Check whether adding the range was successful + this.rangeCount = this.nativeSelection.rangeCount; + + if (this.rangeCount == previousRangeCount + 1) { + // The range was added successfully + + // Check whether the range that we added to the selection is reflected in the last range extracted from + // the selection + if (config.checkSelectionRanges) { + var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1) as Range; + if (nativeRange && !rangesEqual(nativeRange, range)) { + // Happens in WebKit with, for example, a selection placed at the start of a text node + range = new WrappedRange(nativeRange); + } + } + this._ranges[this.rangeCount - 1] = range; + updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection)); + this.isCollapsed = selectionIsCollapsed(this); + } else { + // The range was not added successfully. The simplest thing is to refresh + this.refresh(); + } + } + } setRanges(ranges: WrappedRange[]): void { this.removeAllRanges(); diff --git a/test/core/feature.test.html b/test/core/feature.test.html index baeaabe8..e72fb27f 100644 --- a/test/core/feature.test.html +++ b/test/core/feature.test.html @@ -1,36 +1,20 @@ - - Rangy - TextRange-to-Range Performace Tests + + + + + Rangy - Features Tests - - - - - - - - - - - - - - - - - -
-
- + + + + + + + +
+
+ + diff --git a/test/core/feature.test.ts b/test/core/feature.test.ts index 369ba8df..5ba69bee 100644 --- a/test/core/feature.test.ts +++ b/test/core/feature.test.ts @@ -1,53 +1,52 @@ -xn.test.suite("Browser feature tests", function(s) { - rangy.init(); +import Bowser from "bowser"; + +const browser = Bowser.getParser(window.navigator.userAgent); +QUnit.module("Browser feature tests"); // Detect browser version roughly. It doesn't matter too much: these are only rough tests designed to test whether // Rangy's feature detection is hopelessly wrong + const isIe = browser.satisfies({ie: '>0'}); + const isMozilla = browser.isEngine('gecko'); + const isOpera = browser.isEngine('presto'); - var browser = jQuery.browser; - var isIe = !!browser.msie; - var isMozilla = !!browser.mozilla; - var isOpera = !!browser.opera; - var version = parseFloat(browser.version); - - s.test("DOM Range support", function(t) { - t.assertEquals(rangy.features.implementsDomRange, !isIe || version >= 9); + QUnit.test("DOM Range support", function(t) { + t.equal(rangy.features.implementsDomRange, !browser.satisfies({ie: '<9'})); }); - s.test("TextRange support", function(t) { - t.assertEquals(rangy.features.implementsTextRange, isIe && version >= 4); + QUnit.test("TextRange support", function(t) { + t.equal(false, !!undefined); + t.equal(rangy.features.implementsTextRange, !!browser.satisfies({ie: '>=4'})); }); - s.test("document.selection support", function(t) { - t.assertEquals(rangy.features.implementsTextRange, isIe && version >= 4); + QUnit.test("document.selection support", function(t) { + t.equal(rangy.features.implementsTextRange, !!browser.satisfies({ie: '>=4'})); }); - s.test("window.getSelection() support", function(t) { - t.assertEquals(rangy.features.implementsWinGetSelection, !isIe || version >= 9); + QUnit.test("window.getSelection() support", function(t) { + t.equal(rangy.features.implementsWinGetSelection, !browser.satisfies({ie: '<9'})); }); - s.test("selection has rangeCount", function(t) { - t.assertEquals(rangy.features.selectionHasRangeCount, !isIe || version >= 9); + QUnit.test("selection has rangeCount", function(t) { + t.equal(rangy.features.selectionHasRangeCount, !browser.satisfies({ie: '<9'})); }); - s.test("selection has anchor and focus support", function(t) { - t.assertEquals(rangy.features.selectionHasAnchorAndFocus, !isIe || version >= 9); + QUnit.test("selection has anchor and focus support", function(t) { + t.equal(rangy.features.selectionHasAnchorAndFocus, !browser.satisfies({ie: '<9'})); }); - s.test("selection has extend() method", function(t) { - t.assertEquals(rangy.features.selectionHasExtend, !isIe); + QUnit.test("selection has extend() method", function(t) { + t.equal(rangy.features.selectionHasExtend, !isIe); }); - s.test("HTML parsing", function(t) { - t.assertEquals(rangy.features.htmlParsingConforms, !isIe); + QUnit.test("HTML parsing", function(t) { + t.equal(rangy.features.htmlParsingConforms, !isIe); }); - s.test("Multiple ranges per selection support", function(t) { - t.assertEquals(rangy.features.selectionSupportsMultipleRanges, isMozilla); + QUnit.test("Multiple ranges per selection support", function(t) { + t.equal(rangy.features.selectionSupportsMultipleRanges, isMozilla); }); - s.test("Collapsed non-editable selections support", function(t) { - t.assertEquals(rangy.features.collapsedNonEditableSelectionsSupported, !isOpera); + QUnit.test("Collapsed non-editable selections support", function(t) { + t.equal(rangy.features.collapsedNonEditableSelectionsSupported, !isOpera); }); -}, false); diff --git a/test/core/selection.test.ts b/test/core/selection.test.ts index c8a24131..abb896c4 100644 --- a/test/core/selection.test.ts +++ b/test/core/selection.test.ts @@ -1,4 +1,4 @@ -import {WrappedSelection} from "rangy2"; +import {RangyRangeEx, WrappedSelection} from "rangy2"; var hasNativeGetSelection = "getSelection" in window; var hasNativeDomRange = "createRange" in document; @@ -488,7 +488,7 @@ function testSelectionAndRangeCreators(wins, winName, }, setUp_noRangeCheck, tearDown_noRangeCheck); } - function testRefresh(name: string, testRangeCreator: (t: Assert) => AbstractRange) { + function testRefresh(name: string, testRangeCreator: (t: Assert) => RangyRangeEx) { if (isWrappedSel(selectionCreator, 'refresh')) { QUnit.test("Refresh test: " + name, function(t) { const sel = selectionCreator(win); diff --git a/test/tsconfig.json b/test/tsconfig.json index 2f1db626..e059c8e6 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "esnext", "module": "esnext", + "esModuleInterop": true, "moduleResolution": "node", "sourceMap": true, "stripInternal": true, diff --git a/test/typings.d.ts b/test/typings.d.ts index 34871dbb..98e46cd1 100644 --- a/test/typings.d.ts +++ b/test/typings.d.ts @@ -18,3 +18,11 @@ declare global { tearDown: (assert: Assert) => void): void; } } + +declare module "bowser" { + namespace Parser { + interface Parser { + isEngine(engineName: string): boolean; + } + } +} diff --git a/yarn.lock b/yarn.lock index 7a6e7239..b05599e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -141,6 +141,11 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +bowser@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.3.0.tgz#624798388d972ec9639eb877629bca8ffd415c60" + integrity sha512-qaw3ZkREeT6HfHxr7HAY/xUdJHiopfktBUrctrlaLImZZYW82mLvHd04Fg3Zp/mnbt+YrMfX7PImZIL+65h7FQ== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"