Skip to content

Instantly share code, notes, and snippets.

@lstrrs
Last active August 31, 2017 17:56
Show Gist options
  • Save lstrrs/4d5cfebce25e2d4a1865ebf658314ee6 to your computer and use it in GitHub Desktop.
Save lstrrs/4d5cfebce25e2d4a1865ebf658314ee6 to your computer and use it in GitHub Desktop.
Line Clamp 2
import Ember from 'ember';
export default Ember.Component.extend({
componentName: 'LineClamp',
//tagName: 'span',
text: '',
truncatedText: '',
lines: 3,
init() {
this._super(...arguments);
this.resize = this.resize.bind(this);
this.calcTargetWidth = this.calcTargetWidth.bind(this);
this.measureWidth = this.measureWidth.bind(this);
this.getLines = this.getLines.bind(this);
this.onTruncate = this.onTruncate.bind(this);
// Element used for measuring purposes
this.ellipsisElement = document.createElement('span');
this.ellipsisElement.style = 'position:fixed;visibility:hidden;top:0;left:0';
this.ellipsisElement.innerHTML = `... <a href='#'>${this.moreText}</a>`;
// Actual elment for ellipsis/see more
this.ellipsis = `<span>... <a class="line-clamp__more" href='#'>${this.moreText}</a></span>`;
this._id = `${this.componentName}${Math.floor((Math.random() * 100) + 1)}`;
console.time(`${this._id}-render-${++this.renderCount}`);
},
click(e) {
e.preventDefault();
const target = e.target;
if (target.classList.contains('line-clamp__more')) {
const toggleTruncate = this.get('toggleTruncate');
if (typeof toggleTruncate === 'function') {
toggleTruncate(e);
}
}
},
renderCount: 0,
didRender() {
clearTimeout(this.perfTimeout);
console.timeEnd(`${this._id}-render-${this.renderCount}`);
console.time(`${this._id}-render-${++this.renderCount}`);
this.perfTimeout = setTimeout(() => {
console.timeEnd(`${this._id}-render-${this.renderCount}`);
console.log('-render-done');
}, 5000);
},
didInsertElement() {
const canvas = document.createElement('canvas');
this.canvasContext = canvas.getContext('2d');
document.body.appendChild(this.ellipsisElement);
this.calcTargetWidth(() => {
// Node not needed in document tree to read its content
//if (this.get('text')) {
//console.log('Text: ', this.get('text'));
//this.get('text').parentNode.removeChild(this.get('text'));
//}
});
this.bindResize();
},
willDestroyElement() {
this.ellipsisElement.parentNode.removeChild(ellipsisElement);
this.unbindResize();
window.cancelAnimationFrame(this.timeout);
},
bindResize() {
window.addEventListener('resize', this.get('resize'));
this._resizeHandlerRegistered = true;
},
unbindResize() {
if (this._resizeHandlerRegistered) {
window.removeEventListener('resize', this.get('resize'));
this._resizeHandlerRegistered = false;
}
},
resize() {
this.calcTargetWidth();
},
innerText(node) {
const div = document.createElement('div');
const contentKey = 'innerText' in window.HTMLElement.prototype ? 'innerText' : 'textContent';
div.innerHTML = node.innerHTML.replace(/\r\n|\r|\n/g, ' ');
let text = div[contentKey];
const test = document.createElement('div');
test.innerHTML = 'foo<br/>bar';
if (test[contentKey].replace(/\r\n|\r/g, '\n') !== 'foo\nbar') {
div.innerHTML = div.innerHTML.replace(/<br.*?[\/]?>/gi, '\n');
text = div[contentKey];
}
return text;
},
onTruncate(didTruncate) {
const handleTruncate = this.get('handleTruncate');
if (typeof handleTruncate === 'function') {
this.timeout = window.requestAnimationFrame(() => {
handleTruncate(didTruncate);
});
}
},
calcTargetWidth(callback) {
const targetWidth = this.element.getBoundingClientRect().width;
if (!targetWidth) {
return window.requestAnimationFrame(() => this.calcTargetWidth(callback));
}
const {
fontWeight,
fontStyle,
fontSize,
fontFamily
} = window.getComputedStyle(this.element);
const font = `${fontWeight} ${fontStyle} ${fontSize} ${fontFamily}`;
this.canvasContext.font = font;
this.set('targetWidth', targetWidth);
},
measureWidth(text) {
return this.canvasContext && this.canvasContext.measureText(text).width;
},
ellipsisWidth(node) {
return node.offsetWidth;
},
getLines() {
const lines = [];
const numLines = this.get('lines');
//const text = this.innerText(this.element);
const text = this.text;//this.innerText(this.element);
const textLines = text.split('\n').map(line => line.split(' '));
let didTruncate = true;
const ellipsisWidth = this.ellipsisWidth(this.ellipsisElement);
for (let line = 1; line <= numLines; line++) {
const textWords = textLines[0];
// Handle newline
if (textWords.length === 0) {
lines.push();
textLines.shift();
line--;
continue;
}
let resultLine = textWords.join(' ');
if (this.measureWidth(resultLine) <= this.targetWidth) {
if (textLines.length === 1) {
// Line is end of text and fits without truncating
didTruncate = false;
lines.push(resultLine);
break;
}
}
if (line === numLines) {
// Binary search determining the longest possible line inluding truncate string
const textRest = textWords.join(' ');
let lower = 0;
let upper = textRest.length - 1;
while (lower <= upper) {
const middle = Math.floor((lower + upper) / 2);
const testLine = textRest.slice(0, middle + 1);
if (this.measureWidth(testLine) + ellipsisWidth <= this.targetWidth) {
lower = middle + 1;
} else {
upper = middle - 1;
}
}
resultLine = Ember.String.htmlSafe(`<span>${textRest.slice(0, lower)}${this.get('ellipsis')}</span>`);
} else {
// Binary search determining when the line breaks
let lower = 0;
let upper = textWords.length - 1;
while (lower <= upper) {
const middle = Math.floor((lower + upper) / 2);
const testLine = textWords.slice(0, middle + 1).join(' ');
if (this.measureWidth(testLine) <= this.targetWidth) {
lower = middle + 1;
} else {
upper = middle - 1;
}
}
// The first word of this line is too long to fit it
if (lower === 0) {
// Jump to processing of last line
line = numLines - 1;
continue;
}
resultLine = Ember.String.htmlSafe(textWords.slice(0, lower).join(' '));
textLines[0].splice(0, lower);
}
lines.push(resultLine);
}
this.onTruncate(didTruncate);
return lines;
},
renderLine(line, i, arr) {
if (i === arr.length - 1) {
return `<span key=${i}>${line}</span>`;
} else {
const br = '<br />';
console.log('Line: ', line);
if (line) {
return `<span key=${i}>${line}</span>${br}`;
} else {
return br;
}
}
},
displayText: Ember.computed('lines', 'text', 'targetWidth', function getDisplayText() {
const mounted = !!(this.element && this.get('targetWidth'));
if (typeof window !== 'undefined' && mounted) {
if (this.get('lines') > 0) {
//return this.getLines().map(this.renderLine).join(' ');
return this.getLines();
} else {
this.onTruncate(false);
return this.get('text');
}
}
return this.get('text');
}),
actions: {
}
});
import Ember from 'ember';
export default Ember.Component.extend({
expanded: false,
truncated: false,
lines: 3,
seeLessText: 'See Less',
init() {
this._super(...arguments);
/*
this.handleTruncate = this.handleTruncate.bind(this);
this.toggleTruncate = this.toggleTruncate.bind(this);
*/
this.seeMoreText = 'See More';
},
numLines: Ember.computed('expanded', function getNumLines() {
return !this.get('expanded') && this.get('lines');
}),
actions: {
handleTruncate(truncated) {
if (this.get('truncated') !== truncated) {
this.set('truncated', truncated);
}
},
toggleTruncate() {
event.preventDefault();
this.toggleProperty('expanded');
},
},
});
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
init() {
this._super(...arguments);
this.text = 'After almost 6 years at LinkedIn, I\'m off in search of my next adventure! I am so grateful to everyone who has made LinkedIn such a transformational experience. As I wrote in my goodbye email, I realized how much I have enjoyed my time here at LinkedIn.';
this.text2 = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
this.truncate = true;
},
});
body {
margin: 12px 16px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 12pt;
}
.line-clamp {
overflow: hidden;
position: relative;
}
.line-clamp--single-line {
white-space: nowrap;
text-overflow: ellipsis;
}
.line-clamp--multi-line {
/* autoprefixer: off */
display: -webkit-box;
-webkit-box-orient: vertical;
/* autoprefixer: on */
text-overflow: ellipsis;
}
<h1>Welcome to {{appName}}</h1>
<br>
<h2>My Story</h2>
<br>
{{read-more text=text truncate=truncate}}
<br>
{{read-more text=text truncate=truncate lines=2}}
<br>
{{read-more text=text2 truncate=truncate}}
{{!--
{{#if truncated}}
{{truncatedText}}
{{else}}
{{text}}
{{/if}}
{{#if ellipsis}}
<span>...
{{#if ellipsisComponent}}
{{component ellipsisComponent (action "toggleTruncate")}}
{{/if}}
</span>
{{/if}}
{{!-- <span>... <a href='#' onClick={this.toggleLines}>{more}</a></span> --}}
{{!-- {{displayText}} --}}
{{#each displayText as |line index|}}
<span class="line">{{line}}</span>
<br />
{{else}}
{{displayText}}
{{/each}}
{{line-clamp
lines=numLines
text=text
moreText=seeMoreText
handleTruncate=(action "handleTruncate")
toggleTruncate=(action "toggleTruncate")
}}
{{#unless truncated}}
{{#if expanded}}
<span><a href='#' {{action "toggleTruncate" e}}>{{seeLessText}}</a></span>
{{/if}}
{{/unless}}
{
"version": "0.12.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.12.0",
"ember-template-compiler": "2.12.0",
"ember-testing": "2.12.0"
},
"addons": {
"ember-data": "2.12.1"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment