├── .dockerignore ├── .gitignore ├── .env.example ├── docker-compose.yml ├── public ├── vendor │ └── intl-phone │ │ ├── img │ │ └── flags.png │ │ ├── css │ │ ├── demo.css │ │ └── intlTelInput.css │ │ ├── libphonenumber │ │ └── src │ │ │ └── utils.js │ │ └── js │ │ ├── intlTelInput.min.js │ │ └── intlTelInput.js ├── app.js └── landing.html ├── Dockerfile ├── CONTRIBUTING.md ├── app.js ├── server.js ├── .mergify.yml ├── config.js ├── package.json ├── .github └── workflows │ └── nodejs.yml ├── LICENSE ├── lib └── twilioClient.js ├── app.json ├── spec ├── twilioClient.spec.js └── index.spec.js ├── views └── index.pug ├── routes └── index.js ├── CODE_OF_CONDUCT.md └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | .env 5 | npm-debug.log 6 | .tool-versions 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXX 2 | TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxx 3 | TWILIO_NUMBER=+1XXXXXXXXXX 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | app: 4 | restart: always 5 | build: . 6 | ports: 7 | - "3000:3000" 8 | -------------------------------------------------------------------------------- /public/vendor/intl-phone/img/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-node/HEAD/public/vendor/intl-phone/img/flags.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | CMD [ "npm", "start" ] 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Twilio 2 | 3 | All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. 4 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Create and start server on configured port 2 | require('dotenv').config(); 3 | 4 | var config = require('./config'); 5 | var server = require('./server'); 6 | server.listen(config.port, function() { 7 | console.log('Express server running on port ' + config.port); 8 | }); 9 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var express = require('express'); 3 | 4 | // Create an Express web app 5 | var app = express(); 6 | 7 | // Configure routes and middleware for the application 8 | require('./routes')(app); 9 | 10 | // Create an HTTP server to run our application 11 | var server = http.createServer(app); 12 | 13 | // export the HTTP server as the public module interface 14 | module.exports = server; 15 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - author=dependabot-preview[bot] 5 | - status-success=build (10.x, macos-latest) 6 | - status-success=build (12.x, macos-latest) 7 | - status-success=build (10.x, windows-latest) 8 | - status-success=build (12.x, windows-latest) 9 | - status-success=build (10.x, ubuntu-latest) 10 | - status-success=build (12.x, ubuntu-latest) 11 | actions: 12 | merge: 13 | method: squash 14 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | // Define app configuration in a single location, but pull in values from 2 | // system environment variables (so we don't check them in to source control!) 3 | module.exports = { 4 | // Twilio Account SID - found on your dashboard 5 | accountSid: process.env.TWILIO_ACCOUNT_SID || 'ACXXXXX', 6 | 7 | // Twilio Auth Token - found on your dashboard 8 | authToken: process.env.TWILIO_AUTH_TOKEN || 'XXXXX', 9 | 10 | // A Twilio number that you have purchased through the twilio.com web 11 | // interface or API 12 | twilioNumber: process.env.TWILIO_NUMBER || '+15555555555', 13 | 14 | // The port your web application will run on 15 | port: process.env.PORT || 3000, 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clicktocall-node", 3 | "version": "1.0.0", 4 | "description": "An example of implementing click to call functionality in a Node.js application", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "node app.js" 9 | }, 10 | "keywords": [ 11 | "twilio", 12 | "click", 13 | "to", 14 | "call" 15 | ], 16 | "author": "Kevin Whinnery", 17 | "license": "MIT", 18 | "engines": { 19 | "node": "^12" 20 | }, 21 | "dependencies": { 22 | "body-parser": "^1.19.0", 23 | "dotenv": "^8.2.0", 24 | "express": "^4.17.1", 25 | "morgan": "^1.10.0", 26 | "pug": "^2.0.4", 27 | "twilio": "^3.42.1" 28 | }, 29 | "devDependencies": { 30 | "jest": "^25.3.0", 31 | "supertest": "^4.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.platform }} 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x] 20 | platform: [windows-latest, macos-latest, ubuntu-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm test 30 | env: 31 | CI: true 32 | TWILIO_ACCOUNT_SID: ACXXXXX 33 | TWILIO_AUTH_TOKEN: XXXXX 34 | TWILIO_NUMBER: +15555555555 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Twilio Inc. 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. 22 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | // Execute JavaScript on page load 2 | $(function() { 3 | // Initialize phone number text input plugin 4 | $('#phoneNumber, #salesNumber').intlTelInput({ 5 | responsiveDropdown: true, 6 | autoFormat: true, 7 | utilsScript: '/vendor/intl-phone/libphonenumber/build/utils.js' 8 | }); 9 | 10 | // Intercept form submission and submit the form with ajax 11 | $('#contactForm').on('submit', function(e) { 12 | // Prevent submit event from bubbling and automatically submitting the 13 | // form 14 | e.preventDefault(); 15 | 16 | // Call our ajax endpoint on the server to initialize the phone call 17 | $.ajax({ 18 | url: '/call', 19 | method: 'POST', 20 | dataType: 'json', 21 | data: { 22 | phoneNumber: $('#phoneNumber').val(), 23 | salesNumber: $('#salesNumber').val() 24 | } 25 | }).done(function(data) { 26 | // The JSON sent back from the server will contain a success message 27 | alert(data.message); 28 | }).fail(function(error) { 29 | alert(JSON.stringify(error)); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /lib/twilioClient.js: -------------------------------------------------------------------------------- 1 | var twilio = require('twilio'); 2 | var VoiceResponse = twilio.twiml.VoiceResponse; 3 | var config = require('../config'); 4 | var client = twilio(config.accountSid, config.authToken); 5 | 6 | module.exports = { 7 | createCall: (salesNumber, phoneNumber, headersHost, twilioClient = client) => { 8 | let url = headersHost + '/outbound/' + encodeURIComponent(salesNumber); 9 | let options = { 10 | to: phoneNumber, 11 | from: config.twilioNumber, 12 | url: url, 13 | }; 14 | 15 | return twilioClient.calls.create(options) 16 | .then((message) => { 17 | console.log(message.responseText); 18 | return Promise.resolve('Thank you! We will be calling you shortly.') 19 | }) 20 | .catch((error) => { 21 | console.log(error); 22 | return Promise.reject(error); 23 | }); 24 | }, 25 | voiceResponse: (salesNumber, Voice = VoiceResponse) => { 26 | let twimlResponse = new Voice(); 27 | 28 | twimlResponse.say('Thanks for contacting our sales department. Our ' + 29 | 'next available representative will take your call. ', 30 | { voice: 'alice' }); 31 | twimlResponse.dial(salesNumber); 32 | 33 | return twimlResponse.toString(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Click To Call: Twilio & Node", 3 | "description": "An example of implementing click to call functionality in a Node.js application", 4 | "keywords": [ 5 | "node.js", 6 | "API", 7 | "twilio", 8 | "expressjs", 9 | "Express", 10 | "Tutorial", 11 | "Telephone API", 12 | "Voice API", 13 | "REST API", 14 | "Demo" 15 | ], 16 | "website": "https://twilio.com/docs/howto/click-to-call-walkthrough", 17 | "repository": "https://github.com/TwilioDevEd/clicktocall-node", 18 | "logo": "https://s3-us-west-2.amazonaws.com/deved/twilio-logo.png", 19 | "success_url": "/landing.html", 20 | "env": { 21 | "TWILIO_ACCOUNT_SID": { 22 | "description": "Your Twilio account secret ID, you can find at: https://www.twilio.com/user/account", 23 | "value": "enter_your_account_sid_here" 24 | }, 25 | "TWILIO_AUTH_TOKEN": { 26 | "description": "Your secret Twilio Auth token, you can find at: https://www.twilio.com/user/account", 27 | "value": "enter_your_auth_token_here" 28 | }, 29 | "TWILIO_NUMBER": { 30 | "description": "The Twilio phone number you are using for this app. You can get one here: https://www.twilio.com/user/account/phone-numbers/incoming", 31 | "value": "+15005550006" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/vendor/intl-phone/css/demo.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | -moz-box-sizing: border-box; } 4 | 5 | body { 6 | margin: 20px; 7 | font-size: 14px; 8 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 9 | color: #555; } 10 | 11 | .hide { 12 | display: none; } 13 | 14 | pre { 15 | margin: 0 !important; 16 | display: inline-block; } 17 | 18 | .token.operator, 19 | .token.entity, 20 | .token.url, 21 | .language-css .token.string, 22 | .style .token.string, 23 | .token.variable { 24 | background: none; } 25 | 26 | input, button { 27 | height: 35px; 28 | margin: 0; 29 | padding: 6px 12px; 30 | border-radius: 2px; 31 | font-family: inherit; 32 | font-size: 100%; 33 | color: inherit; } 34 | input[disabled], button[disabled] { 35 | background-color: #eee; } 36 | 37 | input, select { 38 | border: 1px solid #CCC; 39 | width: 250px; } 40 | 41 | ::-webkit-input-placeholder { 42 | color: #BBB; } 43 | 44 | ::-moz-placeholder { 45 | /* Firefox 19+ */ 46 | color: #BBB; 47 | opacity: 1; } 48 | 49 | :-ms-input-placeholder { 50 | color: #BBB; } 51 | 52 | button { 53 | color: #FFF; 54 | background-color: #428BCA; 55 | border: 1px solid #357EBD; } 56 | button:hover { 57 | background-color: #3276B1; 58 | border-color: #285E8E; 59 | cursor: pointer; } 60 | 61 | #result { 62 | margin-bottom: 100px; } 63 | -------------------------------------------------------------------------------- /spec/twilioClient.spec.js: -------------------------------------------------------------------------------- 1 | const twilioClient = require('../lib/twilioClient'); 2 | 3 | class FakeVoiceResponse { 4 | constructor(){} 5 | 6 | say(){ return 'say-function' } 7 | dial(){ return 'dial-function' } 8 | toString(){ return 'fake response voice'; } 9 | } 10 | 11 | describe('twilio client', () => { 12 | describe('createCall', () => { 13 | describe('when success', () => { 14 | test('resolves with a thank you message', async() => { 15 | const fakeTwilio = { 16 | calls: { 17 | create: function(){ 18 | return Promise.resolve({responseText: 'message'}); 19 | } 20 | } 21 | } 22 | 23 | let result = await twilioClient.createCall('sales-number', 'phone-number', 'http://localhost:1234', fakeTwilio); 24 | expect(result).toEqual('Thank you! We will be calling you shortly.'); 25 | }); 26 | }); 27 | 28 | describe('when fails', () => { 29 | test('rejects with the error', async() => { 30 | const fakeTwilio = { 31 | calls: { 32 | create: function(){ 33 | return Promise.reject({error: 'some error'}); 34 | } 35 | } 36 | } 37 | 38 | try { 39 | await twilioClient.createCall('sales-number', 'phone-number', 'http://localhost:1234', fakeTwilio); 40 | }catch(error){ 41 | expect(error).toEqual({ error: 'some error' }); 42 | } 43 | }); 44 | }); 45 | }); 46 | 47 | describe('voiceResponse', () => { 48 | test('returns a string', () => { 49 | let result = twilioClient.voiceResponse('sales-number', FakeVoiceResponse); 50 | expect(result).toEqual('fake response voice'); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Click To Call Tutorial 5 | 6 | // We use Twitter Bootstrap as the default styling for our page 7 | link(rel='stylesheet', 8 | href='//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css') 9 | link(rel='stylesheet', 10 | href='//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css') 11 | 12 | // Include CSS for our third-party telephone input jQuery plugin 13 | link(rel='stylesheet', href='/vendor/intl-phone/css/intlTelInput.css') 14 | body 15 | .container 16 | h1 Click To Call 17 | p. 18 | Click To Call converts your website's users into engaged customers by 19 | creating an easy way for your customers to contact your sales and 20 | support teams right on your website. 21 | 22 | p Here's an example of how it's done! 23 | 24 | hr 25 | 26 | // C2C contact form 27 | .row 28 | .col-md-12 29 | form(id='contactForm', role='form') 30 | .form-group 31 | h3 Call Sales 32 | p.help-block. 33 | Are you interested in impressing your friends and confounding 34 | your enemies? Enter your phone number below, and our team will 35 | contact you right away. 36 | label Your Number 37 | .form-group 38 | input.form-control(type='text', id='phoneNumber', 39 | placeholder='(651) 555-7889') 40 | label Sales Team Number 41 | .form-group 42 | input.form-control(type='text', id='salesNumber', 43 | placeholder='(651) 555-7889') 44 | button(type='submit', class='btn btn-default') Contact Sales 45 | 46 | // Include page dependencies 47 | script(src='//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js') 48 | script(src='/vendor/intl-phone/js/intlTelInput.min.js') 49 | 50 | // Our app's front-end JavaScript logic 51 | script(src='/app.js') 52 | -------------------------------------------------------------------------------- /spec/index.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const server = require('../server'); 4 | const agent = request(server); 5 | 6 | var mockTwilioClient = require('../lib/twilioClient'); 7 | 8 | mockTwilioClient.createCall = jest.fn(); 9 | mockTwilioClient.voiceResponse = jest.fn(); 10 | 11 | describe('index routes', () => { 12 | describe('GET /', () => { 13 | describe('when success', () => { 14 | test('renders home page', async() => { 15 | let result = await agent.get('/'); 16 | expect(result.text).toContain('Click To Call'); 17 | expect(result.text).toContain('Call Sales'); 18 | expect(result.text).toContain(''); 19 | }); 20 | }); 21 | }); 22 | 23 | describe('POST /call', () => { 24 | describe('when success', () => { 25 | test('responds with 200', async() => { 26 | mockTwilioClient.createCall.mockImplementation(() => Promise.resolve('some message')); 27 | 28 | let result = await agent.post('/call') 29 | .send({salesNumber: '+155', phoneNumber: '+123'}); 30 | 31 | expect(result.statusCode).toEqual(200); 32 | expect(result.text).toEqual("{\"message\":\"some message\"}"); 33 | }); 34 | }); 35 | 36 | describe('when fails', () => { 37 | test('responds with 500', async() => { 38 | mockTwilioClient.createCall.mockImplementation(() => Promise.reject({error: 'some err'})); 39 | 40 | let result = await agent 41 | .post('/call') 42 | .send({salesNumber: '+155', phoneNumber: '+123'}); 43 | 44 | expect(result.statusCode).toEqual(500); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('POST /outbound/:salesNumber', () => { 50 | test('responds with a string', async() => { 51 | mockTwilioClient.voiceResponse.mockImplementation(() => 'VoiceResponse'); 52 | 53 | let result = await agent.post('/outbound/123456'); 54 | 55 | expect(result.statusCode).toEqual(200); 56 | expect(result.text).toEqual('VoiceResponse'); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const morgan = require('morgan'); 4 | const bodyParser = require('body-parser'); 5 | 6 | const twilioClient = require('../lib/twilioClient'); 7 | 8 | module.exports = function(app) { 9 | // Set Pug as the default template engine 10 | app.set('view engine', 'pug'); 11 | 12 | // Express static file middleware - serves up JS, CSS, and images from the 13 | // "public" directory where we started our webapp process 14 | app.use(express.static(path.join(process.cwd(), 'public'))); 15 | 16 | // Parse incoming request bodies as form-encoded 17 | app.use(bodyParser.json({})); 18 | app.use(bodyParser.urlencoded({ 19 | extended: true 20 | })); 21 | 22 | // Use morgan for HTTP request logging 23 | app.use(morgan('combined')); 24 | 25 | // Home Page with Click to Call 26 | app.get('/', function(request, response) { 27 | response.render('index'); 28 | }); 29 | 30 | // Handle an AJAX POST request to place an outbound call 31 | app.post('/call', function(request, response) { 32 | let salesNumber = request.body.salesNumber; 33 | let phoneNumber = request.body.phoneNumber; 34 | 35 | // This should be the publicly accessible URL for your application 36 | // Here, we just use the host for the application making the request, 37 | // but you can hard code it or use something different if need be 38 | // For local development purposes remember to use ngrok and replace the headerHost 39 | let headersHost = 'http://' + request.headers.host; 40 | 41 | twilioClient.createCall(salesNumber, phoneNumber, headersHost) 42 | .then((result) => { 43 | response.send({message: result}); 44 | }) 45 | .catch((error) => { 46 | response.status(500).send(error); 47 | }); 48 | }); 49 | 50 | // Return TwiML instructions for the outbound call 51 | app.post('/outbound/:salesNumber', function(request, response) { 52 | let result = twilioClient.voiceResponse(request.params.salesNumber); 53 | response.send(result); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /public/vendor/intl-phone/libphonenumber/src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Follow instructions here to compile this file: 3 | * https://code.google.com/p/libphonenumber/source/browse/trunk/javascript/README 4 | * 5 | * (start by copying the contents of this file into 6 | * libphonenumber/javascript/i18n/phonenumbers/demo.js) 7 | */ 8 | 9 | // includes 10 | goog.require('i18n.phonenumbers.PhoneNumberFormat'); 11 | goog.require('i18n.phonenumbers.PhoneNumberUtil'); 12 | goog.require('i18n.phonenumbers.PhoneNumberUtil.ValidationResult'); 13 | goog.require('i18n.phonenumbers.AsYouTypeFormatter'); 14 | 15 | function getExampleNumber(countryCode) { 16 | var phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance(); 17 | var number = phoneUtil.getExampleNumber(countryCode); 18 | return phoneUtil.format(number, i18n.phonenumbers.PhoneNumberFormat.INTERNATIONAL); 19 | } 20 | 21 | function isValidNumber(number, countryCode) { 22 | try { 23 | var phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance(); 24 | var numberObj = phoneUtil.parseAndKeepRawInput(number, countryCode); 25 | return phoneUtil.isValidNumber(numberObj); 26 | } catch (e) { 27 | return false; 28 | } 29 | } 30 | 31 | function formatNumber(val, addSuffix) { 32 | try { 33 | // ignore numbers if they dont start with an intl dial code 34 | if (val.substr(0, 1) != "+") { 35 | return val; 36 | } 37 | var clean = "+" + val.replace(/\D/g, ""); 38 | var formatter = new i18n.phonenumbers.AsYouTypeFormatter(""); 39 | var result; 40 | for (var i = 0; i < clean.length; i++) { 41 | result = formatter.inputDigit(clean.charAt(i)); 42 | } 43 | // for some reason libphonenumber formats "+44" to "+44 ", but doesn't do the same with "+1" 44 | if (result.charAt(result.length - 1) == " ") { 45 | result = result.substr(0, result.length - 1); 46 | } 47 | if (addSuffix) { 48 | // hack to get formatting suffix 49 | var test = formatter.inputDigit('5'); 50 | // if the number still contains formatting (there's always a space after the dial code) then we're good to go (else the number is too long and libphonenumber removed all the formatting) 51 | if (test.indexOf(' ') !== -1) { 52 | // update the result to the new value (minus that last '5' we just added) 53 | result = test.substr(0, test.length - 1); 54 | } 55 | } 56 | return result; 57 | } catch (e) { 58 | return val; 59 | } 60 | } 61 | 62 | // exports 63 | goog.exportSymbol('intlTelInputUtils', {}); 64 | goog.exportSymbol('intlTelInputUtils.formatNumber', formatNumber); 65 | goog.exportSymbol('intlTelInputUtils.isValidNumber', isValidNumber); 66 | goog.exportSymbol('intlTelInputUtils.getExampleNumber', getExampleNumber); 67 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-source@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Twilio 4 | 5 | 6 | 7 | # Click to Call - Node.js 8 | 9 | > This repository is archived and no longer maintained. Check out the [Twilio Voice](https://www.twilio.com/docs/voice/) docs for links to other tutorials. 10 | 11 | ## Set up 12 | 13 | ### Requirements 14 | 15 | - [Nodejs](https://nodejs.org/) v10 or v12 16 | 17 | ### Twilio Account Settings 18 | 19 | This application should give you a ready-made starting point for writing your own application. 20 | Before we begin, we need to collect all the config values we need to run the application: 21 | 22 | | Config Value | Description | 23 | | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | 24 | | TWILIO_ACCOUNT_SID | Your primary Twilio account identifier - find this [in the Console](https://www.twilio.com/console/project/settings). | 25 | | TWILIO_AUTH_TOKEN | Used to authenticate - just like the above, you'll find this [here](https://www.twilio.com/console/project/settings). | 26 | | TWILIO_NUMBER | A Twilio phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) - you can [get one here](https://www.twilio.com/console/phone-numbers/incoming) | 27 | 28 | ### Local Development 29 | 30 | 1. First clone this repository and `cd` into it. 31 | 32 | ```bash 33 | git clone https://github.com/TwilioDevEd/clicktocall-node.git 34 | cd clicktocall-node 35 | ``` 36 | 37 | 2. Install the dependencies. 38 | 39 | ```bash 40 | npm install 41 | ``` 42 | 43 | 3. Copy the sample configuration file and edit it to match your configuration. 44 | 45 | ```bash 46 | cp .env.example .env 47 | ``` 48 | 49 | See [Twilio Account Settings](#twilio-account-settings) to locate the necessary environment variables. 50 | 51 | 4. Launch local development web server, will run on port 3000. 52 | 53 | ```bash 54 | npm start 55 | ``` 56 | 57 | 5. For Twilio to be able to talk to your application, you'll need a way to make your server publicly available to the internet. For local testing purposes, we recommend using [ngrok](http://ngrok.io/). Ngrok provides secure introspectable tunnels to localhost webhook development: for more information and instructions on setting up ngrok to work with your application, check out [this section of the Click to Call tutorial](https://www.twilio.com/docs/voice/tutorials/click-to-call-node-express#testing-your-app-locally). 58 | 59 | ```bash 60 | ngrok http 3000 61 | ``` 62 | 63 | 6. Finally, open up your browser and go to your ngrok URL. It will look something like this: `http://.ngrok.io` 64 | 65 | That's it! 66 | 67 | ### Docker 68 | 69 | If you have [Docker](https://www.docker.com/) already installed on your machine, you can use our `docker-compose.yml` to setup your project. 70 | 71 | 1. Make sure you have the project cloned. 72 | 2. Setup the `.env` file as outlined in the [Local Development](#local-development) steps. 73 | 3. Run `docker-compose up`. 74 | 4. Follow the steps in [Local Development](#local-development) on how to expose your port to Twilio using a tool like [ngrok](https://ngrok.com/) and configure the remaining parts of your application. 75 | 76 | ### Tests 77 | 78 | To execute tests, run the following command in the project directory: 79 | 80 | ```bash 81 | npm test 82 | ``` 83 | 84 | ## Resources 85 | 86 | - The CodeExchange repository can be found [here](https://github.com/twilio-labs/code-exchange/). 87 | 88 | ## License 89 | 90 | [MIT](http://www.opensource.org/licenses/mit-license.html) 91 | 92 | ## Disclaimer 93 | 94 | No warranty expressed or implied. Software is as is. 95 | 96 | [twilio]: https://www.twilio.com 97 | -------------------------------------------------------------------------------- /public/vendor/intl-phone/css/intlTelInput.css: -------------------------------------------------------------------------------- 1 | .intl-tel-input .flag{width:16px;height:11px;background:url("../img/flags.png")}.intl-tel-input .ad{background-position:-16px 0}.intl-tel-input .ae{background-position:-32px 0}.intl-tel-input .af{background-position:-48px 0}.intl-tel-input .ag{background-position:-64px 0}.intl-tel-input .ai{background-position:-80px 0}.intl-tel-input .al{background-position:-96px 0}.intl-tel-input .am{background-position:-112px 0}.intl-tel-input .ao{background-position:-128px 0}.intl-tel-input .ar{background-position:-144px 0}.intl-tel-input .as{background-position:-160px 0}.intl-tel-input .at{background-position:-176px 0}.intl-tel-input .au{background-position:-192px 0}.intl-tel-input .aw{background-position:-208px 0}.intl-tel-input .az{background-position:-224px 0}.intl-tel-input .ba{background-position:-240px 0}.intl-tel-input .bb{background-position:0 -11px}.intl-tel-input .bd{background-position:-16px -11px}.intl-tel-input .be{background-position:-32px -11px}.intl-tel-input .bf{background-position:-48px -11px}.intl-tel-input .bg{background-position:-64px -11px}.intl-tel-input .bh{background-position:-80px -11px}.intl-tel-input .bi{background-position:-96px -11px}.intl-tel-input .bj{background-position:-112px -11px}.intl-tel-input .bm{background-position:-128px -11px}.intl-tel-input .bn{background-position:-144px -11px}.intl-tel-input .bo{background-position:-160px -11px}.intl-tel-input .br{background-position:-176px -11px}.intl-tel-input .bs{background-position:-192px -11px}.intl-tel-input .bt{background-position:-208px -11px}.intl-tel-input .bw{background-position:-224px -11px}.intl-tel-input .by{background-position:-240px -11px}.intl-tel-input .bz{background-position:0 -22px}.intl-tel-input .ca{background-position:-16px -22px}.intl-tel-input .cd{background-position:-32px -22px}.intl-tel-input .cf{background-position:-48px -22px}.intl-tel-input .cg{background-position:-64px -22px}.intl-tel-input .ch{background-position:-80px -22px}.intl-tel-input .ci{background-position:-96px -22px}.intl-tel-input .ck{background-position:-112px -22px}.intl-tel-input .cl{background-position:-128px -22px}.intl-tel-input .cm{background-position:-144px -22px}.intl-tel-input .cn{background-position:-160px -22px}.intl-tel-input .co{background-position:-176px -22px}.intl-tel-input .cr{background-position:-192px -22px}.intl-tel-input .cu{background-position:-208px -22px}.intl-tel-input .cv{background-position:-224px -22px}.intl-tel-input .cw{background-position:-240px -22px}.intl-tel-input .cy{background-position:0 -33px}.intl-tel-input .cz{background-position:-16px -33px}.intl-tel-input .de{background-position:-32px -33px}.intl-tel-input .dj{background-position:-48px -33px}.intl-tel-input .dk{background-position:-64px -33px}.intl-tel-input .dm{background-position:-80px -33px}.intl-tel-input .do{background-position:-96px -33px}.intl-tel-input .dz{background-position:-112px -33px}.intl-tel-input .ec{background-position:-128px -33px}.intl-tel-input .ee{background-position:-144px -33px}.intl-tel-input .eg{background-position:-160px -33px}.intl-tel-input .er{background-position:-176px -33px}.intl-tel-input .es{background-position:-192px -33px}.intl-tel-input .et{background-position:-208px -33px}.intl-tel-input .fi{background-position:-224px -33px}.intl-tel-input .fj{background-position:-240px -33px}.intl-tel-input .fk{background-position:0 -44px}.intl-tel-input .fm{background-position:-16px -44px}.intl-tel-input .fo{background-position:-32px -44px}.intl-tel-input .fr,.intl-tel-input .bl,.intl-tel-input .mf{background-position:-48px -44px}.intl-tel-input .ga{background-position:-64px -44px}.intl-tel-input .gb{background-position:-80px -44px}.intl-tel-input .gd{background-position:-96px -44px}.intl-tel-input .ge{background-position:-112px -44px}.intl-tel-input .gf{background-position:-128px -44px}.intl-tel-input .gh{background-position:-144px -44px}.intl-tel-input .gi{background-position:-160px -44px}.intl-tel-input .gl{background-position:-176px -44px}.intl-tel-input .gm{background-position:-192px -44px}.intl-tel-input .gn{background-position:-208px -44px}.intl-tel-input .gp{background-position:-224px -44px}.intl-tel-input .gq{background-position:-240px -44px}.intl-tel-input .gr{background-position:0 -55px}.intl-tel-input .gt{background-position:-16px -55px}.intl-tel-input .gu{background-position:-32px -55px}.intl-tel-input .gw{background-position:-48px -55px}.intl-tel-input .gy{background-position:-64px -55px}.intl-tel-input .hk{background-position:-80px -55px}.intl-tel-input .hn{background-position:-96px -55px}.intl-tel-input .hr{background-position:-112px -55px}.intl-tel-input .ht{background-position:-128px -55px}.intl-tel-input .hu{background-position:-144px -55px}.intl-tel-input .id{background-position:-160px -55px}.intl-tel-input .ie{background-position:-176px -55px}.intl-tel-input .il{background-position:-192px -55px}.intl-tel-input .in{background-position:-208px -55px}.intl-tel-input .io{background-position:-224px -55px}.intl-tel-input .iq{background-position:-240px -55px}.intl-tel-input .ir{background-position:0 -66px}.intl-tel-input .is{background-position:-16px -66px}.intl-tel-input .it{background-position:-32px -66px}.intl-tel-input .jm{background-position:-48px -66px}.intl-tel-input .jo{background-position:-64px -66px}.intl-tel-input .jp{background-position:-80px -66px}.intl-tel-input .ke{background-position:-96px -66px}.intl-tel-input .kg{background-position:-112px -66px}.intl-tel-input .kh{background-position:-128px -66px}.intl-tel-input .ki{background-position:-144px -66px}.intl-tel-input .km{background-position:-160px -66px}.intl-tel-input .kn{background-position:-176px -66px}.intl-tel-input .kp{background-position:-192px -66px}.intl-tel-input .kr{background-position:-208px -66px}.intl-tel-input .kw{background-position:-224px -66px}.intl-tel-input .ky{background-position:-240px -66px}.intl-tel-input .kz{background-position:0 -77px}.intl-tel-input .la{background-position:-16px -77px}.intl-tel-input .lb{background-position:-32px -77px}.intl-tel-input .lc{background-position:-48px -77px}.intl-tel-input .li{background-position:-64px -77px}.intl-tel-input .lk{background-position:-80px -77px}.intl-tel-input .lr{background-position:-96px -77px}.intl-tel-input .ls{background-position:-112px -77px}.intl-tel-input .lt{background-position:-128px -77px}.intl-tel-input .lu{background-position:-144px -77px}.intl-tel-input .lv{background-position:-160px -77px}.intl-tel-input .ly{background-position:-176px -77px}.intl-tel-input .ma{background-position:-192px -77px}.intl-tel-input .mc{background-position:-208px -77px}.intl-tel-input .md{background-position:-224px -77px}.intl-tel-input .me{background-position:-112px -154px;height:12px}.intl-tel-input .mg{background-position:0 -88px}.intl-tel-input .mh{background-position:-16px -88px}.intl-tel-input .mk{background-position:-32px -88px}.intl-tel-input .ml{background-position:-48px -88px}.intl-tel-input .mm{background-position:-64px -88px}.intl-tel-input .mn{background-position:-80px -88px}.intl-tel-input .mo{background-position:-96px -88px}.intl-tel-input .mp{background-position:-112px -88px}.intl-tel-input .mq{background-position:-128px -88px}.intl-tel-input .mr{background-position:-144px -88px}.intl-tel-input .ms{background-position:-160px -88px}.intl-tel-input .mt{background-position:-176px -88px}.intl-tel-input .mu{background-position:-192px -88px}.intl-tel-input .mv{background-position:-208px -88px}.intl-tel-input .mw{background-position:-224px -88px}.intl-tel-input .mx{background-position:-240px -88px}.intl-tel-input .my{background-position:0 -99px}.intl-tel-input .mz{background-position:-16px -99px}.intl-tel-input .na{background-position:-32px -99px}.intl-tel-input .nc{background-position:-48px -99px}.intl-tel-input .ne{background-position:-64px -99px}.intl-tel-input .nf{background-position:-80px -99px}.intl-tel-input .ng{background-position:-96px -99px}.intl-tel-input .ni{background-position:-112px -99px}.intl-tel-input .nl,.intl-tel-input .bq{background-position:-128px -99px}.intl-tel-input .no{background-position:-144px -99px}.intl-tel-input .np{background-position:-160px -99px}.intl-tel-input .nr{background-position:-176px -99px}.intl-tel-input .nu{background-position:-192px -99px}.intl-tel-input .nz{background-position:-208px -99px}.intl-tel-input .om{background-position:-224px -99px}.intl-tel-input .pa{background-position:-240px -99px}.intl-tel-input .pe{background-position:0 -110px}.intl-tel-input .pf{background-position:-16px -110px}.intl-tel-input .pg{background-position:-32px -110px}.intl-tel-input .ph{background-position:-48px -110px}.intl-tel-input .pk{background-position:-64px -110px}.intl-tel-input .pl{background-position:-80px -110px}.intl-tel-input .pm{background-position:-96px -110px}.intl-tel-input .pr{background-position:-112px -110px}.intl-tel-input .ps{background-position:-128px -110px}.intl-tel-input .pt{background-position:-144px -110px}.intl-tel-input .pw{background-position:-160px -110px}.intl-tel-input .py{background-position:-176px -110px}.intl-tel-input .qa{background-position:-192px -110px}.intl-tel-input .re{background-position:-208px -110px}.intl-tel-input .ro{background-position:-224px -110px}.intl-tel-input .rs{background-position:-240px -110px}.intl-tel-input .ru{background-position:0 -121px}.intl-tel-input .rw{background-position:-16px -121px}.intl-tel-input .sa{background-position:-32px -121px}.intl-tel-input .sb{background-position:-48px -121px}.intl-tel-input .sc{background-position:-64px -121px}.intl-tel-input .sd{background-position:-80px -121px}.intl-tel-input .se{background-position:-96px -121px}.intl-tel-input .sg{background-position:-112px -121px}.intl-tel-input .sh{background-position:-128px -121px}.intl-tel-input .si{background-position:-144px -121px}.intl-tel-input .sk{background-position:-160px -121px}.intl-tel-input .sl{background-position:-176px -121px}.intl-tel-input .sm{background-position:-192px -121px}.intl-tel-input .sn{background-position:-208px -121px}.intl-tel-input .so{background-position:-224px -121px}.intl-tel-input .sr{background-position:-240px -121px}.intl-tel-input .ss{background-position:0 -132px}.intl-tel-input .st{background-position:-16px -132px}.intl-tel-input .sv{background-position:-32px -132px}.intl-tel-input .sx{background-position:-48px -132px}.intl-tel-input .sy{background-position:-64px -132px}.intl-tel-input .sz{background-position:-80px -132px}.intl-tel-input .tc{background-position:-96px -132px}.intl-tel-input .td{background-position:-112px -132px}.intl-tel-input .tg{background-position:-128px -132px}.intl-tel-input .th{background-position:-144px -132px}.intl-tel-input .tj{background-position:-160px -132px}.intl-tel-input .tk{background-position:-176px -132px}.intl-tel-input .tl{background-position:-192px -132px}.intl-tel-input .tm{background-position:-208px -132px}.intl-tel-input .tn{background-position:-224px -132px}.intl-tel-input .to{background-position:-240px -132px}.intl-tel-input .tr{background-position:0 -143px}.intl-tel-input .tt{background-position:-16px -143px}.intl-tel-input .tv{background-position:-32px -143px}.intl-tel-input .tw{background-position:-48px -143px}.intl-tel-input .tz{background-position:-64px -143px}.intl-tel-input .ua{background-position:-80px -143px}.intl-tel-input .ug{background-position:-96px -143px}.intl-tel-input .us{background-position:-112px -143px}.intl-tel-input .uy{background-position:-128px -143px}.intl-tel-input .uz{background-position:-144px -143px}.intl-tel-input .va{background-position:-160px -143px}.intl-tel-input .vc{background-position:-176px -143px}.intl-tel-input .ve{background-position:-192px -143px}.intl-tel-input .vg{background-position:-208px -143px}.intl-tel-input .vi{background-position:-224px -143px}.intl-tel-input .vn{background-position:-240px -143px}.intl-tel-input .vu{background-position:0 -154px}.intl-tel-input .wf{background-position:-16px -154px}.intl-tel-input .ws{background-position:-32px -154px}.intl-tel-input .ye{background-position:-48px -154px}.intl-tel-input .za{background-position:-64px -154px}.intl-tel-input .zm{background-position:-80px -154px}.intl-tel-input .zw{background-position:-96px -154px}.intl-tel-input{position:relative;display:inline-block}.intl-tel-input *{box-sizing:border-box;-moz-box-sizing:border-box}.intl-tel-input .hide{display:none}.intl-tel-input .v-hide{visibility:hidden}.intl-tel-input input[type=text],.intl-tel-input input[type=tel]{position:relative;z-index:0;margin-top:0 !important;margin-bottom:0 !important;padding-left:44px;margin-left:0}.intl-tel-input .flag-dropdown{position:absolute;top:0;bottom:0;padding:1px}.intl-tel-input .flag-dropdown:hover{cursor:pointer}.intl-tel-input .flag-dropdown:hover .selected-flag{background-color:rgba(0,0,0,0.05)}.intl-tel-input input[disabled]+.flag-dropdown:hover{cursor:default}.intl-tel-input input[disabled]+.flag-dropdown:hover .selected-flag{background-color:transparent}.intl-tel-input .selected-flag{z-index:1;position:relative;width:38px;height:100%;padding:0 0 0 8px}.intl-tel-input .selected-flag .flag{position:absolute;top:50%;margin-top:-5px}.intl-tel-input .selected-flag .arrow{position:relative;top:50%;margin-top:-2px;left:20px;width:0;height:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:4px solid #555}.intl-tel-input .selected-flag .arrow.up{border-top:none;border-bottom:4px solid #555}.intl-tel-input .country-list{list-style:none;position:absolute;z-index:2;padding:0;margin:0 0 0 -1px;box-shadow:1px 1px 4px rgba(0,0,0,0.2);background-color:white;border:1px solid #ccc;width:430px;max-height:200px;overflow-y:scroll}.intl-tel-input .country-list .flag{display:inline-block}.intl-tel-input .country-list .divider{padding-bottom:5px;margin-bottom:5px;border-bottom:1px solid #ccc}.intl-tel-input .country-list .country{padding:5px 10px}.intl-tel-input .country-list .country .dial-code{color:#999}.intl-tel-input .country-list .country.highlight{background-color:rgba(0,0,0,0.05)}.intl-tel-input .country-list .flag,.intl-tel-input .country-list .country-name{margin-right:6px} 2 | -------------------------------------------------------------------------------- /public/vendor/intl-phone/js/intlTelInput.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | International Telephone Input v2.0.10 3 | https://github.com/Bluefieldscom/intl-tel-input.git 4 | */ 5 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],function(b){a(b,window,document)}):a(jQuery,window,document)}(function(a,b,c,d){"use strict";function e(b,c){this.element=b,this.options=a.extend({},h,c),this._defaults=h,this.ns="."+f+g++,this.isGoodBrowser=Boolean(b.setSelectionRange),this._name=f,this.init()}var f="intlTelInput",g=1,h={autoFormat:!1,autoHideDialCode:!0,defaultCountry:"",nationalMode:!1,onlyCountries:[],preferredCountries:["us","gb"],responsiveDropdown:!1,utilsScript:""},i={UP:38,DOWN:40,ENTER:13,ESC:27,PLUS:43,A:65,Z:90,ZERO:48,NINE:57,SPACE:32,BSPACE:8,DEL:46,CTRL:17,CMD1:91,CMD2:224},j=!1;a(b).load(function(){j=!0}),e.prototype={init:function(){this.options.nationalMode&&(this.options.autoFormat=this.options.autoHideDialCode=!1),navigator.userAgent.match(/Android/i)&&navigator.userAgent.match(/Chrome/i)&&(this.options.autoFormat=!1),this._processCountryData(),this._generateMarkup(),this._setInitialState(),this._initListeners()},_processCountryData:function(){this._setInstanceCountryData(),this._setPreferredCountries()},_setInstanceCountryData:function(){var b=this;if(this.options.onlyCountries.length){var c,d,e=[],f={};for(d=0;d1){var i=[];for(d=0;d",{"class":"intl-tel-input"}));var b=a("
",{"class":"flag-dropdown"}).insertAfter(this.telInput),c=a("
",{"class":"selected-flag"}).appendTo(b);this.selectedFlagInner=a("
",{"class":"flag"}).appendTo(c),a("
",{"class":"arrow"}).appendTo(this.selectedFlagInner),this.countryList=a("
    ",{"class":"country-list v-hide"}).appendTo(b),this.preferredCountries.length&&(this._appendListItems(this.preferredCountries,"preferred"),a("
  • ",{"class":"divider"}).appendTo(this.countryList)),this._appendListItems(this.countries,""),this.dropdownHeight=this.countryList.outerHeight(),this.countryList.removeClass("v-hide").addClass("hide"),this.options.responsiveDropdown&&this.countryList.outerWidth(this.telInput.outerWidth()),this.countryListItems=this.countryList.children(".country")},_appendListItems:function(a,b){for(var c="",d=0;d",c+="
    ",c+=""+e.name+"",c+="+"+e.dialCode+"",c+="
  • "}this.countryList.append(c)},_setInitialState:function(){var a=this.telInput.val();if(!a||!this.setNumber(a)){var b;b=this.options.defaultCountry?this._getCountryData(this.options.defaultCountry,!1,!1):this.preferredCountries.length?this.preferredCountries[0]:this.countries[0],this._selectFlag(b.iso2),a||this.options.autoHideDialCode||this._resetToDialCode(b.dialCode)}},_initListeners:function(){var c=this;this.options.autoHideDialCode&&this._initAutoHideDialCode();var d=this.telInput.closest("label");d.length&&d.on("click"+this.ns,function(a){c.countryList.hasClass("hide")?c.telInput.focus():a.preventDefault()}),this.options.autoFormat&&this.telInput.on("keypress"+this.ns,function(a){if(a.which>=i.SPACE){a.preventDefault();var b=a.which>=i.ZERO&&a.which<=i.NINE,d=c.telInput[0],e=c.isGoodBrowser&&d.selectionStart==d.selectionEnd;if(b||e){var f=b?String.fromCharCode(a.which):null;c._handleInputKey(f,!0)}}}),this.telInput.on("keyup"+this.ns,function(a){if(c.options.autoFormat){var b=a.which==i.CTRL||a.which==i.CMD1||a.which==i.CMD2,d=c.telInput[0],e=c.isGoodBrowser&&d.selectionStart==d.selectionEnd,f=c.isGoodBrowser&&d.selectionStart==c.telInput.val().length;if(a.which==i.DEL||a.which==i.BSPACE||b&&e){var g=!(a.which==i.BSPACE&&f);c._handleInputKey(null,g)}var h=c.telInput.val();if("+"!=h.substr(0,1)){var j=c.isGoodBrowser?d.selectionStart+1:0;c.telInput.val("+"+h),c.isGoodBrowser&&d.setSelectionRange(j,j)}}else c._updateFlag()});var e=this.selectedFlagInner.parent();if(e.on("click"+this.ns,function(){c.countryList.hasClass("hide")&&!c.telInput.prop("disabled")&&c._showDropdown()}),this.options.utilsScript&&!a.fn[f].injectedUtilsScript){a.fn[f].injectedUtilsScript=!0;var g=function(){a.getScript(c.options.utilsScript,function(){a(".intl-tel-input input").intlTelInput("utilsLoaded")})};j?g():a(b).load(g)}},_handleInputKey:function(a,b){var c=this.telInput.val(),d=null,e=!1,f=this.telInput[0];if(this.isGoodBrowser){var g=f.selectionEnd,h=c.length;e=g==h,a?(c=c.substring(0,f.selectionStart)+a+c.substring(g,h),e||(d=g+(c.length-h))):d=f.selectionStart}else a&&(c+=a);this.setNumber(c,b),this.isGoodBrowser&&(e&&(d=this.telInput.val().length),f.setSelectionRange(d,d))},_initAutoHideDialCode:function(){var a=this;this.telInput.on("mousedown"+this.ns,function(b){a.telInput.is(":focus")||a.telInput.val()||(b.preventDefault(),a.telInput.focus())}),this.telInput.on("focus"+this.ns,function(){a.telInput.val()||(a._updateVal("+"+a.selectedCountryData.dialCode,!0),a.telInput.one("keypress.plus"+a.ns,function(b){b.which==i.PLUS&&a.telInput.val("+")}),setTimeout(function(){a._cursorToEnd()}))}),this.telInput.on("blur"+this.ns,function(){var b=a.telInput.val(),c="+"==b.substr(0,1);if(c){var d=b.replace(/\D/g,"");d&&a.selectedCountryData.dialCode!=d||a.telInput.val("")}a.telInput.off("keypress.plus"+a.ns)})},_cursorToEnd:function(){var a=this.telInput[0];if(this.isGoodBrowser){var b=this.telInput.val().length;a.setSelectionRange(b,b)}},_showDropdown:function(){this._setDropdownPosition();var a=this.countryList.children(".active");this._highlightListItem(a),this.countryList.removeClass("hide"),this._scrollTo(a),this._bindDropdownListeners(),this.selectedFlagInner.children(".arrow").addClass("up")},_setDropdownPosition:function(){var c=this.telInput.offset().top,d=a(b).scrollTop(),e=c+this.telInput.outerHeight()+this.dropdownHeightd,g=!e&&f?"-"+(this.dropdownHeight-1)+"px":"";this.countryList.css("top",g)},_bindDropdownListeners:function(){var b=this;this.countryList.on("mouseover"+this.ns,".country",function(){b._highlightListItem(a(this))}),this.countryList.on("click"+this.ns,".country",function(){b._selectListItem(a(this))});var d=!0;a("html").on("click"+this.ns,function(){d||b._closeDropdown(),d=!1});var e="",f=null;a(c).on("keydown"+this.ns,function(a){a.preventDefault(),a.which==i.UP||a.which==i.DOWN?b._handleUpDownKey(a.which):a.which==i.ENTER?b._handleEnterKey():a.which==i.ESC?b._closeDropdown():(a.which>=i.A&&a.which<=i.Z||a.which==i.SPACE)&&(f&&clearTimeout(f),e+=String.fromCharCode(a.which),b._searchForCountry(e),f=setTimeout(function(){e=""},1e3))})},_handleUpDownKey:function(a){var b=this.countryList.children(".highlight").first(),c=a==i.UP?b.prev():b.next();c.length&&(c.hasClass("divider")&&(c=a==i.UP?c.prev():c.next()),this._highlightListItem(c),this._scrollTo(c))},_handleEnterKey:function(){var a=this.countryList.children(".highlight").first();a.length&&this._selectListItem(a)},_searchForCountry:function(a){for(var b=0;bh)b&&(j-=k),c.scrollTop(j);else if(i>f){b&&(j+=k);var l=d-g;c.scrollTop(j-l)}},_updateDialCode:function(b){var c,d=this.telInput.val(),e=this._getDialCode();if(e.length>1)c=d.replace(e,b);else{var f=d&&"+"!=d.substr(0,1)?a.trim(d):"";c=b+f}this._updateVal(c,!0)},_getDialCode:function(b){var c="",d=b||this.telInput.val();if("+"==d.charAt(0))for(var e="",f=0;f 2 | 3 | 4 | 5 | 6 | 7 | Click-to-Call Tutorial 8 | 75 | 76 | 77 | 78 |

    Click-to-Call

    79 |
    80 |

    81 | Congratulations! You've just deployed the Click-to-Call demo application. 83 | Let us know if you have 84 | any questions. 85 |

    86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
    View Demo!
    Source Code
    Back To Tutorial
    97 | 100 | 101 | 102 | 103 |
    104 | 105 | 106 | 110 | 111 | 112 | 124 | 125 | -------------------------------------------------------------------------------- /public/vendor/intl-phone/js/intlTelInput.js: -------------------------------------------------------------------------------- 1 | /* 2 | International Telephone Input v2.0.10 3 | https://github.com/Bluefieldscom/intl-tel-input.git 4 | */ 5 | // wrap in UMD - see https://github.com/umdjs/umd/blob/master/jqueryPlugin.js 6 | (function(factory) { 7 | if (typeof define === "function" && define.amd) { 8 | define([ "jquery" ], function($) { 9 | factory($, window, document); 10 | }); 11 | } else { 12 | factory(jQuery, window, document); 13 | } 14 | })(function($, window, document, undefined) { 15 | "use strict"; 16 | var pluginName = "intlTelInput", id = 1, // give each instance it's own id for namespaced event handling 17 | defaults = { 18 | // automatically format the number according to the selected country 19 | autoFormat: false, 20 | // if there is just a dial code in the input: remove it on blur, and re-add it on focus 21 | autoHideDialCode: true, 22 | // default country 23 | defaultCountry: "", 24 | // don't insert international dial codes 25 | nationalMode: false, 26 | // display only these countries 27 | onlyCountries: [], 28 | // the countries at the top of the list. defaults to united states and united kingdom 29 | preferredCountries: [ "us", "gb" ], 30 | // make the dropdown the same width as the input 31 | responsiveDropdown: false, 32 | // specify the path to the libphonenumber script to enable validation/formatting 33 | utilsScript: "" 34 | }, keys = { 35 | UP: 38, 36 | DOWN: 40, 37 | ENTER: 13, 38 | ESC: 27, 39 | PLUS: 43, 40 | A: 65, 41 | Z: 90, 42 | ZERO: 48, 43 | NINE: 57, 44 | SPACE: 32, 45 | BSPACE: 8, 46 | DEL: 46, 47 | CTRL: 17, 48 | CMD1: 91, 49 | // Chrome 50 | CMD2: 224 51 | }, windowLoaded = false; 52 | // keep track of if the window.load event has fired as impossible to check after the fact 53 | $(window).load(function() { 54 | windowLoaded = true; 55 | }); 56 | function Plugin(element, options) { 57 | this.element = element; 58 | this.options = $.extend({}, defaults, options); 59 | this._defaults = defaults; 60 | // event namespace 61 | this.ns = "." + pluginName + id++; 62 | // Chrome, FF, Safari, IE9+ 63 | this.isGoodBrowser = Boolean(element.setSelectionRange); 64 | this._name = pluginName; 65 | this.init(); 66 | } 67 | Plugin.prototype = { 68 | init: function() { 69 | // if in nationalMode, disable options relating to dial codes 70 | if (this.options.nationalMode) { 71 | this.options.autoFormat = this.options.autoHideDialCode = false; 72 | } 73 | // chrome on android has issues with key events 74 | // backspace issues for inputs with type=text: https://code.google.com/p/chromium/issues/detail?id=184812 75 | // and improper key codes for keyup and keydown: https://code.google.com/p/chromium/issues/detail?id=118639 76 | if (navigator.userAgent.match(/Android/i) && navigator.userAgent.match(/Chrome/i)) { 77 | this.options.autoFormat = false; 78 | } 79 | // process all the data: onlyCounties, preferredCountries, defaultCountry etc 80 | this._processCountryData(); 81 | // generate the markup 82 | this._generateMarkup(); 83 | // set the initial state of the input value and the selected flag 84 | this._setInitialState(); 85 | // start all of the event listeners: autoHideDialCode, input keydown, selectedFlag click 86 | this._initListeners(); 87 | }, 88 | /******************** 89 | * PRIVATE METHODS 90 | ********************/ 91 | // prepare all of the country data, including onlyCountries, preferredCountries and 92 | // defaultCountry options 93 | _processCountryData: function() { 94 | // set the instances country data objects 95 | this._setInstanceCountryData(); 96 | // set the preferredCountries property 97 | this._setPreferredCountries(); 98 | }, 99 | // process onlyCountries array if present 100 | _setInstanceCountryData: function() { 101 | var that = this; 102 | if (this.options.onlyCountries.length) { 103 | var newCountries = [], newCountryCodes = {}, dialCode, i; 104 | for (i = 0; i < this.options.onlyCountries.length; i++) { 105 | var countryCode = this.options.onlyCountries[i], countryData = that._getCountryData(countryCode, true, false); 106 | if (countryData) { 107 | newCountries.push(countryData); 108 | // add this country's dial code to the countryCodes 109 | dialCode = countryData.dialCode; 110 | if (newCountryCodes[dialCode]) { 111 | newCountryCodes[dialCode].push(countryCode); 112 | } else { 113 | newCountryCodes[dialCode] = [ countryCode ]; 114 | } 115 | } 116 | } 117 | // maintain country priority 118 | for (dialCode in newCountryCodes) { 119 | if (newCountryCodes[dialCode].length > 1) { 120 | var sortedCountries = []; 121 | // go through all of the allCountryCodes countries for this dialCode and create a new (ordered) array of values (if they're in the newCountryCodes array) 122 | for (i = 0; i < allCountryCodes[dialCode].length; i++) { 123 | var country = allCountryCodes[dialCode][i]; 124 | if ($.inArray(newCountryCodes[dialCode], country)) { 125 | sortedCountries.push(country); 126 | } 127 | } 128 | newCountryCodes[dialCode] = sortedCountries; 129 | } 130 | } 131 | this.countries = newCountries; 132 | this.countryCodes = newCountryCodes; 133 | } else { 134 | this.countries = allCountries; 135 | this.countryCodes = allCountryCodes; 136 | } 137 | }, 138 | // process preferred countries - iterate through the preferences, 139 | // fetching the country data for each one 140 | _setPreferredCountries: function() { 141 | var that = this; 142 | this.preferredCountries = []; 143 | for (var i = 0; i < this.options.preferredCountries.length; i++) { 144 | var countryCode = this.options.preferredCountries[i], countryData = that._getCountryData(countryCode, false, true); 145 | if (countryData) { 146 | that.preferredCountries.push(countryData); 147 | } 148 | } 149 | }, 150 | // generate all of the markup for the plugin: the selected flag overlay, and the dropdown 151 | _generateMarkup: function() { 152 | // telephone input 153 | this.telInput = $(this.element); 154 | // containers (mostly for positioning) 155 | this.telInput.wrap($("
    ", { 156 | "class": "intl-tel-input" 157 | })); 158 | var flagsContainer = $("
    ", { 159 | "class": "flag-dropdown" 160 | }).insertAfter(this.telInput); 161 | // currently selected flag (displayed to left of input) 162 | var selectedFlag = $("
    ", { 163 | "class": "selected-flag" 164 | }).appendTo(flagsContainer); 165 | this.selectedFlagInner = $("
    ", { 166 | "class": "flag" 167 | }).appendTo(selectedFlag); 168 | // CSS triangle 169 | $("
    ", { 170 | "class": "arrow" 171 | }).appendTo(this.selectedFlagInner); 172 | // country list contains: preferred countries, then divider, then all countries 173 | this.countryList = $("
      ", { 174 | "class": "country-list v-hide" 175 | }).appendTo(flagsContainer); 176 | if (this.preferredCountries.length) { 177 | this._appendListItems(this.preferredCountries, "preferred"); 178 | $("
    • ", { 179 | "class": "divider" 180 | }).appendTo(this.countryList); 181 | } 182 | this._appendListItems(this.countries, ""); 183 | // now we can grab the dropdown height, and hide it properly 184 | this.dropdownHeight = this.countryList.outerHeight(); 185 | this.countryList.removeClass("v-hide").addClass("hide"); 186 | // and set the width 187 | if (this.options.responsiveDropdown) { 188 | this.countryList.outerWidth(this.telInput.outerWidth()); 189 | } 190 | // this is useful in lots of places 191 | this.countryListItems = this.countryList.children(".country"); 192 | }, 193 | // add a country
    • to the countryList
        container 194 | _appendListItems: function(countries, className) { 195 | // we create so many DOM elements, I decided it was faster to build a temp string 196 | // and then add everything to the DOM in one go at the end 197 | var tmp = ""; 198 | // for each country 199 | for (var i = 0; i < countries.length; i++) { 200 | var c = countries[i]; 201 | // open the list item 202 | tmp += "
      • "; 203 | // add the flag 204 | tmp += "
        "; 205 | // and the country name and dial code 206 | tmp += "" + c.name + ""; 207 | tmp += "+" + c.dialCode + ""; 208 | // close the list item 209 | tmp += "
      • "; 210 | } 211 | this.countryList.append(tmp); 212 | }, 213 | // set the initial state of the input value and the selected flag 214 | _setInitialState: function() { 215 | var val = this.telInput.val(); 216 | // if the input is not pre-populated, or if it doesn't contain a valid dial code, fall back to the default country 217 | // Note: calling setNumber will also format the number 218 | if (!val || !this.setNumber(val)) { 219 | // flag is not set, so set to the default country 220 | var defaultCountry; 221 | // check the defaultCountry option, else fall back to the first in the list 222 | if (this.options.defaultCountry) { 223 | defaultCountry = this._getCountryData(this.options.defaultCountry, false, false); 224 | } else { 225 | defaultCountry = this.preferredCountries.length ? this.preferredCountries[0] : this.countries[0]; 226 | } 227 | this._selectFlag(defaultCountry.iso2); 228 | // if autoHideDialCode is disabled, insert the default dial code 229 | if (!val && !this.options.autoHideDialCode) { 230 | this._resetToDialCode(defaultCountry.dialCode); 231 | } 232 | } 233 | }, 234 | // initialise the main event listeners: input keydown, and click selected flag 235 | _initListeners: function() { 236 | var that = this; 237 | // auto hide dial code option 238 | if (this.options.autoHideDialCode) { 239 | this._initAutoHideDialCode(); 240 | } 241 | // hack for input nested inside label: clicking the selected-flag to open the dropdown would then automatically trigger a 2nd click on the input which would close it again 242 | var label = this.telInput.closest("label"); 243 | if (label.length) { 244 | label.on("click" + this.ns, function(e) { 245 | // if the dropdown is closed, then focus the input, else ignore the click 246 | if (that.countryList.hasClass("hide")) { 247 | that.telInput.focus(); 248 | } else { 249 | e.preventDefault(); 250 | } 251 | }); 252 | } 253 | if (this.options.autoFormat) { 254 | // format number and update flag on keypress 255 | // use keypress event as we want to ignore all input except for a select few keys, 256 | // but we dont want to ignore the navigation keys like the arrows etc. 257 | // NOTE: no point in refactoring this to only bind these listeners on focus/blur because then you would need to have those 2 listeners running the whole time anyway... 258 | this.telInput.on("keypress" + this.ns, function(e) { 259 | // 32 is space, and after that it's all chars (not meta/nav keys) 260 | // this fix is needed for Firefox, which triggers keypress event for some meta/nav keys 261 | if (e.which >= keys.SPACE) { 262 | e.preventDefault(); 263 | // allowed keys are now just numeric keys 264 | var isAllowed = e.which >= keys.ZERO && e.which <= keys.NINE, input = that.telInput[0], noSelection = that.isGoodBrowser && input.selectionStart == input.selectionEnd; 265 | // still reformat even if not an allowed key as they could by typing a formatting char, but ignore if there's a selection as doesn't make sense to replace selection with illegal char and then immediately remove it 266 | if (isAllowed || noSelection) { 267 | var newChar = isAllowed ? String.fromCharCode(e.which) : null; 268 | that._handleInputKey(newChar, true); 269 | } 270 | } 271 | }); 272 | } 273 | // handle keyup event 274 | // for autoFormat: we use keyup to catch delete events after the fact 275 | this.telInput.on("keyup" + this.ns, function(e) { 276 | if (that.options.autoFormat) { 277 | var isCtrl = e.which == keys.CTRL || e.which == keys.CMD1 || e.which == keys.CMD2, input = that.telInput[0], noSelection = that.isGoodBrowser && input.selectionStart == input.selectionEnd, cursorAtEnd = that.isGoodBrowser && input.selectionStart == that.telInput.val().length; 278 | // if delete: format with suffix 279 | // if backspace: format (if cursorAtEnd: no suffix) 280 | // if ctrl and no selection (i.e. could be paste): format with suffix 281 | if (e.which == keys.DEL || e.which == keys.BSPACE || isCtrl && noSelection) { 282 | var addSuffix = !(e.which == keys.BSPACE && cursorAtEnd); 283 | that._handleInputKey(null, addSuffix); 284 | } 285 | // prevent deleting the plus 286 | var val = that.telInput.val(); 287 | if (val.substr(0, 1) != "+") { 288 | // newCursorPos is current pos + 1 to account for the plus we are about to add 289 | var newCursorPos = that.isGoodBrowser ? input.selectionStart + 1 : 0; 290 | that.telInput.val("+" + val); 291 | if (that.isGoodBrowser) { 292 | input.setSelectionRange(newCursorPos, newCursorPos); 293 | } 294 | } 295 | } else { 296 | // if no autoFormat, just update flag 297 | that._updateFlag(); 298 | } 299 | }); 300 | // toggle country dropdown on click 301 | var selectedFlag = this.selectedFlagInner.parent(); 302 | selectedFlag.on("click" + this.ns, function(e) { 303 | // only intercept this event if we're opening the dropdown 304 | // else let it bubble up to the top ("click-off-to-close" listener) 305 | // we cannot just stopPropagation as it may be needed to close another instance 306 | if (that.countryList.hasClass("hide") && !that.telInput.prop("disabled")) { 307 | that._showDropdown(); 308 | } 309 | }); 310 | // if the user has specified the path to the utils script 311 | // inject a new script element for it at the end of the body 312 | if (this.options.utilsScript && !$.fn[pluginName].injectedUtilsScript) { 313 | // don't do this twice! 314 | $.fn[pluginName].injectedUtilsScript = true; 315 | var injectUtilsScript = function() { 316 | $.getScript(that.options.utilsScript, function() { 317 | // tell all instances the utils are ready 318 | $(".intl-tel-input input").intlTelInput("utilsLoaded"); 319 | }); 320 | }; 321 | // if the plugin is being initialised after the window.load event has already been fired 322 | if (windowLoaded) { 323 | injectUtilsScript(); 324 | } else { 325 | // wait until the load event so we don't block any other requests e.g. the flags image 326 | $(window).load(injectUtilsScript); 327 | } 328 | } 329 | }, 330 | // when autoFormat is enabled: handle various key events on the input: the 2 main situations are 1) adding a new number character, which will replace any selection, reformat, and try to preserve the cursor position. and 2) reformatting on backspace, or paste event 331 | _handleInputKey: function(newNumericChar, addSuffix) { 332 | var val = this.telInput.val(), newCursor = null, cursorAtEnd = false, // raw DOM element 333 | input = this.telInput[0]; 334 | if (this.isGoodBrowser) { 335 | var selectionEnd = input.selectionEnd, originalLen = val.length; 336 | cursorAtEnd = selectionEnd == originalLen; 337 | // if handling a new number character: insert it in the right place and calculate the new cursor position 338 | if (newNumericChar) { 339 | // replace any selection they may have made with the new char 340 | val = val.substring(0, input.selectionStart) + newNumericChar + val.substring(selectionEnd, originalLen); 341 | // if the cursor was not at the end then calculate it's new pos 342 | if (!cursorAtEnd) { 343 | newCursor = selectionEnd + (val.length - originalLen); 344 | } 345 | } else { 346 | // here we're not handling a new char, we're just doing a re-format, but we still need to maintain the cursor position 347 | newCursor = input.selectionStart; 348 | } 349 | } else if (newNumericChar) { 350 | val += newNumericChar; 351 | } 352 | // update the number and flag 353 | this.setNumber(val, addSuffix); 354 | // update the cursor position 355 | if (this.isGoodBrowser) { 356 | // if it was at the end, keep it there 357 | if (cursorAtEnd) { 358 | newCursor = this.telInput.val().length; 359 | } 360 | input.setSelectionRange(newCursor, newCursor); 361 | } 362 | }, 363 | // on focus: if empty add dial code. on blur: if just dial code, then empty it 364 | _initAutoHideDialCode: function() { 365 | var that = this; 366 | // mousedown decides where the cursor goes, so if we're focusing 367 | // we must preventDefault as we'll be inserting the dial code, 368 | // and we want the cursor to be at the end no matter where they click 369 | this.telInput.on("mousedown" + this.ns, function(e) { 370 | if (!that.telInput.is(":focus") && !that.telInput.val()) { 371 | e.preventDefault(); 372 | // but this also cancels the focus, so we must trigger that manually 373 | that.telInput.focus(); 374 | } 375 | }); 376 | // on focus: if empty, insert the dial code for the currently selected flag 377 | this.telInput.on("focus" + this.ns, function() { 378 | if (!that.telInput.val()) { 379 | that._updateVal("+" + that.selectedCountryData.dialCode, true); 380 | // after auto-inserting a dial code, if the first key they hit is '+' then assume 381 | // they are entering a new number, so remove the dial code. 382 | // use keypress instead of keydown because keydown gets triggered for the shift key 383 | // (required to hit the + key), and instead of keyup because that shows the new '+' 384 | // before removing the old one 385 | that.telInput.one("keypress.plus" + that.ns, function(e) { 386 | if (e.which == keys.PLUS) { 387 | that.telInput.val("+"); 388 | } 389 | }); 390 | // after tabbing in, make sure the cursor is at the end 391 | // we must use setTimeout to get outside of the focus handler as it seems the 392 | // selection happens after that 393 | setTimeout(function() { 394 | that._cursorToEnd(); 395 | }); 396 | } 397 | }); 398 | // on blur: if just a dial code then remove it 399 | this.telInput.on("blur" + this.ns, function() { 400 | var value = that.telInput.val(), startsPlus = value.substr(0, 1) == "+"; 401 | if (startsPlus) { 402 | var numeric = value.replace(/\D/g, ""), clean = "+" + numeric; 403 | // if just a plus, or if just a dial code 404 | if (!numeric || that.selectedCountryData.dialCode == numeric) { 405 | that.telInput.val(""); 406 | } 407 | } 408 | // remove the keypress listener we added on focus 409 | that.telInput.off("keypress.plus" + that.ns); 410 | }); 411 | }, 412 | // put the cursor to the end of the input (usually after a focus event) 413 | _cursorToEnd: function() { 414 | var input = this.telInput[0]; 415 | if (this.isGoodBrowser) { 416 | var len = this.telInput.val().length; 417 | input.setSelectionRange(len, len); 418 | } 419 | }, 420 | // show the dropdown 421 | _showDropdown: function() { 422 | this._setDropdownPosition(); 423 | // update highlighting and scroll to active list item 424 | var activeListItem = this.countryList.children(".active"); 425 | this._highlightListItem(activeListItem); 426 | // show it 427 | this.countryList.removeClass("hide"); 428 | this._scrollTo(activeListItem); 429 | // bind all the dropdown-related listeners: mouseover, click, click-off, keydown 430 | this._bindDropdownListeners(); 431 | // update the arrow 432 | this.selectedFlagInner.children(".arrow").addClass("up"); 433 | }, 434 | // decide where to position dropdown (depends on position within viewport, and scroll) 435 | _setDropdownPosition: function() { 436 | var inputTop = this.telInput.offset().top, windowTop = $(window).scrollTop(), // dropdownFitsBelow = (dropdownBottom < windowBottom) 437 | dropdownFitsBelow = inputTop + this.telInput.outerHeight() + this.dropdownHeight < windowTop + $(window).height(), dropdownFitsAbove = inputTop - this.dropdownHeight > windowTop; 438 | // dropdownHeight - 1 for border 439 | var cssTop = !dropdownFitsBelow && dropdownFitsAbove ? "-" + (this.dropdownHeight - 1) + "px" : ""; 440 | this.countryList.css("top", cssTop); 441 | }, 442 | // we only bind dropdown listeners when the dropdown is open 443 | _bindDropdownListeners: function() { 444 | var that = this; 445 | // when mouse over a list item, just highlight that one 446 | // we add the class "highlight", so if they hit "enter" we know which one to select 447 | this.countryList.on("mouseover" + this.ns, ".country", function(e) { 448 | that._highlightListItem($(this)); 449 | }); 450 | // listen for country selection 451 | this.countryList.on("click" + this.ns, ".country", function(e) { 452 | that._selectListItem($(this)); 453 | }); 454 | // click off to close 455 | // (except when this initial opening click is bubbling up) 456 | // we cannot just stopPropagation as it may be needed to close another instance 457 | var isOpening = true; 458 | $("html").on("click" + this.ns, function(e) { 459 | if (!isOpening) { 460 | that._closeDropdown(); 461 | } 462 | isOpening = false; 463 | }); 464 | // listen for up/down scrolling, enter to select, or letters to jump to country name. 465 | // use keydown as keypress doesn't fire for non-char keys and we want to catch if they 466 | // just hit down and hold it to scroll down (no keyup event). 467 | // listen on the document because that's where key events are triggered if no input has focus 468 | var query = "", queryTimer = null; 469 | $(document).on("keydown" + this.ns, function(e) { 470 | // prevent down key from scrolling the whole page, 471 | // and enter key from submitting a form etc 472 | e.preventDefault(); 473 | if (e.which == keys.UP || e.which == keys.DOWN) { 474 | // up and down to navigate 475 | that._handleUpDownKey(e.which); 476 | } else if (e.which == keys.ENTER) { 477 | // enter to select 478 | that._handleEnterKey(); 479 | } else if (e.which == keys.ESC) { 480 | // esc to close 481 | that._closeDropdown(); 482 | } else if (e.which >= keys.A && e.which <= keys.Z || e.which == keys.SPACE) { 483 | // upper case letters (note: keyup/keydown only return upper case letters) 484 | // jump to countries that start with the query string 485 | if (queryTimer) { 486 | clearTimeout(queryTimer); 487 | } 488 | query += String.fromCharCode(e.which); 489 | that._searchForCountry(query); 490 | // if the timer hits 1 second, reset the query 491 | queryTimer = setTimeout(function() { 492 | query = ""; 493 | }, 1e3); 494 | } 495 | }); 496 | }, 497 | // highlight the next/prev item in the list (and ensure it is visible) 498 | _handleUpDownKey: function(key) { 499 | var current = this.countryList.children(".highlight").first(); 500 | var next = key == keys.UP ? current.prev() : current.next(); 501 | if (next.length) { 502 | // skip the divider 503 | if (next.hasClass("divider")) { 504 | next = key == keys.UP ? next.prev() : next.next(); 505 | } 506 | this._highlightListItem(next); 507 | this._scrollTo(next); 508 | } 509 | }, 510 | // select the currently highlighted item 511 | _handleEnterKey: function() { 512 | var currentCountry = this.countryList.children(".highlight").first(); 513 | if (currentCountry.length) { 514 | this._selectListItem(currentCountry); 515 | } 516 | }, 517 | // find the first list item whose name starts with the query string 518 | _searchForCountry: function(query) { 519 | for (var i = 0; i < this.countries.length; i++) { 520 | if (this._startsWith(this.countries[i].name, query)) { 521 | var listItem = this.countryList.children("[data-country-code=" + this.countries[i].iso2 + "]").not(".preferred"); 522 | // update highlighting and scroll 523 | this._highlightListItem(listItem); 524 | this._scrollTo(listItem, true); 525 | break; 526 | } 527 | } 528 | }, 529 | // check if (uppercase) string a starts with string b 530 | _startsWith: function(a, b) { 531 | return a.substr(0, b.length).toUpperCase() == b; 532 | }, 533 | // update the input's value to the given val 534 | // if autoFormat=true, format it first according to the country-specific formatting rules 535 | _updateVal: function(val, addSuffix) { 536 | var formatted; 537 | if (this.options.autoFormat && window.intlTelInputUtils) { 538 | // don't try to add the suffix if we dont have a full dial code 539 | if (!this._getDialCode(val)) { 540 | addSuffix = false; 541 | } 542 | formatted = intlTelInputUtils.formatNumber(val, addSuffix); 543 | } else { 544 | // no autoFormat, so just insert the original value 545 | formatted = val; 546 | } 547 | this.telInput.val(formatted); 548 | }, 549 | // update the selected flag 550 | _updateFlag: function(number) { 551 | // try and extract valid dial code from input 552 | var dialCode = this._getDialCode(number); 553 | if (dialCode) { 554 | // check if one of the matching countries is already selected 555 | var countryCodes = this.countryCodes[dialCode.replace(/\D/g, "")], alreadySelected = false; 556 | // countries with area codes: we must always update the flag as if it's not an exact match 557 | // we should always default to the first country in the list. This is to avoid having to 558 | // explicitly define every possible area code in America (there are 999 possible area codes) 559 | if (!this.selectedCountryData || !this.selectedCountryData.hasAreaCodes) { 560 | for (var i = 0; i < countryCodes.length; i++) { 561 | if (this.selectedFlagInner.hasClass(countryCodes[i])) { 562 | alreadySelected = true; 563 | } 564 | } 565 | } 566 | // else choose the first in the list 567 | if (!alreadySelected) { 568 | this._selectFlag(countryCodes[0]); 569 | } 570 | } 571 | return dialCode; 572 | }, 573 | // reset the input value to just a dial code 574 | _resetToDialCode: function(dialCode) { 575 | // if nationalMode is enabled then don't insert the dial code 576 | var value = this.options.nationalMode ? "" : "+" + dialCode; 577 | this.telInput.val(value); 578 | }, 579 | // remove highlighting from other list items and highlight the given item 580 | _highlightListItem: function(listItem) { 581 | this.countryListItems.removeClass("highlight"); 582 | listItem.addClass("highlight"); 583 | }, 584 | // find the country data for the given country code 585 | // the ignoreOnlyCountriesOption is only used during init() while parsing the onlyCountries array 586 | _getCountryData: function(countryCode, ignoreOnlyCountriesOption, allowFail) { 587 | var countryList = ignoreOnlyCountriesOption ? allCountries : this.countries; 588 | for (var i = 0; i < countryList.length; i++) { 589 | if (countryList[i].iso2 == countryCode) { 590 | return countryList[i]; 591 | } 592 | } 593 | if (allowFail) { 594 | return null; 595 | } else { 596 | throw new Error("No country data for '" + countryCode + "'"); 597 | } 598 | }, 599 | // update the selected flag and the active list item 600 | _selectFlag: function(countryCode) { 601 | this.selectedFlagInner.attr("class", "flag " + countryCode); 602 | // update the placeholder 603 | if (window.intlTelInputUtils) { 604 | this.telInput.attr("placeholder", intlTelInputUtils.getExampleNumber(countryCode)); 605 | } 606 | // update the title attribute 607 | this.selectedCountryData = this._getCountryData(countryCode, false, false); 608 | var title = this.selectedCountryData.name + ": +" + this.selectedCountryData.dialCode; 609 | this.selectedFlagInner.parent().attr("title", title); 610 | // update the active list item 611 | var listItem = this.countryListItems.children(".flag." + countryCode).first().parent(); 612 | this.countryListItems.removeClass("active"); 613 | listItem.addClass("active"); 614 | }, 615 | // called when the user selects a list item from the dropdown 616 | _selectListItem: function(listItem) { 617 | // update selected flag and active list item 618 | var countryCode = listItem.attr("data-country-code"); 619 | this._selectFlag(countryCode); 620 | this._closeDropdown(); 621 | // update input value 622 | if (!this.options.nationalMode) { 623 | this._updateDialCode("+" + listItem.attr("data-dial-code")); 624 | } 625 | // always fire the change event as even if nationalMode=true (and we haven't updated 626 | // the input val), the system as a whole has still changed - see country-sync example 627 | this.telInput.trigger("change"); 628 | // focus the input 629 | this.telInput.focus(); 630 | this._cursorToEnd(); 631 | }, 632 | // close the dropdown and unbind any listeners 633 | _closeDropdown: function() { 634 | this.countryList.addClass("hide"); 635 | // update the arrow 636 | this.selectedFlagInner.children(".arrow").removeClass("up"); 637 | // unbind key events 638 | $(document).off(this.ns); 639 | // unbind click-off-to-close 640 | $("html").off(this.ns); 641 | // unbind hover and click listeners 642 | this.countryList.off(this.ns); 643 | }, 644 | // check if an element is visible within it's container, else scroll until it is 645 | _scrollTo: function(element, middle) { 646 | var container = this.countryList, containerHeight = container.height(), containerTop = container.offset().top, containerBottom = containerTop + containerHeight, elementHeight = element.outerHeight(), elementTop = element.offset().top, elementBottom = elementTop + elementHeight, newScrollTop = elementTop - containerTop + container.scrollTop(), middleOffset = containerHeight / 2 - elementHeight / 2; 647 | if (elementTop < containerTop) { 648 | // scroll up 649 | if (middle) { 650 | newScrollTop -= middleOffset; 651 | } 652 | container.scrollTop(newScrollTop); 653 | } else if (elementBottom > containerBottom) { 654 | // scroll down 655 | if (middle) { 656 | newScrollTop += middleOffset; 657 | } 658 | var heightDifference = containerHeight - elementHeight; 659 | container.scrollTop(newScrollTop - heightDifference); 660 | } 661 | }, 662 | // replace any existing dial code with the new one 663 | // currently this is only called from _selectListItem 664 | _updateDialCode: function(newDialCode) { 665 | var inputVal = this.telInput.val(), prevDialCode = this._getDialCode(), newNumber; 666 | // if the previous number contained a valid dial code, replace it 667 | // (if more than just a plus character) 668 | if (prevDialCode.length > 1) { 669 | newNumber = inputVal.replace(prevDialCode, newDialCode); 670 | } else { 671 | // if the previous number didn't contain a dial code, we should persist it 672 | var existingNumber = inputVal && inputVal.substr(0, 1) != "+" ? $.trim(inputVal) : ""; 673 | newNumber = newDialCode + existingNumber; 674 | } 675 | this._updateVal(newNumber, true); 676 | }, 677 | // try and extract a valid international dial code from a full telephone number 678 | // Note: returns the raw string inc plus character and any whitespace/dots etc 679 | _getDialCode: function(number) { 680 | var dialCode = "", inputVal = number || this.telInput.val(); 681 | // only interested in international numbers (starting with a plus) 682 | if (inputVal.charAt(0) == "+") { 683 | var numericChars = ""; 684 | // iterate over chars 685 | for (var i = 0; i < inputVal.length; i++) { 686 | var c = inputVal.charAt(i); 687 | // if char is number 688 | if ($.isNumeric(c)) { 689 | numericChars += c; 690 | // if current numericChars make a valid dial code 691 | if (this.countryCodes[numericChars]) { 692 | // store the actual raw string (useful for matching later) 693 | dialCode = inputVal.substring(0, i + 1); 694 | } 695 | // longest dial code is 4 chars 696 | if (numericChars.length == 4) { 697 | break; 698 | } 699 | } 700 | } 701 | } 702 | return dialCode; 703 | }, 704 | /******************** 705 | * PUBLIC METHODS 706 | ********************/ 707 | // remove plugin 708 | destroy: function() { 709 | // make sure the dropdown is closed (and unbind listeners) 710 | this._closeDropdown(); 711 | // key events, and focus/blur events if autoHideDialCode=true 712 | this.telInput.off(this.ns); 713 | // click event to open dropdown 714 | this.selectedFlagInner.parent().off(this.ns); 715 | // label click hack 716 | this.telInput.closest("label").off(this.ns); 717 | // remove markup 718 | var container = this.telInput.parent(); 719 | container.before(this.telInput).remove(); 720 | }, 721 | // get the country data for the currently selected flag 722 | getSelectedCountryData: function() { 723 | return this.selectedCountryData; 724 | }, 725 | // validate the input val - assumes the global function isValidNumber 726 | // pass in true if you want to allow national numbers (no country dial code) 727 | isValidNumber: function(allowNational) { 728 | var val = $.trim(this.telInput.val()), countryCode = allowNational ? this.selectedCountryData.iso2 : "", // libphonenumber allows alpha chars, but in order to allow that, we'd need a method to retrieve the processed number, with letters replaced with numbers 729 | containsAlpha = /[a-zA-Z]/.test(val); 730 | return !containsAlpha && window.intlTelInputUtils && intlTelInputUtils.isValidNumber(val, countryCode); 731 | }, 732 | // update the selected flag, and if the input is empty: insert the new dial code 733 | selectCountry: function(countryCode) { 734 | // check if already selected 735 | if (!this.selectedFlagInner.hasClass(countryCode)) { 736 | this._selectFlag(countryCode); 737 | if (!this.telInput.val() && !this.options.autoHideDialCode) { 738 | this._resetToDialCode(this.selectedCountryData.dialCode); 739 | } 740 | } 741 | }, 742 | // set the input value and update the flag 743 | setNumber: function(number, addSuffix) { 744 | // we must update the flag first, which updates this.selectedCountryData, which is used later for formatting the number before displaying it 745 | var dialCode = this._updateFlag(number); 746 | this._updateVal(number, addSuffix); 747 | return dialCode; 748 | }, 749 | // this is called when the utils are ready 750 | utilsLoaded: function() { 751 | // if autoFormat is enabled and there's an initial value in the input, then format it 752 | if (this.options.autoFormat && this.telInput.val()) { 753 | this._updateVal(this.telInput.val()); 754 | } 755 | } 756 | }; 757 | // adapted to allow public functions 758 | // using https://github.com/jquery-boilerplate/jquery-boilerplate/wiki/Extending-jQuery-Boilerplate 759 | $.fn[pluginName] = function(options) { 760 | var args = arguments; 761 | // Is the first parameter an object (options), or was omitted, 762 | // instantiate a new instance of the plugin. 763 | if (options === undefined || typeof options === "object") { 764 | return this.each(function() { 765 | if (!$.data(this, "plugin_" + pluginName)) { 766 | $.data(this, "plugin_" + pluginName, new Plugin(this, options)); 767 | } 768 | }); 769 | } else if (typeof options === "string" && options[0] !== "_" && options !== "init") { 770 | // If the first parameter is a string and it doesn't start 771 | // with an underscore or "contains" the `init`-function, 772 | // treat this as a call to a public method. 773 | // Cache the method call to make it possible to return a value 774 | var returns; 775 | this.each(function() { 776 | var instance = $.data(this, "plugin_" + pluginName); 777 | // Tests that there's already a plugin-instance 778 | // and checks that the requested public method exists 779 | if (instance instanceof Plugin && typeof instance[options] === "function") { 780 | // Call the method of our plugin instance, 781 | // and pass it the supplied arguments. 782 | returns = instance[options].apply(instance, Array.prototype.slice.call(args, 1)); 783 | } 784 | // Allow instances to be destroyed via the 'destroy' method 785 | if (options === "destroy") { 786 | $.data(this, "plugin_" + pluginName, null); 787 | } 788 | }); 789 | // If the earlier cached method gives a value back return the value, 790 | // otherwise return this to preserve chainability. 791 | return returns !== undefined ? returns : this; 792 | } 793 | }; 794 | /******************** 795 | * STATIC METHODS 796 | ********************/ 797 | // get the country data object 798 | $.fn[pluginName].getCountryData = function() { 799 | return allCountries; 800 | }; 801 | // set the country data object 802 | $.fn[pluginName].setCountryData = function(obj) { 803 | allCountries = obj; 804 | }; 805 | // Tell JSHint to ignore this warning: "character may get silently deleted by one or more browsers" 806 | // jshint -W100 807 | // Array of country objects for the flag dropdown. 808 | // Each contains a name, country code (ISO 3166-1 alpha-2) and dial code. 809 | // Originally from https://github.com/mledoze/countries 810 | // then modified using the following JavaScript (NOW OUT OF DATE): 811 | /* 812 | var result = []; 813 | _.each(countries, function(c) { 814 | // ignore countries without a dial code 815 | if (c.callingCode[0].length) { 816 | result.push({ 817 | // var locals contains country names with localised versions in brackets 818 | n: _.findWhere(locals, { 819 | countryCode: c.cca2 820 | }).name, 821 | i: c.cca2.toLowerCase(), 822 | d: c.callingCode[0] 823 | }); 824 | } 825 | }); 826 | JSON.stringify(result); 827 | */ 828 | // then with a couple of manual re-arrangements to be alphabetical 829 | // then changed Kazakhstan from +76 to +7 830 | // and Vatican City from +379 to +39 (see issue 50) 831 | // and Caribean Netherlands from +5997 to +599 832 | // and Curacao from +5999 to +599 833 | // Removed: Åland Islands, Christmas Island, Cocos Islands, Guernsey, Isle of Man, Jersey, Kosovo, Mayotte, Pitcairn Islands, South Georgia, Svalbard, Western Sahara 834 | // Update: converted objects to arrays to save bytes! 835 | // Update: added "priority" for countries with the same dialCode as others 836 | // Update: added array of area codes for countries with the same dialCode as others 837 | // So each country array has the following information: 838 | // [ 839 | // Country name, 840 | // iso2 code, 841 | // International dial code, 842 | // Order (if >1 country with same dial code), 843 | // Area codes (if >1 country with same dial code) 844 | // ] 845 | var allCountries = [ [ "Afghanistan (‫افغانستان‬‎)", "af", "93" ], [ "Albania (Shqipëri)", "al", "355" ], [ "Algeria (‫الجزائر‬‎)", "dz", "213" ], [ "American Samoa", "as", "1684" ], [ "Andorra", "ad", "376" ], [ "Angola", "ao", "244" ], [ "Anguilla", "ai", "1264" ], [ "Antigua and Barbuda", "ag", "1268" ], [ "Argentina", "ar", "54" ], [ "Armenia (Հայաստան)", "am", "374" ], [ "Aruba", "aw", "297" ], [ "Australia", "au", "61" ], [ "Austria (Österreich)", "at", "43" ], [ "Azerbaijan (Azərbaycan)", "az", "994" ], [ "Bahamas", "bs", "1242" ], [ "Bahrain (‫البحرين‬‎)", "bh", "973" ], [ "Bangladesh (বাংলাদেশ)", "bd", "880" ], [ "Barbados", "bb", "1246" ], [ "Belarus (Беларусь)", "by", "375" ], [ "Belgium (België)", "be", "32" ], [ "Belize", "bz", "501" ], [ "Benin (Bénin)", "bj", "229" ], [ "Bermuda", "bm", "1441" ], [ "Bhutan (འབྲུག)", "bt", "975" ], [ "Bolivia", "bo", "591" ], [ "Bosnia and Herzegovina (Босна и Херцеговина)", "ba", "387" ], [ "Botswana", "bw", "267" ], [ "Brazil (Brasil)", "br", "55" ], [ "British Indian Ocean Territory", "io", "246" ], [ "British Virgin Islands", "vg", "1284" ], [ "Brunei", "bn", "673" ], [ "Bulgaria (България)", "bg", "359" ], [ "Burkina Faso", "bf", "226" ], [ "Burundi (Uburundi)", "bi", "257" ], [ "Cambodia (កម្ពុជា)", "kh", "855" ], [ "Cameroon (Cameroun)", "cm", "237" ], [ "Canada", "ca", "1", 1, [ "204", "236", "249", "250", "289", "306", "343", "365", "387", "403", "416", "418", "431", "437", "438", "450", "506", "514", "519", "548", "579", "581", "587", "604", "613", "639", "647", "672", "705", "709", "742", "778", "780", "782", "807", "819", "825", "867", "873", "902", "905" ] ], [ "Cape Verde (Kabu Verdi)", "cv", "238" ], [ "Caribbean Netherlands", "bq", "599", "", 1 ], [ "Cayman Islands", "ky", "1345" ], [ "Central African Republic (République centrafricaine)", "cf", "236" ], [ "Chad (Tchad)", "td", "235" ], [ "Chile", "cl", "56" ], [ "China (中国)", "cn", "86" ], [ "Colombia", "co", "57" ], [ "Comoros (‫جزر القمر‬‎)", "km", "269" ], [ "Congo (DRC) (Jamhuri ya Kidemokrasia ya Kongo)", "cd", "243" ], [ "Congo (Republic) (Congo-Brazzaville)", "cg", "242" ], [ "Cook Islands", "ck", "682" ], [ "Costa Rica", "cr", "506" ], [ "Côte d’Ivoire", "ci", "225" ], [ "Croatia (Hrvatska)", "hr", "385" ], [ "Cuba", "cu", "53" ], [ "Curaçao", "cw", "599", "", 0 ], [ "Cyprus (Κύπρος)", "cy", "357" ], [ "Czech Republic (Česká republika)", "cz", "420" ], [ "Denmark (Danmark)", "dk", "45" ], [ "Djibouti", "dj", "253" ], [ "Dominica", "dm", "1767" ], [ "Dominican Republic (República Dominicana)", "do", "1", "", 2, [ "809", "829", "849" ] ], [ "Ecuador", "ec", "593" ], [ "Egypt (‫مصر‬‎)", "eg", "20" ], [ "El Salvador", "sv", "503" ], [ "Equatorial Guinea (Guinea Ecuatorial)", "gq", "240" ], [ "Eritrea", "er", "291" ], [ "Estonia (Eesti)", "ee", "372" ], [ "Ethiopia", "et", "251" ], [ "Falkland Islands (Islas Malvinas)", "fk", "500" ], [ "Faroe Islands (Føroyar)", "fo", "298" ], [ "Fiji", "fj", "679" ], [ "Finland (Suomi)", "fi", "358" ], [ "France", "fr", "33" ], [ "French Guiana (Guyane française)", "gf", "594" ], [ "French Polynesia (Polynésie française)", "pf", "689" ], [ "Gabon", "ga", "241" ], [ "Gambia", "gm", "220" ], [ "Georgia (საქართველო)", "ge", "995" ], [ "Germany (Deutschland)", "de", "49" ], [ "Ghana (Gaana)", "gh", "233" ], [ "Gibraltar", "gi", "350" ], [ "Greece (Ελλάδα)", "gr", "30" ], [ "Greenland (Kalaallit Nunaat)", "gl", "299" ], [ "Grenada", "gd", "1473" ], [ "Guadeloupe", "gp", "590", "", 0 ], [ "Guam", "gu", "1671" ], [ "Guatemala", "gt", "502" ], [ "Guinea (Guinée)", "gn", "224" ], [ "Guinea-Bissau (Guiné Bissau)", "gw", "245" ], [ "Guyana", "gy", "592" ], [ "Haiti", "ht", "509" ], [ "Honduras", "hn", "504" ], [ "Hong Kong (香港)", "hk", "852" ], [ "Hungary (Magyarország)", "hu", "36" ], [ "Iceland (Ísland)", "is", "354" ], [ "India (भारत)", "in", "91" ], [ "Indonesia", "id", "62" ], [ "Iran (‫ایران‬‎)", "ir", "98" ], [ "Iraq (‫العراق‬‎)", "iq", "964" ], [ "Ireland", "ie", "353" ], [ "Israel (‫ישראל‬‎)", "il", "972" ], [ "Italy (Italia)", "it", "39", 0 ], [ "Jamaica", "jm", "1876" ], [ "Japan (日本)", "jp", "81" ], [ "Jordan (‫الأردن‬‎)", "jo", "962" ], [ "Kazakhstan (Казахстан)", "kz", "7", 1 ], [ "Kenya", "ke", "254" ], [ "Kiribati", "ki", "686" ], [ "Kuwait (‫الكويت‬‎)", "kw", "965" ], [ "Kyrgyzstan (Кыргызстан)", "kg", "996" ], [ "Laos (ລາວ)", "la", "856" ], [ "Latvia (Latvija)", "lv", "371" ], [ "Lebanon (‫لبنان‬‎)", "lb", "961" ], [ "Lesotho", "ls", "266" ], [ "Liberia", "lr", "231" ], [ "Libya (‫ليبيا‬‎)", "ly", "218" ], [ "Liechtenstein", "li", "423" ], [ "Lithuania (Lietuva)", "lt", "370" ], [ "Luxembourg", "lu", "352" ], [ "Macau (澳門)", "mo", "853" ], [ "Macedonia (FYROM) (Македонија)", "mk", "389" ], [ "Madagascar (Madagasikara)", "mg", "261" ], [ "Malawi", "mw", "265" ], [ "Malaysia", "my", "60" ], [ "Maldives", "mv", "960" ], [ "Mali", "ml", "223" ], [ "Malta", "mt", "356" ], [ "Marshall Islands", "mh", "692" ], [ "Martinique", "mq", "596" ], [ "Mauritania (‫موريتانيا‬‎)", "mr", "222" ], [ "Mauritius (Moris)", "mu", "230" ], [ "Mexico (México)", "mx", "52" ], [ "Micronesia", "fm", "691" ], [ "Moldova (Republica Moldova)", "md", "373" ], [ "Monaco", "mc", "377" ], [ "Mongolia (Монгол)", "mn", "976" ], [ "Montenegro (Crna Gora)", "me", "382" ], [ "Montserrat", "ms", "1664" ], [ "Morocco (‫المغرب‬‎)", "ma", "212" ], [ "Mozambique (Moçambique)", "mz", "258" ], [ "Myanmar (Burma) (မြန်မာ)", "mm", "95" ], [ "Namibia (Namibië)", "na", "264" ], [ "Nauru", "nr", "674" ], [ "Nepal (नेपाल)", "np", "977" ], [ "Netherlands (Nederland)", "nl", "31" ], [ "New Caledonia (Nouvelle-Calédonie)", "nc", "687" ], [ "New Zealand", "nz", "64" ], [ "Nicaragua", "ni", "505" ], [ "Niger (Nijar)", "ne", "227" ], [ "Nigeria", "ng", "234" ], [ "Niue", "nu", "683" ], [ "Norfolk Island", "nf", "672" ], [ "North Korea (조선 민주주의 인민 공화국)", "kp", "850" ], [ "Northern Mariana Islands", "mp", "1670" ], [ "Norway (Norge)", "no", "47" ], [ "Oman (‫عُمان‬‎)", "om", "968" ], [ "Pakistan (‫پاکستان‬‎)", "pk", "92" ], [ "Palau", "pw", "680" ], [ "Palestine (‫فلسطين‬‎)", "ps", "970" ], [ "Panama (Panamá)", "pa", "507" ], [ "Papua New Guinea", "pg", "675" ], [ "Paraguay", "py", "595" ], [ "Peru (Perú)", "pe", "51" ], [ "Philippines", "ph", "63" ], [ "Poland (Polska)", "pl", "48" ], [ "Portugal", "pt", "351" ], [ "Puerto Rico", "pr", "1", "", 3, [ "787", "939" ] ], [ "Qatar (‫قطر‬‎)", "qa", "974" ], [ "Réunion (La Réunion)", "re", "262" ], [ "Romania (România)", "ro", "40" ], [ "Russia (Россия)", "ru", "7", 0 ], [ "Rwanda", "rw", "250" ], [ "Saint Barthélemy (Saint-Barthélemy)", "bl", "590", "", 1 ], [ "Saint Helena", "sh", "290" ], [ "Saint Kitts and Nevis", "kn", "1869" ], [ "Saint Lucia", "lc", "1758" ], [ "Saint Martin (Saint-Martin (partie française))", "mf", "590", "", 2 ], [ "Saint Pierre and Miquelon (Saint-Pierre-et-Miquelon)", "pm", "508" ], [ "Saint Vincent and the Grenadines", "vc", "1784" ], [ "Samoa", "ws", "685" ], [ "San Marino", "sm", "378" ], [ "São Tomé and Príncipe (São Tomé e Príncipe)", "st", "239" ], [ "Saudi Arabia (‫المملكة العربية السعودية‬‎)", "sa", "966" ], [ "Senegal (Sénégal)", "sn", "221" ], [ "Serbia (Србија)", "rs", "381" ], [ "Seychelles", "sc", "248" ], [ "Sierra Leone", "sl", "232" ], [ "Singapore", "sg", "65" ], [ "Sint Maarten", "sx", "1721" ], [ "Slovakia (Slovensko)", "sk", "421" ], [ "Slovenia (Slovenija)", "si", "386" ], [ "Solomon Islands", "sb", "677" ], [ "Somalia (Soomaaliya)", "so", "252" ], [ "South Africa", "za", "27" ], [ "South Korea (대한민국)", "kr", "82" ], [ "South Sudan (‫جنوب السودان‬‎)", "ss", "211" ], [ "Spain (España)", "es", "34" ], [ "Sri Lanka (ශ්‍රී ලංකාව)", "lk", "94" ], [ "Sudan (‫السودان‬‎)", "sd", "249" ], [ "Suriname", "sr", "597" ], [ "Swaziland", "sz", "268" ], [ "Sweden (Sverige)", "se", "46" ], [ "Switzerland (Schweiz)", "ch", "41" ], [ "Syria (‫سوريا‬‎)", "sy", "963" ], [ "Taiwan (台灣)", "tw", "886" ], [ "Tajikistan", "tj", "992" ], [ "Tanzania", "tz", "255" ], [ "Thailand (ไทย)", "th", "66" ], [ "Timor-Leste", "tl", "670" ], [ "Togo", "tg", "228" ], [ "Tokelau", "tk", "690" ], [ "Tonga", "to", "676" ], [ "Trinidad and Tobago", "tt", "1868" ], [ "Tunisia (‫تونس‬‎)", "tn", "216" ], [ "Turkey (Türkiye)", "tr", "90" ], [ "Turkmenistan", "tm", "993" ], [ "Turks and Caicos Islands", "tc", "1649" ], [ "Tuvalu", "tv", "688" ], [ "U.S. Virgin Islands", "vi", "1340" ], [ "Uganda", "ug", "256" ], [ "Ukraine (Україна)", "ua", "380" ], [ "United Arab Emirates (‫الإمارات العربية المتحدة‬‎)", "ae", "971" ], [ "United Kingdom", "gb", "44" ], [ "United States", "us", "1", 0 ], [ "Uruguay", "uy", "598" ], [ "Uzbekistan (Oʻzbekiston)", "uz", "998" ], [ "Vanuatu", "vu", "678" ], [ "Vatican City (Città del Vaticano)", "va", "39", 1 ], [ "Venezuela", "ve", "58" ], [ "Vietnam (Việt Nam)", "vn", "84" ], [ "Wallis and Futuna", "wf", "681" ], [ "Yemen (‫اليمن‬‎)", "ye", "967" ], [ "Zambia", "zm", "260" ], [ "Zimbabwe", "zw", "263" ] ]; 846 | // we will build this in the loop below 847 | var allCountryCodes = {}; 848 | var addCountryCode = function(iso2, dialCode, priority) { 849 | if (!(dialCode in allCountryCodes)) { 850 | allCountryCodes[dialCode] = []; 851 | } 852 | var index = priority || 0; 853 | allCountryCodes[dialCode][index] = iso2; 854 | }; 855 | // loop over all of the countries above 856 | for (var i = 0; i < allCountries.length; i++) { 857 | // countries 858 | var c = allCountries[i]; 859 | allCountries[i] = { 860 | name: c[0], 861 | iso2: c[1], 862 | dialCode: c[2] 863 | }; 864 | // area codes 865 | if (c[4]) { 866 | allCountries[i].hasAreaCodes = true; 867 | for (var j = 0; j < c[4].length; j++) { 868 | // full dial code is country code + dial code 869 | var dialCode = c[2] + c[4][j]; 870 | addCountryCode(c[1], dialCode); 871 | } 872 | } 873 | // dial codes 874 | addCountryCode(c[1], c[2], c[3]); 875 | } 876 | }); --------------------------------------------------------------------------------