util.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { createVNode as _createVNode } from "vue";
  2. import { createApp } from 'vue';
  3. import { styleToString } from '../vc-util/Dom/css';
  4. // We only handle element & text node.
  5. const TEXT_NODE = 3;
  6. const COMMENT_NODE = 8;
  7. let ellipsisContainer;
  8. const wrapperStyle = {
  9. padding: 0,
  10. margin: 0,
  11. display: 'inline',
  12. lineHeight: 'inherit'
  13. };
  14. function resetDomStyles(target, origin) {
  15. target.setAttribute('aria-hidden', 'true');
  16. const originStyle = window.getComputedStyle(origin);
  17. const originCSS = styleToString(originStyle);
  18. // Set shadow
  19. target.setAttribute('style', originCSS);
  20. target.style.position = 'fixed';
  21. target.style.left = '0';
  22. target.style.height = 'auto';
  23. target.style.minHeight = 'auto';
  24. target.style.maxHeight = 'auto';
  25. target.style.paddingTop = '0';
  26. target.style.paddingBottom = '0';
  27. target.style.borderTopWidth = '0';
  28. target.style.borderBottomWidth = '0';
  29. target.style.top = '-999999px';
  30. target.style.zIndex = '-1000';
  31. // clean up css overflow
  32. target.style.textOverflow = 'clip';
  33. target.style.whiteSpace = 'normal';
  34. target.style.webkitLineClamp = 'none';
  35. }
  36. function getRealLineHeight(originElement) {
  37. const heightContainer = document.createElement('div');
  38. resetDomStyles(heightContainer, originElement);
  39. heightContainer.appendChild(document.createTextNode('text'));
  40. document.body.appendChild(heightContainer);
  41. // The element real height is always less than multiple of line-height
  42. // Use getBoundingClientRect to get actual single row height of the element
  43. const realHeight = heightContainer.getBoundingClientRect().height;
  44. document.body.removeChild(heightContainer);
  45. return realHeight;
  46. }
  47. export default ((originElement, option, content, fixedContent, ellipsisStr) => {
  48. if (!ellipsisContainer) {
  49. ellipsisContainer = document.createElement('div');
  50. ellipsisContainer.setAttribute('aria-hidden', 'true');
  51. document.body.appendChild(ellipsisContainer);
  52. }
  53. const {
  54. rows,
  55. suffix = ''
  56. } = option;
  57. const lineHeight = getRealLineHeight(originElement);
  58. const maxHeight = Math.round(lineHeight * rows * 100) / 100;
  59. resetDomStyles(ellipsisContainer, originElement);
  60. // Render in the fake container
  61. const vm = createApp({
  62. render() {
  63. return _createVNode("div", {
  64. "style": wrapperStyle
  65. }, [_createVNode("span", {
  66. "style": wrapperStyle
  67. }, [content, suffix]), _createVNode("span", {
  68. "style": wrapperStyle
  69. }, [fixedContent])]);
  70. }
  71. });
  72. vm.mount(ellipsisContainer);
  73. // Check if ellipsis in measure div is height enough for content
  74. function inRange() {
  75. const currentHeight = Math.round(ellipsisContainer.getBoundingClientRect().height * 100) / 100;
  76. return currentHeight - 0.1 <= maxHeight; // -.1 for firefox
  77. }
  78. // Skip ellipsis if already match
  79. if (inRange()) {
  80. vm.unmount();
  81. return {
  82. content,
  83. text: ellipsisContainer.innerHTML,
  84. ellipsis: false
  85. };
  86. }
  87. const childNodes = Array.prototype.slice.apply(ellipsisContainer.childNodes[0].childNodes[0].cloneNode(true).childNodes).filter(_ref => {
  88. let {
  89. nodeType,
  90. data
  91. } = _ref;
  92. return nodeType !== COMMENT_NODE && data !== '';
  93. });
  94. const fixedNodes = Array.prototype.slice.apply(ellipsisContainer.childNodes[0].childNodes[1].cloneNode(true).childNodes);
  95. vm.unmount();
  96. // ========================= Find match ellipsis content =========================
  97. const ellipsisChildren = [];
  98. ellipsisContainer.innerHTML = '';
  99. // Create origin content holder
  100. const ellipsisContentHolder = document.createElement('span');
  101. ellipsisContainer.appendChild(ellipsisContentHolder);
  102. const ellipsisTextNode = document.createTextNode(ellipsisStr + suffix);
  103. ellipsisContentHolder.appendChild(ellipsisTextNode);
  104. fixedNodes.forEach(childNode => {
  105. ellipsisContainer.appendChild(childNode);
  106. });
  107. // Append before fixed nodes
  108. function appendChildNode(node) {
  109. ellipsisContentHolder.insertBefore(node, ellipsisTextNode);
  110. }
  111. // Get maximum text
  112. function measureText(textNode, fullText) {
  113. let startLoc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
  114. let endLoc = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : fullText.length;
  115. let lastSuccessLoc = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0;
  116. const midLoc = Math.floor((startLoc + endLoc) / 2);
  117. const currentText = fullText.slice(0, midLoc);
  118. textNode.textContent = currentText;
  119. if (startLoc >= endLoc - 1) {
  120. // Loop when step is small
  121. for (let step = endLoc; step >= startLoc; step -= 1) {
  122. const currentStepText = fullText.slice(0, step);
  123. textNode.textContent = currentStepText;
  124. if (inRange() || !currentStepText) {
  125. return step === fullText.length ? {
  126. finished: false,
  127. vNode: fullText
  128. } : {
  129. finished: true,
  130. vNode: currentStepText
  131. };
  132. }
  133. }
  134. }
  135. if (inRange()) {
  136. return measureText(textNode, fullText, midLoc, endLoc, midLoc);
  137. }
  138. return measureText(textNode, fullText, startLoc, midLoc, lastSuccessLoc);
  139. }
  140. function measureNode(childNode) {
  141. const type = childNode.nodeType;
  142. // console.log('type', type);
  143. // if (type === ELEMENT_NODE) {
  144. // // We don't split element, it will keep if whole element can be displayed.
  145. // appendChildNode(childNode);
  146. // if (inRange()) {
  147. // return {
  148. // finished: false,
  149. // vNode: contentList[index],
  150. // };
  151. // }
  152. // // Clean up if can not pull in
  153. // ellipsisContentHolder.removeChild(childNode);
  154. // return {
  155. // finished: true,
  156. // vNode: null,
  157. // };
  158. // }
  159. if (type === TEXT_NODE) {
  160. const fullText = childNode.textContent || '';
  161. const textNode = document.createTextNode(fullText);
  162. appendChildNode(textNode);
  163. return measureText(textNode, fullText);
  164. }
  165. // Not handle other type of content
  166. return {
  167. finished: false,
  168. vNode: null
  169. };
  170. }
  171. childNodes.some(childNode => {
  172. const {
  173. finished,
  174. vNode
  175. } = measureNode(childNode);
  176. if (vNode) {
  177. ellipsisChildren.push(vNode);
  178. }
  179. return finished;
  180. });
  181. return {
  182. content: ellipsisChildren,
  183. text: ellipsisContainer.innerHTML,
  184. ellipsis: true
  185. };
  186. });