diff --git a/config/widgets-themes.php b/config/widgets-themes.php
index d799a9098..39fa0dcad 100644
--- a/config/widgets-themes.php
+++ b/config/widgets-themes.php
@@ -8,10 +8,25 @@
use Yiisoft\Yii\DataView\GridView;
use Yiisoft\Yii\DataView\KeysetPagination;
use Yiisoft\Yii\DataView\OffsetPagination;
+use Yiisoft\Yii\DataView\PageSize\InputPageSize;
+use Yiisoft\Yii\DataView\PageSize\SelectPageSize;
return [
'bootstrap5' => [
GridView::class => [
+ 'layout()' => [
+ <<
+
{pager}
+ {pageSize}
+
+ HTML
+ ],
+ 'pageSizeTag()' => [null],
'summaryTag()' => ['p'],
'summaryAttributes()' => [['class' => 'text-secondary']],
'tableClass()' => ['table table-bordered'],
@@ -54,5 +69,11 @@
'currentItemClass()' => ['active'],
'disabledItemClass()' => ['disabled'],
],
+ InputPageSize::class => [
+ 'attributes()' => [['class' => 'form-control d-inline text-center mx-2', 'style' => 'width:60px']],
+ ],
+ SelectPageSize::class => [
+ 'attributes()' => [['class' => 'form-select w-auto d-inline mx-2']],
+ ],
],
];
diff --git a/src/BaseListView.php b/src/BaseListView.php
index b4adcb7bb..227c9b840 100644
--- a/src/BaseListView.php
+++ b/src/BaseListView.php
@@ -33,16 +33,25 @@
use Yiisoft\Widget\Widget;
use Yiisoft\Data\Reader\OrderHelper;
use Yiisoft\Yii\DataView\Exception\DataReaderNotSetException;
+use Yiisoft\Yii\DataView\PageSize\InputPageSize;
+use Yiisoft\Yii\DataView\PageSize\PageSizeContext;
+use Yiisoft\Yii\DataView\PageSize\PageSizeControlInterface;
+use Yiisoft\Yii\DataView\PageSize\SelectPageSize;
+use function array_key_exists;
use function array_slice;
+use function call_user_func_array;
use function extension_loaded;
+use function in_array;
use function is_array;
+use function is_int;
use function is_string;
/**
* @psalm-type UrlArguments = array
* @psalm-type UrlCreator = callable(UrlArguments,array):string
* @psalm-type PageNotFoundExceptionCallback = callable(PageNotFoundException):void
+ * @psalm-type PageSizeConstraint = list|positive-int|bool
* @psalm-import-type TOrder from Sort
*/
abstract class BaseListView extends Widget
@@ -53,8 +62,34 @@ abstract class BaseListView extends Widget
protected $urlCreator = null;
protected UrlConfig $urlConfig;
+ /**
+ * @var int Page size that is used in case it is not set explicitly.
+ * @psalm-var positive-int
+ */
protected int $defaultPageSize = PaginatorInterface::DEFAULT_PAGE_SIZE;
+ /**
+ * @var array|bool|int Page size constraint.
+ * - `true` - default only.
+ * - `false` - no constraint.
+ * - int - maximum page size.
+ * - [int, int, ...] - a list of page sizes to choose from.
+ *
+ * @see PageSizeContext::FIXED_VALUE
+ * @see PageSizeContext::ANY_VALUE
+ *
+ * @psalm-var PageSizeConstraint
+ */
+ protected bool|int|array $pageSizeConstraint = true;
+
+ /**
+ * @psalm-var non-empty-string|null
+ */
+ private ?string $pageSizeTag = 'div';
+ private array $pageSizeAttributes = [];
+ private ?string $pageSizeTemplate = 'Results per page {control}';
+ private PageSizeControlInterface|null $pageSizeControl = null;
+
/**
* A name for {@see CategorySource} used with translator ({@see TranslatorInterface}) by default.
* @psalm-suppress MissingClassConstType
@@ -84,7 +119,7 @@ abstract class BaseListView extends Widget
private array $emptyTextAttributes = [];
private string $header = '';
private array $headerAttributes = [];
- private string $layout = "{header}\n{toolbar}\n{items}\n{summary}\n{pager}";
+ private string $layout = "{header}\n{toolbar}\n{items}\n{summary}\n{pager}\n{pageSize}";
private array $offsetPaginationConfig = [];
private array $keysetPaginationConfig = [];
@@ -372,9 +407,9 @@ private function prepareDataReaderByParams(
}
if ($dataReader->isPaginationRequired()) {
- if ($pageSize !== null) {
- $dataReader = $dataReader->withPageSize((int) $pageSize);
- }
+ $dataReader = $dataReader->withPageSize(
+ $this->preparePageSize($pageSize) ?? $this->getDefaultPageSize()
+ );
if ($page !== null) {
$dataReader = $dataReader->withToken(PageToken::next($page));
@@ -468,6 +503,53 @@ public function layout(string $value): static
return $new;
}
+ final public function pageSizeTag(?string $tag): static
+ {
+ if ($tag === '') {
+ throw new InvalidArgumentException('Tag name cannot be empty.');
+ }
+
+ $new = clone $this;
+ $new->pageSizeTag = $tag;
+ return $new;
+ }
+
+ /**
+ * Returns a new instance with the HTML attributes for page size wrapper tag.
+ *
+ * @param array $values Attribute values indexed by attribute names.
+ */
+ final public function pageSizeAttributes(array $values): static
+ {
+ $new = clone $this;
+ $new->pageSizeAttributes = $values;
+ return $new;
+ }
+
+ /**
+ * Returns a new instance with the page size template.
+ *
+ * @param string|null $template The HTML content to be displayed as the page size control. If you don't want to show
+ * control, you may set it with an empty string or null.
+ *
+ * The following tokens will be replaced with the corresponding values:
+ *
+ * - `{control}` — page size control.
+ */
+ final public function pageSizeTemplate(?string $template): static
+ {
+ $new = clone $this;
+ $new->pageSizeTemplate = $template;
+ return $new;
+ }
+
+ final public function pageSizeControl(?PageSizeControlInterface $widget): static
+ {
+ $new = clone $this;
+ $new->pageSizeControl = $widget;
+ return $new;
+ }
+
public function pagination(string|KeysetPagination|OffsetPagination|null $pagination): static
{
$new = clone $this;
@@ -627,6 +709,7 @@ public function render(): string
'{items}' => $this->renderItems($items, $filterValidationResult),
'{summary}' => $this->renderSummary(),
'{pager}' => $this->renderPagination(),
+ '{pageSize}' => $this->renderPageSize(),
],
)
);
@@ -638,14 +721,48 @@ public function render(): string
->render();
}
+ /**
+ * @psalm-return positive-int
+ */
protected function getDefaultPageSize(): int
{
$dataReader = $this->getDataReader();
- if ($dataReader instanceof PaginatorInterface) {
- return $dataReader->getPageSize();
+ $pageSize = $dataReader instanceof PaginatorInterface
+ ? $dataReader->getPageSize()
+ : $this->defaultPageSize;
+
+ if (is_int($this->pageSizeConstraint)) {
+ return $pageSize <= $this->pageSizeConstraint
+ ? $pageSize
+ : $this->pageSizeConstraint;
+ }
+
+ if (is_array($this->pageSizeConstraint)) {
+ return in_array($pageSize, $this->pageSizeConstraint, true)
+ ? $pageSize
+ : $this->pageSizeConstraint[0];
}
- return $this->defaultPageSize;
+ return $pageSize;
+ }
+
+ /**
+ * Get a new instance with a page size constraint set.
+ *
+ * @param array|bool|int $pageSizeConstraint Page size constraint.
+ * `true` - default only.
+ * `false` - no constraint.
+ * int - maximum page size.
+ * [int, int, ...] - a list of page sizes to choose from.
+ * @return static New instance.
+ *
+ * @psalm-param PageSizeConstraint $pageSizeConstraint
+ */
+ public function pageSizeConstraint(array|int|bool $pageSizeConstraint): static
+ {
+ $new = clone $this;
+ $new->pageSizeConstraint = $pageSizeConstraint;
+ return $new;
}
/**
@@ -705,6 +822,69 @@ private function renderPagination(): string
->render();
}
+ private function renderPageSize(): string
+ {
+ if (empty($this->pageSizeTemplate)) {
+ return '';
+ }
+
+ $dataReader = $this->preparedDataReader;
+ if (!$dataReader instanceof PaginatorInterface) {
+ return '';
+ }
+
+ if ($this->pageSizeControl === null) {
+ if ($this->pageSizeConstraint === false || is_int($this->pageSizeConstraint)) {
+ $widget = InputPageSize::widget();
+ } elseif (is_array($this->pageSizeConstraint)) {
+ $widget = SelectPageSize::widget();
+ } else {
+ return '';
+ }
+ } else {
+ $widget = $this->pageSizeControl;
+ }
+
+ if ($this->urlCreator === null) {
+ $urlPattern = '#pagesize=' . PageSizeContext::URL_PLACEHOLDER;
+ $defaultUrl = '#';
+ } else {
+ $sort = $this->getSortValueForUrl($dataReader);
+ $urlPattern = call_user_func_array(
+ $this->urlCreator,
+ UrlParametersFactory::create(
+ null,
+ PageSizeContext::URL_PLACEHOLDER,
+ $sort,
+ $this->urlConfig
+ ),
+ );
+ $defaultUrl = call_user_func_array(
+ $this->urlCreator,
+ UrlParametersFactory::create(null, null, $sort, $this->urlConfig),
+ );
+ }
+
+ $context = new PageSizeContext(
+ $dataReader->getPageSize(),
+ $this->getDefaultPageSize(),
+ $this->pageSizeConstraint,
+ $urlPattern,
+ $defaultUrl,
+ );
+ $control = $widget->withContext($context)->render();
+
+ $content = $this->translator->translate(
+ $this->pageSizeTemplate,
+ ['control' => $control],
+ $this->translationCategory,
+ );
+
+ return $this->pageSizeTag === null
+ ? $content
+ : Html::tag($this->pageSizeTag, $content, $this->pageSizeAttributes)->encode(false)->render();
+ }
+
private function renderSummary(): string
{
if (empty($this->summaryTemplate)) {
@@ -767,6 +947,39 @@ private function renderHeader(): string
};
}
+ /**
+ * @psalm-return positive-int|null
+ */
+ private function preparePageSize(?string $rawPageSize): ?int
+ {
+ if ($this->pageSizeConstraint === true) {
+ return null;
+ }
+
+ if ($rawPageSize === null) {
+ return null;
+ }
+
+ $pageSize = (int) $rawPageSize;
+ if ($pageSize < 1) {
+ return null;
+ }
+
+ if ($this->pageSizeConstraint === false) {
+ return $pageSize;
+ }
+
+ if (is_int($this->pageSizeConstraint) && $pageSize <= $this->pageSizeConstraint) {
+ return $pageSize;
+ }
+
+ if (is_array($this->pageSizeConstraint) && in_array($pageSize, $this->pageSizeConstraint, true)) {
+ return $pageSize;
+ }
+
+ return null;
+ }
+
/**
* Creates default translator to use if {@see $translator} wasn't set explicitly in the constructor. Depending on
* "intl" extension availability, either {@see IntlMessageFormatter} or {@see SimpleMessageFormatter} is used as
@@ -786,4 +999,41 @@ private function createDefaultTranslator(): Translator
return $translator;
}
+
+ private function getSortValueForUrl(PaginatorInterface $paginator): ?string
+ {
+ $sort = $this->getSort($paginator);
+ if ($sort === null) {
+ return null;
+ }
+
+ $originalSort = $this->getSort($this->dataReader);
+ if ($originalSort?->getOrderAsString() === $sort->getOrderAsString()) {
+ return null;
+ }
+
+ $order = [];
+ $overrideOrderFields = array_flip($this->getOverrideOrderFields());
+ foreach ($sort->getOrder() as $name => $value) {
+ $key = array_key_exists($name, $overrideOrderFields)
+ ? $overrideOrderFields[$name]
+ : $name;
+ $order[$key] = $value;
+ }
+
+ return OrderHelper::arrayToString($order);
+ }
+
+ private function getSort(?ReadableDataInterface $dataReader): ?Sort
+ {
+ if ($dataReader instanceof PaginatorInterface && $dataReader->isSortable()) {
+ return $dataReader->getSort();
+ }
+
+ if ($dataReader instanceof SortableDataInterface) {
+ return $dataReader->getSort();
+ }
+
+ return null;
+ }
}
diff --git a/src/BasePagination.php b/src/BasePagination.php
index a6764d4f3..796b562ae 100644
--- a/src/BasePagination.php
+++ b/src/BasePagination.php
@@ -213,6 +213,9 @@ public function urlConfig(UrlConfig $config): static
/**
* Creates the URL suitable for pagination with the specified page number. This method is mainly called by pagers
* when creating URLs used to perform pagination.
+ *
+ * @param PageToken $pageToken Token for the page.
+ * @return string Created URL.
*/
protected function createUrl(PageToken $pageToken): string
{
diff --git a/src/GridView.php b/src/GridView.php
index 02611b541..79b6cc1ec 100644
--- a/src/GridView.php
+++ b/src/GridView.php
@@ -30,6 +30,7 @@
use Yiisoft\Yii\DataView\Filter\Factory\IncorrectValueException;
use function call_user_func;
+use function call_user_func_array;
use function is_callable;
/**
diff --git a/src/PageSize/InputPageSize.php b/src/PageSize/InputPageSize.php
new file mode 100644
index 000000000..4a778b732
--- /dev/null
+++ b/src/PageSize/InputPageSize.php
@@ -0,0 +1,57 @@
+attributes = array_merge($new->attributes, $attributes);
+ return $new;
+ }
+
+ /**
+ * Replace SELECT tag attributes with a new set.
+ *
+ * @param array $attributes Name-value set of attributes.
+ */
+ public function attributes(array $attributes): self
+ {
+ $new = clone $this;
+ $new->attributes = $attributes;
+ return $new;
+ }
+
+ public function render(): string
+ {
+ $context = $this->getContext();
+
+ $attributes = array_merge($this->attributes, [
+ 'data-default-page-size' => $context->defaultValue,
+ 'data-url-pattern' => $context->urlPattern,
+ 'data-default-url' => $context->defaultUrl,
+ 'onchange' => 'window.location.href = this.value == this.dataset.defaultPageSize ? this.dataset.defaultUrl : this.dataset.urlPattern.replace("' . PageSizeContext::URL_PLACEHOLDER . '", this.value)',
+ ]);
+
+ return Html::textInput(
+ value: $context->currentValue,
+ attributes: $attributes
+ )->render();
+ }
+}
diff --git a/src/PageSize/PageSizeContext.php b/src/PageSize/PageSizeContext.php
new file mode 100644
index 000000000..b4b531b79
--- /dev/null
+++ b/src/PageSize/PageSizeContext.php
@@ -0,0 +1,40 @@
+context = $context;
+ return $new;
+ }
+
+ final protected function getContext(): PageSizeContext
+ {
+ if ($this->context === null) {
+ throw new LogicException('Context is not set.');
+ }
+ return $this->context;
+ }
+}
diff --git a/src/PageSize/PageSizeControlInterface.php b/src/PageSize/PageSizeControlInterface.php
new file mode 100644
index 000000000..bd5a2e547
--- /dev/null
+++ b/src/PageSize/PageSizeControlInterface.php
@@ -0,0 +1,14 @@
+attributes = array_merge($new->attributes, $attributes);
+ return $new;
+ }
+
+ /**
+ * Replace SELECT tag attributes with a new set.
+ *
+ * @param array $attributes Name-value set of attributes.
+ */
+ public function attributes(array $attributes): self
+ {
+ $new = clone $this;
+ $new->attributes = $attributes;
+ return $new;
+ }
+
+ public function render(): string
+ {
+ $context = $this->getContext();
+ if (!is_array($context->constraint) || count($context->constraint) < 2) {
+ return '';
+ }
+
+ $options = [];
+ foreach ($context->constraint as $value) {
+ $options[$value] = (string) $value;
+ }
+
+ $attributes = array_merge($this->attributes, [
+ 'data-default-page-size' => $context->defaultValue,
+ 'data-url-pattern' => $context->urlPattern,
+ 'data-default-url' => $context->defaultUrl,
+ 'onchange' => 'window.location.href = this.value == this.dataset.defaultPageSize ? this.dataset.defaultUrl : this.dataset.urlPattern.replace("' . PageSizeContext::URL_PLACEHOLDER . '", this.value)',
+ ]);
+
+ return Html::select()
+ ->optionsData($options, encode: false)
+ ->value($context->currentValue)
+ ->attributes($attributes)
+ ->render();
+ }
+}
diff --git a/src/UrlParametersFactory.php b/src/UrlParametersFactory.php
index 2047f26e4..a3174225e 100644
--- a/src/UrlParametersFactory.php
+++ b/src/UrlParametersFactory.php
@@ -18,7 +18,7 @@ final class UrlParametersFactory
*/
public static function create(
?PageToken $pageToken,
- int|null $pageSize,
+ int|string|null $pageSize,
string|null $sort,
UrlConfig $context,
): array {
diff --git a/tests/Support/SimpleUrlParameterProvider.php b/tests/Support/SimpleUrlParameterProvider.php
new file mode 100644
index 000000000..13d684202
--- /dev/null
+++ b/tests/Support/SimpleUrlParameterProvider.php
@@ -0,0 +1,23 @@
+parameters[$name] ?? null;
+ }
+}