├── .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 |
Child
\ 41 |
'; 42 | 43 | var childHTML = '
\ 44 |
Child
\ 45 |
\ 46 |
Grandchild
\ 47 |
\ 48 |
'; 49 | 50 | var listHTML = '
\ 51 | \ 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 | --------------------------------------------------------------------------------