├── .github └── dependabot.yml ├── .gitignore ├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── config-sample.js ├── gulpfile.js ├── index.html ├── package-lock.json ├── package.json └── src ├── ajax.js ├── client.js ├── index.css ├── index.js ├── jquery.js ├── search-box.js └── util.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: '04:35' 8 | timezone: America/Los_Angeles 9 | open-pull-requests-limit: 10 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # When adding new items here, don't forget to add them to .npmignore as well! 2 | 3 | .DS_Store 4 | *.sublime-project 5 | *.sublime-workspace 6 | node_modules 7 | npm-debug.log 8 | config.js 9 | dist/ 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "browserify": true, 4 | "globalstrict": true, 5 | "globals": { 6 | "domainr": true, 7 | "module": true, 8 | "require": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # This should be identical to .gitignore except we don't want to ignore the dist directory for npm. 2 | # Also npm automatically ignores node_modules and npm-debug.log. 3 | 4 | *.sublime-project 5 | *.sublime-workspace 6 | config.js 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 nb.io, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Domainr Search Box 2 | 3 | Drop-in [instant domain search](https://domainr.com/) for your site. 4 | 5 | ## Installation 6 | 7 | Make sure you have [Node.js](https://nodejs.org/) and [NPM](https://www.npmjs.com/) installed. 8 | 9 | Run `npm install domainr-search-box` to create a local copy of the module. 10 | 11 | ## Usage 12 | 13 | From the `dist/` directory, include `domainr-search-box.min.js` and `domainr-search-box.css` on your page. 14 | 15 | Add an empty `div` underneath your `input`; the Domainr Search Box will fill it with results. 16 | 17 | To authenticate, use the API [for free via RapidAPI](https://rapidapi.com/domainr/api/Domainr) with your RapidAPI API key, or [contact us](mailto:sales@domainr.com) regarding high-volume usage. 18 | 19 | ### Plain JavaScript 20 | 21 | ```javascript 22 | var box = new domainr.SearchBox({ 23 | mashapeKey: yourRapidAPIKey, // your RapidAPI API key 24 | // clientId: yourClientId, // your high-volume clientId; not needed if using RapidAPI 25 | observe: yourInputElement, 26 | renderTo: yourResultsElement, 27 | onSelect: function(result) { ... } 28 | }); 29 | ``` 30 | 31 | ### jQuery 32 | 33 | ```javascript 34 | $('input#search') 35 | .domainrSearchBox({ 36 | mashapeKey: yourRapidAPIKey, // your RapidAPI API key 37 | clientId: yourClientId, // or alternatively your Client ID; not needed if using RapidAPI 38 | renderTo: yourResultsElementOrSelector, 39 | onSelect: function(result) { ... } 40 | }); 41 | ``` 42 | 43 | ## Reference 44 | 45 | ### Plain JavaScript 46 | 47 | #### SearchBox 48 | 49 | Create a `domainr.SearchBox` with `new domainr.SearchBox(options)`. The `options` argument is an object with the following possible properties: 50 | 51 | * `clientId`: Either this or `mashapeKey` are required. 52 | * `mashapeKey`: Either this or `clientId` are required. 53 | * `observe`: The DOM element to observe. 54 | * `renderTo`: The DOM element to contain the rendered autocomplete. 55 | * `renderWith`: A function that will render the autocomplete. It will receive a state object. 56 | * `limit`: A number for the max number of results to display. Optional; default = 20. 57 | * `registrar`: Registrar or Registry domain name, to limit search results to domains supported by a registrar or registry. e.g. `namecheap.com` or `donuts.co` 58 | * `defaults`: Include the given default zones (an array of strings) in your search, e.g. `["coffee", "cafe", "organic"]`. Results will be sorted according to the order of this array. 59 | * `searchDelay`: How many milliseconds after keyboard entry before the search is made (to avoid redundant network traffic). Optional; default = `250`. 60 | * `onSelect`: An optional function to be called when the user selects a domain (if omitted, a new window will open with a recommended registrar for that result). Receives a single object with these properties: 61 | * `domain` 62 | * `host` 63 | * `path` 64 | * `registerURL` 65 | * `status` 66 | * `subdomain` 67 | * `zone` 68 | 69 | #### Client 70 | 71 | Create a `domainr.Client` with `new domainr.Client(options)`. The `options` argument is an object with the following possible properties: 72 | 73 | * `clientId`: Either this or `mashapeKey` are required. 74 | * `mashapeKey`: Either this or clientId are required. 75 | * `baseURL`: Optional. 76 | 77 | The `domainr.Client` has the following methods: 78 | 79 | * `search(params, callback)`: Search with the given params and call the callback with the results upon completion. The `params` argument is an object with the following possible properties: 80 | * `defaults`: Comma-delimited string, e.g. `coffee,cafe,organic` 81 | * `location`: String, e.g. `de` 82 | * `query`: String. 83 | * `registrar`: String, e.g. `namecheap.com` or `donuts.co` 84 | * `status(domains, callback)`: Call the `status` API with the given domains (an array) and call the callback with the results upon completion. 85 | * `options(domain, callback)`: Call the `options` API with the given domain (a string) and call the callback with the results upon completion. 86 | * `zones(callback)`: Call the `zones` API and call the callback with the results upon completion. 87 | * `registerURL(domain, options)`: Returns a URL for registering the given domain (a string). The options parameter is an object; currently one property is accepted, `registrar`, to specify a specific registrar. 88 | 89 | ### jQuery 90 | 91 | Select the element you want to observe and call `.domainrSearchBox(options)` on it. Possible options include: 92 | 93 | * `clientId`: Either this or `mashapeKey` are required. 94 | * `mashapeKey`: Either this or `clientId` are required. 95 | * `renderTo`: Specification for which element to contain the rendered autocomplete. Can be anything jQuery will accept (selector, element, jQuery object). 96 | * `renderWith`: A function that will render the autocomplete. It will receive a state object. 97 | * `limit`: A number for the max number of results to display. Optional; default = 20. 98 | * `registrar`: Registrar or Registry domain name, to limit search results to domains supported by a registrar or registry. e.g. `namecheap.com` or `donuts.co` 99 | * `defaults`: Include the given default zones (an array of strings) in your search, e.g. `["coffee", "cafe", "organic"]`. Results will be sorted according to the order of this array. 100 | * `searchDelay`: How many milliseconds after keyboard entry before the search is made (to avoid redundant network traffic). Optional; default = 250. 101 | * `onSelect`: An optional function to be called when the user selects a domain (if omitted, a new window will open with a recommended registrar for that result). Receives a single object with these properties: 102 | * `domain` 103 | * `host` 104 | * `path` 105 | * `registerURL` 106 | * `status` 107 | * `subdomain` 108 | * `zone` 109 | 110 | ## Development 111 | 112 | You'll need to have [Node](https://nodejs.org/) and [Gulp](http://gulpjs.com/) installed. 113 | 114 | * `npm install -g gulp` to install Gulp. 115 | * `npm install` to install dependencies 116 | * `gulp build` to build the code. 117 | * `gulp watch` to watch the code, building on changes. 118 | * note: console output will appear when files are changed 119 | * `gulp` builds + watch, and runs the demo in a webpage. 120 | 121 | ### Publishing new versions 122 | 123 | - Always increment the version 124 | - `npm whoami` to see your current npm user 125 | - `npm publish` to publish it 126 | 127 | Note that the demo requires the code to be built. Also, you'll need to get a `mashapeKey` or `clientId` and set up a `config.js`; instructions are in `index.html`. 128 | 129 | © 2018 nb.io, LLC 130 | -------------------------------------------------------------------------------- /config-sample.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | mashapeKey: 'YOUR RapidAPI KEY GOES HERE', // comment out, if using clientId 3 | // clientId: 'YOUR HIGH-VOLUME CLIENT KEY GOES HERE' // comment out, if using RapidAPI 4 | }; 5 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* globals process, console */ 2 | 'use strict'; 3 | 4 | var browserify = require('browserify'); 5 | var gulp = require('gulp'); 6 | var source = require('vinyl-source-stream'); 7 | var buffer = require('vinyl-buffer'); 8 | var rename = require('gulp-rename'); 9 | var uglify = require('gulp-uglify'); 10 | var httpServer = require('http-server'); 11 | var openBrowser = require('opener'); 12 | var minifyCss = require('gulp-clean-css'); 13 | 14 | var dest = './dist/'; 15 | var basename = 'domainr-search-box'; 16 | var jsName = basename + '.js'; 17 | 18 | // ---------- 19 | gulp.task('js', function () { 20 | return browserify('./src/index.js', { standalone: 'domainr' }) 21 | .bundle() 22 | .pipe(source(jsName)) 23 | .pipe(buffer()) 24 | .pipe(gulp.dest(dest)) 25 | .pipe(uglify()) 26 | .pipe(rename({ extname: '.min.js' })) 27 | .pipe(gulp.dest(dest)); 28 | }); 29 | 30 | // ---------- 31 | gulp.task('css', function () { 32 | return gulp.src('./src/index.css') 33 | .pipe(rename({ basename: basename })) 34 | .pipe(minifyCss({ compatibility: 'ie8' })) 35 | .pipe(gulp.dest(dest)); 36 | }); 37 | 38 | // ---------- 39 | gulp.task('build', gulp.series(gulp.parallel('js', 'css'))); 40 | 41 | // ---------- 42 | gulp.task('watch', function() { 43 | var watcher = gulp.watch('src/*.*', gulp.parallel('build')); 44 | watcher.on('all', function (event, path, stats) { 45 | console.log('File ' + path + ' was ' + event + ', running tasks...'); 46 | }); 47 | }) 48 | 49 | // ---------- 50 | gulp.task('serve', function () { 51 | var port = process.env.PORT || 3100; 52 | var server = httpServer.createServer(); 53 | return server.listen(port, 'localhost', function () { 54 | console.log('Server listening at http://localhost:' + port); 55 | openBrowser('http://localhost:' + port); 56 | }); 57 | }); 58 | 59 | gulp.task('default', gulp.parallel('serve', 'watch')); 60 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Domainr Search Demo 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 |
22 |

Simple Domainr Search Demo

23 | 24 |
25 |

To make this demo work, make a config.js with your clientId or mashapeKey and put it in the same directory. See config-sample.js for an example.

26 |
27 | 28 | 29 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domainr-search-box", 3 | "version": "0.0.31", 4 | "description": "Domainr instant search box for your site", 5 | "homepage": "https://github.com/domainr/domainr-search-box", 6 | "repository": "https://github.com/domainr/domainr-search-box.git", 7 | "bugs": { 8 | "url": "https://github.com/domainr/domainr-search-box/issues", 9 | "email": "bugs+search-box@domainr.com" 10 | }, 11 | "licenses": [ 12 | { 13 | "type": "MIT", 14 | "url": "https://github.com/domainr/domainr-search-box/blob/main/LICENSE" 15 | } 16 | ], 17 | "contributors": [ 18 | { 19 | "name": "Randy Reddig" 20 | }, 21 | { 22 | "name": "Ian Gilman" 23 | }, 24 | { 25 | "name": "Eric Case" 26 | }, 27 | { 28 | "name": "Cameron Walters" 29 | } 30 | ], 31 | "keywords": [ 32 | "jquery-plugin", 33 | "ecosystem:jquery", 34 | "domainr", 35 | "domain", 36 | "domains", 37 | "search", 38 | "domain names", 39 | "tld" 40 | ], 41 | "devDependencies": { 42 | "browserify": "^17.0.0", 43 | "graceful-fs": "^4.2.4", 44 | "gulp": "^4.0.2", 45 | "gulp-clean-css": "^4.2.0", 46 | "gulp-rename": "^2.0.0", 47 | "gulp-uglify": "^3.0.0", 48 | "http-server": "^14.0.0", 49 | "lodash": "^4.17.15", 50 | "minimatch": "^9.0.0", 51 | "natives": "^1.1.6", 52 | "opener": "^1.4.1", 53 | "vinyl-buffer": "^1.0.1", 54 | "vinyl-source-stream": "^2.0.0" 55 | }, 56 | "scripts": { 57 | "prepare": "gulp build" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ajax.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('./util'); 4 | 5 | var sequence = 0; 6 | 7 | // Detect CORS support 8 | var cors = false; 9 | if ('withCredentials' in xhr()) { 10 | cors = true; 11 | } 12 | 13 | // ---------- 14 | function getJSON(url, callback, failure) { 15 | if (!callback) { 16 | throw new Error('[domainr] Missing callback'); 17 | } 18 | 19 | if (cors) { 20 | getCORS(url, callback, failure); 21 | return; 22 | } 23 | 24 | getJSONP(url, callback, failure); 25 | } 26 | 27 | // ---------- 28 | // Minimal fallback for IE 29 | // http://toddmotto.com/writing-a-standalone-ajax-xhr-javascript-micro-library/ 30 | function xhr() { 31 | try { 32 | var XHR = window.XMLHttpRequest || window.ActiveXObject; 33 | return new XHR('MSXML2.XMLHTTP.3.0'); 34 | } catch (e) { 35 | return {}; 36 | } 37 | } 38 | 39 | // ---------- 40 | function getCORS(url, callback, failure) { 41 | failure = failure || function() {}; 42 | var x = xhr(); 43 | 44 | x.onreadystatechange = function() { 45 | var message; 46 | 47 | if (x.readyState != 4) { 48 | return; 49 | } 50 | 51 | if (x.status != 200) { 52 | message = 'Error fetching data: ' + x.responseText; 53 | util.error(message); 54 | failure({ message: message }); 55 | return; 56 | } 57 | 58 | var result; 59 | try { 60 | result = JSON.parse(x.responseText); 61 | } catch (e) { 62 | message = 'Unable to parse data: ' + x.responseText + '; ' + e; 63 | util.error(message); 64 | failure({ message: message }); 65 | return; 66 | } 67 | 68 | callback(result); 69 | }; 70 | 71 | x.open('GET', url, true); 72 | x.send(); 73 | } 74 | 75 | // ---------- 76 | // You must provide a success callback, but the failure callback is optional. 77 | // For each call to getJSONP, you'll get at most 1 callback (either success or failure) call. If you 78 | // don't provide a failure callback, you might not receive a call at all (if the function doesn't 79 | // succeed). If you do provide the failure callback, you'll get exactly 1 call, whichever one is 80 | // appropriate to the result. 81 | function getJSONP(url, success, failure) { 82 | var script = document.createElement('script'); 83 | script.async = true; 84 | var id = '_jsonp' + sequence++; 85 | var failureSent = false; 86 | 87 | var timeout = setTimeout(function() { 88 | var message = 'Timeout trying to retrieve ' + url; 89 | util.error(message); 90 | if (failure) { 91 | failure({ message: message }); 92 | 93 | // Here we set a flag so we won't end up sending a success callback later if the result comes 94 | // through after the timeout. 95 | failureSent = true; 96 | } 97 | }, 5000); 98 | 99 | window[id] = function(data) { 100 | clearTimeout(timeout); 101 | 102 | setTimeout(function() { 103 | var scr = document.getElementById(id); 104 | scr.parentNode.removeChild(scr); 105 | window[id] = undefined; 106 | try { 107 | delete window[id]; 108 | } catch (e) {} 109 | }, 0); 110 | 111 | if (!failureSent) { 112 | success(data); 113 | } 114 | }; 115 | 116 | var c = url.indexOf('?') >= 0 ? '&' : '?'; 117 | script.setAttribute('src', url + c + 'callback=' + id); 118 | script.id = id; 119 | var head = document.head || document.getElementsByTagName('head')[0] || document.documentElement; 120 | head.insertBefore(script, head.firstChild); 121 | } 122 | 123 | // ---------- 124 | module.exports = { 125 | getJSON: getJSON 126 | }; 127 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ajax = require('./ajax'); 4 | var util = require('./util'); 5 | 6 | function Client(options) { 7 | options = options || {}; 8 | this.clientId = options.clientId; 9 | this.mashapeKey = options.mashapeKey; 10 | if (!this.clientId && !this.mashapeKey) { 11 | throw new Error('[domainr] Missing mashapeKey or clientId'); 12 | } 13 | this.baseURL = options.baseURL || (this.clientId ? 'https://api.domainr.com/v2' : 'https://domainr.p.rapidapi.com/v2'); 14 | } 15 | 16 | Client.prototype = { 17 | search: function(params, callback) { 18 | this._get('/search', this.searchParams(params), callback); 19 | }, 20 | 21 | searchParams: function(p) { 22 | return util.extract(p, ['query', 'registrar', 'location', 'defaults']); 23 | }, 24 | 25 | status: function(domains, callback) { 26 | var self = this; 27 | 28 | if (!domains) { 29 | throw new Error('[domainr] domains array is required'); 30 | } 31 | 32 | util.uniq(domains); 33 | 34 | var output = { 35 | status: [] 36 | }; 37 | 38 | var completed = 0; 39 | 40 | var incrementCompleted = function() { 41 | completed++; 42 | if (completed === domains.length) { 43 | callback(output); 44 | } 45 | }; 46 | 47 | var doOne = function(domain) { 48 | var params = { 49 | domain: domain 50 | }; 51 | 52 | self._get('/status', params, function(result) { 53 | if (result && result.status && result.status[0]) { 54 | output.status.push(result.status[0]); 55 | } else { 56 | util.error('Empty status result', result); 57 | } 58 | 59 | incrementCompleted(); 60 | }, incrementCompleted); 61 | }; 62 | 63 | for (var i = 0; i < domains.length; i++) { 64 | doOne(domains[i]); 65 | } 66 | }, 67 | 68 | options: function(domain, callback) { 69 | this._get('/options', { 70 | domain: domain, 71 | }, callback); 72 | }, 73 | 74 | zones: function(callback) { 75 | this._get('/zones', {}, callback); 76 | }, 77 | 78 | registerURL: function(domain, options) { 79 | var params = util.extract(options, ['registrar']); 80 | params.domain = domain; 81 | return this._url('/register', params); 82 | }, 83 | 84 | _get: function(path, params, callback, failure) { 85 | var url = this.baseURL + path + '?' + util.qs(params || {}, this._key()); 86 | ajax.getJSON(url, callback, failure); 87 | }, 88 | 89 | _url: function (path, params) { 90 | return this.baseURL + path + '?' + util.qs(params || {}, this._key()); 91 | }, 92 | 93 | _key: function() { 94 | if (this.clientId) { 95 | return { 96 | client_id: this.clientId 97 | }; 98 | } 99 | 100 | return { 101 | 'mashape-key': this.mashapeKey 102 | }; 103 | } 104 | }; 105 | 106 | module.exports = Client; 107 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .domainr-results-container { 2 | position: relative; 3 | width: 100%; 4 | text-align: left; 5 | text-shadow: none; 6 | z-index: 99999; 7 | } 8 | 9 | .domainr-results { 10 | position: absolute; 11 | box-sizing: border-box; 12 | width: 100%; 13 | z-index: 2; 14 | border: 1px solid #ccc; 15 | padding: 0 1px; 16 | background: white; 17 | color: #666; 18 | box-shadow: 0 2px 4px 0 rgba(0,0,0,0.25); 19 | line-height: 1.25em; 20 | } 21 | 22 | .domainr-result { 23 | position: relative; 24 | cursor: pointer; 25 | margin: 1px 0; 26 | border-radius: 1px; 27 | padding: 2px 8px; 28 | } 29 | 30 | .domainr-result:after { 31 | float: right; 32 | opacity: 0.5; 33 | font-weight: normal; 34 | font-size: 60%; 35 | line-height: inherit; 36 | } 37 | 38 | .domainr-result:hover, 39 | .domainr-result.selected { 40 | background-color: #eee; 41 | } 42 | 43 | .domainr-results .active, 44 | .domainr-results .claimed, 45 | .domainr-results .reserved { 46 | color: #006699; 47 | } 48 | 49 | .domainr-results .reserved:after { 50 | content: "reserved"; 51 | } 52 | 53 | .domainr-results .inactive:not(.reserved) { 54 | color: #009900; 55 | background: #eeffee; 56 | } 57 | 58 | .domainr-results .inactive:not(.reserved):hover, 59 | .domainr-results .inactive:not(.reserved).selected { 60 | background: #ddeedd; 61 | } 62 | 63 | .domainr-results .inactive:not(.reserved):not(.premium):after { 64 | content: "available"; 65 | } 66 | 67 | .domainr-results .pending:not(.disallowed):not(.invalid):not(.unregistrable):not(.zone):not(.suffix):after { 68 | content: "new"; 69 | } 70 | 71 | .domainr-results .priced, 72 | .domainr-results .transferable, 73 | .domainr-results .marketed { 74 | color: #009900; 75 | } 76 | 77 | .domainr-results .priced:after, 78 | .domainr-results .transferable:after, 79 | .domainr-results .marketed:after { 80 | content: "for sale"; 81 | } 82 | 83 | .domainr-results .priced, 84 | .domainr-results .transferable { 85 | background: #eeffee; 86 | } 87 | 88 | .domainr-results .priced:hover, 89 | .domainr-results .transferable:hover, 90 | .domainr-results .priced.selected, 91 | .domainr-results .transferable.selected { 92 | background: #ddeedd; 93 | } 94 | 95 | .domainr-results .expiring, 96 | .domainr-results .deleting { 97 | color: #f79a0f; 98 | background-color: #fef4e7; 99 | } 100 | 101 | .domainr-results .expiring:hover, 102 | .domainr-results .deleting:hover, 103 | .domainr-results .expiring.selected, 104 | .domainr-results .deleting.selected { 105 | background-color: #f2e8dc; 106 | } 107 | 108 | .domainr-results .expiring:after, 109 | .domainr-results .deleting:after { 110 | content: "expiring"; 111 | } 112 | 113 | .domainr-results .premium { 114 | color: #009900; 115 | background: #eeffee; 116 | } 117 | 118 | .domainr-results .premium:hover, 119 | .domainr-results .premium.selected { 120 | background: #ddeedd; 121 | } 122 | 123 | .domainr-results .premium:after { 124 | content: "premium"; 125 | } 126 | 127 | .domainr-results .parked:not(.marketed):after { 128 | content: "parked"; 129 | } 130 | 131 | .domainr-results .unregistrable, 132 | .domainr-results .disallowed, 133 | .domainr-results .invalid, 134 | .domainr-results .unknown { 135 | color: rgba(0, 0, 0, 0.3); 136 | } 137 | 138 | .domainr-results .tld, 139 | .domainr-results .zone, 140 | .domainr-results .registry, 141 | .domainr-results .registrar { 142 | font-weight: bold; 143 | color: #333333; 144 | } 145 | 146 | .domainr-results .zone span.domainr-result-domain:before { 147 | content: "."; 148 | display: inline-block; 149 | position: relative; 150 | left: -0.3em; 151 | width: 0; 152 | } 153 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Client = require('./client'); 4 | var SearchBox = require('./search-box'); 5 | require('./jquery'); 6 | 7 | module.exports = { 8 | Client: Client, 9 | SearchBox: SearchBox 10 | }; 11 | -------------------------------------------------------------------------------- /src/jquery.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchBox = require('./search-box'); 4 | var util = require('./util'); 5 | 6 | if (window.jQuery) { 7 | var $ = window.jQuery; 8 | 9 | $.fn.domainrSearchBox = function(config) { 10 | this.each(function(i, el) { 11 | var searchBoxConfig = util.extract(config, ['clientId', 'mashapeKey', 12 | 'renderWith', 'limit', 'registrar', 'defaults', 'onSelect']); 13 | 14 | searchBoxConfig.observe = el; 15 | 16 | if (config.renderTo) { 17 | var $el = $(config.renderTo); 18 | if ($el.length > 0) { 19 | searchBoxConfig.renderTo = $el[0]; 20 | } 21 | } 22 | 23 | var searchBox = new domainr.SearchBox(searchBoxConfig); 24 | }); 25 | 26 | return this; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/search-box.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Client = require('./client'); 4 | var util = require('./util'); 5 | 6 | var SearchBox = function(options) { 7 | var self = this; 8 | 9 | options = options || {}; 10 | 11 | if (!options.observe) { 12 | throw new Error('[domainr] "observe" is required'); 13 | } 14 | 15 | this._client = new Client(util.extract(options, ['clientId', 'mashapeKey'])); 16 | this._state = { 17 | query: '', 18 | results: [], 19 | limit: 20, 20 | selection: -1 21 | }; 22 | 23 | this._seq = 0; 24 | this._last = 0; 25 | this._cache = {}; 26 | this._listeners = {}; 27 | this._searchTimeout = null; 28 | this._searchDelay = 250; 29 | this._renderer = defaultRenderer; 30 | 31 | this._in = options.observe; 32 | on(this._in, 'keyup', this._input, this); 33 | on(this._in, 'input', this._input, this); 34 | on(this._in, 'change', this._input, this); 35 | on(this._in, 'keydown', this._keydown, this); 36 | 37 | if (options.renderTo !== undefined) { 38 | this._out = options.renderTo; 39 | addClass(this._out, 'domainr-results-container'); 40 | on(this._out, 'click', this._click, this); 41 | } 42 | 43 | if (options.renderWith !== undefined) { 44 | this._renderer = options.renderWith; 45 | } 46 | 47 | if (options.limit !== undefined) { 48 | this._state.limit = options.limit; 49 | } 50 | 51 | if (options.registrar !== undefined) { 52 | this._state.registrar = options.registrar; 53 | } 54 | 55 | if (options.defaults !== undefined) { 56 | this._state.defaults = options.defaults.join(','); 57 | } 58 | 59 | if (options.searchDelay !== undefined) { 60 | this._searchDelay = options.searchDelay; 61 | } 62 | 63 | if (options.onSelect !== undefined) { 64 | this._onSelect = options.onSelect; 65 | } else { 66 | this._onSelect = function(result) { 67 | self._in.value = result.domain; 68 | window.open(self._client.registerURL(result.domain)); 69 | }; 70 | } 71 | 72 | this._input(); // In case there's something already in the input 73 | }; 74 | 75 | SearchBox.prototype = { 76 | _input: function() { 77 | if (this._state.query != this._in.value) { 78 | if (this._in.value === '') { 79 | this._state.results = []; 80 | this._update(); 81 | return; 82 | } 83 | this._state.query = this._in.value; 84 | window.clearTimeout(this._searchTimeout); 85 | this._searchTimeout = window.setTimeout(this._search.bind(this), this._searchDelay); 86 | } 87 | }, 88 | 89 | _keydown: function(event) { 90 | event = event || window.event; 91 | var handled = false; 92 | 93 | if (event.keyCode === 38) { // Up arrow 94 | handled = true; 95 | this._state.selection--; 96 | if (this._state.selection < 0) { 97 | this._state.selection = this._state.results.length - 1; 98 | } 99 | 100 | this._update(); 101 | } else if (event.keyCode === 40) { // Down arrow 102 | handled = true; 103 | this._state.selection++; 104 | if (this._state.selection >= this._state.results.length) { 105 | this._state.selection = 0; 106 | } 107 | 108 | this._update(); 109 | } else if (event.keyCode === 13) { // Enter key 110 | if (this._state.selection !== -1) { 111 | handled = true; 112 | this._choose(this._state.results[this._state.selection]); 113 | } 114 | } 115 | 116 | if (handled && event.preventDefault) { 117 | event.preventDefault(); 118 | } 119 | }, 120 | 121 | _click: function(event) { 122 | event = event || window.event; 123 | var rs = this._state.results; 124 | for (var e = event.target || event.srcElement; e && e != document; e = e.parentNode) { 125 | var d = e.getAttribute('data-domain'); 126 | if (d) { 127 | for (var i = 0; i < rs.length; i++) { 128 | var r = rs[i]; 129 | if (r.domain == d) { 130 | this._choose(r); 131 | return; 132 | } 133 | } 134 | } 135 | } 136 | }, 137 | 138 | _render: function() { 139 | if (!this._out) { 140 | return; 141 | } 142 | this._out.innerHTML = this._renderer(this._state); 143 | return this; 144 | }, 145 | 146 | _search: function() { 147 | var self = this; 148 | this._state.selection = -1; 149 | 150 | // Try cache first 151 | var key = util.qs(this._client.searchParams(this._state)); 152 | var res = this._cache[key]; 153 | if (res !== undefined) { 154 | this._state.results = res.results; 155 | this._update(); 156 | return; 157 | } 158 | 159 | // Make network request 160 | var seq = this._seq++; 161 | this._client.search(this._state, function(res) { 162 | self._cache[key] = res; 163 | if (self._last > seq) { 164 | return; 165 | } 166 | self._last = seq; 167 | self._state.results = res.results; 168 | self._update(); 169 | }); 170 | }, 171 | 172 | _update: function() { 173 | this._sort(); 174 | this._limit(); 175 | this._status(); 176 | this._render(); 177 | }, 178 | 179 | _sort: function() { 180 | if (!this._state.defaults || !this._state.results) { 181 | return; 182 | } 183 | 184 | var defaults = this._state.defaults.split(','); 185 | this._state.results.sort(function(a, b) { 186 | var aIndex = util.indexOf(defaults, a.zone); 187 | if (aIndex === -1) { 188 | aIndex = defaults.length; 189 | } 190 | 191 | var bIndex = util.indexOf(defaults, b.zone); 192 | if (bIndex === -1) { 193 | bIndex = defaults.length; 194 | } 195 | 196 | if (aIndex !== bIndex) { 197 | return aIndex - bIndex; 198 | } 199 | 200 | return a.domain - b.domain; 201 | }); 202 | }, 203 | 204 | _limit: function() { 205 | if (this._state.limit >= 0 && this._state.results.length > this._state.limit) { 206 | this._state.results.length = this._state.limit; 207 | } 208 | }, 209 | 210 | _status: function() { 211 | var self = this; 212 | 213 | // Extract domains without status 214 | var i; 215 | var d = []; 216 | var MAX_STATUS_DOMAINS = 10; 217 | var rs = this._state.results; 218 | for (i = 0; i < rs.length && d.length < MAX_STATUS_DOMAINS; i++) { 219 | var r = rs[i]; 220 | r.status = this._cache[r.domain + ':status'] || r.status; 221 | if (!r.status) { 222 | r.status = 'unknown'; 223 | d.push(r.domain); 224 | } 225 | } 226 | 227 | if (d.length === 0) { 228 | return; 229 | } 230 | 231 | util.uniq(d); 232 | 233 | var doOne = function(domain) { 234 | self._client.status([domain], function(res) { 235 | var s = res.status[0]; 236 | if (s) { 237 | self._cache[s.domain + ':status'] = s.status; 238 | self._update(); 239 | } 240 | }); 241 | }; 242 | 243 | for (i = 0; i < d.length; i++) { 244 | doOne(d[i]); 245 | } 246 | }, 247 | 248 | _choose: function(result) { 249 | if (this._onSelect) { 250 | this._onSelect(result); 251 | } 252 | } 253 | }; 254 | 255 | function defaultRenderer(state) { 256 | var rs = state.results; 257 | var l = rs.length; 258 | if (l === 0) { 259 | return ''; 260 | } 261 | var h = ['
']; 262 | for (var i = 0; i < l; i++) { 263 | var r = rs[i]; 264 | 265 | var classNames = [ 266 | 'domainr-result', 267 | r.status 268 | ]; 269 | 270 | if (state.selection === i) { 271 | classNames.push('selected'); 272 | } 273 | 274 | h.push( 275 | '
' + 276 | '' + 277 | '' + r.host + '' + 278 | '' + r.subdomain + '' + 279 | '' + r.zone + '' + 280 | '' + 281 | '' + r.path + '' + 282 | '
' 283 | ); 284 | } 285 | h.push('
'); 286 | return h.join(''); 287 | } 288 | 289 | function on(e, ev, cb, obj) { 290 | if (obj) { 291 | var original = cb; 292 | cb = function() { 293 | return original.apply(obj, arguments); 294 | }; 295 | } 296 | if (e.addEventListener) { 297 | e.addEventListener(ev, cb, false); 298 | } else if (e.attachEvent) { 299 | e.attachEvent('on' + ev, cb); 300 | } else { 301 | e['on' + ev] = cb; 302 | } 303 | } 304 | 305 | function addClass(e, className) { 306 | if (e.classList) { 307 | e.classList.add(className); 308 | } else { 309 | e.className += ' ' + className; 310 | } 311 | } 312 | 313 | module.exports = SearchBox; 314 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var euc = encodeURIComponent; 4 | 5 | function extract(p, keys) { 6 | var x = {}; 7 | if (p) { 8 | for (var i = 0; i < keys.length; i++) { 9 | var k = keys[i]; 10 | if (p[k] !== undefined) { 11 | x[k] = p[k]; 12 | } 13 | } 14 | } 15 | return x; 16 | } 17 | 18 | function qs() { 19 | var q = []; 20 | for (var i = 0; i < arguments.length; i++) { 21 | var p = arguments[i]; 22 | for (var k in p) { 23 | q.push(euc(k) + '=' + euc(p[k])); 24 | } 25 | } 26 | return q.sort().join('&'); 27 | } 28 | 29 | function error(message) { 30 | if (window.console && window.console.error) { 31 | window.console.error('[domainr] ' + message); 32 | } 33 | } 34 | 35 | function uniq(a) { 36 | if (!a) { 37 | return; 38 | } 39 | 40 | var i, j; 41 | for (i = 0; i < a.length; i++) { 42 | for (j = i + 1; j < a.length; j++) { 43 | if (a[i] === a[j]) { 44 | a.splice(j, 1); 45 | j--; 46 | } 47 | } 48 | } 49 | } 50 | 51 | function indexOf(a, v) { 52 | if (!a) { 53 | return -1; 54 | } 55 | 56 | var i; 57 | for (i = 0; i < a.length; i++) { 58 | if (a[i] === v) { 59 | return i; 60 | } 61 | } 62 | 63 | return -1; 64 | } 65 | 66 | module.exports = { 67 | euc: euc, 68 | extract: extract, 69 | qs: qs, 70 | error: error, 71 | uniq: uniq, 72 | indexOf: indexOf 73 | }; 74 | --------------------------------------------------------------------------------