interfaceTable.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { getElements, replaceAll, findFirstAdjacent } from '../util';
  2. type ShowHide = 'show' | 'hide';
  3. function isShowHide(value: unknown): value is ShowHide {
  4. return typeof value === 'string' && ['show', 'hide'].includes(value);
  5. }
  6. /**
  7. * When this error is thrown, it's an indication that we don't need to manage this table, because
  8. * it doesn't contain the required elements.
  9. */
  10. class TableStateError extends Error {
  11. table: HTMLTableElement;
  12. constructor(message: string, table: HTMLTableElement) {
  13. super(message);
  14. this.table = table;
  15. }
  16. }
  17. /**
  18. * Manage the display text of a button element as well as the visibility of its corresponding rows.
  19. */
  20. class ButtonState {
  21. /**
  22. * Underlying Button DOM Element
  23. */
  24. public button: HTMLButtonElement;
  25. /**
  26. * Table rows provided in constructor
  27. */
  28. private rows: NodeListOf<HTMLTableRowElement>;
  29. constructor(button: HTMLButtonElement, rows: NodeListOf<HTMLTableRowElement>) {
  30. this.button = button;
  31. this.rows = rows;
  32. }
  33. /**
  34. * Remove visibility of button state rows.
  35. */
  36. private hideRows(): void {
  37. for (const row of this.rows) {
  38. row.classList.add('d-none');
  39. }
  40. }
  41. /**
  42. * Update the DOM element's `data-state` attribute.
  43. */
  44. public set buttonState(state: Nullable<ShowHide>) {
  45. if (isShowHide(state)) {
  46. this.button.setAttribute('data-state', state);
  47. }
  48. }
  49. /**
  50. * Get the DOM element's `data-state` attribute.
  51. */
  52. public get buttonState(): Nullable<ShowHide> {
  53. const state = this.button.getAttribute('data-state');
  54. if (isShowHide(state)) {
  55. return state;
  56. }
  57. return null;
  58. }
  59. /**
  60. * Update the DOM element's display text to reflect the action opposite the current state. For
  61. * example, if the current state is to hide enabled interfaces, the DOM text should say
  62. * "Show Enabled Interfaces".
  63. */
  64. private toggleButton(): void {
  65. if (this.buttonState === 'show') {
  66. this.button.innerText = replaceAll(this.button.innerText, 'Show', 'Hide');
  67. } else if (this.buttonState === 'hide') {
  68. this.button.innerText = replaceAll(this.button.innerHTML, 'Hide', 'Show');
  69. }
  70. }
  71. /**
  72. * Toggle the DOM element's `data-state` attribute.
  73. */
  74. private toggleState(): void {
  75. if (this.buttonState === 'show') {
  76. this.buttonState = 'hide';
  77. } else if (this.buttonState === 'hide') {
  78. this.buttonState = 'show';
  79. }
  80. }
  81. /**
  82. * Toggle all controlled elements.
  83. */
  84. private toggle(): void {
  85. this.toggleState();
  86. this.toggleButton();
  87. }
  88. /**
  89. * When the button is clicked, toggle all controlled elements and hide rows based on
  90. * buttonstate.
  91. */
  92. public handleClick(event: Event): void {
  93. const button = event.currentTarget as HTMLButtonElement;
  94. if (button.isEqualNode(this.button)) {
  95. this.toggle();
  96. }
  97. if (this.buttonState === 'hide') {
  98. this.hideRows();
  99. }
  100. }
  101. }
  102. /**
  103. * Manage the state of a table and its elements.
  104. */
  105. class TableState {
  106. /**
  107. * Underlying DOM Table Element.
  108. */
  109. private table: HTMLTableElement;
  110. /**
  111. * Instance of ButtonState for the 'show/hide enabled rows' button.
  112. */
  113. // @ts-expect-error null handling is performed in the constructor
  114. private enabledButton: ButtonState;
  115. /**
  116. * Instance of ButtonState for the 'show/hide disabled rows' button.
  117. */
  118. // @ts-expect-error null handling is performed in the constructor
  119. private disabledButton: ButtonState;
  120. /**
  121. * Instance of ButtonState for the 'show/hide virtual rows' button.
  122. */
  123. // @ts-expect-error null handling is performed in the constructor
  124. private virtualButton: ButtonState;
  125. /**
  126. * Instance of ButtonState for the 'show/hide virtual rows' button.
  127. */
  128. // @ts-expect-error null handling is performed in the constructor
  129. private disconnectedButton: ButtonState;
  130. /**
  131. * All table rows in table
  132. */
  133. private rows: NodeListOf<HTMLTableRowElement>;
  134. constructor(table: HTMLTableElement) {
  135. this.table = table;
  136. this.rows = this.table.querySelectorAll('tr');
  137. try {
  138. const toggleEnabledButton = findFirstAdjacent<HTMLButtonElement>(
  139. this.table,
  140. 'button.toggle-enabled',
  141. );
  142. const toggleDisabledButton = findFirstAdjacent<HTMLButtonElement>(
  143. this.table,
  144. 'button.toggle-disabled',
  145. );
  146. const toggleVirtualButton = findFirstAdjacent<HTMLButtonElement>(
  147. this.table,
  148. 'button.toggle-virtual',
  149. );
  150. const toggleDisconnectedButton = findFirstAdjacent<HTMLButtonElement>(
  151. this.table,
  152. 'button.toggle-disconnected',
  153. );
  154. if (toggleEnabledButton === null) {
  155. throw new TableStateError("Table is missing a 'toggle-enabled' button.", table);
  156. }
  157. if (toggleDisabledButton === null) {
  158. throw new TableStateError("Table is missing a 'toggle-disabled' button.", table);
  159. }
  160. if (toggleVirtualButton === null) {
  161. throw new TableStateError("Table is missing a 'toggle-virtual' button.", table);
  162. }
  163. if (toggleDisconnectedButton === null) {
  164. throw new TableStateError("Table is missing a 'toggle-disconnected' button.", table);
  165. }
  166. // Attach event listeners to the buttons elements.
  167. toggleEnabledButton.addEventListener('click', event => this.handleClick(event, this));
  168. toggleDisabledButton.addEventListener('click', event => this.handleClick(event, this));
  169. toggleVirtualButton.addEventListener('click', event => this.handleClick(event, this));
  170. toggleDisconnectedButton.addEventListener('click', event => this.handleClick(event, this));
  171. // Instantiate ButtonState for each button for state management.
  172. this.enabledButton = new ButtonState(
  173. toggleEnabledButton,
  174. table.querySelectorAll<HTMLTableRowElement>('tr[data-enabled="enabled"]'),
  175. );
  176. this.disabledButton = new ButtonState(
  177. toggleDisabledButton,
  178. table.querySelectorAll<HTMLTableRowElement>('tr[data-enabled="disabled"]'),
  179. );
  180. this.virtualButton = new ButtonState(
  181. toggleVirtualButton,
  182. table.querySelectorAll<HTMLTableRowElement>('tr[data-type="virtual"]'),
  183. );
  184. this.disconnectedButton = new ButtonState(
  185. toggleDisconnectedButton,
  186. table.querySelectorAll<HTMLTableRowElement>('tr[data-connected="disconnected"]'),
  187. );
  188. } catch (err) {
  189. if (err instanceof TableStateError) {
  190. // This class is useless for tables that don't have toggle buttons.
  191. console.debug('Table does not contain enable/disable toggle buttons');
  192. return;
  193. } else {
  194. throw err;
  195. }
  196. }
  197. }
  198. /**
  199. * When toggle buttons are clicked, reapply visability all rows and
  200. * pass the event to all button handlers
  201. *
  202. * @param event onClick event for toggle buttons.
  203. * @param instance Instance of TableState (`this` cannot be used since that's context-specific).
  204. */
  205. public handleClick(event: Event, instance: TableState): void {
  206. for (const row of this.rows) {
  207. row.classList.remove('d-none');
  208. }
  209. instance.enabledButton.handleClick(event);
  210. instance.disabledButton.handleClick(event);
  211. instance.virtualButton.handleClick(event);
  212. instance.disconnectedButton.handleClick(event);
  213. }
  214. }
  215. /**
  216. * Initialize table states.
  217. */
  218. export function initInterfaceTable(): void {
  219. for (const element of getElements<HTMLTableElement>('table')) {
  220. new TableState(element);
  221. }
  222. }