pager.inc 22.1 KB
Newer Older
Dries's avatar
Dries committed
1 2
<?php

3 4 5
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectExtender;
use Drupal\Core\Database\Query\SelectInterface;
6

7
/**
8 9
 * @file
 * Functions to aid in presenting database results as a set of pages.
10
 */
11

12 13 14 15 16 17
/**
 * Query extender for pager queries.
 *
 * This is the "default" pager mechanism.  It creates a paged query with a fixed
 * number of entries per page.
 */
18
class PagerDefault extends SelectExtender {
19 20 21 22 23 24

  /**
   * The highest element we've autogenerated so far.
   *
   * @var int
   */
25
  static $maxElement = 0;
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

  /**
   * The number of elements per page to allow.
   *
   * @var int
   */
  protected $limit = 10;

  /**
   * The unique ID of this pager on this page.
   *
   * @var int
   */
  protected $element = NULL;

  /**
   * The count query that will be used for this pager.
   *
   * @var SelectQueryInterface
   */
  protected $customCountQuery = FALSE;

48
  public function __construct(SelectInterface $query, Connection $connection) {
49 50 51 52 53 54 55
    parent::__construct($query, $connection);

    // Add pager tag. Do this here to ensure that it is always added before
    // preExecute() is called.
    $this->addTag('pager');
  }

56 57 58 59 60 61 62 63
  /**
   * Override the execute method.
   *
   * Before we run the query, we need to add pager-based range() instructions
   * to it.
   */
  public function execute() {

64 65 66 67 68 69 70
    // Add convenience tag to mark that this is an extended query. We have to
    // do this in the constructor to ensure that it is set before preExecute()
    // gets called.
    if (!$this->preExecute($this)) {
      return NULL;
    }

71 72 73 74 75 76
    // A NULL limit is the "kill switch" for pager queries.
    if (empty($this->limit)) {
      return;
    }
    $this->ensureElement();

77 78 79
    $total_items = $this->getCountQuery()->execute()->fetchField();
    $current_page = pager_default_initialize($total_items, $this->limit, $this->element);
    $this->range($current_page * $this->limit, $this->limit);
80 81 82 83 84 85 86

    // Now that we've added our pager-based range instructions, run the query normally.
    return $this->query->execute();
  }

  /**
   * Ensure that there is an element associated with this query.
87 88
   * If an element was not specified previously, then the value of the
   * $maxElement counter is taken, after which the counter is incremented.
89
   *
90
   * After running this method, access $this->element to get the element for this
91 92 93
   * query.
   */
  protected function ensureElement() {
94 95
    if (!isset($this->element)) {
      $this->element = self::$maxElement++;
96 97 98 99 100 101 102 103 104 105 106 107 108
    }
  }

  /**
   * Specify the count query object to use for this pager.
   *
   * You will rarely need to specify a count query directly.  If not specified,
   * one is generated off of the pager query itself.
   *
   * @param SelectQueryInterface $query
   *   The count query object.  It must return a single row with a single column,
   *   which is the total number of records.
   */
109
  public function setCountQuery(SelectInterface $query) {
110 111 112 113 114 115 116 117 118
    $this->customCountQuery = $query;
  }

  /**
   * Retrieve the count query for this pager.
   *
   * The count query may be specified manually or, by default, taken from the
   * query we are extending.
   *
119 120
   * @return SelectQueryInterface
   *   A count query object.
121
   */
122
  public function getCountQuery() {
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
    if ($this->customCountQuery) {
      return $this->customCountQuery;
    }
    else {
      return $this->query->countQuery();
    }
  }

  /**
   * Specify the maximum number of elements per page for this query.
   *
   * The default if not specified is 10 items per page.
   *
   * @param $limit
   *   An integer specifying the number of elements per page.  If passed a false
   *   value (FALSE, 0, NULL), the pager is disabled.
   */
  public function limit($limit = 10) {
    $this->limit = $limit;
    return $this;
  }

  /**
   * Specify the element ID for this pager query.
   *
   * The element is used to differentiate different pager queries on the same
   * page so that they may be operated independently.  If you do not specify an
   * element, every pager query on the page will get a unique element.  If for
   * whatever reason you want to explicitly define an element for a given query,
   * you may do so here.
   *
154 155 156 157 158 159
   * Setting the element here also increments the static $maxElement counter,
   * which is used for determining the $element when there's none specified.
   *
   * Note that no collision detection is done when setting an element ID
   * explicitly, so it is possible for two pagers to end up using the same ID
   * if both are set explicitly.
160
   *
161 162 163 164
   * @param $element
   */
  public function element($element) {
    $this->element = $element;
165 166 167
    if ($element >= self::$maxElement) {
      self::$maxElement = $element + 1;
    }
168 169 170 171
    return $this;
  }
}

172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
/**
 * Returns the current page being requested for display within a pager.
 *
 * @param $element
 *  An optional integer to distinguish between multiple pagers on one page.
 *
 * @return
 *  The number of the current requested page, within the pager represented by
 *  $element. This is determined from the URL query parameter $_GET['page'], or
 *  0 by default. Note that this number may differ from the actual page being
 *  displayed. For example, if a search for "example text" brings up three
 *  pages of results, but a users visits search/node/example+text?page=10, this
 *  function will return 10, even though the default pager implementation
 *  adjusts for this and still displays the third page of search results at
 *  that URL.
 *
 * @see pager_default_initialize()
 */
function pager_find_page($element = 0) {
  $page = isset($_GET['page']) ? $_GET['page'] : '';
  $page_array = explode(',', $page);
  if (!isset($page_array[$element])) {
    $page_array[$element] = 0;
  }
  return (int) $page_array[$element];
}

/**
 * Initializes a pager for theme('pager').
 *
 * This function sets up the necessary global variables so that future calls
 * to theme('pager') will render a pager that correctly corresponds to the
 * items being displayed.
 *
206
 * If the items being displayed result from a database query performed using
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
 * Drupal's database API, and if you have control over the construction of the
 * database query, you do not need to call this function directly; instead, you
 * can simply extend the query object with the 'PagerDefault' extender before
 * executing it. For example:
 * @code
 *   $query = db_select('some_table')->extend('PagerDefault');
 * @endcode
 *
 * However, if you are using a different method for generating the items to be
 * paged through, then you should call this function in preparation.
 *
 * The following example shows how this function can be used in a page callback
 * that invokes an external datastore with an SQL-like syntax:
 * @code
 *   // First find the total number of items and initialize the pager.
 *   $where = "status = 1";
 *   $total = mymodule_select("SELECT COUNT(*) FROM data " . $where)->result();
 *   $num_per_page = variable_get('mymodule_num_per_page', 10);
 *   $page = pager_default_initialize($total, $num_per_page);
 *
 *   // Next, retrieve and display the items for the current page.
 *   $offset = $num_per_page * $page;
 *   $result = mymodule_select("SELECT * FROM data " . $where . " LIMIT %d, %d", $offset, $num_per_page)->fetchAll();
 *   $output = theme('mymodule_results', array('result' => $result));
 *
 *   // Finally, display the pager controls, and return.
 *   $output .= theme('pager');
 *   return $output;
 * @endcode
 *
 * A second example involves a page callback that invokes an external search
 * service where the total number of matching results is provided as part of
 * the returned set (so that we do not need a separate query in order to obtain
 * this information). Here, we call pager_find_page() to calculate the desired
 * offset before the search is invoked:
 * @code
 *   // Perform the query, using the requested offset from pager_find_page().
 *   // This comes from a URL parameter, so here we are assuming that the URL
 *   // parameter corresponds to an actual page of results that will exist
 *   // within the set.
 *   $page = pager_find_page();
 *   $num_per_page = variable_get('mymodule_num_per_page', 10);
 *   $offset = $num_per_page * $page;
 *   $result = mymodule_remote_search($keywords, $offset, $num_per_page);
 *
 *   // Now that we have the total number of results, initialize the pager.
 *   pager_default_initialize($result->total, $num_per_page);
 *
 *   // Display the search results.
 *   $output = theme('search_results', array('results' => $result->data, 'type' => 'remote'));
 *
 *   // Finally, display the pager controls, and return.
 *   $output .= theme('pager');
 *   return $output;
 * @endcode
 *
 * @param $total
 *  The total number of items to be paged.
 * @param $limit
 *  The number of items the calling code will display per page.
 * @param $element
 *  An optional integer to distinguish between multiple pagers on one page.
 *
 * @return
 *   The number of the current page, within the pager represented by $element.
 *   This is determined from the URL query parameter $_GET['page'], or 0 by
 *   default. However, if a page that does not correspond to the actual range
 *   of the result set was requested, this function will return the closest
 *   page actually within the result set.
 */
function pager_default_initialize($total, $limit, $element = 0) {
  global $pager_page_array, $pager_total, $pager_total_items, $pager_limits;

  $page = pager_find_page($element);

  // We calculate the total of pages as ceil(items / limit).
  $pager_total_items[$element] = $total;
  $pager_total[$element] = ceil($pager_total_items[$element] / $limit);
  $pager_page_array[$element] = max(0, min($page, ((int) $pager_total[$element]) - 1));
  $pager_limits[$element] = $limit;
  return $pager_page_array[$element];
}

290
/**
291
 * Compose a URL query parameter array for pager links.
292 293
 *
 * @return
294 295
 *   A URL query parameter array that consists of all components of the current
 *   page request except for those pertaining to paging.
296
 */
297 298 299
function pager_get_query_parameters() {
  $query = &drupal_static(__FUNCTION__);
  if (!isset($query)) {
300
    $query = drupal_get_query_parameters($_GET, array('q', 'page'));
301
  }
302
  return $query;
303 304
}

305
/**
306
 * Returns HTML for a query pager.
307 308
 *
 * Menu callbacks that display paged query results should call theme('pager') to
309 310
 * retrieve a pager control so that users can view other results. Format a list
 * of nearby pages with additional query results.
311
 *
312 313 314 315 316 317 318 319 320
 * @param $variables
 *   An associative array containing:
 *   - tags: An array of labels for the controls in the pager.
 *   - element: An optional integer to distinguish between multiple pagers on
 *     one page.
 *   - parameters: An associative array of query string parameters to append to
 *     the pager links.
 *   - quantity: The number of pages in the list.
 *
321
 * @ingroup themeable
322
 */
323 324 325 326 327
function theme_pager($variables) {
  $tags = $variables['tags'];
  $element = $variables['element'];
  $parameters = $variables['parameters'];
  $quantity = $variables['quantity'];
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
  global $pager_page_array, $pager_total;

  // Calculate various markers within this pager piece:
  // Middle is used to "center" pages around the current page.
  $pager_middle = ceil($quantity / 2);
  // current is the page we are currently paged to
  $pager_current = $pager_page_array[$element] + 1;
  // first is the first page listed by this pager piece (re quantity)
  $pager_first = $pager_current - $pager_middle + 1;
  // last is the last page listed by this pager piece (re quantity)
  $pager_last = $pager_current + $quantity - $pager_middle;
  // max is the maximum page number
  $pager_max = $pager_total[$element];
  // End of marker calculations.

  // Prepare for generation loop.
  $i = $pager_first;
  if ($pager_last > $pager_max) {
    // Adjust "center" if at end of query.
    $i = $i + ($pager_max - $pager_last);
    $pager_last = $pager_max;
  }
  if ($i <= 0) {
    // Adjust "center" if at start of query.
    $pager_last = $pager_last + (1 - $i);
    $i = 1;
  }
  // End of generation loop preparation.

357 358 359 360
  $li_first = theme('pager_first', array('text' => (isset($tags[0]) ? $tags[0] : t('« first')), 'element' => $element, 'parameters' => $parameters));
  $li_previous = theme('pager_previous', array('text' => (isset($tags[1]) ? $tags[1] : t('‹ previous')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters));
  $li_next = theme('pager_next', array('text' => (isset($tags[3]) ? $tags[3] : t('next ›')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters));
  $li_last = theme('pager_last', array('text' => (isset($tags[4]) ? $tags[4] : t('last »')), 'element' => $element, 'parameters' => $parameters));
361

362
  if ($pager_total[$element] > 1) {
363 364
    if ($li_first) {
      $items[] = array(
365
        'class' => array('pager-first'),
366 367 368 369 370
        'data' => $li_first,
      );
    }
    if ($li_previous) {
      $items[] = array(
371
        'class' => array('pager-previous'),
372 373 374 375 376 377 378 379
        'data' => $li_previous,
      );
    }

    // When there is more than one page, create the pager list.
    if ($i != $pager_max) {
      if ($i > 1) {
        $items[] = array(
380
          'class' => array('pager-ellipsis'),
381 382 383 384 385 386 387
          'data' => '…',
        );
      }
      // Now generate the actual pager piece.
      for (; $i <= $pager_last && $i <= $pager_max; $i++) {
        if ($i < $pager_current) {
          $items[] = array(
388
            'class' => array('pager-item'),
389
            'data' => theme('pager_previous', array('text' => $i, 'element' => $element, 'interval' => ($pager_current - $i), 'parameters' => $parameters)),
390 391 392 393
          );
        }
        if ($i == $pager_current) {
          $items[] = array(
394
            'class' => array('pager-current'),
395 396 397 398 399
            'data' => $i,
          );
        }
        if ($i > $pager_current) {
          $items[] = array(
400
            'class' => array('pager-item'),
401
            'data' => theme('pager_next', array('text' => $i, 'element' => $element, 'interval' => ($i - $pager_current), 'parameters' => $parameters)),
402 403 404 405 406
          );
        }
      }
      if ($i < $pager_max) {
        $items[] = array(
407
          'class' => array('pager-ellipsis'),
408 409 410 411 412 413 414
          'data' => '…',
        );
      }
    }
    // End generation.
    if ($li_next) {
      $items[] = array(
415
        'class' => array('pager-next'),
416 417 418 419 420
        'data' => $li_next,
      );
    }
    if ($li_last) {
      $items[] = array(
421
        'class' => array('pager-last'),
422 423 424
        'data' => $li_last,
      );
    }
425 426 427 428
    return '<h2 class="element-invisible">' . t('Pages') . '</h2>' . theme('item_list', array(
      'items' => $items,
      'attributes' => array('class' => array('pager')),
    ));
Dries's avatar
Dries committed
429
  }
Dries's avatar
Dries committed
430
}
431

432

433
/**
434
 * @defgroup pagerpieces Pager pieces
435
 * @{
436
 * Theme functions for customizing pager elements.
437 438
 *
 * Note that you should NOT modify this file to customize your pager.
439
 */
Dries's avatar
Dries committed
440

441
/**
442
 * Returns HTML for the "first page" link in a query pager.
443
 *
444 445 446 447 448 449 450 451
 * @param $variables
 *   An associative array containing:
 *   - text: The name (or image) of the link.
 *   - element: An optional integer to distinguish between multiple pagers on
 *     one page.
 *   - parameters: An associative array of query string parameters to append to
 *     the pager links.
 *
452
 * @ingroup themeable
453
 */
454 455 456 457
function theme_pager_first($variables) {
  $text = $variables['text'];
  $element = $variables['element'];
  $parameters = $variables['parameters'];
458
  global $pager_page_array;
459
  $output = '';
Dries's avatar
Dries committed
460

461 462
  // If we are anywhere but the first page
  if ($pager_page_array[$element] > 0) {
463
    $output = theme('pager_link', array('text' => $text, 'page_new' => pager_load_array(0, $element, $pager_page_array), 'element' => $element, 'parameters' => $parameters));
Dries's avatar
Dries committed
464
  }
465

466
  return $output;
Dries's avatar
Dries committed
467 468
}

469
/**
470
 * Returns HTML for the "previous page" link in a query pager.
471
 *
472 473 474 475 476 477 478 479 480
 * @param $variables
 *   An associative array containing:
 *   - text: The name (or image) of the link.
 *   - element: An optional integer to distinguish between multiple pagers on
 *     one page.
 *   - interval: The number of pages to move backward when the link is clicked.
 *   - parameters: An associative array of query string parameters to append to
 *     the pager links.
 *
481
 * @ingroup themeable
482
 */
483 484 485 486 487
function theme_pager_previous($variables) {
  $text = $variables['text'];
  $element = $variables['element'];
  $interval = $variables['interval'];
  $parameters = $variables['parameters'];
488
  global $pager_page_array;
489 490
  $output = '';

491 492 493
  // If we are anywhere but the first page
  if ($pager_page_array[$element] > 0) {
    $page_new = pager_load_array($pager_page_array[$element] - $interval, $element, $pager_page_array);
494

495 496
    // If the previous page is the first page, mark the link as such.
    if ($page_new[$element] == 0) {
497
      $output = theme('pager_first', array('text' => $text, 'element' => $element, 'parameters' => $parameters));
498 499 500
    }
    // The previous page is not the first page.
    else {
501
      $output = theme('pager_link', array('text' => $text, 'page_new' => $page_new, 'element' => $element, 'parameters' => $parameters));
502
    }
Dries's avatar
Dries committed
503
  }
504

505
  return $output;
Dries's avatar
Dries committed
506 507
}

508
/**
509
 * Returns HTML for the "next page" link in a query pager.
510
 *
511 512 513 514 515 516 517 518 519
 * @param $variables
 *   An associative array containing:
 *   - text: The name (or image) of the link.
 *   - element: An optional integer to distinguish between multiple pagers on
 *     one page.
 *   - interval: The number of pages to move forward when the link is clicked.
 *   - parameters: An associative array of query string parameters to append to
 *     the pager links.
 *
520
 * @ingroup themeable
521
 */
522 523 524 525 526
function theme_pager_next($variables) {
  $text = $variables['text'];
  $element = $variables['element'];
  $interval = $variables['interval'];
  $parameters = $variables['parameters'];
527
  global $pager_page_array, $pager_total;
528 529
  $output = '';

530 531 532 533 534
  // If we are anywhere but the last page
  if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
    $page_new = pager_load_array($pager_page_array[$element] + $interval, $element, $pager_page_array);
    // If the next page is the last page, mark the link as such.
    if ($page_new[$element] == ($pager_total[$element] - 1)) {
535
      $output = theme('pager_last', array('text' => $text, 'element' => $element, 'parameters' => $parameters));
536 537 538
    }
    // The next page is not the last page.
    else {
539
      $output = theme('pager_link', array('text' => $text, 'page_new' => $page_new, 'element' => $element, 'parameters' => $parameters));
540
    }
541
  }
542

543
  return $output;
Dries's avatar
Dries committed
544 545
}

546
/**
547
 * Returns HTML for the "last page" link in query pager.
548
 *
549 550 551 552 553 554 555 556
 * @param $variables
 *   An associative array containing:
 *   - text: The name (or image) of the link.
 *   - element: An optional integer to distinguish between multiple pagers on
 *     one page.
 *   - parameters: An associative array of query string parameters to append to
 *     the pager links.
 *
557
 * @ingroup themeable
558
 */
559 560 561 562
function theme_pager_last($variables) {
  $text = $variables['text'];
  $element = $variables['element'];
  $parameters = $variables['parameters'];
563
  global $pager_page_array, $pager_total;
564
  $output = '';
Dries's avatar
Dries committed
565

566 567
  // If we are anywhere but the last page
  if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
568
    $output = theme('pager_link', array('text' => $text, 'page_new' => pager_load_array($pager_total[$element] - 1, $element, $pager_page_array), 'element' => $element, 'parameters' => $parameters));
569
  }
570

571
  return $output;
Dries's avatar
Dries committed
572 573 574
}


575
/**
576
 * Returns HTML for a link to a specific query result page.
577
 *
578 579
 * @param $variables
 *   An associative array containing:
580 581 582 583 584
 *   - text: The link text. Also used to figure out the title attribute of the
 *     link, if it is not provided in $variables['attributes']['title']; in
 *     this case, $variables['text'] must be one of the standard pager link
 *     text strings that would be generated by the pager theme functions, such
 *     as a number or t('« first').
585 586 587 588 589
 *   - page_new: The first result to display on the linked page.
 *   - element: An optional integer to distinguish between multiple pagers on
 *     one page.
 *   - parameters: An associative array of query string parameters to append to
 *     the pager link.
590 591 592 593
 *   - attributes: An associative array of HTML attributes to apply to the
 *     pager link.
 *
 * @see theme_pager()
594
 *
595
 * @ingroup themeable
596
 */
597 598 599 600 601 602 603
function theme_pager_link($variables) {
  $text = $variables['text'];
  $page_new = $variables['page_new'];
  $element = $variables['element'];
  $parameters = $variables['parameters'];
  $attributes = $variables['attributes'];

604 605
  $page = isset($_GET['page']) ? $_GET['page'] : '';
  if ($new_page = implode(',', pager_load_array($page_new[$element], $element, explode(',', $page)))) {
606
    $parameters['page'] = $new_page;
607
  }
608

609
  $query = array();
610
  if (count($parameters)) {
611
    $query = drupal_get_query_parameters($parameters, array());
612
  }
613 614
  if ($query_pager = pager_get_query_parameters()) {
    $query = array_merge($query, $query_pager);
Dries's avatar
Dries committed
615
  }
616

617
  // Set each pager link title
618
  if (!isset($attributes['title'])) {
619
    static $titles = NULL;
620 621 622 623 624 625 626 627 628 629 630
    if (!isset($titles)) {
      $titles = array(
        t('« first') => t('Go to first page'),
        t('‹ previous') => t('Go to previous page'),
        t('next ›') => t('Go to next page'),
        t('last »') => t('Go to last page'),
      );
    }
    if (isset($titles[$text])) {
      $attributes['title'] = $titles[$text];
    }
631
    elseif (is_numeric($text)) {
632
      $attributes['title'] = t('Go to page @number', array('@number' => $text));
633 634
    }
  }
635

636 637 638 639 640 641 642
  // @todo l() cannot be used here, since it adds an 'active' class based on the
  //   path only (which is always the current path for pager links). Apparently,
  //   none of the pager links is active at any time - but it should still be
  //   possible to use l() here.
  // @see http://drupal.org/node/1410574
  $attributes['href'] = url($_GET['q'], array('query' => $query));
  return '<a' . drupal_attributes($attributes) . '>' . check_plain($text) . '</a>';
Dries's avatar
Dries committed
643
}
644

645 646 647
/**
 * @} End of "Pager pieces".
 */
Dries's avatar
Dries committed
648

649 650 651 652 653 654
/**
 * Helper function
 *
 * Copies $old_array to $new_array and sets $new_array[$element] = $value
 * Fills in $new_array[0 .. $element - 1] = 0
 */
Dries's avatar
Dries committed
655 656
function pager_load_array($value, $element, $old_array) {
  $new_array = $old_array;
657
  // Look for empty elements.
Dries's avatar
Dries committed
658
  for ($i = 0; $i < $element; $i++) {
659
    if (empty($new_array[$i])) {
660
      // Load found empty element with 0.
Dries's avatar
Dries committed
661 662 663
      $new_array[$i] = 0;
    }
  }
664
  // Update the changed element.
665
  $new_array[$element] = (int) $value;
Dries's avatar
Dries committed
666 667
  return $new_array;
}