|
| 1 | +import { define, css } from 'uce'; |
| 2 | +import { ResizeObserver } from '@juggle/resize-observer'; |
| 3 | + |
| 4 | +/** |
| 5 | + * Removes the last word from text and returns the new string. |
| 6 | + * Trims text before starting. |
| 7 | + * A "word" is text seprated with one or more spaces. |
| 8 | + * @param {[type]} text [description] |
| 9 | + * @return {[type]} [description] |
| 10 | + */ |
| 11 | +function removeLastWord(text) { |
| 12 | + return text.trim().substring(0, text.lastIndexOf(' ')); |
| 13 | +} |
| 14 | + |
| 15 | +/** |
| 16 | + * <pep-p>PepsiCo Paragraph Custom Element.</pep-p> |
| 17 | + * Paragraph Element with multiline ellipse support. |
| 18 | + * Text is trimmed to fit inside the parent element. (via 100% width/height) |
| 19 | + */ |
| 20 | +define('pep-p', { |
| 21 | + style: (selector) => css` |
| 22 | + ${selector} { |
| 23 | + /* Prevent the overflow from flashing */ |
| 24 | + overflow: hidden; |
| 25 | + /* Need defined width/height, so use the parents values */ |
| 26 | + /* height: 100%; */ /* removed height so text can center in parent. */ |
| 27 | + width: 100%; |
| 28 | + display: block; |
| 29 | + } |
| 30 | + ${selector} textarea { |
| 31 | + box-sizing: border-box; |
| 32 | + } |
| 33 | + `, |
| 34 | + init() { |
| 35 | + // Create a Resize Observer so we can re-adjust the length of the text content |
| 36 | + // when the element changes size. |
| 37 | + try { |
| 38 | + this._resizeObserver = new ResizeObserver((elms) => { |
| 39 | + this.render(); |
| 40 | + }); |
| 41 | + // Start observing. |
| 42 | + this._resizeObserver.observe(this); |
| 43 | + } |
| 44 | + catch(e) { |
| 45 | + console.log('<pep-p /> _resizeObserver error', e, this); |
| 46 | + } |
| 47 | + }, |
| 48 | + connected() { |
| 49 | + // Create a copy of the children and text before we start modifying it. |
| 50 | + this.originalChildNodes = Array.from(this.childNodes).map(node => node.cloneNode(true)); |
| 51 | + this.originalTextContent = this.textContent; |
| 52 | + // this.setAttribute('tooltip', this.originalTextContent.trim()); |
| 53 | + this.trimTextContent(); |
| 54 | + }, |
| 55 | + disconnected() { |
| 56 | + try { |
| 57 | + this._resizeObserver.disconnect(); |
| 58 | + this.restoreChildren(); |
| 59 | + } |
| 60 | + catch(e) { |
| 61 | + console.log('<pep-p /> Disconnected error', e, this); |
| 62 | + } |
| 63 | + }, |
| 64 | + |
| 65 | + // Restores the original children |
| 66 | + restoreChildren() { |
| 67 | + this.innerHTML = ''; // clear out all the children |
| 68 | + // Create clones of the original children and put them back. |
| 69 | + this.originalChildNodes.forEach(child => this.appendChild(child.cloneNode(true))); |
| 70 | + }, |
| 71 | + |
| 72 | + // render by updating the trimmed text to match the current size. |
| 73 | + render() { |
| 74 | + // Clear the tooltip. |
| 75 | + this.removeAttribute('tooltip'); |
| 76 | + // Find the minimum height needed to display text. |
| 77 | + this.innerHTML = 'ÀEIOUhy'; |
| 78 | + this.minHeight = this.scrollHeight; |
| 79 | + // restore the children and then re-trim to the new size. |
| 80 | + this.restoreChildren(); |
| 81 | + this.trimTextContent(); |
| 82 | + |
| 83 | + if (this.didTrim) { |
| 84 | + this.setAttribute('tooltip', this.originalTextContent.trim()); |
| 85 | + } |
| 86 | + }, |
| 87 | + |
| 88 | + // Returns true if the content overflows. |
| 89 | + get hasOverflow() { |
| 90 | + if (this.scrollWidth <= this.clientWidth |
| 91 | + && this.scrollHeight <= this.minHeight) { |
| 92 | + return false; |
| 93 | + } |
| 94 | + |
| 95 | + return true; |
| 96 | + }, |
| 97 | + // Returns true if the element has a size. |
| 98 | + get hasSize() { |
| 99 | + if (0 === this.scrollWidth || 0 === this.scrollHeight) { |
| 100 | + return false; |
| 101 | + } |
| 102 | + return true; |
| 103 | + }, |
| 104 | + |
| 105 | + // Trims the text content of the children to make them fit without overflowing. |
| 106 | + trimTextContent() { |
| 107 | + this.didTrim = false; |
| 108 | + |
| 109 | + // Skip if the element has no size. We can't trim to an unknown. |
| 110 | + if (!this.hasSize) { |
| 111 | + return; |
| 112 | + } |
| 113 | + // Loop over each child starting with the last, |
| 114 | + // untill we are no longer overflowing, or we run out of children. |
| 115 | + for (let idx=(this.childNodes.length-1); |
| 116 | + idx >= 0 && this.hasOverflow; |
| 117 | + idx--) { |
| 118 | + |
| 119 | + const childElm = this.childNodes[idx]; |
| 120 | + |
| 121 | + // We don't want to change user's input, so skip those elements. |
| 122 | + if (['TEXTAREA', 'INPUT', 'SELECT'].includes(childElm.nodeName)) { |
| 123 | + continue; |
| 124 | + } |
| 125 | + |
| 126 | + // We can remove line breaks if we are trying to shrink the content. |
| 127 | + if (childElm.nodeName === 'BR') { |
| 128 | + childElm.remove(); |
| 129 | + continue; |
| 130 | + } |
| 131 | + |
| 132 | + // Remove one word at a time until either the content fits, |
| 133 | + // or we run out of text. |
| 134 | + let shorterText = childElm.textContent; |
| 135 | + do { |
| 136 | + this.didTrim = true; |
| 137 | + // Remove the last word. |
| 138 | + shorterText = removeLastWord(shorterText); |
| 139 | + // update the element so we can check the new size. |
| 140 | + childElm.textContent = shorterText; |
| 141 | + } |
| 142 | + // Stop when we run out of text, or we are no longer overflowing. |
| 143 | + while(this.hasOverflow && shorterText.length > 0); |
| 144 | + |
| 145 | + // If the text is empty, remove the element, |
| 146 | + if ((!shorterText || shorterText.length === 0)) { |
| 147 | + childElm.remove(); |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + // Check if we did anything. |
| 152 | + if (this.didTrim) { |
| 153 | + // Find the last text element so we can add ellipsis. |
| 154 | + let lastTextElm = this.childNodes[this.childNodes.length-1]; |
| 155 | + while (lastTextElm && lastTextElm.nodeName !== '#text') { |
| 156 | + lastTextElm = lastTextElm.previousSibling; |
| 157 | + } |
| 158 | + // there is a chance there are no text elements left. |
| 159 | + if (lastTextElm) { |
| 160 | + lastTextElm.textContent = removeLastWord(lastTextElm.textContent) + ' ...'; |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + } |
| 165 | +}); |
0 commit comments