/**
* A Backbone view that renders a single date bucket within the version history
* timeline, including the list of ObjectVersionView entries for that date.
*/
define([
"backbone",
"collections/DataONEObjects",
"common/DateUtility",
"views/versionHistory/ObjectVersionsView",
], (Backbone, DataONEObjects, DateUtility, ObjectVersionsView) => {
"use strict";
const DATE_NOTE_ICON_LABEL =
"Note on dates: Upload dates may appear out of order due to later curation.";
// SVG for the timeline point, included inline to avoid an extra request and
// allow CSS styling
const TIMELINE_POINT_SVG = `<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/></svg>`;
// Base class for the timeline group, used in the className property and for
// scoping styles
const BASE_CLASS = "version-history-group";
// All class names used in this view, for easy reference and to avoid typos
const CLASS_NAMES = {
container: BASE_CLASS,
hidden: `version-history--hidden`,
dateNote: `${BASE_CLASS}--date-note`,
timelinePoint: `${BASE_CLASS}__timeline-point`,
timelinePointDateNoteIcon: `${BASE_CLASS}__point-note-icon`,
timelineContent: `${BASE_CLASS}__timeline-content`,
header: `${BASE_CLASS}__header`,
dateTitle: `${BASE_CLASS}__date-title`,
objectVersionsList: `object-versions`,
};
/**
* @class VersionTimelineGroupView
* @classdesc A group of object versions for a specific date within the
* version history timeline.
* @classcategory Views/VersionHistory
* @augments Backbone.View
* @screenshot views/versionHistory/VersionTimelineGroupView.png
* @since 2.37.0
*/
const VersionTimelineGroupView = Backbone.View.extend(
/** @lends VersionTimelineGroupView.prototype */ {
/**
* Identifier used when the application inspects view types.
* @type {string}
*/
type: "VersionTimelineGroupView",
/** @inheritdoc */
tagName: "div",
/** @inheritdoc */
className: CLASS_NAMES.container,
/**
* @param {object} [options] View options.
* @param {Backbone.Model} [options.model] VersionTimeLineGroup model
* (defined in VersionTimelineGroups collection).
* @param {string} [options.referencePid] PID used for badge context.
*/
initialize(options = {}) {
this.referencePid = options.referencePid || null;
this.model = options.model || null;
if (!this.model) {
this.model = new Backbone.Model({
models: new DataONEObjects(),
});
}
const models = this.model.get("models");
this.date = this.model.get("date") ?? null;
this.label = this.model.get("label") ?? null;
this.collection =
models instanceof DataONEObjects
? models
: new DataONEObjects(models || [], { sort: false });
this.model.set("models", this.collection);
this.dateKey = null;
},
/**
* Base markup for the timeline group.
* @param {{date: Date|null, label: string|null}} params Template parameters.
* @param {Date|null} params.date Group date at midnight.
* @param {string|null} params.label Human-readable label for synthetic groups.
* @returns {string} HTML string for the timeline group shell.
*/
template({ date, label }) {
let displayDate =
label ||
DateUtility.toLocaleDateString(date, {
formatOptions: {
year: "numeric",
month: "long",
day: "numeric",
},
});
// If it's a date and not e.g. "unknown" label, wrap it in <time>
if (DateUtility.isValidDate(date)) {
const dateAttr = DateUtility.toISODateOnly(date);
displayDate = `<time datetime="${dateAttr}">${displayDate}</time>`;
}
return `
<div class="${CLASS_NAMES.timelinePoint}">
${TIMELINE_POINT_SVG}
<span
class="${CLASS_NAMES.timelinePointDateNoteIcon}"
role="img"
aria-label="${DATE_NOTE_ICON_LABEL}"
title="Note on dates"
>
<i class="icon icon-info-sign" aria-hidden="true"></i>
</span>
</div>
<div class="${CLASS_NAMES.timelineContent}">
<div class="${CLASS_NAMES.header}">
<h3 class="${CLASS_NAMES.dateTitle}">${displayDate}</h3>\
</div>
<ul class="${CLASS_NAMES.objectVersionsList}"></ul>
</div>
`;
},
/**
* Count visible versions in this group that have a version date note
* (version date is later than the one that obsoletes it).
* @returns {number} Number of visible versions with a date note.
*/
getVisibleDateNotes() {
if (!this.collection?.length) return 0;
return this.collection.reduce((count, model) => {
if (model.get("hiddenByUI") === true) return count;
if (model.get("versionDateNote")) return count + 1;
return count;
}, 0);
},
/**
* Compute a stable key for the current date/label combination, used to
* determine when the group header needs to be re-rendered.
* @returns {string} Unique key representing the current date and label of
* this group.
*/
getDateKey() {
const dateKey = DateUtility.isValidDate(this.date)
? this.date.getTime()
: "nodate";
return `${dateKey}|${this.label || ""}`;
},
/** Start listening for row-level UI-affecting changes */
listenToCollection() {
const events = "change:hiddenByUI change:versionDateNote";
this.stopListening(this.collection, events);
this.listenTo(this.collection, events, this.updateVisualState);
},
/**
* Show or hide the group depending on whether all versions are hidden,
* and add/remove the date note class based on whether any visible
* versions have a date note. Update the ObjectVersionsView to show/hide
* individual rows.
*/
updateVisualState() {
// Hide or show the entire group if all versions are hidden
const allHidden =
this.collection.length === 0 || this.collection.allHidden();
this.el.classList.toggle(`${CLASS_NAMES.hidden}`, allHidden);
// Add or remove the date note class based on whether any visible
// versions have a date note
const numVisibleDateNotes = allHidden ? 0 : this.getVisibleDateNotes();
this.el.classList.toggle(
`${CLASS_NAMES.dateNote}`,
numVisibleDateNotes > 0,
);
// Show or hide the individual object version views based on their
// "hiddenByUI" state
this.objectVersionsView?.applyVisibilityState?.();
},
/**
* Renders the timeline group shell and embeds an ObjectVersionsView.
* @returns {this} The view instance.
*/
render() {
this.listenToCollection();
const newDateKey = this.getDateKey();
// Only re-render the group if the date/label has changed, to preserve
// UI state and avoid unnecessary re-renders when updating the
// collection models
if (this.dateKey !== newDateKey) {
this.el.innerHTML = this.template({
date: this.date,
label: this.label,
});
const listEl = this.el.querySelector(
`.${CLASS_NAMES.objectVersionsList}`,
);
this.objectVersionsView?.remove();
this.objectVersionsView = new ObjectVersionsView({
collection: this.collection,
el: listEl,
referencePid: this.referencePid,
}).render();
this.dateKey = this.getDateKey();
}
this.updateVisualState();
return this;
},
/**
* Replace the models backing this group while keeping the same collection
* reference.
* @param {object[]} models - Plain model attributes grouped for the date.
*/
setModels(models) {
const newModels = Array.isArray(models) ? models : [];
const hasChanges =
this.collection.length !== newModels.length ||
this.collection.every((model, index) => model === newModels[index]);
if (hasChanges) {
this.collection.set(newModels, {
parse: true,
merge: true,
sort: false,
});
}
this.updateVisualState();
},
/**
* Update the group header date/label and re-render this group.
* @param {Date|null} date Group date.
* @param {string|null} label Group label.
*/
setDateAndLabel(date, label) {
this.date = date ?? null;
this.label = label ?? null;
this.dateKey = null;
this.render();
},
/**
* Add tooltips to the ObjectVersionViews within this group. Should be
* called after the view is in the DOM to ensure tooltips are properly
* attached.
*/
addTooltips() {
this.objectVersionsView?.addTooltips?.();
},
/**
* Tears down nested views and clears the collection when the group is
* removed.
*/
onClose() {
this.stopListening(this.collection);
this.objectVersionsView?.remove();
this.objectVersionsView = null;
this.collection.reset();
this.dateKey = null;
},
/** @inheritdoc */
remove() {
this.onClose();
return Backbone.View.prototype.remove.call(this);
},
},
);
return VersionTimelineGroupView;
});