├── .gitignore ├── .travis.yml ├── FragmentAnchor.js ├── LICENSE ├── README.md ├── dist ├── FragmentAnchor.js ├── FragmentAnchor.min.js └── FragmentAnchor.min.js.map ├── gulpfile.js ├── karma.conf.js ├── package.json └── test ├── FragmentAnchor.spec.js └── fixtures └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | sudo: false 5 | before_script: 6 | - npm install -g karma-cli 7 | - npm install karma-coveralls 8 | notifications: 9 | email: false 10 | irc: 11 | channels: 12 | - chat.freenode.net#annotator 13 | on_success: change 14 | on_failure: change 15 | env: 16 | global: 17 | - secure: MKqJA6PJXOR+uofTSwi0085AMWoBnZen/Y0GNrvV1qkdB54HqhsDAq/iDzpxiGIOQ/lGIDo1B9TUCyMgI0nKKj/y+sGENg8tbIPdQTn5fFNuuvuNk49/Z6YoXrL5kHIsweVdhHUEqsIbPbSvXgPUSoK7S8AC6EmD/KeKsWjAKtao7E4XXUHyVCAsUzqz5cXMU4Aq74zGGd5hfm7PPTSxy5PWZD94FfxpD/4LELmHx5QSrWeG+4WQLFBXaVL3VZehOQcvbFzdqnk3nrkox7pFtf74UO18sD+7zN00xAm3OE7+6j5mg6d1lcfTysiCxiKLO3lo5rUmXesclI+lpZLRvMmMNJi8UerUvmFKsn1xbDS6Eqv+dzf/M1Am9hmuoPwpcFXudwwkN8M412cNN64OgUE5gG2VsSXr7/ijsElhxVLiRKV1LoGSTCNlROgAQAe0/L2IAt1gr2Kn4UQfs4mlT+hoAt6kkpKrrsEhPLfUjcgw5bsCqeaPQJXksgGqiTC1lcvdbCHEub/+ZG0b54kcTkX8/pliJwmofISJbKIycat/I9lXBDcpJ5i8aaKfvnHAQKG6PHylrYmQSJQBAifuKmLkwZStLzKNj9FmUUWf3UsIGeRNhPhjSfJx1LhIUs9J9MMIj7RPN+2P+tg2kbTxh90RG2q9OWmbpQwTXf3o2XQ= 18 | cache: 19 | directories: 20 | - node_modules 21 | -------------------------------------------------------------------------------- /FragmentAnchor.js: -------------------------------------------------------------------------------- 1 | export default class FragmentAnchor { 2 | constructor(root, id) { 3 | if (root === undefined) { 4 | throw new Error('missing required parameter "root"'); 5 | } 6 | if (id === undefined) { 7 | throw new Error('missing required parameter "id"'); 8 | } 9 | 10 | this.root = root; 11 | this.id = id; 12 | } 13 | 14 | static fromRange(root, range) { 15 | if (root === undefined) { 16 | throw new Error('missing required parameter "root"'); 17 | } 18 | if (range === undefined) { 19 | throw new Error('missing required parameter "range"'); 20 | } 21 | 22 | let el = range.commonAncestorContainer; 23 | while (el != null && !el.id) { 24 | if (root.compareDocumentPosition(el) & 25 | Node.DOCUMENT_POSITION_CONTAINED_BY) { 26 | el = el.parentElement; 27 | } else { 28 | throw new Error('no fragment identifier found'); 29 | } 30 | } 31 | 32 | return new FragmentAnchor(root, el.id); 33 | } 34 | 35 | static fromSelector(root, selector = {}) { 36 | return new FragmentAnchor(root, selector.value); 37 | } 38 | 39 | toRange() { 40 | let el = this.root.querySelector('#' + this.id); 41 | if (el == null) { 42 | throw new Error('no element found with id "' + this.id + '"'); 43 | } 44 | 45 | let range = this.root.ownerDocument.createRange(); 46 | range.selectNodeContents(el); 47 | 48 | return range; 49 | } 50 | 51 | toSelector() { 52 | let el = this.root.querySelector('#' + this.id); 53 | if (el == null) { 54 | throw new Error('no element found with id "' + this.id + '"'); 55 | } 56 | 57 | let conformsTo = 'https://tools.ietf.org/html/rfc3236'; 58 | if (el instanceof SVGElement) { 59 | conformsTo = 'http://www.w3.org/TR/SVG/'; 60 | } 61 | 62 | return { 63 | type: 'FragmentSelector', 64 | value: this.id, 65 | conformsTo: conformsTo, 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Randall Leeds 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fragment Anchor 2 | =============== 3 | 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) 5 | [![NPM Package](https://img.shields.io/npm/v/dom-anchor-fragment.svg)](https://www.npmjs.com/package/dom-anchor-fragment) 6 | [![Build Status](https://travis-ci.org/tilgovi/dom-anchor-fragment.svg?branch=master)](https://travis-ci.org/tilgovi/dom-anchor-fragment) 7 | [![Coverage Status](https://coveralls.io/repos/tilgovi/dom-anchor-fragment/badge.svg?branch=master)](https://coveralls.io/r/tilgovi/dom-anchor-fragment?branch=master) 8 | 9 | This library offers conversion between a DOM `Range` instance and a fragment 10 | selector as defined by the Web Annotation Data Model. 11 | 12 | For more information on `Range` see 13 | [the documentation](https://developer.mozilla.org/en-US/docs/Web/API/Range). 14 | 15 | For more information on the fragment selector see 16 | [the specification](http://www.w3.org/TR/annotation-model/#fragment-selector). 17 | 18 | Installation 19 | ============ 20 | 21 | There are a few different ways to include the library. 22 | 23 | With a CommonJS bundler, to `require('dom-anchor-fragment')`: 24 | 25 | npm install dom-anchor-fragment 26 | 27 | With a script tag, include one of the scripts from the `dist` directory. 28 | 29 | With AMD loaders, these scripts should also work. 30 | 31 | Usage 32 | ===== 33 | 34 | ## API Documentation 35 | 36 | The module exposes a single constructor function, `FragmentAnchor`. 37 | 38 | ### `new FragmentAnchor(root, id)` 39 | 40 | The `root` argument is required and sets the context for the selector. A 41 | fragment is valid if it refers to a node contained by the root. 42 | 43 | The `id` argument is required and sets the fragment identifier selected by this 44 | anchor. 45 | 46 | It is not necessary for any node to exist in the DOM with a matching `id` 47 | property. Only when this anchor is converted to a `Range` or a selector will 48 | the instance check the validity of the identifier. 49 | 50 | ### `FragmentAnchor.fromRange(root, range)` 51 | 52 | Provided with an existing `Range` instance this will return a `FragmentAnchor` 53 | instance that stores the `id` attribute of the common ancestor container. If 54 | the common ancestor container has no `id` attribute then the anchor will take 55 | the `id` of its first ancestor that does have a non-empty `id` attribute. 56 | 57 | If no element can be found in the ancestry of the `Range` that has a non-empty 58 | `id` attribute and is contained by the root then this function will raise an 59 | exception. 60 | 61 | ### `FragmentAnchor.fromSelector(root, selector)` 62 | 63 | Provided with root `Element` and an `Object` containing a `value` key that has 64 | a `String` value this will return a `FragmentAnchor` that refers to an 65 | `Element` with an `id` matching the value contained by the root. 66 | 67 | ### `FragmentAnchor.prototype.toRange()` 68 | 69 | This method returns a `Range` object that selects the contents of the element 70 | identified by the anchor. 71 | 72 | ### `FragmentAnchor.prototype.toSelector()` 73 | 74 | This method returns an `Object` that has keys `type` and `value` where `type` 75 | is `"FragmentSelector"` and the value is the `id` referred to be the anchor. 76 | -------------------------------------------------------------------------------- /dist/FragmentAnchor.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['exports', 'module'], factory); 4 | } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { 5 | factory(exports, module); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports, mod); 11 | global.FragmentAnchor = mod.exports; 12 | } 13 | })(this, function (exports, module) { 14 | 'use strict'; 15 | 16 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 17 | 18 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 19 | 20 | var FragmentAnchor = (function () { 21 | function FragmentAnchor(root, id) { 22 | _classCallCheck(this, FragmentAnchor); 23 | 24 | if (root === undefined) { 25 | throw new Error('missing required parameter "root"'); 26 | } 27 | if (id === undefined) { 28 | throw new Error('missing required parameter "id"'); 29 | } 30 | 31 | this.root = root; 32 | this.id = id; 33 | } 34 | 35 | _createClass(FragmentAnchor, [{ 36 | key: 'toRange', 37 | value: function toRange() { 38 | var el = this.root.querySelector('#' + this.id); 39 | if (el == null) { 40 | throw new Error('no element found with id "' + this.id + '"'); 41 | } 42 | 43 | var range = this.root.ownerDocument.createRange(); 44 | range.selectNodeContents(el); 45 | 46 | return range; 47 | } 48 | }, { 49 | key: 'toSelector', 50 | value: function toSelector() { 51 | var el = this.root.querySelector('#' + this.id); 52 | if (el == null) { 53 | throw new Error('no element found with id "' + this.id + '"'); 54 | } 55 | 56 | var conformsTo = 'https://tools.ietf.org/html/rfc3236'; 57 | if (el instanceof SVGElement) { 58 | conformsTo = 'http://www.w3.org/TR/SVG/'; 59 | } 60 | 61 | return { 62 | type: 'FragmentSelector', 63 | value: this.id, 64 | conformsTo: conformsTo 65 | }; 66 | } 67 | }], [{ 68 | key: 'fromRange', 69 | value: function fromRange(root, range) { 70 | if (root === undefined) { 71 | throw new Error('missing required parameter "root"'); 72 | } 73 | if (range === undefined) { 74 | throw new Error('missing required parameter "range"'); 75 | } 76 | 77 | var el = range.commonAncestorContainer; 78 | while (el != null && !el.id) { 79 | if (root.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_CONTAINED_BY) { 80 | el = el.parentElement; 81 | } else { 82 | throw new Error('no fragment identifier found'); 83 | } 84 | } 85 | 86 | return new FragmentAnchor(root, el.id); 87 | } 88 | }, { 89 | key: 'fromSelector', 90 | value: function fromSelector(root) { 91 | var selector = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 92 | 93 | return new FragmentAnchor(root, selector.value); 94 | } 95 | }]); 96 | 97 | return FragmentAnchor; 98 | })(); 99 | 100 | module.exports = FragmentAnchor; 101 | }); 102 | //# sourceMappingURL=data:application/json;base64,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Ozs7V0FwQ2tCLGNBQWM7OzttQkFBZCxjQUFjIiwiZmlsZSI6IkZyYWdtZW50QW5jaG9yLmpzIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgY2xhc3MgRnJhZ21lbnRBbmNob3Ige1xuICBjb25zdHJ1Y3Rvcihyb290LCBpZCkge1xuICAgIGlmIChyb290ID09PSB1bmRlZmluZWQpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignbWlzc2luZyByZXF1aXJlZCBwYXJhbWV0ZXIgXCJyb290XCInKTtcbiAgICB9XG4gICAgaWYgKGlkID09PSB1bmRlZmluZWQpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignbWlzc2luZyByZXF1aXJlZCBwYXJhbWV0ZXIgXCJpZFwiJyk7XG4gICAgfVxuXG4gICAgdGhpcy5yb290ID0gcm9vdDtcbiAgICB0aGlzLmlkID0gaWQ7XG4gIH1cblxuICBzdGF0aWMgZnJvbVJhbmdlKHJvb3QsIHJhbmdlKSB7XG4gICAgaWYgKHJvb3QgPT09IHVuZGVmaW5lZCkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdtaXNzaW5nIHJlcXVpcmVkIHBhcmFtZXRlciBcInJvb3RcIicpO1xuICAgIH1cbiAgICBpZiAocmFuZ2UgPT09IHVuZGVmaW5lZCkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdtaXNzaW5nIHJlcXVpcmVkIHBhcmFtZXRlciBcInJhbmdlXCInKTtcbiAgICB9XG5cbiAgICBsZXQgZWwgPSByYW5nZS5jb21tb25BbmNlc3RvckNvbnRhaW5lcjtcbiAgICB3aGlsZSAoZWwgIT0gbnVsbCAmJiAhZWwuaWQpIHtcbiAgICAgIGlmIChyb290LmNvbXBhcmVEb2N1bWVudFBvc2l0aW9uKGVsKSAmXG4gICAgICAgICAgTm9kZS5ET0NVTUVOVF9QT1NJVElPTl9DT05UQUlORURfQlkpIHtcbiAgICAgICAgZWwgPSBlbC5wYXJlbnRFbGVtZW50O1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdubyBmcmFnbWVudCBpZGVudGlmaWVyIGZvdW5kJyk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIG5ldyBGcmFnbWVudEFuY2hvcihyb290LCBlbC5pZCk7XG4gIH1cblxuICBzdGF0aWMgZnJvbVNlbGVjdG9yKHJvb3QsIHNlbGVjdG9yID0ge30pIHtcbiAgICByZXR1cm4gbmV3IEZyYWdtZW50QW5jaG9yKHJvb3QsIHNlbGVjdG9yLnZhbHVlKTtcbiAgfVxuXG4gIHRvUmFuZ2UoKSB7XG4gICAgbGV0IGVsID0gdGhpcy5yb290LnF1ZXJ5U2VsZWN0b3IoJyMnICsgdGhpcy5pZCk7XG4gICAgaWYgKGVsID09IG51bGwpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignbm8gZWxlbWVudCBmb3VuZCB3aXRoIGlkIFwiJyArIHRoaXMuaWQgKyAnXCInKTtcbiAgICB9XG5cbiAgICBsZXQgcmFuZ2UgPSB0aGlzLnJvb3Qub3duZXJEb2N1bWVudC5jcmVhdGVSYW5nZSgpO1xuICAgIHJhbmdlLnNlbGVjdE5vZGVDb250ZW50cyhlbCk7XG5cbiAgICByZXR1cm4gcmFuZ2U7XG4gIH1cblxuICB0b1NlbGVjdG9yKCkge1xuICAgIGxldCBlbCA9IHRoaXMucm9vdC5xdWVyeVNlbGVjdG9yKCcjJyArIHRoaXMuaWQpO1xuICAgIGlmIChlbCA9PSBudWxsKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ25vIGVsZW1lbnQgZm91bmQgd2l0aCBpZCBcIicgKyB0aGlzLmlkICsgJ1wiJyk7XG4gICAgfVxuXG4gICAgbGV0IGNvbmZvcm1zVG8gPSAnaHR0cHM6Ly90b29scy5pZXRmLm9yZy9odG1sL3JmYzMyMzYnO1xuICAgIGlmIChlbCBpbnN0YW5jZW9mIFNWR0VsZW1lbnQpIHtcbiAgICAgIGNvbmZvcm1zVG8gPSAnaHR0cDovL3d3dy53My5vcmcvVFIvU1ZHLyc7XG4gICAgfVxuXG4gICAgcmV0dXJuIHtcbiAgICAgIHR5cGU6ICdGcmFnbWVudFNlbGVjdG9yJyxcbiAgICAgIHZhbHVlOiB0aGlzLmlkLFxuICAgICAgY29uZm9ybXNUbzogY29uZm9ybXNUbyxcbiAgICB9O1xuICB9XG59XG4iXSwic291cmNlUm9vdCI6Ii4vIn0= -------------------------------------------------------------------------------- /dist/FragmentAnchor.min.js: -------------------------------------------------------------------------------- 1 | !function(e,r){if("function"==typeof define&&define.amd)define(["exports","module"],r);else if("undefined"!=typeof exports&&"undefined"!=typeof module)r(exports,module);else{var t={exports:{}};r(t.exports,t),e.FragmentAnchor=t.exports}}(this,function(e,r){"use strict";function t(e,r){if(!(e instanceof r))throw new TypeError("Cannot call a class as a function")}var n=function(){function e(e,r){for(var t=0;t", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/tilgovi/dom-anchor-fragment/issues" 23 | }, 24 | "homepage": "https://github.com/tilgovi/dom-anchor-fragment", 25 | "main": "dist/FragmentAnchor.js", 26 | "browser": "FragmentAnchor.js", 27 | "browserify": { 28 | "transform": [ 29 | "babelify" 30 | ] 31 | }, 32 | "dependencies": {}, 33 | "devDependencies": { 34 | "babel": "^5.4.3", 35 | "babelify": "^6.1.1", 36 | "browserify": "^10.2.4", 37 | "browserify-istanbul": "^0.2.1", 38 | "chai": "^2.3.0", 39 | "gulp": "^3.9.0", 40 | "gulp-babel": "^5.1.0", 41 | "gulp-rename": "^1.2.2", 42 | "gulp-sourcemaps": "^1.5.2", 43 | "gulp-uglify": "^1.2.0", 44 | "karma": "^0.12.32", 45 | "karma-browserify": "^4.2.1", 46 | "karma-chai": "^0.1.0", 47 | "karma-coverage": "^0.3.1", 48 | "karma-fixture": "^0.2.4", 49 | "karma-html2js-preprocessor": "^0.1.0", 50 | "karma-mocha": "^0.1.10", 51 | "karma-phantomjs-launcher": "^0.1.4", 52 | "karma-sauce-launcher": "^0.2.11", 53 | "karma-source-map-support": "^1.0.0", 54 | "mocha": "^2.2.5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/FragmentAnchor.spec.js: -------------------------------------------------------------------------------- 1 | import FragmentAnchor from '../FragmentAnchor'; 2 | 3 | describe('FragmentAnchor', () => { 4 | before(() => { 5 | fixture.setBase('test/fixtures'); 6 | }); 7 | 8 | beforeEach(() => { 9 | fixture.load('test.html'); 10 | }); 11 | 12 | afterEach(() => { 13 | fixture.cleanup(); 14 | }); 15 | 16 | describe('constructor', () => { 17 | it('is a function', () => { 18 | assert.isFunction(FragmentAnchor); 19 | }); 20 | 21 | it('requires root argument', () => { 22 | let construct = () => new FragmentAnchor(); 23 | assert.throws(construct,'required parameter'); 24 | }); 25 | 26 | it('requires an id argument', () => { 27 | let construct = () => new FragmentAnchor(fixture.el); 28 | assert.throws(construct,'required parameter'); 29 | }); 30 | 31 | it('constructs a new instance with the given root and id', () => { 32 | let root = fixture.el; 33 | let instance = new FragmentAnchor(root, 'foo'); 34 | assert.instanceOf(instance, FragmentAnchor); 35 | assert.equal(instance.root, fixture.el); 36 | assert.equal(instance.id, 'foo'); 37 | }); 38 | }); 39 | 40 | describe('fromRange', () => { 41 | it('requires a root argument', () => { 42 | let construct = () => FragmentAnchor.fromRange(); 43 | assert.throws(construct, 'required parameter'); 44 | }); 45 | 46 | it('requires a range argument', () => { 47 | let construct = () => FragmentAnchor.fromRange(document.body); 48 | assert.throws(construct, 'required parameter'); 49 | }); 50 | 51 | it('throws an error if no fragment identifier is found', () => { 52 | let root = fixture.el; 53 | let range = document.createRange(); 54 | range.selectNode(root); 55 | let attempt = () => FragmentAnchor.fromRange(root, range); 56 | assert.throws(attempt, 'no fragment'); 57 | }); 58 | 59 | it('returns a FragmentAnchor if the common ancestor has an id', () => { 60 | let root = fixture.el; 61 | let range = document.createRange(); 62 | range.selectNodeContents(root); 63 | let anchor = FragmentAnchor.fromRange(root, range); 64 | assert.equal(anchor.id, fixture.el.id); 65 | }); 66 | 67 | it('returns a FragmentAnchor if any ancestor has an id', () => { 68 | let root = fixture.el; 69 | let range = document.createRange(); 70 | range.selectNodeContents(fixture.el.children[0]); 71 | let anchor = FragmentAnchor.fromRange(root, range); 72 | assert.equal(anchor.id, fixture.el.id); 73 | }); 74 | }); 75 | 76 | describe('fromSelector', () => { 77 | it('requires a root argument', () => { 78 | let construct = () => FragmentAnchor.fromSelector(); 79 | assert.throws(construct, 'required parameter'); 80 | }); 81 | 82 | it('requires a selector argument', () => { 83 | let construct = () => FragmentAnchor.fromSelector(fixture.el); 84 | assert.throws(construct, 'required parameter'); 85 | }); 86 | 87 | it('returns a FragmentAnchor from the value of the selector', () => { 88 | let selector = { 89 | value: 'foo', 90 | }; 91 | let anchor = FragmentAnchor.fromSelector(fixture.el, selector); 92 | assert(anchor.root === fixture.el); 93 | assert(anchor.id === selector.value); 94 | }); 95 | }); 96 | 97 | describe('toRange', () => { 98 | it('returns a range selecting the contents of the Element', () => { 99 | let root = document.body; 100 | let anchor = new FragmentAnchor(root, fixture.el.id); 101 | let range = anchor.toRange(); 102 | assert.strictEqual(range.commonAncestorContainer, fixture.el); 103 | }); 104 | 105 | it('throws an error if no Element exists with the stored id', () => { 106 | let root = document.body; 107 | let anchor = new FragmentAnchor(root, 'bogus'); 108 | let attempt = () => anchor.toRange(); 109 | assert.throws(attempt, 'no element found'); 110 | }); 111 | }); 112 | 113 | describe('toSelector', () => { 114 | it('returns a selector for an HTMLElement', () => { 115 | let anchor = new FragmentAnchor(document.body, fixture.el.id); 116 | let selector = anchor.toSelector(); 117 | assert.equal(selector.type, 'FragmentSelector'); 118 | assert.equal(selector.value, fixture.el.id); 119 | assert.equal(selector.conformsTo, 'https://tools.ietf.org/html/rfc3236'); 120 | }); 121 | 122 | it('returns a selector for an SVGElement', () => { 123 | let svg = document.createElementNS( 124 | 'http://www.w3.org/2000/svg', 'svg'); 125 | let rect = document.createElementNS( 126 | 'http://www.w3.org/2000/svg', 'rect'); 127 | rect.id = 'rectangle1'; 128 | fixture.el.appendChild(svg); 129 | svg.appendChild(rect); 130 | let anchor = new FragmentAnchor(svg, rect.id); 131 | let selector = anchor.toSelector(); 132 | assert.equal(selector.type, 'FragmentSelector'); 133 | assert.equal(selector.value, rect.id); 134 | assert.equal(selector.conformsTo, 'http://www.w3.org/TR/SVG/'); 135 | }); 136 | 137 | it('throws an error if no Element exists with the stored id', () => { 138 | let anchor = new FragmentAnchor(fixture.el, 'bogus'); 139 | let attempt = () => anchor.toSelector(); 140 | assert.throws(attempt, 'no element found'); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /test/fixtures/test.html: -------------------------------------------------------------------------------- 1 |

Pellentesque habitant morbi tristique senectus et netus et 2 | malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, 3 | ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas 4 | semper. Aenean ultricies mi vitae est. Mauris placerat eleifend 5 | leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat 6 | wisi, condimentum sed, commodo vitae, ornare sit amet, 7 | wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum 8 | orci, sagittis tempus lacus enim ac dui. Donec non enim in 9 | turpis pulvinar facilisis. Ut felis.

10 | --------------------------------------------------------------------------------