Skip to content

Instantly share code, notes, and snippets.

@mariechatfield
Last active May 22, 2018 18:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mariechatfield/9759ba653b04cfc232d454465ac517fe to your computer and use it in GitHub Desktop.
Save mariechatfield/9759ba653b04cfc232d454465ac517fe to your computer and use it in GitHub Desktop.
Timeline with Fixed Sidebars
import Ember from 'ember';
const START_MINUTE = 480; // 8:00am
const PIXELS_PER_MINUTE = 1;
function timeToPixels(minutes) {
const leftOffset = START_MINUTE * PIXELS_PER_MINUTE;
return (minutes * PIXELS_PER_MINUTE) - leftOffset;
}
export default Ember.Component.extend({
time: null,
classNames: ['timeline-block'],
didRender() {
this._super(...arguments);
const clockinPixel = timeToPixels(this.get('time.start'));
const clockoutPixel = timeToPixels(this.get('time.end'));
const width = clockoutPixel - clockinPixel;
const $timecardElement = this.$();
$timecardElement.css('width', `${width}px`);
$timecardElement.css('left', `${clockinPixel}px`);
},
totalMinutes: Ember.computed('time.{start,end}', function() {
return this.get('time.end') - this.get('time.start');
})
});
import Ember from 'ember';
export default Ember.Component.extend({
summaries: null,
hours: Ember.computed(function() {
return [
'8:00am',
'9:00am',
'10:00am',
'11:00am',
'12:00pm',
'1:00pm',
'2:00pm',
'3:00pm',
'4:00pm',
'5:00pm',
'6:00pm',
'7:00pm',
'8:00pm'
];
})
});
import Ember from 'ember';
export default Ember.Component.extend({
times: null,
totalMinutes: Ember.computed('times.@each.{start,end}', function() {
return this.get('times').reduce((total, { start, end }) => {
return total + (end - start);
}, 0);
})
});
import Ember from 'ember';
const { set } = Ember;
export default Ember.Controller.extend({
summaries: Ember.computed(function() {
return [
{
name: 'Alexander Hamilton',
times: [
{ start: 605, end: 615 },
{ start: 720, end: 780 }
]
},
{
name: 'Angelica Schuyler',
times: [
{ start: 663, end: 698 }
]
},
{
name: 'Marquis de Lafayette',
times: []
}
];
}),
name: 'Marquis de Lafayette',
start: 480,
end: 500,
actions: {
addTime(event) {
event.preventDefault();
const summary = this.get('summaries').findBy('name', this.get('name'));
summary.times.pushObject({ start: this.get('start'), end: this.get('end') });
},
clearTimes() {
this.get('summaries').forEach(summary => {
set(summary, 'times', []);
});
}
}
});
import Ember from 'ember';
export function eq([a, b]) {
return a === b;
}
export default Ember.Helper.helper(eq);
/*
General Positioning Styles
*/
html {
--fixed-column-width: 150px;
--column-spacing-width: 4px;
--fixed-column-width-with-spacing: calc(
var(--fixed-column-width) +
var(--column-spacing-width)
);
--hour-width: 60px;
--num-hours: 13;
--timeline-column-width: calc(var(--hour-width) * var(--num-hours) - 1px);
}
.wrapper {
position: relative;
max-width: calc(
(var(--column-spacing-width) * 3) +
(var(--fixed-column-width) * 2) +
var(--timeline-column-width)
);
background-color: #efefef;
}
.scroller {
margin-left: var(--fixed-column-width-with-spacing);
margin-right: var(--fixed-column-width-with-spacing);
overflow-x: scroll;
background-color: #e3eded;
}
/*
Table Styles
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
/*
This flexbox usage forces all the columns in the row to align at the top.
Without it, the absolutely positioned columns need to be next to each other in the DOM to align correctly - but having DOM order not match visual order poses accessibility concerns.
*/
tr {
display: flex;
}
th, td {
border: 1px solid black;
height: 30px;
}
/*
Column Positioning Styles
*/
.column--fixed-left {
position: absolute;
left: 0;
width: var(--fixed-column-width);
background-color: #fdfdfd;
}
.column--fixed-right {
position: absolute;
right: 0;
width: var(--fixed-column-width);
background-color: #fdfdfd;
}
.column--stretch {
position: relative;
display: flex;
width: var(--timeline-column-width);
overflow-x: visible;
background-color: #fdfdfd;
}
/*
Timeline Styles
*/
.hour-cell {
flex: 0 0 calc(
var(--hour-width) - 1px
);
border-right: 1px solid pink;
text-overflow: clip;
overflow-x: hidden;
}
.timeline-block {
position: absolute;
left: 0;
color: white;
background-color: blue;
}
{{timeline-table summaries=summaries}}
<h4>Add Time</h4>
<small>
<p>Hint: all time is calculated in minutes based on 24 hr time.</p>
<p>So 8:00am is (8 * 60) = 480 </p>
</small>
<form onsubmit={{action "addTime"}}>
<select onchange={{action (mut name) value="target.value"}}>
{{#each summaries as |summary|}}
<option value={{summary.name}} selected={{eq name summary.name}}>
{{summary.name}}
</option>
{{/each}}
</select>
<input type="number" min=480 max=1199 step=1 value={{readonly start}} onchange={{action (mut start) value="target.value"}}>
<input type="number" min=480 max=1199 step=1 value={{readonly end}} onchange={{action (mut end) value="target.value"}}>
<button type="submit">Add Time</button>
</form>
<h4>Clear Times</h4>
<button onclick={{action "clearTimes"}}>Clear Times</button>
<div class="wrapper">
<div class="scroller">
<table>
<tr>
<th class="column--fixed-left">Name</th>
<th class="column--stretch">
{{#each hours as |hour|}}
<div class="hour-cell">{{hour}}</div>
{{/each}}
</th>
<th class="column--fixed-right">Total Time on Stage</th>
</tr>
{{#each summaries as |summary|}}
<tr>
<td class="column--fixed-left">{{summary.name}}</td>
<td class="column--stretch">
{{#each hours as |hour|}}
<div class="hour-cell"></div>
{{/each}}
{{#each summary.times as |time|}}
{{timeline-block time=time}}
{{/each}}
</td>
<td class="column--fixed-right">
{{timeline-total times=summary.times}}
</td>
</tr>
{{/each}}
</table>
</div>
</div>
{
"version": "0.13.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.16.2",
"ember-template-compiler": "2.16.2",
"ember-testing": "2.16.2"
},
"addons": {
"ember-data": "2.16.3"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment