├── .gitignore ├── README.md ├── composer.json ├── dist ├── js │ ├── field.js │ └── field.js.LICENSE.txt └── mix-manifest.json ├── docs ├── detail-field-clickable.png ├── detail-field-plain.png ├── form-field.png ├── index-field-clickable.png └── index-field.png ├── nova.mix.js ├── package.json ├── resources └── js │ ├── components │ ├── DetailField.vue │ ├── EmailField.vue │ ├── FormField.vue │ └── IndexField.vue │ └── field.js ├── src ├── Email.php └── EmailFieldServiceProvider.php └── webpack.mix.js /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Nova Email Field 2 | An email input and mailto link field for Laravel Nova. ***Version 2.0 now supports Nova 4.0 and Vue 3.0!*** 3 | 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/inspheric/nova-email-field.svg?style=flat-square)](https://packagist.org/packages/inspheric/nova-email-field) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/inspheric/nova-email-field.svg?style=flat-square)](https://packagist.org/packages/inspheric/nova-email-field) 6 | 7 | ## Installation 8 | 9 | Install the package into a Laravel app that uses [Nova](https://nova.laravel.com) with Composer: 10 | 11 | ```bash 12 | composer require inspheric/nova-email-field 13 | ``` 14 | 15 | ## Usage 16 | 17 | Add the field to your resource in the ```fields``` method: 18 | ```php 19 | use Inspheric\Fields\Email; 20 | 21 | Email::make('Email') 22 | ->rules('email', /* ... */), 23 | ``` 24 | 25 | The field extends the `Laravel\Nova\Fields\Text` field, so all the usual methods are available. 26 | 27 | **Now supports readonly, placeholder and overriding the default `type="email"` if you prefer not to have the validation in the browser. This is from the standard Nova `Text` field so is not documented here.** 28 | 29 | It is recommended that you include the standard `email` validation rule, as it is not automatically added. 30 | 31 | ### Options 32 | #### Clickable 33 | Make the field display as a mailto link on the detail page: 34 | 35 | ```php 36 | Email::make('Email') 37 | ->clickable(), 38 | ``` 39 | 40 | #### Clickable on Index 41 | Make the field display as a mailto link on the index page: 42 | 43 | ```php 44 | Email::make('Email') 45 | ->clickableOnIndex(), 46 | ``` 47 | 48 | #### Always Clickable 49 | Combination of the two functions above for simplicity: 50 | 51 | ```php 52 | Email::make('Email') 53 | ->alwaysClickable(), 54 | ``` 55 | 56 | ## Appearance 57 | ### Index (default) 58 | ![index-field](https://raw.githubusercontent.com/inspheric/nova-email-field/master/docs/index-field.png) 59 | 60 | The field is displayed as a plain `` element. If the field value is blank, an em dash will be displayed. 61 | 62 | ### Index (clickable) 63 | ![index-field-clickable](https://raw.githubusercontent.com/inspheric/nova-email-field/master/docs/index-field-clickable.png) 64 | 65 | The field is displayed as an `` element with an icon. If the field value is blank, an em dash will be displayed instead of a link. 66 | 67 | ### Detail (default) 68 | ![detail-field](https://raw.githubusercontent.com/inspheric/nova-email-field/master/docs/detail-field-plain.png) 69 | 70 | The field is displayed as a plain `` element. If the field value is blank, an em dash will be displayed. 71 | 72 | ### Detail (clickable) 73 | ![detail-field-clickable](https://raw.githubusercontent.com/inspheric/nova-email-field/master/docs/detail-field-clickable.png) 74 | 75 | The field is displayed as an `` element with an icon. If the field value is blank, an em dash will be displayed instead of a link. 76 | 77 | ### Form 78 | ![form-field](https://raw.githubusercontent.com/inspheric/nova-email-field/master/docs/form-field.png) 79 | 80 | The field is displayed as an `` element. 81 | 82 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inspheric/nova-email-field", 3 | "description": "A Laravel Nova email field.", 4 | "keywords": [ 5 | "laravel", 6 | "nova", 7 | "field", 8 | "email", 9 | "mailto" 10 | ], 11 | "license": "MIT", 12 | "require": { 13 | "php": "^8.0", 14 | "laravel/nova": "^4.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Inspheric\\Fields\\": "src/" 19 | } 20 | }, 21 | "extra": { 22 | "laravel": { 23 | "providers": [ 24 | "Inspheric\\Fields\\EmailFieldServiceProvider" 25 | ] 26 | } 27 | }, 28 | "config": { 29 | "sort-packages": true 30 | }, 31 | "minimum-stability": "dev", 32 | "prefer-stable": true 33 | } 34 | -------------------------------------------------------------------------------- /dist/js/field.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see field.js.LICENSE.txt */ 2 | (()=>{var e={3744:(e,t)=>{"use strict";t.Z=(e,t)=>{const r=e.__vccOpts||e;for(const[e,n]of t)r[e]=n;return r}},6649:(e,t,r)=>{function n(e){return e&&"object"==typeof e&&"default"in e?e.default:e}var o=n(r(3950)),i=r(8009),a=n(r(6533));function s(){return(s=Object.assign||function(e){for(var t=1;t0&&"back_forward"===window.performance.getEntriesByType("navigation")[0].type},r.handleBackForwardVisit=function(e){var t=this;window.history.state.version=e.version,this.setPage(window.history.state,{preserveScroll:!0,preserveState:!0}).then((function(){t.restoreScrollPositions(),b(e)}))},r.locationVisit=function(e,t){try{window.sessionStorage.setItem("inertiaLocationVisit",JSON.stringify({preserveScroll:t})),window.location.href=e.href,v(window.location).href===v(e).href&&window.location.reload()}catch(e){return!1}},r.isLocationVisit=function(){try{return null!==window.sessionStorage.getItem("inertiaLocationVisit")}catch(e){return!1}},r.handleLocationVisit=function(e){var t,r,n,o,i=this,a=JSON.parse(window.sessionStorage.getItem("inertiaLocationVisit")||"");window.sessionStorage.removeItem("inertiaLocationVisit"),e.url+=window.location.hash,e.rememberedState=null!=(t=null==(r=window.history.state)?void 0:r.rememberedState)?t:{},e.scrollRegions=null!=(n=null==(o=window.history.state)?void 0:o.scrollRegions)?n:[],this.setPage(e,{preserveScroll:a.preserveScroll,preserveState:!0}).then((function(){a.preserveScroll&&i.restoreScrollPositions(),b(e)}))},r.isLocationVisitResponse=function(e){return e&&409===e.status&&e.headers["x-inertia-location"]},r.isInertiaResponse=function(e){return null==e?void 0:e.headers["x-inertia"]},r.createVisitId=function(){return this.visitId={},this.visitId},r.cancelVisit=function(e,t){var r=t.cancelled,n=void 0!==r&&r,o=t.interrupted,i=void 0!==o&&o;!e||e.completed||e.cancelled||e.interrupted||(e.cancelToken.cancel(),e.onCancel(),e.completed=!1,e.cancelled=n,e.interrupted=i,g(e),e.onFinish(e))},r.finishVisit=function(e){e.cancelled||e.interrupted||(e.completed=!0,e.cancelled=!1,e.interrupted=!1,g(e),e.onFinish(e))},r.resolvePreserveOption=function(e,t){return"function"==typeof e?e(t):"errors"===e?Object.keys(t.props.errors||{}).length>0:e},r.visit=function(e,r){var n=this,i=void 0===r?{}:r,a=i.method,c=void 0===a?t.n$.GET:a,l=i.data,p=void 0===l?{}:l,d=i.replace,g=void 0!==d&&d,b=i.preserveScroll,w=void 0!==b&&b,O=i.preserveState,S=void 0!==O&&O,j=i.only,x=void 0===j?[]:j,_=i.headers,E=void 0===_?{}:_,P=i.errorBag,A=void 0===P?"":P,k=i.forceFormData,F=void 0!==k&&k,T=i.onCancelToken,N=void 0===T?function(){}:T,I=i.onBefore,C=void 0===I?function(){}:I,R=i.onStart,M=void 0===R?function(){}:R,D=i.onProgress,L=void 0===D?function(){}:D,V=i.onFinish,B=void 0===V?function(){}:V,U=i.onCancel,$=void 0===U?function(){}:U,q=i.onSuccess,W=void 0===q?function(){}:q,H=i.onError,z=void 0===H?function(){}:H,G=i.queryStringArrayFormat,J=void 0===G?"brackets":G,Q="string"==typeof e?h(e):e;if(!function e(t){return t instanceof File||t instanceof Blob||t instanceof FileList&&t.length>0||t instanceof FormData&&Array.from(t.values()).some((function(t){return e(t)}))||"object"==typeof t&&null!==t&&Object.values(t).some((function(t){return e(t)}))}(p)&&!F||p instanceof FormData||(p=f(p)),!(p instanceof FormData)){var X=y(c,Q,p,J),K=X[1];Q=h(X[0]),p=K}var Z={url:Q,method:c,data:p,replace:g,preserveScroll:w,preserveState:S,only:x,headers:E,errorBag:A,forceFormData:F,queryStringArrayFormat:J,cancelled:!1,completed:!1,interrupted:!1};if(!1!==C(Z)&&function(e){return m("before",{cancelable:!0,detail:{visit:e}})}(Z)){this.activeVisit&&this.cancelVisit(this.activeVisit,{interrupted:!0}),this.saveScrollPositions();var Y=this.createVisitId();this.activeVisit=s({},Z,{onCancelToken:N,onBefore:C,onStart:M,onProgress:L,onFinish:B,onCancel:$,onSuccess:W,onError:z,queryStringArrayFormat:J,cancelToken:o.CancelToken.source()}),N({cancel:function(){n.activeVisit&&n.cancelVisit(n.activeVisit,{cancelled:!0})}}),function(e){m("start",{detail:{visit:e}})}(Z),M(Z),o({method:c,url:v(Q).href,data:c===t.n$.GET?{}:p,params:c===t.n$.GET?p:{},cancelToken:this.activeVisit.cancelToken.token,headers:s({},E,{Accept:"text/html, application/xhtml+xml","X-Requested-With":"XMLHttpRequest","X-Inertia":!0},x.length?{"X-Inertia-Partial-Component":this.page.component,"X-Inertia-Partial-Data":x.join(",")}:{},A&&A.length?{"X-Inertia-Error-Bag":A}:{},this.page.version?{"X-Inertia-Version":this.page.version}:{}),onUploadProgress:function(e){p instanceof FormData&&(e.percentage=Math.round(e.loaded/e.total*100),function(e){m("progress",{detail:{progress:e}})}(e),L(e))}}).then((function(e){var t;if(!n.isInertiaResponse(e))return Promise.reject({response:e});var r=e.data;x.length&&r.component===n.page.component&&(r.props=s({},n.page.props,r.props)),w=n.resolvePreserveOption(w,r),(S=n.resolvePreserveOption(S,r))&&null!=(t=window.history.state)&&t.rememberedState&&r.component===n.page.component&&(r.rememberedState=window.history.state.rememberedState);var o=Q,i=h(r.url);return o.hash&&!i.hash&&v(o).href===i.href&&(i.hash=o.hash,r.url=i.href),n.setPage(r,{visitId:Y,replace:g,preserveScroll:w,preserveState:S})})).then((function(){var e=n.page.props.errors||{};if(Object.keys(e).length>0){var t=A?e[A]?e[A]:{}:e;return function(e){m("error",{detail:{errors:e}})}(t),z(t)}return m("success",{detail:{page:n.page}}),W(n.page)})).catch((function(e){if(n.isInertiaResponse(e.response))return n.setPage(e.response.data,{visitId:Y});if(n.isLocationVisitResponse(e.response)){var t=h(e.response.headers["x-inertia-location"]),r=Q;r.hash&&!t.hash&&v(r).href===t.href&&(t.hash=r.hash),n.locationVisit(t,!0===w)}else{if(!e.response)return Promise.reject(e);m("invalid",{cancelable:!0,detail:{response:e.response}})&&u.show(e.response.data)}})).then((function(){n.activeVisit&&n.finishVisit(n.activeVisit)})).catch((function(e){if(!o.isCancel(e)){var t=m("exception",{cancelable:!0,detail:{exception:e}});if(n.activeVisit&&n.finishVisit(n.activeVisit),t)return Promise.reject(e)}}))}},r.setPage=function(e,t){var r=this,n=void 0===t?{}:t,o=n.visitId,i=void 0===o?this.createVisitId():o,a=n.replace,s=void 0!==a&&a,c=n.preserveScroll,u=void 0!==c&&c,l=n.preserveState,f=void 0!==l&&l;return Promise.resolve(this.resolveComponent(e.component)).then((function(t){i===r.visitId&&(e.scrollRegions=e.scrollRegions||[],e.rememberedState=e.rememberedState||{},(s=s||h(e.url).href===window.location.href)?r.replaceState(e):r.pushState(e),r.swapComponent({component:t,page:e,preserveState:f}).then((function(){u||r.resetScrollPositions(),s||b(e)})))}))},r.pushState=function(e){this.page=e,window.history.pushState(e,"",e.url)},r.replaceState=function(e){this.page=e,window.history.replaceState(e,"",e.url)},r.handlePopstateEvent=function(e){var t=this;if(null!==e.state){var r=e.state,n=this.createVisitId();Promise.resolve(this.resolveComponent(r.component)).then((function(e){n===t.visitId&&(t.page=r,t.swapComponent({component:e,page:r,preserveState:!1}).then((function(){t.restoreScrollPositions(),b(r)})))}))}else{var o=h(this.page.url);o.hash=window.location.hash,this.replaceState(s({},this.page,{url:o.href})),this.resetScrollPositions()}},r.get=function(e,r,n){return void 0===r&&(r={}),void 0===n&&(n={}),this.visit(e,s({},n,{method:t.n$.GET,data:r}))},r.reload=function(e){return void 0===e&&(e={}),this.visit(window.location.href,s({},e,{preserveScroll:!0,preserveState:!0}))},r.replace=function(e,t){var r;return void 0===t&&(t={}),console.warn("Inertia.replace() has been deprecated and will be removed in a future release. Please use Inertia."+(null!=(r=t.method)?r:"get")+"() instead."),this.visit(e,s({preserveState:!0},t,{replace:!0}))},r.post=function(e,r,n){return void 0===r&&(r={}),void 0===n&&(n={}),this.visit(e,s({preserveState:!0},n,{method:t.n$.POST,data:r}))},r.put=function(e,r,n){return void 0===r&&(r={}),void 0===n&&(n={}),this.visit(e,s({preserveState:!0},n,{method:t.n$.PUT,data:r}))},r.patch=function(e,r,n){return void 0===r&&(r={}),void 0===n&&(n={}),this.visit(e,s({preserveState:!0},n,{method:t.n$.PATCH,data:r}))},r.delete=function(e,r){return void 0===r&&(r={}),this.visit(e,s({preserveState:!0},r,{method:t.n$.DELETE}))},r.remember=function(e,t){var r,n;void 0===t&&(t="default"),w||this.replaceState(s({},this.page,{rememberedState:s({},null==(r=this.page)?void 0:r.rememberedState,(n={},n[t]=e,n))}))},r.restore=function(e){var t,r;if(void 0===e&&(e="default"),!w)return null==(t=window.history.state)||null==(r=t.rememberedState)?void 0:r[e]},r.on=function(e,t){var r=function(e){var r=t(e);e.cancelable&&!e.defaultPrevented&&!1===r&&e.preventDefault()};return document.addEventListener("inertia:"+e,r),function(){return document.removeEventListener("inertia:"+e,r)}},e}(),S={buildDOMElement:function(e){var t=document.createElement("template");t.innerHTML=e;var r=t.content.firstChild;if(!e.startsWith(" 20 | -------------------------------------------------------------------------------- /resources/js/components/EmailField.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 53 | -------------------------------------------------------------------------------- /resources/js/components/FormField.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 44 | -------------------------------------------------------------------------------- /resources/js/components/IndexField.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /resources/js/field.js: -------------------------------------------------------------------------------- 1 | import IndexField from './components/IndexField' 2 | import DetailField from './components/DetailField' 3 | import FormField from './components/FormField' 4 | 5 | Nova.booting((app, store) => { 6 | app.component('index-inspheric-email-field', IndexField) 7 | app.component('detail-inspheric-email-field', DetailField) 8 | app.component('form-inspheric-email-field', FormField) 9 | }) 10 | -------------------------------------------------------------------------------- /src/Email.php: -------------------------------------------------------------------------------- 1 | withMeta(['clickable' => $clickable]); 26 | } 27 | 28 | /** 29 | * Whether the email should be displayed as a clickable 30 | * mailto link on the index page. 31 | * 32 | * @param bool $clickable 33 | * @return $this 34 | */ 35 | public function clickableOnIndex(bool $clickable = true) 36 | { 37 | return $this->withMeta(['clickableOnIndex' => $clickable]); 38 | } 39 | 40 | /** 41 | * Whether the email should be displayed as a clickable 42 | * mailto link on both the index and detail pages. 43 | * 44 | * @param bool $clickable 45 | * @return $this 46 | */ 47 | public function alwaysClickable(bool $clickable = true) 48 | { 49 | return $this->clickable($clickable) 50 | ->clickableOnIndex($clickable); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/EmailFieldServiceProvider.php: -------------------------------------------------------------------------------- 1 |