K-panel styling in custom vue.js (custom plugin)

Hi,

I’ve created a custom field-picker plugin. I’d like to stay as close to kirby cms native styling as possible so I’d like to use the k-table kind of styling in the template section of my vue.js to present my fields in a table.

Seemingly just adding “k-table” like syntax isn’t enough to get the table styled as a default kirby table. Since whenever I wrap my table in

the sole table dissapears… it is however present in the page source… when I remove the k-table class the table show up again.

Did I forget something?

My current template part in my vue.js:

<template>
  <k-field :label="label" :help="help" :disabled="disabled" :required="required">
    <div v-if="fields.length > 0" class="field-selector-simple">
      <div class="k-table">
      <table>
        <thead>
          <tr>
            <th></th>
            <th data-align="left">Field</th>
            <th data-align="left">Type</th>
            <th data-align="left">Party</th>
            <th data-align="left">Category</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="field in fields" :key="field.id">
            <td>
              <k-button @click="addField(field)"
                :title="`Add ${field.label || field.title} to view`" icon="angle-left">
              </k-button>
              
            </td>
            <td>
              <strong>{{ field.label || field.title }}</strong>
            </td>
            <td>
              <span>
                {{ field.type }}
              </span>
            </td>
            <td>
              <span :style="`background: ${field.party === 1 ? '#3498db' : '#27ae60'}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 12px;`">
                {{ field.party_label }}
              </span>
            </td>
            <td>
              {{ field.category }}
            </td>
          </tr>
        </tbody>
        </table>
      </div>
    </div>
    
    <div v-else-if="!loading" style="text-align: center; padding: 2rem; color: #666; font-style: italic;">
      <p>Select a relationship schema to see available fields</p>
    </div>
    
    <div v-if="loading" style="text-align: center; padding: 2rem; color: #666;">
      <p>Loading fields...</p>
    </div>
  </k-field>
</template>

If the table is present in the source, you should be able to inspect the styling in dev tools to see why it is hidden?

Thanks Sonja,

Did that again and found out that with the k-table also a k-grid css class is “forced” in the panel layout… at least i do not include a .k-grid class manually in my vue.js template part…

When toggling display: grid to “off” the k-table appears but… in one column layout such that all components are stacked vertically.

Any Idea how to circumvent this .k-grid behavior?

.k-grid {
    --columns: 12;
    --grid-inline-gap: 0;
    --grid-block-gap: 0;
    /* display: grid; */
    align-items: start;
    grid-column-gap: var(--grid-inline-gap);
    grid-row-gap: var(--grid-block-gap);
}

I’ll experiment some more with my yml layout.

thanks in advance

I guess that happens because the table is wrapped inside the k-field component?

Thanks Sonja,

That’s probably the case I’ve tried some things like basing the custom plugin on another component like section and re-define css classes in the vue styling block but as soon as I use k-table the display: grid property gets inherited and the table visually vanishes in the panel while still existing. I’ve managed to remove the k-table object and replace it with vanilla html table with kirby-look-alike css. For me this is currently acceptable.

To be complete my full vue.js file below.

Thanks for your support Sonja.

<template>
  <k-field :label="label" :help="help" :disabled="disabled" :required="required">
    <div v-if="fields.length > 0" class="field-selector-simple">
      <div class="kirby-native-table">
        <table>
          <thead>
            <tr>
              <th></th>
              <th data-align="left">Field</th>
              <th data-align="left">Type</th>
              <th data-align="left">Party</th>
              <th data-align="left">Category</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="field in fields" :key="field.id">
              <td>
                <k-button @click="addField(field)"
                  :title="`Add ${field.label || field.title} to view`" icon="angle-left">
                </k-button>
              </td>
              <td>
                <strong>{{ field.label || field.title }}</strong>
              </td>
              <td>
                <span>
                  {{ field.type }}
                </span>
              </td>
              <td>
                <span :style="`background: ${field.party === 1 ? '#3498db' : '#27ae60'}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 12px;`">
                  {{ field.party_label }}
                </span>
              </td>
              <td>
                {{ field.category }}
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    
    <div v-else-if="!loading" style="text-align: center; padding: 2rem; color: #666; font-style: italic;">
      <p>Select a relationship schema to see available fields</p>
    </div>
    
    <div v-if="loading" style="text-align: center; padding: 2rem; color: #666;">
      <p>Loading fields...</p>
    </div>
  </k-field>
</template>

<script>
export default {
  props: {
    label: String,
    help: String,
    disabled: Boolean,
    required: Boolean,
    value: [String, Array, Object],
    relationship: {
      type: String,
      default: ''
    },
    filter: {
      type: String,
      default: 'relation_fields'
    },
    search: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      fields: [],
      loading: false,
      lastKnownRelationship: null,
      relationshipCheckInterval: null
    };
  },
  created() {
    this.loadFields();
  },
  methods: {
    async getRelationshipFromPage() {
      try {
        // Method 1: Check direct parent
        if (this.$parent?.value?.relationship_reference) {
          const parentValue = this.$parent.value.relationship_reference;
          let pageId = null;
          if (Array.isArray(parentValue) && parentValue.length > 0) {
            const firstItem = parentValue[0];
            try {
              pageId = firstItem.id || firstItem.uuid || firstItem.link || firstItem.url || firstItem;
            } catch (error) {
              pageId = firstItem;
            }
          } else if (typeof parentValue === 'object' && parentValue !== null) {
            try {
              pageId = parentValue.id || parentValue.uuid || parentValue.link || parentValue.url || parentValue;
            } catch (error) {
              pageId = parentValue;
            }
          } else {
            pageId = parentValue;
          }
          return pageId;
        }

        // Method 2: Traverse component tree to find form
        let current = this.$parent;
        while (current) {
          if (current.value && current.value.relationship_reference) {
            const relationshipValue = current.value.relationship_reference;
            let pageId = null;
            if (Array.isArray(relationshipValue) && relationshipValue.length > 0) {
              const firstItem = relationshipValue[0];
              try {
                pageId = firstItem.id || firstItem.uuid || firstItem.link || firstItem.url || firstItem;
              } catch (error) {
                pageId = firstItem;
              }
            } else if (typeof relationshipValue === 'object' && relationshipValue !== null) {
              try {
                pageId = relationshipValue.id || relationshipValue.uuid || relationshipValue.link || relationshipValue.url || relationshipValue;
              } catch (error) {
                pageId = relationshipValue;
              }
            } else {
              pageId = relationshipValue;
            }
            return pageId;
          }
          current = current.$parent;
        }

        // Method 3: Try to access form data through $refs or global state
        if (this.$root && this.$root.$children) {
          const formComponent = this.$root.$children.find(child => child.value && child.value.relationship_reference);
          if (formComponent) {
            const relationshipValue = formComponent.value.relationship_reference;
            let pageId = null;
            if (Array.isArray(relationshipValue) && relationshipValue.length > 0) {
              const firstItem = relationshipValue[0];
              try {
                pageId = firstItem.id || firstItem.uuid || firstItem.link || firstItem.url || firstItem;
              } catch (error) {
                pageId = firstItem;
              }
            } else if (typeof relationshipValue === 'object' && relationshipValue !== null) {
              try {
                pageId = relationshipValue.id || relationshipValue.uuid || relationshipValue.link || relationshipValue.url || relationshipValue;
              } catch (error) {
                pageId = relationshipValue;
              }
            } else {
              pageId = relationshipValue;
            }
            return pageId;
          }
        }

      } catch (error) {
        // Silent error handling
      }
      
      return null;
    },

    async loadFields() {
      this.loading = true;
      try {
        // Get relationship from prop or try to get it dynamically
        let relationshipId = this.relationship || await this.getRelationshipFromPage();
        
        if (!relationshipId) {
          this.fields = [];
          this.loading = false;
          return;
        }

        const filter = this.filter;
        const apiUrl = `view-builder/field-library/${encodeURIComponent(relationshipId)}/${encodeURIComponent(filter)}`;

        // Call the API to get field data
        const response = await this.$api.get(apiUrl);

        if (response && response.fields) {
          this.fields = response.fields;
        }
      } catch (error) {
        this.fields = [];
      } finally {
        this.loading = false;
      }
    },

    addField(field) {
      // Get current form data from parent
      let current = this.$parent;
      let formData = null;

      while (current && !formData) {
        if (current.value) {
          formData = current.value;
          break;
        }
        current = current.$parent;
      }

      if (!formData) {
        return;
      }

      // Determine which structure field to update based on current context
      const currentContext = formData.current_view_context || 'party_1';
      const targetField = currentContext === 'party_1' ? 'party_1_view_fields' : 'party_2_view_fields';

      // Get current structure value
      const currentStructure = formData[targetField] || [];

      // Create new field entry
      const newFieldEntry = {
        field_reference: field.id,
        field_label: field.title,
        field_type: field.type
      };

      // Add to structure
      const updatedStructure = [...currentStructure, newFieldEntry];

      // Trigger form update through the parent component
      if (current && current.$emit) {
        current.$emit('input', {
          ...formData,
          [targetField]: updatedStructure
        });
      }
    },

    async checkForRelationshipChanges() {
      const currentRelationship = await this.getRelationshipFromPage();
      if (currentRelationship && currentRelationship !== this.lastKnownRelationship) {
        this.lastKnownRelationship = currentRelationship;
        this.loadFields();
      }
    }
  },
  watch: {
    relationship: {
      handler() {
        this.loadFields();
      }
    },
    filter: {
      handler() {
        this.loadFields();
      }
    }
  },
  mounted() {
    // Set up polling to check for relationship changes
    this.relationshipCheckInterval = setInterval(() => {
      this.checkForRelationshipChanges();
    }, 1000);
  },
  beforeDestroy() {
    if (this.relationshipCheckInterval) {
      clearInterval(this.relationshipCheckInterval);
    }
  },
}
</script>

<style scoped>
/* Scoped styles to prevent leakage */
.field-selector-simple {
  width: 100%;
  overflow: visible;
}

.kirby-native-table {
  border: 1px solid var(--color-border);
  border-radius: var(--rounded);
  overflow-x: auto;
  overflow-y: hidden;
  background: var(--color-white);
  width: 100%;
  max-width: 100%;
  display: block;
  scroll-behavior: smooth;
  scrollbar-width: thin;
  scrollbar-color: var(--color-gray-400) var(--color-gray-100);
  position: relative;
}

.kirby-native-table::-webkit-scrollbar {
  height: 8px;
}

.kirby-native-table::-webkit-scrollbar-track {
  background: var(--color-gray-100);
  border-radius: 4px;
}

.kirby-native-table::-webkit-scrollbar-thumb {
  background: var(--color-gray-400);
  border-radius: 4px;
}

.kirby-native-table::-webkit-scrollbar-thumb:hover {
  background: var(--color-gray-500);
}

.kirby-native-table table {
  min-width: 700px;
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
  font-size: var(--text-sm);
  background: transparent;
  table-layout: auto; /* Changed from fixed to allow dynamic column shifting */
}

.kirby-native-table th {
  background: var(--color-gray-100);
  border-bottom: 1px solid var(--color-border);
  padding: var(--spacing-1) 0;
  text-align: left;
  font-weight: var(--font-medium);
  color: var(--color-gray-700);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
  line-height: 1.2;
  vertical-align: middle; /* Ensure consistent vertical alignment */
}

.kirby-native-table td {
  padding: var(--spacing-1) 0;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle; /* Ensure consistent vertical alignment */
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.2;
  font-size: 0.8125rem;
  box-sizing: border-box;
}

.kirby-native-table td:first-child {
  text-overflow: clip;
  white-space: normal;
  overflow: visible;
}

.kirby-native-table td:nth-child(2) {
  max-width: 100px; /* Constrain to ~16 characters */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Removed display: block to maintain table-cell behavior */
}

.kirby-native-table th:nth-child(2) {
  max-width: 100px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Removed display: block to maintain table-cell behavior */
}

.kirby-native-table td::before,
.kirby-native-table td::after {
  content: none;
}

.kirby-native-table tr:last-child td {
  border-bottom: none;
}

.kirby-native-table tr:hover {
  background: var(--color-gray-50);
}

/* Column sizing */
.kirby-native-table th:first-child,
.kirby-native-table td:first-child {
  width: 2rem;
  text-align: center;
}

.kirby-native-table th:nth-child(2),
.kirby-native-table td:nth-child(2) {
  max-width: 30px; /* Constrain width */
  min-width: 0; /* Allow shrinking */
}

.kirby-native-table th:nth-child(3),
.kirby-native-table td:nth-child(3) {
  width: 10%;
  min-width: 0; /* Allow flexible resizing */
}

.kirby-native-table th:nth-child(4),
.kirby-native-table td:nth-child(4) {
  width: 12%;
  min-width: 0; /* Allow flexible resizing */
}

.kirby-native-table th:nth-child(5),
.kirby-native-table td:nth-child(5) {
  width: auto; /* Allow to take remaining space */
  min-width: 0;
}

/* Dark mode support */
@media (prefers-color-scheme: dark) {
  .kirby-native-table {
    background: var(--color-back);
    border-color: var(--color-border);
  }
  
  .kirby-native-table th {
    background: var(--color-gray-900);
    color: var(--color-gray-400);
  }
  
  .kirby-native-table tr:hover {
    background: var(--color-gray-800);
  }
}

.k-table-text {
  color: var(--color-gray-700);
  font-size: 0.875rem;
}
</style>