diff --git a/dev-app/app.html b/dev-app/app.html index de632c2f..47ec0e33 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/properties/index.ts b/dev-app/routes/components/timeline/properties/index.ts index e247687c..7ea738db 100644 --- a/dev-app/routes/components/timeline/properties/index.ts +++ b/dev-app/routes/components/timeline/properties/index.ts @@ -121,10 +121,10 @@ export class TimelineProperties { type: 'boolean', }, { - default: '() => false', - description: 'Callback to determine if a click on the timeline should prevent adding a new entry.', + default: 'null', + description: 'Callback to determine if a click on the timeline should prevent adding a new entry. Alternatively can just be a boolean.', name: 'preventCreate', - type: '(isoString: string) => boolean', + type: '(isoString: string) => boolean || boolean', }, { default: 'null', diff --git a/package-lock.json b/package-lock.json index 7fe10bd0..04ca1fed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bindable-ui/bindable", - "version": "1.0.21", + "version": "1.0.22", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f86db176..bb8b11ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@bindable-ui/bindable", "description": "An Aurelia component library", - "version": "1.0.21", + "version": "1.0.22", "repository": { "type": "git", "url": "https://github.com/bindable-ui/bindable" diff --git a/src/components/timeline/c-time-entry/c-time-entry.ts b/src/components/timeline/c-time-entry/c-time-entry.ts index 9d7522b3..e3cf4794 100644 --- a/src/components/timeline/c-time-entry/c-time-entry.ts +++ b/src/components/timeline/c-time-entry/c-time-entry.ts @@ -23,6 +23,11 @@ export class CTimeEntry { constructor(public element: Element, private vPopoverService: CPopoverService) {} public openPopover($event) { + if ($event) { + $event.preventDefault(); + $event.stopPropagation(); + } + if (!this.item) { return; } diff --git a/src/components/timeline/c-timeline-block/c-timeline-block.html b/src/components/timeline/c-timeline-block/c-timeline-block.html index ff74927d..d5555710 100644 --- a/src/components/timeline/c-timeline-block/c-timeline-block.html +++ b/src/components/timeline/c-timeline-block/c-timeline-block.html @@ -6,10 +6,8 @@ diff --git a/src/components/timeline/c-timeline-block/c-timeline-block.ts b/src/components/timeline/c-timeline-block/c-timeline-block.ts index 104daef5..4a09d95f 100644 --- a/src/components/timeline/c-timeline-block/c-timeline-block.ts +++ b/src/components/timeline/c-timeline-block/c-timeline-block.ts @@ -4,66 +4,12 @@ Licensed under the terms of the MIT license. See the LICENSE file in the project */ import {bindable} from 'aurelia-framework'; -import * as moment from 'moment'; -import {authState} from '../../../decorators/auth-state'; import * as styles from './c-timeline-block.css.json'; -@authState export class CTimelineBlock { @bindable public time: string; - @bindable - public isoTime: string; - @bindable - public addEntryOffset: ({isoTime, mouseOffset}: {isoTime: string; mouseOffset: number}) => [string, number]; - @bindable - public newEntryViewModel: string; public styles = styles; - - private newItem: any = null; - private placeholderEntry; - @bindable - public preventCreate: ({isoTime}?: {isoTime: string}) => boolean = _isoTime => false - - public togglePopoverFunction($event) { - if (!this.isoTime || this.preventCreate({isoTime: this.isoTime}) || !_.isFunction(this.addEntryOffset)) { - return; - } - - if (!this.newItem) { - const mouseOffset = $event && $event.layerY ? $event.layerY : 0; - let top; - let isoTime; - - [isoTime, top] = this.addEntryOffset({isoTime: this.isoTime, mouseOffset}); - - if (!_.isNumber(top)) { - top = mouseOffset; - } - - if (!moment(isoTime).isValid()) { - isoTime = this.isoTime; - } - - this.newItem = { - isoTime, - top, - blockIsoTime: this.isoTime, - color: 'secondary', - height: 50, - placeholder: true, - title: `${moment(isoTime).format('HH:mm')} (New Item)`, - }; - - if (this.newEntryViewModel) { - this.newItem.contentViewModel = this.newEntryViewModel; - } - } - - _.defer(() => { - this.placeholderEntry.openPopover(); - }); - } } diff --git a/src/components/timeline/c-timeline-container/c-timeline-container.ts b/src/components/timeline/c-timeline-container/c-timeline-container.ts index ac4e4879..caa5cc5b 100644 --- a/src/components/timeline/c-timeline-container/c-timeline-container.ts +++ b/src/components/timeline/c-timeline-container/c-timeline-container.ts @@ -3,13 +3,12 @@ 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, containerless} from 'aurelia-framework'; +import {bindable} from 'aurelia-framework'; import {generateRandom} from '../../../helpers/generate-random'; import * as styles from './c-timeline-container.css.json'; -@containerless export class CTimelineContainer { @bindable public currentTimeTop = -1; diff --git a/src/components/timeline/c-timeline/c-timeline-interfaces.ts b/src/components/timeline/c-timeline/c-timeline-interfaces.ts index b43c1663..c334d300 100644 --- a/src/components/timeline/c-timeline/c-timeline-interfaces.ts +++ b/src/components/timeline/c-timeline/c-timeline-interfaces.ts @@ -27,6 +27,8 @@ export interface ITimeEntry extends ITimeEntryBasic { contentViewModel?: string; endTime: string; height: number; + isoTime?: string; + placeholder?: boolean; rightCalc?: number; shiftIcons?: boolean; startTime: string; @@ -35,11 +37,9 @@ export interface ITimeEntry extends ITimeEntryBasic { } export interface ITimeBlock { - addNewMiddle?: boolean; - addNewTop?: boolean; isoTime?: string; - showTime?: boolean; time: string; + newItem?: any; } export interface ITimeDay { @@ -47,6 +47,8 @@ export interface ITimeDay { blocks: ITimeBlock[]; date: string; entries: ITimeEntry[]; + newItem?: any; parsedDate?: string; + placeholderEntry?: any; today?: boolean; } diff --git a/src/components/timeline/c-timeline/c-timeline.html b/src/components/timeline/c-timeline/c-timeline.html index bdb97ffb..a1b0271d 100644 --- a/src/components/timeline/c-timeline/c-timeline.html +++ b/src/components/timeline/c-timeline/c-timeline.html @@ -13,14 +13,11 @@ current-time-top.bind="currentTimeLine" loading-top.bind="isLoadingTop" loading-bottom.bind="isLoadingBottom" + click.trigger="togglePopover($event)" > + +
- +
- - - + + + + +
diff --git a/src/components/timeline/c-timeline/c-timeline.ts b/src/components/timeline/c-timeline/c-timeline.ts index aa7817c1..722e8b74 100644 --- a/src/components/timeline/c-timeline/c-timeline.ts +++ b/src/components/timeline/c-timeline/c-timeline.ts @@ -13,6 +13,8 @@ import {authState} from '../../../decorators/auth-state'; import {generateRandom} from '../../../helpers/generate-random'; import {CToastsService} from '../../toasts/c-toasts/c-toasts-service'; +import * as CTimelineWeekContainerStyles from '../c-timeline-week-container/c-timeline-week-container.css.json'; + type Moment = moment.Moment; /** @@ -45,11 +47,12 @@ export const ZOOM_LEVELS: any = [ }, ]; +export const BLOCK_HEIGHT = 50; + const DEFAULT_ZOOM = 2; const MINUTES_IN_DAY = 1440; const HOURS_IN_DAY = 24; const TIME_REGEX = /^[0-9]{1,4}$/; -const BLOCK_HEIGHT = 50; const SECONDS_IN_MINUTE = 60; /** @@ -158,6 +161,8 @@ export class CTimeline { public id = generateRandom(); public currentScroll: number = 0; public preventScrollCheck: boolean = true; + public placeholderEntry: any; + public newItem: any = null; /** * Build out the timeline. Put in a throttle so it doesn't bind up @@ -256,6 +261,120 @@ export class CTimeline { }); }, 100); + public togglePopover = _.debounce( + ($event, day?: ITimeDay) => { + if ( + // @ts-ignore + this._state === 'disabled' || + (_.isBoolean(this.preventCreate) && this.preventCreate) + ) { + return; + } + + let isoTime; + const dayWeek = day; + + const dayDate = day ? dayWeek.date : null; + const [zoomLevelData, pxPerMinute] = this.getZoomLevelData(); + const [blockStart] = this.getDayStartEndTimes(dayDate); + + const relativeY = $event.pageY - this.parentScrollElem.offset().top; + // On week view, get the height of the date names so we can offset + const elemDiff = day ? $(`.${CTimelineWeekContainerStyles.dates}`).outerHeight() : 0; + const pxDown = relativeY + this.parentScrollElem.scrollTop() - elemDiff; + const blockIndex = Math.floor(pxDown / BLOCK_HEIGHT); + + if (dayWeek) { + dayWeek.newItem = null; + isoTime = dayWeek.blocks[blockIndex].isoTime; + + _.forEach(this.displayDays, weekDay => (weekDay.newItem = null)); + } else { + this.newItem = null; + isoTime = this.blocks[blockIndex].isoTime; + } + + const minutesFromTop = pxDown / pxPerMinute; + + const clickedTime = moment(blockStart) + .add(minutesFromTop, 'minutes') + .startOf(this.zoomLevel < 5 ? 'minute' : 'second'); + const startTime: Moment = moment(clickedTime).subtract(zoomLevelData.minutes / 2, 'minutes'); + const endTime: Moment = moment(clickedTime).add(zoomLevelData.minutes / 2, 'minutes'); + + const matchingEntries: ITimeEntry[] = []; + + const entries = dayWeek ? dayWeek.entries : this.transformedEntries; + + if (this.snapAdd) { + (matchingEntries as any[]) = _.filter(entries, entry => + moment(entry.end).isBetween(startTime, endTime, null, '[)'), + ); + } + + let top = blockIndex * BLOCK_HEIGHT; + let startIso = null; + + if (!matchingEntries.length) { + const isoTimeMoment = moment(isoTime); + const halfBlock = BLOCK_HEIGHT / 2; + + if ($event.layerY >= halfBlock) { + top += halfBlock; + isoTimeMoment.add((halfBlock / pxPerMinute) * SECONDS_IN_MINUTE, 'seconds'); + } + + startIso = isoTimeMoment.toISOString(); + } else { + const sortedEntries = _.sortBy(matchingEntries, entry => + Math.abs(moment(entry.end).diff(clickedTime, 'seconds')), + ); + const firstEntry = _.first(sortedEntries); + const diff = Math.ceil(moment(isoTime).diff(firstEntry.end, 'seconds')) * -1; + + top += Math.floor((diff / SECONDS_IN_MINUTE) * pxPerMinute); + startIso = firstEntry.end; + } + + if (_.isFunction(this.preventCreate) && this.preventCreate({isoTime: startIso})) { + return; + } + + const newItem: any = { + top, + color: 'secondary', + height: 50, + isoTime: startIso, + placeholder: true, + title: `${moment(startIso).format(this.zoomLevel < 5 ? 'HH:mm' : 'HH:mm:ss')} (New Item)`, + }; + + if (this.newEntryViewModel) { + newItem.contentViewModel = this.newEntryViewModel; + } + + if (!dayWeek) { + this.newItem = _.cloneDeep(newItem); + + _.defer(() => { + if (this.placeholderEntry) { + this.placeholderEntry.openPopover(); + } + }); + } else { + dayWeek.newItem = _.cloneDeep(newItem); + + _.defer(() => { + if (dayWeek.placeholderEntry) { + dayWeek.placeholderEntry.openPopover(); + } + }); + } + }, + 500, + {leading: true, trailing: false}, + ); + private calculateCurrentTimeLine = _.throttle( () => { if (this.timeView === 'month') { @@ -404,81 +523,6 @@ export class CTimeline { this.scrollToSpot(this.scrollTime); } - /** - * Passed into `c-time-block` elements. Allows you to set spot for placeholder - * new time entry while popover is showing. - * - * @param isoTime ISO Date string - * @param mouseOffset How many pixels down the mouse was clicked - */ - public calculatePlaceholder(isoTime: string, mouseOffset: number): [string, number] { - const zoomLevelData = ZOOM_LEVELS[this.zoomLevel]; - const pxPerMinute = BLOCK_HEIGHT / zoomLevelData.minutes; - const offsetMinutes = mouseOffset / pxPerMinute; - - // Buffer around clicked time to snap - const clickedTime = moment(isoTime) - .add(offsetMinutes, 'minutes') - .startOf(this.zoomLevel < 5 ? 'minute' : 'second'); - const startTime = moment(clickedTime).subtract(zoomLevelData.minutes, 'minutes'); - const endTime = moment(clickedTime).add(zoomLevelData.minutes, 'minutes'); - - let matchingEntries: any[] = []; - - if (this.snapAdd) { - if (this.timeView === 'day') { - matchingEntries = _.filter(this.transformedEntries, entry => { - return moment(entry.end).isBetween(startTime, endTime, null, '[)'); - }); - } else { - _.forEach(this.displayDays, day => { - const dayMatches = _.filter(day.entries, entry => { - return moment(entry.end).isBetween(startTime, endTime, null, '[)'); - }); - - matchingEntries = [...matchingEntries, ...dayMatches]; - }); - } - } - - let offset = 0; - - if (!matchingEntries.length) { - const isoTimeMoment = moment(isoTime); - const halfBlock = BLOCK_HEIGHT / 2; - - if (mouseOffset >= halfBlock) { - offset = halfBlock; - isoTimeMoment.add((halfBlock / pxPerMinute) * SECONDS_IN_MINUTE, 'seconds'); - } - - return [isoTimeMoment.toISOString(), offset]; - } - - const sortedEntries = _.sortBy(matchingEntries, entry => - Math.abs(moment(entry.end).diff(clickedTime, 'seconds')), - ); - const firstEntry = _.first(sortedEntries); - const diff = Math.ceil(moment(isoTime).diff(firstEntry.end, 'seconds')) * -1; - offset = Math.floor((diff / SECONDS_IN_MINUTE) * pxPerMinute); - - return [firstEntry.end, offset]; - } - - /** - * Passed into `c-time-block` elements. Allows you to check if clicking in a block - * will allow you to create a new entry. - * - * @param isoTime ISO Date string - */ - public checkPreventAdd(isoTime) { - if (!_.isFunction(this.preventCreate)) { - return false; - } - - return this.preventCreate({isoTime}); - } - /** * Changes week view to day view and navigates to that date * @@ -848,7 +892,7 @@ export class CTimeline { /** * Calculate the times for when the day starts */ - private getDayStartEndTimes(): [Moment, Moment] { + private getDayStartEndTimes(date?): [Moment, Moment] { const zoomLevelData = ZOOM_LEVELS[this.zoomLevel]; let startTime; @@ -884,6 +928,10 @@ export class CTimeline { } } + if (date) { + startTime = moment(date).startOf('day'); + } + const blockTime = this.blocks[0].time; const numOfBlocks = this.blocks.length; addHours = moment(blockTime, 'HH:mm').format('H'); @@ -892,7 +940,7 @@ export class CTimeline { startTime.add(addHours, 'hours').add(addMinutes, 'minutes'); endTime = moment(startTime).add(numOfBlocks * zoomLevelData.minutes, 'minutes'); - return [startTime, endTime]; + return [moment(startTime), moment(endTime)]; } private getZoomLevelData() { diff --git a/src/index.ts b/src/index.ts index dd41a248..b3ff2b59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import {CToastsService} from './components/toasts/c-toasts/c-toasts-service'; import {dirtyCheckPrompt} from './decorators/dirty-check-prompt/index'; // Value Converters +import {AsyncBindingBehavior} from './value-converters/async-binding'; import {BooleanYesNoValueConverter} from './value-converters/boolean-yes-no'; import {CapitalizeValueConverter} from './value-converters/capitalize'; import {CountValueConverter} from './value-converters/count'; @@ -122,6 +123,7 @@ export function configure(config: FrameworkConfiguration) { PLATFORM.moduleName('./value-converters/string-to-number'), PLATFORM.moduleName('./value-converters/th-class-for'), PLATFORM.moduleName('./value-converters/vsort'), + PLATFORM.moduleName('./value-converters/async-binding'), // Components PLATFORM.moduleName('./components/copy/c-copy/c-copy'), @@ -266,6 +268,7 @@ export { StringToNumberValueConverter, ThClassForValueConverter, CsortValueConverter, + AsyncBindingBehavior, }; // Interfaces diff --git a/src/value-converters/async-binding.ts b/src/value-converters/async-binding.ts new file mode 100644 index 00000000..6c4ba94f --- /dev/null +++ b/src/value-converters/async-binding.ts @@ -0,0 +1,20 @@ +/** + * This will allow you to pipe an promise to a value converter + * + * ex. `value | value-converter & async` + */ +export class AsyncBindingBehavior { + public bind(binding) { + binding.originalupdateTarget = binding.updateTarget; + + binding.updateTarget = async a => { + const d = await a; + binding.originalupdateTarget(d); + }; + } + + public unbind(binding) { + binding.updateTarget = binding.originalupdateTarget; + binding.originalupdateTarget = null; + } +}