Repository: jackocnr/intl-tel-input Branch: master Commit: 419966a2c2dc Files: 394 Total size: 874.7 KB Directory structure: gitextract_v3v4kr9z/ ├── .eslintignore ├── .eslintrc.js ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 1_bug_report.yml │ │ ├── 2_feature_request.yml │ │ └── config.yml │ ├── copilot-instructions.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .node-version ├── .npmrc ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── angular/ │ ├── README.md │ ├── build.js │ ├── demo/ │ │ ├── form/ │ │ │ ├── form.component.ts │ │ │ ├── index.html │ │ │ └── main.ts │ │ ├── set-number/ │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ └── set-number.component.ts │ │ ├── simple/ │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ └── simple.component.ts │ │ ├── toggle-disabled/ │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ └── toggle-disabled.component.ts │ │ └── validation/ │ │ ├── index.html │ │ ├── main.ts │ │ └── validation.component.ts │ ├── src/ │ │ └── intl-tel-input/ │ │ ├── angular.ts │ │ └── angularWithUtils.ts │ └── tsconfig.json ├── build.js ├── composer.json ├── cspell.json ├── demo.html ├── functions/ │ └── _middleware.js ├── grunt/ │ ├── bump.js │ ├── clean.js │ ├── closure-compiler.js │ ├── connect.js │ ├── cssmin.js │ ├── generate-sprite.js │ ├── replace.js │ ├── sass.js │ ├── shell.js │ ├── translations.js │ └── watch.js ├── index.js ├── jest.config.js ├── package.json ├── playwright.config.ts ├── react/ │ ├── README.md │ ├── build.js │ ├── demo/ │ │ ├── set-number/ │ │ │ ├── SetNumberApp.tsx │ │ │ └── set-number.html │ │ ├── simple/ │ │ │ ├── SimpleApp.tsx │ │ │ └── simple.html │ │ ├── toggle-disabled/ │ │ │ ├── ToggleDisabledApp.tsx │ │ │ └── toggle-disabled.html │ │ └── validation/ │ │ ├── ValidationApp.tsx │ │ └── validation.html │ ├── src/ │ │ └── intl-tel-input/ │ │ ├── react.tsx │ │ └── reactWithUtils.tsx │ └── tsconfig.json ├── scripts/ │ ├── check-lpn-metadata.cjs │ └── playwright-linux-docker.sh ├── site/ │ ├── .gitignore │ ├── Gruntfile.js │ ├── README.md │ ├── esbuild/ │ │ ├── build.mjs │ │ └── externalUtilsPlugin.mjs │ ├── grunt/ │ │ ├── copy.js │ │ ├── cssmin.js │ │ ├── fetchStats.js │ │ ├── replace.js │ │ ├── sass.js │ │ ├── shell.js │ │ ├── template.js │ │ ├── templateGruntHelpers.js │ │ ├── templateNav.js │ │ ├── templateUtils.js │ │ └── watch.js │ ├── package.json │ ├── src/ │ │ ├── 404/ │ │ │ ├── 404_content.html │ │ │ └── 404_page_template.html.ejs │ │ ├── css/ │ │ │ ├── _base.scss │ │ │ ├── _forms.scss │ │ │ ├── _layout.scss │ │ │ ├── _navbar.scss │ │ │ ├── _variables.scss │ │ │ ├── docs.scss │ │ │ ├── highlightjs_overrides.scss │ │ │ ├── homepage.scss │ │ │ ├── large_flags_overrides.scss │ │ │ ├── playground.scss │ │ │ └── website.scss │ │ ├── docs/ │ │ │ ├── docs_content_template.html.ejs │ │ │ ├── docs_nav_template.html.ejs │ │ │ ├── docs_page_template.html.ejs │ │ │ └── markdown/ │ │ │ ├── accessibility.md │ │ │ ├── angular_component.md │ │ │ ├── choose_integration.md │ │ │ ├── events.md │ │ │ ├── faq.md │ │ │ ├── getting_started.md │ │ │ ├── localisation.md │ │ │ ├── methods.md │ │ │ ├── options.md │ │ │ ├── react_component.md │ │ │ ├── svelte_component.md │ │ │ ├── theming.md │ │ │ ├── troubleshooting.md │ │ │ ├── utils.md │ │ │ └── vue_component.md │ │ ├── examples/ │ │ │ ├── copy/ │ │ │ │ ├── angular_component_desc.html │ │ │ │ ├── display_number_desc.html │ │ │ │ ├── hidden_input_desc.html │ │ │ │ ├── large_flags_desc.html │ │ │ │ ├── lookup_country_desc.html │ │ │ │ ├── multiple_instances_desc.html │ │ │ │ ├── react_component_desc.html │ │ │ │ ├── right_to_left_desc.html │ │ │ │ ├── single_country_desc.html │ │ │ │ ├── svelte_component_desc.html │ │ │ │ ├── validation_practical_desc.html │ │ │ │ ├── validation_precise_desc.html │ │ │ │ └── vue_component_desc.html │ │ │ ├── css/ │ │ │ │ ├── multiple_instances.css │ │ │ │ └── validation.css │ │ │ ├── examples_content_template.html.ejs │ │ │ ├── examples_nav_template.html.ejs │ │ │ ├── examples_page_template.html.ejs │ │ │ ├── html/ │ │ │ │ ├── component.html │ │ │ │ ├── display_number.html │ │ │ │ ├── display_number_display_code.html │ │ │ │ ├── multiple_instances.html │ │ │ │ ├── multiple_instances_display_code.html │ │ │ │ ├── simple_input.html │ │ │ │ ├── simple_input_display_code.html │ │ │ │ ├── validation.html │ │ │ │ └── validation_display_code.html │ │ │ └── js/ │ │ │ ├── angular_component.ts │ │ │ ├── angular_component_display_code.js │ │ │ ├── hidden_input.js │ │ │ ├── hidden_input_display_code.js │ │ │ ├── lookup_country.js │ │ │ ├── lookup_country_display_code.js │ │ │ ├── multiple_instances.js │ │ │ ├── multiple_instances_display_code.js │ │ │ ├── react_component.js │ │ │ ├── react_component_display_code.js │ │ │ ├── right_to_left.js │ │ │ ├── right_to_left_display_code.js │ │ │ ├── simple_init_plugin.js │ │ │ ├── simple_init_plugin_display_code.js │ │ │ ├── single_country.js │ │ │ ├── single_country_display_code.js │ │ │ ├── svelte_component.svelte │ │ │ ├── svelte_component_display_code.svelte │ │ │ ├── svelte_main.js │ │ │ ├── validation.js │ │ │ ├── validation_display_code.js │ │ │ ├── viteSvelteDemo.config.mjs │ │ │ ├── viteVueDemo.config.js │ │ │ ├── vue_component.vue │ │ │ ├── vue_component_display_code.vue │ │ │ └── vue_main.js │ │ ├── homepage/ │ │ │ ├── homepage_content.html │ │ │ └── homepage_page_template.html.ejs │ │ ├── js/ │ │ │ ├── homepage.js │ │ │ └── iti-live-results.js │ │ ├── layout_template.html.ejs │ │ ├── playground/ │ │ │ ├── js/ │ │ │ │ ├── modules/ │ │ │ │ │ ├── clipboard.js │ │ │ │ │ ├── forms.js │ │ │ │ │ ├── i18n.js │ │ │ │ │ ├── initCode.js │ │ │ │ │ ├── itiController.js │ │ │ │ │ ├── playgroundConfig.js │ │ │ │ │ ├── stateUtils.js │ │ │ │ │ └── urlState.js │ │ │ │ ├── playground.js │ │ │ │ └── templates/ │ │ │ │ └── playgroundConstants.js.ejs │ │ │ ├── playground_content.html │ │ │ └── playground_page_template.html.ejs │ │ └── shared/ │ │ ├── common_body_end.html │ │ ├── common_head_end_prod.html │ │ ├── common_meta_tags.html │ │ ├── common_styles.html.ejs │ │ ├── iti_live_results_script.html.ejs │ │ └── iti_script.html.ejs │ └── static/ │ ├── _redirects │ ├── ads.txt │ ├── css/ │ │ └── intlTelInput-largeFlags.css │ └── screenshotting.html ├── src/ │ ├── css/ │ │ ├── _metadata.scss │ │ ├── demo.scss │ │ ├── intlTelInput.scss │ │ └── intlTelInputWithAssets.scss │ └── js/ │ ├── intl-tel-input/ │ │ ├── data.ts │ │ ├── i18n/ │ │ │ ├── ar/ │ │ │ │ └── index.ts │ │ │ ├── bg/ │ │ │ │ └── index.ts │ │ │ ├── bn/ │ │ │ │ └── index.ts │ │ │ ├── bs/ │ │ │ │ └── index.ts │ │ │ ├── ca/ │ │ │ │ └── index.ts │ │ │ ├── cs/ │ │ │ │ └── index.ts │ │ │ ├── da/ │ │ │ │ └── index.ts │ │ │ ├── de/ │ │ │ │ └── index.ts │ │ │ ├── el/ │ │ │ │ └── index.ts │ │ │ ├── en/ │ │ │ │ └── index.ts │ │ │ ├── es/ │ │ │ │ └── index.ts │ │ │ ├── et/ │ │ │ │ └── index.ts │ │ │ ├── fa/ │ │ │ │ └── index.ts │ │ │ ├── fi/ │ │ │ │ └── index.ts │ │ │ ├── fr/ │ │ │ │ └── index.ts │ │ │ ├── hi/ │ │ │ │ └── index.ts │ │ │ ├── hr/ │ │ │ │ └── index.ts │ │ │ ├── hu/ │ │ │ │ └── index.ts │ │ │ ├── id/ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── it/ │ │ │ │ └── index.ts │ │ │ ├── ja/ │ │ │ │ └── index.ts │ │ │ ├── kn/ │ │ │ │ └── index.ts │ │ │ ├── ko/ │ │ │ │ └── index.ts │ │ │ ├── lt/ │ │ │ │ └── index.ts │ │ │ ├── mr/ │ │ │ │ └── index.ts │ │ │ ├── nl/ │ │ │ │ └── index.ts │ │ │ ├── no/ │ │ │ │ └── index.ts │ │ │ ├── pl/ │ │ │ │ └── index.ts │ │ │ ├── pt/ │ │ │ │ └── index.ts │ │ │ ├── ro/ │ │ │ │ └── index.ts │ │ │ ├── ru/ │ │ │ │ └── index.ts │ │ │ ├── sk/ │ │ │ │ └── index.ts │ │ │ ├── sl/ │ │ │ │ └── index.ts │ │ │ ├── sq/ │ │ │ │ └── index.ts │ │ │ ├── sr/ │ │ │ │ └── index.ts │ │ │ ├── sv/ │ │ │ │ └── index.ts │ │ │ ├── te/ │ │ │ │ └── index.ts │ │ │ ├── th/ │ │ │ │ └── index.ts │ │ │ ├── tr/ │ │ │ │ └── index.ts │ │ │ ├── types.ts │ │ │ ├── uk/ │ │ │ │ └── index.ts │ │ │ ├── ur/ │ │ │ │ └── index.ts │ │ │ ├── uz/ │ │ │ │ └── index.ts │ │ │ ├── vi/ │ │ │ │ └── index.ts │ │ │ ├── zh/ │ │ │ │ └── index.ts │ │ │ └── zh-hk/ │ │ │ └── index.ts │ │ └── intlTelInputWithUtils.ts │ ├── intl-tel-input.ts │ ├── modules/ │ │ ├── constants.ts │ │ ├── core/ │ │ │ ├── countrySearch.ts │ │ │ ├── icons.ts │ │ │ ├── numerals.ts │ │ │ ├── options.ts │ │ │ └── ui.ts │ │ ├── data/ │ │ │ ├── country-data.ts │ │ │ ├── intl-regionless.ts │ │ │ └── nanp-regionless.ts │ │ ├── format/ │ │ │ ├── caret.ts │ │ │ └── formatting.ts │ │ ├── types/ │ │ │ ├── events.ts │ │ │ ├── forEachInstanceArgsMap.ts │ │ │ └── public-api.ts │ │ └── utils/ │ │ ├── dom.ts │ │ ├── isAndroid.ts │ │ └── string.ts │ └── utils.js ├── svelte/ │ ├── README.md │ ├── demo/ │ │ ├── set-number/ │ │ │ ├── App.svelte │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ └── vite.config.mjs │ │ ├── simple/ │ │ │ ├── App.svelte │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ └── vite.config.mjs │ │ ├── toggle-disabled/ │ │ │ ├── App.svelte │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ └── vite.config.mjs │ │ └── validation/ │ │ ├── App.svelte │ │ ├── index.html │ │ ├── main.js │ │ └── vite.config.mjs │ ├── src/ │ │ └── intl-tel-input/ │ │ ├── IntlTelInput.svelte │ │ └── IntlTelInputWithUtils.svelte │ ├── viteConfig.mjs │ └── viteConfigWithUtils.mjs ├── tests/ │ ├── integration/ │ │ ├── core/ │ │ │ ├── dropdownShortcuts.test.js │ │ │ ├── easternNumerals.test.js │ │ │ ├── initialValues.test.js │ │ │ ├── multipleInstances.test.js │ │ │ ├── regionless.test.js │ │ │ ├── usingDropdown.test.js │ │ │ └── usingInput.test.js │ │ ├── events/ │ │ │ ├── closeCountryDropdownEvent.test.js │ │ │ ├── countryChangeEvent.test.js │ │ │ └── openCountryDropdownEvent.test.js │ │ ├── helpers/ │ │ │ ├── helpers.js │ │ │ └── matchers.js │ │ ├── methods/ │ │ │ ├── destroy.test.js │ │ │ ├── getExtension.test.js │ │ │ ├── getInstance.test.js │ │ │ ├── getNumber.test.js │ │ │ ├── getNumberType.test.js │ │ │ ├── getSelectedCountryData.test.js │ │ │ ├── getValidationError.test.js │ │ │ ├── isValidNumber.test.js │ │ │ ├── isValidNumberPrecise.test.js │ │ │ ├── setCountry.test.js │ │ │ ├── setDisabled.test.js │ │ │ ├── setNumber.test.js │ │ │ └── setPlaceholderNumberType.test.js │ │ ├── options/ │ │ │ ├── allowDropdown.test.js │ │ │ ├── allowNumberExtensions.test.js │ │ │ ├── allowPhonewords.test.js │ │ │ ├── allowedNumberTypes.test.js │ │ │ ├── autoPlaceholder.test.js │ │ │ ├── containerClass.test.js │ │ │ ├── countryNameLocale.test.js │ │ │ ├── countryOrder.test.js │ │ │ ├── countrySearch.test.js │ │ │ ├── customPlaceholder.test.js │ │ │ ├── dropdownContainer.test.js │ │ │ ├── excludeCountries.test.js │ │ │ ├── fixDropdownWidth.test.js │ │ │ ├── formatAsYouType.test.js │ │ │ ├── formatOnDisplay.test.js │ │ │ ├── geoIpLookup.test.js │ │ │ ├── hiddenInput.test.js │ │ │ ├── i18n-locales.test.js │ │ │ ├── i18n.test.js │ │ │ ├── initialCountry.test.js │ │ │ ├── loadUtils.test.js │ │ │ ├── nationalMode.test.js │ │ │ ├── onlyCountries.test.js │ │ │ ├── placeholderNumberType.test.js │ │ │ ├── separateDialCode.test.js │ │ │ ├── showFlags.test.js │ │ │ ├── strictMode.test.js │ │ │ └── useFullscreenPopup.test.js │ │ └── static/ │ │ ├── attachUtils.test.js │ │ ├── defaults.test.js │ │ └── getCountryData.test.js │ └── unit/ │ ├── core/ │ │ ├── countrySearch.test.js │ │ └── options.test.js │ ├── data/ │ │ ├── country-data.test.js │ │ └── nanp-regionless.test.js │ ├── format/ │ │ ├── caret.test.js │ │ └── formatting.test.js │ ├── intl-tel-input/ │ │ └── constructor.test.js │ └── utils/ │ ├── dom.test.js │ └── string.test.js ├── tests-e2e/ │ ├── angular.spec.ts │ ├── fixtures/ │ │ └── vanilla.html │ ├── react.spec.ts │ ├── simple.spec.ts │ ├── svelte.spec.ts │ ├── visual.spec.ts │ └── vue.spec.ts ├── tsconfig.json └── vue/ ├── README.md ├── demo/ │ ├── set-number/ │ │ ├── App.vue │ │ ├── index.html │ │ ├── main.js │ │ └── vite.config.js │ ├── simple/ │ │ ├── App.vue │ │ ├── index.html │ │ ├── main.js │ │ └── vite.config.js │ ├── toggle-disabled/ │ │ ├── App.vue │ │ ├── index.html │ │ ├── main.js │ │ └── vite.config.js │ └── validation/ │ ├── App.vue │ ├── index.html │ ├── main.js │ └── vite.config.js ├── src/ │ ├── IntlTelInput.vue │ ├── IntlTelInputWithUtils.vue │ ├── env.d.ts │ └── exports/ │ ├── IntlTelInput.ts │ └── IntlTelInputWithUtils.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.mts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ **/demo/** **/grunt/** **/build/** **/third_party/** **/tmp/** Gruntfile.js **/intl-tel-input/utils.js site/static/js/highlight.min.js site/static/js/silktide-consent-manager.js site/src/examples/js/*_display_code* playwright-report/ ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, env: { browser: true, es2021: true, node: true, "jest/globals": true, }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react-hooks/recommended", ], parser: "@typescript-eslint/parser", parserOptions: { "ecmaVersion": "latest", "sourceType": "module", }, plugins: [ "@typescript-eslint", "react", "jest", ], globals: { goog: true, i18n: true, require: true, }, rules: { semi: ["error", "always"], "comma-dangle": ["error", "always-multiline"], quotes: ["error", "double"], "no-unused-vars": "off", "no-prototype-builtins": "off", "class-methods-use-this": "error", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_", "ignoreRestSiblings": true, }], "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-require-imports": "off", }, overrides: [{ files: [".eslintrc.{js,cjs}"], env: { "node": true }, parserOptions: { "sourceType": "script" }, }], settings: { "import/resolver": { "typescript": {}, }, "react": { "version": "detect", }, }, }; ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing I'm very open to contributions, big and small! For general instructions on submitting a pull request on GitHub, see these guides: [Fork A Repo](https://help.github.com/articles/fork-a-repo) and [Creating a pull request from a fork](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). ## Table of Contents - [Changes to the plugin](#changes-to-the-plugin) - [Updating the flag images](#updating-the-flag-images) - [Adding a new translation](#adding-a-new-translation) ## Changes to the plugin ### Setup Once you have [forked the repository](https://help.github.com/articles/fork-a-repo) and checked out your fork on your local machine, you need to initialise the submodules with `git submodule update --init --recursive`, then run `npm install`, and then `npm run build`. You should now be able to open the included demo.html in your browser and have a working plugin! ### Making changes Any time you make changes, you’ll need to rebuild the plugin. You can run `npm run watch` to do this automatically. Else you can manually run one of the build commands below: - `npm run build` to build everything (slow) - Builds flag images, translations, CSS, and all of the JS (see below) - `npm run build:js` to build all of the JS (slow) - Builds utils script, main plugin module, TS type declaration files, react/vue/angular/svelte components and demo bundles - `npm run build:jsfast` to just build the JS needed for the demo/tests (fast) - `npm run build:css` to just build the CSS (fast) - And lots more - see [package.json "scripts" section](https://github.com/jackocnr/intl-tel-input/blob/master/package.json#L7-L29) for full list ### Tests After building all the assets (`npm run build`) you can run `npm test` to run all the tests (Jest + Playwright). For Playwright, you may also need to install the browsers first with `npx playwright install`. ## Updating the flag images We get our flags from the [flag-icons](https://github.com/lipis/flag-icons) project. If there is a problem with the flags, you'll need to raise it with them. When there is an update in that project that you want to pull into this project, you can update the npm package with `npm install flag-icons@VERSION --save-dev`, and then rebuild the flag sprite images with `npm run build:img`. Once you've checked everything looks ok (by opening the included demo.html in your browser), you can then create a pull request on GitHub. _NOTE: since we removed the build files from the repo, the only changes you will be committing are in package.json and package-lock.json._ ## Adding a new translation NOTE: that country names are now translated automatically using the native `Intl.DisplayNames` (see `countryNameLocale` option), so there's no need to provide translations for these anymore. The [provided translations](https://github.com/jackocnr/intl-tel-input/tree/master/src/js/intl-tel-input/i18n) are now just for the user interface strings (e.g. the country search placeholder). If we don't yet support a language you need, it's easy to contribute this yourself - you only need to provide a handful of strings (for example, see the [English translations](https://github.com/jackocnr/intl-tel-input/blob/master/src/js/intl-tel-input/i18n/en/index.ts)). The translation files are located in src/js/intl-tel-input/i18n/. There is a directory for each language we support (e.g. "en" for English). Inside each of these directories, there is an index.ts, which contains the translations. All you need to do to add a new translation is create a new language directory, create the index.ts file and populate it with your translation strings, following the same pattern as the other languages. If you haven't already, you will need to run `npm install` to install the project dependencies, and then you can run `npm run build:translations` to automatically add your new language to the root index.ts file. Once you have tested and confirmed that the new translations are working, you can create a pull request on GitHub. ================================================ FILE: .github/FUNDING.yml ================================================ github: jackocnr ================================================ FILE: .github/ISSUE_TEMPLATE/1_bug_report.yml ================================================ name: Bug Report description: Report a bug or issue with intl-tel-input labels: ["bug"] body: - type: checkboxes attributes: label: Prerequisites description: Take a couple of minutes to help our maintainers work faster. options: - label: I am using the latest version (v26.8.1) of both intl-tel-input and utils.js. required: true - label: This plugin uses libphonenumber for formatting, validation, and placeholder numbers. If my issue relates to one of these things, I confirm I have used their [test site](https://libphonenumber.appspot.com) to ensure the issue is not with them, and will provide a link to the test results page showing this. Else, I have [reported the issue](https://github.com/google/libphonenumber/blob/master/CONTRIBUTING.md) to them instead of here. required: true - label: I have searched the existing issues (including closed issues) to ensure this has not already been discussed. required: true - type: textarea id: current attributes: label: Current behaviour description: A concise description of what you're experiencing. validations: required: true - type: textarea id: expected attributes: label: Expected behaviour description: A concise description of what you expected to happen. validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce description: How can we reproduce the issue? (e.g. provide a [Playground](https://intl-tel-input.com/playground) link with the right configuration) value: | 1. 2. 3. validations: required: true - type: textarea id: comments attributes: label: Anything else? description: Additional comments, screenshots/videos, initialisation options, browser/device info, etc. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/2_feature_request.yml ================================================ name: Feature Request description: Suggest a new feature or improvement labels: ["enhancement"] body: - type: textarea id: description attributes: label: Description description: Describe the feature or improvement you'd like to see. validations: required: true - type: textarea id: use-case attributes: label: Use case description: Why do you need this feature? What problem does it solve? validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask the community url: https://github.com/jackocnr/intl-tel-input/discussions about: Ask questions and get help from the community ================================================ FILE: .github/copilot-instructions.md ================================================ At the beginning of a chat, remind me to run `npm run watch`, so any changes get automatically built, meaning I can manually test them in the browser, and also when you run the tests, they will be running the latest code. Do not offer to run the build command yourself. Do not ever make changes to files in the build/ directories, e.g. the root ./build/ directory, or the component build/ directories (angular/build/, react/build/, svelte/build/ and vue/build/). These files are auto-generated, so do not update them. When making changes to the root src/ directory, do not touch any of the files in the component directories (angular/, react/, svelte/ and vue/). A lot of the files in those directories are just symlinks to the root src/ directory. When making changes to the root src/ code, make sure to also update the tests (in the root tests/ directory) if necessary, and run the tests to make sure they are passing. When making changes to the root src/ code, make sure to also update the website documentation and example pages if necessary. The documentation is located in the site/src/docs/ directory, and the examples are located in the site/src/examples/ directory. If you are not sure how to update the documentation or examples, just ask me. When writing code, prioritise clarity and brevity. Try to follow the existing code style as much as possible. When making changes in the root site/ directory, remember there are no tests for this, so don't bother running the tests. This also applies to the component directories (angular/, react/, svelte/ and vue/), as they also don't have tests. When making changes in the component directories (angular/, react/, svelte/ and vue/), note that the "withUtils" files are auto-generated, so do not update them. For example, when making changes to react/src/intl-tel-input/react.tsx, do not update react/src/intl-tel-input/react-withUtils.tsx, as it is auto-generated. The same applies to the other component directories. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [ "**" ] pull_request: branches: [ "**" ] permissions: contents: read jobs: build-and-test: name: Build and Test (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: [20] steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Run JS tests run: npm run test:js -- --runInBand - name: Run e2e tests run: npm run test:e2e:linux - name: Upload Playwright artifacts if: failure() uses: actions/upload-artifact@v4 with: name: playwright path: | playwright-report/ test-results/ ================================================ FILE: .gitignore ================================================ /node_modules/ /lib/* !/lib/libphonenumber /.sass-cache/ /.grunt/ /tmp/ /vendor/ /.idea/ *.iml .DS_Store /build/ /react/build/ /svelte/build/ /vue/build/ /angular/build/ /*/demo/*/*-bundle.js /test-results/ /playwright-report/ /.claude/ ================================================ FILE: .gitmodules ================================================ [submodule "third_party/libphonenumber"] path = third_party/libphonenumber url = https://github.com/google/libphonenumber ================================================ FILE: .node-version ================================================ v20.12.0 ================================================ FILE: .npmrc ================================================ # Jest uses the `vm` module, which does not have production ready module support # yet. This option lets us use dynamic imports. node-options='--experimental-vm-modules' ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "rvest.vs-code-prettier-eslint", "davidanson.vscode-markdownlint", "aaron-bond.better-comments" ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", "editor.formatOnPaste": false, //* required "editor.formatOnType": false, //* required "editor.formatOnSaveMode": "file", //* required to format on save "vs-code-prettier-eslint.prettierLast": false, //* set as "true" to run 'prettier' last not first "typescript.tsdk": "node_modules/typescript/lib", "files.associations": { "LICENSE": "text", }, "markdownlint.config": { "blanks-around-headings": false, "blanks-around-lists": false, "ul-style": false, "blanks-around-fences": false, "ol-prefix": false, "no-inline-html": false, }, "cSpell.language": "en-GB", "cSpell.enabledLanguageIds": [ "asciidoc", "c", "cpp", "csharp", "css", "dotenv", "go", "handlebars", "html", "ignore", "jade", "java", "javascript", "javascriptreact", "json", "jsonc", "latex", "less", "markdown", "php", "plaintext", "pug", "python", "restructuredtext", "rust", "scala", "scss", "text", "typescript", "typescriptreact", "yaml", "yml", "COBOL", "ACUCOBOL" ], "files.trimTrailingWhitespace": true, "[markdown]": { "files.trimTrailingWhitespace": false } } ================================================ FILE: CHANGELOG.md ================================================ See the Github Releases page for changelog: https://github.com/jackocnr/intl-tel-input/releases Or to view a specific version, e.g. v20.0.0, update the URL accordingly, e.g. https://github.com/jackocnr/intl-tel-input/releases/tag/v20.0.0 ## Breaking changes - v26.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v26.0.0 - v25.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v25.0.0 - v24.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v24.0.0 - v23.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v23.0.0 - v22.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v22.0.0 - v21.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v21.0.0 - v20.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v20.0.0 - v19.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v19.0.0 - v18.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v18.0.0 - v17.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v17.0.0 - v16.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v16.0.0 - v15.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v15.0.0 - v14.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v14.0.0 - v13.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v13.0.0 - v12.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v12.0.0 - v11.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v11.0.0 - v10.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v10.0.0 - v9.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v9.0.0 - v8.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v8.0.0 - v7.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v7.0.0 - v6.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v6.0.0 - v5.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v5.0.0 - v4.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v4.0.0 - v3.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v3.0.0 - v2.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v2.0.0 - v1.0.0 https://github.com/jackocnr/intl-tel-input/releases/tag/v1.0.0 ================================================ FILE: Gruntfile.js ================================================ module.exports = function(grunt) { // load all tasks from package.json require('load-grunt-config')(grunt); require('time-grunt')(grunt); require('google-closure-compiler').grunt(grunt, { platfrom: 'native' }); /** * BUILD TASKS */ // build everything ready for a commit grunt.registerTask('build', [ 'clean:allBuild', 'build:img', 'translations', 'build:js', ]); // build translations grunt.registerTask('build:translations', [ 'clean:buildJs', 'clean:tmpIntermediates', 'translations', 'build:js', ]); // build utils grunt.registerTask('build:utils', [ 'clean:utils', 'closure-compiler:utils', 'shell:checkLpnMetadata', ]); // just CSS grunt.registerTask('build:css', [ 'clean:buildCss', 'sass', 'cssmin', ]); // just images (and CSS) grunt.registerTask('build:img', [ 'clean:buildImg', 'generate-sprite', 'build:css', ]); // just javascript grunt.registerTask('build:js', [ 'clean:buildJs', 'clean:tmpIntermediates', 'shell:eslint', 'closure-compiler:utils', 'shell:genTsDeclaration', 'shell:buildJs', 'build:components', ]); // just 4 components grunt.registerTask('build:components', [ 'clean:reactBuild', 'clean:vueBuild', 'clean:angularBuild', 'clean:svelteBuild', 'build:react', 'build:vue', 'build:angular', 'build:svelte', ]); // Ensure build/js/utils.js exists (src/js/intl-tel-input/utils.js is a symlink to it). grunt.registerTask('ensure:utils', 'Build utils if missing', function() { if (!grunt.file.exists('build/js/utils.js')) { grunt.task.run('closure-compiler:utils'); } }); // fast version which only builds the main plugin JS files (see root build.js file for details) grunt.registerTask('build:jsfast', [ 'clean:buildJsKeepUtils', 'clean:tmpIntermediates', 'ensure:utils', 'shell:buildJs', ]); // just react grunt.registerTask('build:react', [ 'clean:reactBuild', 'replace:reactWithUtils', 'shell:genReactTsDeclaration', 'shell:buildReact', ]); // just vue grunt.registerTask('build:vue', [ 'clean:vueBuild', 'replace:vueWithUtils', 'shell:buildVue', ]); // just angular grunt.registerTask('build:angular', [ 'clean:angularBuild', 'replace:angularWithUtils', 'shell:genAngularTsDeclarationAndJs', 'shell:buildAngular', ]); // just svelte grunt.registerTask('build:svelte', [ 'clean:svelteBuild', 'replace:svelteWithUtils', 'shell:buildSvelte', ]); /** * VERSIONING TASKS */ // (1) build for tests, and run tests before allowing a version bump // (2) bump version number in package.json etc // (3) rebuild js to update version numbers in those files, as well as readme etc // (4) commit, tag and push grunt.registerTask('version', [ 'build:jsfast', 'shell:test', 'bump-only', 'versionNumbers', 'bump-commit', ]); grunt.registerTask('version:minor', [ 'build:jsfast', 'shell:test', 'bump-only:minor', 'versionNumbers', 'bump-commit' ]); grunt.registerTask('version:major', [ 'build:jsfast', 'shell:test', 'bump-only:major', 'versionNumbers', 'bump-commit' ]); // update version numbers in docs etc grunt.registerTask('versionNumbers', [ 'replace:siteDocs', 'replace:issueTemplate', 'replace:packageLockInner', ]); }; ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014-2016 Jack O'Connor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # International Telephone Input [![CI](https://github.com/jackocnr/intl-tel-input/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/jackocnr/intl-tel-input/actions/workflows/ci.yml) version downloads [![NerdyData.com logo](https://badges.nerdydata.com/719de9d2-d0e7-4988-b02f-9f9d52687076)](https://www.nerdydata.com/reports/international-telephone-input/719de9d2-d0e7-4988-b02f-9f9d52687076) A JavaScript plugin for entering, formatting and validating international telephone numbers. Includes TypeScript definitions, plus React, Vue, Angular and Svelte components. [Explore docs »](https://intl-tel-input.com/docs/choose-integration) Plugin screenshot showing country dropdown open ## Sponsored by Twilio Use [Twilio's API to build phone verification, SMS 2FA, appointment reminders, marketing notifications and so much more](https://www.twilio.com/blog/international-telephone-input-twilio?utm_source=github&utm_medium=referral&utm_campaign=intl_tel_input). We can't wait to see what you build. ## React, Vue, Angular and Svelte Components We provide React, Vue, Angular and Svelte (beta) components alongside the regular JavaScript plugin. This readme is for the JavaScript plugin. View the [React Component](https://intl-tel-input.com/docs/react-component), the [Vue Component](https://intl-tel-input.com/docs/vue-component) the [Angular Component](https://intl-tel-input.com/docs/angular-component), or the [Svelte component](https://intl-tel-input.com/docs/svelte-component). ## Docs and Examples We have a newly updated website, where you can find [a full set of docs](https://intl-tel-input.com/docs/getting-started.html), a [live playground](https://intl-tel-input.com/playground/) where you can try out all of the options, as well as plenty of [examples](https://intl-tel-input.com/examples/validation-practical.html) of different setups. ## Features * Automatically select the user's current country using an IP lookup * Automatically set the input placeholder to an example number for the selected country * Navigate the country dropdown by typing a country's name, or using the up/down keys * Automatically format the number as the user types * Optionally, only allow numeric characters and cap the number at the maximum valid length * The user types their national number, and the plugin gives you the full standardised international number * Number validation, including specific error types * High-resolution flag images * Accessibility provided via ARIA tags * Typescript type definitions included * Easily customise styles by overriding CSS variables, e.g. support dark mode * React, Vue, Angular and Svelte components also included * Translations provided in over 40 languages, as well as support for RTL layout and alternative numeral sets * Lots of initialisation options for customisation, as well as instance methods/events for interaction ## Contributing See the [contributing guide](https://github.com/jackocnr/intl-tel-input/blob/master/.github/CONTRIBUTING.md) for instructions on setting up the project and making changes, and also on how to update the flag images, or how to add a new translation. ## Attributions * Flag images from [flag-icons](https://github.com/lipis/flag-icons) * Original country data from mledoze's [World countries in JSON, CSV and XML](https://github.com/mledoze/countries) * Formatting/validation/example number code from [libphonenumber](https://github.com/googlei18n/libphonenumber) User testing powered by [BrowserStack Open-Source Program](https://www.browserstack.com/open-source) Browser testing via ================================================ FILE: angular/README.md ================================================ # IntlTelInput Angular Component An Angular component for the [intl-tel-input](https://github.com/jackocnr/intl-tel-input) JavaScript plugin. View the [source code](https://github.com/jackocnr/intl-tel-input/blob/master/angular/src/intl-tel-input/angular.ts). [Explore docs »](https://intl-tel-input.com/docs/angular-component) ================================================ FILE: angular/build.js ================================================ const { build } = require("esbuild"); const fs = require("fs"); const packageJson = require("../package.json"); const mainShared = { bundle: true, external: ["@angular/core", "@angular/forms"], logLevel: "info", minify: false, define: { "process.env.VERSION": `"${packageJson.version}"` }, }; async function buildMain() { //* Angular Component - Default (ES Modules) await build({ ...mainShared, entryPoints: ["angular/build/temp/intl-tel-input/angular.js"], format: "esm", outfile: "angular/build/IntlTelInput.js", }); //* Angular Component With Utils - Default (ES Modules) await build({ ...mainShared, entryPoints: ["angular/build/temp/intl-tel-input/angularWithUtils.js"], format: "esm", outfile: "angular/build/IntlTelInputWithUtils.js", }); // remove temp folder after builds are complete fs.rmSync("angular/build/temp", { recursive: true, force: true }); } buildMain().catch(console.error); const demoShared = { bundle: true, define: { "process.env.VERSION": `"${packageJson.version}"` }, format: "iife", }; build({ ...demoShared, entryPoints: ["angular/demo/simple/main.ts"], outfile: "angular/demo/simple/simple-bundle.js", }); build({ ...demoShared, entryPoints: ["angular/demo/validation/main.ts"], outfile: "angular/demo/validation/validation-bundle.js", }); build({ ...demoShared, entryPoints: ["angular/demo/set-number/main.ts"], outfile: "angular/demo/set-number/set-number-bundle.js", }); build({ ...demoShared, entryPoints: ["angular/demo/toggle-disabled/main.ts"], outfile: "angular/demo/toggle-disabled/toggle-disabled-bundle.js", }); build({ ...demoShared, entryPoints: ["angular/demo/form/main.ts"], outfile: "angular/demo/form/form-bundle.js", }); ================================================ FILE: angular/demo/form/form.component.ts ================================================ import { Component, OnInit, ViewChild } from "@angular/core"; import { FormControl, FormGroup, ReactiveFormsModule, Validators, } from "@angular/forms"; import { IntlTelInputComponent } from "../../src/intl-tel-input/angularWithUtils"; @Component({ selector: "app-root", template: `
@if (phone?.errors?.["required"] && phone?.touched) { Phone number is required. } @else if (phone?.errors?.["invalidPhone"] && phone?.touched) { {{ phone?.errors?.["invalidPhone"].errorMessage }} } @else if (isSubmitted && fg.valid) { Valid number: {{ telInput.getInstance()?.getNumber() }} }
`, standalone: true, imports: [IntlTelInputComponent, ReactiveFormsModule], }) export class AppComponent implements OnInit { @ViewChild("telInput") telInput!: IntlTelInputComponent; fg: FormGroup = new FormGroup({ phone: new FormControl("", [Validators.required]), }); isSubmitted = false; get phone() { return this.fg.get("phone"); } ngOnInit(): void { this.phone?.valueChanges.subscribe(() => { this.isSubmitted = false; }); } handleSubmit(): void { this.phone?.markAsTouched(); if (this.fg.valid) { this.isSubmitted = true; } } } ================================================ FILE: angular/demo/form/index.html ================================================ Angular App - Form Validation Demo

Angular Form Validation Demo

A simple Angular app using reactive forms with the IntlTelInput component for phone number entry and validation.

Enter a phone number and click "Validate" to see form validation in action.

================================================ FILE: angular/demo/form/main.ts ================================================ import 'zone.js'; import "@angular/compiler"; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './form.component'; bootstrapApplication(AppComponent) .catch((err) => console.error(err)); ================================================ FILE: angular/demo/set-number/index.html ================================================ Angular App - Set Number Demo

Angular App - Set Number Demo

A simple Angular app, using the IntlTelInput component to handle phone number entry, calling setNumber and validation.

Click "Set Number" then click "Validate" to check that the angular internals have updated correctly.

================================================ FILE: angular/demo/set-number/main.ts ================================================ import 'zone.js'; import "@angular/compiler"; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './set-number.component'; bootstrapApplication(AppComponent) .catch((err) => console.error(err)); ================================================ FILE: angular/demo/set-number/set-number.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { IntlTelInputComponent, PHONE_ERROR_MESSAGES } from '../../src/intl-tel-input/angularWithUtils'; @Component({ selector: "app-root", template: `
@if (notice) {
{{ notice }}
}
`, standalone: true, imports: [IntlTelInputComponent] }) export class AppComponent { @ViewChild('telInput') telInput!: IntlTelInputComponent; isValid: boolean | null = null; number: string | null = null; errorCode: number | null = null; notice: string | null = null; handleNumberChange(value: string): void { this.number = value; } handleValidityChange(value: boolean): void { this.isValid = value; } handleErrorCodeChange(value: number | null): void { this.errorCode = value; } handleSetNumber(): void { const instance = this.telInput?.getInstance(); if (instance) { instance.setNumber('+14155552671'); } } handleSubmit(): void { if (this.isValid) { this.notice = `Valid number: ${this.number}`; } else { const errorMessage = PHONE_ERROR_MESSAGES[this.errorCode || 0] || "Invalid number"; this.notice = `Error: ${errorMessage}`; } } } ================================================ FILE: angular/demo/simple/index.html ================================================ Angular App - Simple Demo

Angular App - Simple Demo

A simple Angular app, using the IntlTelInput component to handle phone number entry.

================================================ FILE: angular/demo/simple/main.ts ================================================ import 'zone.js'; import "@angular/compiler"; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './simple.component'; bootstrapApplication(AppComponent) .catch((err) => console.error(err)); ================================================ FILE: angular/demo/simple/simple.component.ts ================================================ import { Component } from '@angular/core'; import { IntlTelInputComponent } from '../../src/intl-tel-input/angularWithUtils'; @Component({ selector: "app-root", template: ` `, standalone: true, imports: [IntlTelInputComponent] }) export class AppComponent {} ================================================ FILE: angular/demo/toggle-disabled/index.html ================================================ Angular App - Toggle disabled prop Demo

Angular App - Toggle disabled prop Demo

A simple Angular app, using the IntlTelInput component to show it toggle.

Click the button to enable/disable component.

================================================ FILE: angular/demo/toggle-disabled/main.ts ================================================ import 'zone.js'; import "@angular/compiler"; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './toggle-disabled.component'; bootstrapApplication(AppComponent) .catch((err) => console.error(err)); ================================================ FILE: angular/demo/toggle-disabled/toggle-disabled.component.ts ================================================ import { Component } from '@angular/core'; import { IntlTelInputComponent } from '../../src/intl-tel-input/angularWithUtils'; @Component({ selector: "app-root", template: `
`, standalone: true, imports: [IntlTelInputComponent] }) export class AppComponent { isDisabled: boolean = false; toggleDisabled(): void { this.isDisabled = !this.isDisabled; } } ================================================ FILE: angular/demo/validation/index.html ================================================ Angular App - Validation Demo

Angular App - Validation Demo

A simple Angular app, using the IntlTelInput component to handle phone number entry and validation.

Enter a phone number below and click "Validate".

================================================ FILE: angular/demo/validation/main.ts ================================================ import 'zone.js'; import "@angular/compiler"; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './validation.component'; bootstrapApplication(AppComponent) .catch((err) => console.error(err)); ================================================ FILE: angular/demo/validation/validation.component.ts ================================================ import { Component } from '@angular/core'; import { IntlTelInputComponent, PHONE_ERROR_MESSAGES } from '../../src/intl-tel-input/angularWithUtils'; @Component({ selector: "app-root", template: `
@if (notice) {
{{ notice }}
}
`, standalone: true, imports: [IntlTelInputComponent] }) export class AppComponent { isValid: boolean | null = null; number: string | null = null; errorCode: number | null = null; notice: string | null = null; handleNumberChange(value: string): void { this.number = value; } handleValidityChange(value: boolean): void { this.isValid = value; } handleErrorCodeChange(value: number | null): void { this.errorCode = value; } handleSubmit(): void { if (this.isValid) { this.notice = `Valid number: ${this.number}`; } else { const errorMessage = PHONE_ERROR_MESSAGES[this.errorCode || 0] || "Invalid number"; this.notice = `Error: ${errorMessage}`; } } } ================================================ FILE: angular/src/intl-tel-input/angular.ts ================================================ import intlTelInput from "../intl-tel-input"; //* Keep the TS imports separate, as the above line gets substituted in the angularWithUtils build process. import { Iti } from "../intl-tel-input"; import { Component, Input, OnDestroy, ViewChild, ElementRef, Output, EventEmitter, forwardRef, AfterViewInit, OnChanges, SimpleChanges, } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, Validator, AbstractControl, ValidationErrors, } from "@angular/forms"; import { SomeOptions } from "../modules/types/public-api"; export { intlTelInput }; export const PHONE_ERROR_MESSAGES: string[] = [ "invalid", "invalid-country-code", "too-short", "too-long", "invalid-format", ]; @Component({ selector: "intl-tel-input", standalone: true, template: ` `, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IntlTelInputComponent), multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => IntlTelInputComponent), multi: true, }, ], }) export class IntlTelInputComponent implements AfterViewInit, OnDestroy, OnChanges, ControlValueAccessor, Validator { @ViewChild("inputRef", { static: true }) inputRef!: ElementRef; @Input() initialValue?: string; @Input() usePreciseValidation: boolean = false; @Input() inputProps: Record = {}; @Input() disabled: boolean = false; @Input() initOptions?: SomeOptions; @Output() numberChange = new EventEmitter(); @Output() countryChange = new EventEmitter(); @Output() validityChange = new EventEmitter(); @Output() errorCodeChange = new EventEmitter(); @Output() blur = new EventEmitter(); @Output() focus = new EventEmitter(); @Output() keydown = new EventEmitter(); @Output() keyup = new EventEmitter(); @Output() paste = new EventEmitter(); @Output() click = new EventEmitter(); private iti?: Iti; private appliedInputPropKeys = new Set(); private lastEmittedNumber?: string; private lastEmittedCountry?: string; private lastEmittedValidity?: boolean; private lastEmittedErrorCode?: number | null; private countryChangeHandler = () => this.handleInput(); // eslint-disable-next-line class-methods-use-this private onChange: (value: string) => void = () => {}; // eslint-disable-next-line class-methods-use-this private onTouched: () => void = () => {}; // eslint-disable-next-line class-methods-use-this private onValidatorChange: () => void = () => {}; ngAfterViewInit() { if (this.inputRef.nativeElement) { this.iti = intlTelInput(this.inputRef.nativeElement, this.initOptions); } this.inputRef.nativeElement.addEventListener( "countrychange", this.countryChangeHandler, ); this.applyInputProps(); if (this.initialValue) { this.iti?.setNumber(this.initialValue); } if (this.disabled) { this.iti?.setDisabled(this.disabled); } } ngOnChanges(changes: SimpleChanges) { if (changes["disabled"]) { this.iti?.setDisabled(this.disabled); } if (changes["inputProps"]) { this.applyInputProps(); } } handleInput() { if (!this.iti) return; const num = this.iti.getNumber() || ""; const countryIso = this.iti.getSelectedCountryData().iso2 || ""; let hasChanged = false; if (num !== this.lastEmittedNumber) { this.lastEmittedNumber = num; this.numberChange.emit(num); this.onChange(num); hasChanged = true; } if (countryIso !== this.lastEmittedCountry) { this.lastEmittedCountry = countryIso; this.countryChange.emit(countryIso); hasChanged = true; } const isValid = this.usePreciseValidation ? this.iti.isValidNumberPrecise() : this.iti.isValidNumber(); const errorCode = isValid ? null : this.iti.getValidationError(); if (isValid !== this.lastEmittedValidity) { this.lastEmittedValidity = isValid; this.validityChange.emit(isValid); hasChanged = true; } if (errorCode !== this.lastEmittedErrorCode) { this.lastEmittedErrorCode = errorCode; this.errorCodeChange.emit(errorCode); hasChanged = true; } if (hasChanged) { this.onValidatorChange(); } } handleBlur(event: FocusEvent) { this.onTouched(); this.blur.emit(event); } handleFocus(event: FocusEvent) { this.focus.emit(event); } handleKeyDown(event: KeyboardEvent) { this.keydown.emit(event); } handleKeyUp(event: KeyboardEvent) { this.keyup.emit(event); } handlePaste(event: ClipboardEvent) { this.paste.emit(event); } handleClick(event: MouseEvent) { this.click.emit(event); } /** * This method must be called in `ngAfterViewInit` or later lifecycle hooks, * not in `ngOnInit` or the `constructor`, as the component needs to be fully initialized. */ getInstance(): Iti | null { return this.iti; } /** * This method must be called in `ngAfterViewInit` or later lifecycle hooks, * not in `ngOnInit` or the `constructor`, as the component needs to be fully initialized. */ getInput(): HTMLInputElement | null { return this.inputRef.nativeElement; } ngOnDestroy() { this.iti?.destroy(); this.inputRef.nativeElement.removeEventListener( "countrychange", this.countryChangeHandler, ); } private applyInputProps(): void { const currentKeys = new Set(); Object.entries(this.inputProps).forEach(([key, value]) => { currentKeys.add(key); this.inputRef.nativeElement.setAttribute(key, value); }); this.appliedInputPropKeys.forEach((key) => { if (!currentKeys.has(key)) { this.inputRef.nativeElement.removeAttribute(key); } }); this.appliedInputPropKeys = currentKeys; } // ============ ControlValueAccessor Implementation ============ writeValue(value: string | null): void { if (this.iti) { this.iti.setNumber(value || ""); } } registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; this.iti?.setDisabled(isDisabled); } // ============ Validator Implementation ============ validate(control: AbstractControl): ValidationErrors | null { if (!control.value || !this.iti) { return null; } const isValid = this.usePreciseValidation ? this.iti.isValidNumberPrecise() : this.iti.isValidNumber(); if (isValid) { return null; } const errorCode = this.iti.getValidationError(); return { invalidPhone: { errorCode, errorMessage: PHONE_ERROR_MESSAGES[errorCode] ?? "unknown", }, }; } registerOnValidatorChange(fn: () => void): void { this.onValidatorChange = fn; } } ================================================ FILE: angular/src/intl-tel-input/angularWithUtils.ts ================================================ //* THIS FILE IS AUTO-GENERATED. DO NOT EDIT. import intlTelInput from "./intlTelInputWithUtils"; //* Keep the TS imports separate, as the above line gets substituted in the angularWithUtils build process. import { Iti } from "../intl-tel-input"; import { Component, Input, OnDestroy, ViewChild, ElementRef, Output, EventEmitter, forwardRef, AfterViewInit, OnChanges, SimpleChanges, } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, Validator, AbstractControl, ValidationErrors, } from "@angular/forms"; import { SomeOptions } from "../modules/types/public-api"; export { intlTelInput }; export const PHONE_ERROR_MESSAGES: string[] = [ "invalid", "invalid-country-code", "too-short", "too-long", "invalid-format", ]; @Component({ selector: "intl-tel-input", standalone: true, template: ` `, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IntlTelInputComponent), multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => IntlTelInputComponent), multi: true, }, ], }) export class IntlTelInputComponent implements AfterViewInit, OnDestroy, OnChanges, ControlValueAccessor, Validator { @ViewChild("inputRef", { static: true }) inputRef!: ElementRef; @Input() initialValue?: string; @Input() usePreciseValidation: boolean = false; @Input() inputProps: Record = {}; @Input() disabled: boolean = false; @Input() initOptions?: SomeOptions; @Output() numberChange = new EventEmitter(); @Output() countryChange = new EventEmitter(); @Output() validityChange = new EventEmitter(); @Output() errorCodeChange = new EventEmitter(); @Output() blur = new EventEmitter(); @Output() focus = new EventEmitter(); @Output() keydown = new EventEmitter(); @Output() keyup = new EventEmitter(); @Output() paste = new EventEmitter(); @Output() click = new EventEmitter(); private iti?: Iti; private appliedInputPropKeys = new Set(); private lastEmittedNumber?: string; private lastEmittedCountry?: string; private lastEmittedValidity?: boolean; private lastEmittedErrorCode?: number | null; private countryChangeHandler = () => this.handleInput(); // eslint-disable-next-line class-methods-use-this private onChange: (value: string) => void = () => {}; // eslint-disable-next-line class-methods-use-this private onTouched: () => void = () => {}; // eslint-disable-next-line class-methods-use-this private onValidatorChange: () => void = () => {}; ngAfterViewInit() { if (this.inputRef.nativeElement) { this.iti = intlTelInput(this.inputRef.nativeElement, this.initOptions); } this.inputRef.nativeElement.addEventListener( "countrychange", this.countryChangeHandler, ); this.applyInputProps(); if (this.initialValue) { this.iti?.setNumber(this.initialValue); } if (this.disabled) { this.iti?.setDisabled(this.disabled); } } ngOnChanges(changes: SimpleChanges) { if (changes["disabled"]) { this.iti?.setDisabled(this.disabled); } if (changes["inputProps"]) { this.applyInputProps(); } } handleInput() { if (!this.iti) return; const num = this.iti.getNumber() || ""; const countryIso = this.iti.getSelectedCountryData().iso2 || ""; let hasChanged = false; if (num !== this.lastEmittedNumber) { this.lastEmittedNumber = num; this.numberChange.emit(num); this.onChange(num); hasChanged = true; } if (countryIso !== this.lastEmittedCountry) { this.lastEmittedCountry = countryIso; this.countryChange.emit(countryIso); hasChanged = true; } const isValid = this.usePreciseValidation ? this.iti.isValidNumberPrecise() : this.iti.isValidNumber(); const errorCode = isValid ? null : this.iti.getValidationError(); if (isValid !== this.lastEmittedValidity) { this.lastEmittedValidity = isValid; this.validityChange.emit(isValid); hasChanged = true; } if (errorCode !== this.lastEmittedErrorCode) { this.lastEmittedErrorCode = errorCode; this.errorCodeChange.emit(errorCode); hasChanged = true; } if (hasChanged) { this.onValidatorChange(); } } handleBlur(event: FocusEvent) { this.onTouched(); this.blur.emit(event); } handleFocus(event: FocusEvent) { this.focus.emit(event); } handleKeyDown(event: KeyboardEvent) { this.keydown.emit(event); } handleKeyUp(event: KeyboardEvent) { this.keyup.emit(event); } handlePaste(event: ClipboardEvent) { this.paste.emit(event); } handleClick(event: MouseEvent) { this.click.emit(event); } /** * This method must be called in `ngAfterViewInit` or later lifecycle hooks, * not in `ngOnInit` or the `constructor`, as the component needs to be fully initialized. */ getInstance(): Iti | null { return this.iti; } /** * This method must be called in `ngAfterViewInit` or later lifecycle hooks, * not in `ngOnInit` or the `constructor`, as the component needs to be fully initialized. */ getInput(): HTMLInputElement | null { return this.inputRef.nativeElement; } ngOnDestroy() { this.iti?.destroy(); this.inputRef.nativeElement.removeEventListener( "countrychange", this.countryChangeHandler, ); } private applyInputProps(): void { const currentKeys = new Set(); Object.entries(this.inputProps).forEach(([key, value]) => { currentKeys.add(key); this.inputRef.nativeElement.setAttribute(key, value); }); this.appliedInputPropKeys.forEach((key) => { if (!currentKeys.has(key)) { this.inputRef.nativeElement.removeAttribute(key); } }); this.appliedInputPropKeys = currentKeys; } // ============ ControlValueAccessor Implementation ============ writeValue(value: string | null): void { if (this.iti) { this.iti.setNumber(value || ""); } } registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; this.iti?.setDisabled(isDisabled); } // ============ Validator Implementation ============ validate(control: AbstractControl): ValidationErrors | null { if (!control.value || !this.iti) { return null; } const isValid = this.usePreciseValidation ? this.iti.isValidNumberPrecise() : this.iti.isValidNumber(); if (isValid) { return null; } const errorCode = this.iti.getValidationError(); return { invalidPhone: { errorCode, errorMessage: PHONE_ERROR_MESSAGES[errorCode] ?? "unknown", }, }; } registerOnValidatorChange(fn: () => void): void { this.onValidatorChange = fn; } } ================================================ FILE: angular/tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "module": "esnext", "moduleResolution": "node", "esModuleInterop": true, //* Required for react (etc) default imports. "experimentalDecorators": true, //* Required for angular decorators. "emitDecoratorMetadata": true, //* Required for angular decorators. "declaration": true, //* Generate .d.ts files. "outDir": "build/temp", "declarationDir": "build/types", "rootDir": "./src", "allowJs": true, // allow importing .mjs files e.g. i18n files }, "angularCompilerOptions": { "strictTemplates": true, }, "include": [ "src/intl-tel-input/angular.ts", "src/intl-tel-input/angularWithUtils.ts", "src/intl-tel-input/i18n/index.ts", ], } ================================================ FILE: build.js ================================================ /* eslint-disable no-undef */ /* eslint-disable @typescript-eslint/no-var-requires */ const { build } = require("esbuild"); const packageJson = require("./package.json"); const getBanner = (moduleName) => "/*\n" + ` * International Telephone Input v${packageJson.version}\n` + ` * ${packageJson.repository.url}\n` + " * Licensed under the MIT license\n" + " */\n\n" + // we can remove this UMD hack once it is supported by esbuild: https://github.com/evanw/esbuild/issues/507 "// UMD\n" + "(function(factory) {\n" + " if (typeof module === 'object' && module.exports) {\n" + " module.exports = factory();\n" + " } else {\n" + ` window.${moduleName} = factory();\n` + " }\n" + "}(() => {\n"; const footer = "\n// UMD\n" + " return factoryOutput.default;\n" + "}));"; const shared = { bundle: true, logLevel: "info", format: "iife", globalName: "factoryOutput", footer: { js: footer, }, define: { "process.env.VERSION": `"${packageJson.version}"`, }, }; //* build/js/intlTelInput.js build({ ...shared, banner: { js: getBanner("intlTelInput"), }, entryPoints: ["src/js/intl-tel-input.ts"], minify: false, outfile: "build/js/intlTelInput.js", }); //* build/js/intlTelInput.min.js build({ ...shared, banner: { js: getBanner("intlTelInput"), }, entryPoints: ["src/js/intl-tel-input.ts"], minify: true, outfile: "build/js/intlTelInput.min.js", }); //* build/js/data.js build({ ...shared, banner: { js: getBanner("allCountries"), }, entryPoints: ["src/js/intl-tel-input/data.ts"], minify: false, outfile: "build/js/data.js", }); //* build/js/data.min.js build({ ...shared, banner: { js: getBanner("allCountries"), }, entryPoints: ["src/js/intl-tel-input/data.ts"], minify: true, outfile: "build/js/data.min.js", }); //* build/js/intlTelInputWithUtils.js build({ ...shared, banner: { js: getBanner("intlTelInput"), }, entryPoints: ["src/js/intl-tel-input/intlTelInputWithUtils.ts"], minify: false, outfile: "build/js/intlTelInputWithUtils.js", }); //* build/js/intlTelInputWithUtils.min.js build({ ...shared, banner: { js: getBanner("intlTelInput"), }, entryPoints: ["src/js/intl-tel-input/intlTelInputWithUtils.ts"], minify: true, outfile: "build/js/intlTelInputWithUtils.min.js", }); //* build/js/i18n build({ charset: "utf8", entryPoints: ["src/js/intl-tel-input/i18n/**/*.ts"], outdir: "build/js/i18n", }); ================================================ FILE: composer.json ================================================ { "name": "jackocnr/intl-tel-input", "version": "26.8.1", "description": "A JavaScript plugin for entering and validating international telephone numbers", "keywords": [ "international", "i18n", "country", "dial", "code", "telephone", "tel", "number", "mobile", "input", "flag" ], "homepage": "https://github.com/jackocnr/intl-tel-input", "type": "library", "license": "MIT", "authors": [ { "name": "Jack O'Connor", "homepage": "http://jackocnr.com" } ], "minimum-stability": "stable", "prefer-stable": true } ================================================ FILE: cspell.json ================================================ { "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", "version": "0.2", "allowCompoundWords": true, "maxNumberOfProblems": 1000, // "checkLimit": 5000, //! Setting that is in the vscode eextension not in the cli // "diagnosticLevel": "Warning", //! Setting that is in the vscode eextension not in the cli "language": "en_GB", "useGitignore": true, "minWordLength": 4, "dictionaries": [ "css", "en_GB", "html", "ignore", "javascript", "javascriptreact", "json", "jsonc", "markdown", "plaintext", "scss", "text", "typescript", "typescriptreact", "COBOL", "ACUCOBOL" ], "dictionaryDefinitions": [], "import": [], "ignorePaths": [ //* Directories "**/.git/**", "**/build/**", "**/node_modules/**", "**/vscode-extension/**", "**/.vscode/**", "**/.scraps/**", "**/third_party/**", //* Files "**/package-lock.json", "**/yarn.lock", "**/Gemfile", "**/cspell.json", "**/.env", //* File Extensions "**/*.svg", //* Specific files and folders "src/js/intl-tel-input/data.ts", "react/demo/validation-bundle.js", "react/demo/simple-bundle.js", "react/demo/toggle-disabled.js" ], "ignoreWords": [], "flagWords": [], "words": [ "Åland", "AYTF", "dropup", "evenizer", "FAYT", "geoip", "iife", "ipapi", "jackocnr", "jsfast", "librsvg", "mledoze's", "NANP", "normalised", "optipng", "sass", "tmpl", "vars", "Veaudry" ] } ================================================ FILE: demo.html ================================================ International Telephone Input

International Telephone Input

================================================ FILE: functions/_middleware.js ================================================ /** * Cloudflare Workers middleware to inject a script tag with the user's geographical information (window.__IS_EUROPE) */ const EUROPE = new Set([ // EU members "AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR", "DE","GR","HU","IE","IT","LV","LT","LU","MT","NL", "PL","PT","RO","SK","SI","ES","SE", // EEA + other European countries "GB","NO","IS","LI","CH","AL","AD","BA","BY","GE", "MD","ME","MK","MC","RS","SM","TR","UA","VA", ]); class GeoInjector { constructor(isEurope) { this.isEurope = isEurope; } element(element) { element.prepend( ``, { html: true }, ); } } export async function onRequest(context) { const country = context.request.cf?.country || ""; const isEurope = EUROPE.has(country); const response = await context.next(); const contentType = response.headers.get("content-type") || ""; if (!contentType.includes("text/html")) { return response; } // eslint-disable-next-line no-undef return new HTMLRewriter() .on("head", new GeoInjector(isEurope)) .transform(response); } ================================================ FILE: grunt/bump.js ================================================ module.exports = function(grunt) { return { options: { files: ['package.json', 'package-lock.json', 'composer.json'], updateConfigs: ['package'], commitFiles: ['-a'], pushTo: 'origin' } }; }; ================================================ FILE: grunt/clean.js ================================================ module.exports = function(grunt) { return { buildCss: ['build/css/*'], buildImg: ['build/img/*'], buildJs: ['build/js/*'], // Used by build:jsfast/watch. Preserves build/js/utils.js because // src/js/intl-tel-input/utils.js is a symlink to it. buildJsKeepUtils: ['build/js/*', '!build/js/utils.js'], reactBuild: ['react/build/*'], vueBuild: ['vue/build/*'], angularBuild: ['angular/build/*'], svelteBuild: ['svelte/build/*'], // Intermediate artifacts used by the build/minify/replace steps. tmpIntermediates: ['tmp/built.min.js', 'tmp/one.min.js'], // build:utils output utils: ['build/js/utils.js'], // Convenience target for top-level build. allBuild: [ 'build/css/*', 'build/img/*', 'build/js/*', 'react/build/*', 'vue/build/*', 'angular/build/*', 'svelte/build/*', 'tmp/built.min.js', 'tmp/one.min.js', ], }; }; ================================================ FILE: grunt/closure-compiler.js ================================================ module.exports = function (grunt) { return { utils: { files: { "build/js/utils.js": "src/js/utils.js", }, options: { js: [ "node_modules/google-closure-library/**.js", "third_party/libphonenumber/javascript/i18n/phonenumbers/**.js", "!third_party/libphonenumber/javascript/i18n/phonenumbers/demo-compiled.js", "!third_party/libphonenumber/javascript/i18n/phonenumbers/metadatafortesting.js", "!third_party/libphonenumber/javascript/i18n/phonenumbers/metadatalite.js", "!third_party/libphonenumber/javascript/i18n/phonenumbers/regioncodefortesting.js", "!third_party/libphonenumber/javascript/i18n/phonenumbers/**_test.js", ], entry_point: "goog:i18n.phonenumbers.demo", compilation_level: "ADVANCED_OPTIMIZATIONS", output_wrapper: "(function () {%output%})();\nconst globalContext = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : this;\nconst utils = globalContext.intlTelInputUtilsTemp;\ndelete globalContext.intlTelInputUtilsTemp;\nexport default utils;", }, }, }; }; ================================================ FILE: grunt/connect.js ================================================ module.exports = function(grunt) { return { test: { port: 8000 } }; }; ================================================ FILE: grunt/cssmin.js ================================================ module.exports = function(grunt) { return { target: { files: { 'build/css/intlTelInput.min.css': 'build/css/intlTelInput.css', 'build/css/intlTelInput-no-assets.min.css': 'build/css/intlTelInput-no-assets.css' } } }; }; ================================================ FILE: grunt/generate-sprite.js ================================================ const fs = require('fs'); const path = require('path'); // ts-node allows us to require TypeScript files require("ts-node").register(); const supportedCountries = require('../src/js/intl-tel-input/data.ts').default; module.exports = function(grunt) { grunt.registerTask('generate-sprite', async function() { const done = this.async(); // Require "sharp" on demand, else Travis was breaking with "Error: Could not load the "sharp" module using the linux-x64 runtime" when Travis doesn't even use this task const sharp = require('sharp'); // ensure /build/img/ dir exists before trying to write to it const buildImgDir = path.join(__dirname, '..', 'build', 'img'); if (!fs.existsSync(buildImgDir)) { fs.mkdirSync(buildImgDir, { recursive: true }); } const supportedCountryFilenames = supportedCountries.map(country => `${country.iso2}.svg`).sort(); // customise this number to change the size of the flags (NOTE: flags are 4x3 ratio) // must be a multiple of 3 const TARGET_HEIGHT = 12; const TARGET_WIDTH = (TARGET_HEIGHT / 3) * 4; const FLAG_MARGIN = 0; const specialCases = { 'ac.svg': 'sh-ac.svg', // Ascension Island // Add more special cases here if needed }; const handleSpecialCases = (filename) => specialCases[filename] || filename; const generateFlagMetadataAndSprite = async () => { try { const fileWarning = "//* THIS FILE IS AUTO-GENERATED. DO NOT EDIT."; const flagsPath = 'node_modules/flag-icons/flags/4x3'; const outputFile = 'src/css/_metadata.scss'; const spriteFile1xWebP = "build/img/flags.webp"; const spriteFile2xWebP = "build/img/flags@2x.webp"; const spriteFile1xPNG = "build/img/flags.png"; const spriteFile2xPNG = "build/img/flags@2x.png"; let outputFileContent = ''; let totalWidth = supportedCountryFilenames.length * (TARGET_WIDTH + FLAG_MARGIN) - FLAG_MARGIN; const maxHeight = TARGET_HEIGHT; let flagsMetadata = "$flags: (\n"; let currentOffset = 0; const scaledImages1x = []; const scaledImages2x = []; for (const filename of supportedCountryFilenames) { const countryCode = filename.split('.')[0]; const processedFilename = handleSpecialCases(filename); const imagePath = path.join(flagsPath, processedFilename); const imagePathExists = fs.existsSync(imagePath); if (!imagePathExists) { console.log(`WARNING: Missing flag image: ${imagePath} - skipping this flag.`); break; } const svgBuffer = fs.readFileSync(imagePath); const pngBuffer1x = await sharp(svgBuffer) .resize({ width: TARGET_WIDTH, height: TARGET_HEIGHT, fit: sharp.fit.fill, position: sharp.strategy.centre }) .ensureAlpha() .png({ compressionLevel: 9, adaptiveFiltering: true, force: true }) .toBuffer(); const pngBuffer2x = await sharp(svgBuffer) .resize({ width: TARGET_WIDTH * 2, height: TARGET_HEIGHT * 2, fit: sharp.fit.fill, position: sharp.strategy.centre }) .ensureAlpha() .png({ compressionLevel: 9, adaptiveFiltering: true, force: true }) .toBuffer(); scaledImages1x.push({ buffer: pngBuffer1x, offset: currentOffset }); scaledImages2x.push({ buffer: pngBuffer2x, offset: currentOffset * 2 }); flagsMetadata += ` ${countryCode}: (\n`; flagsMetadata += ` offset: ${-currentOffset}px,\n`; flagsMetadata += " ),\n"; currentOffset += TARGET_WIDTH + FLAG_MARGIN; } flagsMetadata += ");"; // Create 1x sprites await createSprite(scaledImages1x, totalWidth, maxHeight, spriteFile1xWebP, 'webp'); await createSprite(scaledImages1x, totalWidth, maxHeight, spriteFile1xPNG, 'png'); console.log(`1x combined images saved as ${spriteFile1xWebP} and ${spriteFile1xPNG}`); // Create 2x sprites await createSprite(scaledImages2x, totalWidth * 2, maxHeight * 2, spriteFile2xWebP, 'webp'); await createSprite(scaledImages2x, totalWidth * 2, maxHeight * 2, spriteFile2xPNG, 'png'); console.log(`2x combined images saved as ${spriteFile2xWebP} and ${spriteFile2xPNG}`); // Generate SCSS content outputFileContent += fileWarning + "\n\n"; outputFileContent += `$flags-sprite-1x: (\n`; outputFileContent += ` height: ${maxHeight}px,\n`; outputFileContent += ` width: ${totalWidth}px,\n`; outputFileContent += ");\n\n"; outputFileContent += `$flag-width: ${TARGET_WIDTH}px;\n\n`; outputFileContent += `$flag-height: ${TARGET_HEIGHT}px;\n\n`; outputFileContent += flagsMetadata + "\n\n"; outputFileContent += fileWarning + "\n"; fs.writeFileSync(outputFile, outputFileContent); console.log('SCSS file generated successfully.'); done(); } catch (error) { console.error('Error:', error); done(error); } }; const createSprite = async (images, width, height, outputFile, format) => { const combinedImage = sharp({ create: { width: width, height: height, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }); const compositeOperations = images.map((img) => ({ input: img.buffer, left: img.offset, top: 0 })); let processedImage = combinedImage.composite(compositeOperations); if (format === 'webp') { processedImage = processedImage.webp({ quality: 100, lossless: true, effort: 6 }); } else if (format === 'png') { processedImage = processedImage.png({ compressionLevel: 9, adaptiveFiltering: true, force: true }); } await processedImage.toFile(outputFile); }; generateFlagMetadataAndSprite(); }); }; ================================================ FILE: grunt/replace.js ================================================ module.exports = function(grunt) { return { /************** * Update version numbers **************/ siteDocs: { options: { patterns: [ { match: /intl-tel-input@([0-9.]+)\/build/g, replacement: 'intl-tel-input@<%= package.version %>/build' } ] }, files: { 'site/src/docs/markdown/options.md': 'site/src/docs/markdown/options.md', 'site/src/docs/markdown/getting_started.md': 'site/src/docs/markdown/getting_started.md', } }, issueTemplate: { options: { patterns: [ { match: /the latest version \(v[0-9]+\.[0-9]+\.[0-9]+\)/, replacement: 'the latest version (v<%= package.version %>)' } ] }, files: { '.github/ISSUE_TEMPLATE/1_bug_report.yml': '.github/ISSUE_TEMPLATE/1_bug_report.yml' } }, // grunt bump already updates the version number at the beginning of package-lock, but not the "inner" one (aprx line 9), so do that here packageLockInner: { options: { patterns: [ { match: /"name": "intl-tel-input",\n "version": "[0-9]+\.[0-9]+\.[0-9]+"/, replacement: '"name": "intl-tel-input",\n "version": "<%= package.version %>"' } ] }, files: { 'package-lock.json': 'package-lock.json' } }, /************** * Generate reactWithUtils.tsx **************/ reactWithUtils: { options: { patterns: [ { match: /import intlTelInput from \"\.\.\/intl\-tel\-input\"\;/, replacement: '//* THIS FILE IS AUTO-GENERATED. DO NOT EDIT.\nimport intlTelInput from "./intlTelInputWithUtils";' } ] }, files: { 'react/src/intl-tel-input/reactWithUtils.tsx': 'react/src/intl-tel-input/react.tsx', } }, /************** * Generate vue/src/IntlTelInputWithUtils.vue **************/ vueWithUtils: { options: { patterns: [ { match: /\ ================================================ FILE: react/demo/simple/SimpleApp.tsx ================================================ import React, { ReactElement } from "react"; import { createRoot } from "react-dom/client"; import IntlTelInput from "../../src/intl-tel-input/reactWithUtils"; const App = (): ReactElement => ( ); const container = document.getElementById("app"); if (container) { const root = createRoot(container); root.render(); } ================================================ FILE: react/demo/simple/simple.html ================================================ React App - Simple Demo

React App - Simple Demo

A simple react app, using the IntlTelInput component to handle phone number entry.

================================================ FILE: react/demo/toggle-disabled/ToggleDisabledApp.tsx ================================================ import React, { useState, ReactElement } from "react"; import { createRoot } from "react-dom/client"; import IntlTelInput from "../../src/intl-tel-input/reactWithUtils"; const App = (): ReactElement => { const [isDisabled, setIsDisabled] = useState(true); const toggleDisabled = () => setIsDisabled(!isDisabled); return (
); }; const container = document.getElementById("app"); if (container) { const root = createRoot(container); root.render(); } ================================================ FILE: react/demo/toggle-disabled/toggle-disabled.html ================================================ React App - Validation Demo

React App - Toggle disabled prop Demo

A simple react app, using the IntlTelInput component to show it toggle.

Click the button to enable/disable component.

================================================ FILE: react/demo/validation/ValidationApp.tsx ================================================ import React, { useState, ReactElement } from "react"; import { createRoot } from "react-dom/client"; import IntlTelInput from "../../src/intl-tel-input/reactWithUtils"; const errorMap = [ "Invalid number", "Invalid country code", "Too short", "Too long", "Invalid number", ]; const App = (): ReactElement => { const [isValid, setIsValid] = useState(null); const [number, setNumber] = useState(null); const [errorCode, setErrorCode] = useState(null); const [notice, setNotice] = useState(null); const handleSubmit = (): void => { if (isValid) { setNotice(`Valid number: ${number}`); } else { const errorMessage = errorMap[errorCode || 0] || "Invalid number"; setNotice(`Error: ${errorMessage}`); } }; return (
{notice &&
{notice}
} ); }; const container = document.getElementById("app"); if (container) { const root = createRoot(container); root.render(); } ================================================ FILE: react/demo/validation/validation.html ================================================ React App - Validation Demo

React App - Validation Demo

A simple react app, using the IntlTelInput component to handle phone number entry and validation.

Enter a phone number below and click "Validate".

================================================ FILE: react/src/intl-tel-input/react.tsx ================================================ import intlTelInput from "../intl-tel-input"; //* Keep the TS imports separate, as the above line gets substituted in the reactWithUtils build process. import { Iti } from "../intl-tel-input"; import React, { useRef, useEffect, forwardRef, useImperativeHandle, useCallback, } from "react"; import { SomeOptions } from "../modules/types/public-api"; // make this available as a named export, so react users can access globals like intlTelInput.utils export { intlTelInput }; type InputProps = Omit, "onInput">; type ItiProps = { initialValue?: string; onChangeNumber?: (number: string) => void; onChangeCountry?: (country: string) => void; onChangeValidity?: (valid: boolean) => void; onChangeErrorCode?: (errorCode: number | null) => void; usePreciseValidation?: boolean; initOptions?: SomeOptions; inputProps?: InputProps; disabled?: boolean | undefined; }; export type IntlTelInputRef = { getInstance: () => Iti | null; getInput: () => HTMLInputElement | null; }; const IntlTelInput = forwardRef(function IntlTelInput( { initialValue = "", onChangeNumber = () => {}, onChangeCountry = () => {}, onChangeValidity = () => {}, onChangeErrorCode = () => {}, usePreciseValidation = false, initOptions = {}, inputProps = {}, disabled = undefined, }: ItiProps, ref: React.ForwardedRef, ) { const inputRef = useRef(null); const itiRef = useRef(null); const lastEmittedNumberRef = useRef(); const lastEmittedCountryRef = useRef(); const lastEmittedValidityRef = useRef(); const lastEmittedErrorCodeRef = useRef(); // expose the instance and input ref to the parent component useImperativeHandle(ref, () => ({ getInstance: () => itiRef.current, getInput: () => inputRef.current, })); const update = useCallback((): void => { // if the instance is not valid (e.g. has been destroyed/unmounted), do not attempt to call any methods on it if (!itiRef.current?.isActive()) { return; } const num = itiRef.current?.getNumber() || ""; const countryIso = itiRef.current?.getSelectedCountryData().iso2 || ""; // note: this number will be in standard E164 format, but any container component can use // intlTelInput.utils.formatNumber() to convert this to another format // as well as intlTelInput.utils.getNumberType() etc. if need be if (num !== lastEmittedNumberRef.current) { lastEmittedNumberRef.current = num; onChangeNumber(num); } if (countryIso !== lastEmittedCountryRef.current) { lastEmittedCountryRef.current = countryIso; onChangeCountry(countryIso); } if (itiRef.current) { const isValid = usePreciseValidation ? itiRef.current.isValidNumberPrecise() : itiRef.current.isValidNumber(); const errorCode = isValid ? null : itiRef.current.getValidationError(); if (isValid !== lastEmittedValidityRef.current) { lastEmittedValidityRef.current = isValid; onChangeValidity(isValid); } if (errorCode !== lastEmittedErrorCodeRef.current) { lastEmittedErrorCodeRef.current = errorCode; onChangeErrorCode(errorCode); } } }, [ onChangeCountry, onChangeErrorCode, onChangeNumber, onChangeValidity, usePreciseValidation, ]); useEffect(() => { if (inputRef.current) { itiRef.current = intlTelInput(inputRef.current, initOptions); } return (): void => { itiRef.current?.destroy(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { // store a reference to the current input ref, which otherwise is already lost in the cleanup function const inputRefCurrent = inputRef.current; if (inputRefCurrent) { inputRefCurrent.addEventListener("countrychange", update); // when plugin initialisation has finished (e.g. loaded utils script), update all the state values itiRef.current.promise.then(update); } return (): void => { if (inputRefCurrent) { inputRefCurrent.removeEventListener("countrychange", update); } }; }, [update]); useEffect(() => { if (itiRef.current && disabled !== undefined) { itiRef.current.setDisabled(disabled); } }, [disabled]); // ignore keys that would break functionality const { value: _value, // disabled: _disabled, ...sanitizedInputProps } = inputProps as unknown as Record; return ( ); }); export default IntlTelInput; ================================================ FILE: react/src/intl-tel-input/reactWithUtils.tsx ================================================ //* THIS FILE IS AUTO-GENERATED. DO NOT EDIT. import intlTelInput from "./intlTelInputWithUtils"; //* Keep the TS imports separate, as the above line gets substituted in the reactWithUtils build process. import { Iti } from "../intl-tel-input"; import React, { useRef, useEffect, forwardRef, useImperativeHandle, useCallback, } from "react"; import { SomeOptions } from "../modules/types/public-api"; // make this available as a named export, so react users can access globals like intlTelInput.utils export { intlTelInput }; type InputProps = Omit, "onInput">; type ItiProps = { initialValue?: string; onChangeNumber?: (number: string) => void; onChangeCountry?: (country: string) => void; onChangeValidity?: (valid: boolean) => void; onChangeErrorCode?: (errorCode: number | null) => void; usePreciseValidation?: boolean; initOptions?: SomeOptions; inputProps?: InputProps; disabled?: boolean | undefined; }; export type IntlTelInputRef = { getInstance: () => Iti | null; getInput: () => HTMLInputElement | null; }; const IntlTelInput = forwardRef(function IntlTelInput( { initialValue = "", onChangeNumber = () => {}, onChangeCountry = () => {}, onChangeValidity = () => {}, onChangeErrorCode = () => {}, usePreciseValidation = false, initOptions = {}, inputProps = {}, disabled = undefined, }: ItiProps, ref: React.ForwardedRef, ) { const inputRef = useRef(null); const itiRef = useRef(null); const lastEmittedNumberRef = useRef(); const lastEmittedCountryRef = useRef(); const lastEmittedValidityRef = useRef(); const lastEmittedErrorCodeRef = useRef(); // expose the instance and input ref to the parent component useImperativeHandle(ref, () => ({ getInstance: () => itiRef.current, getInput: () => inputRef.current, })); const update = useCallback((): void => { // if the instance is not valid (e.g. has been destroyed/unmounted), do not attempt to call any methods on it if (!itiRef.current?.isActive()) { return; } const num = itiRef.current?.getNumber() || ""; const countryIso = itiRef.current?.getSelectedCountryData().iso2 || ""; // note: this number will be in standard E164 format, but any container component can use // intlTelInput.utils.formatNumber() to convert this to another format // as well as intlTelInput.utils.getNumberType() etc. if need be if (num !== lastEmittedNumberRef.current) { lastEmittedNumberRef.current = num; onChangeNumber(num); } if (countryIso !== lastEmittedCountryRef.current) { lastEmittedCountryRef.current = countryIso; onChangeCountry(countryIso); } if (itiRef.current) { const isValid = usePreciseValidation ? itiRef.current.isValidNumberPrecise() : itiRef.current.isValidNumber(); const errorCode = isValid ? null : itiRef.current.getValidationError(); if (isValid !== lastEmittedValidityRef.current) { lastEmittedValidityRef.current = isValid; onChangeValidity(isValid); } if (errorCode !== lastEmittedErrorCodeRef.current) { lastEmittedErrorCodeRef.current = errorCode; onChangeErrorCode(errorCode); } } }, [ onChangeCountry, onChangeErrorCode, onChangeNumber, onChangeValidity, usePreciseValidation, ]); useEffect(() => { if (inputRef.current) { itiRef.current = intlTelInput(inputRef.current, initOptions); } return (): void => { itiRef.current?.destroy(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { // store a reference to the current input ref, which otherwise is already lost in the cleanup function const inputRefCurrent = inputRef.current; if (inputRefCurrent) { inputRefCurrent.addEventListener("countrychange", update); // when plugin initialisation has finished (e.g. loaded utils script), update all the state values itiRef.current.promise.then(update); } return (): void => { if (inputRefCurrent) { inputRefCurrent.removeEventListener("countrychange", update); } }; }, [update]); useEffect(() => { if (itiRef.current && disabled !== undefined) { itiRef.current.setDisabled(disabled); } }, [disabled]); // ignore keys that would break functionality const { value: _value, // disabled: _disabled, ...sanitizedInputProps } = inputProps as unknown as Record; return ( ); }); export default IntlTelInput; ================================================ FILE: react/tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "jsx": "react", "module": "nodenext", "moduleResolution": "nodenext", "esModuleInterop": true, //* Required for react (etc) default imports. "declaration": true, //* Generate .d.ts files. "emitDeclarationOnly": true, //* ONLY generate .d.ts files (we use esbuild for the actual bundling). "outFile": "build/IntlTelInput.d.ts", "rootDir": "./src", "allowJs": true, // allow importing .mjs files e.g. i18n files }, "include": [ "src/intl-tel-input/react.tsx", "src/intl-tel-input/reactWithUtils.tsx", "src/intl-tel-input/i18n/index.ts", ], } ================================================ FILE: scripts/check-lpn-metadata.cjs ================================================ /** * This script loads the existing country data from data.ts, * then parses libphonenumber's PhoneNumberMetadata.xml to look for any updates. * * I decided to keep the hand-made data.ts data instead of just having it all auto-generated from the LPN metadata because LPN only stores the precise ranges that are known to be in use, whereas (1) we know that there are other ranges that have been assigned to a territory and so would make more sense belonging to that one rather than the main country, and (2) because of the high level of precision, the resulting output is massive from LPN e.g. outputting 20x 6-digit numbers for IM (isle of man), when it is clearly documented on the wiki page that they have been assigned the 5 short strings that I have included in data.ts */ /* eslint-disable no-console */ const fs = require("fs"); const path = require("path"); const vm = require("vm"); const REPO_ROOT = path.resolve(__dirname, "..", ""); const XML_PATH = path.resolve( REPO_ROOT, "third_party/libphonenumber/resources/PhoneNumberMetadata.xml", ); const OUT_TS = path.resolve(REPO_ROOT, "tmp/generated-rawCountryData.ts"); const CURATED_DATA_TS_PATH = path.resolve(REPO_ROOT, "src/js/intl-tel-input/data.ts"); // Extract a compact leading digits prefix representative for a region. // Strategy: // - Prefer a short literal digit prefix found in the first national number pattern for fixed-line or mobile // - Fallback to a header-level leadingDigits string when present, trimming to a short digit-only prefix // (legacy single-prefix extractor removed in favor of extractLeadingPrefixes) function sortDigitStringsNumeric(arr) { // Lexicographic digit-by-digit ordering (e.g., 74576 < 7524) return arr.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); } // Shared simple prefix parser used for XML mode function splitTopLevelAlts(inner) { const parts = []; let cur = ""; let depth = 0; for (let i = 0; i < inner.length; i++) { const ch = inner[i]; if (ch === "(") depth++; else if (ch === ")") depth = Math.max(0, depth - 1); if (ch === "|" && depth === 0) { parts.push(cur); cur = ""; continue; } cur += ch; } parts.push(cur); return parts; } function expandCharClass(cls) { const res = new Set(); for (let i = 0; i < cls.length; i++) { const c = cls[i]; if (/\d/.test(c)) { if (i + 2 < cls.length && cls[i + 1] === "-" && /\d/.test(cls[i + 2])) { const start = Number(c), end = Number(cls[i + 2]); for (let d = start; d <= end; d++) res.add(String(d)); i += 2; } else { res.add(c); } } } return Array.from(res); } function expandSimple(prefixPart) { if (/^\d{2,6}$/.test(prefixPart)) return [prefixPart]; const m = prefixPart.match(/^(\d*)\[([0-9-]+)\](\d*)$/); if (m) { const lead = m[1] || ""; const cls = m[2]; const tail = m[3] || ""; return expandCharClass(cls).map((d) => lead + d + tail).filter((s) => /^\d{2,6}$/.test(s)); } return []; } function simplePrefixesFromPattern(pat) { // Normalize: collapse all whitespace to simplify parsing of multi-line XML patterns pat = String(pat).replace(/\s+/g, ""); // Handle a leading literal digit sequence followed by a non-capturing group, e.g., '7(?:781|839)\\d|911[17])\\d{5}' let mLeadGroup = pat.match(/^(\d{1,3})\(\?:/); if (mLeadGroup) { const lead = mLeadGroup[1]; // Extract the first non-capturing group's inner content after the leading digits let i = lead.length + 3; // position after '(?:' let depth = 1; let inner = ""; while (i < pat.length) { const ch = pat[i]; if (ch === "\\") { i += 2; continue; } if (ch === "(") depth++; else if (ch === ")") { depth--; if (depth === 0) { i++; break; } } inner += ch; i++; } const alts = splitTopLevelAlts(inner); const set = new Set(); for (const a of alts) { const arr = simplePrefixesFromPattern(a); for (const v of arr) set.add(lead + v); } return Array.from(set).filter((s) => /^\d{2,6}$/.test(s)); } // If the pattern starts with a top-level non-capturing group, extract its inner content if (pat.startsWith("(?:")) { let i = 3; // after (?: let depth = 1; let inner = ""; while (i < pat.length) { const ch = pat[i]; if (ch === "\\") { // skip escaped char i += 2; continue; } if (ch === "(") depth++; else if (ch === ")") { depth--; if (depth === 0) { i++; break; } } inner += ch; i++; } // Now 'inner' holds the content of the outer non-capturing group; ignore any suffix like \d{n} const alts = splitTopLevelAlts(inner); if (alts.length > 1) { const set = new Set(); for (const a of alts) { const arr = simplePrefixesFromPattern(a); for (const v of arr) set.add(v); } return Array.from(set); } // Single alternative inside, recurse into it directly return simplePrefixesFromPattern(inner); } // Handle top-level alternations like "658|876" or "8001|8[024]9" const top = splitTopLevelAlts(pat); if (top.length > 1) { const set = new Set(); for (const part of top) { const arr = simplePrefixesFromPattern(part); for (const v of arr) set.add(v); } return Array.from(set); } // Handle bare character class at start, e.g., "[347]" or "[3-7]24" let m = pat.match(/^\[([0-9-]+)\](\d*)$/); if (m) { const cls = m[1]; const tail = m[2] || ""; return expandCharClass(cls).map((d) => d + tail).filter((s) => /^\d{1,6}$/.test(s)); } m = pat.match(/^(\d+)\[([0-9-]+)\](\d*)/); if (m) { const lead = m[1]; const cls = m[2]; const tail = m[3] || ""; return expandCharClass(cls).map((d) => lead + d + tail).filter((s) => /^\d{2,6}$/.test(s)); } m = pat.match(/^(\d{1,6})/); if (m) return [m[1]]; return []; } // Collapse exhaustive ranges (prefix-aware): if for a parent prefix p we see all 10 next digits // across any longer codes (e.g., p0*, p1*, ..., p9*), then replace all those children with p. function collapseExhaustiveRanges(codes) { if (!Array.isArray(codes) || !codes.length) return codes || []; const set = new Set(codes); let changed = true; while (changed) { changed = false; // Build parent -> set(nextDigits) const parents = new Map(); for (const s of set) { if (typeof s !== "string" || s.length < 2) continue; for (let k = 1; k < s.length; k++) { const parent = s.slice(0, k); const next = s[k]; if (!/\d/.test(next)) continue; let bag = parents.get(parent); if (!bag) { bag = new Set(); parents.set(parent, bag); } bag.add(next); } } // For any parent with all 10 digits seen, collapse all children starting with parent+digit for (const [parent, bag] of parents) { if (bag.size === 10) { let mutated = false; const toDelete = []; for (const s of set) { if (s.length > parent.length && s.startsWith(parent)) { toDelete.push(s); } } for (const s of toDelete) { set.delete(s); mutated = true; } if (!set.has(parent)) { set.add(parent); mutated = true; } if (mutated) changed = true; } } } return sortDigitStringsNumeric(Array.from(set)); } // Attempt to extract 3-digit NANP NPAs from a complex pattern like CA's, // which uses structure: (?: 2(?:..|..|..) | 3(?:..|..) | ... )[2-9]\d{6} function extractNanpNpasFromPattern(pat) { if (typeof pat !== "string" || !pat) return []; const s = pat.replace(/\s+/g, ""); // Find the top-level non-capturing group at the start, if present let inner = s; if (s.startsWith("(?:")) { let i = 3, depth = 1; inner = ""; while (i < s.length) { const ch = s[i]; if (ch === "\\") { i += 2; continue; } if (ch === "(") depth++; else if (ch === ")") { depth--; if (depth === 0) { i++; break; } } inner += ch; i++; } } const alts = splitTopLevelAlts(inner); const out = new Set(); for (const a of alts) { // Expect a like: 2(?:04|[23]6|...) const m = a.match(/^([2-9])\(\?:(.+)\)$/); if (!m) continue; const d = m[1]; const rest = m[2]; const parts = splitTopLevelAlts(rest); for (const p of parts) { // Expand simple pieces like 04, [23]6, 5[07], 63 const expansions = expandSimple(d + p); for (const e of expansions) { if (/^\d{3}$/.test(e)) out.add(e); } // Also handle pure two-digit literals (e.g., '63') after the 1st digit if (/^\d{2}$/.test(p)) out.add(d + p); } } return sortDigitStringsNumeric(Array.from(out)); } function deriveNanpNpasFromPatterns(fixedList, mobileList) { const set = new Set(); const addFrom = (list) => { if (!Array.isArray(list)) return; for (const pat of list) { // 1) General extraction using simplePrefixesFromPattern const simple = simplePrefixesFromPattern(pat); for (const s of simple) { if (/^[2-9]\d{2}$/.test(s)) set.add(s); else if (/^[2-9]\d{3,6}$/.test(s)) set.add(s.slice(0, 3)); } // 2) CA-style nested non-capturing groups as a supplement const caLike = extractNanpNpasFromPattern(pat); for (const v of caLike) set.add(v); if (set.size > 400) break; // safety guardrail } }; addFrom(fixedList); addFrom(mobileList); return sortDigitStringsNumeric(Array.from(set)); } // XML-mode generic extractor: only use territory.leadingDigits + fixedLine/mobile patterns // Apply safe expansions and filter out obvious service/example-like prefixes function extractGenericPrefixesXml(src) { const out = new Set(); function isDisallowedPrefix(s) { // Leading zero prefixes are often trunk/formatting hints if (/^0/.test(s)) return true; // Non-geo/service families and known service groups if (/^(?:800|80\d|900|90\d|13|1300|1800|190\d)\d*$/.test(s)) return true; // Example-like tails if (/(?:0123|1234)$/.test(s)) return true; // Common synthetic sequences sometimes found in examples if (/701234$/.test(s)) return true; return false; } function addFrom(list) { if (!Array.isArray(list)) return; for (const p of list) { if (typeof p !== "string" || !p) continue; const arr = simplePrefixesFromPattern(p); for (const s of arr) out.add(s); if (out.size > 40) break; } } // Use only fixedLine and mobile patterns (ignore leadingDigits entirely) addFrom(src.fixed); addFrom(src.mobile); // Perform prefix-aware collapse before filtering so that short parents (e.g., '4') can be retained const rawList = sortDigitStringsNumeric(Array.from(out)); const collapsed = collapseExhaustiveRanges(rawList); const rawSet = new Set(rawList); // Filter out obvious service/example-like prefixes const filtered = Array.from(collapsed).filter((s) => { if (isDisallowedPrefix(s)) return false; // Drop 709x branch (special allocations seen in 262/590 groups) to match curated area codes if (src && (src.dial === "262" || src.dial === "590") && /^709/.test(s)) return false; // Do not treat the full dial code itself as an area-code discriminator, // except for Kazakhstan (kz) which legitimately needs '7' to disambiguate from RU. if (src && s === src.dial && src.iso2 !== "kz") return false; // Enforce minimum length with exceptions: // - allow 1-digit for dial 599 (BQ group) // - allow 1-digit for KZ (iso2 === 'kz') const minLen = (src && (src.dial === "599" || src.iso2 === "kz")) ? 1 : 3; // If this value is a collapsed parent (i.e., not present in raw expansions), allow even if short const isCollapsedParent = !rawSet.has(s); if (s.length < minLen && !isCollapsedParent) return false; return true; }); return sortDigitStringsNumeric(filtered); } // XML-only: extend a base prefix to at least minLen using only safe expansions from XML patterns function extendPrefixXml(iso2, base, minLen, xmlSources) { const src = xmlSources[iso2]; if (!src || !base) return base; const candidates = new Set(); // Reuse filtering from extractor function isDisallowedPrefix(s) { if (/^0/.test(s)) return true; if (/^(?:800|80\d|900|90\d|13|1300|1800|190\d)\d*$/.test(s)) return true; if (/(?:0123|1234)$/.test(s)) return true; if (/701234$/.test(s)) return true; return false; } const collect = (list) => { if (!Array.isArray(list)) return; for (const pat of list) { if (typeof pat !== "string") continue; const arr = simplePrefixesFromPattern(pat); for (const p of arr) { if (p.startsWith(base) && p.length >= minLen && !isDisallowedPrefix(p)) candidates.add(p); } } }; collect(src.fixed); collect(src.mobile); if (!candidates.size) return base; // choose the shortest candidate return Array.from(candidates).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0))[0]; } // No iso2-codes.json; we derive the allowlist from curated data.ts function main() { let countryToMetadata; const dialCodeToRegions = {}; const xmlNationalPrefixByIso2 = {}; // Regions where we intentionally do NOT derive areaCodes as there is no way to distinguish numbers from main region const AREA_CODES_EXCLUDE = new Set(["bl", "mf", "cc", "cx"]); // For XML mode, we also keep a structured source set per region const xmlSourcesByIso2 = {}; console.log("Loading libphonenumber XML from:", XML_PATH); // Lazy-require to keep default path lightweight let XMLParser; try { ({ XMLParser } = require("fast-xml-parser")); } catch { throw new Error( "fast-xml-parser is required. Install with: npm i -D fast-xml-parser", ); } const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "" }); const xmlRaw = fs.readFileSync(XML_PATH, "utf8"); const xmlDoc = parser.parse(xmlRaw); const territoriesRoot = xmlDoc && xmlDoc.phoneNumberMetadata && xmlDoc.phoneNumberMetadata.territories; if (!territoriesRoot) throw new Error("Invalid PhoneNumberMetadata.xml structure: missing territories"); let territories = territoriesRoot.territory || []; if (!Array.isArray(territories)) territories = [territories]; // Build minimal structures compatible with the rest of the pipeline // countryToMetadataXml: REG -> array of arrays of strings (patterns) so extendPrefix can scan const countryToMetadataXml = {}; territories.forEach((t) => { const reg = (t.id || "").toString(); const iso2 = reg.toLowerCase(); if (!reg) return; const dial = (t.countryCode || "").toString(); if (!dial) return; const natPref = t.nationalPrefix ? String(t.nationalPrefix) : null; if (natPref) xmlNationalPrefixByIso2[iso2] = natPref; // Collect safe patterns from territory-level leadingDigits, fixedLine, mobile const patterns = []; const leading = []; const fixedPats = []; const mobilePats = []; // Some schemas put territory-level leadingDigits as child elements or arrays const ld = t.leadingDigits; if (typeof ld === "string") { patterns.push(ld); leading.push(ld); } else if (Array.isArray(ld)) ld.forEach((s) => { if (typeof s === "string") { patterns.push(s); leading.push(s); } }); const fixed = t.fixedLine && t.fixedLine.nationalNumberPattern; if (typeof fixed === "string") { patterns.push(fixed); fixedPats.push(fixed); } const mobile = t.mobile && t.mobile.nationalNumberPattern; if (typeof mobile === "string") { patterns.push(mobile); mobilePats.push(mobile); } // Build dialCodeToRegions honoring mainCountryForCode where available if (!dialCodeToRegions[dial]) dialCodeToRegions[dial] = []; const isMain = String(t.mainCountryForCode || "") === "true"; if (isMain) { // Ensure main region is first dialCodeToRegions[dial].unshift(iso2); } else { dialCodeToRegions[dial].push(iso2); } // Provide a scanning surface for extendPrefix: one inner array with collected patterns countryToMetadataXml[reg] = [patterns]; xmlSourcesByIso2[iso2] = { iso2, leading, fixed: fixedPats, mobile: mobilePats, dial, main: isMain }; }); countryToMetadata = countryToMetadataXml; // Load curated area codes from src/js/intl-tel-input/data.ts and seed from those first. // We only add newly-derived codes if they are not already covered by curated ones. // Also capture curated priority per iso2 to preserve priority values in generated output (we don't compute priorities ourselves). const curatedPriorityByIso2 = new Map(); const curatedIso2Set = new Set(); function loadCuratedAreaCodes() { const map = new Map(); if (!fs.existsSync(CURATED_DATA_TS_PATH)) return map; try { const ts = fs.readFileSync(CURATED_DATA_TS_PATH, "utf8"); const anchor = ts.indexOf("export const rawCountryData"); if (anchor === -1) return map; // Find first '[' after anchor and then parse matching brackets until the corresponding ']' let i = ts.indexOf("[", anchor); if (i === -1) return map; let depth = 0; let start = i; for (; i < ts.length; i++) { const ch = ts[i]; if (ch === "[") depth++; else if (ch === "]") { depth--; if (depth === 0) { i++; break; } } } if (depth !== 0) return map; const arrSrc = ts.slice(start, i); // Evaluate the array literal. Comments are allowed in JS arrays, so this should work. // Wrap in parentheses to make it a valid expression. // eslint-disable-next-line no-new-func const curated = Function("return (" + arrSrc + ")")(); if (Array.isArray(curated)) { for (const row of curated) { if (!Array.isArray(row) || row.length < 2) continue; const iso2 = row[0]; const priority = row[2]; const area = row[3]; if (typeof iso2 === "string") curatedIso2Set.add(iso2.toLowerCase()); if (typeof iso2 === "string" && Array.isArray(area) && area.length) { map.set(iso2.toLowerCase(), area.slice()); } if (typeof iso2 === "string" && (typeof priority === "number" || typeof priority === "string")) { const p = Number(priority); if (!Number.isNaN(p)) curatedPriorityByIso2.set(iso2.toLowerCase(), p); } } } } catch (e) { console.warn("Warning: failed to parse curated data.ts for area codes:", e && e.message ? e.message : e); } return map; } const curatedAreaCodesByIso2 = loadCuratedAreaCodes(); // Build allowlist from curated data.ts iso2s console.log("Building iso2 allowlist from curated data.ts"); const allowIso2 = curatedIso2Set; const tuples = []; // Precompute candidate leading prefixes per region const candidatePrefixesByIso2 = {}; const collapsedParentsByIso2 = {}; // NANP behavior: derive 3-digit NPAs from fixed/mobile patterns Object.keys(countryToMetadata).forEach((REG) => { const iso2 = REG.toLowerCase(); if (!allowIso2.has(iso2)) return; // find dial code for region let dial = null; // In XML mode we already built dialCodeToRegions, so find the dial by reverse lookup for (const [dc, regs] of Object.entries(dialCodeToRegions)) { if (regs.includes(iso2)) { dial = dc; break; } } if (!dial) return; const siblings = dialCodeToRegions[dial] || []; if (siblings.length <= 1) return; // unique dial code -> no areaCodes // Use explicit main flag from XML to decide skipping area code derivation const srcForRegion = xmlSourcesByIso2[iso2]; if (srcForRegion && srcForRegion.main) return; // main region -> skip areaCodes // Skip area-code derivation for excluded regions if (AREA_CODES_EXCLUDE.has(iso2)) return; let cands = []; if (dial === "1") { // NANP: for non-main regions, extract 3-digit NPAs from fixed/mobile patterns const src = srcForRegion; if (src) { let npas = deriveNanpNpasFromPatterns(src.fixed, src.mobile); // Fallback: take a 3-digit slice from the first simple prefix if needed if (!npas.length) { const simple = []; (src.fixed || []).forEach((p) => simple.push(...simplePrefixesFromPattern(p))); (src.mobile || []).forEach((p) => simple.push(...simplePrefixesFromPattern(p))); const first = simple.find((s) => /^[2-9]\d{2,6}$/.test(s)); if (first) npas = [first.slice(0, 3)]; } if (npas.length) cands = npas; } } else { // XML mode: use filtered IM-style extraction from leading/fixed/mobile only const src = srcForRegion; if (src) cands = extractGenericPrefixesXml(src); } if (cands && cands.length) { const uniq = Array.from(new Set(cands)); const collapsed = collapseExhaustiveRanges(uniq); candidatePrefixesByIso2[iso2] = collapsed; // Build raw expansion set from fixed/mobile patterns to detect which entries are collapsed parents const rawSet = new Set(); const src = srcForRegion; if (src) { const collect = (list) => { if (!Array.isArray(list)) return; for (const p of list) { if (typeof p !== "string" || !p) continue; for (const s of simplePrefixesFromPattern(p)) rawSet.add(s); } }; collect(src.fixed); collect(src.mobile); } const parents = new Set(collapsed.filter((s) => !rawSet.has(s))); if (parents.size) collapsedParentsByIso2[iso2] = parents; } }); // For each dial code group, shrink prefixes to minimal unique values among non-main regions // Allow per-dial-code minimum lengths to ensure meaningful disambiguation (e.g., 44 → 4 digits) // const minUniqueLenByDial = { // 1: 3, // 7: 1, // 44: 4, // 262: 3, // 212: 4, // 599: 1, // }; const uniquePrefixByIso2 = {}; Object.keys(dialCodeToRegions).forEach((dial) => { const regions = dialCodeToRegions[dial]; if (!regions || regions.length <= 1) return; const rest = regions.slice(1); const entries = rest .map((r) => ({ iso2: r, full: candidatePrefixesByIso2[r] })) .filter((e) => Array.isArray(e.full) && e.full.length); if (!entries.length) return; // determine minimal unique truncation length between 2 and full length const allLens = entries.flatMap((e) => e.full.map((s) => s.length)); const maxLen = Math.min(6, Math.max(...allLens)); // const minLenDefault = 2; const minLen = 1;//Math.max(minLenDefault, Number(minUniqueLenByDial[dial]) || minLenDefault); // Choose the longest possible unique length first (descending), then collapse if possible for (let len = maxLen; len >= minLen; len--) { const seen = new Map(); let collision = false; // Expand all candidates per region, extending to len when needed const expanded = entries.map((e) => { const arr = []; for (const full of e.full) { let val = full; const parents = collapsedParentsByIso2[e.iso2]; const isCollapsed = parents && parents.has(full); if (val.length < len && !isCollapsed) { val = extendPrefixXml(e.iso2, val, len, xmlSourcesByIso2); } arr.push(val.slice(0, Math.min(len, val.length))); } return { iso2: e.iso2, arr: Array.from(new Set(arr)) }; }); // Check collisions across all regions for (const e of expanded) { for (const k of e.arr) { if (seen.has(k)) { collision = true; break; } seen.set(k, e.iso2); } if (collision) break; } if (!collision) { // Try to collapse ranges per region without breaking uniqueness const collapsedPerIso2 = new Map(); let collapseCollision = false; const seenCollapsed = new Map(); for (const e of expanded) { const collapsedArr = collapseExhaustiveRanges(e.arr); collapsedPerIso2.set(e.iso2, collapsedArr); } // Validate uniqueness after collapse for (const e of expanded) { const arrToUse = collapsedPerIso2.get(e.iso2) || e.arr; for (const k of arrToUse) { if (seenCollapsed.has(k)) { collapseCollision = true; break; } seenCollapsed.set(k, e.iso2); } if (collapseCollision) break; } if (!collapseCollision) { for (const e of expanded) uniquePrefixByIso2[e.iso2] = collapsedPerIso2.get(e.iso2) || e.arr; } else { for (const e of expanded) uniquePrefixByIso2[e.iso2] = e.arr; } return; } } // If we reach here, use full prefixes for (const e of entries) uniquePrefixByIso2[e.iso2] = e.full; }); function getNationalPrefix(iso2) { const np = xmlNationalPrefixByIso2[iso2]; return typeof np === "string" && np.length ? np : null; } const dialCodes = Object.keys(dialCodeToRegions); console.log("Found", dialCodes.length, "country calling codes"); dialCodes .sort((a, b) => Number(a) - Number(b)) .forEach((dialCode) => { const regions = dialCodeToRegions[dialCode]; regions.forEach((iso2) => { if (!allowIso2.has(iso2)) return; // Priority is ignored; always 0 // Compute area codes starting from curated data.ts, then add uncovered generated codes let areaCodes = null; const baseline = curatedAreaCodesByIso2.get(iso2) || null; const area = uniquePrefixByIso2[iso2]; const generated = Array.isArray(area) && area.length ? area.slice() : []; const usedCurated = Array.isArray(baseline) && baseline.length > 0; if (usedCurated) { const base = baseline.slice(); // Determine which generated codes are not already covered by baseline const isCoveredByBaseline = (code) => base.some((b) => typeof b === "string" && code.startsWith(b)); const isBroaderThanBaseline = (code) => base.some((b) => typeof b === "string" && b.startsWith(code)); for (const g of generated) { if (!isCoveredByBaseline(g) && !isBroaderThanBaseline(g)) base.push(g); } areaCodes = base; } else { areaCodes = generated.length ? generated : null; } // Sort generated area codes numerically for deterministic, readable output if (Array.isArray(areaCodes)) { areaCodes = sortDigitStringsNumeric(areaCodes); } // Generic collapse: avoid collapsing when using curated baseline to preserve existing sets if (Array.isArray(areaCodes)) { const usedCurated = Array.isArray(baseline) && baseline.length > 0; if (!usedCurated) areaCodes = collapseExhaustiveRanges(areaCodes); } const nationalPrefix = getNationalPrefix(iso2); const curatedPriority = curatedPriorityByIso2.get(iso2) || 0; const tuple = [iso2, dialCode, curatedPriority, null, null]; if (areaCodes) tuple[3] = areaCodes; if (nationalPrefix) tuple[4] = nationalPrefix; tuples.push(tuple); }); }); tuples.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)); const ts = `export const rawCountryData = ${JSON.stringify(tuples, null, 2)} as const;\n`; fs.mkdirSync(path.dirname(OUT_TS), { recursive: true }); fs.writeFileSync(OUT_TS, ts, "utf8"); console.log(`Wrote ${tuples.length} entries as TS to ${path.relative(process.cwd(), OUT_TS)}`); // Always run diff against curated data.ts after generation function loadArray(tsPath, isDataTs = false) { let s = fs.readFileSync(tsPath, "utf8"); if (isDataTs) { // strip line comments s = s.replace(/\/\/.*$/mg, ""); // drop trailing TS type exports if present s = s.replace(/export type[\s\S]*/, ""); } // normalize export for eval s = s.replace(/export const rawCountryData\s*=\s*/, "var rawCountryData = "); s = s.replace(/\] as const;?\s*$/, "];\n"); const ctx = {}; vm.createContext(ctx); vm.runInContext(s, ctx, { filename: tsPath }); return ctx.rawCountryData; } function toMap(arr) { const m = new Map(); for (const t of arr) { const [iso2, dialCode, priority = 0, areaCodes = null, nationalPrefix = null] = t; m.set(iso2, { iso2, dialCode, priority, areaCodes: areaCodes ?? null, nationalPrefix: nationalPrefix ?? null }); } return m; } function eqArr(a, b) { if (a === b) return true; if (!a && !b) return true; if (!a || !b) return false; if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; } function summarizeArrayChange(name, oldArr, newArr) { const oa = Array.isArray(oldArr) ? oldArr : []; const na = Array.isArray(newArr) ? newArr : []; const oSet = new Set(oa); const nSet = new Set(na); const added = na.filter((x) => !oSet.has(x)); const removed = oa.filter((x) => !nSet.has(x)); const header = `${name}: ${oldArr ? oa.length : "null"} items -> ${newArr ? na.length : "null"} items`; const limit = 12; const fmt = (arr) => { if (!arr.length) return "[]"; if (arr.length <= limit) return `[${arr.map((s)=>JSON.stringify(s)).join(",")}]`; const head = arr.slice(0, limit - 2).map((s)=>JSON.stringify(s)).join(","); const tail = arr.slice(-2).map((s)=>JSON.stringify(s)).join(","); const more = arr.length - (limit); return `[${head},…${more} more… ,${tail}]`; }; if (added.length === 0 && removed.length === 0 && Array.isArray(oldArr) && Array.isArray(newArr)) { // Order-only change return `${name}: reordered (${oa.length} items)`; } // For small diffs, show full arrays for readability if ((oa.length + na.length) <= 40 && (added.length + removed.length) <= 20) { return `${name}: ${JSON.stringify(oldArr)} -> ${JSON.stringify(newArr)}`; } const parts = []; if (added.length) parts.push(`New item(s): ${fmt(added)}`); if (removed.length) parts.push(`Removed item(s): ${fmt(removed)}`); return `${header}${parts.length ? ", " + parts.join(", ") : ""}`; } function runDiff() { const GEN_PATH = OUT_TS; const ORIG_PATH = CURATED_DATA_TS_PATH; if (!fs.existsSync(GEN_PATH)) { console.error("Missing generated file:", GEN_PATH); return; } if (!fs.existsSync(ORIG_PATH)) { console.error("Missing original file:", ORIG_PATH); return; } const gen = loadArray(GEN_PATH, false); const orig = loadArray(ORIG_PATH, true); const g = toMap(gen); const o = toMap(orig); const allIso2 = [...new Set([...g.keys(), ...o.keys()])].sort(); const lines = []; let added = 0, removed = 0, changed = 0; for (const iso of allIso2) { const G = g.get(iso); const O = o.get(iso); if (!G) { removed++; lines.push(`- ${iso}: dial=${O.dialCode}, priority=${O.priority}, areaCodes=${JSON.stringify(O.areaCodes)}, nationalPrefix=${JSON.stringify(O.nationalPrefix)}`); continue; } if (!O) { added++; lines.push(`+ ${iso}: dial=${G.dialCode}, priority=${G.priority}, areaCodes=${JSON.stringify(G.areaCodes)}, nationalPrefix=${JSON.stringify(G.nationalPrefix)}`); continue; } const parts = []; if (G.dialCode !== O.dialCode) parts.push(`dial: ${O.dialCode} -> ${G.dialCode}`); if ((G.priority || 0) !== (O.priority || 0)) parts.push(`priority: ${O.priority || 0} -> ${G.priority || 0}`); if (!eqArr(G.areaCodes, O.areaCodes)) parts.push(summarizeArrayChange("areaCodes", O.areaCodes, G.areaCodes)); if ((G.nationalPrefix || null) !== (O.nationalPrefix || null)) parts.push(`nationalPrefix: ${JSON.stringify(O.nationalPrefix || null)} -> ${JSON.stringify(G.nationalPrefix || null)}`); if (parts.length) { changed++; lines.push(`~ ${iso}: ${parts.join(", ")}`); } } const out = [`# Summary: +${added} added, ~${changed} changed, -${removed} removed`, ...(lines.length ? lines : [])].join("\n"); console.log(out); // Write to tmp file const outDir = path.resolve(REPO_ROOT, "tmp"); try { fs.mkdirSync(outDir, { recursive: true }); } catch { /* ignore */ } fs.writeFileSync(path.join(outDir, "rawCountryData.diff.txt"), out + "\n", "utf8"); if (added || removed || changed) { // Exit with non-zero to signal differences (suitable for CI failure) process.exit(1); } } runDiff(); // iso2 list is sourced from iso2-codes.js; we never write it from this script } try { main(); } catch (e) { console.error("Generation failed:", e && e.stack ? e.stack : e); process.exit(1); } ================================================ FILE: scripts/playwright-linux-docker.sh ================================================ #!/usr/bin/env bash set -euo pipefail PLAYWRIGHT_VERSION_DEFAULT="1.58.0" PLAYWRIGHT_VERSION="$PLAYWRIGHT_VERSION_DEFAULT" # Try to keep the Docker image version in sync with the repo's Playwright version. if command -v node >/dev/null 2>&1; then DETECTED_VERSION="$(node -e " try { const pkg = require('./package.json'); const spec = (pkg.devDependencies && (pkg.devDependencies['@playwright/test'] || pkg.devDependencies.playwright)) || ''; const v = String(spec).replace(/^[^0-9]*/, ''); if (v) process.stdout.write(v); } catch (e) { // ignore } " 2>/dev/null || true)" if [[ -n "$DETECTED_VERSION" ]]; then PLAYWRIGHT_VERSION="$DETECTED_VERSION" fi fi IMAGE_DEFAULT="mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-jammy" IMAGE="${PLAYWRIGHT_DOCKER_IMAGE:-$IMAGE_DEFAULT}" if ! command -v docker >/dev/null 2>&1; then echo "Docker is required to run Playwright in Linux." >&2 echo "Install Docker Desktop, then re-run this command." >&2 exit 1 fi WORKDIR="/work" HOST_UID="" HOST_GID="" if command -v id >/dev/null 2>&1; then HOST_UID="$(id -u)" HOST_GID="$(id -g)" fi DOCKER_ARGS=( --rm --shm-size=1g -e HOME=/tmp -e npm_config_cache=/tmp/.npm -e npm_config_prefix=/tmp/.npm-global -e npm_config_update_notifier=false -e npm_config_fund=false -e npm_config_audit=false -e HOST_UID -e HOST_GID -v "$(pwd):$WORKDIR" -w "$WORKDIR" # Keep node_modules inside the container (avoid host permission issues and platform-specific deps). -v "$WORKDIR/node_modules" ) # Useful on Apple Silicon if the image/platform doesn't match. if [[ -n "${PLAYWRIGHT_DOCKER_PLATFORM:-}" ]]; then DOCKER_ARGS+=( --platform "$PLAYWRIGHT_DOCKER_PLATFORM" ) fi # Run the full workflow inside the container: # - install deps # - build # - run Playwright tests (pass through any extra args) exec docker run "${DOCKER_ARGS[@]}" "$IMAGE" bash -lc \ ' set -euo pipefail mkdir -p "$npm_config_cache" "$npm_config_prefix" npm ci # Closure Compiler (grunt-google-closure-compiler) requires Java. apt-get update apt-get install -y --no-install-recommends openjdk-17-jre-headless rm -rf /var/lib/apt/lists/* npm run build npm run test:e2e -- "$@" # If we ran as root, ensure generated files are owned by the host user. if [[ -n "${HOST_UID:-}" && -n "${HOST_GID:-}" ]]; then chown -R "${HOST_UID}:${HOST_GID}" \ "$WORKDIR/build" \ "$WORKDIR/tmp" \ "$WORKDIR/playwright-report" \ "$WORKDIR/test-results" \ "$WORKDIR/tests-e2e" \ 2>/dev/null || true fi ' -- "$@" ================================================ FILE: site/.gitignore ================================================ node_modules/ tmp/ build/ ================================================ FILE: site/Gruntfile.js ================================================ module.exports = function (grunt) { // load all tasks from package.json require("load-grunt-config")(grunt); // build css grunt.registerTask("build:css", [ "sass", "template:website_css", "template:large_flags_overrides_css", "cssmin", ]); // build esbuild grunt.registerTask("build:esbuild", [ "template:lookup_country_js", // needs to go through esbuild for IPAPI_TOKEN injection "template:right_to_left_js", "template:angular_component_js", "template:react_component_js", "template:playground_js", "shell:esbuild", ]); // build vue component grunt.registerTask("build:vue_component", [ "template:vue_component_js", "shell:vite", ]); // build svelte component grunt.registerTask("build:svelte_component", [ "template:svelte_component_js", "shell:viteSvelte", ]); // build all grunt.registerTask("build", [ "shell:clearBuild", "shell:fetchStats", "copy", "build:css", "build:esbuild", "build:vue_component", "build:svelte_component", "template", "replace:validationPrecise", "strip-html-comments", ]); grunt.registerTask("strip-html-comments", () => { const htmlFiles = grunt.file.expand({ dot: true }, ["build/**/*.html"]); let updatedCount = 0; htmlFiles.forEach((filePath) => { const input = grunt.file.read(filePath); const output = input.replace(//g, ""); if (output !== input) { grunt.file.write(filePath, output); updatedCount += 1; } }); grunt.log.writeln( `strip-html-comments: processed ${htmlFiles.length} files, updated ${updatedCount}` ); }); }; ================================================ FILE: site/README.md ================================================ # Website for intl-tel-input Live here: https://intl-tel-input.com There are 4 page types: - Homepage - Docs page - Playground - Examples page ## Contributing To build and run the site locally: - Install the dependencies: run `npm install` in the root of the intl-tel-input project - Build the website: cd into the site/ directory and run `npm run build` - Run the website: cd into the build/ directory and start a web server, e.g. by running `http-server` (you may have to run `npm install -g http-server` first), and it will give you an address to open in your browser, and you should see the site running locally. Making changes: In the site/ directory, run `npm run watch` to automatically re-build when you edit the source files. See src/ directory for HTML templates/partials, JS/CSS and the docs markdown files. See grunt/template.js for where everything is threaded together. ================================================ FILE: site/esbuild/build.mjs ================================================ import { build } from "esbuild"; import externalUtilsPlugin from "./externalUtilsPlugin.mjs"; const sharedOptions = { bundle: true, plugins: [externalUtilsPlugin], minify: true, define: { // This replaces the string "process.env.IPAPI_TOKEN" with the actual value from Cloudflare's environment "process.env.IPAPI_TOKEN": JSON.stringify(process.env.IPAPI_TOKEN || ""), }, }; // lookup country example build({ ...sharedOptions, entryPoints: ["tmp/examples/js/lookup_country.js"], outfile: "build/examples/js/lookup_country.js", }); // right to left example build({ ...sharedOptions, entryPoints: ["tmp/examples/js/right_to_left.js"], outfile: "build/examples/js/right_to_left_bundle.js", }); // react component example build({ ...sharedOptions, loader: { ".js": "jsx" }, entryPoints: ["tmp/examples/js/react_component.js"], outfile: "build/examples/js/react_component_bundle.js", }); // angular component example build({ ...sharedOptions, loader: { ".ts": "ts" }, tsconfig: "../angular/tsconfig.json", entryPoints: ["tmp/examples/js/angular_component.ts"], outfile: "build/examples/js/angular_component_bundle.js", }); // playground build({ ...sharedOptions, entryPoints: ["src/playground/js/playground.js"], outfile: "build/js/playground.js", }); // all JS files in /src/js build({ ...sharedOptions, entryPoints: ["src/js/**/*.js"], outdir: "build/js", }); ================================================ FILE: site/esbuild/externalUtilsPlugin.mjs ================================================ const externalUtilsPlugin = { name: "external-utils-plugin", setup({ onResolve }) { onResolve({ filter: /utils/ }, args => { return { path: args.path, external: true }; }); }, }; export default externalUtilsPlugin; ================================================ FILE: site/grunt/copy.js ================================================ module.exports = function (grunt) { return { plugin: { cwd: "../build", // set working folder / root to copy src: "**/*", // copy all files and subfolders dest: "build/intl-tel-input", // destination folder expand: true, // required when using cwd }, react: { cwd: "../react/build", // set working folder / root to copy src: "**/*", // copy all files and subfolders dest: "build/intl-tel-input/react", // destination folder expand: true, // required when using cwd }, vue: { cwd: "../vue/build", // set working folder / root to copy src: "**/*", // copy all files and subfolders dest: "build/intl-tel-input/vue", // destination folder expand: true, // required when using cwd }, angular: { cwd: "../angular/build", // set working folder / root to copy src: "**/*", // copy all files and subfolders dest: "build/intl-tel-input/angular", // destination folder expand: true, // required when using cwd }, svelte: { cwd: "../svelte/build", // set working folder / root to copy src: "**/*", // copy all files and subfolders dest: "build/intl-tel-input/svelte", // destination folder expand: true, // required when using cwd }, static: { cwd: "static", // set working folder / root to copy src: "**/*", // copy all files and subfolders dest: "build", // destination folder expand: true, // required when using cwd }, htaccess: { cwd: "static", // set working folder / root to copy src: ".htaccess", // copy the .htaccess dotfile dest: "build", // destination folder expand: true, // required when using cwd }, examples_css: { cwd: "src/examples/css", // set working folder / root to copy src: "*", dest: "build/examples/css", // destination folder expand: true, // required when using cwd } }; }; ================================================ FILE: site/grunt/cssmin.js ================================================ module.exports = function(grunt) { return { target: { files: { // NOTE: don't minify large_flags_overrides.css because we link to it as an example from the large_flags example page 'build/css/website.css': 'build/css/website.css', 'build/css/playground.css': 'build/css/playground.css', 'build/css/homepage.css': 'build/css/homepage.css', 'build/css/docs.css': 'build/css/docs.css' } } }; }; ================================================ FILE: site/grunt/fetchStats.js ================================================ const https = require("https"); const fs = require("fs"); const path = require("path"); function httpsGet(url) { return new Promise((resolve, reject) => { https.get(url, { headers: { "User-Agent": "intl-tel-input-build" } }, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode} for ${url}`)); } else { resolve(data); } }); }).on("error", reject); }); } function formatNumber(n) { if (n >= 1_000_000) { const val = n / 1_000_000; return `${val % 1 === 0 ? val.toFixed(0) : val.toFixed(1)}M`; } if (n >= 1_000) { const val = n / 1_000; return `${val % 1 === 0 ? val.toFixed(0) : val.toFixed(1)}k`; } return `${n}`; } function roundToDisplay(n) { if (n >= 1_000_000) { return Math.round(n / 100_000) * 100_000; } if (n >= 100_000) { return Math.round(n / 1_000) * 1_000; } if (n >= 1_000) { return Math.round(n / 100) * 100; } return n; } async function fetchStats() { const statsPath = path.join(__dirname, "..", "tmp", "stats.json"); // Default/fallback values const defaults = { websites: "120k", downloads: "3.2M", stars: "8.2k" }; const isProd = process.argv.includes("--env=prod"); if (!isProd) { console.log("Dev build — using default stats"); fs.mkdirSync(path.dirname(statsPath), { recursive: true }); fs.writeFileSync(statsPath, JSON.stringify(defaults, null, 2)); return; } let stats = { ...defaults }; try { const ghData = JSON.parse( await httpsGet("https://api.github.com/repos/jackocnr/intl-tel-input") ); stats.stars = formatNumber(roundToDisplay(ghData.stargazers_count)); } catch (e) { console.warn("Failed to fetch GitHub stars, using fallback:", e.message); } try { const npmData = JSON.parse( await httpsGet("https://api.npmjs.org/downloads/point/last-month/intl-tel-input") ); stats.downloads = formatNumber(roundToDisplay(npmData.downloads)); } catch (e) { console.warn("Failed to fetch npm downloads, using fallback:", e.message); } try { const nerdyHtml = await httpsGet( "https://www.nerdydata.com/reports/international-telephone-input/719de9d2-d0e7-4988-b02f-9f9d52687076" ); const match = nerdyHtml.match(/"answerCount"\s*:\s*(\d+)/); if (match) { stats.websites = formatNumber(roundToDisplay(Number(match[1]))); } } catch (e) { console.warn("Failed to fetch NerdyData count, using fallback:", e.message); } fs.mkdirSync(path.dirname(statsPath), { recursive: true }); fs.writeFileSync(statsPath, JSON.stringify(stats, null, 2)); console.log("Stats fetched:", stats); } fetchStats(); ================================================ FILE: site/grunt/replace.js ================================================ module.exports = function(grunt) { return { validationPrecise: { options: { patterns: [ { match: /\biti\.isValidNumber\(\)/g, replacement: "iti.isValidNumberPrecise()", }, ], }, files: { "build/examples/js/validation_precise.js": "build/examples/js/validation_precise.js", }, } }; }; ================================================ FILE: site/grunt/sass.js ================================================ const sass = require('sass'); module.exports = function(grunt) { return { main: { options: { implementation: sass, sourcemap: "none", }, files: { 'build/css/website.css': 'src/css/website.scss', 'build/css/large_flags_overrides.css': 'src/css/large_flags_overrides.scss', 'build/css/highlightjs_overrides.css': 'src/css/highlightjs_overrides.scss', 'build/css/playground.css': 'src/css/playground.scss', 'build/css/docs.css': 'src/css/docs.scss', 'build/css/homepage.css': 'src/css/homepage.scss', } }, }; }; ================================================ FILE: site/grunt/shell.js ================================================ const os = require('os'); module.exports = function(grunt) { return { clearBuild: { command: "rm -rf build tmp", }, fetchStats: { command: `node grunt/fetchStats.js --env=${grunt.option("env") || "dev"}`, }, esbuild: { command: "node esbuild/build.mjs", }, vite: { command: "vite build --config src/examples/js/viteVueDemo.config.js", }, viteSvelte: { command: "vite build --config src/examples/js/viteSvelteDemo.config.mjs", }, }; }; ================================================ FILE: site/grunt/template.js ================================================ module.exports = function (grunt) { const path = require("path"); const { cacheBust, getDirHash, getI18nLanguages, createMarkdownRenderer, buildOpenGraphMetaTags, } = require("./templateUtils"); const env = grunt.option("env"); const isDevBuild = env === "dev" || env === "development"; const showRightSidebarAd = !isDevBuild; const { makeTemplateTask, makeLayoutTask, readCommonPagePartials, readCommonBodyEndScript, readItiLiveResultsScript, readItiScript, } = require("./templateGruntHelpers"); // Helper: create a cache-bust template task for a built asset path const makeCacheBustTask = (assetPath) => ({ src: assetPath, dest: assetPath, options: { data: () => ({ cacheBust }) }, }); const { docsDropdownPages, examplesDropdownPages, } = require("./templateNav"); const md = createMarkdownRenderer(); const toBcp47LanguageTag = (code) => { const raw = String(code || "").trim(); if (!raw) return ""; const parts = raw.split("-"); if (parts.length === 1) return parts[0].toLowerCase(); const [lang, region, ...rest] = parts; const normLang = String(lang).toLowerCase(); const normRegion = region && region.length === 2 ? String(region).toUpperCase() : String(region || ""); return [normLang, normRegion, ...rest].filter(Boolean).join("-"); }; const escapeHtml = (value) => String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); const createI18nLanguageListText = (languageCodes) => { const codes = Array.isArray(languageCodes) ? languageCodes.filter(Boolean) : []; if (!codes.length) { return "_No language modules found._"; } let displayNames = null; try { if (typeof Intl !== "undefined" && Intl.DisplayNames) { displayNames = new Intl.DisplayNames(["en"], { type: "language" }); } } catch { displayNames = null; } const items = codes .map((code) => { const tag = toBcp47LanguageTag(code); let label = null; try { label = displayNames && tag ? displayNames.of(tag) : null; } catch { label = null; } return { code: String(code), label: label ? String(label) : "", }; }); items.sort((a, b) => { const aKey = a.label || a.code; const bKey = b.label || b.code; return aKey.localeCompare(bKey, "en", { sensitivity: "base" }); }); return items .map(({ code, label }) => { const text = label ? `${label} (${code})` : code; return escapeHtml(text); }) .join(", "); }; const homepageTitle = "International Telephone Input"; const homepageMetaDesc = "A JavaScript plugin for entering, formatting and validating international telephone numbers. Includes React, Vue, Angular and Svelte components."; const homepageCanonicalUrl = "https://intl-tel-input.com"; const playgroundTitle = "Playground - International Telephone Input"; const playgroundMetaDesc = "Try different initialisation options and see the plugin update live."; const playgroundCanonicalUrl = "https://intl-tel-input.com/playground"; const notFoundTitle = "404 - Page not found | intl-tel-input"; const notFoundMetaDesc = "Page not found."; const notFoundCanonicalUrl = "https://intl-tel-input.com/404"; const config = { // cache bust common assets iti_script: { src: "src/shared/iti_script.html.ejs", dest: "tmp/shared/iti_script.html", options: { data: () => ({ cacheBust, isDevBuild }), }, }, website_css: makeCacheBustTask("build/css/website.css"), homepage_css: makeCacheBustTask("build/css/homepage.css"), docs_css: makeCacheBustTask("build/css/docs.css"), playground_css: makeCacheBustTask("build/css/playground.css"), large_flags_overrides_css: makeCacheBustTask("build/css/large_flags_overrides.css"), // homepage homepage_layout: { src: "src/layout_template.html.ejs", dest: "tmp/homepage/homepage_layout.html", options: { data: () => { const stats = JSON.parse(grunt.file.read("tmp/stats.json")); const content = grunt.file.read("src/homepage/homepage_content.html") .replace("{{STAT_WEBSITES}}", stats.websites) .replace("{{STAT_DOWNLOADS}}", stats.downloads) .replace("{{STAT_STARS}}", stats.stars); return { layoutClass: "iti-layout-no-sidebars", showLeftSidebar: false, content, name: "home", pageType: "home", docsDropdownPages, examplesDropdownPages, }; }, }, }, homepage_page: { src: "src/homepage/homepage_page_template.html.ejs", dest: "build/index.html", options: { data: () => ({ homepageTitle, homepageMetaDesc, homepageCanonicalUrl, cacheBust, isDevBuild, ...readCommonPagePartials(grunt, { cacheBust, isDevBuild, iti_styles: "homepage", }), og_meta_tags: buildOpenGraphMetaTags({ title: homepageTitle, description: homepageMetaDesc, url: homepageCanonicalUrl, }), layout: grunt.file.read("tmp/homepage/homepage_layout.html"), common_body_end: readCommonBodyEndScript(grunt), iti_live_results_script: readItiLiveResultsScript(grunt, { cacheBust }), iti_script: readItiScript(grunt), }), }, }, // playground playground_js: { src: "src/playground/js/templates/playgroundConstants.js.ejs", dest: "tmp/playground/playgroundConstants.js", options: { data: () => ({ cacheBust, getDirHash, i18nLanguages: getI18nLanguages(), }), }, }, playground_layout: { src: "src/layout_template.html.ejs", dest: "tmp/playground/playground_layout.html", options: { data: () => ({ showLeftSidebar: false, layoutClass: "iti-layout-no-sidebars iti-layout--playground", content: grunt.file.read("src/playground/playground_content.html"), pageType: "playground", name: "playground", docsDropdownPages, examplesDropdownPages, }), }, }, playground_page: { src: "src/playground/playground_page_template.html.ejs", dest: "build/playground.html", options: { data: () => ({ playgroundTitle, playgroundMetaDesc, playgroundCanonicalUrl, cacheBust, ...readCommonPagePartials(grunt, { cacheBust, isDevBuild, iti_styles: "normal", highlightjs_styles: true, }), og_meta_tags: buildOpenGraphMetaTags({ title: playgroundTitle, description: playgroundMetaDesc, url: playgroundCanonicalUrl, }), layout: grunt.file.read("tmp/playground/playground_layout.html"), common_body_end: readCommonBodyEndScript(grunt), iti_live_results_script: readItiLiveResultsScript(grunt, { cacheBust }), iti_script: readItiScript(grunt), }), }, }, // 404 not_found_layout: { src: "src/layout_template.html.ejs", dest: "tmp/404/not_found_layout.html", options: { data: () => ({ showLeftSidebar: false, layoutClass: "iti-layout-no-sidebars", content: grunt.file.read("src/404/404_content.html"), pageType: "home", name: "404", docsDropdownPages, examplesDropdownPages, }), }, }, not_found_page: { src: "src/404/404_page_template.html.ejs", dest: "build/404.html", options: { data: () => ({ cacheBust, head_title: notFoundTitle, canonical_url: notFoundCanonicalUrl, meta_desc: notFoundMetaDesc, og_meta_tags: buildOpenGraphMetaTags({ title: notFoundTitle, description: notFoundMetaDesc, url: notFoundCanonicalUrl, }), ...readCommonPagePartials(grunt, { cacheBust, isDevBuild, iti_styles: "none", }), layout: grunt.file.read("tmp/404/not_found_layout.html"), common_body_end: readCommonBodyEndScript(grunt), }), }, }, }; const registerExample = ({ key, title, metaDesc, js = {}, extraJsTasks = [], content = {}, layoutExtra = {}, pageExtra = {}, }) => { const slug = key.replace(/_/g, "-"); const jsSrc = js.src || `src/examples/js/${key}.js`; // some examples build to tmp first const jsDest = js.dest || `${js.destDir || "build"}/examples/js/${key}.js`; const displayCode = content.displayCode || jsDest; const scriptName = js.script || `${key}.js`; const contentDest = content.dest || `tmp/examples/${key}_content.html`; const layoutDest = content.layoutDest || `tmp/examples/${key}_layout.html`; const pageDest = content.pageDest || `build/examples/${slug}.html`; // the HTML to actually use for the demo const markupName = content.markupName || key; const markupPath = `src/examples/html/${markupName}.html`; // the (cleaner) HTML we want to display in the "Html" section const displayMarkupCandidate = `src/examples/html/${markupName}_display_code.html`; const displayMarkupPath = grunt.file.exists(displayMarkupCandidate) ? displayMarkupCandidate : markupPath; const fullTitle = `${title} example - International Telephone Input`; const canonicalUrl = `https://intl-tel-input.com/examples/${slug}`; const templateData = { cacheBust, ...(js.data || {}), }; config[`${key}_js`] = makeTemplateTask(jsSrc, jsDest, () => templateData); extraJsTasks.forEach((t) => { config[t.key] = makeTemplateTask(t.src, t.dest, () => ({ cacheBust })); }); config[`${key}_content`] = makeTemplateTask( "src/examples/examples_content_template.html.ejs", contentDest, () => ({ cacheBust, content_title: title, desc: grunt.file.read(`src/examples/copy/${key}_desc.html`), markup: grunt.file.read(markupPath), display_markup: grunt.file.read(displayMarkupPath), display_code: (() => { let displayCodeContent = grunt.file.read(displayCode); // hack so that the validation_precise example page shows the right validation method in the displayed code if (key === "validation_precise") { displayCodeContent = displayCodeContent.replace( /\biti\.isValidNumber\(\)/g, "iti.isValidNumberPrecise()" ); } return grunt.template.process(displayCodeContent, { data: templateData }); })(), script: scriptName, ...(content.demo_note ? { demo_note: content.demo_note } : {}), ...(content.hideMarkupSection ? { hideMarkupSection: true } : {}), ...(content.isRtl ? { isRtl: true } : {}), ...(content.extraData ? content.extraData() : {}), common_body_end: readCommonBodyEndScript(grunt), ...(content.includeItiScript ? { iti_script: readItiScript(grunt) } : {}), }) ); config[`${key}_layout`] = makeLayoutTask(grunt, { dest: layoutDest, showLeftSidebar: true, layoutClass: "iti-layout-both-sidebars", navPath: "src/examples/examples_nav_template.html.ejs", contentPath: contentDest, name: key, pageType: "examples", docsDropdownPages, examplesDropdownPages, extra: { ...layoutExtra, show_right_sidebar_ad: showRightSidebarAd, }, }); config[`${key}_page`] = makeTemplateTask( "src/examples/examples_page_template.html.ejs", pageDest, () => ({ cacheBust, head_title: fullTitle, canonical_url: canonicalUrl, meta_desc: metaDesc, og_meta_tags: buildOpenGraphMetaTags({ title: fullTitle, description: metaDesc, url: canonicalUrl, }), ...readCommonPagePartials(grunt, { cacheBust, isDevBuild, iti_styles: pageExtra.iti_styles || "normal", highlightjs_styles: true, }), content: grunt.file.read(layoutDest), ...pageExtra, }) ); }; const docsDefinitions = [{ key: "choose_integration", title: "Choose integration", metaDesc: "Which integration of intl-tel-input is right for you? Pure JavaScript, React, Vue, Angular or Svelte component?", }, { key: "getting_started", title: "Getting started", metaDesc: "How to quickly get up and running with intl-tel-input.", }, { key: "options", title: "Initialisation options", metaDesc: "All the different options you can use when initialising intl-tel-input.", }, { key: "localisation", title: "Localisation", metaDesc: "How to localise country names and user interface strings, including RTL support.", }, { key: "accessibility", title: "Accessibility", metaDesc: "Accessibility guidance for intl-tel-input, including keyboard and screen reader support.", }, { key: "methods", title: "Methods", metaDesc: "All the different methods you can call on an intl-tel-input instance.", }, { key: "events", title: "Events", metaDesc: "All the different events that an intl-tel-input instance can emit.", }, { key: "utils", title: "Utilities script", metaDesc: "Learn about the utils script, what it's for and how to load it.", }, { key: "theming", title: "Theming / dark mode", metaDesc: "How to theme the plugin, including how to set it up for dark mode.", }, { key: "troubleshooting", title: "Troubleshooting", metaDesc: "Solutions to common problems and FAQs about intl-tel-input.", }, { key: "faq", title: "FAQ", metaDesc: "Frequently asked questions about intl-tel-input, including common setup and localisation topics.", }, { key: "react_component", title: "React component", metaDesc: "How to use the intl-tel-input React component.", }, { key: "vue_component", title: "Vue component", metaDesc: "How to use the intl-tel-input Vue component.", }, { key: "angular_component", title: "Angular component", metaDesc: "How to use the intl-tel-input Angular component.", }, { key: "svelte_component", title: "Svelte component", metaDesc: "How to use the intl-tel-input Svelte component.", }]; const exampleDefinitions = [{ key: "lookup_country", title: "Lookup user's country", metaDesc: "Automatically set the country based on the user's IP address.", js: { // evaluate the template into tmp, then use esbuild for IPAPI_TOKEN injection destDir: "tmp", }, content: { markupName: "simple_input", includeItiScript: true, displayCode: "src/examples/js/lookup_country_display_code.js", }, }, { key: "right_to_left", title: "Right to left", metaDesc: "Support for right-to-left languages.", js: { destDir: "tmp", script: "right_to_left_bundle.js", }, content: { markupName: "simple_input", isRtl: true, displayCode: "src/examples/js/right_to_left_display_code.js", }, layoutExtra: { isRtl: true }, }, { key: "single_country", title: "Single country", metaDesc: "When you only need to handle numbers from a single country.", content: { demo_note: "

Enter a US number:

", markupName: "validation", includeItiScript: true, displayCode: "src/examples/js/single_country_display_code.js", }, pageExtra: { stylesheet_after_website_css: "/examples/css/validation.css" }, }, { key: "validation_practical", title: "Validation", metaDesc: "Validate the user's phone number and if there's an error, display a relevant message.", js: { src: "src/examples/js/validation.js", }, content: { markupName: "validation", includeItiScript: true, displayCode: "src/examples/js/validation_display_code.js", extraData: () => ({ demo_note: `

NOTE: by default, isValidNumber only returns true for mobile and fixed line numbers. See allowedNumberTypes option for more information.

`, }), }, pageExtra: { stylesheet_after_website_css: "/examples/css/validation.css", }, }, { key: "validation_precise", title: "Precise validation (advanced)", metaDesc: "Validate the user's phone number using the more precise method, and if there's an error, display a relevant message.", js: { src: "src/examples/js/validation.js", }, content: { markupName: "validation", includeItiScript: true, displayCode: "src/examples/js/validation_display_code.js", extraData: () => ({ demo_note: `

NOTE: by default, isValidNumberPrecise only returns true for mobile and fixed line numbers. See allowedNumberTypes option for more information.

`, }), }, pageExtra: { stylesheet_after_website_css: "/examples/css/validation.css", }, }, { key: "hidden_input", title: "Hidden input", metaDesc: "Automatically populate a hidden input with the full international number, so it gets submitted to your backend.", content: { includeItiScript: true, markupName: "validation", displayCode: "src/examples/js/hidden_input_display_code.js", }, }, { key: "multiple_instances", title: "Multiple instances", metaDesc: "Use multiple instances of the plugin with different configurations on the same page.", content: { includeItiScript: true, displayCode: "src/examples/js/multiple_instances_display_code.js", }, pageExtra: { stylesheet_after_website_css: "/examples/css/multiple_instances.css", }, }, { key: "display_number", title: "Display existing number", metaDesc: "Automatically format an existing number during initialisation.", js: { src: "src/examples/js/simple_init_plugin.js", }, content: { includeItiScript: true, displayCode: "src/examples/js/simple_init_plugin_display_code.js", }, }, { key: "large_flags", title: "Large flags", metaDesc: "How to display extra large flag images.", js: { src: "src/examples/js/simple_init_plugin.js", }, content: { markupName: "simple_input", includeItiScript: true, displayCode: "src/examples/js/simple_init_plugin_display_code.js", }, pageExtra: { iti_styles: "largeFlags", }, }, { key: "angular_component", title: "Angular component", metaDesc: "How to use intl-tel-input with Angular.", js: { src: "src/examples/js/angular_component.ts", dest: "tmp/examples/js/angular_component.ts", script: "angular_component_bundle.js", }, content: { markupName: "component", hideMarkupSection: true, displayCode: "src/examples/js/angular_component_display_code.js", script: "angular_component_bundle.js", }, }, { key: "react_component", title: "React component", metaDesc: "How to use intl-tel-input with React.", js: { dest: "tmp/examples/js/react_component.js", script: "react_component_bundle.js", }, content: { markupName: "component", hideMarkupSection: true, displayCode: "src/examples/js/react_component_display_code.js", script: "react_component_bundle.js", }, }, { key: "vue_component", title: "Vue component", metaDesc: "How to use intl-tel-input with Vue.", js: { // need to specify the source because of the alternative .vue extension src: "src/examples/js/vue_component.vue", dest: "tmp/examples/js/vue_component.vue", script: "vue_component_bundle.js", }, content: { markupName: "component", hideMarkupSection: true, displayCode: "src/examples/js/vue_component_display_code.vue", script: "vue_component_bundle.js", }, }, { key: "svelte_component", title: "Svelte component", metaDesc: "How to use intl-tel-input with Svelte.", js: { // need to specify the source because of the alternative .svelte extension src: "src/examples/js/svelte_component.svelte", dest: "tmp/examples/js/svelte_component.svelte", script: "svelte_component_bundle.js", }, content: { markupName: "component", hideMarkupSection: true, displayCode: "src/examples/js/svelte_component_display_code.svelte", script: "svelte_component_bundle.js", }, }]; exampleDefinitions.forEach((definition) => registerExample(definition)); docsDefinitions.forEach(({ key, title, metaDesc }) => { const mdPath = path.join("src", "docs", "markdown", `${key}.md`); const urlSlug = key.replace(/_/g, "-"); const destPath = `build/docs/${urlSlug}.html`; const canonicalUrl = `https://intl-tel-input.com/docs/${urlSlug}`; const fullTitle = `${title} docs - International Telephone Input`; config[`docs_content_${key}`] = { src: "src/docs/docs_content_template.html.ejs", dest: `tmp/docs/${key}_content.html`, options: { data: () => ({ docKey: key, html: (() => { let source = grunt.file.read(mdPath); if (key === "localisation") { const languageList = createI18nLanguageListText(getI18nLanguages()); source = source.replace("", `\n${languageList}\n`); } return md.render(source, { docKey: key }); })(), }), }, }; config[`docs_layout_${key}`] = { src: "src/layout_template.html.ejs", dest: `tmp/docs/${key}_layout.html`, options: { data: () => ({ showLeftSidebar: true, layoutClass: "iti-layout-both-sidebars", nav: grunt.file.read("src/docs/docs_nav_template.html.ejs"), content: grunt.file.read(`tmp/docs/${key}_content.html`), name: key, pageType: "docs", docsDropdownPages, examplesDropdownPages, show_right_sidebar_ad: showRightSidebarAd, }), }, }; config[`docs_page_${key}`] = { src: "src/docs/docs_page_template.html.ejs", dest: destPath, options: { data: () => ({ cacheBust, head_title: fullTitle, canonical_url: canonicalUrl, meta_desc: metaDesc, og_meta_tags: buildOpenGraphMetaTags({ title: fullTitle, description: metaDesc, url: canonicalUrl, }), ...readCommonPagePartials(grunt, { cacheBust, isDevBuild, highlightjs_styles: true, iti_styles: "none", }), layout: grunt.file.read(`tmp/docs/${key}_layout.html`), common_body_end: readCommonBodyEndScript(grunt), }), }, }; }); return config; }; ================================================ FILE: site/grunt/templateGruntHelpers.js ================================================ const makeTemplateTask = (src, dest, data) => ({ src, dest, options: { data, }, }); const readCommonPagePartials = (grunt, data) => ({ common_meta_tags: grunt.file.read("src/shared/common_meta_tags.html"), common_styles: grunt.template.process(grunt.file.read("src/shared/common_styles.html.ejs"), { data, }), common_head_end_prod: data && data.isDevBuild ? "" : grunt.file.read("src/shared/common_head_end_prod.html"), }); const readCommonBodyEndScript = (grunt) => grunt.file.read("src/shared/common_body_end.html"); const readItiLiveResultsScript = (grunt, data) => grunt.template.process(grunt.file.read("src/shared/iti_live_results_script.html.ejs"), { data, }); const readItiScript = (grunt) => grunt.file.read("tmp/shared/iti_script.html"); const makeLayoutTask = ( grunt, { dest, showLeftSidebar, layoutClass, navPath, contentPath, name, pageType, docsDropdownPages, examplesDropdownPages, extra = {}, } ) => makeTemplateTask("src/layout_template.html.ejs", dest, () => ({ showLeftSidebar, layoutClass, ...(navPath ? { nav: grunt.file.read(navPath) } : {}), content: grunt.file.read(contentPath), name, pageType, docsDropdownPages, examplesDropdownPages, ...extra, })); module.exports = { makeTemplateTask, makeLayoutTask, readCommonPagePartials, readCommonBodyEndScript, readItiLiveResultsScript, readItiScript, }; ================================================ FILE: site/grunt/templateNav.js ================================================ const docsDropdownPages = [ { name: "choose_integration", href: "/docs/choose-integration", label: "Choose integration" }, { name: "getting_started", href: "/docs/getting-started", label: "Getting started" }, { name: "options", href: "/docs/options", label: "Initialisation options" }, { name: "localisation", href: "/docs/localisation", label: "Localisation" }, { name: "accessibility", href: "/docs/accessibility", label: "Accessibility" }, { name: "methods", href: "/docs/methods", label: "Methods" }, { name: "events", href: "/docs/events", label: "Events" }, { name: "utils", href: "/docs/utils", label: "Utilities script" }, { name: "theming", href: "/docs/theming", label: "Theming / dark mode" }, { name: "troubleshooting", href: "/docs/troubleshooting", label: "Troubleshooting" }, { name: "faq", href: "/docs/faq", label: "FAQ" }, { name: "react_component", href: "/docs/react-component", label: "React component" }, { name: "vue_component", href: "/docs/vue-component", label: "Vue component" }, { name: "angular_component", href: "/docs/angular-component", label: "Angular component" }, { name: "svelte_component", href: "/docs/svelte-component", label: "Svelte component" }, ]; const examplesDropdownPages = [ { name: "validation_practical", href: "/examples/validation-practical", label: "Validation" }, { name: "lookup_country", href: "/examples/lookup-country", label: "Lookup user's country" }, { name: "single_country", href: "/examples/single-country", label: "Single country" }, { name: "right_to_left", href: "/examples/right-to-left", label: "Right to left" }, { name: "hidden_input", href: "/examples/hidden-input", label: "Hidden input" }, { name: "display_number", href: "/examples/display-number", label: "Display existing number" }, { name: "multiple_instances", href: "/examples/multiple-instances", label: "Multiple instances" }, { name: "validation_precise", href: "/examples/validation-precise", label: "Precise validation (advanced)" }, { name: "large_flags", href: "/examples/large-flags", label: "Large flags" }, { name: "react_component", href: "/examples/react-component", label: "React component" }, { name: "vue_component", href: "/examples/vue-component", label: "Vue component" }, { name: "angular_component", href: "/examples/angular-component", label: "Angular component" }, { name: "svelte_component", href: "/examples/svelte-component", label: "Svelte component" }, ]; module.exports = { docsDropdownPages, examplesDropdownPages, }; ================================================ FILE: site/grunt/templateUtils.js ================================================ const path = require("path"); const fs = require("fs"); const crypto = require("crypto"); const MarkdownIt = require("markdown-it"); const markdownItAnchor = require("markdown-it-anchor"); const BUILD_DIR = "build"; const HASH_LENGTH = 12; const hashCacheByPath = new Map(); const toPosixPath = (p) => String(p || "").replace(/\\/g, "/"); const defaultSlugifyHeading = (value) => String(value) .trim() .toLowerCase() // remove apostrophes .replace(/['’]/g, "") // replace non-alphanumeric with hyphens .replace(/[^a-z0-9]+/g, "-") // collapse repeats .replace(/-+/g, "-") // trim hyphens .replace(/^-|-$/g, ""); // This plugin (AI-generated) looks for the "options" doc (etc), and injects table layout markup for display purposes. It relies on each h6 option being immediately followed by a paragraph containing the Type/Default info, and then any remaining content for that option (e.g. description, examples) coming after that in the same section (until the next heading). const addDocOptionsLayoutPlugin = (md) => { md.core.ruler.after("inline", "iti_doc_options_layout", (state) => { const env = state.env || {}; const applyToDocKeys = ["options", "react_component", "vue_component", "angular_component", "svelte_component"]; if (!applyToDocKeys.includes(env.docKey)) return; const tokens = state.tokens; const isHeadingOpen = (token, tag) => token && token.type === "heading_open" && token.tag === tag; const isAnyHeadingOpen = (token) => token && token.type === "heading_open" && /^h[1-6]$/.test(token.tag); const makeHtmlBlock = (content) => { const token = new state.Token("html_block", "", 0); token.content = content; return token; }; const startRowAndKeyCellMarkup = () => makeHtmlBlock( '
\n' + '
\n', ); const endKeyCellAndStartValueCellMarkup = () => makeHtmlBlock( "
\n" + '
\n', ); const endValueCellAndRowMarkup = () => makeHtmlBlock("
\n
\n"); const nextTokens = []; let i = 0; const consumeThroughHeadingClose = (tagName) => { while (i < tokens.length) { const t = tokens[i]; nextTokens.push(t); i += 1; if (t.type === "heading_close" && t.tag === tagName) return; } }; const findHeadingCloseIndex = (startIndex, tagName) => { for (let k = startIndex; k < tokens.length; k += 1) { const t = tokens[k]; if (t.type === "heading_close" && t.tag === tagName) return k; } return -1; }; // ad blocks are divs with class="article-ad", but MarkdownIt treats divs and their contents as a single raw "html_block" token const isAdBlockOpen = (token) => token.type === "html_block" && token.content.includes('class="article-ad"'); const consumeUntilNextHeadingOrAdBlock = () => { while (i < tokens.length) { const t = tokens[i]; if (isAnyHeadingOpen(t) || isAdBlockOpen(t)) return; nextTokens.push(t); i += 1; } }; const consumeOptionBlock = () => { nextTokens.push(startRowAndKeyCellMarkup()); // H6 heading (option name) lives in the key cell. nextTokens.push(tokens[i]); i += 1; consumeThroughHeadingClose("h6"); // The Type/Default paragraph also lives in the key cell. // Consume paragraph_open, inline, paragraph_close. while (i < tokens.length && tokens[i].type !== "paragraph_close") { nextTokens.push(tokens[i]); i += 1; } if (i < tokens.length && tokens[i].type === "paragraph_close") { nextTokens.push(tokens[i]); i += 1; } // Switch to the value cell for the rest of the option content. nextTokens.push(endKeyCellAndStartValueCellMarkup()); consumeUntilNextHeadingOrAdBlock(); nextTokens.push(endValueCellAndRowMarkup()); }; while (i < tokens.length) { const token = tokens[i]; if (isHeadingOpen(token, "h6")) { // Only treat this heading as an "option" row when the very next block // after the heading close is a paragraph containing the Type/Default meta info const closeIndex = findHeadingCloseIndex(i, "h6"); const afterClose = closeIndex >= 0 ? closeIndex + 1 : -1; if (afterClose >= 0 && tokens[afterClose] && tokens[afterClose].type === "paragraph_open") { const inlineToken = tokens[afterClose + 1]; const inlineContent = inlineToken && inlineToken.type === "inline" ? inlineToken.content : ""; if (inlineContent.includes("Type:") && inlineContent.includes("Default:")) { consumeOptionBlock(); continue; } } // Not an options entry: emit the heading tokens unchanged. nextTokens.push(token); i += 1; continue; } nextTokens.push(token); i += 1; } state.tokens = nextTokens; }); }; const createMarkdownRenderer = ({ slugifyHeading = defaultSlugifyHeading } = {}) => { const md = new MarkdownIt({ html: true, linkify: true, typographer: true, }).use(markdownItAnchor, { slugify: slugifyHeading, permalink: markdownItAnchor.permalink.headerLink({ safariReaderFix: true }), }); addDocOptionsLayoutPlugin(md); return md; }; const resolveBuildPathFromUrl = (urlPath) => { const clean = toPosixPath(String(urlPath || "").split("?")[0]); const withoutLeadingSlash = clean.replace(/^\//, ""); return path.join(BUILD_DIR, withoutLeadingSlash); }; const hashFile = (filePath) => { const resolved = path.resolve(filePath); if (hashCacheByPath.has(resolved)) return hashCacheByPath.get(resolved); try { const content = fs.readFileSync(resolved); const hash = crypto .createHash("sha256") .update(content) .digest("hex") .slice(0, HASH_LENGTH); hashCacheByPath.set(resolved, hash); return hash; } catch { const fallback = "missing"; hashCacheByPath.set(resolved, fallback); return fallback; } }; const hashDirRecursive = (dirPath) => { const resolved = path.resolve(dirPath); const cacheKey = `${resolved}:dir`; if (hashCacheByPath.has(cacheKey)) return hashCacheByPath.get(cacheKey); try { const files = []; const walk = (currentDir) => { fs.readdirSync(currentDir, { withFileTypes: true }).forEach((entry) => { const abs = path.join(currentDir, entry.name); if (entry.isDirectory()) { walk(abs); } else if (entry.isFile()) { files.push(abs); } }); }; walk(resolved); files.sort((a, b) => a.localeCompare(b)); const hasher = crypto.createHash("sha256"); files.forEach((abs) => { const rel = path.relative(resolved, abs); hasher.update(rel); hasher.update("\0"); hasher.update(fs.readFileSync(abs)); hasher.update("\0"); }); const hash = hasher.digest("hex").slice(0, HASH_LENGTH); hashCacheByPath.set(cacheKey, hash); return hash; } catch { const fallback = "missing"; hashCacheByPath.set(cacheKey, fallback); return fallback; } }; // Take a URL path (e.g. "/intl-tel-input/js/utils.js"), resolve it to a file in the build directory, hash that file, and return the URL with a cache-busting query param (e.g. "/intl-tel-input/js/utils.js?v=abc123"). const cacheBust = (urlPath) => { const resolvedPath = resolveBuildPathFromUrl(urlPath); return `${urlPath}?v=${hashFile(resolvedPath)}`; }; // Take a URL path to a directory (e.g. "/intl-tel-input/js/i18n"), resolve it to a directory in the build directory, hash all files within that directory recursively, and return the hash to use as a cache-busting query param (e.g. "abc123"). const getDirHash = (urlDirPath) => { const resolvedPath = resolveBuildPathFromUrl(urlDirPath); return hashDirRecursive(resolvedPath); }; const getI18nLanguages = () => { try { const i18nDir = path.join(BUILD_DIR, "intl-tel-input", "js", "i18n"); return fs .readdirSync(i18nDir, { withFileTypes: true }) .filter((d) => d.isDirectory()) .map((d) => d.name) .filter(Boolean) .sort((a, b) => a.localeCompare(b)); } catch { return []; } }; const escapeHtmlAttr = (value) => String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """); const buildOpenGraphMetaTags = ({ title, description, url, }) => { return [ ``, ``, ``, ``, ``, ``, ].join("\n"); }; module.exports = { createMarkdownRenderer, cacheBust, getDirHash, getI18nLanguages, buildOpenGraphMetaTags, }; ================================================ FILE: site/grunt/watch.js ================================================ module.exports = function(grunt) { return { js: { files: ["src/**/*", "static/**/*", "grunt/**/*", "../build/**/*"], tasks: "build" } }; }; ================================================ FILE: site/package.json ================================================ { "name": "iti-website", "version": "1.0.0", "description": "The website for intl-tel-input", "dependencies": { "intl-tel-input": "*", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@angular/compiler": "^19.2.18", "@angular/core": "^19.2.18", "@angular/forms": "^19.2.18", "@angular/platform-browser": "^19.2.18", "@vitejs/plugin-vue": "^5.1.3", "esbuild": "^0.25.0", "eslint": "^8.38.0", "grunt": "^1.6.1", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-cssmin": "^5.0.0", "grunt-contrib-watch": "^1.1.0", "grunt-replace": "^2.0.2", "grunt-sass": "^3.0.0", "grunt-shell": "^4.0.0", "grunt-template": "^1.0.0", "load-grunt-config": "^4.0.1", "markdown-it": "^14.1.0", "markdown-it-anchor": "^9.2.0", "prettier": "^2.8.7", "prop-types": "^15.8.1", "zone.js": "^0.15.1" }, "scripts": { "watch": "grunt watch --env=dev", "build": "grunt build --env=dev", "build:prod": "grunt build --env=prod" } } ================================================ FILE: site/src/404/404_content.html ================================================

404

That page doesn’t exist.

================================================ FILE: site/src/404/404_page_template.html.ejs ================================================ <%= common_meta_tags %> <%= head_title %> <%= og_meta_tags %> <%= common_styles %> <%= common_head_end_prod %> <%= layout %> <%= common_body_end %> ================================================ FILE: site/src/css/_base.scss ================================================ html { /* fix: links with # scroll values in (e.g. docs/options#separatedialcode) scroll until subtitle is below header, as it has position:sticky */ scroll-padding-top: 70px; } body { min-height: 100vh; background-repeat: no-repeat; background-size: cover; } * { box-sizing: border-box; } /* vue and react logos in news section on homepage */ p img { vertical-align: baseline; } h2, h3 { margin-top: 20px; margin-bottom: 10px; } h1 { margin-bottom: 20px; } /* bootstrap override */ @media (min-width: 1200px) { .h2, h2 { font-size: 1.7rem; } } .section { margin-bottom: 30px; } /** Cookie / Google consent repositioning (desktop) **/ @media (min-width: 800px) { body .fc-consent-root .fc-dialog-container { position: absolute !important; bottom: 30px !important; right: 30px !important; } } .iti-live-results { margin: 10px 0 20px; border: 1px solid rgb(var(--header-bg-color-rgb)); padding: 10px; border-radius: 3px; background-color: rgb(var(--header-bg-color-rgb) / 0.3); font-style: italic; text-align: left; } /* Page breadcrumb (Docs/Examples) */ .iti-breadcrumb { margin-bottom: 0.25rem; font-size: 0.875rem; color: var(--bs-secondary-color); &__list { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; } &__item + &__item::before { content: "/"; opacity: 0.6; padding: 0 0.4rem; } a { color: inherit; text-decoration: none; &:hover { text-decoration: underline; } } } // prevent layout shift when initialise components / show validation errors in demos #app, .validation-demo { min-height: 71px; } // in-article ads should have some margin .article-ad { margin: 20px 0; } // Anchor link styles for clickable section titles (used on Docs and Playground pages) .header-anchor { // reset title styles, now that they're wrapped in a link color: inherit; &:not(:hover) { text-decoration: none; } // on non-touch screens @media (hover: hover) and (pointer: fine) { &::before { position: absolute; content: "#"; margin-left: -0.9em; // space from title padding-right: 0.9em; // allow moving cursor from title to icon without losing hover state margin-top: 0.3em; font-size: 0.75em; color: var(--bs-secondary-color); } } &:not(:hover,:focus)::before { visibility: hidden; } } ================================================ FILE: site/src/css/_forms.scss ================================================ // example page demos .demo input[type="tel"] { width: 250px; } // override bootstrap default placeholder color which is too high contrast for our design input::placeholder, .demo input::placeholder, input.form-control::placeholder, textarea.form-control::placeholder { color: #bbb; @media (prefers-color-scheme: dark) { color: #5a5a5a; } } // match bootstrap input border radius .iti__selected-country-primary { border-top-left-radius: var(--bs-border-radius); border-bottom-left-radius: var(--bs-border-radius); } :root { /* plugin overrides (NOTE: the bootstrap vars handle dark mode automatically) */ --iti-border-color: var(--bs-border-color); --iti-dropdown-bg: var(--bs-body-bg); --iti-icon-color: var(--bs-body-color); --iti-hover-color: #f8f9fa; @media (prefers-color-scheme: dark) { --iti-hover-color: #30363d; } } .iti__dropdown-content, .iti__search-input { border-radius: var(--bs-border-radius); } ================================================ FILE: site/src/css/_layout.scss ================================================ /* LAYOUT */ .iti-gutter { --bs-gutter-x: 3rem; } /* never show the right sidebar on mobile (or on some desktop pages) */ /* NOTE: can't include left-sidebar here, because it is always used on mobile */ .iti-right-sidebar { display: none; } @media (min-width: 992px) { .iti-layout-both-sidebars { display: grid; grid-template-areas: "nav main ads"; /* this minmax ensures main block doesn't encroach on sidebars */ grid-template-columns: minmax(0, 1fr) minmax(0, 4fr) minmax(0, 1fr); gap: 1.5rem; } } .iti-layout-both-sidebars { .iti-left-sidebar { grid-area: nav; min-width: 0; } .iti-right-sidebar { grid-area: ads; min-width: 0; } } @media (min-width: 992px) { .iti-layout-both-sidebars { .iti-left-sidebar, .iti-right-sidebar { position: sticky; top: 5rem; display: block !important; height: calc(100vh - 6rem); padding-left: 0.25rem; margin-left: -0.25rem; overflow-y: auto; } } } .iti-main { grid-area: main; min-width: 0; padding-bottom: 30px; } /* Sticky-footer layout: ensure short pages still fill the viewport so any * elements injected at end of (e.g. AdSense) get pushed down. */ .iti-page { min-height: 100vh; display: flex; flex-direction: column; } .iti-page__main { flex: 1 0 auto; } ================================================ FILE: site/src/css/_navbar.scss ================================================ /* TOP NAVBAR */ .iti-navbar { padding: 0.75rem 0; background-image: linear-gradient(rgb(var(--header-bg-color-rgb) / 1), rgb(var(--header-bg-color-rgb) / 0.95)); box-shadow: 0 .5rem 1rem #00000026; .navbar-toggler { padding: 0; border: 0; } /* Header dropdown toggle: replace Bootstrap caret triangle with CSS chevron */ .dropdown-toggle::after { display: inline-block; width: 7px; height: 7px; margin: 0px 0 3px 4px; content: ""; border: solid currentColor; border-width: 0 2px 2px 0; transform: rotate(45deg); } // cute spinny logo animation on hover @media (hover: hover) and (pointer: fine) { .iti-logo { transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); } .navbar-brand:hover .iti-logo { transform: rotate(360deg) scale(1.1); } } } @media (min-width: 992px) { .iti-navbar-container { position: relative; } // Center search on the page (not within remaining flex space) .iti-navbar-search { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } } .DocSearch-Container { /* above Bootstrap navbar (z-1020) but below mobile sidebar (z-1055) */ z-index: 1051; } .DocSearch-Button { width: 260px; --docsearch-search-button-background: rgba(0, 0, 0, .1); --docsearch-search-button-text-color: rgba(255, 255, 255, .65); border: 1px solid rgba(255, 255, 255, .4); &:hover { --docsearch-search-button-text-color: rgba(255, 255, 255, 1); border-color: rgba(255, 255, 255, 1); } } .DocSearch-Button-Keys { min-width: 0; padding: .125rem .25rem; background: rgba(0, 0, 0, .25); border-radius: .25rem } .DocSearch-Button-Key { color: rgba(255, 255, 255, .65); width: auto; height: 1.25rem; padding-right: .125rem; padding-left: .125rem; background: none; border: none; margin: 0 !important; } // disable DocSearch's "pressed" styles .DocSearch-Button-Key--pressed { box-shadow: none !important; transform: none !important; } // mobile search icon button in header .iti-navbar-icon-btn { padding: 0.25rem; border: 0; background: transparent; line-height: 1; svg { width: 20px; } } .iti-navbar-search { min-width: 0; } // both in header and mobile sidebar .iti-logo { margin-right: 10px; } #bdNavbar { .nav-link { color: white; // fix: unselected playground link not looking centered (to make space for bold active state) text-align: center; /* Prevent nav items shifting when toggling font-weight by reserving space for the bold version of the label (used on the active state). */ &.iti-navlink-no-shift > span { display: inline-block; } &.iti-navlink-no-shift > span::after { content: attr(data-text); display: block; height: 0; overflow: hidden; visibility: hidden; } &.iti-navlink-no-shift > span::after, &.active { font-weight: 700; } } .nav-link--github { height: 100%; /* required for vertical alignment */ } @media (min-width: 992px) { .navbar-nav { .nav-link { opacity: 0.85; } .nav-link:hover, .nav-link:focus, .nav-link.active, .show > .nav-link { opacity: 1; } } } /* Header docs dropdown: active item uses header green */ .dropdown-item.active, .dropdown-item:active { background-color: rgb(var(--header-bg-color-rgb) / 0.5); color: white; } /* Header nav dropdown: open on hover (desktop) + keyboard focus */ @media (hover: hover) and (pointer: fine) { .dropdown:hover > .dropdown-menu { display: block; /* Match Bootstrap's positioning styles that normally apply when JS adds data-bs-popper */ top: 100%; left: 0; } } } /* override Bootstrap's margin which causes a gap between the dropdown and toggle */ .dropdown-menu[data-bs-popper] { margin-top: 0 !important; } .offcanvas-title a { color: var(--bs-body-color); text-decoration: none; } /* Mobile nav: remove hover styles (prevents iOS/Android sticky :hover states) */ .iti-mobile-sidebar { .nav-pills .nav-link:not(.active):hover { background-color: transparent; } } .iti-left-sidebar .nav-link, .iti-mobile-sidebar .nav-link { color: var(--bs-body-color); } .nav--primary-links > .nav-item > .nav-link { font-weight: 700; font-size: 1.05rem; padding-top: 0.65rem; padding-bottom: 0.65rem; } .iti-mobile-sidebar { .iti-mobile-submenu .nav-link { font-size: 0.95rem; font-weight: 500; opacity: 0.95; } /* Mobile sidebar icon + label spacing: slightly wider than Bootstrap gap-2 */ .d-flex.align-items-center.gap-2 { gap: calc(0.5rem + 2px) !important; } } /* Mobile sidebar icon + label spacing: slightly wider than Bootstrap gap-2 */ .iti-mobile-nav-toggle { border: 0; background: none; &__chevron { margin: 0px 0 3px 6px; width: 8px; height: 8px; display: inline-block; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(45deg); opacity: 0.85; } &[aria-expanded="true"] &__chevron { transform: rotate(225deg); margin-top: 8px; } } .nav-pills { .nav-link.active { background-color: rgb(var(--header-bg-color-rgb) / 0.5); } .nav-link:not(.active):hover { background-color: rgb(var(--header-bg-color-rgb) / 0.5); } } ================================================ FILE: site/src/css/_variables.scss ================================================ :root { /* Header background color as space-separated RGB for easy alpha usage via: rgb(var(--header-bg-color-rgb) / ) */ --header-bg-color-rgb: 16 135 75; /* override flag paths, to include cache busting */ --iti-path-flags-1x: url("<%= cacheBust('/intl-tel-input/img/flags.webp') %>"); --iti-path-flags-2x: url("<%= cacheBust('/intl-tel-input/img/flags@2x.webp') %>"); --iti-icon-color: var(--bs-body-color); } ================================================ FILE: site/src/css/docs.scss ================================================ /* Docs options page layout: keep code blocks within the content width */ .iti-doc-options { &__row { border: var(--bs-border-width, 1px) solid var(--bs-border-color, #dee2e6); display: flex; p:last-child, pre:last-child { margin-bottom: 0; } & + & { border-top: none; } } &__cell { vertical-align: top; box-sizing: border-box; padding: var(--bs-table-cell-padding-y, 0.5rem) var(--bs-table-cell-padding-x, 0.5rem); min-width: 0; } &__cell--key { flex: 0 0 210px; /* this is wide enough for all of the option names */ border-right: var(--bs-border-width, 1px) solid var(--bs-border-color, #dee2e6); h6 { font-weight: 700; } } &__cell--value { flex: 1 1 auto; min-width: 0; } /* Docs options layout: stack key/value on small screens */ @media (max-width: 600px) { &__row { flex-direction: column; } &__cell--key { flex: 0 0 auto; border-right: none; } } } ================================================ FILE: site/src/css/highlightjs_overrides.scss ================================================ /* Highlight.js overrides */ .hljs { @media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) { background-color: #f6f8fa; /* match GitHub */ } @media (prefers-color-scheme: dark) { background-color: #151b23; /* match GitHub */ } } ================================================ FILE: site/src/css/homepage.scss ================================================ @use 'variables'; /************** * HOMEPAGE **************/ body.iti-page--homepage { background-image: linear-gradient( 182deg, rgba(var(--bs-body-bg-rgb), 0.012), rgba(var(--bs-body-bg-rgb), 1) 87% ), radial-gradient( ellipse at 12% 18%, rgba(65, 133, 234, 0.32), transparent 55% ), radial-gradient( ellipse at 88% 14%, rgba(111, 167, 104, 0.45), transparent 55% ), radial-gradient( ellipse at 90% 56%, rgba(128, 63, 240, 0.42), transparent 52% ), radial-gradient( ellipse at 10% 60%, rgba(224, 76, 135, 0.42), transparent 52% ); } .homepage-content { margin-top: 60px; margin-left: auto; margin-right: auto; .section--homepage-demo, .section--twilio { max-width: 580px; } h1 { font-size: 41px; font-weight: 600; } /* allow this part of title to wrap on small screens, but keep together on larger screens */ @media (min-width: 420px) { h1 .keep-together { white-space: nowrap; } } .text-muted a { color: inherit; /* keep the same muted hue, but remove Bootstrap's alpha transparency on interaction */ &:hover { color: rgb(var(--bs-secondary-color-rgb)); } } .section { margin-bottom: 60px; } /* remove margin on last section */ .section:last-child { margin-bottom: 0; } @media (min-width: 992px) { margin-top: 100px; .section--homepage-demo, .section--twilio { max-width: 700px; } h1 { font-size: 58px; } p.subtitle { font-size: 23px; } } .section--homepage-demo { margin-top: 10px; /* homepage: make the demo input width match the live results box */ .iti, input[type="tel"] { width: 100%; text-align: left; /* required because we have text-align:center on this section */ } /* to match the other sections, which have a

at the bottom with this margin */ .btn-primary { margin-bottom: 16px; } } .section--twilio { h2 { margin-bottom: 0; } } .section--social-proof { .social-proof-logos { margin-top: 24px; max-width: 900px; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 24px 32px; .logo { height: 40px; width: auto; opacity: 0.55; filter: grayscale(100%); transition: opacity 0.2s; &:hover { opacity: 0.8; } } // hide last 3 logos on mobile .logo:nth-last-child(-n+3) { display: none; @media (min-width: 768px) { display: inline-flex; } } .logo-pair { display: inline-flex; align-items: center; img { height: 100%; width: auto; } } /* The following AI generated code attempts to approximate var(--bs-body-color) (#212529 light / #dee2e6 dark) via filters */ // nike, unicef, forbes, ubuntu_text, and quickbooks_text logos are black on transparent img[src*="nike.png"], img[src*="unicef.png"], img[src*="forbes"], img[src*="ubuntu_text"], img[src*="quickbooks_text"] { filter: invert(0.13); @media (prefers-color-scheme: dark) { filter: invert(0.87); } } // dell logo is blue on transparent, so use brightness(0) to make it black before inverting img[src*="dell"] { filter: brightness(0) invert(0.13); @media (prefers-color-scheme: dark) { filter: brightness(0) invert(0.87); } } // universal music logo is white on transparent img[src*="universal-music"] { filter: invert(0.87); @media (prefers-color-scheme: dark) { filter: invert(0.13); } } } } @media (min-width: 992px) { .section--social-proof .social-proof-logos .logo { height: 48px; } } .section--stats { .stats-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 24px 48px; } .stat { display: flex; flex-direction: column; align-items: center; } .stat-number { font-size: 36px; font-weight: 700; color: rgb(var(--bs-link-color-rgb)); } .stat-label { font-size: 14px; color: var(--bs-secondary-color); margin-top: 4px; } } @media (min-width: 992px) { .section--stats { .stat-number { font-size: 48px; } .stat-label { font-size: 16px; } } } /* icons that should match link colour */ .section--features { .link-icon { color: rgb(var(--bs-link-color-rgb)) !important; } } /* there is a br in the middle of the playground preset links: ignore it on mobile, show it on larger screens to break the links into two rows */ @media (min-width: 550px) { .playground-presets { max-width: 550px; } } /* Make the playground preset outline buttons use the link colour */ .playground-presets .btn-outline-primary { color: rgb(var(--bs-link-color-rgb)); border-color: rgb(var(--bs-link-color-rgb)); } .playground-presets .btn-outline-primary:hover, .playground-presets .btn-outline-primary:focus { background-color: rgba(var(--bs-link-color-rgb), 0.1); border-color: rgb(var(--bs-link-color-rgb)); } } ================================================ FILE: site/src/css/large_flags_overrides.scss ================================================ :root { --iti-spacer-horizontal: 12px; --iti-globe-height: 22px; --iti-search-clear-icon-height: 16px; --iti-arrow-height: 5px; --iti-arrow-padding: 9px; --iti-path-flags-1x: url("<%= cacheBust('/img/largeFlags.webp') %>"); --iti-path-flags-2x: url("<%= cacheBust('/img/largeFlags@2x.webp') %>"); } .iti { font-size: 20px; } .iti, .section.demo input { width: 350px !important; } .section.demo input, .iti__search-input { font-size: inherit !important; // bootstrap override } .iti__search-icon { left: 13px; } .iti__search-clear { right: 8px; } input.iti__search-input { padding-left: 44px; padding-right: 40px; } .iti--inline-dropdown .iti__country-list { max-height: 210px; } ================================================ FILE: site/src/css/playground.scss ================================================ @use 'variables'; /************** * PLAYGROUND **************/ // When navigating to a Playground heading via hash, leave space above it. .iti-playground h2[id] { scroll-margin-top: 30px; } // Make enable span behave like a label (default cursor) .form-check-enable-span { cursor: default; } // force these buttons to have the same colour as text-muted, which has a high enough contrast ratio to pass a11y guidelines. .playground-action-btn { color: var(--bs-secondary-color) !important; } .playground-action-btn:hover { background-color: #e3e1e1; @media (prefers-color-scheme: dark) { background-color: #363535; } } /* multidropdown toggle: disable hover styling when closed */ .iti-playground-multidropdown-toggle { cursor: default; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; --bs-btn-bg: var(--bs-body-bg); --bs-btn-color: var(--bs-body-color); --bs-btn-border-color: var(--bs-border-color, #ced4da); --bs-btn-hover-bg: var(--bs-btn-bg); --bs-btn-hover-color: var(--bs-btn-color); --bs-btn-hover-border-color: var(--bs-btn-border-color); --bs-btn-active-bg: var(--bs-btn-bg); --bs-btn-active-color: var(--bs-btn-color); --bs-btn-active-border-color: var(--bs-btn-border-color); /* multidropdown toggle: use CSS chevron for down arrow */ &.dropdown-toggle::after { opacity: 0.85; margin: 0 3px 2px auto; display: inline-block; width: 8px; height: 8px; content: ""; border: solid currentColor; border-width: 0 2px 2px 0; transform: rotate(45deg); } /* multidropdown toggle: flip down arrow when open */ &.dropdown-toggle.show::after, .dropdown.show > &.dropdown-toggle::after { margin-top: 6px; transform: rotate(225deg); } } // mobile: make the presets select smaller so it doesn't distract from the page title @media (max-width: 576px) { .playground-presets-select { width: 7em; } } .iti-playground-layout { display: flex; flex-direction: column; gap: 1.5rem; /* bootstrap g-4 */ /* On mobile, flatten the column wrappers so we can control ordering. */ &__col { display: contents; } &__demo { order: 1; } &__options { order: 2; } &__attrs { order: 3; } &__code { order: 4; } &__demo, &__options, &__attrs, &__code { min-width: 0; } @media (min-width: 992px) { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 1.5rem; /* bootstrap g-4 */ align-items: start; &__col { display: flex; flex-direction: column; gap: 1.5rem; /* bootstrap g-4 */ min-width: 0; } &__col--left { grid-column: 1; } &__col--right { grid-column: 2; position: sticky; top: calc(70px + 1rem); max-height: calc(100vh - 70px - 2rem); overflow: auto; } } &__demo { .iti, input[type="tel"], .iti-live-results { width: 100%; max-width: 350px; } } } .iti-playground--keep-dropdown-open { .iti__dropdown-content.iti__hide { display: block !important; } .playground-dropdown-checkbox { margin-top: 240px !important; } } /* playground: option info tooltip */ .iti-playground-info { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; margin-left: 0; color: var(--bs-secondary-color, #6c757d); user-select: none; cursor: help; > svg { display: block; width: 16px; height: 16px; } &:focus-visible { outline: 2px solid rgb(var(--header-bg-color-rgb)); outline-offset: 2px; } } .iti-playground-labelgroup { padding-top: 3px; display: inline-flex; align-items: center; gap: 0.4rem; > .form-label { margin-bottom: 0; } } .iti-playground-example-toggle { margin-top: 6px; } /* preserve bootstrap-like spacing for stacked (mobile) label + control */ .iti-playground-control { &:not(.iti-playground-control--check) > .iti-playground-labelgroup { margin-bottom: var(--bs-form-label-margin-bottom, 0.5rem); } /* playground: on wider screens, align label + control horizontally to save space */ @media (min-width: 600px) { /* playground: loadUtils has an example snippet + a checkbox, so keep the example aligned with the label, and put the checkbox beneath. */ &--example-code { align-items: start; } &--example-code > .iti-playground-example-toggle { grid-column: 2; margin-top: 0; } /* Shared grid layout for controls on wider screens */ & { display: grid; grid-template-columns: minmax(160px, 220px) 1fr; grid-template-areas: "label control"; column-gap: 12px; > .iti-playground-labelgroup { grid-area: label; margin-bottom: 0; } } /* Non-checkbox controls: slightly more vertical spacing and form-specific tweaks */ &:not(.iti-playground-control--check) { row-gap: 6px; align-items: start; > .form-control, > .form-select, > .dropdown { min-width: 0; } > .form-text { grid-column: 2; } } /* On mobile, checkboxes appear on LHS. Here, we move them to the RHS. */ &.iti-playground-control--check { row-gap: 0; align-items: center; padding-left: 0; /* override bootstrap .form-check */ > .form-check-input { grid-area: control; margin-left: 0; float: none; justify-self: start; } } } } /* contextual hint (shown after toggling certain options) */ .iti-playground-hint { display: block; margin-left: -1.5em; animation: iti-hint-fade-in 0.2s ease-out; @media (min-width: 600px) { grid-column: 2; margin-left: 0; } } @keyframes iti-hint-fade-in { from { opacity: 0; } to { opacity: 1; } } .iti-playground pre { display: block; } ================================================ FILE: site/src/css/website.scss ================================================ /* Split into partials for maintainability. Partial files live alongside this file. Keep this file as the single entrypoint imported by the build system. */ @use 'variables'; @use 'base'; @use 'navbar'; @use 'layout'; @use 'forms'; /* Any small remaining overrides or page-specific rules can go here. */ ================================================ FILE: site/src/docs/docs_content_template.html.ejs ================================================

<%= html %>
================================================ FILE: site/src/docs/docs_nav_template.html.ejs ================================================
Docs
================================================ FILE: site/src/docs/docs_page_template.html.ejs ================================================ <%= common_meta_tags %> <%= head_title %> <%= og_meta_tags %> <%= common_styles %> <%= common_head_end_prod %> <%= layout %> <%= common_body_end %> ================================================ FILE: site/src/docs/markdown/accessibility.md ================================================ # Accessibility intl-tel-input aims to be accessible out of the box, but good accessibility also depends on how you integrate it into your form. This page covers: - How the country dropdown and search are exposed to assistive tech - Keyboard interaction - What you should do to ensure the phone input has an accessible name and helpful errors - How to translate the plugin’s accessibility strings ## Contents - [Accessible naming](#accessible-naming) - [Keyboard support](#keyboard-support) - [Screen reader support](#screen-reader-support) - [Translating accessibility strings](#translating-accessibility-strings) - [Form validation and errors](#form-validation-and-errors) ## Accessible naming Make sure the telephone input has an accessible name. Recommended: - Use a visible `