├── dist
├── mix-manifest.json
└── js
│ └── field.js
├── .github
├── FUNDING.yml
└── workflows
│ └── update-assets.yml
├── screenshots
├── screenshot-1.png
└── nova-phone-number-input-social-image.png
├── mix-manifest.json
├── .gitignore
├── resources
└── js
│ ├── field.js
│ └── components
│ ├── DetailField.vue
│ ├── IndexField.vue
│ └── FormField.vue
├── webpack.mix.js
├── package.json
├── nova.mix.js
├── src
├── FieldServiceProvider.php
└── PhoneNumber.php
├── LICENSE
├── composer.json
└── README.md
/dist/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/js/field.js": "/js/field.js"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ dniccum ]
4 |
--------------------------------------------------------------------------------
/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dniccum/nova-phone-number/HEAD/screenshots/screenshot-1.png
--------------------------------------------------------------------------------
/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/dist/js/field.js": "/dist/js/field.js",
3 | "/dist/css/field.css": "/dist/css/field.css"
4 | }
5 |
--------------------------------------------------------------------------------
/screenshots/nova-phone-number-input-social-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dniccum/nova-phone-number/HEAD/screenshots/nova-phone-number-input-social-image.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /vendor
3 | /node_modules
4 | package-lock.json
5 | composer.phar
6 | composer.lock
7 | phpunit.xml
8 | .phpunit.result.cache
9 | .DS_Store
10 | Thumbs.db
11 | auth.json
--------------------------------------------------------------------------------
/resources/js/field.js:
--------------------------------------------------------------------------------
1 | import Maska from 'maska'
2 |
3 | import IndexField from './components/IndexField'
4 | import DetailField from './components/DetailField'
5 | import FormField from './components/FormField'
6 |
7 | Nova.booting((app, router) => {
8 | app.use(Maska);
9 |
10 | app.component('index-phone-number', IndexField);
11 | app.component('detail-phone-number', DetailField);
12 | app.component('form-phone-number', FormField);
13 | })
14 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | let mix = require('laravel-mix')
2 |
3 | require('./nova.mix')
4 |
5 | // NOTE: stop .LICENSE FILES
6 | // REF: https://github.com/laravel-mix/laravel-mix/issues/2738
7 | mix.options({
8 | terser: {
9 | extractComments: false,
10 | terserOptions: {
11 | output: {
12 | comments: false,
13 | },
14 | },
15 | },
16 | });
17 |
18 | mix
19 | .setPublicPath('dist')
20 | .js('resources/js/field.js', 'js')
21 | .vue({ version: 3 })
22 | .nova('dniccum/phone-number')
23 |
--------------------------------------------------------------------------------
/resources/js/components/DetailField.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
--------------------------------------------------------------------------------
/resources/js/components/IndexField.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "npm run development",
5 | "development": "mix",
6 | "watch": "mix watch",
7 | "watch-poll": "mix watch -- --watch-options-poll=1000",
8 | "hot": "mix watch --hot",
9 | "prod": "npm run production",
10 | "production": "mix --production"
11 | },
12 | "devDependencies": {
13 | "@vue/compiler-sfc": "^3.2.22",
14 | "laravel-mix": "^6.0",
15 | "vue-loader": "^16.8.3"
16 | },
17 | "dependencies": {
18 | "maska": "^1.5.0",
19 | "vue": "^3.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/nova.mix.js:
--------------------------------------------------------------------------------
1 | const mix = require('laravel-mix')
2 | const path = require('path')
3 |
4 | class NovaExtension {
5 | name() {
6 | return 'nova-extension'
7 | }
8 |
9 | register(name) {
10 | this.name = name
11 | }
12 |
13 | webpackConfig(webpackConfig) {
14 | webpackConfig.externals = {
15 | vue: 'Vue',
16 | 'laravel-nova': 'LaravelNova'
17 | }
18 |
19 | // webpackConfig.resolve.alias = {
20 | // ...(webpackConfig.resolve.alias || {}),
21 | // 'laravel-nova': path.join(
22 | // __dirname,
23 | // 'vendor/laravel/nova/resources/js/mixins/packages.js'
24 | // ),
25 | // }
26 |
27 | webpackConfig.output = {
28 | uniqueName: this.name,
29 | }
30 | }
31 | }
32 |
33 | mix.extend('nova', new NovaExtension())
34 |
--------------------------------------------------------------------------------
/.github/workflows/update-assets.yml:
--------------------------------------------------------------------------------
1 | name: "Update Assets"
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [master]
7 | pull_request:
8 |
9 | jobs:
10 | update:
11 | name: Update assets
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v3
18 |
19 | - name: Setup Node 16
20 | uses: actions/setup-node@v2
21 | with:
22 | node-version: '16.x'
23 |
24 | - name: Compile Asset
25 | run: |
26 | yarn install
27 | yarn run prod
28 | env:
29 | TAILWIND_MODE: build
30 |
31 | - name: Commit changes
32 | uses: stefanzweifel/git-auto-commit-action@v4
33 | with:
34 | commit_message: Update Assets
35 |
--------------------------------------------------------------------------------
/src/FieldServiceProvider.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
61 |
--------------------------------------------------------------------------------
/src/PhoneNumber.php:
--------------------------------------------------------------------------------
1 | setRules();
29 |
30 | parent::resolve($resource, $attribute);
31 | }
32 |
33 | /**
34 | * Tells the VueJS component what format to implement on the mask
35 | * @param string $newFormat
36 | * @return PhoneNumber
37 | */
38 | public function format(string $newFormat="(###) ###-####")
39 | {
40 | return $this->withMeta([
41 | 'format' => $newFormat
42 | ]);
43 | }
44 |
45 | /**
46 | * Set the placeholder text for the field if supported.
47 | *
48 | * @param string $text
49 | * @return $this
50 | */
51 | public function placeholder($text)
52 | {
53 | $this->placeholder = $text;
54 | $this->withMeta([
55 | 'placeholder' => $text,
56 | 'extraAttributes' => [
57 | 'placeholder' => $text
58 | ]
59 | ]);
60 |
61 | return $this;
62 | }
63 |
64 | /**
65 | * Tells the field to use the defined or default mask as a placeholder
66 | * @param bool $useMaskPlaceholder
67 | * @return PhoneNumber
68 | */
69 | public function useMaskPlaceholder(bool $useMaskPlaceholder=true)
70 | {
71 | return $this->withMeta([
72 | 'useMaskPlaceholder' => $useMaskPlaceholder
73 | ]);
74 | }
75 |
76 | /**
77 | * Tells the field to show as a clickable tel link on the index view
78 | * @param bool $linkOnIndex
79 | * @return PhoneNumber
80 | */
81 | public function linkOnIndex(bool $linkOnIndex=true)
82 | {
83 | return $this->withMeta([
84 | 'linkOnIndex' => $linkOnIndex
85 | ]);
86 | }
87 |
88 | /**
89 | * Tells the field to show as a clickable tel link on the detail view
90 | * @param bool $linkOnDetail
91 | * @return PhoneNumber
92 | */
93 | public function linkOnDetail(bool $linkOnDetail=true)
94 | {
95 | return $this->withMeta([
96 | 'linkOnDetail' => $linkOnDetail
97 | ]);
98 | }
99 |
100 | /**
101 | * Overrides the default US country phone number validation
102 | * @param string $country
103 | * @return PhoneNumber
104 | */
105 | public function country(string $country="US")
106 | {
107 | $this->countriesToValidate = $country;
108 |
109 | return $this->setRules();
110 | }
111 |
112 | /**
113 | * Provides a list of countries to validate against
114 | * @param array $countries
115 | * @return PhoneNumber
116 | */
117 | public function countries(array $countries)
118 | {
119 | $this->countriesToValidate = implode(',', $countries);
120 |
121 | return $this->setRules();
122 | }
123 |
124 | /**
125 | * Tells the plugin to disable any of the field validation
126 | * @param bool $ignore
127 | * @return $this
128 | */
129 | public function disableValidation(bool $ignore=true)
130 | {
131 | $this->ignoreValidation = $ignore;
132 |
133 | $this->withMeta([
134 | 'disableValidation' => $ignore
135 | ]);
136 |
137 | $this->setRules();
138 |
139 | return $this;
140 | }
141 |
142 | /**
143 | * Appends the Phone number validation rules
144 | *
145 | * @param callable|array|string $rules
146 | * @return $this
147 | */
148 | public function rules($rules)
149 | {
150 | parent::rules($rules);
151 |
152 | return $this->setRules();
153 | }
154 |
155 | /**
156 | * Sets the rules for the class
157 | *
158 | * @return void
159 | */
160 | private function setRules()
161 | {
162 | $phoneValidationRules = [];
163 |
164 | if ($this->ignoreValidation === false) {
165 | $phoneValidationRules = ["phone:".$this->countriesToValidate];
166 |
167 | if ($this->nullable) {
168 | array_push($phoneValidationRules, 'nullable');
169 | }
170 | }
171 |
172 | $this->rules = array_merge_recursive(
173 | $phoneValidationRules, $this->rules
174 | );
175 |
176 | return $this;
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://packagist.org/packages/dniccum/phone-number)
4 | [](https://packagist.org/packages/dniccum/phone-number)
5 | [](https://packagist.org/packages/dniccum/phone-number)
6 |
7 | A Laravel Nova field to format using a dynamic input mask and additional phone number validation.
8 |
9 | **NOTE: This field utilizes [Propaganistas / Laravel-Phone package](https://github.com/Propaganistas/Laravel-Phone) for validation.**
10 |
11 | 
12 |
13 | ## Installation
14 |
15 | To install this tool, use the installation code below:
16 |
17 | ```
18 | composer require dniccum/phone-number
19 | ```
20 |
21 | ## Code
22 |
23 | To use the field, add the following code to your Nova resource. As this is a field, all of the default field properties can be applied.
24 |
25 | ```php
26 | use Dniccum\PhoneNumber\PhoneNumber;
27 |
28 | PhoneNumber::make('Phone Number')
29 | ```
30 |
31 | ### Options
32 |
33 | To support multiple types and formats of phone numbers, this field has multiple methods for input masking and validation that are available.
34 |
35 | #### Defaults
36 |
37 | | Method/Options | Default |
38 | |--------------------|-------------------------------|
39 | | format | **string:** '(###) ###-####' |
40 | | placeholder | **string:** '[Name of Field]' |
41 | | useMaskPlaceholder | **boolean:** false |
42 | | country | **string:** 'US' |
43 | | countries | **string[]:** ['US'] |
44 | | disableValidation | **boolean:** false |
45 | | linkOnIndex | **boolean:** false |
46 | | linkOnDetail | **boolean:** false |
47 |
48 | #### format
49 |
50 | ```php
51 | PhoneNumber::make('Phone Number')
52 | ->format('###-###-####')
53 | ```
54 |
55 | **Type:** string
56 |
57 | **Default:** (###) ###-####
58 |
59 | This is the value that the javascript controlling the input mask will use define it's values; and depending the field's configuration the placeholder text. To indicate numbers, use the hash (#) symbol.
60 |
61 | **Note:** Other types of content can be included within this input like an phone extension:
62 |
63 | ```php
64 | PhoneNumber::make('Phone Number')
65 | ->format('###-###-#### ext ####')
66 | ```
67 |
68 | However the built-in phone number validation will **FAIL** as this is technically an invalid phone number. To prevent the validation from failing, turn off the phone number validation like so:
69 |
70 | ```php
71 | PhoneNumber::make('Phone Number')
72 | ->format('###-###-####')
73 | ->disableValidation()
74 | ```
75 |
76 | #### placeholder
77 |
78 | ```php
79 | PhoneNumber::make('Phone Number')
80 | ->placeholder('Personal Home Number')
81 | ```
82 |
83 | **Type:** string
84 |
85 | **Default:** [Name of the Field]
86 |
87 | If you would like to override the default placeholder supplied by Nova, which is the name of field, user a simple string.
88 |
89 | **Note:** If you are telling the input to override the placeholder by using the input's mask with the `useMaskPlaceholder` method, this will not work.
90 |
91 | #### useMaskPlaceholder
92 |
93 | ```php
94 | PhoneNumber::make('Phone Number')
95 | ->useMaskPlaceholder()
96 | ```
97 |
98 | **Type:** boolean
99 |
100 | **Default:** false
101 |
102 | This will tell the field to replace the input's defined placeholder with the input mask from the `->format()` method.
103 |
104 | #### country
105 |
106 | ```php
107 | PhoneNumber::make('Phone Number')
108 | ->country('CA')
109 | ```
110 |
111 | **Type:** string
112 |
113 | **Default:** US
114 |
115 | This tells the field what type of phone number validation to use. To define a type of validation, define a [ISO 3166-1 alpha-2 compliant](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) country code.
116 |
117 | You can only define one country here. If you would like to define more than one, please see the `->countries()` method.
118 |
119 | **NOTE: This field utilizes [Propaganistas / Laravel-Phone package](https://github.com/Propaganistas/Laravel-Phone) for validation.**
120 |
121 | #### countries
122 |
123 | ```php
124 | PhoneNumber::make('Phone Number')
125 | ->countries(['US', 'CA'])
126 | ```
127 |
128 | **Type:** string[]
129 |
130 | **Default:** US
131 |
132 | If you would like to define more than one country to validate against, define string-based array of [ISO 3166-1 alpha-2 compliant](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) country codes.
133 |
134 | **NOTE: This field utilizes [Propaganistas / Laravel-Phone package](https://github.com/Propaganistas/Laravel-Phone) for validation.**
135 |
136 | #### linkOnIndex
137 |
138 | ```php
139 | PhoneNumber::make('Phone Number')
140 | ->linkOnIndex()
141 | ```
142 |
143 | **Type:** boolean
144 |
145 | **Default:** false
146 |
147 | Render's the phone number as a clickable link on the index view.
148 |
149 | #### linkOnDetail
150 |
151 | ```php
152 | PhoneNumber::make('Phone Number')
153 | ->linkOnDetail()
154 | ```
155 |
156 | **Type:** boolean
157 |
158 | **Default:** false
159 |
160 | Render's the phone number as a clickable link on the detail view.
161 |
162 | ## Credits
163 |
164 | * [Doug Niccum](https://github.com/dniccum)
165 | * [Braden Keith](https://github.com/bradenkeith)
166 | * [lintaba](https://github.com/lintaba)
167 | * [Maxim Kot](https://github.com/batFormat)
168 | * [Shawn Heide](https://github.com/shawnheide)
169 |
170 | ## License
171 |
172 | The MIT License (MIT). Please see [License File](./LICENSE.md) for more information.
173 |
--------------------------------------------------------------------------------
/dist/js/field.js:
--------------------------------------------------------------------------------
1 | (()=>{"use strict";var e={262:(e,t)=>{t.A=(e,t)=>{const n=e.__vccOpts||e;for(const[e,r]of t)n[e]=r;return n}}},t={};function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e,t){for(var n=0;n2&&void 0!==arguments[2]?arguments[2]:s,r=!(arguments.length>3&&void 0!==arguments[3])||arguments[3];return u(t).length>1?c(t)(e,t,n,r):p(e,t,n,r)}function u(e){try{return JSON.parse(e)}catch(t){return[e]}}function c(e){var t=u(e).sort((function(e,t){return e.length-t.length}));return function(e,r,a){var o=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],i=t.map((function(t){return p(e,t,a,!1)})),s=i.pop();for(var l in t)if(n(s,t[l],a))return p(e,t[l],a,o);return""};function n(e,t,n){for(var r in n)n[r].escape&&(t=t.replace(new RegExp(r+".{1}","g"),""));return t.split("").filter((function(e){return n[e]&&n[e].pattern})).length>=e.length}}function p(e,t,n){for(var r=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],a=0,o=0,i="",s="";a1&&void 0!==arguments[1]?arguments[1]:{};if(n(this,e),!t)throw new Error("Maska: no element for mask");if(null!=a.preprocessor&&"function"!=typeof a.preprocessor)throw new Error("Maska: preprocessor must be a function");if(a.tokens)for(var o in a.tokens)a.tokens[o]=i({},a.tokens[o]),a.tokens[o].pattern&&v(a.tokens[o].pattern)&&(a.tokens[o].pattern=new RegExp(a.tokens[o].pattern));this._opts={mask:a.mask,tokens:i(i({},s),a.tokens),preprocessor:a.preprocessor},this._el=v(t)?document.querySelectorAll(t):t.length?t:[t],this.inputEvent=function(e){return r.updateValue(e.target,e)},this.init()}var t,a;return t=e,(a=[{key:"init",value:function(){for(var e=this,t=function(t){var n=d(e._el[t]);!e._opts.mask||n.dataset.mask&&n.dataset.mask===e._opts.mask||(n.dataset.mask=e._opts.mask),setTimeout((function(){return e.updateValue(n)}),0),n.dataset.maskInited||(n.dataset.maskInited=!0,n.addEventListener("input",e.inputEvent),n.addEventListener("beforeinput",e.beforeInput))},n=0;n1&&void 0!==arguments[1]?arguments[1]:null,n=document.createEvent("Event");return n.initEvent(e,!0,!0),t&&(n.inputType=t),n}(e,n&&n.inputType||null))}}])&&r(t.prototype,a),e}(),k=(h=new WeakMap,function(e,t){t.value&&(h.has(e)&&!function(e){return!(v(e.value)&&e.value===e.oldValue||Array.isArray(e.value)&&JSON.stringify(e.value)===JSON.stringify(e.oldValue)||e.value&&e.value.mask&&e.oldValue&&e.oldValue.mask&&e.value.mask===e.oldValue.mask)}(t)||h.set(e,new m(e,function(e){var t={};return e.mask?(t.mask=Array.isArray(e.mask)?JSON.stringify(e.mask):e.mask,t.tokens=e.tokens?i({},e.tokens):{},t.preprocessor=e.preprocessor):t.mask=Array.isArray(e)?JSON.stringify(e):e,t}(t.value))))});function y(e){e.directive("maska",k)}"undefined"!=typeof window&&window.Vue&&window.Vue.use&&window.Vue.use(y);const g=y,b=Vue;var w=["innerHTML"],O={key:1},E=["innerHTML"];const x={props:["resourceName","field"],beforeMount:function(){this.field.linkOnIndex&&(this.field.value='').concat(this.field.value,""))}};var V=function n(r){var a=t[r];if(void 0!==a)return a.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}(262);const A=(0,V.A)(x,[["render",function(e,t,n,r,a,o){return this.field.linkOnIndex?((0,b.openBlock)(),(0,b.createElementBlock)("div",{key:0,onClick:t[0]||(t[0]=(0,b.withModifiers)((function(){}),["stop"]))},[(0,b.createElementVNode)("span",{innerHTML:n.field.value},null,8,w)])):((0,b.openBlock)(),(0,b.createElementBlock)("div",O,[(0,b.createElementVNode)("span",{innerHTML:n.field.value},null,8,E)]))}]]);const _={props:["index","resource","resourceName","resourceId","field"],beforeMount:function(){this.field.linkOnDetail&&this.field.value&&(this.field.asHtml=!0,this.field.value='').concat(this.field.value,""))}},j=(0,V.A)(_,[["render",function(e,t,n,r,a,o){var i=(0,b.resolveComponent)("PanelItem");return(0,b.openBlock)(),(0,b.createBlock)(i,{index:n.index,field:n.field},null,8,["index","field"])}]]);var N=["id","placeholder"];const I=LaravelNova;const S={mixins:[I.DependentFormField,I.HandlesValidationErrors],props:["resourceName","resourceId","field"],computed:{placeholder:function(){var e=this.field.useMaskPlaceholder,t=this.field.placeholder;return e?this.mask:t||this.field.name},mask:function(){return this.field.format?this.field.format:"(###) ###-####"}},methods:{setInitialValue:function(){this.value=this.field.value||""},fill:function(e){e.append(this.field.attribute,this.value||"")},handleChange:function(e){this.value=e}}},T=(0,V.A)(S,[["render",function(e,t,n,r,a,o){var i=(0,b.resolveComponent)("DefaultField"),s=(0,b.resolveDirective)("maska");return(0,b.openBlock)(),(0,b.createBlock)(i,{field:e.currentField,errors:e.errors,"show-help-text":e.showHelpText},{field:(0,b.withCtx)((function(){return[(0,b.withDirectives)((0,b.createElementVNode)("input",{id:e.currentField.name,type:"tel",class:(0,b.normalizeClass)(["w-full form-control form-input form-input-bordered",e.errorClasses]),placeholder:o.placeholder,"onUpdate:modelValue":t[0]||(t[0]=function(t){return e.value=t})},null,10,N),[[s,o.mask],[b.vModelText,e.value]])]})),_:1},8,["field","errors","show-help-text"])}]]);Nova.booting((function(e,t){e.use(g),e.component("index-phone-number",A),e.component("detail-phone-number",j),e.component("form-phone-number",T)}))})();
--------------------------------------------------------------------------------