pagination.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.97
  5. */
  6. import { h, Fragment } from "@stencil/core";
  7. import { connectLocalized, disconnectLocalized, numberStringFormatter } from "../../utils/locale";
  8. import { CSS, TEXT } from "./resources";
  9. const maxPagesDisplayed = 5;
  10. export class Pagination {
  11. constructor() {
  12. //--------------------------------------------------------------------------
  13. //
  14. // Public Properties
  15. //
  16. //--------------------------------------------------------------------------
  17. /**
  18. * When `true`, number values are displayed with a group separator corresponding to the language and country format.
  19. */
  20. this.groupSeparator = false;
  21. /** Specifies the number of items per page. */
  22. this.num = 20;
  23. /** Specifies the starting item number. */
  24. this.start = 1;
  25. /** Specifies the total number of items. */
  26. this.total = 0;
  27. /**
  28. * Accessible name for the component's next button.
  29. *
  30. * @default "Next"
  31. */
  32. this.textLabelNext = TEXT.nextLabel;
  33. /**
  34. * Accessible name for the component's previous button.
  35. *
  36. * @default "Previous"
  37. */
  38. this.textLabelPrevious = TEXT.previousLabel;
  39. /** Specifies the size of the component. */
  40. this.scale = "m";
  41. //--------------------------------------------------------------------------
  42. //
  43. // State
  44. //
  45. //--------------------------------------------------------------------------
  46. this.effectiveLocale = "";
  47. this.previousClicked = () => {
  48. this.previousPage().then();
  49. this.emitUpdate();
  50. };
  51. this.nextClicked = () => {
  52. this.nextPage();
  53. this.emitUpdate();
  54. };
  55. /**
  56. * Returns a string representing the localized label value based on groupSeparator prop being on or off.
  57. *
  58. * @param value
  59. */
  60. this.determineGroupSeparator = (value) => {
  61. numberStringFormatter.numberFormatOptions = {
  62. locale: this.effectiveLocale,
  63. numberingSystem: this.numberingSystem,
  64. useGrouping: this.groupSeparator
  65. };
  66. return this.groupSeparator
  67. ? numberStringFormatter.localize(value.toString())
  68. : value.toString();
  69. };
  70. }
  71. // --------------------------------------------------------------------------
  72. //
  73. // Lifecycle
  74. //
  75. // --------------------------------------------------------------------------
  76. connectedCallback() {
  77. connectLocalized(this);
  78. }
  79. disconnectedCallback() {
  80. disconnectLocalized(this);
  81. }
  82. // --------------------------------------------------------------------------
  83. //
  84. // Public Methods
  85. //
  86. // --------------------------------------------------------------------------
  87. /** Go to the next page of results. */
  88. async nextPage() {
  89. this.start = Math.min(this.getLastStart(), this.start + this.num);
  90. }
  91. /** Go to the previous page of results. */
  92. async previousPage() {
  93. this.start = Math.max(1, this.start - this.num);
  94. }
  95. // --------------------------------------------------------------------------
  96. //
  97. // Private Methods
  98. //
  99. // --------------------------------------------------------------------------
  100. getLastStart() {
  101. const { total, num } = this;
  102. const lastStart = total % num === 0 ? total - num : Math.floor(total / num) * num;
  103. return lastStart + 1;
  104. }
  105. showLeftEllipsis() {
  106. return Math.floor(this.start / this.num) > 3;
  107. }
  108. showRightEllipsis() {
  109. return (this.total - this.start) / this.num > 3;
  110. }
  111. emitUpdate() {
  112. const changePayload = {
  113. start: this.start,
  114. total: this.total,
  115. num: this.num
  116. };
  117. this.calcitePaginationChange.emit(changePayload);
  118. this.calcitePaginationUpdate.emit(changePayload);
  119. }
  120. //--------------------------------------------------------------------------
  121. //
  122. // Render Methods
  123. //
  124. //--------------------------------------------------------------------------
  125. renderPages() {
  126. const lastStart = this.getLastStart();
  127. let end;
  128. let nextStart;
  129. // if we don't need ellipses render the whole set
  130. if (this.total / this.num <= maxPagesDisplayed) {
  131. nextStart = 1 + this.num;
  132. end = lastStart - this.num;
  133. }
  134. else {
  135. // if we're within max pages of page 1
  136. if (this.start / this.num < maxPagesDisplayed - 1) {
  137. nextStart = 1 + this.num;
  138. end = 1 + 4 * this.num;
  139. }
  140. else {
  141. // if we're within max pages of last page
  142. if (this.start + 3 * this.num >= this.total) {
  143. nextStart = lastStart - 4 * this.num;
  144. end = lastStart - this.num;
  145. }
  146. else {
  147. nextStart = this.start - this.num;
  148. end = this.start + this.num;
  149. }
  150. }
  151. }
  152. const pages = [];
  153. while (nextStart <= end) {
  154. pages.push(nextStart);
  155. nextStart = nextStart + this.num;
  156. }
  157. return pages.map((page) => this.renderPage(page));
  158. }
  159. renderPage(start) {
  160. const page = Math.floor(start / this.num) + (this.num === 1 ? 0 : 1);
  161. const displayedPage = this.determineGroupSeparator(page);
  162. return (h("button", { class: {
  163. [CSS.page]: true,
  164. [CSS.selected]: start === this.start
  165. }, onClick: () => {
  166. this.start = start;
  167. this.emitUpdate();
  168. } }, displayedPage));
  169. }
  170. renderLeftEllipsis() {
  171. if (this.total / this.num > maxPagesDisplayed && this.showLeftEllipsis()) {
  172. return h("span", { class: `${CSS.ellipsis} ${CSS.ellipsisStart}` }, "\u2026");
  173. }
  174. }
  175. renderRightEllipsis() {
  176. if (this.total / this.num > maxPagesDisplayed && this.showRightEllipsis()) {
  177. return h("span", { class: `${CSS.ellipsis} ${CSS.ellipsisEnd}` }, "\u2026");
  178. }
  179. }
  180. render() {
  181. const { total, num, start } = this;
  182. const prevDisabled = num === 1 ? start <= num : start < num;
  183. const nextDisabled = num === 1 ? start + num > total : start + num > total;
  184. return (h(Fragment, null, h("button", { "aria-label": this.textLabelPrevious, class: {
  185. [CSS.previous]: true,
  186. [CSS.disabled]: prevDisabled
  187. }, disabled: prevDisabled, onClick: this.previousClicked }, h("calcite-icon", { flipRtl: true, icon: "chevronLeft", scale: "s" })), total > num ? this.renderPage(1) : null, this.renderLeftEllipsis(), this.renderPages(), this.renderRightEllipsis(), this.renderPage(this.getLastStart()), h("button", { "aria-label": this.textLabelNext, class: {
  188. [CSS.next]: true,
  189. [CSS.disabled]: nextDisabled
  190. }, disabled: nextDisabled, onClick: this.nextClicked }, h("calcite-icon", { flipRtl: true, icon: "chevronRight", scale: "s" }))));
  191. }
  192. static get is() { return "calcite-pagination"; }
  193. static get encapsulation() { return "shadow"; }
  194. static get originalStyleUrls() {
  195. return {
  196. "$": ["pagination.scss"]
  197. };
  198. }
  199. static get styleUrls() {
  200. return {
  201. "$": ["pagination.css"]
  202. };
  203. }
  204. static get properties() {
  205. return {
  206. "groupSeparator": {
  207. "type": "boolean",
  208. "mutable": false,
  209. "complexType": {
  210. "original": "boolean",
  211. "resolved": "boolean",
  212. "references": {}
  213. },
  214. "required": false,
  215. "optional": false,
  216. "docs": {
  217. "tags": [],
  218. "text": "When `true`, number values are displayed with a group separator corresponding to the language and country format."
  219. },
  220. "attribute": "group-separator",
  221. "reflect": true,
  222. "defaultValue": "false"
  223. },
  224. "num": {
  225. "type": "number",
  226. "mutable": false,
  227. "complexType": {
  228. "original": "number",
  229. "resolved": "number",
  230. "references": {}
  231. },
  232. "required": false,
  233. "optional": false,
  234. "docs": {
  235. "tags": [],
  236. "text": "Specifies the number of items per page."
  237. },
  238. "attribute": "num",
  239. "reflect": true,
  240. "defaultValue": "20"
  241. },
  242. "numberingSystem": {
  243. "type": "string",
  244. "mutable": false,
  245. "complexType": {
  246. "original": "NumberingSystem",
  247. "resolved": "\"arab\" | \"arabext\" | \"bali\" | \"beng\" | \"deva\" | \"fullwide\" | \"gujr\" | \"guru\" | \"hanidec\" | \"khmr\" | \"knda\" | \"laoo\" | \"latn\" | \"limb\" | \"mlym\" | \"mong\" | \"mymr\" | \"orya\" | \"tamldec\" | \"telu\" | \"thai\" | \"tibt\"",
  248. "references": {
  249. "NumberingSystem": {
  250. "location": "import",
  251. "path": "../../utils/locale"
  252. }
  253. }
  254. },
  255. "required": false,
  256. "optional": true,
  257. "docs": {
  258. "tags": [],
  259. "text": "Specifies the Unicode numeral system used by the component for localization."
  260. },
  261. "attribute": "numbering-system",
  262. "reflect": false
  263. },
  264. "start": {
  265. "type": "number",
  266. "mutable": true,
  267. "complexType": {
  268. "original": "number",
  269. "resolved": "number",
  270. "references": {}
  271. },
  272. "required": false,
  273. "optional": false,
  274. "docs": {
  275. "tags": [],
  276. "text": "Specifies the starting item number."
  277. },
  278. "attribute": "start",
  279. "reflect": true,
  280. "defaultValue": "1"
  281. },
  282. "total": {
  283. "type": "number",
  284. "mutable": false,
  285. "complexType": {
  286. "original": "number",
  287. "resolved": "number",
  288. "references": {}
  289. },
  290. "required": false,
  291. "optional": false,
  292. "docs": {
  293. "tags": [],
  294. "text": "Specifies the total number of items."
  295. },
  296. "attribute": "total",
  297. "reflect": true,
  298. "defaultValue": "0"
  299. },
  300. "textLabelNext": {
  301. "type": "string",
  302. "mutable": false,
  303. "complexType": {
  304. "original": "string",
  305. "resolved": "string",
  306. "references": {}
  307. },
  308. "required": false,
  309. "optional": false,
  310. "docs": {
  311. "tags": [{
  312. "name": "default",
  313. "text": "\"Next\""
  314. }],
  315. "text": "Accessible name for the component's next button."
  316. },
  317. "attribute": "text-label-next",
  318. "reflect": false,
  319. "defaultValue": "TEXT.nextLabel"
  320. },
  321. "textLabelPrevious": {
  322. "type": "string",
  323. "mutable": false,
  324. "complexType": {
  325. "original": "string",
  326. "resolved": "string",
  327. "references": {}
  328. },
  329. "required": false,
  330. "optional": false,
  331. "docs": {
  332. "tags": [{
  333. "name": "default",
  334. "text": "\"Previous\""
  335. }],
  336. "text": "Accessible name for the component's previous button."
  337. },
  338. "attribute": "text-label-previous",
  339. "reflect": false,
  340. "defaultValue": "TEXT.previousLabel"
  341. },
  342. "scale": {
  343. "type": "string",
  344. "mutable": false,
  345. "complexType": {
  346. "original": "Scale",
  347. "resolved": "\"l\" | \"m\" | \"s\"",
  348. "references": {
  349. "Scale": {
  350. "location": "import",
  351. "path": "../interfaces"
  352. }
  353. }
  354. },
  355. "required": false,
  356. "optional": false,
  357. "docs": {
  358. "tags": [],
  359. "text": "Specifies the size of the component."
  360. },
  361. "attribute": "scale",
  362. "reflect": true,
  363. "defaultValue": "\"m\""
  364. }
  365. };
  366. }
  367. static get states() {
  368. return {
  369. "effectiveLocale": {}
  370. };
  371. }
  372. static get events() {
  373. return [{
  374. "method": "calcitePaginationUpdate",
  375. "name": "calcitePaginationUpdate",
  376. "bubbles": true,
  377. "cancelable": false,
  378. "composed": true,
  379. "docs": {
  380. "tags": [{
  381. "name": "deprecated",
  382. "text": "use calcitePaginationChange instead"
  383. }],
  384. "text": "Emits when the selected page changes."
  385. },
  386. "complexType": {
  387. "original": "PaginationDetail",
  388. "resolved": "PaginationDetail",
  389. "references": {
  390. "PaginationDetail": {
  391. "location": "local"
  392. }
  393. }
  394. }
  395. }, {
  396. "method": "calcitePaginationChange",
  397. "name": "calcitePaginationChange",
  398. "bubbles": true,
  399. "cancelable": false,
  400. "composed": true,
  401. "docs": {
  402. "tags": [{
  403. "name": "see",
  404. "text": "[PaginationDetail](https://github.com/Esri/calcite-components/blob/master/src/components/pagination/pagination.tsx#L23)"
  405. }],
  406. "text": "Emits when the selected page changes."
  407. },
  408. "complexType": {
  409. "original": "PaginationDetail",
  410. "resolved": "PaginationDetail",
  411. "references": {
  412. "PaginationDetail": {
  413. "location": "local"
  414. }
  415. }
  416. }
  417. }];
  418. }
  419. static get methods() {
  420. return {
  421. "nextPage": {
  422. "complexType": {
  423. "signature": "() => Promise<void>",
  424. "parameters": [],
  425. "references": {
  426. "Promise": {
  427. "location": "global"
  428. }
  429. },
  430. "return": "Promise<void>"
  431. },
  432. "docs": {
  433. "text": "Go to the next page of results.",
  434. "tags": []
  435. }
  436. },
  437. "previousPage": {
  438. "complexType": {
  439. "signature": "() => Promise<void>",
  440. "parameters": [],
  441. "references": {
  442. "Promise": {
  443. "location": "global"
  444. }
  445. },
  446. "return": "Promise<void>"
  447. },
  448. "docs": {
  449. "text": "Go to the previous page of results.",
  450. "tags": []
  451. }
  452. }
  453. };
  454. }
  455. static get elementRef() { return "el"; }
  456. }