src/Modal.php
<?php
declare(strict_types=1);
namespace Atk4\Ui;
use Atk4\Ui\Js\JsChain;
use Atk4\Ui\Js\JsExpressionable;
/**
* This class add modal dialog to a page.
*
* Modal are added to the layout but their content is hidden by default.
* $modal->jsShow() is the triggered needed to actually display the modal.
*
* Modal can be use as a regular view, simply by adding other view to it.
* Message::addTo($modal, ['title' => 'Welcome to Agile Toolkit'])->text('Your text here');
*
* Modal can add content dynamically via CallbackLater.
* $modal->set(function (View $p) {
* Form::addTo($p);
* });
*
* Modal can use Fomantic-UI predefine method onApprove or onDeny by passing
* a jsAction to Modal::addDenyAction or Modal::addApproveAction method. It will not close until the jsAction return true.
* $modal->addDenyAction('No', new JsExpression('function () { window.alert(\'Cannot do that.\'); return false; }'));
* $modal->addApproveAction('Yes', new JsExpression('function () { window.alert(\'You are good to go!\'); }'));
*
* You may also prevent modal from closing via the esc or dimmed area click using $modal->notClosable().
*/
class Modal extends View
{
public $ui = 'modal';
public $defaultTemplate = 'modal.html';
/** @var string|null Set null for no title */
public $title;
/** @var string */
public $loadingLabel = 'Loading...';
/** @var string */
public $headerClass = 'header';
/** @var \Closure(View): void|null */
public $fx;
/** @var CallbackLater|null */
public $cb;
/** @var View|null */
public $cbView;
/** @var array<string, mixed> */
public $args = [];
/** @var array<string, mixed> */
public $options = [];
/** @var string Currently only "json" response type is supported. */
public $type = 'json';
/** @var list<string> Add ability to add CSS classes to "content" div. */
public $contentClass = ['img', 'content', 'atk-dialog-content'];
/**
* If true, the <div class="actions"> at the bottom of the modal is
* shown. Automatically set to true if any actions are added.
*
* @var bool
*/
public $showActions = false;
/**
* Set callback function for this modal.
*
* @param \Closure(View): void $fx
*/
#[\Override]
public function set($fx = null)
{
if (!$fx instanceof \Closure) {
throw new \TypeError('$fx must be of type Closure');
}
$this->fx = $fx;
$this->enableCallback();
return $this;
}
/**
* Add View to be loaded in this modal and
* attach CallbackLater to it.
* The cbView only will be loaded dynamically within modal
* div.atk-content.
*/
public function enableCallback(): void
{
$this->cbView = View::addTo($this);
$this->cbView->stickyGet('__atk_m', $this->name);
if (!$this->cb) {
$this->cb = CallbackLater::addTo($this->cbView);
}
$this->cb->set(function () {
($this->fx)($this->cbView);
$this->cb->terminateJsonIfCanTerminate($this->cbView);
});
}
/**
* Add CSS classes to "content" div.
*
* @param string|list<string> $class
*/
public function addContentClass($class): void
{
$this->contentClass = array_merge($this->contentClass, is_string($class) ? [$class] : $class);
}
/**
* Show modal on page.
*
* Example: $button->on('click', $modal->jsShow());
*
* @param array<string, string> $args
*
* @return JsChain
*/
public function jsShow(array $args = []): JsExpressionable
{
$chain = $this->js();
if ($args !== []) {
$chain->data(['args' => $args]);
}
return $chain->modal('show');
}
/**
* Hide modal from page.
*
* @return JsChain
*/
public function jsHide(): JsExpressionable
{
return $this->js()->modal('hide');
}
/**
* Set modal option.
*
* @param string $option
* @param mixed $value
*
* @return $this
*/
public function setOption($option, $value)
{
$this->options[$option] = $value;
return $this;
}
/**
* Add scrolling capability to modal.
*
* @return $this
*/
public function addScrolling()
{
$this->addContentClass('scrolling');
return $this;
}
/**
* Add a deny action to modal.
*
* @param string $label
* @param JsExpressionable $jsAction will run when deny is click
*
* @return $this
*/
public function addDenyAction($label, JsExpressionable $jsAction)
{
$button = new Button();
$button->set($label)->addClass('red cancel');
$this->addButtonAction($button);
$this->options['onDeny'] = $jsAction;
return $this;
}
/**
* Add an approve action button to modal.
*
* @param string $label
* @param JsExpressionable $jsAction will run when deny is click
*
* @return $this
*/
public function addApproveAction($label, JsExpressionable $jsAction)
{
$b = new Button();
$b->set($label)->addClass('green ok');
$this->addButtonAction($b);
$this->options['onApprove'] = $jsAction;
return $this;
}
/**
* Add an action button to modal.
*
* @param View $button
*
* @return $this
*/
public function addButtonAction($button)
{
$this->add($button, 'actions');
$this->showActions = true;
return $this;
}
/**
* Make this modal not closable via close icon, esc key or via the dimmer area.
*
* @return $this
*/
public function notClosable()
{
$this->options['closable'] = false;
return $this;
}
#[\Override]
protected function renderView(): void
{
$data = [];
$data['type'] = $this->type;
$data['loadingLabel'] = $this->loadingLabel;
if ($this->title) {
$this->template->trySet('title', $this->title);
$this->template->trySet('headerClass', $this->headerClass);
} else {
// fix top modal corner rounding, first div must not be empty (must not be lower than 5px)
// https://github.com/fomantic/Fomantic-UI/blob/2.9.0/src/definitions/modules/modal.less#L43
$this->template->loadFromString(preg_replace('~<div class="\{\$headerClass\}">\{\$title\}</div>\s*~', '', $this->template->toLoadableString(), 1));
}
$this->template->trySet('contentClass', implode(' ', $this->contentClass));
if ($this->fx !== null) {
$data['url'] = $this->cb->getJsUrl();
}
if (!$this->showActions) {
$this->template->del('ActionContainer');
}
$this->js(true)->modal($this->options);
if (!isset($this->options['closable']) || $this->options['closable']) {
$this->template->trySet('closeIcon', 'close');
} else {
// fix no extra space for icon
// TODO should be replaced with i tag render
$this->template->loadFromString(preg_replace('~<i class="\{\$closeIcon\} icon"></i>~', '', $this->template->toLoadableString(), 1));
}
if ($this->args) {
$data['args'] = $this->args;
}
$this->js(true)->data($data);
parent::renderView();
}
}