[Panel] Tabs-field implementation discussion

Hello,

I like to edit my content with a sticky sidebar, but when there’s too much fields within, the sidebar becomes bigger than the available vertical space and the stick behaviour needs a lot of scrolling to access some fields.
So I thought about adding tabs, to reduce the vertical space. In occurence, I have a ‘meta’ and ‘files’ tab in my sidebar.

As there’s no need to store content, I first tried making a panel section, as recommended by the extensions reference. But by analysing some of the panel code, I came to realise that I could easily re-use the when conditional field and section blueprint features by making it a field with a value.
So I came up with a quite simple solution that wraps the new (k3.5, yet undocumented?) k-tabs ui component in a panel field, using the active tab as field value.

I’m a aware that this is quite a hacky use of the panel API, but it works great with very few code.
Here’s the code I’m using :

// index.js
panel.plugin('anonimousse/tabsfield', {
fields: {
    tabs: {
      data() {
        return {
          // Set initial tab and load it
          tabsData: [],
          tab: null,
          //disabled: true, // Disables saving the value (edit: not needed)
        };
      },

      // Blueprint values
      props: {
        tabs: Array, // tabs property from blueprint file
      },

      created: function(){
        this.tabsData = [];
        // Strip keys from blueprint data, and sanitize
        Object.keys(this.tabs).forEach( key => {
          this.tabsData.push( this.tabs[key] );
          this.tabsData[this.tabsData.length-1]['columns']=[];
        });

        // Set tab by priority : tab set by k-tabs, tab in hash, first tab
        let defaultTab = this.tab ?? this.$route.hash.replace('#','');
        if( !defaultTab && this.tabsData.length > 0 ) defaultTab = this.tabsData[0].name;
        this.setTab( defaultTab );
      },

      watch: {
        '$route'() {
          // Set tab to new route
          this.setTab( this.$route.hash.replace('#','') ?? this.tabsData[0].name );
        },
      },

      methods: {
        setTab(newTab){
          // Only watch known tabs (ignores unknown tabs)
          for (let i = 0; i < this.tabsData.length; i++){
            if( this.tabsData[i].name == newTab ){
              this.tab = newTab;
              // Sends the value to the kirby store and registers the field virtual new value
              this.$emit("input", newTab);
              // Dirty: replace original value with changed value to fool kirby's changes indicator
              this.$store.getters["content/model"]('pages/test/fr').originals.sidebarview = newTab;
              break;
            }
          };
        },
      },

      template: `<div><k-tabs theme="notice" :tabs="tabsData" :tab="tab" /></div>`
    }
  }
});
// index.php
Kirby::plugin('anonimousse/tabsfield', [
  'fields' => [
        'tabs' => [
            'props' => [
                'value' => null, // Null values are not saved in content folder, seemingly
                //'disabled'=>true, // Edit: not needed
            ],
            'computed' => [],
        ],
    ],
// Blueprint - Sidebar sections
// [...]
sections: 
  sidebarswitch:
    type: fields
    fields:
      sidebarview:
        type: tabs
      tabs:
        metainfo:
          name: meta
          icon: tag
          label: Info
        files:
          name: files
          icon: file
          label: Files
  sidebarfields:
    type: fields
    when:
      sidebarview: meta
    fields:
      author:
        type: text  
  sidebarfiles:
        type: files
        when:
          sidebarview: files
// [...]

Here’s a screenshot of the result:

Now, everything works as expected, but if you’ve read my comments in the code, there are some tricky workarounds. For learning purposes, I’m wondering if there are better ways to achieve this ?
Particularly how to prevent a field from being written to the content file, and the originals being synced with values to prevent the “changes indicator” in the panel.

Kirby’s fields have a save method, you can see this in some fields set to false, for example the info or line or headline fields:

Hey Sonja, Thanks for your reply.
I’ve just tried that option, but it disables the field’s value in the panel (no key in originals and values), so then the when selectors don’t work anymore.
The Vue part doesn’t seem to react to such a component option nor prop.
Edit: It sets the component’s this.$attrs.saveable to false, but trying to bypass PHP, I found no way in vue to set this variable to false.

Meanwhile, I found out that my php props are useless, as my code is not setting this.value anymore, saving the value never happens anyways.
Edit: Wrong. I’ve edited the initial code to comment useless lines.

So just this.$emit("input", newTab); is enough, with the $store originals value syncing to remove the changes warning.
Setting props.disabled=false doesn’t seem to affect the change indicator either, neither does it prevent saving.

Is there a list of all available vue variables/methods within components, including inheritance ?

Over time I’ve found better ways to implement a tabs field and made it a plugin :

1 Like

A little related: if you need a re-useable k-tabs component, here’s a small change that you can make so that they can be clicked without link which triggers a page reload.

// Register anywhere as Vue component (`myplugin/index.js ['components']` for example)
// ...
'k-tabs-fixed' : {
  extends: 'k-tabs',
  mounted(){
    const thisRef = this;
    for(const child of this.$children){
      if(child.$el.classList.contains('k-tab-button')){
        child.$el.addEventListener('click', function(){ thisRef.onClick(child.$vnode.key); } );
      }
    }
  },
  emits: ['tabChange'],
  methods: {
    onClick(tabKey){
      if(this.tab !== tabKey){
        this.$emit('tabChange', tabKey);
      }
    }
  },
}
// Example Template pseudo-code
// data.curTab = 'defaultTab';
<k-tabs-fixed tabs="[...]" tab="curTab" @changeTab="(newTab)=>curTab=newTab"/>