├── .gitignore
├── LICENSE
├── README.md
├── dist
├── scopedQuerySelectorShim.js
└── scopedQuerySelectorShim.min.js
├── gruntfile.js
├── index.js
├── karma.conf.js
├── package.json
├── src
└── scopedQuerySelectorShim.js
└── test
└── testShim.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /misc
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Lawrence Davis
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 |
9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scopedQuerySelectorShim
2 | > querySelector/querySelectorAll shims that enable the use of :scope
3 |
4 | ## What is :scope in the context of querySelector?
5 |
6 | `:scope`, when combined with the immediate child selector `>`, lets you query for elements that are immediate children of a [HTMLElement] instance.
7 |
8 | For instance, you might want to find all list items of an unordered list that is an immediate child of `node`:
9 |
10 | ```javascript
11 | var listItems = node.querySelector(':scope > ul > li');
12 | ```
13 |
14 | This is effectively equivalent to using [jQuery's `find()`][jQuery.find]:
15 |
16 | ```javascript
17 | var $listItems = $(node).find('> ul > li');
18 | ```
19 |
20 | See the [Mozilla Developer Network article on :scope][:scope] for more information.
21 |
22 |
23 | ## Usage
24 |
25 | Simply include the JavaScript file:
26 |
27 | ```html
28 |
29 | ```
30 |
31 |
32 | ## Notes
33 |
34 | * Tests `:scope` support before inserting itself, and uses it if it's available
35 | * Falls back to an ID-based `querySelector` call against the the parent if not
36 | * Shimmed `querySelectorAll` returns a [NodeList], just like the native method
37 | * Can be called on an element that does not have an ID
38 | * Can be called on an element that is not currently in the DOM
39 | * Modifies `HTMLElement.prototype`
40 | * `Document.prototype`'s `querySelector`/`querySelectorAll` methods are not shimmed
41 | * `:scope` is not relevant at the document level
42 | * Use `document.documentElement.querySelector` instead without `:scope`
43 |
44 |
45 | ## Tests
46 |
47 | To run the tests:
48 |
49 | ```shell
50 | npm install
51 | grunt test
52 | ```
53 |
54 |
55 | ## License
56 |
57 | scopedQuerySelectorShim is licensed under the permissive BSD license.
58 |
59 |
60 | [:scope]: https://developer.mozilla.org/en-US/docs/Web/CSS/:scope
61 | [NodeList]: https://developer.mozilla.org/en-US/docs/Web/API/NodeList
62 | [HTMLElement]: http://mdn.io/HTMLElement
63 | [jQuery.find]: http://api.jquery.com/find/
64 |
--------------------------------------------------------------------------------
/dist/scopedQuerySelectorShim.js:
--------------------------------------------------------------------------------
1 | /* scopeQuerySelectorShim.js
2 | *
3 | * Copyright (C) 2015 Larry Davis
4 | * All rights reserved.
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the BSD license. See the LICENSE file for details.
8 | */
9 | (function() {
10 | if (!HTMLElement.prototype.querySelectorAll) {
11 | throw new Error("rootedQuerySelectorAll: This polyfill can only be used with browsers that support querySelectorAll");
12 | }
13 | // A temporary element to query against for elements not currently in the DOM
14 | // We'll also use this element to test for :scope support
15 | var container = document.createElement("div");
16 | // Check if the browser supports :scope
17 | try {
18 | // Browser supports :scope, do nothing
19 | container.querySelectorAll(":scope *");
20 | } catch (e) {
21 | // Match usage of scope
22 | var scopeRE = /^\s*:scope/gi;
23 | // Overrides
24 | function overrideNodeMethod(prototype, methodName) {
25 | // Store the old method for use later
26 | var oldMethod = prototype[methodName];
27 | // Override the method
28 | prototype[methodName] = function(query) {
29 | var nodeList, gaveId = false, gaveContainer = false;
30 | if (query.match(scopeRE)) {
31 | // Remove :scope
32 | query = query.replace(scopeRE, "");
33 | if (!this.parentNode) {
34 | // Add to temporary container
35 | container.appendChild(this);
36 | gaveContainer = true;
37 | }
38 | parentNode = this.parentNode;
39 | if (!this.id) {
40 | // Give temporary ID
41 | this.id = "rootedQuerySelector_id_" + new Date().getTime();
42 | gaveId = true;
43 | }
44 | // Find elements against parent node
45 | nodeList = oldMethod.call(parentNode, "#" + this.id + " " + query);
46 | // Reset the ID
47 | if (gaveId) {
48 | this.id = "";
49 | }
50 | // Remove from temporary container
51 | if (gaveContainer) {
52 | container.removeChild(this);
53 | }
54 | return nodeList;
55 | } else {
56 | // No immediate child selector used
57 | return oldMethod.call(this, query);
58 | }
59 | };
60 | }
61 | // Browser doesn't support :scope, add polyfill
62 | overrideNodeMethod(HTMLElement.prototype, "querySelector");
63 | overrideNodeMethod(HTMLElement.prototype, "querySelectorAll");
64 | }
65 | })();
--------------------------------------------------------------------------------
/dist/scopedQuerySelectorShim.min.js:
--------------------------------------------------------------------------------
1 | /* scopeQuerySelectorShim.js
2 | *
3 | * Copyright (C) 2015 Larry Davis
4 | * All rights reserved.
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the BSD license. See the LICENSE file for details.
8 | */
9 | !function(){function a(a,c){var e=a[c];a[c]=function(a){var c,f=!1,g=!1;return a.match(d)?(a=a.replace(d,""),this.parentNode||(b.appendChild(this),g=!0),parentNode=this.parentNode,this.id||(this.id="rootedQuerySelector_id_"+(new Date).getTime(),f=!0),c=e.call(parentNode,"#"+this.id+" "+a),f&&(this.id=""),g&&b.removeChild(this),c):e.call(this,a)}}if(!HTMLElement.prototype.querySelectorAll)throw new Error("rootedQuerySelectorAll: This polyfill can only be used with browsers that support querySelectorAll");var b=document.createElement("div");try{b.querySelectorAll(":scope *")}catch(c){var d=/^\s*:scope/gi;a(HTMLElement.prototype,"querySelector"),a(HTMLElement.prototype,"querySelectorAll")}}();
--------------------------------------------------------------------------------
/gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | grunt.loadNpmTasks('grunt-contrib-jshint');
3 | grunt.loadNpmTasks('grunt-contrib-watch');
4 | grunt.loadNpmTasks('grunt-contrib-uglify');
5 | grunt.loadNpmTasks('grunt-karma');
6 |
7 | grunt.initConfig({
8 | pkg: grunt.file.readJSON('package.json'),
9 | year: new Date().getFullYear(),
10 | jshint: {
11 | gruntfile: 'Gruntfile.js',
12 | tests: 'test/*.js',
13 | src: 'src/*.js',
14 | options: {
15 | multistr: true,
16 | globals: {
17 | eqeqeq: true
18 | }
19 | }
20 | },
21 | karma: {
22 | options: {
23 | configFile: 'karma.conf.js',
24 | reporters: ['progress']
25 | },
26 | // Watch configuration
27 | watch: {
28 | background: true
29 | },
30 | // Single-run configuration for development
31 | single: {
32 | singleRun: true
33 | }
34 | },
35 | uglify:{
36 | options:{
37 | banner:'/* scopeQuerySelectorShim.js' + '\n*' + '\n* Copyright (C) <%= year %> Larry Davis' + '\n* All rights reserved.' + '\n*' + '\n* This software may be modified and distributed under the terms' + '\n* of the BSD license. See the LICENSE file for details.' + '\n*/\n'
38 | },
39 | expanded:{
40 | options:{
41 | mangle:false,
42 | compress:false,
43 | beautify:true,
44 | preserveComments:true
45 | },
46 | files:{
47 | 'dist/scopedQuerySelectorShim.js':[ 'src/scopedQuerySelectorShim.js' ]
48 | }
49 | },
50 | minified:{
51 | files:{
52 | 'dist/scopedQuerySelectorShim.min.js':[ 'src/scopedQuerySelectorShim.js' ]
53 | }
54 | }
55 | },
56 | watchFiles: {
57 | gruntfile: {
58 | files: 'Gruntfile.js',
59 | tasks: 'jshint:gruntfile'
60 | },
61 | src: {
62 | files: [ 'src/**' ],
63 | tasks: [ 'jshint:src', 'uglify', 'karma:watch:run' ]
64 | },
65 | unitTests: {
66 | files: [ 'test/*.js' ],
67 | tasks: [ 'jshint:tests', 'karma:watch:run' ]
68 | }
69 | }
70 | });
71 |
72 | // Rename watch tasks
73 | grunt.renameTask('watch', 'watchFiles');
74 |
75 | // Setup watch task to include Karma
76 | grunt.registerTask('watch', [ 'karma:watch:start', 'watchFiles' ]);
77 |
78 | // Run tests once
79 | grunt.registerTask('test', [ 'jshint:tests', 'karma:single' ]);
80 |
81 | // Start watching and run tests when files change
82 | grunt.registerTask('default', [ 'jshint', 'test', 'watch' ]);
83 |
84 | };
85 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/scopedQuerySelectorShim.js');
2 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | config.set({
3 | // base path, that will be used to resolve files and exclude
4 | basePath: './',
5 |
6 | frameworks: ['mocha', 'chai'],
7 |
8 | // list of files / patterns to load in the browser
9 | files: [
10 | 'src/scopedQuerySelectorShim.js',
11 | 'test/**/*.js'
12 | ],
13 |
14 | // list of files to exclude
15 | exclude: [
16 | ],
17 |
18 | // use dots reporter, as travis terminal does not support escaping sequences
19 | // possible values: 'dots', 'progress'
20 | // CLI --reporters progress
21 | reporters: ['progress'],
22 |
23 | // web server port
24 | // CLI --port 9876
25 | port: 9876,
26 |
27 | // enable / disable colors in the output (reporters and logs)
28 | // CLI --colors --no-colors
29 | colors: true,
30 |
31 | // level of logging
32 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
33 | // CLI --log-level debug
34 | logLevel: config.LOG_INFO,
35 |
36 | // enable / disable watching file and executing tests whenever any file changes
37 | // CLI --auto-watch --no-auto-watch
38 | autoWatch: false,
39 |
40 | // Start these browsers, currently available:
41 | // - Chrome
42 | // - ChromeCanary
43 | // - Firefox
44 | // - Opera
45 | // - Safari (only Mac)
46 | // - PhantomJS
47 | // - IE (only Windows)
48 | // CLI --browsers Chrome,Firefox,Safari
49 | browsers: process.env.TRAVIS ? [ 'Firefox' ] : [
50 | 'Firefox',
51 | 'Chrome'
52 | ],
53 |
54 | // If browser does not capture in given timeout [ms], kill it
55 | // CLI --capture-timeout 5000
56 | captureTimeout: 20000,
57 |
58 | // Auto run tests on start (when browsers are captured) and exit
59 | // CLI --single-run --no-single-run
60 | singleRun: false,
61 |
62 | // report which specs are slower than 500ms
63 | // CLI --report-slower-than 500
64 | reportSlowerThan: 500,
65 |
66 | plugins: [
67 | 'karma-chai',
68 | 'karma-mocha',
69 | 'karma-chrome-launcher',
70 | 'karma-firefox-launcher'
71 | ]
72 | });
73 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scopedQuerySelectorShim",
3 | "version": "0.0.0",
4 | "author": "Larry Davis ",
5 | "description": "querySelector/querySelectorAll shims that enable the use of :scope",
6 | "license": "Public Domain",
7 | "scripts": {
8 | "test": "grunt test"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git@github.com:lazd/scopedQuerySelectorShim.git"
13 | },
14 | "devDependencies": {
15 | "grunt": "~0.4.1",
16 | "grunt-contrib-watch": "~0.5.3",
17 | "grunt-contrib-jshint": "~0.7.2",
18 | "grunt-contrib-uglify": "~0.9.1",
19 | "grunt-karma": "~0.6.2",
20 | "karma": "~0.10.5",
21 | "karma-firefox-launcher": "~0.1.0",
22 | "karma-chrome-launcher": "~0.1.0",
23 | "karma-mocha": "~0.1",
24 | "karma-chai": "0.0.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/scopedQuerySelectorShim.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | if (!HTMLElement.prototype.querySelectorAll) {
3 | throw new Error('rootedQuerySelectorAll: This polyfill can only be used with browsers that support querySelectorAll');
4 | }
5 |
6 | // A temporary element to query against for elements not currently in the DOM
7 | // We'll also use this element to test for :scope support
8 | var container = document.createElement('div');
9 |
10 | // Check if the browser supports :scope
11 | try {
12 | // Browser supports :scope, do nothing
13 | container.querySelectorAll(':scope *');
14 | }
15 | catch (e) {
16 | // Match usage of scope
17 | var scopeRE = /^\s*:scope/gi;
18 |
19 | // Overrides
20 | function overrideNodeMethod(prototype, methodName) {
21 | // Store the old method for use later
22 | var oldMethod = prototype[methodName];
23 |
24 | // Override the method
25 | prototype[methodName] = function(query) {
26 | var nodeList,
27 | gaveId = false,
28 | gaveContainer = false;
29 |
30 | if (query.match(scopeRE)) {
31 | // Remove :scope
32 | query = query.replace(scopeRE, '');
33 |
34 | if (!this.parentNode) {
35 | // Add to temporary container
36 | container.appendChild(this);
37 | gaveContainer = true;
38 | }
39 |
40 | var parentNode = this.parentNode;
41 |
42 | if (!this.id) {
43 | // Give temporary ID
44 | this.id = 'rootedQuerySelector_id_'+(new Date()).getTime();
45 | gaveId = true;
46 | }
47 |
48 | // Find elements against parent node
49 | nodeList = oldMethod.call(parentNode, '#'+this.id+' '+query);
50 |
51 | // Reset the ID
52 | if (gaveId) {
53 | this.id = '';
54 | }
55 |
56 | // Remove from temporary container
57 | if (gaveContainer) {
58 | container.removeChild(this);
59 | }
60 |
61 | return nodeList;
62 | }
63 | else {
64 | // No immediate child selector used
65 | return oldMethod.call(this, query);
66 | }
67 | };
68 | }
69 |
70 | // Browser doesn't support :scope, add polyfill
71 | overrideNodeMethod(HTMLElement.prototype, 'querySelector');
72 | overrideNodeMethod(HTMLElement.prototype, 'querySelectorAll');
73 | }
74 | }());
75 |
--------------------------------------------------------------------------------
/test/testShim.js:
--------------------------------------------------------------------------------
1 | /* jshint -W030 */
2 | describe('scopedQuerySelectorShim', function() {
3 | function makeNode(html) {
4 | var div = document.createElement('div');
5 | div.innerHTML = html;
6 | var node = div.children[0];
7 | div.removeChild(node);
8 | return node;
9 | }
10 |
11 | function makeNodeAndAddToDOM(html) {
12 | var node = makeNode(html);
13 | document.body.appendChild(node);
14 | return node;
15 | }
16 |
17 | function testChildNode(node) {
18 | expect(node.innerHTML).to.equal('Child');
19 | }
20 |
21 | function testChildNodeList(nodeList) {
22 | expect(nodeList.length).to.equal(1);
23 | testChildNode(nodeList[0]);
24 | }
25 |
26 | function testGrandChildNode(node) {
27 | expect(node.innerHTML).to.equal('Grandchild 1');
28 | }
29 |
30 | function testGrandChildNode1(node) {
31 | expect(node.innerHTML).to.equal('Grandchild 2');
32 | }
33 |
34 | function testGrandChildNodeList(nodeList) {
35 | testGrandChildNode(nodeList[0]);
36 | testGrandChildNode1(nodeList[1]);
37 | }
38 |
39 | var idHTML = '\
40 | \
41 |
';
42 |
43 | var childHTML = '\
44 |
\
45 |
\
46 | \
47 |
\
48 |
';
49 |
50 | var listHTML = '\
51 |
\
52 | - Grandchild 1
\
53 | - Grandchild 2
\
54 |
\
55 |
';
56 |
57 | describe('when nodes are in the DOM', function() {
58 | it('should find child nodes', function() {
59 | testChildNode(makeNodeAndAddToDOM(childHTML).querySelector(':scope > header'));
60 | testChildNodeList(makeNodeAndAddToDOM(childHTML).querySelectorAll(':scope > header'));
61 | });
62 |
63 | it('should find grandchild nodes', function() {
64 | testGrandChildNode(makeNodeAndAddToDOM(listHTML).querySelector(':scope > ul > li'));
65 | testGrandChildNodeList(makeNodeAndAddToDOM(listHTML).querySelectorAll(':scope > ul > li'));
66 | });
67 | });
68 |
69 | describe('when nodes are not in the DOM', function() {
70 | it('should find child nodes', function() {
71 | testChildNode(makeNode(childHTML).querySelector(':scope > header'));
72 | testChildNodeList(makeNode(childHTML).querySelectorAll(':scope > header'));
73 | });
74 |
75 | it('should find grandchild nodes', function() {
76 | testGrandChildNode(makeNode(listHTML).querySelector(':scope > ul > li'));
77 | testGrandChildNodeList(makeNode(listHTML).querySelectorAll(':scope > ul > li'));
78 | });
79 | });
80 |
81 | describe('when temporary containers and IDs are used', function() {
82 | it('should not leave nodes in temporary container', function() {
83 | var node = makeNode(childHTML);
84 | expect(node.parentNode).to.be.null;
85 | node.querySelectorAll(':scope > header');
86 | expect(node.parentNode).to.be.null;
87 | });
88 |
89 | it('should not leave temporary IDs', function() {
90 | var node = makeNode(childHTML);
91 | expect(node.id).to.equal('');
92 | node.querySelectorAll(':scope > header');
93 | expect(node.id).to.equal('');
94 | });
95 | });
96 |
97 | describe('when nodes have IDs', function() {
98 | it('should not overwrite existing IDs', function() {
99 | var node = makeNode(idHTML);
100 | expect(node.id).to.equal('myDiv');
101 | node.querySelectorAll(':scope > header');
102 | expect(node.id).to.equal('myDiv');
103 | });
104 | });
105 | });
106 |
--------------------------------------------------------------------------------