暫無描述
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

enter-navigation.directive.ts 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { Directive, ElementRef, HostListener } from '@angular/core';
  2. @Directive({
  3. selector: 'input[matInput]:not([type="submit"]):not([type="button"]), textarea[matInput]',
  4. standalone: true
  5. })
  6. export class EnterNavigationDirective {
  7. private isComposing = false;
  8. constructor(private elementRef: ElementRef<HTMLElement>) {}
  9. @HostListener('keydown.enter', ['$event'])
  10. onEnter(event: KeyboardEvent): void {
  11. // 跳过IME组合输入状态下的回车(让IME完成输入确认)
  12. if (event.isComposing || this.isComposing) {
  13. return;
  14. }
  15. // 阻止默认的提交行为,保持现有表单提交逻辑
  16. event.preventDefault();
  17. // 如果是textarea且按着Shift键,允许换行
  18. if (this.isTextArea() && event.shiftKey) {
  19. return;
  20. }
  21. // 查找下一个可聚焦元素
  22. const nextElement = this.findNextFocusable();
  23. if (nextElement) {
  24. nextElement.focus();
  25. // 对于select元素,阻止事件进一步传播
  26. if (nextElement.tagName === 'SELECT') {
  27. event.stopPropagation();
  28. }
  29. } else {
  30. // 没有下一个可聚焦元素,触发默认行为(提交表单)
  31. this.triggerFormSubmit(event);
  32. }
  33. }
  34. @HostListener('focus')
  35. onFocus(): void {
  36. const element = this.elementRef.nativeElement;
  37. // 只处理input元素(排除textarea、select等)
  38. if (element.tagName !== 'INPUT') return;
  39. const inputElement = element as HTMLInputElement;
  40. // 排除只读字段和禁用字段
  41. if (inputElement.readOnly || inputElement.disabled) return;
  42. // 使用setTimeout确保在Angular变更检测后执行
  43. setTimeout(() => inputElement.select(), 0);
  44. }
  45. @HostListener('compositionstart')
  46. onCompositionStart(): void {
  47. this.isComposing = true;
  48. }
  49. @HostListener('compositionend')
  50. onCompositionEnd(): void {
  51. this.isComposing = false;
  52. }
  53. private isTextArea(): boolean {
  54. return this.elementRef.nativeElement.tagName === 'TEXTAREA';
  55. }
  56. private findNextFocusable(): HTMLElement | null {
  57. const currentElement = this.elementRef.nativeElement;
  58. const form = this.findParentForm(currentElement);
  59. if (!form) {
  60. return null;
  61. }
  62. const focusableElements = this.getFocusableElements(form);
  63. const currentIndex = focusableElements.indexOf(currentElement);
  64. // 如果当前元素不在列表中或已经是最后一个,返回null
  65. if (currentIndex === -1 || currentIndex >= focusableElements.length - 1) {
  66. return null;
  67. }
  68. return focusableElements[currentIndex + 1];
  69. }
  70. private findParentForm(element: HTMLElement): HTMLElement | null {
  71. let parent = element.parentElement;
  72. while (parent && parent.tagName !== 'FORM') {
  73. parent = parent.parentElement;
  74. }
  75. return parent;
  76. }
  77. private getFocusableElements(container: HTMLElement): HTMLElement[] {
  78. // 定义输入类可聚焦元素的CSS选择器(排除按钮)
  79. const inputSelectors = [
  80. 'input[matInput]:not([type="hidden"]):not([disabled]):not([readonly])',
  81. 'textarea[matInput]:not([disabled]):not([readonly])',
  82. 'select[matInput]:not([disabled])',
  83. 'input:not([matInput]):not([type="hidden"]):not([type="submit"]):not([type="button"]):not([disabled]):not([readonly])',
  84. 'textarea:not([matInput]):not([disabled]):not([readonly])',
  85. 'select:not([matInput]):not([disabled])',
  86. '[contenteditable="true"]:not([disabled])'
  87. ].join(', ');
  88. // 获取所有匹配元素
  89. const allElements = Array.from(container.querySelectorAll(inputSelectors)) as HTMLElement[];
  90. // 过滤掉隐藏的元素和不可交互的元素
  91. return allElements.filter(element => {
  92. const style = window.getComputedStyle(element);
  93. const isVisible = style.display !== 'none' &&
  94. style.visibility !== 'hidden' &&
  95. style.opacity !== '0' &&
  96. !element.hasAttribute('hidden');
  97. // 检查元素是否在视图中(粗略检查)
  98. const rect = element.getBoundingClientRect();
  99. const isInViewport = rect.width > 0 && rect.height > 0;
  100. return isVisible && isInViewport;
  101. });
  102. }
  103. private triggerFormSubmit(event: KeyboardEvent): void {
  104. const currentElement = this.elementRef.nativeElement;
  105. const form = this.findParentForm(currentElement);
  106. if (form) {
  107. // 查找表单中的提交按钮并点击
  108. const submitButton = form.querySelector('button[type="submit"], input[type="submit"]') as HTMLElement;
  109. if (submitButton) {
  110. submitButton.click();
  111. } else {
  112. // 如果没有找到提交按钮,尝试触发表单的submit事件
  113. const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
  114. form.dispatchEvent(submitEvent);
  115. }
  116. }
  117. }
  118. }