Could you post the code for your plugin?
index.php
<?php
// Some of this is adapted from: https://github.com/rasteiner/k3-pagesdisplay-section/blob/master/src/PagesDisplaySection.php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Cms\Section;
use Kirby\Toolkit\Query;
Kirby::plugin('myNamespace/parentChildSelect', [
'collectionFilters' => [
'hasCategory' => function ($collection, $field, $pageSlug, $keyForLookup) {
foreach ($collection->data as $key => $item) {
$fieldValue = $collection->getAttribute($item, $field);
$parsed = Json::decode($fieldValue->toString());
if (in_array($pageSlug, $parsed[$keyForLookup])) {
continue;
} else {
unset($collection->$key);
}
}
return $collection;
},
],
'fields' => [
'parentChildSelect' => [
'props' => [
'query' => function (string $query = null) {
return $query;
},
'label' => function (string $label = null) {
return $label;
},
'help' => function (string $help = null) {
return $help;
},
],
'computed' => [
'categoryPages' => function () {
$kirby = kirby();
$q = new Query($this->query, [
'kirby' => $kirby,
'site' => $kirby->site(),
'pages' => $kirby->site()->pages(),
'page' => $this->model(),
]);
$pages = $q->result();
if (!is_a($pages, \Kirby\Cms\Pages::class)) {
$result = $pages === null ? 'null' : get_class($pages);
throw new InvalidArgumentException(
"Query result must be of type \"Kirby\\Cms\\Pages\", \"{$result}\" given"
);
}
foreach ($pages->data as $item) {
$item->title = $item->title()->value();
$item->children = $item->children()->data();
foreach ($item->children as $child) {
$child->title = $child->title()->value();
}
}
return Json::encode($pages->data);
},
],
'save' => function ($value = null) {
if (is_array($value) && count($value) > 0) {
$value = Json::encode($value);
} else {
$value = null;
}
return $value;
},
],
],
]);
index.js
window.panel.plugin('myNamespace/parentChildSelect', {
fields: {
parentChildSelect: {
props: {
categoryPages: Array,
label: String,
help: String,
},
data() {
return {
items: this.categoryPages && JSON.parse(this.categoryPages),
selection: {
parents: [],
children: [],
},
}
},
mounted() {
// If there's a draft, any existing categories will have already been parsed from JSON into a JS object.
// Accordingly, we need to handle loading them as either.
var maybeExistingSelection = this.$store.getters['content/values']().categories
if (maybeExistingSelection) {
this.selection =
typeof maybeExistingSelection === 'object'
? maybeExistingSelection
: JSON.parse(maybeExistingSelection)
}
},
methods: {
select(selectionPath, value) {
this.selection[selectionPath].push(value)
},
unselect(selectionPath, value) {
this.selection[selectionPath] = this.selection[selectionPath].filter((i) => i !== value)
},
handleClick(selectionPath, value) {
if (this.selection[selectionPath].includes(value)) {
// Unselect the clicked item
this.unselect(selectionPath, value)
// If unselecting a parent, also ensure all children are unselected.
if (selectionPath === 'parents') {
if (this.items[value].children) {
Object.keys(this.items[value].children).forEach((childKey) => {
this.unselect('children', childKey)
})
}
}
} else {
// Select the clicked item.
this.select(selectionPath, value)
// If it's a child, ensure that the parent is selected.
if (selectionPath === 'children') {
this.select('parents', value.substring(value.lastIndexOf('/'), 0))
}
}
// This effectively tells Kirby that a change has been made that needs to be saved or discarded.
// Without this no changes on the field are ever saved.
this.$emit('input', this.selection)
},
},
template: /* html */ `
<div>
<k-headline class="_mb075">
{{ label }}
<span
v-if="selection.parents.length + selection.children.length"
class="_c-light"
>
({{ selection.parents.length + selection.children.length }})
</span>
</k-headline>
<div class="parentChildSelect-container">
<div
class="parentChildSelect-parentChildWrapper"
v-for="(parentValue, parentKey) in items"
>
<button
type="button"
class="parentChildSelect-entry parentChildSelect-entryParent"
v-bind:class="{ 'parentChildSelect-entrySelected': selection.parents.includes(parentKey) }"
@click="handleClick('parents', parentKey)"
>
<span class="parentChildSelect-checkbox"></span>
{{ parentValue.title }}
</button>
<div v-if="parentValue.children">
<button
type="button"
class="parentChildSelect-entry parentChildSelect-entryChild"
v-bind:class="{ 'parentChildSelect-entrySelected': selection.children.includes(childKey) }"
@click="handleClick('children', childKey)"
v-for="(childValue, childKey) in parentValue.children"
>
<span class="parentChildSelect-checkbox"></span>
{{ childValue.title }}
</button>
</div>
</div>
</div>
<div
v-if="help"
class="_mt075 _fs-14 _c-light"
v-html="help"
>
</div>
</div>
`,
},
},
})
index.css
(Note: This is not relevant to the bug but including it here in case it’s helpful for others. There’s also some other css used that’s defined in a global css file that’s not included here.)
.parentChildSelect-container {
background: var(--field-input-background);
padding: 5px;
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
}
.parentChildSelect-entry {
line-height: 1;
width: 100%;
padding: 6px 6px;
text-align: left;
cursor: pointer;
display: flex;
color: var(--color-gray-600);
}
.parentChildSelect-entry:hover {
background: var(--color-gray-100);
}
.parentChildSelect-entryChild {
margin-left: 24px;
position: relative;
}
.parentChildSelect-entrySelected {
color: var(--color-text);
}
.parentChildSelect-entryChild:before {
content: '∟';
position: absolute;
left: -14px;
top: 0;
color: var(--color-gray-400);
}
/* Checkbox */
.parentChildSelect-checkbox {
width: 14px;
height: 14px;
border: solid 1px var(--color-gray-400);
margin-right: 8px;
position: relative;
background: var(--color-gray-200);
}
.parentChildSelect-entrySelected .parentChildSelect-checkbox {
border-color: gray;
background: var(--color-green-600);
}
.parentChildSelect-entrySelected .parentChildSelect-checkbox:after {
content: '✓';
position: absolute;
color: white;
font-size: 14px;
top: -1px;
}
Example of what’s saved to the content file (pretty printed here, it’s not saved as such):
Categories: {
"parents": [
"blog-categories/healthy-food",
"blog-categories/healthy-lifestyle"
],
"children": [
"blog-categories/healthy-lifestyle/gardening"
]
}
On a side note: Why don’t you use the pages field?
There are a few things about the pages field that make it not work for our use case:
- We need to ensure that if a child is selected the parent is also selected. AFAIK there’s no way to achieve this with the pages field.
- Likewise, we need to sure that if a parent is unselected all of the children are also unselected. AFAIK there’s no way to achieve this with the pages field.
- Selected child pages from the pages field are not displayed in their hierarchy (i.e. it’s not clear that their children or what’s their parent is when they’re displayed).
- From a content editor’s perspective, being able to see all the options at one time is helpful. The UX of the pages field requires users to click a parent in order to view their children.
For context: We’re migrating a blog from WP which made extensive use of WP’s nested categories. Having a way to assign categories in a similar way was more or less a requirement.
Here’s a screenshot for reference: