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';