index.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import React from 'react';
  2. import { cloneDeepWith, set, get } from 'lodash';
  3. import warning from '@douyinfe/semi-foundation/utils/warning';
  4. import { findAll } from '@douyinfe/semi-foundation/utils/getHighlight';
  5. import { isHTMLElement } from '@douyinfe/semi-foundation/utils/dom';
  6. /**
  7. * stop propagation
  8. *
  9. * @param {React.MouseEvent<HTMLElement>} e React mouse event object
  10. * @param {boolean} noImmediate Skip stopping immediate propagation
  11. */
  12. export function stopPropagation(e: React.MouseEvent | React.FocusEvent<HTMLElement>, noImmediate?: boolean) {
  13. if (e && typeof e.stopPropagation === 'function') {
  14. e.stopPropagation();
  15. }
  16. if (!noImmediate && e.nativeEvent && typeof e.nativeEvent.stopImmediatePropagation === 'function') {
  17. e.nativeEvent.stopImmediatePropagation();
  18. }
  19. }
  20. /**
  21. * use in Table, Form, Navigation
  22. *
  23. * skip clone function and react element
  24. */
  25. export function cloneDeep(value: any, customizer?: (value: any) => void) {
  26. return cloneDeepWith(value, v => {
  27. if (typeof customizer === 'function') {
  28. return customizer(v);
  29. }
  30. if (typeof v === 'function' || React.isValidElement(v)) {
  31. return v;
  32. }
  33. if (Object.prototype.toString.call(v) === '[object Error]') {
  34. return v;
  35. }
  36. // it is tricky
  37. // when array length beyond max length, array.length will be 0
  38. if (Array.isArray(v) && v.length === 0) {
  39. const keys: string[] = Object.keys(v);
  40. if (keys.length) {
  41. const newArray: any[] = [];
  42. keys.forEach(key => {
  43. set(newArray, key, v[key]);
  44. });
  45. // internal-issues:887
  46. try {
  47. warning(
  48. get(process, 'env.NODE_ENV') !== 'production',
  49. `[Semi] You may use an out-of-bounds array. In some cases, your program may not behave as expected.
  50. The maximum length of an array is 4294967295.
  51. Please check whether the array subscript in your data exceeds the maximum value of the JS array subscript`
  52. );
  53. } catch (e) {
  54. }
  55. return newArray;
  56. } else {
  57. return undefined;
  58. }
  59. }
  60. return undefined;
  61. });
  62. }
  63. /**
  64. * [getHighLightTextHTML description]
  65. *
  66. * @param {string} sourceString [source content text]
  67. * @param {Array<string>} searchWords [keywords to be highlighted]
  68. * @param {object} option
  69. * @param {true} option.highlightTag [The tag wrapped by the highlighted content, mark is used by default]
  70. * @param {true} option.highlightClassName
  71. * @param {true} option.highlightStyle
  72. * @param {boolean} option.caseSensitive
  73. *
  74. * @return {Array<object>}
  75. */
  76. export const getHighLightTextHTML = ({
  77. sourceString = '',
  78. searchWords = [],
  79. option = { autoEscape: true, caseSensitive: false }
  80. }: GetHighLightTextHTMLProps) => {
  81. const chunks: HighLightTextHTMLChunk[] = findAll({ sourceString, searchWords, ...option });
  82. const markEle = option.highlightTag || 'mark';
  83. const highlightClassName = option.highlightClassName || '';
  84. const highlightStyle = option.highlightStyle || {};
  85. return chunks.map((chunk: HighLightTextHTMLChunk, index: number) => {
  86. const { end, start, highlight } = chunk;
  87. const text = sourceString.substr(start, end - start);
  88. if (highlight) {
  89. return React.createElement(
  90. markEle,
  91. {
  92. style: highlightStyle,
  93. className: highlightClassName,
  94. key: text + index
  95. },
  96. text
  97. );
  98. } else {
  99. return text;
  100. }
  101. });
  102. };
  103. export interface RegisterMediaQueryOption {
  104. match?: (e: MediaQueryList | MediaQueryListEvent) => void;
  105. unmatch?: (e: MediaQueryList | MediaQueryListEvent) => void;
  106. callInInit?: boolean
  107. }
  108. /**
  109. * register matchFn and unMatchFn callback while media query
  110. * @param {string} media media string
  111. * @param {object} param param object
  112. * @returns function
  113. */
  114. export const registerMediaQuery = (media: string, { match, unmatch, callInInit = true }: RegisterMediaQueryOption): () => void => {
  115. if (typeof window !== 'undefined') {
  116. const mediaQueryList = window.matchMedia(media);
  117. function handlerMediaChange(e: MediaQueryList | MediaQueryListEvent): void {
  118. if (e.matches) {
  119. match && match(e);
  120. } else {
  121. unmatch && unmatch(e);
  122. }
  123. }
  124. callInInit && handlerMediaChange(mediaQueryList);
  125. if (Object.prototype.hasOwnProperty.call(mediaQueryList, 'addEventListener')) {
  126. mediaQueryList.addEventListener('change', handlerMediaChange);
  127. return (): void => mediaQueryList.removeEventListener('change', handlerMediaChange);
  128. }
  129. mediaQueryList.addListener(handlerMediaChange);
  130. return (): void => mediaQueryList.removeListener(handlerMediaChange);
  131. }
  132. return () => undefined;
  133. };
  134. export interface GetHighLightTextHTMLProps {
  135. sourceString?: string;
  136. searchWords?: string[];
  137. option: HighLightTextHTMLOption
  138. }
  139. export interface HighLightTextHTMLOption {
  140. highlightTag?: string;
  141. highlightClassName?: string;
  142. highlightStyle?: React.CSSProperties;
  143. caseSensitive: boolean;
  144. autoEscape: boolean
  145. }
  146. export interface HighLightTextHTMLChunk {
  147. start?: number;
  148. end?: number;
  149. highlight?: any
  150. }
  151. /**
  152. * Determine whether the incoming element is a built-in icon
  153. * @param icon 元素
  154. * @returns boolean
  155. */
  156. export const isSemiIcon = (icon: any): boolean => React.isValidElement(icon) && get(icon.type, 'elementType') === 'Icon';
  157. export function getActiveElement(): HTMLElement | null {
  158. return document ? document.activeElement as HTMLElement : null;
  159. }
  160. export function isNodeContainsFocus(node: HTMLElement) {
  161. const activeElement = getActiveElement();
  162. return activeElement === node || node.contains(activeElement);
  163. }
  164. export function getFocusableElements(node: HTMLElement) {
  165. if (!isHTMLElement(node)) {
  166. return [];
  167. }
  168. const focusableSelectorsList = [
  169. "input:not([disabled]):not([tabindex='-1'])",
  170. "textarea:not([disabled]):not([tabindex='-1'])",
  171. "button:not([disabled]):not([tabindex='-1'])",
  172. "a[href]:not([tabindex='-1'])",
  173. "select:not([disabled]):not([tabindex='-1'])",
  174. "area[href]:not([tabindex='-1'])",
  175. "iframe:not([tabindex='-1'])",
  176. "object:not([tabindex='-1'])",
  177. "*[tabindex]:not([tabindex='-1'])",
  178. "*[contenteditable]:not([tabindex='-1'])",
  179. ];
  180. const focusableSelectorsStr = focusableSelectorsList.join(',');
  181. // we are not filtered elements which are invisible
  182. const focusableElements = Array.from(node.querySelectorAll<HTMLElement>(focusableSelectorsStr));
  183. return focusableElements;
  184. }