ScrollPager.php
<?php
namespace kop\y2sp;
use kop\y2sp\assets\InfiniteAjaxScrollAsset;
use Yii;
use yii\base\InvalidConfigException;
use yii\base\Widget;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;
use yii\i18n\PhpMessageSource;
use yii\web\JsExpression;
use yii\web\View;
use yii\widgets\LinkPager;
/**
* ScrollPager turns your regular paginated page into an infinite scrolling page using AJAX.
*
* ScrollPager works with a [[Pagination]] object which specifies the totally number of pages and the current page number.
*
* <br>
* <i>Example usage:</i>
* <code>
* echo ListView::widget([
* 'dataProvider' => $dataProvider,
* 'itemOptions' => ['class' => 'item'],
* 'itemView' => '_item_view',
* 'pager' => ['class' => \kop\y2sp\ScrollPager::className()]
* ]);
* </code>
*
* This widget is using {@link http://infiniteajaxscroll.com/ JQuery Infinite Ajax Scroll plugin}.
*
* @link http://kop.github.io/yii2-scroll-pager Y2SP project page.
* @license https://github.com/kop/yii2-scroll-pager/blob/master/LICENSE.md MIT
*
* @author Ivan Koptiev <ivan.koptiev@codex.systems>
*/
class ScrollPager extends Widget
{
/**
* @const EXTENSION_TRIGGER IAS Extension "IASTriggerExtension".
*/
const EXTENSION_TRIGGER = 'IASTriggerExtension';
/**
* @const EXTENSION_SPINNER IAS Extension "IASSpinnerExtension".
*/
const EXTENSION_SPINNER = 'IASSpinnerExtension';
/**
* @const EXTENSION_NONE_LEFT IAS Extension "IASNoneLeftExtension".
*/
const EXTENSION_NONE_LEFT = 'IASNoneLeftExtension';
/**
* @const EXTENSION_PAGING IAS Extension "IASPagingExtension".
*/
const EXTENSION_PAGING = 'IASPagingExtension';
/**
* @const EXTENSION_HISTORY IAS Extension "IASHistoryExtension".
*/
const EXTENSION_HISTORY = 'IASHistoryExtension';
/**
* @var string $container Enter the selector of the element containing your items that you want to paginate.
*/
public $container = '.list-view';
/**
* @var string $item Enter the selector of the element that each item has.
* Make sure the elements are inside the container element.
*/
public $item = '.item';
/**
* @var string $paginationSelector Enter the selector of the element containing the pagination.
*/
public $paginationSelector = '.list-view .pagination';
/**
* @var string $next Enter the selector of the link element that links to the next page.
* The href attribute of this element will be used to get the items from the next page.
* Make sure there is only one(1) element that matches the selector.
*/
public $next = '.next a';
/**
* @var int $delay Minimal number of milliseconds to stay in a loading state.
*/
public $delay = 600;
/**
* @var int $thresholdMargin On default IAS starts loading new items when you scroll to the latest .item element.
* The negativeMargin will be added to the items' offset, giving you the ability to load new items earlier
* (please note that the margin is always transformed to a negative integer).
* <br><br>
* <i>For example:</i>
* <br>
* Setting a negativeMargin of 250 means that IAS will start loading 250 pixel before the last item has scrolled into view.
*/
public $negativeMargin = 10;
/**
* @var string $triggerText Text of trigger the link.
* Default: "Load more items".
*/
public $triggerText;
/**
* @var string $triggerTemplate Allows you to override the trigger html template.
*/
public $triggerTemplate = '<div class="ias-trigger" style="text-align: center; cursor: pointer;"><a>{text}</a></div>';
/**
* @var int $triggerOffset The number of pages which should load automatically.
* After that the trigger is shown for every subsequent page.
* <br><br>
* <i>For example:</i>
* <br>
* if you set the offset to 2, the pages 2 and 3 (page 1 is always shown) would load automatically and for every
* subsequent page the user has to press the trigger to load it.
*/
public $triggerOffset = 0;
/**
* @var string $spinnerSrc The src attribute of the spinner image.
*/
public $spinnerSrc;
/**
* @var string $spinnerTemplate Allows you to override the spinner html template.
*/
public $spinnerTemplate = '<div class="ias-spinner" style="text-align: center;"><img src="{src}"/></div>';
/**
* @var string $noneLeftText Text of the "nothing left" message.
* Default: "You reached the end".
*/
public $noneLeftText;
/**
* @var string $noneLeftTemplate Allows you to override the "nothing left" message html template.
*/
public $noneLeftTemplate = '<div class="ias-noneleft" style="text-align: center;">{text}</div>';
/**
* @var string $historyPrev Enter the selector of the link element that links to the previous page.
* The href attribute of this element will be used to get the items from the previous page.
* Make sure there is only one element that matches the selector.
*/
public $historyPrev = '.previous';
/**
* @var null $historyPrevText Text of the "load previous" message.
* Default: "Load previous items".
*/
public $historyPrevText = null;
/**
* @var string $historyPrevTemplate Allows you to override the "load previous" message html template.
*/
public $historyPrevTemplate = '<div class="ias-trigger ias-trigger-prev" style="text-align: center; cursor: pointer;"><a>{text}</a></div>';
/**
* @var string $overflowContainer A selector for "div" HTML element to use as an overflow container.
* @see http://infiniteajaxscroll.com/examples/overflow.html
*/
public $overflowContainer;
/**
* @var string|JsExpression $eventOnScroll Triggered when the visitors scrolls.
* @see http://infiniteajaxscroll.com/docs/events.html
*/
public $eventOnScroll;
/**
* @var string|JsExpression $eventOnLoad Triggered when a new url will be loaded from the server.
* @see http://infiniteajaxscroll.com/docs/events.html
*/
public $eventOnLoad;
/**
* @var string|JsExpression $eventOnLoaded Triggered after a new page was loaded from the server.
* @see http://infiniteajaxscroll.com/docs/events.html
*/
public $eventOnLoaded;
/**
* @var string|JsExpression $eventOnRender Triggered before new items will be rendered.
* @see http://infiniteajaxscroll.com/docs/events.html
*/
public $eventOnRender;
/**
* @var string|JsExpression $eventOnRendered Triggered after new items have rendered.
* Note: This event is only fired once.
* @see http://infiniteajaxscroll.com/docs/events.html
*/
public $eventOnRendered;
/**
* @var string|JsExpression $eventOnNoneLeft Triggered when there are no more pages left.
* @see http://infiniteajaxscroll.com/docs/events.html
*/
public $eventOnNoneLeft;
/**
* @var string|JsExpression $eventOnNext Triggered when the next page should be loaded.
* Happens before loading of the next page starts. With this event it is possible to cancel the loading of the next page.
* You can do this by returning false from your callback.
* @see http://infiniteajaxscroll.com/docs/events.html
*/
public $eventOnNext;
/**
* @var string|JsExpression $eventOnReady Triggered when IAS and all the extensions have been initialized.
* @see http://infiniteajaxscroll.com/docs/events.html
*/
public $eventOnReady;
/**
* @var string|JsExpression $eventOnPageChange Triggered when a used scroll to another page.
* @see http://infiniteajaxscroll.com/docs/extension-paging.html
*/
public $eventOnPageChange;
/**
* @var array $enabledExtensions The list of the enabled plugin extensions.
*/
public $enabledExtensions = [
self::EXTENSION_TRIGGER,
self::EXTENSION_SPINNER,
self::EXTENSION_NONE_LEFT,
self::EXTENSION_PAGING,
self::EXTENSION_HISTORY
];
/**
* @var \yii\data\Pagination The pagination object that this pager is associated with.
* You must set this property in order to make ScrollPager work.
*/
public $pagination;
/**
* @var array The options for yii\widgets\LinkPager.
*/
public $linkPager = [];
public $linkPagerOptions;
/**
* @var $linkPagerWrapper string Wrapper template for pagination.
*/
public $linkPagerWrapperTemplate = '{pager}';
/**
* Initializes the pager.
*/
public function init()
{
parent::init();
// Register translations source
Yii::$app->i18n->translations = ArrayHelper::merge(Yii::$app->i18n->translations, [
'kop\y2sp' => [
'class' => PhpMessageSource::className(),
'basePath' => '@vendor/kop/yii2-scroll-pager/messages',
'fileMap' => [
'kop\y2sp' => 'general.php'
]
]
]);
// Register required assets
$this->registerAssets();
// Set default trigger text if not set
if ($this->triggerText === null) {
$this->triggerText = Yii::t('kop\y2sp', 'Load more items');
}
// Set default "none left" message text if not set
if ($this->noneLeftText === null) {
$this->noneLeftText = Yii::t('kop\y2sp', 'You reached the end');
}
// Set default "load previous" message text if not set
if ($this->historyPrevText === null) {
$this->historyPrevText = Yii::t('kop\y2sp', 'Load previous items');
}
// Set default class for pagination
if ($this->linkPagerOptions === null) {
$this->linkPagerOptions = ['class' => 'pagination hidden'];
} elseif (!isset($this->linkPagerOptions['class'])) {
$this->linkPagerOptions['class'] = 'pagination hidden';
}
}
/**
* Executes the widget.
*
* This overrides the parent implementation by initializing jQuery IAS and displaying the generated page buttons.
*
* @return mixed
* @throws \yii\base\InvalidConfigException
*/
public function run()
{
// Initialize jQuery IAS plugin
$pluginSettings = Json::encode([
'container' => $this->container,
'item' => $this->item,
'pagination' => $this->paginationSelector,
'next' => $this->next,
'delay' => $this->delay,
'negativeMargin' => $this->negativeMargin
]);
$initString = empty($this->overflowContainer)
? "if(typeof window.{$this->id}_ias === 'object') { window.{$this->id}_ias.reinitialize() }
else { window.{$this->id}_ias = jQuery.ias({$pluginSettings}); };"
: "if(typeof window.{$this->id}_ias === 'object') { window.{$this->id}_ias.reinitialize() }
else { window.{$this->id}_ias = jQuery('{$this->overflowContainer}').ias({$pluginSettings}); };";
$this->view->registerJs($initString, View::POS_READY, "{$this->id}_ias_main");
// Register IAS extensions
$this->registerExtensions([
[
'name' => self::EXTENSION_PAGING
],
[
'name' => self::EXTENSION_SPINNER,
'options' =>
!empty($this->spinnerSrc)
? ['html' => $this->spinnerTemplate, 'src' => $this->spinnerSrc]
: ['html' => $this->spinnerTemplate]
],
[
'name' => self::EXTENSION_TRIGGER,
'options' => [
'text' => $this->triggerText,
'html' => $this->triggerTemplate,
'offset' => $this->triggerOffset,
'textPrev' => $this->historyPrevText,
'htmlPrev' => $this->historyPrevTemplate,
]
],
[
'name' => self::EXTENSION_NONE_LEFT,
'options' => [
'text' => $this->noneLeftText,
'html' => $this->noneLeftTemplate
]
],
[
'name' => self::EXTENSION_HISTORY,
'options' => [
'prev' => $this->historyPrev
],
'depends' => [
self::EXTENSION_TRIGGER,
self::EXTENSION_PAGING
]
]
]);
// Register event handlers
$this->registerEventHandlers([
'scroll' => [],
'load' => [],
'loaded' => [],
'render' => [],
'rendered' => [],
'noneLeft' => [],
'next' => [],
'ready' => [],
'pageChange' => [
self::EXTENSION_PAGING,
]
]);
// Render pagination links with wrapper
echo str_replace(
'{pager}',
LinkPager::widget([
'pagination' => $this->pagination,
'options' => $this->linkPagerOptions,
] + $this->linkPager),
$this->linkPagerWrapperTemplate
);
}
/**
* Register required asset bundles.
*
* You can override this method in case if you want to use your own JQuery Infinite Ajax Scroll plugin files
* (for example, some forked plugin version).
*/
protected function registerAssets()
{
InfiniteAjaxScrollAsset::register($this->view);
}
/**
* Register jQuery IAS extensions.
*
* This method takes jQuery IAS extensions definition as a parameter and registers this extensions.
*
* @param array $config jQuery IAS extensions definition.
* @throws \yii\base\InvalidConfigException If extension dependencies are not met.
*/
protected function registerExtensions(array $config)
{
foreach ($config as $entry) {
// Parse config entry values
$name = ArrayHelper::getValue($entry, 'name', false);
$options = ArrayHelper::getValue($entry, 'options', '');
$depends = ArrayHelper::getValue($entry, 'depends', []);
// If extension is enabled
if (in_array($name, $this->enabledExtensions)) {
// Make sure dependencies are met
if (!$this->checkEnabledExtensions($depends)) {
throw new InvalidConfigException(
"Extension {$name} requires " . implode(', ', $depends) . " extensions to be enabled."
);
}
$this->view->registerAssetBundle("kop\y2sp\assets\\{$name}Asset");
// Register extension
$options = Json::encode($options);
$this->view->registerJs(<<<JS
;(function() {
if((window.{$this->id}_ias.extensions.map(function(item) {return item.constructor.name;}).indexOf('$name')) === -1) {
// prevent duplicate plugin registration
window.{$this->id}_ias.extension(new $name($options));
};
}
)();
JS
,
View::POS_READY,
"{$this->id}_ias_{$name}"
);
}
}
}
/**
* Register jQuery IAS event handlers.
*
* This method takes jQuery IAS event handlers definition as a parameter and registers this event handlers.
*
* @param array $config jQuery IAS event handlers definition.
* @throws \yii\base\InvalidConfigException If vent handlers dependencies are not met.
*/
protected function registerEventHandlers(array $config)
{
foreach ($config as $name => $depends) {
// If event is enabled
$eventName = 'eventOn' . ucfirst($name);
if (!empty($this->$eventName)) {
// Make sure dependencies are met
if (!$this->checkEnabledExtensions($depends)) {
throw new InvalidConfigException(
"The \"{$name}\" event requires " . implode(', ', $depends) . " extensions to be enabled."
);
}
// Replace the variable template
$callback = str_replace('{{ias}}', "{$this->id}_ias", $this->$eventName);
// Register event
$this->view->registerJs(
"window.{$this->id}_ias.on('{$name}', {$callback});",
View::POS_READY,
"{$this->id}_ias_event_{$eventName}"
);
}
}
}
/**
* Check whether the given extensions are enabled.
*
* @param string|array $extensions Single or multiple extensions names.
* @return bool Operation result.
*/
protected function checkEnabledExtensions($extensions)
{
$extensions = (array)$extensions;
if (empty($extensions)) {
return true;
} else {
return (count(array_intersect($this->enabledExtensions, $extensions)) == count($extensions));
}
}
}