diff --git a/dev-app/app.html b/dev-app/app.html
index 47ec0e33..d7558d78 100644
--- a/dev-app/app.html
+++ b/dev-app/app.html
@@ -24,7 +24,7 @@
state="${link.isActive ? 'active' : ''}"
>
-
+
diff --git a/dev-app/routes/components/timeline/demo/index.html b/dev-app/routes/components/timeline/demo/index.html
index 759c873d..56ce6a1e 100644
--- a/dev-app/routes/components/timeline/demo/index.html
+++ b/dev-app/routes/components/timeline/demo/index.html
@@ -13,7 +13,7 @@
snap-add.bind="true"
zoom-level.bind="zoomLevel"
is-loading.bind="loading"
- prevent-create.call="preventCreate(isoTime)"
+ prevent-create.bind="preventCreate"
snap-add.bind="true"
>
diff --git a/dev-app/routes/components/timeline/demo/index.ts b/dev-app/routes/components/timeline/demo/index.ts
index b5da718c..238cd175 100644
--- a/dev-app/routes/components/timeline/demo/index.ts
+++ b/dev-app/routes/components/timeline/demo/index.ts
@@ -168,10 +168,10 @@ export class TimelineExample {
// },
];
- public zoomLevel = 5;
+ public zoomLevel = 2;
public displayView = 'three-day';
public loading = false;
- public preventCreate = _isoTime => false;
+ public preventCreate = false;
// constructor() {
// const genRandom = (min, max) => Math.random() * (max - min + 1) + min;
diff --git a/package-lock.json b/package-lock.json
index f23b1e44..71bf8b61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@bindable-ui/bindable",
- "version": "1.0.23",
+ "version": "1.0.24",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index a5e2d408..eea377d2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@bindable-ui/bindable",
"description": "An Aurelia component library",
- "version": "1.0.23",
+ "version": "1.0.24",
"repository": {
"type": "git",
"url": "https://github.com/bindable-ui/bindable"
diff --git a/src/components/forms/date/c-form-date/c-form-date.ts b/src/components/forms/date/c-form-date/c-form-date.ts
index ac028cdd..ea6a8e4d 100644
--- a/src/components/forms/date/c-form-date/c-form-date.ts
+++ b/src/components/forms/date/c-form-date/c-form-date.ts
@@ -3,24 +3,14 @@ Copyright 2020, Verizon Media
Licensed under the terms of the MIT license. See the LICENSE file in the project root for license terms.
*/
-import {bindable, bindingMode, computedFrom, containerless} from 'aurelia-framework';
-import * as datetimepicker from 'eonasdan-bootstrap-datetimepicker';
+import {bindable, bindingMode, containerless} from 'aurelia-framework';
import * as moment from 'moment';
import {authState} from '../../../../decorators/auth-state';
import * as styles from './c-form-date.css.json';
-declare const window: any;
declare let $: any;
-if (window.$) {
- $ = window.$;
-}
-
-$.fn.extend({
- datetimepicker,
-});
-
/**
* @param id {String} - Element ID.
* @param timestamp {Number} - Unix timestamp.
diff --git a/src/components/timeline/c-timeline/c-timeline.test.ts b/src/components/timeline/c-timeline/c-timeline.test.ts
index 6b4d0990..75d581e1 100644
--- a/src/components/timeline/c-timeline/c-timeline.test.ts
+++ b/src/components/timeline/c-timeline/c-timeline.test.ts
@@ -4,6 +4,7 @@ Licensed under the terms of the MIT license. See the LICENSE file in the project
*/
import {TaskQueue} from 'aurelia-framework';
+import * as moment from 'moment';
import {instance, mock} from 'ts-mockito';
import {CTimeline, ZOOM_LEVELS} from './c-timeline';
@@ -13,6 +14,42 @@ import {CToastsService} from '../../toasts/c-toasts/c-toasts-service';
const taskQueue = mock(TaskQueue);
const toastsService = mock(CToastsService);
+// Mock _.debounce
+// @ts-ignore
+jest.spyOn(_, 'debounce').mockImplementation(fn => fn);
+
+const now = moment('12/12/2020', 'MM/DD/YYYY')
+ .startOf('day')
+ .add(12, 'hours');
+const startDay = moment(now)
+ .startOf('day')
+ .toISOString();
+
+const sortedEntries: any[] = [
+ {
+ duration: 240,
+ start: now.toISOString(),
+ },
+ {
+ duration: 120,
+ start: moment(now)
+ .add(1, 'hour')
+ .toISOString(),
+ },
+ {
+ duration: 120,
+ start: moment(now)
+ .add(1, 'hour')
+ .toISOString(),
+ },
+ {
+ duration: 120,
+ start: moment(now)
+ .add(1, 'day')
+ .toISOString(),
+ },
+];
+
describe('c-timeline-block element', () => {
let component;
@@ -30,7 +67,26 @@ describe('c-timeline-block element', () => {
describe('Unit', () => {
beforeEach(() => {
+ jest.useFakeTimers();
component = new CTimeline(instance(taskQueue), instance(toastsService));
+
+ component.date = startDay;
+ component.entries = sortedEntries;
+ component.placeholderEntry = {
+ openPopover: jest.fn(),
+ };
+
+ component.attached();
+
+ component.parentScrollElem = {
+ offset: () => ({top: 0}),
+ scrollTop: () => 0,
+ };
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
});
describe('#mapAllowedTimes', () => {
@@ -73,6 +129,57 @@ describe('c-timeline-block element', () => {
});
});
+ describe('#togglePopover', () => {
+ test('regular interval positioning', () => {
+ const ev = {
+ layerY: 10,
+ pageY: 10,
+ };
+
+ component.togglePopover(ev);
+ expect(component.newItem.title).toBe('00:00 (New Item)');
+
+ ev.layerY = 30;
+ component.togglePopover(ev);
+ expect(component.newItem.title).toBe('00:15 (New Item)');
+
+ ev.pageY = 60;
+ component.togglePopover(ev);
+ expect(component.newItem.title).toBe('00:45 (New Item)');
+ });
+
+ test('snap add positioning', () => {
+ const ev = {
+ layerY: 10,
+ pageY: 1600,
+ };
+
+ component.snapAdd = true;
+
+ // No snapping
+ component.togglePopover(ev);
+ expect(component.newItem.title).toBe('16:00 (New Item)');
+
+ // Snapping
+ ev.pageY = 1200;
+ component.togglePopover(ev);
+ expect(component.newItem.title).toBe('12:04 (New Item)');
+ });
+
+ test('opening popover', () => {
+ const ev = {
+ layerY: 10,
+ pageY: 10,
+ };
+
+ component.togglePopover(ev);
+
+ jest.runOnlyPendingTimers();
+
+ expect(component.placeholderEntry.openPopover).toHaveBeenCalled();
+ });
+ });
+
describe('#getHoursMinutes', () => {
let data;
diff --git a/src/components/timeline/c-timeline/c-timeline.ts b/src/components/timeline/c-timeline/c-timeline.ts
index 722e8b74..adc79b74 100644
--- a/src/components/timeline/c-timeline/c-timeline.ts
+++ b/src/components/timeline/c-timeline/c-timeline.ts
@@ -209,6 +209,8 @@ export class CTimeline {
this.timeView,
this.editEntryViewModel,
this.date,
+ this.getTzOffet(),
+ this.zoomLevel,
);
}
@@ -227,6 +229,8 @@ export class CTimeline {
this.timeView,
this.editEntryViewModel,
this.date,
+ this.getTzOffet(),
+ this.zoomLevel,
);
startTime.add(1, 'day');
@@ -438,6 +442,11 @@ export class CTimeline {
}
public attached() {
+ // Trigger the default TZ stuff
+ if (!this.timezone) {
+ this.setupTimezone();
+ }
+
this.getParentScrollElem();
this.buildTimeline();
@@ -496,16 +505,7 @@ export class CTimeline {
}
public timezoneChanged() {
- const tzNames = moment.tz.names();
- const tzIndex = tzNames.findIndex(name => name === this.timezone);
-
- if (tzIndex > -1) {
- moment.tz.setDefault(this.timezone);
- } else {
- moment.tz.setDefault();
- }
-
- this.renderTimeline();
+ this.setupTimezone(true);
}
public scrollTimeChanged(_new, old) {
@@ -535,6 +535,42 @@ export class CTimeline {
// Private methods
+ /**
+ * Return the offset in minutes from the selected timezone to the browser timezone
+ */
+ private getTzOffet(): number {
+ const browserOffset = moment.tz.zone(moment.tz.guess()).utcOffset(moment());
+ const offset = moment().utcOffset();
+
+ return browserOffset + offset;
+ }
+
+ /**
+ * Sets up the timezone data
+ *
+ * @param updateDate Boolean value to determine whether or not to update `this.date`
+ */
+ private setupTimezone(updateDate?: boolean) {
+ const tzNames = moment.tz.names();
+ const tzIndex = tzNames.findIndex(name => name === this.timezone);
+
+ if (tzIndex > -1) {
+ moment.tz.setDefault(this.timezone);
+ } else {
+ moment.tz.setDefault();
+ }
+
+ if (updateDate) {
+ // This prevents issues where the date will look in the past and throw off the three-day view
+ const offset = this.getTzOffet();
+ this.date = moment(this.date)
+ .add(offset * -1, 'minutes')
+ .toISOString();
+ }
+
+ this.renderTimeline();
+ }
+
/**
* Get the closest parent element that scrolls
*/
diff --git a/src/components/timeline/c-timeline/workers.test.ts b/src/components/timeline/c-timeline/workers.test.ts
index f37e39bf..1d5a442f 100644
--- a/src/components/timeline/c-timeline/workers.test.ts
+++ b/src/components/timeline/c-timeline/workers.test.ts
@@ -55,21 +55,21 @@ describe('Web worker functions', () => {
describe('#mapEntries', () => {
it('tests formatting', async () => {
- const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString());
+ const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString(), 0, 2);
expect(data[0].startTime).toBe('12:00');
expect(data[0].endTime).toBe('12:04');
});
it('tests positioning', async () => {
- const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString());
+ const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString(), 0, 2);
expect(data[0].top).toBe(1440);
expect(data[0].height).toBe(8);
});
it('tests same time entries', async () => {
- const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString());
+ const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString(), 0, 2);
expect(data[0].widthCalc).toBeUndefined();
expect(data[2].widthCalc).toBeDefined();
@@ -77,7 +77,7 @@ describe('Web worker functions', () => {
});
it('tests nested entries', async () => {
- const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString());
+ const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString(), 0, 2);
expect(data[0].widthCalc).toBeUndefined();
expect(data[1].widthCalc).toBeDefined();
diff --git a/src/components/timeline/c-timeline/workers.ts b/src/components/timeline/c-timeline/workers.ts
index 0972a41c..157a60a1 100644
--- a/src/components/timeline/c-timeline/workers.ts
+++ b/src/components/timeline/c-timeline/workers.ts
@@ -32,6 +32,8 @@ function mapEntriesFn(
timeView: string,
editEntryViewModel: string,
date: string,
+ tzOffset: number,
+ zoomLevel: number,
) {
const SECONDS_IN_MINUTE = 60;
@@ -44,10 +46,17 @@ function mapEntriesFn(
};
const formatHHmm = isoString => {
- const dateObj = new Date(isoString);
+ const dateObj = new Date(new Date(isoString).getTime() + tzOffset * 60 * 1000);
return `${appendLeadingZeroes(dateObj.getHours())}:${appendLeadingZeroes(dateObj.getMinutes())}`;
};
+ const formatHHmmss = isoString => {
+ const dateObj = new Date(new Date(isoString).getTime() + tzOffset * 60 * 1000);
+ return `${appendLeadingZeroes(dateObj.getHours())}:${appendLeadingZeroes(
+ dateObj.getMinutes(),
+ )}:${appendLeadingZeroes(dateObj.getSeconds())}`;
+ };
+
const upToMm = isoString => {
const dateObj = new Date(isoString);
const year = dateObj.getFullYear();
@@ -75,13 +84,13 @@ function mapEntriesFn(
entry.start = startTime;
}
- entry.startTime = formatHHmm(entry.start);
+ entry.startTime = zoomLevel === 5 ? formatHHmmss(entry.start) : formatHHmm(entry.start);
if (!entry.end) {
entry.end = new Date(new Date(entry.start).getTime() + entry.duration * 1000).toISOString();
}
- entry.endTime = formatHHmm(entry.end);
+ entry.endTime = zoomLevel === 5 ? formatHHmmss(entry.end) : formatHHmm(entry.end);
const entryStartDate: any = new Date(entry.start);
const startTimeDate: any = new Date(startTime);
@@ -225,6 +234,8 @@ export const mapEntries = async (
timeView: string,
editEntryViewModel: string,
date: string,
+ tzOffset: number,
+ zoomLevel: number,
): Promise => {
if (window.Worker) {
return await worker.postMessage('mapEntries', [
@@ -235,10 +246,22 @@ export const mapEntries = async (
timeView,
editEntryViewModel,
date,
+ tzOffset,
+ zoomLevel,
]);
}
- return mapEntriesFn(sortedEntries, pxPerMinute, startTime, endTime, timeView, editEntryViewModel, date);
+ return mapEntriesFn(
+ sortedEntries,
+ pxPerMinute,
+ startTime,
+ endTime,
+ timeView,
+ editEntryViewModel,
+ date,
+ tzOffset,
+ zoomLevel,
+ );
};
export const filterEntriesDay = async (sortedEntries: any[], startTime: string, endTime: string): Promise => {
diff --git a/src/index.ts b/src/index.ts
index b3ff2b59..325f8b1e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,6 +3,14 @@ Copyright 2020, Verizon Media
Licensed under the terms of the MIT license. See the LICENSE file in the project root for license terms.
*/
+// Setup jQuery
+import * as datetimepicker from 'eonasdan-bootstrap-datetimepicker';
+import * as $ from 'jquery';
+
+$.fn.extend({
+ datetimepicker,
+});
+
import {FrameworkConfiguration} from 'aurelia-framework';
import {PLATFORM} from 'aurelia-pal';