├── .gitignore ├── docs ├── screenshot1.png └── screenshot2.png ├── postcss.config.js ├── dist └── build │ ├── assets │ ├── statamic-tabs-77515875.css │ └── statamic-tabs-5c54fed2.js │ └── manifest.json ├── resources ├── js │ ├── statamic-tabs.js │ └── compontents │ │ └── TabFieldtype.vue └── css │ └── statamic-tabs.css ├── tailwind.config.js ├── package.json ├── vite.config.js ├── src ├── ServiceProvider.php └── Fieldtypes │ └── TabFieldtype.php ├── composer.json ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | vendor 3 | -------------------------------------------------------------------------------- /docs/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eminos/statamic-tabs/HEAD/docs/screenshot1.png -------------------------------------------------------------------------------- /docs/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eminos/statamic-tabs/HEAD/docs/screenshot2.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /dist/build/assets/statamic-tabs-77515875.css: -------------------------------------------------------------------------------- 1 | .super-invisible{visibility:hidden;position:absolute;top:0;left:0;width:0;height:0}.tabpanel>.help-block{margin-top:0!important} 2 | -------------------------------------------------------------------------------- /resources/js/statamic-tabs.js: -------------------------------------------------------------------------------- 1 | import TabFieldtype from './compontents/TabFieldtype.vue'; 2 | 3 | Statamic.booting(() => { 4 | Statamic.$components.register('tab-fieldtype', TabFieldtype); 5 | }); 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prefix: 'tabs-', 3 | content: [ 4 | './resources/**/*.vue', 5 | './resources/**/*.js', 6 | './resources/**/*.css', 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [ 12 | // 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /resources/css/statamic-tabs.css: -------------------------------------------------------------------------------- 1 | /* @import "tailwindcss/components"; 2 | @import "tailwindcss/utilities"; */ 3 | 4 | .super-invisible { 5 | visibility: hidden; 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | width: 0; 10 | height: 0; 11 | } 12 | 13 | .tabpanel > .help-block { 14 | margin-top: 0 !important; 15 | } -------------------------------------------------------------------------------- /dist/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources/css/statamic-tabs.css": { 3 | "file": "assets/statamic-tabs-77515875.css", 4 | "isEntry": true, 5 | "src": "resources/css/statamic-tabs.css" 6 | }, 7 | "resources/js/statamic-tabs.js": { 8 | "file": "assets/statamic-tabs-5c54fed2.js", 9 | "isEntry": true, 10 | "src": "resources/js/statamic-tabs.js" 11 | } 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build" 6 | }, 7 | "devDependencies": { 8 | "@vitejs/plugin-vue2": "^2.2.0", 9 | "autoprefixer": "^10.4.14", 10 | "laravel-vite-plugin": "^0.7.2", 11 | "postcss": "^8.4.23", 12 | "tailwindcss": "^3.3.2", 13 | "uniqid": "^5.4.0", 14 | "vite": "^4.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import vue2 from '@vitejs/plugin-vue2'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | laravel({ 8 | input: [ 9 | 'resources/js/statamic-tabs.js', 10 | 'resources/css/statamic-tabs.css', 11 | ], 12 | hotFile: 'dist/vite.hot', 13 | publicDirectory: 'dist', 14 | }), 15 | vue2(), 16 | ], 17 | }); -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/../dist/vite.hot', 16 | 'publicDirectory' => 'dist', 17 | 'input' => [ 18 | 'resources/js/statamic-tabs.js', 19 | 'resources/css/statamic-tabs.css' 20 | ], 21 | ]; 22 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eminos/statamic-tabs", 3 | "type": "statamic-addon", 4 | "description": "A Statamic addon to be able to group fields into tabs.", 5 | "license": "MIT", 6 | "require": { 7 | "statamic/cms": "^3.0||^4.0||^5.0" 8 | }, 9 | "require-dev": { 10 | "php": "^8.1", 11 | "laravel/framework": "^11.0", 12 | "statamic/cms": "^5.0" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "Eminos\\StatamicTabs\\": "src" 17 | } 18 | }, 19 | "extra": { 20 | "statamic": { 21 | "name": "Statamic Tabs", 22 | "description": "A Statamic addon to be able to group fields into tabs." 23 | }, 24 | "laravel": { 25 | "providers": [ 26 | "Eminos\\StatamicTabs\\ServiceProvider" 27 | ] 28 | } 29 | }, 30 | "config": { 31 | "allow-plugins": { 32 | "pixelfear/composer-dist-plugin": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Emin Jasarevic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Statamic Tabs 2 | 3 | A Statamic addon to add a Tab Fieldtype. Add tabs anywhere you have other fields! 4 | 5 | ![Screenshot of Tabs](docs/screenshot1.png) 6 | 7 | ## Installation 8 | 9 | Install this addon using composer. 10 | 11 | ```cli 12 | composer require eminos/statamic-tabs 13 | ``` 14 | 15 | ## Features 16 | 17 | - Add Tabs anywhere you need them. Entry, side panel, Replicator set, Bard set, Global set, etc... 18 | - Doesn't touch your other field data, ie. the data is not scoped. 19 | - Conditionally show/hide a tab. You add the conditions just as with any other field. 20 | - Optionally add an icon to the tab. 21 | - You can search and pick an Iconify icon (over 150 000 icons!) if you have the [Iconify Addon](https://github.com/eminos/statamic-iconify) installed. 22 | 23 | ## Usage 24 | 25 | You just add a Tab field wherever you want to start a new tab. 26 | 27 | All the fields that comes after it (and that are not an other tab field) will end up in that tab. 28 | 29 | ![Screenshot of the blueprint editing with added tabs](docs/screenshot2.png) 30 | 31 | ## Possible improvements 32 | 33 | - Show if there are validation errors on a field inside a tab 34 | - Nested tabs?! 35 | - Improve accessibility, keyboard navigation etc. 36 | 37 | ## License 38 | 39 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 40 | -------------------------------------------------------------------------------- /src/Fieldtypes/TabFieldtype.php: -------------------------------------------------------------------------------- 1 | '; 10 | 11 | /** 12 | * The blank/default value. 13 | * 14 | * @return array 15 | */ 16 | public function defaultValue() 17 | { 18 | return null; 19 | } 20 | 21 | /** 22 | * Pre-process the data before it gets sent to the publish page. 23 | * 24 | * @param mixed $data 25 | * @return array|mixed 26 | */ 27 | public function preProcess($data) 28 | { 29 | return $data; 30 | } 31 | 32 | /** 33 | * Process the data before it gets saved. 34 | * 35 | * @param mixed $data 36 | * @return array|mixed 37 | */ 38 | public function process($data) 39 | { 40 | return $data; 41 | } 42 | 43 | protected function configFieldItems(): array 44 | { 45 | $configFieldItems = [ 46 | 'tab_icon' => [ 47 | 'display' => 'Tab Icon', 48 | 'instructions' => 'The icon to display for this tab.', 49 | 'type' => 'icon', 50 | 'width' => 50 51 | ], 52 | ]; 53 | 54 | if (class_exists('StatamicIconify\Fieldtypes\IconifyFieldtype')) { 55 | $configFieldItems['tab_iconify_icon'] = [ 56 | 'display' => 'Tab Iconify Icon', 57 | 'instructions' => 'The icon to display for this tab. If set, this will override the "Tab Icon" field.', 58 | 'type' => 'iconify', 59 | 'width' => 50 60 | ]; 61 | } 62 | 63 | return $configFieldItems; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /dist/build/assets/statamic-tabs-5c54fed2.js: -------------------------------------------------------------------------------- 1 | function T(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function w(e){if(e.__esModule)return e;var t=e.default;if(typeof t=="function"){var i=function n(){return this instanceof n?Reflect.construct(t,arguments,this.constructor):t.apply(this,arguments)};i.prototype=t.prototype}else i={};return Object.defineProperty(i,"__esModule",{value:!0}),Object.keys(e).forEach(function(n){var s=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(i,n,s.get?s:{enumerable:!0,get:function(){return e[n]}})}),i}function E(e){throw new Error('Could not dynamically require "'+e+'". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.')}var d={exports:{}};const C={},q=Object.freeze(Object.defineProperty({__proto__:null,default:C},Symbol.toStringTag,{value:"Module"})),k=w(q);var g=typeof process<"u"&&process.pid?process.pid.toString(36):"",_="";if(typeof __webpack_require__!="function"&&typeof E<"u"){var h="",v=k;if(v.networkInterfaces)var p=v.networkInterfaces();if(p){e:for(let e in p){const t=p[e],i=t.length;for(var o=0;ot?e:t+1}var M=d.exports;const O=T(M);function P(e,t,i,n,s,u,f,y){var a=typeof e=="function"?e.options:e;t&&(a.render=t,a.staticRenderFns=i,a._compiled=!0),n&&(a.functional=!0),u&&(a._scopeId="data-v-"+u);var r;if(f?(r=function(l){l=l||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,!l&&typeof __VUE_SSR_CONTEXT__<"u"&&(l=__VUE_SSR_CONTEXT__),s&&s.call(this,l),l&&l._registeredComponents&&l._registeredComponents.add(f)},a._ssrRegister=r):s&&(r=y?function(){s.call(this,(a.functional?this.parent:this).$root.$options.shadowRoot)}:s),r)if(a.functional){a._injectStyles=r;var $=a.render;a.render=function(S,m){return r.call(m),$(S,m)}}else{var b=a.beforeCreate;a.beforeCreate=b?[].concat(b,r):[r]}return{exports:e,options:a}}const R={mixins:[Fieldtype],data(){return{isMainTab:!1,mainTab:null,padding:0,tab:{id:O(),handle:this.config.handle,name:this.config.display,icon:this.config.tab_icon,iconify_icon:this.config.tab_iconify_icon,active:!1,hidden:!1},tabs:[]}},computed:{tabPanelStyle(){return{marginLeft:"-"+this.padding+"px",marginRight:"-"+this.padding+"px"}}},methods:{setTabActive(e){this.tabs.forEach((t,i)=>{t.active=i===e})},checkIfFirstSibling(){let e=this.$el.closest(".publish-field");for(;e.previousElementSibling;){if(e.previousElementSibling.classList.contains("tab-fieldtype"))return!1;e=e.previousElementSibling}return!0},findMainTab(){let e=this.$el.closest(".publish-field");for(;e.previousElementSibling;){if(e.previousElementSibling.classList.contains("main-tab"))return e.previousElementSibling;e=e.previousElementSibling}return null},calculatePadding(){let e=this.$el.querySelector(".tabpanel.block .publish-field");if(e){const i=window.getComputedStyle(e).getPropertyValue("padding-left"),n=parseInt(i,10);this.padding=n}}},mounted(){this.isMainTab=this.checkIfFirstSibling(),this.isMainTab?(this.$el.closest(".publish-field").classList.add("main-tab"),this.$el.closest(".publish-field").dataset.uniqid=this.tab.id,this.mainTab=this.$el.closest(".publish-field")):this.mainTab=this.findMainTab(),this.isMainTab?(this.tab.active=!0,this.tabs.push(this.tab),this.$events.$on("tabs.push-"+this.tab.id,i=>{this.tabs.push(i)})):this.$events.$emit("tabs.push-"+this.mainTab.dataset.uniqid,this.tab),this.$nextTick(()=>{const i=this.$el.closest(".publish-field").querySelector(".help-block");i&&(this.isMainTab?(document.getElementById("tab-content-"+this.tabs[0].id).prepend(i),this.$events.$on("tabs.prepend-instructions-"+this.tab.id,(n,s)=>{document.getElementById("tab-content-"+s).prepend(n)})):this.$events.$emit("tabs.prepend-instructions-"+this.mainTab.dataset.uniqid,i,this.tab.id))});let e=this.$el.closest(".publish-field"),t=[];for(;e.nextElementSibling&&!e.nextElementSibling.classList.contains("tab-fieldtype");)t.push(e.nextElementSibling),e=e.nextElementSibling;this.$nextTick(()=>{this.isMainTab?(t.forEach(i=>{document.getElementById("tab-content-"+this.tabs[0].id).querySelector(".publish-fields").appendChild(i)}),this.$events.$on("tabs.append-"+this.tab.id,(i,n)=>{document.getElementById("tab-content-"+n).querySelector(".publish-fields").appendChild(i)})):t.forEach(i=>{this.$events.$emit("tabs.append-"+this.mainTab.dataset.uniqid,i,this.tab.id)})}),this.isMainTab&&this.$el.closest(".publish-field").querySelector("label").classList.add("super-invisible"),this.isMainTab||this.$el.closest(".publish-field").classList.add("super-invisible"),this.$nextTick(()=>{this.isMainTab&&(this.calculatePadding(),window.addEventListener("resize",i=>{this.calculatePadding()}))}),this.$nextTick(()=>{const i=this.$el.closest(".publish-field");window.getComputedStyle(i).display==="none"?this.tab.hidden=!0:this.tab.hidden=!1})},created(){this.$nextTick(()=>{const e=this.$el.closest(".publish-field");new MutationObserver(i=>{i.forEach(n=>{n.type==="attributes"&&n.attributeName==="style"&&(window.getComputedStyle(e).display==="none"?this.tab.hidden=!0:this.tab.hidden=!1)})}).observe(e,{attributes:!0,attributeFilter:["style"]})})}};var x=function(){var t=this,i=t._self._c;return i("div",[t.isMainTab?i("div",{staticClass:"tabs-container relative"},[i("div",{staticClass:"tabs flex-1 flex space-x-3 overflow-auto pr-6",attrs:{role:"tablist"}},t._l(t.tabs,function(n,s){return i("button",{staticClass:"tab-button",class:{active:n.active,hidden:n.hidden},attrs:{role:"tab"},on:{click:function(u){return t.setTabActive(s)}}},[n.iconify_icon?i("iconify-icon",{staticClass:"h-4 w-4 text-lg mr-2",attrs:{icon:n.iconify_icon}}):n.icon?i("svg-icon",{staticClass:"h-4 w-4 mr-2",attrs:{name:n.icon}}):t._e(),t._v(" "+t._s(t.__(n.name))+" ")],1)}),0)]):t._e(),t._l(t.tabs,function(n,s){return i("div",{staticClass:"tabpanel",class:{block:n.active,hidden:!n.active},attrs:{role:"tabpanel",id:"tab-content-"+n.id}},[i("div",{style:t.tabPanelStyle},[i("div",{staticClass:"publish-fields @container w-full"})])])})],2)},F=[],I=P(R,x,F,!1,null,null,null,null);const j=I.exports;Statamic.booting(()=>{Statamic.$components.register("tab-fieldtype",j)}); 2 | -------------------------------------------------------------------------------- /resources/js/compontents/TabFieldtype.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | --------------------------------------------------------------------------------