Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Area and MenuItem classes #6969

Merged
merged 15 commits into from
Feb 4, 2025
2 changes: 1 addition & 1 deletion config/areas/logout.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

return function ($kirby) {
return [
'icon' => 'user',
'icon' => 'logout',
'label' => I18n::translate('logout'),
'views' => [
'logout' => [
Expand Down
11 changes: 10 additions & 1 deletion panel/src/components/View/Inside.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
<template>
<k-panel class="k-panel-inside">
<k-panel-menu />
<k-panel-menu
v-bind="$panel.menu.props"
:hover="$panel.menu.hover"
:is-open="$panel.menu.isOpen"
:license="$panel.license"
:searches="$panel.searches"
@hover="$panel.menu.hover = $event"
@search="$panel.search()"
@toggle="$panel.menu.toggle()"
/>
<main class="k-panel-main">
<k-topbar :breadcrumb="$panel.view.breadcrumb" :view="$panel.view">
<!-- @slot Additional content for the Topbar -->
Expand Down
53 changes: 34 additions & 19 deletions panel/src/components/View/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<nav
class="k-panel-menu"
:aria-label="$t('menu')"
:data-hover="$panel.menu.hover"
@mouseenter="$panel.menu.hover = true"
@mouseleave="$panel.menu.hover = false"
:data-hover="hover"
@mouseenter="$emit('hover', true)"
@mouseleave="$emit('hover', false)"
>
<div class="k-panel-menu-body">
<!-- Search button -->
Expand All @@ -13,7 +13,7 @@
:text="$t('search')"
icon="search"
class="k-panel-menu-search k-panel-menu-button"
@click="$panel.search()"
@click="$emit('search')"
/>

<!-- Menus -->
Expand All @@ -23,13 +23,14 @@
:data-second-last="menuIndex === menus.length - 2"
class="k-panel-menu-buttons"
>
<k-button
v-for="entry in menu"
:key="entry.id"
v-bind="entry"
:title="entry.title ?? entry.text"
class="k-panel-menu-button"
/>
<template v-for="entry in menu">
<component
:is="entry.component"
:key="entry.key"
v-bind="entry.props"
class="k-panel-menu-button"
/>
</template>
</menu>

<menu v-if="activationButton">
Expand All @@ -40,17 +41,17 @@
theme="love"
variant="filled"
/>
<k-activation :status="$panel.license" />
<k-activation :status="license" />
</menu>
</div>

<!-- Collapse/expand toggle -->
<k-button
:icon="$panel.menu.isOpen ? 'angle-left' : 'angle-right'"
:title="$panel.menu.isOpen ? $t('collapse') : $t('expand')"
:icon="isOpen ? 'angle-left' : 'angle-right'"
:title="isOpen ? $t('collapse') : $t('expand')"
size="xs"
class="k-panel-menu-toggle"
@click="$panel.menu.toggle()"
@click="$emit('toggle')"
/>
</nav>
</template>
Expand All @@ -61,21 +62,35 @@
* @internal
*/
export default {
props: {
hover: Boolean,
isOpen: Boolean,
items: {
type: Array,
default: () => []
},
license: String,
searches: {
type: Object,
default: () => ({})
}
},
emits: ["search", "toggle"],
data() {
return {
over: false
};
},
computed: {
activationButton() {
if (this.$panel.license === "missing") {
if (this.license === "missing") {
return {
click: () => this.$dialog("registration"),
text: this.$t("activate")
};
}

if (this.$panel.license === "legacy") {
if (this.license === "legacy") {
return {
click: () => this.$dialog("license"),
text: this.$t("renew")
Expand All @@ -85,10 +100,10 @@ export default {
return false;
},
hasSearch() {
return this.$helper.object.length(this.$panel.searches) > 0;
return this.$helper.object.length(this.searches) > 0;
},
menus() {
return this.$helper.array.split(this.$panel.menu.entries, "-");
return this.$helper.array.split(this.items, "-");
}
}
};
Expand Down
6 changes: 3 additions & 3 deletions panel/src/panel/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import State from "./state.js";

export const defaults = () => {
return {
entries: [],
props: {},
hover: false,
isOpen: false
};
Expand Down Expand Up @@ -100,8 +100,8 @@ export default (panel) => {
*
* @param {Array} entries
*/
set(entries) {
this.entries = entries;
set(menu) {
this.props = menu.props;
this.resize();
return this.state();
},
Expand Down
197 changes: 197 additions & 0 deletions src/Panel/Area.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<?php

namespace Kirby\Panel;

use Closure;
use Kirby\Panel\Ui\MenuItem;
use Kirby\Toolkit\I18n;

/**
* @package Kirby Panel
* @author Nico Hoffmann <[email protected]>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
*/
class Area
{
public function __construct(
protected string $id,
protected array $breadcrumb = [],
protected Closure|array|string|null $breadcrumbLabel = null,
protected array $buttons = [],
protected Closure|bool|null $current = null,
protected string|null $dialog = null,
protected array $dialogs = [],
protected string|null $drawer = null,
protected array $drawers = [],
protected array $dropdowns = [],
protected string|null $icon = null,
protected Closure|array|string|null $label = null,
protected string|null $link = null,
protected Closure|array|bool|string $menu = false,
protected string|null $search = null,
protected array $searches = [],
protected array $requests = [],
protected Closure|array|string|null $title = null,
protected array $views = [],
) {
}

public function __call(string $name, array $args = [])
{
return $this->{$name};
}

/**
* A custom breadcrumb label that will be used for the
* breadcrumb instead of the default label
*/
public function breadcrumbLabel(): string
{
return $this->i18n($this->breadcrumbLabel ?? $this->label());
}

/**
* Translator for breadcrumbLabel, label & title
*/
protected function i18n(Closure|array|string|null $value): string|null
{
if ($value instanceof Closure) {
$value = $value();
}

return I18n::translate($value, $value);
}

/**
* Checks for access permissions for this area
*/
public function isAccessible(array $permissions): bool
{
return ($permissions['access'][$this->id] ?? true) === true;
}

/**
* Checks if the area is currently active
*/
public function isCurrent(string|null $current = null): bool
{
if ($this->current === null) {
return $this->id === $current;
}

if ($this->current instanceof Closure) {
return ($this->current)($current);
}

return $this->current;
}

/**
* The label is used for the menu item and the breadcrumb
* unless a custom breadcrumb label is defined
*/
public function label(): string
{
return $this->i18n($this->label ?? $this->id);
}

/**
* Link for the menu item
*/
public function link(): string
{
return $this->link ?? $this->id;
}

/**
* Set or overwrite additional props via array
*/
public function merge(array $props): static
{
foreach ($props as $key => $value) {
$this->{$key} = $value;
}

return $this;
}

/**
* Evaluate the menu settings to determine
* how the menu item for the area should be rendered
* and if it should be rendered at all
*/
public function menuItem(
array $areas = [],
array $permissions = [],
string|null $current = null
): MenuItem|null {
// areas without access permissions get skipped entirely
if ($this->isAccessible($permissions) === false) {
return null;
}

$menu = $this->menu;

// menu setting can be a callback
// that returns true, false or 'disabled'
if ($menu instanceof Closure) {
$menu = $menu($areas, $permissions, $current);
}

// false will remove the area/entry entirely
// just like with disabled permissions
if ($menu === false) {
return null;
}

// create a new menu item instance for the area
$item = new MenuItem(
current: $this->isCurrent($current),
icon: $this->icon() ?? $this->id(),
text: $this->label(),
dialog: $this->dialog(),
drawer: $this->drawer(),
link: $this->link(),
);

// add the custom menu settings
$item->merge(match ($menu) {
'disabled' => ['disabled' => true],
true => [],
default => $menu
});

return $item;
}

/**
* The title is used for the browser title. It will fall back
* to the label if no custom title is defined.
*/
public function title(): string
{
return $this->i18n($this->title ?? $this->label());
}

/**
* Returns parameters that will be added to the
* view response (one level above the props) to
* render the view component properly
*/
public function toView(): array
{
return [
'breadcrumb' => $this->breadcrumb(),
'breadcrumbLabel' => $this->breadcrumbLabel(),
'icon' => $this->icon(),
'id' => $this->id(),
'label' => $this->label(),
'link' => $this->link(),
'search' => $this->search(),
'title' => $this->title(),
];
}
}
Loading
Loading