├── .env.php ├── .gitignore ├── CONTRIBUTING.md ├── Procfile ├── app.json ├── app ├── commands │ └── .gitkeep ├── config │ ├── app.php │ ├── auth.php │ ├── cache.php │ ├── compile.php │ ├── database.php │ ├── local │ │ ├── app.php │ │ └── database.php │ ├── mail.php │ ├── packages │ │ └── .gitkeep │ ├── queue.php │ ├── remote.php │ ├── services.php │ ├── session.php │ ├── testing │ │ ├── cache.php │ │ └── session.php │ ├── view.php │ └── workbench.php ├── controllers │ ├── .gitkeep │ └── BaseController.php ├── database │ ├── .gitignore │ ├── migrations │ │ └── .gitkeep │ └── seeds │ │ ├── .gitkeep │ │ └── DatabaseSeeder.php ├── filters.php ├── lang │ └── en │ │ ├── pagination.php │ │ ├── reminders.php │ │ └── validation.php ├── models │ └── User.php ├── routes.php ├── start │ ├── artisan.php │ ├── global.php │ └── local.php ├── storage │ ├── .gitignore │ ├── cache │ │ └── .gitignore │ ├── logs │ │ └── .gitignore │ ├── meta │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ └── views │ │ └── .gitignore ├── tests │ ├── ExampleTest.php │ └── TestCase.php └── views │ ├── emails │ └── auth │ │ └── reminder.blade.php │ └── home.php ├── artisan ├── bootstrap ├── autoload.php ├── paths.php └── start.php ├── composer.json ├── composer.lock ├── phpunit.xml ├── public ├── .htaccess ├── app.js ├── favicon.ico ├── index.php ├── landing.html ├── packages │ └── .gitkeep ├── robots.txt └── vendor │ └── intl-phone │ ├── css │ ├── demo.css │ └── intlTelInput.css │ ├── img │ └── flags.png │ ├── js │ ├── intlTelInput.js │ └── intlTelInput.min.js │ └── libphonenumber │ ├── build │ └── utils.js │ └── src │ └── utils.js ├── readme.md └── server.php /.env.php: -------------------------------------------------------------------------------- 1 | getenv('TWILIO_ACCOUNT_SID'), 4 | 'TWILIO_AUTH_TOKEN' => getenv('TWILIO_AUTH_TOKEN'), 5 | 'TWILIO_NUMBER' => getenv('TWILIO_NUMBER') 6 | ); 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /vendor 3 | .idea 4 | .env 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Please submit all issues and pull requests to the [laravel/framework](http://github.com/laravel/framework) repository! 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-apache2 public -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Twilio Click To Call: Laravel", 3 | "description": "An example of implementing click to call functionality in a Laravel application", 4 | "keywords": [ 5 | "PHP", 6 | "API", 7 | "twilio", 8 | "Laravel", 9 | "Tutorial", 10 | "Telephone API", 11 | "Voice API", 12 | "REST API", 13 | "Demo" 14 | ], 15 | "website": "https://twilio.com/docs/howto/click-to-call-walkthrough", 16 | "repository": "https://github.com/TwilioDevEd/clicktocall-laravel", 17 | "logo": "https://s3-us-west-2.amazonaws.com/deved/twilio-logo.png", 18 | "success_url": "/landing.html", 19 | "env": { 20 | "TWILIO_ACCOUNT_SID": { 21 | "description": "Your Twilio account secret ID, you can find at: https://www.twilio.com/user/account", 22 | "value": "enter_your_account_sid_here" 23 | }, 24 | "TWILIO_AUTH_TOKEN": { 25 | "description": "Your secret Twilio Auth token, you can find at: https://www.twilio.com/user/account", 26 | "value": "enter_your_auth_token_here" 27 | }, 28 | "TWILIO_NUMBER": { 29 | "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", 30 | "value": "+15005550006" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-laravel/1151b0befb06c979e0fae44fc14e38445411b28b/app/commands/.gitkeep -------------------------------------------------------------------------------- /app/config/app.php: -------------------------------------------------------------------------------- 1 | true, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application URL 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This URL is used by the console to properly generate URLs when using 24 | | the Artisan command line tool. You should set this to the root of 25 | | your application so that it is used when running Artisan tasks. 26 | | 27 | */ 28 | 29 | 'url' => 'http://localhost', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Timezone 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you may specify the default timezone for your application, which 37 | | will be used by the PHP date and date-time functions. We have gone 38 | | ahead and set this to a sensible default for you out of the box. 39 | | 40 | */ 41 | 42 | 'timezone' => 'UTC', 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Application Locale Configuration 47 | |-------------------------------------------------------------------------- 48 | | 49 | | The application locale determines the default locale that will be used 50 | | by the translation service provider. You are free to set this value 51 | | to any of the locales which will be supported by the application. 52 | | 53 | */ 54 | 55 | 'locale' => 'en', 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Application Fallback Locale 60 | |-------------------------------------------------------------------------- 61 | | 62 | | The fallback locale determines the locale to use when the current one 63 | | is not available. You may change the value to correspond to any of 64 | | the language folders that are provided through your application. 65 | | 66 | */ 67 | 68 | 'fallback_locale' => 'en', 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Encryption Key 73 | |-------------------------------------------------------------------------- 74 | | 75 | | This key is used by the Illuminate encrypter service and should be set 76 | | to a random, 32 character string, otherwise these encrypted strings 77 | | will not be safe. Please do this before deploying an application! 78 | | 79 | */ 80 | 81 | 'key' => 'YourSecretKey!!!', 82 | 83 | 'cipher' => MCRYPT_RIJNDAEL_128, 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Autoloaded Service Providers 88 | |-------------------------------------------------------------------------- 89 | | 90 | | The service providers listed here will be automatically loaded on the 91 | | request to your application. Feel free to add your own services to 92 | | this array to grant expanded functionality to your applications. 93 | | 94 | */ 95 | 96 | 'providers' => array( 97 | 98 | 'Illuminate\Foundation\Providers\ArtisanServiceProvider', 99 | 'Illuminate\Auth\AuthServiceProvider', 100 | 'Illuminate\Cache\CacheServiceProvider', 101 | 'Illuminate\Session\CommandsServiceProvider', 102 | 'Illuminate\Foundation\Providers\ConsoleSupportServiceProvider', 103 | 'Illuminate\Routing\ControllerServiceProvider', 104 | 'Illuminate\Cookie\CookieServiceProvider', 105 | 'Illuminate\Database\DatabaseServiceProvider', 106 | 'Illuminate\Encryption\EncryptionServiceProvider', 107 | 'Illuminate\Filesystem\FilesystemServiceProvider', 108 | 'Illuminate\Hashing\HashServiceProvider', 109 | 'Illuminate\Html\HtmlServiceProvider', 110 | 'Illuminate\Log\LogServiceProvider', 111 | 'Illuminate\Mail\MailServiceProvider', 112 | 'Illuminate\Database\MigrationServiceProvider', 113 | 'Illuminate\Pagination\PaginationServiceProvider', 114 | 'Illuminate\Queue\QueueServiceProvider', 115 | 'Illuminate\Redis\RedisServiceProvider', 116 | 'Illuminate\Remote\RemoteServiceProvider', 117 | 'Illuminate\Auth\Reminders\ReminderServiceProvider', 118 | 'Illuminate\Database\SeedServiceProvider', 119 | 'Illuminate\Session\SessionServiceProvider', 120 | 'Illuminate\Translation\TranslationServiceProvider', 121 | 'Illuminate\Validation\ValidationServiceProvider', 122 | 'Illuminate\View\ViewServiceProvider', 123 | 'Illuminate\Workbench\WorkbenchServiceProvider', 124 | 125 | ), 126 | 127 | /* 128 | |-------------------------------------------------------------------------- 129 | | Service Provider Manifest 130 | |-------------------------------------------------------------------------- 131 | | 132 | | The service provider manifest is used by Laravel to lazy load service 133 | | providers which are not needed for each request, as well to keep a 134 | | list of all of the services. Here, you may set its storage spot. 135 | | 136 | */ 137 | 138 | 'manifest' => storage_path().'/meta', 139 | 140 | /* 141 | |-------------------------------------------------------------------------- 142 | | Class Aliases 143 | |-------------------------------------------------------------------------- 144 | | 145 | | This array of class aliases will be registered when this application 146 | | is started. However, feel free to register as many as you wish as 147 | | the aliases are "lazy" loaded so they don't hinder performance. 148 | | 149 | */ 150 | 151 | 'aliases' => array( 152 | 153 | 'App' => 'Illuminate\Support\Facades\App', 154 | 'Artisan' => 'Illuminate\Support\Facades\Artisan', 155 | 'Auth' => 'Illuminate\Support\Facades\Auth', 156 | 'Blade' => 'Illuminate\Support\Facades\Blade', 157 | 'Cache' => 'Illuminate\Support\Facades\Cache', 158 | 'ClassLoader' => 'Illuminate\Support\ClassLoader', 159 | 'Config' => 'Illuminate\Support\Facades\Config', 160 | 'Controller' => 'Illuminate\Routing\Controller', 161 | 'Cookie' => 'Illuminate\Support\Facades\Cookie', 162 | 'Crypt' => 'Illuminate\Support\Facades\Crypt', 163 | 'DB' => 'Illuminate\Support\Facades\DB', 164 | 'Eloquent' => 'Illuminate\Database\Eloquent\Model', 165 | 'Event' => 'Illuminate\Support\Facades\Event', 166 | 'File' => 'Illuminate\Support\Facades\File', 167 | 'Form' => 'Illuminate\Support\Facades\Form', 168 | 'Hash' => 'Illuminate\Support\Facades\Hash', 169 | 'HTML' => 'Illuminate\Support\Facades\HTML', 170 | 'Input' => 'Illuminate\Support\Facades\Input', 171 | 'Lang' => 'Illuminate\Support\Facades\Lang', 172 | 'Log' => 'Illuminate\Support\Facades\Log', 173 | 'Mail' => 'Illuminate\Support\Facades\Mail', 174 | 'Paginator' => 'Illuminate\Support\Facades\Paginator', 175 | 'Password' => 'Illuminate\Support\Facades\Password', 176 | 'Queue' => 'Illuminate\Support\Facades\Queue', 177 | 'Redirect' => 'Illuminate\Support\Facades\Redirect', 178 | 'Redis' => 'Illuminate\Support\Facades\Redis', 179 | 'Request' => 'Illuminate\Support\Facades\Request', 180 | 'Response' => 'Illuminate\Support\Facades\Response', 181 | 'Route' => 'Illuminate\Support\Facades\Route', 182 | 'Schema' => 'Illuminate\Support\Facades\Schema', 183 | 'Seeder' => 'Illuminate\Database\Seeder', 184 | 'Session' => 'Illuminate\Support\Facades\Session', 185 | 'SoftDeletingTrait' => 'Illuminate\Database\Eloquent\SoftDeletingTrait', 186 | 'SSH' => 'Illuminate\Support\Facades\SSH', 187 | 'Str' => 'Illuminate\Support\Str', 188 | 'URL' => 'Illuminate\Support\Facades\URL', 189 | 'Validator' => 'Illuminate\Support\Facades\Validator', 190 | 'View' => 'Illuminate\Support\Facades\View', 191 | 192 | ), 193 | 194 | ); 195 | -------------------------------------------------------------------------------- /app/config/auth.php: -------------------------------------------------------------------------------- 1 | 'eloquent', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Authentication Model 23 | |-------------------------------------------------------------------------- 24 | | 25 | | When using the "Eloquent" authentication driver, we need to know which 26 | | Eloquent model should be used to retrieve your users. Of course, it 27 | | is often just the "User" model but you may use whatever you like. 28 | | 29 | */ 30 | 31 | 'model' => 'User', 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Authentication Table 36 | |-------------------------------------------------------------------------- 37 | | 38 | | When using the "Database" authentication driver, we need to know which 39 | | table should be used to retrieve your users. We have chosen a basic 40 | | default value but you may easily change it to any table you like. 41 | | 42 | */ 43 | 44 | 'table' => 'users', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Password Reminder Settings 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Here you may set the settings for password reminders, including a view 52 | | that should be used as your password reminder e-mail. You will also 53 | | be able to set the name of the table that holds the reset tokens. 54 | | 55 | | The "expire" time is the number of minutes that the reminder should be 56 | | considered valid. This security feature keeps tokens short-lived so 57 | | they have less time to be guessed. You may change this as needed. 58 | | 59 | */ 60 | 61 | 'reminder' => array( 62 | 63 | 'email' => 'emails.auth.reminder', 64 | 65 | 'table' => 'password_reminders', 66 | 67 | 'expire' => 60, 68 | 69 | ), 70 | 71 | ); 72 | -------------------------------------------------------------------------------- /app/config/cache.php: -------------------------------------------------------------------------------- 1 | 'file', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | File Cache Location 23 | |-------------------------------------------------------------------------- 24 | | 25 | | When using the "file" cache driver, we need a location where the cache 26 | | files may be stored. A sensible default has been specified, but you 27 | | are free to change it to any other place on disk that you desire. 28 | | 29 | */ 30 | 31 | 'path' => storage_path().'/cache', 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Database Cache Connection 36 | |-------------------------------------------------------------------------- 37 | | 38 | | When using the "database" cache driver you may specify the connection 39 | | that should be used to store the cached items. When this option is 40 | | null the default database connection will be utilized for cache. 41 | | 42 | */ 43 | 44 | 'connection' => null, 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Database Cache Table 49 | |-------------------------------------------------------------------------- 50 | | 51 | | When using the "database" cache driver we need to know the table that 52 | | should be used to store the cached items. A default table name has 53 | | been provided but you're free to change it however you deem fit. 54 | | 55 | */ 56 | 57 | 'table' => 'cache', 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Memcached Servers 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Now you may specify an array of your Memcached servers that should be 65 | | used when utilizing the Memcached cache driver. All of the servers 66 | | should contain a value for "host", "port", and "weight" options. 67 | | 68 | */ 69 | 70 | 'memcached' => array( 71 | 72 | array('host' => '127.0.0.1', 'port' => 11211, 'weight' => 100), 73 | 74 | ), 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Cache Key Prefix 79 | |-------------------------------------------------------------------------- 80 | | 81 | | When utilizing a RAM based store such as APC or Memcached, there might 82 | | be other applications utilizing the same cache. So, we'll specify a 83 | | value to get prefixed to all our keys so we can avoid collisions. 84 | | 85 | */ 86 | 87 | 'prefix' => 'laravel', 88 | 89 | ); 90 | -------------------------------------------------------------------------------- /app/config/compile.php: -------------------------------------------------------------------------------- 1 | PDO::FETCH_CLASS, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Database Connection Name 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may specify which of the database connections below you wish 24 | | to use as your default connection for all database work. Of course 25 | | you may use many connections at once using the Database library. 26 | | 27 | */ 28 | 29 | 'default' => 'mysql', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Database Connections 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here are each of the database connections setup for your application. 37 | | Of course, examples of configuring each database platform that is 38 | | supported by Laravel is shown below to make development simple. 39 | | 40 | | 41 | | All database work in Laravel is done through the PHP PDO facilities 42 | | so make sure you have the driver for your particular database of 43 | | choice installed on your machine before you begin development. 44 | | 45 | */ 46 | 47 | 'connections' => array( 48 | 49 | 'sqlite' => array( 50 | 'driver' => 'sqlite', 51 | 'database' => __DIR__.'/../database/production.sqlite', 52 | 'prefix' => '', 53 | ), 54 | 55 | 'mysql' => array( 56 | 'driver' => 'mysql', 57 | 'host' => 'localhost', 58 | 'database' => 'forge', 59 | 'username' => 'forge', 60 | 'password' => '', 61 | 'charset' => 'utf8', 62 | 'collation' => 'utf8_unicode_ci', 63 | 'prefix' => '', 64 | ), 65 | 66 | 'pgsql' => array( 67 | 'driver' => 'pgsql', 68 | 'host' => 'localhost', 69 | 'database' => 'forge', 70 | 'username' => 'forge', 71 | 'password' => '', 72 | 'charset' => 'utf8', 73 | 'prefix' => '', 74 | 'schema' => 'public', 75 | ), 76 | 77 | 'sqlsrv' => array( 78 | 'driver' => 'sqlsrv', 79 | 'host' => 'localhost', 80 | 'database' => 'database', 81 | 'username' => 'root', 82 | 'password' => '', 83 | 'prefix' => '', 84 | ), 85 | 86 | ), 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Migration Repository Table 91 | |-------------------------------------------------------------------------- 92 | | 93 | | This table keeps track of all the migrations that have already run for 94 | | your application. Using this information, we can determine which of 95 | | the migrations on disk haven't actually been run in the database. 96 | | 97 | */ 98 | 99 | 'migrations' => 'migrations', 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Redis Databases 104 | |-------------------------------------------------------------------------- 105 | | 106 | | Redis is an open source, fast, and advanced key-value store that also 107 | | provides a richer set of commands than a typical key-value systems 108 | | such as APC or Memcached. Laravel makes it easy to dig right in. 109 | | 110 | */ 111 | 112 | 'redis' => array( 113 | 114 | 'cluster' => false, 115 | 116 | 'default' => array( 117 | 'host' => '127.0.0.1', 118 | 'port' => 6379, 119 | 'database' => 0, 120 | ), 121 | 122 | ), 123 | 124 | ); 125 | -------------------------------------------------------------------------------- /app/config/local/app.php: -------------------------------------------------------------------------------- 1 | true, 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /app/config/local/database.php: -------------------------------------------------------------------------------- 1 | array( 22 | 23 | 'mysql' => array( 24 | 'driver' => 'mysql', 25 | 'host' => 'localhost', 26 | 'database' => 'homestead', 27 | 'username' => 'homestead', 28 | 'password' => 'secret', 29 | 'charset' => 'utf8', 30 | 'collation' => 'utf8_unicode_ci', 31 | 'prefix' => '', 32 | ), 33 | 34 | 'pgsql' => array( 35 | 'driver' => 'pgsql', 36 | 'host' => 'localhost', 37 | 'database' => 'homestead', 38 | 'username' => 'homestead', 39 | 'password' => 'secret', 40 | 'charset' => 'utf8', 41 | 'prefix' => '', 42 | 'schema' => 'public', 43 | ), 44 | 45 | ), 46 | 47 | ); 48 | -------------------------------------------------------------------------------- /app/config/mail.php: -------------------------------------------------------------------------------- 1 | 'smtp', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | SMTP Host Address 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may provide the host address of the SMTP server used by your 26 | | applications. A default option is provided that is compatible with 27 | | the Mailgun mail service which will provide reliable deliveries. 28 | | 29 | */ 30 | 31 | 'host' => 'smtp.mailgun.org', 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | SMTP Host Port 36 | |-------------------------------------------------------------------------- 37 | | 38 | | This is the SMTP port used by your application to deliver e-mails to 39 | | users of the application. Like the host we have set this value to 40 | | stay compatible with the Mailgun e-mail application by default. 41 | | 42 | */ 43 | 44 | 'port' => 587, 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Global "From" Address 49 | |-------------------------------------------------------------------------- 50 | | 51 | | You may wish for all e-mails sent by your application to be sent from 52 | | the same address. Here, you may specify a name and address that is 53 | | used globally for all e-mails that are sent by your application. 54 | | 55 | */ 56 | 57 | 'from' => array('address' => null, 'name' => null), 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | E-Mail Encryption Protocol 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Here you may specify the encryption protocol that should be used when 65 | | the application send e-mail messages. A sensible default using the 66 | | transport layer security protocol should provide great security. 67 | | 68 | */ 69 | 70 | 'encryption' => 'tls', 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | SMTP Server Username 75 | |-------------------------------------------------------------------------- 76 | | 77 | | If your SMTP server requires a username for authentication, you should 78 | | set it here. This will get used to authenticate with your server on 79 | | connection. You may also set the "password" value below this one. 80 | | 81 | */ 82 | 83 | 'username' => null, 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | SMTP Server Password 88 | |-------------------------------------------------------------------------- 89 | | 90 | | Here you may set the password required by your SMTP server to send out 91 | | messages from your application. This will be given to the server on 92 | | connection so that the application will be able to send messages. 93 | | 94 | */ 95 | 96 | 'password' => null, 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Sendmail System Path 101 | |-------------------------------------------------------------------------- 102 | | 103 | | When using the "sendmail" driver to send e-mails, we will need to know 104 | | the path to where Sendmail lives on this server. A default path has 105 | | been provided here, which will work well on most of your systems. 106 | | 107 | */ 108 | 109 | 'sendmail' => '/usr/sbin/sendmail -bs', 110 | 111 | /* 112 | |-------------------------------------------------------------------------- 113 | | Mail "Pretend" 114 | |-------------------------------------------------------------------------- 115 | | 116 | | When this option is enabled, e-mail will not actually be sent over the 117 | | web and will instead be written to your application's logs files so 118 | | you may inspect the message. This is great for local development. 119 | | 120 | */ 121 | 122 | 'pretend' => false, 123 | 124 | ); 125 | -------------------------------------------------------------------------------- /app/config/packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-laravel/1151b0befb06c979e0fae44fc14e38445411b28b/app/config/packages/.gitkeep -------------------------------------------------------------------------------- /app/config/queue.php: -------------------------------------------------------------------------------- 1 | 'sync', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Queue Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may configure the connection information for each server that 26 | | is used by your application. A default configuration has been added 27 | | for each back-end shipped with Laravel. You are free to add more. 28 | | 29 | */ 30 | 31 | 'connections' => array( 32 | 33 | 'sync' => array( 34 | 'driver' => 'sync', 35 | ), 36 | 37 | 'beanstalkd' => array( 38 | 'driver' => 'beanstalkd', 39 | 'host' => 'localhost', 40 | 'queue' => 'default', 41 | 'ttr' => 60, 42 | ), 43 | 44 | 'sqs' => array( 45 | 'driver' => 'sqs', 46 | 'key' => 'your-public-key', 47 | 'secret' => 'your-secret-key', 48 | 'queue' => 'your-queue-url', 49 | 'region' => 'us-east-1', 50 | ), 51 | 52 | 'iron' => array( 53 | 'driver' => 'iron', 54 | 'host' => 'mq-aws-us-east-1.iron.io', 55 | 'token' => 'your-token', 56 | 'project' => 'your-project-id', 57 | 'queue' => 'your-queue-name', 58 | 'encrypt' => true, 59 | ), 60 | 61 | 'redis' => array( 62 | 'driver' => 'redis', 63 | 'queue' => 'default', 64 | ), 65 | 66 | ), 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Failed Queue Jobs 71 | |-------------------------------------------------------------------------- 72 | | 73 | | These options configure the behavior of failed queue job logging so you 74 | | can control which database and table are used to store the jobs that 75 | | have failed. You may change them to any database / table you wish. 76 | | 77 | */ 78 | 79 | 'failed' => array( 80 | 81 | 'database' => 'mysql', 'table' => 'failed_jobs', 82 | 83 | ), 84 | 85 | ); 86 | -------------------------------------------------------------------------------- /app/config/remote.php: -------------------------------------------------------------------------------- 1 | 'production', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Remote Server Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | These are the servers that will be accessible via the SSH task runner 24 | | facilities of Laravel. This feature radically simplifies executing 25 | | tasks on your servers, such as deploying out these applications. 26 | | 27 | */ 28 | 29 | 'connections' => array( 30 | 31 | 'production' => array( 32 | 'host' => '', 33 | 'username' => '', 34 | 'password' => '', 35 | 'key' => '', 36 | 'keyphrase' => '', 37 | 'root' => '/var/www', 38 | ), 39 | 40 | ), 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Remote Server Groups 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you may list connections under a single group name, which allows 48 | | you to easily access all of the servers at once using a short name 49 | | that is extremely easy to remember, such as "web" or "database". 50 | | 51 | */ 52 | 53 | 'groups' => array( 54 | 55 | 'web' => array('production') 56 | 57 | ), 58 | 59 | ); 60 | -------------------------------------------------------------------------------- /app/config/services.php: -------------------------------------------------------------------------------- 1 | array( 18 | 'domain' => '', 19 | 'secret' => '', 20 | ), 21 | 22 | 'mandrill' => array( 23 | 'secret' => '', 24 | ), 25 | 26 | 'stripe' => array( 27 | 'model' => 'User', 28 | 'secret' => '', 29 | ), 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /app/config/session.php: -------------------------------------------------------------------------------- 1 | 'file', 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Session Lifetime 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may specify the number of minutes that you wish the session 27 | | to be allowed to remain idle before it expires. If you want them 28 | | to immediately expire on the browser closing, set that option. 29 | | 30 | */ 31 | 32 | 'lifetime' => 120, 33 | 34 | 'expire_on_close' => false, 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Session File Location 39 | |-------------------------------------------------------------------------- 40 | | 41 | | When using the native session driver, we need a location where session 42 | | files may be stored. A default has been set for you but a different 43 | | location may be specified. This is only needed for file sessions. 44 | | 45 | */ 46 | 47 | 'files' => storage_path().'/sessions', 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Session Database Connection 52 | |-------------------------------------------------------------------------- 53 | | 54 | | When using the "database" or "redis" session drivers, you may specify a 55 | | connection that should be used to manage these sessions. This should 56 | | correspond to a connection in your database configuration options. 57 | | 58 | */ 59 | 60 | 'connection' => null, 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Session Database Table 65 | |-------------------------------------------------------------------------- 66 | | 67 | | When using the "database" session driver, you may specify the table we 68 | | should use to manage the sessions. Of course, a sensible default is 69 | | provided for you; however, you are free to change this as needed. 70 | | 71 | */ 72 | 73 | 'table' => 'sessions', 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Session Sweeping Lottery 78 | |-------------------------------------------------------------------------- 79 | | 80 | | Some session drivers must manually sweep their storage location to get 81 | | rid of old sessions from storage. Here are the chances that it will 82 | | happen on a given request. By default, the odds are 2 out of 100. 83 | | 84 | */ 85 | 86 | 'lottery' => array(2, 100), 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Session Cookie Name 91 | |-------------------------------------------------------------------------- 92 | | 93 | | Here you may change the name of the cookie used to identify a session 94 | | instance by ID. The name specified here will get used every time a 95 | | new session cookie is created by the framework for every driver. 96 | | 97 | */ 98 | 99 | 'cookie' => 'laravel_session', 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Session Cookie Path 104 | |-------------------------------------------------------------------------- 105 | | 106 | | The session cookie path determines the path for which the cookie will 107 | | be regarded as available. Typically, this will be the root path of 108 | | your application but you are free to change this when necessary. 109 | | 110 | */ 111 | 112 | 'path' => '/', 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Session Cookie Domain 117 | |-------------------------------------------------------------------------- 118 | | 119 | | Here you may change the domain of the cookie used to identify a session 120 | | in your application. This will determine which domains the cookie is 121 | | available to in your application. A sensible default has been set. 122 | | 123 | */ 124 | 125 | 'domain' => null, 126 | 127 | /* 128 | |-------------------------------------------------------------------------- 129 | | HTTPS Only Cookies 130 | |-------------------------------------------------------------------------- 131 | | 132 | | By setting this option to true, session cookies will only be sent back 133 | | to the server if the browser has a HTTPS connection. This will keep 134 | | the cookie from being sent to you if it can not be done securely. 135 | | 136 | */ 137 | 138 | 'secure' => false, 139 | 140 | ); 141 | -------------------------------------------------------------------------------- /app/config/testing/cache.php: -------------------------------------------------------------------------------- 1 | 'array', 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /app/config/testing/session.php: -------------------------------------------------------------------------------- 1 | 'array', 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /app/config/view.php: -------------------------------------------------------------------------------- 1 | array(__DIR__.'/../views'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Pagination View 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This view will be used to render the pagination link output, and can 24 | | be easily customized here to show any view you like. A clean view 25 | | compatible with Twitter's Bootstrap is given to you by default. 26 | | 27 | */ 28 | 29 | 'pagination' => 'pagination::slider-3', 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /app/config/workbench.php: -------------------------------------------------------------------------------- 1 | '', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Workbench Author E-Mail Address 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Like the option above, your e-mail address is used when generating new 24 | | workbench packages. The e-mail is placed in your composer.json file 25 | | automatically after the package is created by the workbench tool. 26 | | 27 | */ 28 | 29 | 'email' => '', 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-laravel/1151b0befb06c979e0fae44fc14e38445411b28b/app/controllers/.gitkeep -------------------------------------------------------------------------------- /app/controllers/BaseController.php: -------------------------------------------------------------------------------- 1 | layout)) 13 | { 14 | $this->layout = View::make($this->layout); 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /app/database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-laravel/1151b0befb06c979e0fae44fc14e38445411b28b/app/database/migrations/.gitkeep -------------------------------------------------------------------------------- /app/database/seeds/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-laravel/1151b0befb06c979e0fae44fc14e38445411b28b/app/database/seeds/.gitkeep -------------------------------------------------------------------------------- /app/database/seeds/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call('UserTableSeeder'); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/filters.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 18 | 'next' => 'Next »', 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /app/lang/en/reminders.php: -------------------------------------------------------------------------------- 1 | "Passwords must be at least six characters and match the confirmation.", 17 | 18 | "user" => "We can't find a user with that e-mail address.", 19 | 20 | "token" => "This password reset token is invalid.", 21 | 22 | "sent" => "Password reminder sent!", 23 | 24 | "reset" => "Password has been reset!", 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /app/lang/en/validation.php: -------------------------------------------------------------------------------- 1 | "The :attribute must be accepted.", 17 | "active_url" => "The :attribute is not a valid URL.", 18 | "after" => "The :attribute must be a date after :date.", 19 | "alpha" => "The :attribute may only contain letters.", 20 | "alpha_dash" => "The :attribute may only contain letters, numbers, and dashes.", 21 | "alpha_num" => "The :attribute may only contain letters and numbers.", 22 | "array" => "The :attribute must be an array.", 23 | "before" => "The :attribute must be a date before :date.", 24 | "between" => array( 25 | "numeric" => "The :attribute must be between :min and :max.", 26 | "file" => "The :attribute must be between :min and :max kilobytes.", 27 | "string" => "The :attribute must be between :min and :max characters.", 28 | "array" => "The :attribute must have between :min and :max items.", 29 | ), 30 | "boolean" => "The :attribute field must be true or false", 31 | "confirmed" => "The :attribute confirmation does not match.", 32 | "date" => "The :attribute is not a valid date.", 33 | "date_format" => "The :attribute does not match the format :format.", 34 | "different" => "The :attribute and :other must be different.", 35 | "digits" => "The :attribute must be :digits digits.", 36 | "digits_between" => "The :attribute must be between :min and :max digits.", 37 | "email" => "The :attribute must be a valid email address.", 38 | "exists" => "The selected :attribute is invalid.", 39 | "image" => "The :attribute must be an image.", 40 | "in" => "The selected :attribute is invalid.", 41 | "integer" => "The :attribute must be an integer.", 42 | "ip" => "The :attribute must be a valid IP address.", 43 | "max" => array( 44 | "numeric" => "The :attribute may not be greater than :max.", 45 | "file" => "The :attribute may not be greater than :max kilobytes.", 46 | "string" => "The :attribute may not be greater than :max characters.", 47 | "array" => "The :attribute may not have more than :max items.", 48 | ), 49 | "mimes" => "The :attribute must be a file of type: :values.", 50 | "min" => array( 51 | "numeric" => "The :attribute must be at least :min.", 52 | "file" => "The :attribute must be at least :min kilobytes.", 53 | "string" => "The :attribute must be at least :min characters.", 54 | "array" => "The :attribute must have at least :min items.", 55 | ), 56 | "not_in" => "The selected :attribute is invalid.", 57 | "numeric" => "The :attribute must be a number.", 58 | "regex" => "The :attribute format is invalid.", 59 | "required" => "The :attribute field is required.", 60 | "required_if" => "The :attribute field is required when :other is :value.", 61 | "required_with" => "The :attribute field is required when :values is present.", 62 | "required_with_all" => "The :attribute field is required when :values is present.", 63 | "required_without" => "The :attribute field is required when :values is not present.", 64 | "required_without_all" => "The :attribute field is required when none of :values are present.", 65 | "same" => "The :attribute and :other must match.", 66 | "size" => array( 67 | "numeric" => "The :attribute must be :size.", 68 | "file" => "The :attribute must be :size kilobytes.", 69 | "string" => "The :attribute must be :size characters.", 70 | "array" => "The :attribute must contain :size items.", 71 | ), 72 | "unique" => "The :attribute has already been taken.", 73 | "url" => "The :attribute format is invalid.", 74 | "timezone" => "The :attribute must be a valid zone.", 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Custom Validation Language Lines 79 | |-------------------------------------------------------------------------- 80 | | 81 | | Here you may specify custom validation messages for attributes using the 82 | | convention "attribute.rule" to name the lines. This makes it quick to 83 | | specify a specific custom language line for a given attribute rule. 84 | | 85 | */ 86 | 87 | 'custom' => array( 88 | 'attribute-name' => array( 89 | 'rule-name' => 'custom-message', 90 | ), 91 | ), 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Custom Validation Attributes 96 | |-------------------------------------------------------------------------- 97 | | 98 | | The following language lines are used to swap attribute place-holders 99 | | with something more reader friendly such as E-Mail Address instead 100 | | of "email". This simply helps us make messages a little cleaner. 101 | | 102 | */ 103 | 104 | 'attributes' => array(), 105 | 106 | ); 107 | -------------------------------------------------------------------------------- /app/models/User.php: -------------------------------------------------------------------------------- 1 | /.env.php 27 | $client = new Twilio\Rest\Client( 28 | getenv('TWILIO_ACCOUNT_SID'), 29 | getenv('TWILIO_AUTH_TOKEN') 30 | ); 31 | 32 | try { 33 | $client->calls->create( 34 | $userPhone, // The visitor's phone number 35 | getenv('TWILIO_NUMBER'), // A Twilio number in your account 36 | array( 37 | "url" => "http://$host/outbound/$encodedSalesPhone" 38 | ) 39 | ); 40 | } catch (Exception $e) { 41 | // Failed calls will throw 42 | return $e; 43 | } 44 | 45 | // return a JSON response 46 | return array('message' => 'Call incoming!'); 47 | }); 48 | 49 | // POST URL to handle form submission and make outbound call 50 | Route::post('/outbound/{salesPhone}', function ($salesPhone) { 51 | // A message for Twilio's TTS engine to repeat 52 | $sayMessage = 'Thanks for contacting our sales department. Our 53 | next available representative will take your call.'; 54 | 55 | $twiml = new Twilio\Twiml(); 56 | $twiml->say($sayMessage, array('voice' => 'alice')); 57 | $twiml->dial($salesPhone); 58 | 59 | $response = Response::make($twiml, 200); 60 | $response->header('Content-Type', 'text/xml'); 61 | return $response; 62 | }); 63 | -------------------------------------------------------------------------------- /app/start/artisan.php: -------------------------------------------------------------------------------- 1 | client->request('GET', '/'); 13 | 14 | $this->assertTrue($this->client->getResponse()->isOk()); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Password Reset

8 | 9 |
10 | To reset your password, complete this form: {{ URL::to('password/reset', array($token)) }}.
11 | This link will expire in {{ Config::get('auth.reminder.expire', 60) }} minutes. 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app/views/home.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Click To Call Tutorial 5 | 6 | 8 | 10 | 11 | 12 | 13 | 14 |
15 |

Click To Call

16 |

17 | Click To Call converts your website's users into engaged customers by 18 | creating an easy way for your customers to contact your sales and 19 | support teams right on your website. 20 |

21 |

Here's an example of how it's done!

22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 |

Call Sales

30 |

31 | Are you interested in impressing your friends and 32 | confounding your enemies? Enter your phone number 33 | below, and our team will contact you right away. 34 |

35 |
36 | 37 |
38 | 40 |
41 | 42 |
43 | 45 |
46 | 49 |
50 |
51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | setRequestForConsoleEnvironment(); 45 | 46 | $artisan = Illuminate\Console\Application::start($app); 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Run The Artisan Application 51 | |-------------------------------------------------------------------------- 52 | | 53 | | When we run the console application, the current CLI command will be 54 | | executed in this console and the response sent back to a terminal 55 | | or another output device for the developers. Here goes nothing! 56 | | 57 | */ 58 | 59 | $status = $artisan->run(); 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Shutdown The Application 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Once Artisan has finished running. We will fire off the shutdown events 67 | | so that any final work may be done by the application before we shut 68 | | down the process. This is the last thing to happen to the request. 69 | | 70 | */ 71 | 72 | $app->shutdown(); 73 | 74 | exit($status); 75 | -------------------------------------------------------------------------------- /bootstrap/autoload.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/../app', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Public Path 21 | |-------------------------------------------------------------------------- 22 | | 23 | | The public path contains the assets for your web application, such as 24 | | your JavaScript and CSS files, and also contains the primary entry 25 | | point for web requests into these applications from the outside. 26 | | 27 | */ 28 | 29 | 'public' => __DIR__.'/../public', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Base Path 34 | |-------------------------------------------------------------------------- 35 | | 36 | | The base path is the root of the Laravel installation. Most likely you 37 | | will not need to change this value. But, if for some wild reason it 38 | | is necessary you will do so here, just proceed with some caution. 39 | | 40 | */ 41 | 42 | 'base' => __DIR__.'/..', 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Storage Path 47 | |-------------------------------------------------------------------------- 48 | | 49 | | The storage path is used by Laravel to store cached Blade views, logs 50 | | and other pieces of information. You may modify the path here when 51 | | you want to change the location of this directory for your apps. 52 | | 53 | */ 54 | 55 | 'storage' => __DIR__.'/../app/storage', 56 | 57 | ); 58 | -------------------------------------------------------------------------------- /bootstrap/start.php: -------------------------------------------------------------------------------- 1 | detectEnvironment(array( 28 | 29 | 'local' => array('homestead'), 30 | 31 | )); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Bind Paths 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Here we are binding the paths configured in paths.php to the app. You 39 | | should not be changing these here. If you need to change these you 40 | | may do so within the paths.php file and they will be bound here. 41 | | 42 | */ 43 | 44 | $app->bindInstallPaths(require __DIR__.'/paths.php'); 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Load The Application 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Here we will load this Illuminate application. We will keep this in a 52 | | separate location so we can isolate the creation of an application 53 | | from the actual running of the application with a given request. 54 | | 55 | */ 56 | 57 | $framework = $app['path.base']. 58 | '/vendor/laravel/framework/src'; 59 | 60 | require $framework.'/Illuminate/Foundation/start.php'; 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Return The Application 65 | |-------------------------------------------------------------------------- 66 | | 67 | | This script returns the application instance. The instance is given to 68 | | the calling script so we can separate the building of the instances 69 | | from the actual running of the application and sending responses. 70 | | 71 | */ 72 | 73 | return $app; 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "description": "The Laravel Framework.", 4 | "keywords": [ 5 | "framework", 6 | "laravel" 7 | ], 8 | "license": "MIT", 9 | "type": "project", 10 | "require": { 11 | "php": "^5.5.0", 12 | "laravel/framework": "4.2.*", 13 | "twilio/sdk": "5.0.0-RC19" 14 | }, 15 | "autoload": { 16 | "classmap": [ 17 | "app/commands", 18 | "app/controllers", 19 | "app/models", 20 | "app/database/migrations", 21 | "app/database/seeds", 22 | "app/tests/TestCase.php" 23 | ] 24 | }, 25 | "scripts": { 26 | "post-install-cmd": [ 27 | "php artisan clear-compiled", 28 | "php artisan optimize" 29 | ], 30 | "post-update-cmd": [ 31 | "php artisan clear-compiled", 32 | "php artisan optimize" 33 | ], 34 | "post-create-project-cmd": [ 35 | "php artisan key:generate" 36 | ] 37 | }, 38 | "config": { 39 | "preferred-install": "dist" 40 | }, 41 | "minimum-stability": "stable" 42 | } 43 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./app/tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Redirect Trailing Slashes... 9 | RewriteRule ^(.*)/$ /$1 [L,R=301] 10 | 11 | # Handle Front Controller... 12 | RewriteCond %{REQUEST_FILENAME} !-d 13 | RewriteCond %{REQUEST_FILENAME} !-f 14 | RewriteRule ^ index.php [L] 15 | 16 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | // Execute JavaScript on page load 2 | $(function() { 3 | // Initialize phone number text input plugin 4 | $('#userPhone, #salesPhone').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 | userPhone: $('#userPhone').val(), 23 | salesPhone: $('#salesPhone').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 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-laravel/1151b0befb06c979e0fae44fc14e38445411b28b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Register The Auto Loader 12 | |-------------------------------------------------------------------------- 13 | | 14 | | Composer provides a convenient, automatically generated class loader 15 | | for our application. We just need to utilize it! We'll require it 16 | | into the script here so that we do not have to worry about the 17 | | loading of any our classes "manually". Feels great to relax. 18 | | 19 | */ 20 | 21 | require __DIR__.'/../bootstrap/autoload.php'; 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Turn On The Lights 26 | |-------------------------------------------------------------------------- 27 | | 28 | | We need to illuminate PHP development, so let's turn on the lights. 29 | | This bootstraps the framework and gets it ready for use, then it 30 | | will load up this application so that we can run it and send 31 | | the responses back to the browser and delight these users. 32 | | 33 | */ 34 | 35 | $app = require_once __DIR__.'/../bootstrap/start.php'; 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Run The Application 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Once we have the application, we can simply call the run method, 43 | | which will execute the request and send the response back to 44 | | the client's browser allowing them to enjoy the creative 45 | | and wonderful application we have whipped up for them. 46 | | 47 | */ 48 | 49 | $app->run(); 50 | -------------------------------------------------------------------------------- /public/landing.html: -------------------------------------------------------------------------------- 1 | 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/packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-laravel/1151b0befb06c979e0fae44fc14e38445411b28b/public/packages/.gitkeep -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/img/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/clicktocall-laravel/1151b0befb06c979e0fae44fc14e38445411b28b/public/vendor/intl-phone/img/flags.png -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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 | Twilio 3 | 4 | 5 | # Deprecated Tutorial - Use Click to Call PHP 6 | 7 | We have deprecated this Laravel tutorial, but please try our [Click to Call PHP example](https://github.com/TwilioDevEd/clicktocall-php) 8 | 9 | [Learn more about Click to Call here](https://www.twilio.com/docs/voice/tutorials/click-to-call)! 10 | 11 | ## Local Development 12 | 13 | 1. Grab latest source 14 | 15 | ```bash 16 | $ git clone git://github.com/TwilioDevEd/clicktocall-laravel.git 17 | ``` 18 | 19 | 1. Setup your environment variables 20 | 21 | Are you using a bash shell? Use echo $SHELL to find out. For a bash 22 | shell, edit the ~/.bashrc or ~/.bashprofile file and add: 23 | 24 | ```sh 25 | export TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxx 26 | export TWILIO_AUTH_TOKEN=yyyyyyyyyyyyyyyyy 27 | export TWILIO_NUMBER=+15556667777 28 | ``` 29 | 30 | Or just export the same variables for this session. 31 | 32 | 1. Install the dependencies with [Composer](https://getcomposer.org/). 33 | 34 | ```bash 35 | $ composer install 36 | ``` 37 | 38 | 1. Generate an `APP_KEY`. 39 | 40 | ```bash 41 | $ php artisan key:generate 42 | ``` 43 | 44 | 1. Start the server. 45 | 46 | ```bash 47 | $ php artisan serve 48 | ``` 49 | 50 | 1. [Expose the application to the wider internet](#expose-the-application-to-the-wider-internet) 51 | 52 | 1. Check out the app at `http://.ngrok.io`. 53 | 54 | ### Expose the Application to the Wider Internet 55 | 56 | 1. Expose your application to the wider internet using [ngrok](http://ngrok.com). 57 | You can click[here](#expose-the-application-to-the-wider-internet) 58 | for more details. This step is important because the application won't 59 | work as expected if you run it through localhost. 60 | 61 | ```bash 62 | $ ngrok http 8000 63 | ``` 64 | 65 | Once ngrok is running, open up your browser and go to your ngrok URL. 66 | It will look something like this: `http://.ngrok.io` 67 | 68 | ## Meta 69 | 70 | * No warranty expressed or implied. Software is as is. Diggity. 71 | * [MIT License](http://www.opensource.org/licenses/mit-license.html) 72 | * Lovingly crafted by Twilio Developer Education. 73 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 |