diff --git a/addon/components/content-panel.hbs b/addon/components/content-panel.hbs index ccb2811..586a776 100644 --- a/addon/components/content-panel.hbs +++ b/addon/components/content-panel.hbs @@ -3,15 +3,17 @@
- - - + {{#unless @hideCaret}} + + + + {{/unless}} {{#if @isLoading}} {{/if}} {{#if @titleIcon}} - - + + {{/if}}
diff --git a/addon/components/content-panel.js b/addon/components/content-panel.js index b5d4fdd..acc4584 100644 --- a/addon/components/content-panel.js +++ b/addon/components/content-panel.js @@ -2,31 +2,148 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +/** + * Component representing a content panel with toggle functionality. + * + * @class ContentPanelComponent + * @extends Component + */ export default class ContentPanelComponent extends Component { + /** + * Indicates whether the content panel is currently open. + * + * @property {boolean} isOpen - Whether the content panel is open or closed. + * @default false + * @public + */ @tracked isOpen = false; + /** + * Determines whether the toggle action should be triggered only when clicking on the caret (icon-container). + * + * @property {boolean} toggleOnCaretOnly - Whether to toggle the content panel only when clicking on the caret. + * @default false + * @public + */ + @tracked toggleOnCaretOnly = false; + + /** + * Array of icon containers used for checking if the user clicked on or within a caret. + * + * @property {HTMLElement[]} iconContainers - Array of icon containers. + * @default [] + * @private + */ + @tracked iconContainers = []; + + /** + * Initializes the content panel component. + * + * @constructor + * @public + */ constructor() { super(...arguments); this.isOpen = this.args.open === true; + this.toggleOnCaretOnly = this.args.toggleOnCaretOnly === true; if (typeof this.args.onInsert === 'function') { this.args.onInsert(...arguments); } } - @action toggle() { - this.isOpen = !this.isOpen; + /** + * API context for content panel component + * + * @memberof ContentPanelComponent + */ + context = { + toggle: this.toggleDropdown.bind(this), + open: this.open.bind(this), + close: this.close.bind(this), + }; + + /** + * Toggles the content panel's open/closed state based on click events. + * + * @method toggle + * @param {Event} event - The click event. + * @returns {void} + * @public + */ + @action toggle(event) { + // Fire onclick + this.fireOnClick(); + + // Check if the click target is within any of the icon containers + const clickedOnCaret = this.iconContainers.length > 0 && this.iconContainers.some((iconContainerNode) => iconContainerNode.contains(event.target)); + + // If we only want to toggle via the caret (icon-container) + if (clickedOnCaret) { + this.fireOnClickCaret(); + + if (this.toggleOnCaretOnly) { + // Run toggle + this.toggleDropdown(); + return; + } + } else { + this.fireOnClickPanelTitle(); + } + + // Do not toggle + if (this.toggleOnCaretOnly) { + return; + } + + // Toggle dropdown + this.toggleDropdown(); } + /** + * Opens the content panel. + * + * @method open + * @returns {void} + * @public + */ @action open() { this.isOpen = true; } + /** + * Closes the content panel. + * + * @method close + * @returns {void} + * @public + */ @action close() { this.isOpen = false; } + /** + * Tracks an icon container node to be used for checking if the user clicked on or within a caret. + * + * @method trackIconContainer + * @param {HTMLElement} node - The icon container node. + * @returns {void} + * @public + */ + @action trackIconContainer(node) { + this.iconContainers.pushObject(node); + } + + /** + * Handles the click event on a dropdown item. + * + * @method onDropdownItemClick + * @param {Object} action - The action associated with the clicked dropdown item. + * @param {Object} dd - The dropdown object. + * @returns {void} + * @public + */ @action onDropdownItemClick(action, dd) { if (typeof dd.actions === 'object' && typeof dd.actions.close === 'function') { dd.actions.close(); @@ -40,4 +157,59 @@ export default class ContentPanelComponent extends Component { action.onClick(action.context); } } + + /** + * Toggles the content panel's open/closed state and fires a callback. + * + * @method toggleDropdown + * @returns {void} + * @private + */ + toggleDropdown() { + this.isOpen = !this.isOpen; + + // Fire callback on toggle + if (typeof this.args.onToggle === 'function') { + this.args.onToggle(this.isOpen); + } + } + + /** + * Fires the onClick callback if provided. + * + * @method fireOnClick + * @returns {void} + * @private + */ + fireOnClick() { + if (typeof this.args.onClick === 'function') { + this.args.onClick(this.context); + } + } + + /** + * Fires the onClickCaret callback if provided. + * + * @method fireOnClickCaret + * @returns {void} + * @private + */ + fireOnClickCaret() { + if (typeof this.args.onClickCaret === 'function') { + this.args.onClickCaret(this.context); + } + } + + /** + * Fires the onClickPanelTitle callback if provided. + * + * @method fireOnClickPanelTitle + * @returns {void} + * @private + */ + fireOnClickPanelTitle() { + if (typeof this.args.onClickPanelTitle === 'function') { + this.args.onClickPanelTitle(this.context); + } + } } diff --git a/addon/components/countdown.hbs b/addon/components/countdown.hbs new file mode 100644 index 0000000..2480f18 --- /dev/null +++ b/addon/components/countdown.hbs @@ -0,0 +1,3 @@ +
+

{{this.remaining}}

+
\ No newline at end of file diff --git a/addon/components/countdown.js b/addon/components/countdown.js new file mode 100644 index 0000000..e1efbc6 --- /dev/null +++ b/addon/components/countdown.js @@ -0,0 +1,188 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { formatDuration, intervalToDuration } from 'date-fns'; +import { isArray } from '@ember/array'; +import { run } from '@ember/runloop'; +import { computed } from '@ember/object'; + +export default class CountdownComponent extends Component { + /** + * An array that defines the units to display in the countdown. + * + * @memberof CountdownComponent + * @type {Array} + * @default ['seconds'] + */ + @tracked display = ['seconds']; + + /** + * The remaining time in the countdown. + * + * @memberof CountdownComponent + * @type {string} + */ + @tracked remaining; + + /** + * The duration of the countdown, specified in days, hours, minutes, and seconds. + * + * @memberof CountdownComponent + * @type {Object} + */ + @tracked duration = {}; + + /** + * The interval ID for the countdown timer. + * + * @memberof CountdownComponent + * @type {number} + */ + @tracked interval; + + /** + * Creates an instance of CountdownComponent. + * + * @param {Object} owner - The owning object. + * @param {Object} options - Options for configuring the countdown. + * @param {Date} options.expiry - The expiration date for the countdown. + * @param {number} options.hours - The initial hours for the countdown. + * @param {number} options.minutes - The initial minutes for the countdown. + * @param {number} options.seconds - The initial seconds for the countdown. + * @param {number} options.days - The initial days for the countdown. + * @param {(string|Array)} options.display - The units to display in the countdown. + */ + constructor(owner, { expiry, hours, minutes, seconds, days, display }) { + super(...arguments); + + this.setDuration( + { + days, + hours, + minutes, + seconds, + }, + expiry + ); + + if (display) { + if (typeof display === 'string' && display.includes(',')) { + display = display.split(','); + } + + if (isArray(display)) { + this.display = display; + } + } + + this.startCountdown(); + } + + @computed('remaining', 'duration') get remainingClass() { + // Customize the threshold and class names as needed + if (this.remaining && this.durationToSeconds(this.duration) <= 5) { + return 'remaining-low'; // Add a CSS class for low time + } else { + return 'remaining-normal'; // Add a default CSS class + } + } + + setDuration(duration = {}, expiry) { + if (expiry instanceof Date) { + // use the date provided to set the hours minutes seconds + duration = intervalToDuration({ start: new Date(), end: expiry }); + } + + // handle when only 2 minutes + if (duration && duration.minutes < 3) { + duration = { + ...duration, + seconds: duration.seconds + duration.minutes * 60, + minutes: 0, + }; + } + + this.duration = duration; + } + + /** + * Starts the countdown timer. + * + * @memberof CountdownComponent + * @method + */ + startCountdown() { + this.interval = setInterval(() => { + run(() => { + let { duration } = this; + + // if onlyDisplaySeconds === true + if (this.args.onlyDisplaySeconds === true) { + duration = { + seconds: this.durationToSeconds(this.duration), + }; + } + + this.remaining = formatDuration(duration); + + // decrement seconds + duration.seconds--; + + // set duration + if (duration.seconds < 0) { + duration.seconds = 0; // Stop the countdown at 0 + clearInterval(this.interval); + + if (typeof this.args.onCountdownEnd === 'function') { + this.args.onCountdownEnd(); + } + + if (typeof this.args.onEnd === 'function') { + this.args.onEnd(); + } + } + }); + }, 1000); + } + + /** + * Converts the duration object to total seconds. + * + * @memberof CountdownComponent + * @method + * @param {Object} duration - The duration object. + * @returns {number} - The total seconds. + */ + durationToSeconds(duration) { + const { years = 0, months = 0, weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0 } = duration; + const totalSeconds = years * 365 * 24 * 60 * 60 + months * 30 * 24 * 60 * 60 + weeks * 7 * 24 * 60 * 60 + days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds; + + return totalSeconds; + } + + /** + * Restarts the countdown by resetting the timeRemaining property and clearing the interval. + * + * @method restartCountdown + */ + restartCountdown() { + clearInterval(this.interval); + // Reset properties + this.remaining = null; + this.duration = { + days: this.args.days || 0, + hours: this.args.hours || 0, + minutes: this.args.minutes || 0, + seconds: this.args.seconds || 0, + }; + this.startCountdown(); + } + + /** + * Cleans up the interval when the component is being destroyed. + * @method willDestroy + */ + willDestroy() { + super.willDestroy(...arguments); + clearInterval(this.interval); + } +} diff --git a/addon/components/layout/section/header.hbs b/addon/components/layout/section/header.hbs index 740b721..09dd0fe 100644 --- a/addon/components/layout/section/header.hbs +++ b/addon/components/layout/section/header.hbs @@ -17,7 +17,7 @@ {{/if}}
{{#if @onSearch}} - + {{/if}}
{{#unless @hideActions}} diff --git a/addon/components/layout/sidebar/item.hbs b/addon/components/layout/sidebar/item.hbs index 8669837..25ec162 100644 --- a/addon/components/layout/sidebar/item.hbs +++ b/addon/components/layout/sidebar/item.hbs @@ -21,7 +21,7 @@ {{/if}} {{#if @dropdownButton}} - +