init with kirby, vue and pagedjs interactive

This commit is contained in:
isUnknown 2025-11-24 14:01:48 +01:00
commit dc0ae26464
968 changed files with 211706 additions and 0 deletions

View file

@ -0,0 +1,33 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Cms\App;
use Kirby\Toolkit\I18n;
/**
* View button to create a new language
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class LanguageCreateButton extends ViewButton
{
public function __construct()
{
$user = App::instance()->user();
$permission = $user?->role()->permissions()->for('languages', 'create');
parent::__construct(
dialog: 'languages/create',
disabled: $permission !== true,
icon: 'add',
text: I18n::translate('language.create'),
);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Cms\App;
use Kirby\Cms\Language;
use Kirby\Toolkit\I18n;
/**
* View button to delete a language
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class LanguageDeleteButton extends ViewButton
{
public function __construct(Language $language)
{
$user = App::instance()->user();
$permission = $user?->role()->permissions()->for('languages', 'delete');
parent::__construct(
dialog: 'languages/' . $language->id() . '/delete',
disabled: $permission !== true,
icon: 'trash',
title: I18n::translate('delete'),
);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Cms\App;
use Kirby\Cms\Language;
use Kirby\Toolkit\I18n;
/**
* View button to update settings of a language
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class LanguageSettingsButton extends ViewButton
{
public function __construct(Language $language)
{
$user = App::instance()->user();
$permission = $user?->role()->permissions()->for('languages', 'update');
parent::__construct(
dialog: 'languages/' . $language->id() . '/update',
disabled: $permission !== true,
icon: 'cog',
title: I18n::translate('settings'),
);
}
}

View file

@ -0,0 +1,120 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Cms\App;
use Kirby\Cms\Language;
use Kirby\Cms\Languages;
use Kirby\Cms\ModelWithContent;
use Kirby\Toolkit\Str;
/**
* View button to switch content translation languages
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class LanguagesDropdown extends ViewButton
{
protected App $kirby;
public function __construct(
ModelWithContent $model
) {
$this->kirby = $model->kirby();
parent::__construct(
component: 'k-languages-dropdown',
model: $model,
class: 'k-languages-dropdown',
icon: 'translate',
// Fiber dropdown endpoint to load options
// only when dropdown is opened
options: $model->panel()->url(true) . '/languages',
responsive: 'text',
text: Str::upper($this->kirby->language()?->code())
);
}
/**
* Returns if any translation other than the current one has unsaved changes
* (the current language has to be handled in `k-languages-dropdown` as its
* state can change dynamically without another backend request)
*/
public function hasDiff(): bool
{
foreach (Languages::ensure() as $language) {
if ($this->kirby->language()?->code() !== $language->code()) {
if ($this->model->version('changes')->exists($language) === true) {
return true;
}
}
}
return false;
}
public function option(Language $language): array
{
$changes = $this->model->version('changes');
return [
'text' => $language->name(),
'code' => $language->code(),
'current' => $language->code() === $this->kirby->language()?->code(),
'default' => $language->isDefault(),
'changes' => $changes->exists($language),
'lock' => $changes->isLocked('*')
];
}
/**
* Options are used in the Fiber dropdown routes
*/
public function options(): array
{
$languages = $this->kirby->languages();
$options = [];
if ($this->kirby->multilang() === false) {
return $options;
}
// add the primary/default language first
if ($default = $languages->default()) {
$options[] = $this->option($default);
$options[] = '-';
$languages = $languages->not($default);
}
// add all secondary languages after the separator
foreach ($languages as $language) {
$options[] = $this->option($language);
}
return $options;
}
public function props(): array
{
return [
...parent::props(),
'hasDiff' => $this->hasDiff()
];
}
public function render(): array|null
{
// hides the language selector when there are less than 2 languages
if ($this->kirby->languages()->count() < 2) {
return null;
}
return parent::render();
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Toolkit\I18n;
/**
* Open view button
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class OpenButton extends ViewButton
{
public function __construct(
public string|null $link,
public string|null $target = '_blank'
) {
parent::__construct(
class: 'k-open-view-button',
icon: 'open',
link: $link,
target: $target,
title: I18n::translate('open')
);
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Cms\Page;
use Kirby\Toolkit\I18n;
/**
* Status view button for pages
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class PageStatusButton extends ViewButton
{
public function __construct(
Page $page
) {
$status = $page->status();
$blueprint = $page->blueprint()->status()[$status] ?? null;
$disabled = $page->permissions()->cannot('changeStatus');
$text = $blueprint['label'] ?? I18n::translate('page.status.' . $status);
$title = I18n::translate('page.status') . ': ' . $text;
if ($disabled === true) {
$title .= ' (' . I18n::translate('disabled') . ')';
}
parent::__construct(
class: 'k-status-view-button k-page-status-button',
component: 'k-status-view-button',
dialog: $page->panel()->url(true) . '/changeStatus',
disabled: $disabled,
icon: 'status-' . $status,
style: '--icon-size: 15px',
text: $text,
title: $title,
theme: match($status) {
'draft' => 'negative-icon',
'unlisted' => 'info-icon',
'listed' => 'positive-icon'
}
);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Toolkit\I18n;
/**
* Preview view button
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class PreviewButton extends ViewButton
{
public function __construct(
public string|null $link
) {
parent::__construct(
class: 'k-preview-view-button',
icon: 'window',
link: $link,
title: I18n::translate('preview')
);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Cms\ModelWithContent;
use Kirby\Toolkit\I18n;
/**
* Settings view button for models
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class SettingsButton extends ViewButton
{
public function __construct(
ModelWithContent $model
) {
parent::__construct(
component: 'k-settings-view-button',
class: 'k-settings-view-button',
icon: 'cog',
options: $model->panel()->url(true),
title: I18n::translate('settings'),
);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Cms\ModelWithContent;
use Kirby\Content\VersionId;
use Kirby\Toolkit\I18n;
/**
* Versions view button for models
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class VersionsButton extends ViewButton
{
public function __construct(
ModelWithContent $model,
VersionId|string $versionId = 'latest'
) {
$versionId = $versionId === 'compare' ? 'compare' : VersionId::from($versionId)->value();
$viewUrl = $model->panel()->url(true) . '/preview';
parent::__construct(
class: 'k-versions-view-button',
icon: $versionId === 'compare' ? 'layout-columns' : 'git-branch',
options: [
[
'label' => I18n::translate('version.latest'),
'icon' => 'git-branch',
'link' => $viewUrl . '/latest',
'current' => $versionId === 'latest'
],
[
'label' => I18n::translate('version.changes'),
'icon' => 'git-branch',
'link' => $viewUrl . '/changes',
'current' => $versionId === 'changes'
],
'-',
[
'label' => I18n::translate('version.compare'),
'icon' => 'layout-columns',
'link' => $viewUrl . '/compare',
'current' => $versionId === 'compare'
],
],
text: I18n::translate('version.' . $versionId),
);
}
}

View file

@ -0,0 +1,215 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Closure;
use Kirby\Cms\App;
use Kirby\Cms\Language;
use Kirby\Cms\ModelWithContent;
use Kirby\Panel\Panel;
use Kirby\Panel\Ui\Button;
use Kirby\Toolkit\Controller;
/**
* A view button is a UI button, by default small in size and filles,
* that optionally defines options for a dropdown
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
*/
class ViewButton extends Button
{
public function __construct(
public string $component = 'k-view-button',
public readonly ModelWithContent|Language|null $model = null,
public array|null $badge = null,
public string|null $class = null,
public string|bool|null $current = null,
public string|null $dialog = null,
public bool $disabled = false,
public string|null $drawer = null,
public bool|null $dropdown = null,
public string|null $icon = null,
public string|null $link = null,
public array|string|null $options = null,
public bool|string $responsive = true,
public string|null $size = 'sm',
public string|null $style = null,
public string|null $target = null,
public string|array|null $text = null,
public string|null $theme = null,
public string|array|null $title = null,
public string $type = 'button',
public string|null $variant = 'filled',
...$attrs
) {
$this->attrs = $attrs;
}
/**
* Creates new view button by looking up
* the button in all areas, if referenced by name
* and resolving to proper instance
*/
public static function factory(
string|array|Closure|bool $button = true,
string|int|null $name = null,
string|null $view = null,
ModelWithContent|Language|null $model = null,
array $data = []
): static|null {
// if referenced by name (`name: false`),
// don't render anything
if ($button === false) {
return null;
}
// transform `- name` notation to `name: true`
if (
is_string($name) === false &&
is_string($button) === true
) {
$name = $button;
$button = true;
}
// if referenced by name (`name: true`),
// try to get button definition from areas or config
if ($button === true) {
$button = static::find($name, $view);
}
// resolve Closure to button object or array
if ($button instanceof Closure) {
$button = static::resolve($button, $model, $data);
}
if (
$button === null ||
$button instanceof ViewButton
) {
return $button;
}
// flatten array into list of arguments for this class
$button = static::normalize($button);
// if button definition has a name, use it for the component name
if (is_string($name) === true) {
// if this specific component does not exist,
// `k-view-buttons` will fall back to `k-view-button` again
$button['component'] ??= 'k-' . $name . '-view-button';
}
return new static(...$button, model: $model);
}
/**
* Finds a view button by name
* among the defined buttons from all areas
* @unstable
*/
public static function find(
string $name,
string|null $view = null
): array|Closure {
// collect all buttons from areas and config
$buttons = [
...Panel::buttons(),
...App::instance()->option('panel.viewButtons.' . $view, [])
];
// try to find by full name (view-prefixed)
if ($view && $button = $buttons[$view . '.' . $name] ?? null) {
return $button;
}
// try to find by just name
if ($button = $buttons[$name] ?? null) {
return $button;
}
// assume it must be a custom view button component
return ['component' => 'k-' . $name . '-view-button'];
}
/**
* Transforms an array to be used as
* named arguments in the constructor
* @unstable
*/
public static function normalize(array $button): array
{
// if component and props are both not set, assume shortcut
// where props were directly passed on top-level
if (
isset($button['component']) === false &&
isset($button['props']) === false
) {
return $button;
}
// flatten array
if ($props = $button['props'] ?? null) {
$button = [...$props, ...$button];
unset($button['props']);
}
return $button;
}
public function props(): array
{
// helper for props that support Kirby queries
$resolve = fn ($value) =>
$value ?
$this->model?->toSafeString($value) ?? $value :
null;
return [
...$props = parent::props(),
'dialog' => $resolve($props['dialog']),
'drawer' => $resolve($props['drawer']),
'icon' => $resolve($props['icon']),
'link' => $resolve($props['link']),
'text' => $resolve($props['text']),
'theme' => $resolve($props['theme']),
'options' => $this->options
];
}
/**
* Transforms a closure to the actual view button
* by calling it with the provided arguments
*/
public static function resolve(
Closure $button,
ModelWithContent|Language|null $model = null,
array $data = []
): static|array|null {
$kirby = App::instance();
$controller = new Controller($button);
if (
$model instanceof ModelWithContent ||
$model instanceof Language
) {
$data = [
'model' => $model,
$model::CLASS_ALIAS => $model,
...$data
];
}
return $controller->call(data: [
'kirby' => $kirby,
'site' => $kirby->site(),
'user' => $kirby->user(),
...$data
]);
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace Kirby\Panel\Ui\Buttons;
use Kirby\Cms\App;
use Kirby\Cms\Language;
use Kirby\Cms\ModelWithContent;
use Kirby\Panel\Model;
/**
* Collects view buttons for a specific view
*
* @package Kirby Panel
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class ViewButtons
{
public function __construct(
public readonly string $view,
public readonly ModelWithContent|Language|null $model = null,
public array|false|null $buttons = null,
public array $data = []
) {
// if no specific buttons are passed,
// use default buttons for this view from config
$this->buttons ??= App::instance()->option(
'panel.viewButtons.' . $view
);
}
/**
* Adds data passed to view button closures
*
* @return $this
*/
public function bind(array $data): static
{
$this->data = [...$this->data, ...$data];
return $this;
}
/**
* Sets the default buttons
*
* @return $this
*/
public function defaults(string ...$defaults): static
{
$this->buttons ??= $defaults;
return $this;
}
/**
* Returns array of button component-props definitions
*/
public function render(): array
{
// hides all buttons when `buttons: false` set
if ($this->buttons === false) {
return [];
}
$buttons = [];
foreach ($this->buttons ?? [] as $name => $button) {
$buttons[] = ViewButton::factory(
button: $button,
name: $name,
view: $this->view,
model: $this->model,
data: $this->data
)?->render();
}
return array_values(array_filter($buttons));
}
/**
* Creates new instance for a view
* with special support for model views
*/
public static function view(
string|Model $view,
ModelWithContent|Language|null $model = null
): static {
if ($view instanceof Model) {
$model = $view->model();
$blueprint = $model->blueprint()->buttons();
$view = $model::CLASS_ALIAS;
}
return new static(
view: $view,
model: $model ?? null,
buttons: $blueprint ?? null
);
}
}