├── .gitignore ├── .travis.yml ├── test-main.js ├── package.json ├── README.md ├── karma.conf.js ├── LICENSE ├── test └── CSSselectorSimple.spec.js └── lib └── CssSelector.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start 7 | 8 | -------------------------------------------------------------------------------- /test-main.js: -------------------------------------------------------------------------------- 1 | var allTestFiles = []; 2 | var TEST_REGEXP = /(spec|test)\.js$/i; 3 | 4 | var pathToModule = function(path) { 5 | return path.replace(/^\/base\//, '').replace(/\.js$/, ''); 6 | }; 7 | 8 | Object.keys(window.__karma__.files).forEach(function(file) { 9 | if (TEST_REGEXP.test(file)) { 10 | // Normalize paths to RequireJS module names. 11 | allTestFiles.push(pathToModule(file)); 12 | } 13 | }); 14 | 15 | require.config({ 16 | // Karma serves files under /base, which is the basePath from your config file 17 | baseUrl: '/base', 18 | 19 | // dynamically load all test files 20 | deps: allTestFiles, 21 | 22 | // we have to kickoff jasmine, as it is asynchronous 23 | callback: window.__karma__.start 24 | }); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-selector", 3 | "version": "v0.1.0", 4 | "description": "Retrieves CSS selector for a given element in DOM.", 5 | "homepage": "https://github.com/martinsbalodis/css-selector", 6 | "license": "LGPL-3.0+", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/martinsbalodis/css-selector" 10 | }, 11 | "author": { 12 | "name": "Martins Balodis", 13 | "email": "martins256@gmail.com" 14 | }, 15 | "scripts": { 16 | "test": "./node_modules/karma/bin/karma start --browsers Firefox --single-run" 17 | }, 18 | "dependencies": { 19 | "jquery": "~2.1.1" 20 | }, 21 | "devDependencies": { 22 | "karma-chrome-launcher": "^0.1.4", 23 | "karma-firefox-launcher": "~0.1", 24 | "karma-jasmine": "^0.1.5", 25 | "karma-requirejs": "^0.2.2", 26 | "requirejs": "^2.1.14" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Selector 2 | [![Build Status](https://api.travis-ci.org/martinsbalodis/css-selector.svg)](https://travis-ci.org/martinsbalodis/css-selector) 3 | 4 | CSS selector can be used to retrieve CSS selector for a given element in DOM. The resulting selector will be optimized to be as short as possible. 5 | CSS selector can be retrieved also for multiple elements. In such case the resulting selector might be a much wider CSS selector which will point to similar elements. 6 | 7 | ## Usage 8 | ```javascript 9 | var selector = new CssSelector({ 10 | parent: document, 11 | enableResultStripping: true, 12 | ignoredTags: ['font'], 13 | enableSmartTableSelector: true, 14 | allowMultipleSelectors: false, 15 | query: jQuery, 16 | ignoredClasses: [ 17 | 'my-class' 18 | ] 19 | }); 20 | var elements = document.getElementsByClassName('my-class'); 21 | var result_selector = selector.getCssSelector(elements); 22 | // #id div:nth-of-type(1) .another-class 23 | ``` 24 | 25 | ## Features 26 | 27 | - Tag name selector 28 | - Id selector 29 | - Class name selector 30 | - nth-of-child selector 31 | - Direct Child selector (a > b) 32 | - Smart table selector 33 | 34 | ### Smart table selector 35 | For example you have a table like you can see below and you need to get the CSS selector for `banana`. The selector could be retrieved with `nth-of-child` selector. But in this case the resulting selector wouldn't be a very strong one. Using smart table you would get CSS selector like this `tr:contains('title:') td:nth-of-type(2)`. 36 | ```html 37 | 38 | 39 | 40 |
title:banana
color:yellow
41 | ``` 42 | 43 | ## Contributions 44 | Please include tests for added features. 45 | 46 | ## License 47 | LGPLv3 48 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Jun 19 2014 20:42:33 GMT+0300 (EEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine', 'requirejs'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test-main.js', 19 | {pattern: 'node_modules/jquery/dist/jquery.js', included:true}, 20 | {pattern: 'lib/CssSelector.js', included:true}, 21 | {pattern: 'test/*spec.js', included: false} 22 | ], 23 | 24 | 25 | // list of files to exclude 26 | exclude: [ 27 | 28 | ], 29 | 30 | 31 | // preprocess matching files before serving them to the browser 32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 33 | preprocessors: { 34 | 35 | }, 36 | 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 41 | reporters: ['dots'], 42 | 43 | 44 | // web server port 45 | port: 9876, 46 | 47 | 48 | // enable / disable colors in the output (reporters and logs) 49 | colors: true, 50 | 51 | 52 | // level of logging 53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 54 | logLevel: config.LOG_WARN, 55 | 56 | 57 | // enable / disable watching file and executing test whenever any file changes 58 | autoWatch: false, 59 | 60 | 61 | // start these browsers 62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 63 | browsers: ['Chrome'], 64 | 65 | 66 | // Continuous Integration mode 67 | // if true, Karma captures browsers, runs the test and exits 68 | singleRun: false 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /test/CSSselectorSimple.spec.js: -------------------------------------------------------------------------------- 1 | var selector, $el; 2 | jQuery("body").append("
"); 3 | 4 | describe("CSS Selector Simple", function () { 5 | 6 | beforeEach(function () { 7 | 8 | $el = jQuery("#tests").html(""); 9 | selector = new CssSelector({ 10 | parent: jQuery('#tests')[0], 11 | enableResultStripping: false, 12 | ignoredClasses: [ 13 | 'test-ignore-tags', 14 | 'test-multi-element-n', 15 | 'test-skip-top-n' 16 | ] 17 | }); 18 | }); 19 | 20 | it("should be able to select one element", function () { 21 | 22 | $el.append('
'); 23 | 24 | var element = document.getElementsByName('test-simple-element')[0]; 25 | var css_selector = selector.getCssSelector([element]); 26 | 27 | expect(css_selector).toBe("div:nth-of-type(1) > a:nth-of-type(1)"); 28 | }); 29 | 30 | it("should be able to select multiple elements", function () { 31 | 32 | $el.append('
'); 33 | 34 | var elements = document.getElementsByName('test-milti-element'); 35 | var css_selector = selector.getCssSelector(elements); 36 | 37 | expect(css_selector).toBe("div:nth-of-type(1) > span > a:nth-of-type(1)"); 38 | }); 39 | 40 | it("should be able to select multiple elements n+", function () { 41 | 42 | $el.append('
'); 43 | 44 | var elements = jQuery('.test-multi-element-n'); 45 | var css_selector = selector.getCssSelector(elements); 46 | 47 | 48 | expect(css_selector).toBe("div:nth-of-type(1) > span:nth-of-type(n+2) > a:nth-of-type(1)"); 49 | }); 50 | 51 | it("should be able to ignore tags", function () { 52 | 53 | $el.append('
'); 54 | var elements = jQuery('.test-ignore-tags'); 55 | var css_selector = selector.getCssSelector(elements); 56 | 57 | expect(css_selector).toBe("table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1)"); 58 | }); 59 | 60 | it("should escape colon in tag name, id and class", function () { 61 | 62 | $el.append(''); 63 | var elements = jQuery('.colon-test'); 64 | var cssSelector = selector.getCssSelector(elements); 65 | 66 | expect(cssSelector).toBe("div\\:test#colon\\:testid.colon-test.colon\\:class:nth-of-type(1)"); 67 | expect($(cssSelector).length).toBe(1); 68 | }); 69 | 70 | it("should be able to skip elements from top", function () { 71 | 72 | $el.append('
'); 73 | 74 | var elements = jQuery('.test-skip-top'); 75 | var css_selector = selector.getCssSelector(elements, 2); 76 | 77 | expect(css_selector).toBe("table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1)"); 78 | }); 79 | 80 | it("should be able to skip elements from top and use n+1 selectors", function () { 81 | 82 | $el.append('
'); 83 | 84 | var elements = jQuery('.test-skip-top-n'); 85 | var css_selector = selector.getCssSelector(elements, 2); 86 | 87 | expect(css_selector).toBe("table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(n+2)"); 88 | }); 89 | 90 | it("should be able to select body", function () { 91 | 92 | var elements = jQuery('body'); 93 | selector.parent = jQuery('html')[0]; 94 | var css_selector = selector.getCssSelector(elements); 95 | 96 | expect(css_selector).toBe("body"); 97 | }); 98 | 99 | it("should be able to select html", function () { 100 | 101 | var elements = jQuery('html'); 102 | selector.parent = document; 103 | var css_selector = selector.getCssSelector(elements); 104 | 105 | expect(css_selector).toBe("html"); 106 | }); 107 | }); 108 | 109 | describe("CSS Selector Strip", function () { 110 | 111 | beforeEach(function () { 112 | 113 | $el = jQuery("#tests").html(""); 114 | selector = new CssSelector({ 115 | parent: jQuery('#tests')[0], 116 | ignoredClasses:['do-not-strip-direct-child-test'] 117 | }); 118 | }); 119 | 120 | it("should be able to strip indexes", function () { 121 | 122 | $el.append(''); 123 | var element = document.getElementsByName('strip-index-test')[0]; 124 | var css_selector = selector.getCssSelector([element]); 125 | 126 | expect(css_selector).toBe("input"); 127 | }); 128 | 129 | it("should be able to strip ids", function () { 130 | 131 | $el.append(''); 132 | var element = document.getElementsByName('strip-id-test')[0]; 133 | var css_selector = selector.getCssSelector([element]); 134 | 135 | expect(css_selector).toBe("textarea"); 136 | }); 137 | 138 | it("should be able to strip classes", function () { 139 | 140 | $el.append('
'); 141 | var element = document.getElementsByName('strip-tags-test')[0]; 142 | var css_selector = selector.getCssSelector([element]); 143 | 144 | expect(css_selector).toBe("select"); 145 | }); 146 | 147 | it("should be able to strip whole tags", function () { 148 | 149 | $el.append('
'); 150 | var element = document.getElementsByName('strip-classes-test')[0]; 151 | var css_selector = selector.getCssSelector([element]); 152 | 153 | expect(css_selector).toBe("span.needed a"); 154 | }); 155 | 156 | it("should not strip direct child when a subchild exists", function () { 157 | 158 | $el.append('
'); 159 | var elements = $('.do-not-strip-direct-child-test'); 160 | var css_selector = selector.getCssSelector(elements); 161 | 162 | expect(css_selector).toBe("span > div"); 163 | }); 164 | 165 | }); 166 | 167 | describe("Combine css selectors", function() { 168 | 169 | beforeEach(function () { 170 | 171 | $el = jQuery("#tests").html(""); 172 | selector = new CssSelector({ 173 | parent: jQuery('#tests')[0], 174 | allowMultipleSelectors: true 175 | }); 176 | }); 177 | 178 | it("should find elements similar", function() { 179 | 180 | $el.append(''); 181 | var span1 = $('.span1', $el)[0]; 182 | var span2 = $('.span2', $el)[0]; 183 | var result = selector.checkSimilarElements(span1, span2); 184 | 185 | expect(result).toBe(true); 186 | }); 187 | 188 | it("should not find elements similar at different deepnesses", function() { 189 | 190 | $el.append('
'); 191 | var span1 = $('.span1', $el)[0]; 192 | var span2 = $('.span2', $el)[0]; 193 | var result = selector.checkSimilarElements(span1, span2); 194 | 195 | expect(result).toBe(false); 196 | }); 197 | 198 | it("should group similar elements", function(){ 199 | 200 | $el.append(''); 201 | var elements = $('span', $el).get(); 202 | var result = selector.getElementGroups(elements); 203 | 204 | expect(result.length).toBe(1); 205 | expect(result[0]).toEqual(elements); 206 | }); 207 | 208 | it("should not group not similar elements", function(){ 209 | 210 | $el.append('
'); 211 | var elements = $('span', $el).get(); 212 | var result = selector.getElementGroups(elements); 213 | 214 | expect(result.length).toBe(2); 215 | expect(result[0]).toEqual([elements[0]]); 216 | expect(result[1]).toEqual([elements[1]]); 217 | }); 218 | 219 | it("should combine two selectors", function() { 220 | 221 | $el.append('
'); 222 | var elements = $('div, span', $el); 223 | var cssSelector = selector.getCssSelector(elements); 224 | 225 | expect(cssSelector).toBe("div, span"); 226 | }); 227 | 228 | it("should combine two selectors at different deepnesses", function() { 229 | 230 | $el.append('
'); 231 | var elements = $('.div1, span', $el); 232 | var cssSelector = selector.getCssSelector(elements); 233 | 234 | expect(cssSelector).toBe("div.div1, span"); 235 | }); 236 | }); 237 | 238 | describe("Smart table selectors", function () { 239 | 240 | beforeEach(function () { 241 | 242 | $el = jQuery("#tests").html(""); 243 | selector = new CssSelector({ 244 | parent: jQuery('#tests')[0], 245 | enableSmartTableSelector: true, 246 | query: jQuery 247 | }); 248 | }); 249 | 250 | it("should be select cells based on text in desciption cell", function () { 251 | 252 | $el.append('
Item:needed data
Not needed itemNot needed item
'); 253 | var element = document.getElementsByClassName('table-cell-test1')[0]; 254 | var css_selector = selector.getCssSelector([element]); 255 | 256 | expect(css_selector).toBe("tr:contains('Item:') td.table-cell-test1"); 257 | }); 258 | 259 | it("should be select cells based on text in desciption cell(th)", function () { 260 | 261 | $el.append('
Item2:needed data
Not needed itemNot needed item
'); 262 | var element = document.getElementsByClassName('table-cell-test3')[0]; 263 | var css_selector = selector.getCssSelector([element]); 264 | 265 | expect(css_selector).toBe("tr:contains('Item2:') td.table-cell-test3"); 266 | }); 267 | 268 | it("should drop its smartness when selecting multiple items", function () { 269 | 270 | $el.append('
1
2
'); 271 | var elements = jQuery('.table-cell-test2'); 272 | var css_selector = selector.getCssSelector(elements); 273 | 274 | expect(css_selector).toBe("td.table-cell-test2"); 275 | }); 276 | 277 | }); 278 | 279 | describe("Ignored classes", function () { 280 | 281 | beforeEach(function () { 282 | 283 | $el = jQuery("#tests").html(""); 284 | selector = new CssSelector({ 285 | parent: jQuery('#tests')[0], 286 | ignoredClasses: ['ignored'] 287 | }); 288 | }); 289 | 290 | it("should ignore classes", function () { 291 | 292 | $el.append('
'); 293 | var element = document.getElementsByClassName('ignored-class-test1')[0]; 294 | var css_selector = selector.getCssSelector([element]); 295 | 296 | expect(css_selector).toBe("a.ignored-class-test1"); 297 | }); 298 | 299 | }); 300 | 301 | describe("Bugs", function () { 302 | 303 | beforeEach(function () { 304 | 305 | $el = jQuery("#tests").html(""); 306 | }); 307 | 308 | it("should select children of parent while using jquery", function () { 309 | 310 | $el.append('
'); 311 | selector = new CssSelector({ 312 | parent: jQuery('#jquery-children-test-parent')[0], 313 | query: jQuery 314 | }); 315 | var elements = jQuery('#jquery-children-test-parent a'); 316 | var css_selector = selector.getCssSelector(elements); 317 | 318 | expect(css_selector).toBe("a"); 319 | }); 320 | 321 | }); 322 | -------------------------------------------------------------------------------- /lib/CssSelector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var CssSelector = function (options) { 3 | 4 | var me = this; 5 | 6 | // defaults 7 | this.ignoredTags = ['font', 'b', 'i', 's']; 8 | this.parent = document; 9 | this.ignoredClassBase = false; 10 | this.enableResultStripping = true; 11 | this.enableSmartTableSelector = false; 12 | this.ignoredClasses = []; 13 | this.allowMultipleSelectors = false; 14 | this.query = function (selector) { 15 | return me.parent.querySelectorAll(selector); 16 | }; 17 | 18 | // overrides defaults with options 19 | for (var i in options) { 20 | this[i] = options[i]; 21 | } 22 | 23 | // jquery parent selector fix 24 | if (this.query === window.jQuery) { 25 | this.query = function (selector) { 26 | return jQuery(me.parent).find(selector); 27 | }; 28 | } 29 | }; 30 | 31 | // TODO refactor element selector list into a ~ class 32 | var ElementSelector = function (element, ignoredClasses) { 33 | 34 | this.element = element; 35 | this.isDirectChild = true; 36 | this.tag = element.localName; 37 | this.tag = this.tag.replace(/:/g, '\\:'); 38 | 39 | // nth-of-child(n+1) 40 | this.indexn = null; 41 | this.index = 1; 42 | this.id = null; 43 | this.classes = new Array(); 44 | 45 | // do not add additinal info to html, body tags. 46 | // html:nth-of-type(1) cannot be selected 47 | if(this.tag === 'html' || this.tag === 'HTML' 48 | || this.tag === 'body' || this.tag === 'BODY') { 49 | this.index = null; 50 | return; 51 | } 52 | 53 | if (element.parentNode !== undefined) { 54 | // nth-child 55 | //this.index = [].indexOf.call(element.parentNode.children, element)+1; 56 | 57 | // nth-of-type 58 | for (var i = 0; i < element.parentNode.children.length; i++) { 59 | var child = element.parentNode.children[i]; 60 | if (child === element) { 61 | break; 62 | } 63 | if (child.tagName === element.tagName) { 64 | this.index++; 65 | } 66 | } 67 | } 68 | 69 | if (element.id !== '') { 70 | if (typeof element.id === 'string') { 71 | this.id = element.id; 72 | this.id = this.id.replace(/:/g, '\\:'); 73 | } 74 | } 75 | 76 | for (var i = 0; i < element.classList.length; i++) { 77 | var cclass = element.classList[i]; 78 | if (ignoredClasses.indexOf(cclass) === -1) { 79 | cclass = cclass.replace(/:/g, '\\:'); 80 | this.classes.push(cclass); 81 | } 82 | } 83 | }; 84 | 85 | var ElementSelectorList = function (CssSelector) { 86 | this.CssSelector = CssSelector; 87 | }; 88 | 89 | ElementSelectorList.prototype = new Array(); 90 | 91 | ElementSelectorList.prototype.getCssSelector = function () { 92 | 93 | var resultSelectors = []; 94 | 95 | // TDD 96 | for (var i = 0; i < this.length; i++) { 97 | var selector = this[i]; 98 | 99 | var isFirstSelector = i === this.length-1; 100 | var resultSelector = selector.getCssSelector(isFirstSelector); 101 | 102 | if (this.CssSelector.enableSmartTableSelector) { 103 | if (selector.tag === 'tr') { 104 | if (selector.element.children.length === 2) { 105 | if (selector.element.children[0].tagName === 'TD' 106 | || selector.element.children[0].tagName === 'TH' 107 | || selector.element.children[0].tagName === 'TR') { 108 | 109 | var text = selector.element.children[0].textContent; 110 | text = text.trim(); 111 | 112 | // escape quotes 113 | text.replace(/(\\*)(')/g, function (x) { 114 | var l = x.length; 115 | return (l % 2) ? x : x.substring(0, l - 1) + "\\'"; 116 | }); 117 | resultSelector += ":contains('" + text + "')"; 118 | } 119 | } 120 | } 121 | } 122 | 123 | resultSelectors.push(resultSelector); 124 | } 125 | 126 | var resultCSSSelector = resultSelectors.reverse().join(' '); 127 | return resultCSSSelector; 128 | }; 129 | 130 | ElementSelector.prototype = { 131 | 132 | getCssSelector: function (isFirstSelector) { 133 | 134 | if(isFirstSelector === undefined) { 135 | isFirstSelector = false; 136 | } 137 | 138 | var selector = this.tag; 139 | if (this.id !== null) { 140 | selector += '#' + this.id; 141 | } 142 | if (this.classes.length) { 143 | for (var i = 0; i < this.classes.length; i++) { 144 | selector += "." + this.classes[i]; 145 | } 146 | } 147 | if (this.index !== null) { 148 | selector += ':nth-of-type(' + this.index + ')'; 149 | } 150 | if (this.indexn !== null && this.indexn !== -1) { 151 | selector += ':nth-of-type(n+' + this.indexn + ')'; 152 | } 153 | if(this.isDirectChild && isFirstSelector === false) { 154 | selector = "> "+selector; 155 | } 156 | 157 | return selector; 158 | }, 159 | // merges this selector with another one. 160 | merge: function (mergeSelector) { 161 | 162 | if (this.tag !== mergeSelector.tag) { 163 | throw "different element selected (tag)"; 164 | } 165 | 166 | if (this.index !== null) { 167 | if (this.index !== mergeSelector.index) { 168 | 169 | // use indexn only for two elements 170 | if (this.indexn === null) { 171 | var indexn = Math.min(mergeSelector.index, this.index); 172 | if (indexn > 1) { 173 | this.indexn = Math.min(mergeSelector.index, this.index); 174 | } 175 | } 176 | else { 177 | this.indexn = -1; 178 | } 179 | 180 | this.index = null; 181 | } 182 | } 183 | 184 | if(this.isDirectChild === true) { 185 | this.isDirectChild = mergeSelector.isDirectChild; 186 | } 187 | 188 | if (this.id !== null) { 189 | if (this.id !== mergeSelector.id) { 190 | this.id = null; 191 | } 192 | } 193 | 194 | if (this.classes.length !== 0) { 195 | var classes = new Array(); 196 | 197 | for (var i in this.classes) { 198 | var cclass = this.classes[i]; 199 | if (mergeSelector.classes.indexOf(cclass) !== -1) { 200 | classes.push(cclass); 201 | } 202 | } 203 | 204 | this.classes = classes; 205 | } 206 | } 207 | }; 208 | 209 | CssSelector.prototype = { 210 | mergeElementSelectors: function (newSelecors) { 211 | 212 | if (newSelecors.length < 1) { 213 | throw "No selectors specified"; 214 | } 215 | else if (newSelecors.length === 1) { 216 | return newSelecors[0]; 217 | } 218 | 219 | // check selector total count 220 | var elementCountInSelector = newSelecors[0].length; 221 | for (var i = 0; i < newSelecors.length; i++) { 222 | var selector = newSelecors[i]; 223 | if (selector.length !== elementCountInSelector) { 224 | throw "Invalid element count in selector"; 225 | } 226 | } 227 | 228 | // merge selectors 229 | var resultingElements = newSelecors[0]; 230 | for (var i = 1; i < newSelecors.length; i++) { 231 | var mergeElements = newSelecors[i]; 232 | 233 | for (var j = 0; j < elementCountInSelector; j++) { 234 | resultingElements[j].merge(mergeElements[j]); 235 | } 236 | } 237 | return resultingElements; 238 | }, 239 | stripSelector: function (selectors) { 240 | 241 | var cssSeletor = selectors.getCssSelector(); 242 | var baseSelectedElements = this.query(cssSeletor); 243 | 244 | var compareElements = function (elements) { 245 | if (baseSelectedElements.length !== elements.length) { 246 | return false; 247 | } 248 | 249 | for (var j = 0; j < baseSelectedElements.length; j++) { 250 | if ([].indexOf.call(elements, baseSelectedElements[j]) === -1) { 251 | return false; 252 | } 253 | } 254 | return true; 255 | }; 256 | // strip indexes 257 | for (var i = 0; i < selectors.length; i++) { 258 | var selector = selectors[i]; 259 | if (selector.index !== null) { 260 | var index = selector.index; 261 | selector.index = null; 262 | var cssSeletor = selectors.getCssSelector(); 263 | var newSelectedElements = this.query(cssSeletor); 264 | // if results doesn't match then undo changes 265 | if (!compareElements(newSelectedElements)) { 266 | selector.index = index; 267 | } 268 | } 269 | } 270 | 271 | // strip isDirectChild 272 | for (var i = 0; i < selectors.length; i++) { 273 | var selector = selectors[i]; 274 | if (selector.isDirectChild === true) { 275 | selector.isDirectChild = false; 276 | var cssSeletor = selectors.getCssSelector(); 277 | var newSelectedElements = this.query(cssSeletor); 278 | // if results doesn't match then undo changes 279 | if (!compareElements(newSelectedElements)) { 280 | selector.isDirectChild = true; 281 | } 282 | } 283 | } 284 | 285 | // strip ids 286 | for (var i = 0; i < selectors.length; i++) { 287 | var selector = selectors[i]; 288 | if (selector.id !== null) { 289 | var id = selector.id; 290 | selector.id = null; 291 | var cssSeletor = selectors.getCssSelector(); 292 | var newSelectedElements = this.query(cssSeletor); 293 | // if results doesn't match then undo changes 294 | if (!compareElements(newSelectedElements)) { 295 | selector.id = id; 296 | } 297 | } 298 | } 299 | 300 | // strip classes 301 | for (var i = 0; i < selectors.length; i++) { 302 | var selector = selectors[i]; 303 | if (selector.classes.length !== 0) { 304 | for (var j = selector.classes.length - 1; j > 0; j--) { 305 | var cclass = selector.classes[j]; 306 | selector.classes.splice(j, 1); 307 | var cssSeletor = selectors.getCssSelector(); 308 | var newSelectedElements = this.query(cssSeletor); 309 | // if results doesn't match then undo changes 310 | if (!compareElements(newSelectedElements)) { 311 | selector.classes.splice(j, 0, cclass); 312 | } 313 | } 314 | } 315 | } 316 | 317 | // strip tags 318 | for (var i = selectors.length - 1; i > 0; i--) { 319 | var selector = selectors[i]; 320 | selectors.splice(i, 1); 321 | var cssSeletor = selectors.getCssSelector(); 322 | var newSelectedElements = this.query(cssSeletor); 323 | // if results doesn't match then undo changes 324 | if (!compareElements(newSelectedElements)) { 325 | selectors.splice(i, 0, selector); 326 | } 327 | } 328 | 329 | return selectors; 330 | }, 331 | getElementSelectors: function (elements, top) { 332 | var elementSelectors = []; 333 | 334 | for (var i = 0; i < elements.length; i++) { 335 | var element = elements[i]; 336 | var elementSelector = this.getElementSelector(element, top); 337 | elementSelectors.push(elementSelector); 338 | } 339 | 340 | return elementSelectors; 341 | }, 342 | getElementSelector: function (element, top) { 343 | 344 | var elementSelectorList = new ElementSelectorList(this); 345 | while (true) { 346 | if (element === this.parent) { 347 | break; 348 | } 349 | else if (element === undefined || element === document) { 350 | throw 'element is not a child of the given parent'; 351 | } 352 | if (this.isIgnoredTag(element.tagName)) { 353 | 354 | element = element.parentNode; 355 | continue; 356 | } 357 | if (top > 0) { 358 | top--; 359 | element = element.parentNode; 360 | continue; 361 | } 362 | 363 | var selector = new ElementSelector(element, this.ignoredClasses); 364 | // document does not have a tagName 365 | if(element.parentNode === document || this.isIgnoredTag(element.parentNode.tagName)) { 366 | selector.isDirectChild = false; 367 | } 368 | 369 | elementSelectorList.push(selector); 370 | element = element.parentNode; 371 | } 372 | 373 | return elementSelectorList; 374 | }, 375 | 376 | /** 377 | * Compares whether two elements are similar. Similar elements should 378 | * have a common parrent and all parent elements should be the same type. 379 | * @param element1 380 | * @param element2 381 | */ 382 | checkSimilarElements: function(element1, element2) { 383 | 384 | while (true) { 385 | 386 | if(element1.tagName !== element2.tagName) { 387 | return false; 388 | } 389 | if(element1 === element2) { 390 | return true; 391 | } 392 | 393 | // stop at body tag 394 | if (element1 === undefined || element1.tagName === 'body' 395 | || element1.tagName === 'BODY') { 396 | return false; 397 | } 398 | if (element2 === undefined || element2.tagName === 'body' 399 | || element2.tagName === 'BODY') { 400 | return false; 401 | } 402 | 403 | element1 = element1.parentNode; 404 | element2 = element2.parentNode; 405 | } 406 | }, 407 | 408 | /** 409 | * Groups elements into groups if the emelents are not similar 410 | * @param elements 411 | */ 412 | getElementGroups: function(elements) { 413 | 414 | // first elment is in the first group 415 | // @TODO maybe i dont need this? 416 | var groups = [[elements[0]]]; 417 | 418 | for(var i = 1; i < elements.length; i++) { 419 | var elementNew = elements[i]; 420 | var addedToGroup = false; 421 | for(var j = 0; j < groups.length; j++) { 422 | var group = groups[j]; 423 | var elementGroup = group[0]; 424 | if(this.checkSimilarElements(elementNew, elementGroup)) { 425 | group.push(elementNew); 426 | addedToGroup = true; 427 | break; 428 | } 429 | } 430 | 431 | // add new group 432 | if(!addedToGroup) { 433 | groups.push([elementNew]); 434 | } 435 | } 436 | 437 | return groups; 438 | }, 439 | getCssSelector: function (elements, top) { 440 | 441 | top = top || 0; 442 | 443 | var enableSmartTableSelector = this.enableSmartTableSelector; 444 | if (elements.length > 1) { 445 | this.enableSmartTableSelector = false; 446 | } 447 | 448 | // group elements into similarity groups 449 | var elementGroups = this.getElementGroups(elements); 450 | 451 | var resultCSSSelector; 452 | 453 | if(this.allowMultipleSelectors) { 454 | 455 | var groupSelectors = []; 456 | 457 | for(var i = 0; i < elementGroups.length; i++) { 458 | var groupElements = elementGroups[i]; 459 | 460 | var elementSelectors = this.getElementSelectors(groupElements, top); 461 | var resultSelector = this.mergeElementSelectors(elementSelectors); 462 | if (this.enableResultStripping) { 463 | resultSelector = this.stripSelector(resultSelector); 464 | } 465 | 466 | groupSelectors.push(resultSelector.getCssSelector()); 467 | } 468 | 469 | resultCSSSelector = groupSelectors.join(', '); 470 | } 471 | else { 472 | if(elementGroups.length !== 1) { 473 | throw "found multiple element groups, but allowMultipleSelectors disabled"; 474 | } 475 | 476 | var elementSelectors = this.getElementSelectors(elements, top); 477 | var resultSelector = this.mergeElementSelectors(elementSelectors); 478 | if (this.enableResultStripping) { 479 | resultSelector = this.stripSelector(resultSelector); 480 | } 481 | 482 | resultCSSSelector = resultSelector.getCssSelector(); 483 | } 484 | 485 | this.enableSmartTableSelector = enableSmartTableSelector; 486 | 487 | // strip down selector 488 | return resultCSSSelector; 489 | }, 490 | isIgnoredTag: function (tag) { 491 | return this.ignoredTags.indexOf(tag.toLowerCase()) !== -1; 492 | } 493 | }; 494 | --------------------------------------------------------------------------------