Have you ever missed a menu field in the Panel? I don't mean a full menu builder, just a simple list of pages for quick navigation. In theory there's the Pages Section for that, but the order there has to follow some logic, and sometimes you just want a manual selection of pages that fits the current UI.
So today we're building our own field for the Panel: a quicklinks field that behaves like a normal pages field but can be locked down until you explicitly want to edit it.
Before we touch any code, three things matter to me:
First, the field should basically behave like a normal pages field, but it can be switched to readonly. Second, a user permission controls whether a role is even allowed to edit the field. And third, the one I care about most: I want to override as little code as possible. The less we touch the original, the smoother future updates stay.
The PHP Side
Let's start with PHP. We create a new plugin and add a field type plus a permission.
<?php
use Kirby\Cms\App;
use Kirby\Cms\App as Kirby;
Kirby::plugin('project/quicklinks', [
'fields' => [
'quicklinks' => [
// We use the existing pages field
'extends' => 'pages',
'props' => [
// We add a new property based on our permissions
'canEdit' => function () {
$user = App::instance()->user();
if (!$user) {
return false;
}
$permissions = $user->role()->permissions();
return $permissions->for('project.quicklinks', 'update');
}
]
]
],
// Here we define the available permissions
'permissions' => [
'update' => true
]
]);
We extend the existing pages field, which means we get all of its behavior for free. Then we add a single new prop, canEdit. It grabs the current user, checks their role's permissions, and asks: is this user allowed to update quicklinks? If there's no user, it's a clear no.
And down in the permissions block we register that very permission, so for every role you can decide whether quicklinks may be edited or not.
That's the PHP side done. Now we need to create the field in JavaScript too, so we actually see something in the Panel.
The JavaScript Side
panel.plugin("project-quicklinks", {
fields: {
quicklinks: {
// Here we extend the existing field as well
extends: "k-pages-field",
// The property from PHP arrives here
props: {
canEdit: Boolean
},
data() {
return {
overwrite: false
}
},
computed: {
disabled() {
return !this.overwrite;
}
},
}
}
});
Same idea as before: we extend the existing k-pages-field instead of rebuilding it. The canEdit prop we defined in PHP comes in right here.
Then we add a little local state, overwrite, which starts as false. We'll compute disabled from it: as long as we haven't switched overwrite on, the field stays disabled. So by default, the field is locked.
Now the field is locked, but we still need a way to unlock it. Let's add a button for that.
panel.plugin("project-quicklinks", {
fields: {
quicklinks: {
extends: "k-pages-field",
props: {
canEdit: Boolean
},
data() {
return {
overwrite: false
}
},
computed: {
disabled() {
return !this.overwrite;
}
},
watch: {
disabled(val) {
if (val !== true) return;
this.$nextTick(() => {
this.removeDisabledVisuals();
});
}
},
methods: {
removeDisabledVisuals() {
const items = this.$el.querySelectorAll("[data-theme='disabled']");
items.forEach((item) => {
item.removeAttribute("data-theme");
});
},
addControls() {
if (!this.canEdit) return;
const button = this.createButton();
const icon = this.createIcon();
button.appendChild(icon);
button.addEventListener('click', () => {
this.overwrite = !this.overwrite;
});
const label = this.$el.querySelector('.k-label');
if (label) {
label.appendChild(button);
} else {
this.$el.appendChild(button);
}
},
createButton() {
const button = document.createElement('button');
const attributes = {
'data-has-icon': true,
'data-has-text': false,
'aria-label': 'Edit',
'data-responsive': true,
'data-size': 'xs',
'data-variant': 'filled',
'type': 'button'
};
Object.entries(attributes).forEach(([key, value]) => {
button.setAttribute(key, value);
});
button.classList.add('k-button');
return button;
},
createIcon() {
const icon = document.createElement('span');
icon.classList.add('k-button-icon');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('k-icon');
svg.setAttribute('aria-hidden', true);
svg.setAttribute('data-type', 'edit');
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', '#icon-edit');
svg.appendChild(use);
icon.appendChild(svg);
return icon;
}
},
mounted() {
this.removeDisabledVisuals()
this.addControls();
}
}
}
});
There's a bit more going on here, so let's walk through it.
addControls is the heart of it. First it checks canEdit, so if the user isn't allowed, no button is ever added. Then it builds a button with an edit icon, and on click it simply flips overwrite. That's the toggle between locked and editable. We try to drop the button into the field's label, and fall back to the field itself if there's no label.
createButton and createIcon are just there to build a button that looks like a native Kirby button. We reuse Kirby's own classes and data attributes, plus the built-in edit icon via #icon-edit, so it blends right in.
Then there's removeDisabledVisuals. When the field is disabled, Kirby marks elements with a disabled theme. For a quick navigation list that's a bit much visual noise, so we strip that data-theme attribute off. The watch on disabled makes sure we do this again on the next tick whenever the field goes back to disabled, and mounted runs it once on load along with addControls.
A Bit of Styling
Last but not least we add a bit of styling to our index.css file.
.k-field-type-quicklinks .k-field-label button {
opacity: 0;
margin-left: var(--spacing-1);
transition: opacity 0.2s ease-out;
}
.k-field-type-quicklinks .k-field-label:hover button,
.k-field-type-quicklinks .k-field-label:focus-within button {
opacity: 1;
}
Putting It to Use
Since we registered this as a field, we can simply put it anywhere fields are allowed, for example in a fields section of our site.yml.
quicklinks:
type: quicklinks
To use the user permissions, simply add it to your [role].yml file like this:
permissions:
project.quicklinks:
update: false