1
0
mirror of https://github.com/webrecorder/pywb.git synced 2025-03-15 00:03:28 +01:00
pywb/pywb/static/query.js
Jonas Linde 0cc912da95
Enable translation for the remaining strings on the search results page (#752)
* Enable translation for the remaining strings on the search results page

* Use toLocaleString() to format timestamps also for search results without matchType
2022-08-18 23:27:22 -07:00

1087 lines
37 KiB
JavaScript

var colon = ':';
var usingWorkerStrat =
typeof window.Worker === 'function' &&
typeof window.fetch === 'function' &&
typeof window.TextDecoder === 'function' &&
typeof window.ReadableStream === 'function';
var first = 1;
/**
* Class responsible for rendering the results of the query. If the query is considered regular
* (no filter or match type query modifiers) the calender view is rendered otherwise the advanced search is rendered
* @param {Object} init - Initialization info for rendering the results of the query
*/
function RenderCalendar(init) {
if (!(this instanceof RenderCalendar)) return new RenderCalendar(init);
this.prefix = init.prefix;
this.cdxURL = this.prefix + 'cdx';
this.staticPrefix = init.staticPrefix;
this.queryInfo = {
calView: true,
complete: false,
cdxQueryURL: null,
url: null,
hasFilter: false,
searchParams: {
present: false,
includeInURL: false
}
};
// references to the DOM structure elements that contain the to be rendered markup
this.containers = {
numCaptures: null,
updatesSpinner: null,
yearsListDiv: null,
monthsListDiv: null,
daysListDiv: null,
advancedResultsList: null,
countTextNode: null,
versionsTextNode: null
};
// ids of the to be created dom elements that are important to rendering the query results
this.domStructureIds = {
queryInfoId: 'display-query-type-info',
capturesMount: 'captures',
numCaptures: 'num-captures',
updatesSpinner: 'still-updating-spinner'
};
// memoized last active year month day tab information
this.lastActiveYMD = {};
this.lastActiveDaysTab = null;
// regular expressing for checking the URL when no query params are present
this.starQueries = {
regularStr: '/*/',
dtFromTo: /\/([^-]+)-([^/]+)\/(.+)/i,
dtFrom: /\/([^/]+)\/(.+)/i
};
// regex for extracting the filter constraints and filter mods to human explanation
this.filterRE = /filter([^a-z]+)([a-z]+):(.+)/i;
this.filterMods = {
'=': 'Contains',
'==': 'Matches Exactly',
'=~': 'Matches Regex',
'=!': 'Does Not Contains',
'=!=': 'Is Not',
'=!~': 'Does Not Begins With'
};
this.text = init.text;
this.versionString = null;
}
/**
* Initializes the rendering process and checks the our locations URL to determine which
* result view to render (calendar or advanced)
* @return {void}
*/
RenderCalendar.prototype.init = function() {
// strip the prefix from our locations URL leaving us with /dt-info/url or /*?...
var queryPart = location.href.substring(this.prefix.length - 1);
// check for from the search interface
if (queryPart.indexOf('/*?url=') === 0) {
// the query info came from the collections query interface and since we are
// mimicking the cdx api here we need to parse our locations URL using the
// WHATWG URL Standard since location.search is un-helpful here
var searchURL = new URL(location.href);
this.queryInfo.url = searchURL.searchParams.get('url');
var haveMatchType = searchURL.searchParams.has('matchType');
var haveFilter = searchURL.searchParams.has('filter');
if (!haveMatchType) {
// be extra sure the user did not input a URL that includes the match type
this.determineViewFromQInfoURL();
} else {
this.queryInfo.searchParams.matchType = searchURL.searchParams.get(
'matchType'
);
}
if (this.queryInfo.calView) {
this.queryInfo.calView = !(haveMatchType || haveFilter);
}
this.queryInfo.searchParams.from = searchURL.searchParams.get('from');
this.queryInfo.searchParams.to = searchURL.searchParams.get('to');
this.queryInfo.searchParams.present = true;
this.queryInfo.rawSearch = location.search;
this.queryInfo.hasFilter = haveFilter;
return this.makeCDXRequest();
}
// check for /*/URL
if (queryPart.indexOf(this.starQueries.regularStr) === 0) {
this.queryInfo.url = queryPart.substring(
queryPart.indexOf(this.starQueries.regularStr) +
this.starQueries.regularStr.length
);
this.determineViewFromQInfoURL();
return this.makeCDXRequest();
}
// check for /fromDT*-toDT*/url*
var maybeDTStarQ = this.maybeExtractDTStarQuery(queryPart);
if (maybeDTStarQ) {
this.queryInfo.searchParams.present = true;
this.queryInfo.searchParams.includeInURL = true;
this.queryInfo.searchParams.from = maybeDTStarQ.from;
this.queryInfo.searchParams.to = maybeDTStarQ.to;
this.queryInfo.url = maybeDTStarQ.url;
this.determineViewFromQInfoURL();
return this.makeCDXRequest();
}
};
/**
* Determines the match type from the query URL if it includes the shortcuts
* @return {void}
*/
RenderCalendar.prototype.determineViewFromQInfoURL = function() {
var purl;
var domainMatch =
this.queryInfo.url.charAt(this.queryInfo.url.length - 1) === '*';
try {
// if parsing the query url via the WHATWG URL class fails (no scheme)
// we are probably not rendering the date calendar
purl = new URL(this.queryInfo.url);
} catch (e) {}
if (!purl) {
// since purl is null we know we do not hav a valid URL and need to check
// for the match type cases and if one is present update queryInfo
var prefixMatch = this.queryInfo.url.substring(0, 2) === '*.';
return this.updateMatchType(prefixMatch, domainMatch);
}
if (purl.search) {
// the URL we are querying with has some search params
// in this case we know we can not check for match type domain because
// http://example.com?it=* is valid and the * here is for the search param
return;
}
// there are no search params and may have http://example.com[*|/*]
// indicating we are operating under match type domain
return this.updateMatchType(null, domainMatch);
};
/**
* Updates the queryInfo's searchParams property to reflect if CDX query is
* using a match type depending on the supplied T/falsy mPrefix and mDomain values
* @param {?boolean} prefixMatch - T/falsy indicating if the match type prefix condition is satisfied
* @param {?boolean} domainMatch - T/falsy indicating if the match type domain condition is satisfied
* @return {void}
*/
RenderCalendar.prototype.updateMatchType = function(prefixMatch, domainMatch) {
// if mPrefix && mDomain something weird is up and we got *.xyz.com[*|/*]
// default to prefix just to be safe since we know we got that for sure
if ((prefixMatch && domainMatch) || prefixMatch) {
this.queryInfo.searchParams.present = true;
this.queryInfo.searchParams.matchType = 'prefix';
this.queryInfo.calView = false;
return;
}
if (domainMatch) {
this.queryInfo.searchParams.present = true;
this.queryInfo.searchParams.matchType = 'domain';
this.queryInfo.calView = false;
}
};
/**
* Extracts the datetime information and url from the query part of our
* locations URL if it exists otherwise returns null
* @param {string} queryPart
* @return {?{from: string, to?: string, url: string}}
*/
RenderCalendar.prototype.maybeExtractDTStarQuery = function(queryPart) {
// check /from-to/url first
var match = this.starQueries.dtFromTo.exec(queryPart);
if (match) return { from: match[1], to: match[2], url: match[3] };
// lastly check /from/url first
match = this.starQueries.dtFrom.exec(queryPart);
if (match) return { from: match[1], url: match[2] };
return null;
};
/**
* Constructs and returns the URL for the CDX query. Also updates the
* queryInfo's cdxQueryURL property with the constructed URL.
* @return {string}
*/
RenderCalendar.prototype.makeCDXQueryURL = function() {
var queryURL;
if (this.queryInfo.rawSearch) {
queryURL = this.cdxURL + this.queryInfo.rawSearch + '&output=json';
} else {
queryURL =
this.cdxURL +
'?output=json&url=' +
// ensure that any query params in the search URL are not considered
// apart of the full query URL
encodeURIComponent(this.queryInfo.url);
}
var querySearchParams = this.queryInfo.searchParams;
if (querySearchParams.present && querySearchParams.includeInURL) {
if (querySearchParams.from) {
queryURL += '&from=' + querySearchParams.from.trim();
}
if (querySearchParams.to) {
queryURL += '&to=' + querySearchParams.to.trim();
}
}
this.queryInfo.cdxQueryURL = queryURL;
return queryURL;
};
/**
* Performs the query by requesting the CDX information from the cdx server.
* How the results are received from the CDX API is done by of two strategies: using a web worker or in the main thread.
* This is due to the fact that the query results can be LARGE and extracting the results in the main thread is not
* preformat and will lock up the browser. The determination for if we can use the web worker strategy is
* by checking if the browser has the symbols Worker, fetch, TextDecoder, and ReadableStream defined and are functions.
* If we can use the web worker strategy a worker is created and updates the result view as it sends us the queries
* cdx info. Otherwise the query is executed in the main thread and the results are both parsed and rendered in the
* main thread.
* @return {void}
*/
RenderCalendar.prototype.makeCDXRequest = function() {
// if we are rendering the calendar view (regular result view) we need memoizedYMDT to be an object otherwise nothing
var memoizedYMDT = this.queryInfo.calView ? {} : null;
var renderCal = this;
// initialized the dom structure
this.createContainers();
if (usingWorkerStrat) {
// execute the query and render the results using the query worker
var queryWorker = new window.Worker(this.staticPrefix + '/queryWorker.js');
var cdxRecordMsg = 'cdxRecord';
var done = 'finished';
var months = this.text.months;
queryWorker.onmessage = function(msg) {
var data = msg.data;
var terminate = false;
if (data.type === cdxRecordMsg) {
data.timeInfo.month = months[data.timeInfo.month];
// render the results sent to us from the worker
renderCal.displayedCountStr(
data.recordCount,
data.recordCountFormatted
);
if (renderCal.queryInfo.calView) {
renderCal.renderDateCalPart(
memoizedYMDT,
data.record,
data.timeInfo,
data.recordCount === first
);
} else {
renderCal.renderAdvancedSearchPart(data.record);
}
} else if (data.type === done) {
// the worker has consumed the entirety of the response body
terminate = true;
// if there were no results we need to inform the user
renderCal.displayedCountStr(
data.recordCount,
data.recordCountFormatted
);
}
if (terminate) {
queryWorker.terminate();
var spinner = document.getElementById(
renderCal.domStructureIds.updatesSpinner
);
if (spinner && spinner.parentNode) {
spinner.parentNode.removeChild(spinner);
}
}
};
queryWorker.postMessage({
type: 'query',
queryURL: this.makeCDXQueryURL()
});
return;
}
// main thread processing
$.ajax(this.makeCDXQueryURL(), {
dataType: 'text',
success: function(data) {
var cdxLines = data ? data.trim().split('\n') : [];
if (cdxLines.length === 0) {
renderCal.displayedCountStr(0, '0');
return;
}
var numCdxEntries = cdxLines.length;
renderCal.displayedCountStr(
numCdxEntries,
numCdxEntries.toLocaleString()
);
for (var i = 0; i < numCdxEntries; ++i) {
var cdxObj = JSON.parse(cdxLines[i]);
if (renderCal.queryInfo.calView) {
renderCal.renderDateCalPart(
memoizedYMDT,
cdxObj,
renderCal.makeTimeInfo(cdxObj),
i === 0
);
} else {
renderCal.renderAdvancedSearchPart(cdxObj);
}
}
var spinner = document.getElementById(
renderCal.domStructureIds.updatesSpinner
);
if (spinner && spinner.parentNode) {
spinner.parentNode.removeChild(spinner);
}
}
});
};
/**
* Creates the DOM structure required for rendering the query results based on if we are rendering the
* calendar view or the advanced query view and populates the containers object
*/
RenderCalendar.prototype.createContainers = function() {
var queryResults = document.getElementById(
this.domStructureIds.capturesMount
);
var queryInfo = document.getElementById(this.domStructureIds.queryInfoId);
var renderCal = this;
if (this.queryInfo.calView) {
// create regulars count structure
this.createAndAddElementTo(queryInfo, {
tag: 'h3',
children: [
{
tag: 'i',
className: 'fas fa-spinner fa-pulse mr-2',
id: this.domStructureIds.updatesSpinner
},
{
tag: 'b',
child: {
tag: 'textNode',
value: '',
ref: function(refToElem) {
renderCal.containers.countTextNode = refToElem;
}
}
},
{ tag: 'textNode', value: ' ' },
{
tag: 'textNode',
value: '',
ref: function(refToElem) {
renderCal.containers.versionsTextNode = refToElem;
}
},
{ tag: 'b', innerText: ' ' + this.queryInfo.url }
]
});
// create the row that will hold the results of the regular query
this.createAndAddElementTo(queryResults, {
tag: 'div',
className: 'row q-row',
children: [
// years column and initial display structure
{
tag: 'div',
className: 'col-12 col-sm-2 pr-1 pl-1 h-100',
child: {
tag: 'div',
className: 'list-group h-100 auto-overflow',
id: 'year-tab-list',
attributes: { role: 'tablist' },
ref: function(refToElem) {
renderCal.containers.yearsListDiv = refToElem;
}
}
},
// months initial structure
{
tag: 'div',
className: 'col-12 mt-2 col-sm-2 mt-sm-0 pr-1 pl-1 h-100',
child: {
tag: 'div',
className: 'tab-content h-100',
id: 'year-month-tab-list',
ref: function(refToElem) {
renderCal.containers.monthsListDiv = refToElem;
}
}
},
// days initial structure
{
tag: 'div',
className: 'col-12 mt-3 mb-3 pr-1 mt-sm-0 mb-sm-0 pr-sm-0 col-sm-8 pl-1 h-100',
child: {
tag: 'div',
className: 'tab-content h-100',
id: 'year-month-day-tab-list',
ref: function(refToElem) {
renderCal.containers.daysListDiv = refToElem;
}
}
}
]
});
this.containers.updatesSpinner = document.getElementById(
this.domStructureIds.updatesSpinner
);
return;
}
// create the advanced results query info DOM structure
var forString = ' for ';
var forElems;
if (this.queryInfo.searchParams.matchType) {
forString = ' ' + this.text.matching + ' ';
forElems = [
{ tag: 'b', innerText: this.queryInfo.url },
{ tag: 'textNode', value: ' ' + this.text.by + ' ' },
{ tag: 'b', innerText: this.text.types[this.queryInfo.searchParams.matchType] }
];
} else {
forElems = [{ tag: 'b', innerText: this.queryInfo.url }];
}
this.createAndAddElementTo(queryInfo, {
tag: 'div',
className: 'col-12',
child: {
tag: 'h3',
className: 'text-center',
children: [
{
tag: 'i',
className: 'fas fa-spinner fa-pulse mr-2',
id: this.domStructureIds.updatesSpinner
},
{
tag: 'b',
child: {
tag: 'textNode',
value: '',
ref: function(refToElem) {
renderCal.containers.countTextNode = refToElem;
}
}
},
{ tag: 'textNode', value: ' ' },
{
tag: 'textNode',
value: '',
ref: function(refToElem) {
renderCal.containers.versionsTextNode = refToElem;
}
},
{ tag: 'textNode', value: forString }
].concat(forElems)
}
});
// next we will display only the URL or the URL + match type or the URL + date range
// if the executed query contained filters display the humanized version of them
if (this.queryInfo.searchParams.from || this.queryInfo.searchParams.to) {
this.createAndAddElementTo(queryInfo, {
tag: 'div',
className: 'col-6 align-self-center',
child: {
tag: 'p',
className: 'lead text-center',
child: { tag: 'textNode', value: ' ' + this.niceDateRange() }
}
});
}
if (this.queryInfo.hasFilter) {
this.createAndAddElementTo(queryInfo, {
tag: 'div',
className: 'col-6',
children: [
{
tag: 'p',
className: 'text-center mb-0 mt-1',
innerText: 'Filtering by'
},
{
tag: 'ul',
className: 'list-group auto-overflow',
style: 'max-height: 150px',
children: this.niceFilterDisplay()
}
]
});
}
// create the advanced results DOM structure
this.containers.advancedResultsList = this.createAndAddElementTo(
queryResults,
{
tag: 'div',
className: 'row q-row',
child: {
tag: 'ul',
className: 'list-group h-100 auto-overflow w-100'
}
}
).firstElementChild;
};
/**
* Updates the calendar view with the supplied cdx information
* @param {Object} memoizedYMDT - Object containing the counts for the captures
* @param {Object} cdxObj - CDX object for this capture
* @param {Object} timeInfo - Object containing the date time information for this capture
* @param {boolean} active - Should we add the active classes to the markup rendered here
*/
RenderCalendar.prototype.renderDateCalPart = function(
memoizedYMDT,
cdxObj,
timeInfo,
active
) {
if (memoizedYMDT[timeInfo.year] == null) {
// we have not seen this year month day before
// create the year month day structure (occurs once per result year)
var timeVal = {};
timeVal[timeInfo.time] = 1;
var dayVal = {};
dayVal[timeInfo.day] = timeVal;
var monthVal = {};
monthVal[timeInfo.month] = dayVal;
memoizedYMDT[timeInfo.year] = monthVal;
this.addRegYearListItem(timeInfo, active);
this.addRegYearMonthListItem(timeInfo, active);
return this.addRegYearMonthDayListItem(cdxObj, timeInfo, 1, active);
} else if (memoizedYMDT[timeInfo.year][timeInfo.month] == null) {
// we have seen the year before but not the month and day
// create the month day structure (occurs for every new month encountered for an existing year)
var timeVal = {};
timeVal[timeInfo.time] = 1;
var dayVal = {};
dayVal[timeInfo.day] = timeVal;
memoizedYMDT[timeInfo.year][timeInfo.month] = dayVal;
this.addRegYearMonthListItem(timeInfo, active);
return this.addRegYearMonthDayListItem(cdxObj, timeInfo, 1, active);
}
// in cases 1 & 2 a new day at time entry is added otherwise only the captures count is updated (case 3)
var count = 1;
var month = memoizedYMDT[timeInfo.year][timeInfo.month];
if (month[timeInfo.day] == null) {
// never seen this day (case 1)
var timeVal = {};
timeVal[timeInfo.time] = count;
month[timeInfo.day] = timeVal;
} else if (month[timeInfo.day][timeInfo.time] == null) {
// we have seen this day before but not at this time (case 2)
month[timeInfo.day][timeInfo.time] = count;
} else {
// we have seen this day at time before so just increment the captures count (case 3)
count = month[timeInfo.day][timeInfo.time] + 1;
month[timeInfo.day][timeInfo.time] = count;
}
this.addRegYearMonthDayListItem(cdxObj, timeInfo, count, active);
};
/**
* Updates the advanced view with the supplied cdx information
* @param {Object} cdxObj - CDX object for this capture
*/
RenderCalendar.prototype.renderAdvancedSearchPart = function(cdxObj) {
// display the URL of the result
var displayedInfo = [
{
tag: 'small',
innerText: this.text.dateTime + this.tsToDate(cdxObj.timestamp)
}
];
// display additional information about the result under the URL
if (cdxObj.mime) {
displayedInfo.push({
tag: 'small',
innerText: this.text.mimeType + cdxObj.mime
});
}
if (cdxObj.status) {
displayedInfo.push({
tag: 'small',
innerText: this.text.httpStatus + cdxObj.status
});
}
displayedInfo.push({
tag: 'a',
attributes: {
href: this.prefix + '*' + '/' + cdxObj.url,
target: '_blank'
},
child: { tag: 'small', innerText: this.text.viewAllCaptures }
});
this.createAndAddElementTo(this.containers.advancedResultsList, {
tag: 'li',
className: 'list-group-item flex-column align-items-start w-100',
children: [
{
tag: 'a',
className: 'w-100 text-small long-text',
attributes: {
href: this.prefix + cdxObj.timestamp + '/' + cdxObj.url,
target: '_blank'
},
innerText: cdxObj.url
},
{
tag: 'div',
className: 'pt-1 d-flex w-100 justify-content-between',
children: displayedInfo
}
]
});
};
/**
* Creates and adds a year entry for the calendar view
* @param {Object} timeInfo - Object containing the date time information for this capture
* @param {boolean} active - Should we add the active classes to the markup rendered here
*/
RenderCalendar.prototype.addRegYearListItem = function(timeInfo, active) {
// adds an year to div[id=year-tab-list]
var renderCal = this;
var year = timeInfo.year;
this.createAndAddElementTo(this.containers.yearsListDiv, {
tag: 'a',
className:
'list-group-item list-group-item-action' + (active ? ' active' : ''),
innerText: year,
attributes: { role: 'tab', href: '#' + this.displayMonthsId(year) },
dataset: { toggle: 'list' },
events: {
// add a click listener to ensure we display the correct year month days combination
// when choosing a new year or month
click: function() {
renderCal.ensureCorrectActive(year);
}
}
});
};
/**
* Adds a years months to it's month list. If the years month list was not previously created it is created.
* @param {Object} timeInfo - Object containing the date time information for this capture
* @param {boolean} active - Should we add the active classes to the markup rendered here
*/
RenderCalendar.prototype.addRegYearMonthListItem = function(timeInfo, active) {
var yeaMonthsId = this.displayMonthsId(timeInfo.year); // _year-Display-Month
var yearMonthListId = this.displayMonthsListId(timeInfo.year); // _year-Month-Display-List
var yml = document.getElementById(yearMonthListId);
if (yml == null) {
/*
we have not created this years month list before before
so we must create a tab pane for it and month list
<!-- adding -->
div[id=_year-Display-Months, role=tabpanel].h-100.tab-pane
div[id=_year-Display-Months-List, role=tablist].list-group.h-100.auto-overflow
*/
yml = this.createAndAddElementTo(this.containers.monthsListDiv, {
tag: 'div',
className: 'h-100 tab-pane' + (active ? ' active show' : ''),
id: yeaMonthsId,
attributes: { role: 'tabpanel' },
child: {
tag: 'div',
className: 'list-group h-100 auto-overflow',
id: yearMonthListId,
attributes: { role: 'tablist' }
}
}).firstElementChild;
}
var renderCal = this;
var showDaysId = this.displayYearMonthDaysId(timeInfo.year, timeInfo.month);
var year = timeInfo.year;
// adds an years month to div[id=_year-Display-Months-List]
this.createAndAddElementTo(yml, {
tag: 'a',
className:
'list-group-item list-group-item-action' + (active ? ' active' : ''),
innerText: timeInfo.month,
attributes: {
role: 'tab',
href: '#' + showDaysId // _year-month-Display-Days
},
events: {
click: function() {
renderCal.lastActiveYMD[year] = {
month: yeaMonthsId,
days: showDaysId
};
renderCal.lastActiveDaysTab = document.getElementById(showDaysId);
}
},
dataset: { toggle: 'list' }
});
};
/**
* Updates the number of captures for a day at time for a year month. If the years month month day at time list
* was not previously created it is created. If the day was not present in it's years months days list it is created.
* @param {Object} cdxObj - CDX object for this capture
* @param {Object} timeInfo - Object containing the date time information for this capture
* @param {number} numCaptures - How many captures do we have currently for this URL
* @param {boolean} active - Should we add the active classes to the markup rendered here
*/
RenderCalendar.prototype.addRegYearMonthDayListItem = function(
cdxObj,
timeInfo,
numCaptures,
active
) {
var yearMonthDaysListId = this.displayYearMonthDaysListId(
timeInfo.year,
timeInfo.month
); // _year-month-Display-Days-List
var ymlDL = document.getElementById(yearMonthDaysListId);
if (ymlDL == null) {
/*
we have not created this years months day tab and list before before
so we must create a tab pane for it and days list
<!-- adding -->
div[id=_year-month-Display-Days, role=tabpanel].h-100.tab-pane.active.show
ul[id=_year-month-Display-Days-List].list-group.h-100.auto-overflow
*/
ymlDL = this.createAndAddElementTo(this.containers.daysListDiv, {
tag: 'div',
className: 'h-100 tab-pane' + (active ? ' active show' : ''),
id: this.displayYearMonthDaysId(timeInfo.year, timeInfo.month),
attributes: { role: 'tabpanel' },
child: {
tag: 'ul',
className: 'list-group h-100 auto-overflow',
id: yearMonthDaysListId
}
}).firstElementChild;
}
// retrieve the existing days num captures display element
var existingDayId = 'count_' + cdxObj.timestamp;
var existingDayCount = document.getElementById(existingDayId);
if (existingDayCount == null) {
/*
it does not exist so we have not displayed this day before
<!-- adding -->
li.list-group-item
a[href="replay url"]
span[id=count_ts].badge.badge-info.badge-pill.float-right
*/
const options = {
dateStyle: 'long',
timeStyle: 'medium',
};
var dateTimeString = this.tsToDate(cdxObj.timestamp, false, options);
this.createAndAddElementTo(ymlDL, {
tag: 'li',
className: 'list-group-item',
children: [
{
tag: 'a',
attributes: {
href: this.prefix + cdxObj.timestamp + '/' + cdxObj.url,
target: '_blank'
},
innerText: dateTimeString
},
{
tag: 'span',
id: existingDayId,
className: 'badge badge-info badge-pill float-right',
ref: function(refToElem) {
existingDayCount = refToElem;
}
}
]
});
if (active) {
// populate the lastActives with the selected correct year month days combination
var daysID = this.displayYearMonthDaysId(timeInfo.year, timeInfo.month);
this.lastActiveYMD[timeInfo.year] = {
month: this.displayMonthsId(timeInfo.year),
days: daysID
};
this.lastActiveDaysTab = document.getElementById(daysID);
}
}
existingDayCount.innerText = numCaptures + '';
};
/**
* Ensures that we display the correct year month days combination when switching year's.
* Updates the lastActiveDaysTab property.
* @param {string} year - The year to be shown
*/
RenderCalendar.prototype.ensureCorrectActive = function(year) {
// un-activates currently active year month day display tab
if (this.lastActiveDaysTab != null) {
this.lastActiveDaysTab.classList.remove('active', 'show');
}
// activates the last year month day display tab for the current year chosen
if (this.lastActiveYMD[year] && this.lastActiveYMD[year].days) {
this.lastActiveDaysTab = document.getElementById(
this.lastActiveYMD[year].days
);
this.lastActiveDaysTab.classList.add('active', 'show');
}
};
/**
* Creates a DOM element(s) based on the supplied creation options.
* Creation options:
* - tag: The name of the tag to be created (supplied to doc.createElement) or 'textNode' to create a text
* node using doc.createTextNode [required]
* - value: A string used as the contents for the to be created text node (only used if tag == 'textNode')
* - id: An string used to set the `id` of the element
* - className: An string of CSS class names used to set the elements `className` property
* - style: An string containing style definition(s) used to set the elements `style` property
* - innerText: An string used to set the elements `innerText` property
* - innerHTML: An string used to set the elements `innerHTML` property
* - attributes: An object containing string key value pairs that will be used to set the elements attributes
* using element.setAttribute(key, value)
* - dataset: An object containing string key value pairs that will be used to set the elements attributes
* using element.dataset[key] = value
* - events: An object containing string keys and function value pairs that will be used to add event listeners
* on the element using addEventListener.addEventListener(key, value)
* - child: An object (same properties as the creationOptions parameter for this function) that represent a child
* element of the element created using this function, added via element.appendChild once created
* - children: An array of objects (same properties as the creationOptions parameter for this function) that
* represent a child element of the element created using this function, added via element.appendChild once created
* - ref: An function that if supplied will be called with the created element as the only argument
* @param {Object} creationOptions - Options for new element creation
* @return {HTMLElement|Node} - The newly created element
*/
RenderCalendar.prototype.createElement = function(creationOptions) {
if (creationOptions.tag === 'textNode') {
var tn = document.createTextNode(creationOptions.value);
if (creationOptions.ref) {
creationOptions.ref(tn);
}
return tn;
}
var elem = document.createElement(creationOptions.tag);
if (creationOptions.id) {
elem.id = creationOptions.id;
}
if (creationOptions.className) {
elem.className = creationOptions.className;
}
if (creationOptions.style) {
elem.style = creationOptions.style;
}
if (creationOptions.innerText) {
elem.innerText = creationOptions.innerText;
}
if (creationOptions.innerHTML) {
elem.innerHTML = creationOptions.innerHTML;
}
if (creationOptions.attributes) {
var atts = creationOptions.attributes;
for (var attributeName in atts) {
elem.setAttribute(attributeName, atts[attributeName]);
}
}
if (creationOptions.dataset) {
var dataset = creationOptions.dataset;
for (var dataName in dataset) {
elem.dataset[dataName] = dataset[dataName];
}
}
if (creationOptions.events) {
var events = creationOptions.events;
for (var eventName in events) {
elem.addEventListener(eventName, events[eventName]);
}
}
if (creationOptions.child) {
elem.appendChild(this.createElement(creationOptions.child));
}
if (creationOptions.children) {
var kids = creationOptions.children;
for (var i = 0; i < kids.length; i++) {
elem.appendChild(this.createElement(kids[i]));
}
}
if (creationOptions.ref) {
creationOptions.ref(elem);
}
return elem;
};
/**
* Adds the newly created element to the supplied existing element.
* @param {Node} addTo - The DOM node the newly created element will be added to via addTo.appendChild
* @param {Object} creationOptions - Options for creating the new element.
* See the description of {@link createElement} for more information.
* @return {HTMLElement|Node} - The newly created element
*/
RenderCalendar.prototype.createAndAddElementTo = function(
addTo,
creationOptions
) {
var created = this.createElement(creationOptions);
addTo.appendChild(created);
return created;
};
/**
* Returns element creation options for the humanized filtering options of the advanced view
* @returns {Array<Object>}
*/
RenderCalendar.prototype.niceFilterDisplay = function() {
var filterList = [];
var qSplit = location.search.split('&');
for (var i = 0; i < qSplit.length; i++) {
var match = this.filterRE.exec(qSplit[i]);
if (match) {
filterList.push({
tag: 'li',
className: 'list-group-item',
innerText: match[2] + ' ' + this.filterMods[match[1]] + ' ' + match[3]
});
}
}
return filterList;
};
/**
* Returns a humanized representation of an advanced queries from or to constraints
* @returns {string}
*/
RenderCalendar.prototype.niceDateRange = function() {
var from = this.queryInfo.searchParams.from;
var to = this.queryInfo.searchParams.to;
if (from && to) {
return 'From ' + from + ' to ' + to;
} else if (from) {
return 'From ' + from + ' until ' + 'present';
}
return 'From earliest until ' + to;
};
/**
* Returns the id for a years months tab
* @param {string} year - The year part for the id
* @returns {string}
*/
RenderCalendar.prototype.displayMonthsId = function(year) {
return '_' + year + '-Display-Month';
};
/**
* Returns the id for displaying a years months list within a years month tab
* @param {string} year - The year part for the id
* @returns {string}
*/
RenderCalendar.prototype.displayMonthsListId = function(year) {
return '_' + year + '-Month-Display-List';
};
/**
* Returns the id for a year month days tab
* @param {string} year - The year part for the id
* @param {string} month - The month part for the id
* @returns {string}
*/
RenderCalendar.prototype.displayYearMonthDaysId = function(year, month) {
return '_' + year + '-' + month + '-Display-Days';
};
/**
* Returns the id for displaying a year month days list within a year month days tab
* @param {string} year - The year part for the id
* @param {string} month - The month part for the id
* @returns {string}
*/
RenderCalendar.prototype.displayYearMonthDaysListId = function(year, month) {
return '_' + year + '-' + month + '-Display-Days-List';
};
/**
* Converts the supplied timestamp to either a local data string or a gmt string (if is_gmt is true)
* @param {string} ts - The timestamp to be converted to a string
* @param {boolean} [is_gmt] - Should the timestamp be converted to a gmt string
* @param {Object} [options] - String formatting options
* @returns {string}
*/
RenderCalendar.prototype.tsToDate = function(ts, is_gmt, options) {
if (ts.length < 14) return ts;
var datestr =
ts.substring(0, 4) +
'-' +
ts.substring(4, 6) +
'-' +
ts.substring(6, 8) +
'T' +
ts.substring(8, 10) +
colon +
ts.substring(10, 12) +
colon +
ts.substring(12, 14) +
'-00:00';
var date = new Date(datestr);
return is_gmt ? date.toUTCString() : date.toLocaleString(document.documentElement.lang, options);
};
/**
* Extracts the time information for a cdx object's timestamp
* @param {Object} cdxObj - The cdx object to have its timestamp information extracted from
* @returns {{month: string, year: string, time: string, day: string}}
*/
RenderCalendar.prototype.makeTimeInfo = function(cdxObj) {
var ts = cdxObj.timestamp;
var day = ts.substring(6, 8);
return {
year: ts.substring(0, 4),
month: this.text.months[ts.substring(4, 6)],
day: day.charAt(0) === '0' ? day.charAt(1) : day,
time:
ts.substring(8, 10) +
colon +
ts.substring(10, 12) +
colon +
ts.substring(12, 14)
};
};
/**
* Updates the displayed number of query results.
*
* The version(s) text that is displayed only needs to be updated a maximum of two times.
* When count (1) === first (1) we are displaying the first or only result and need the
* singular version text. Otherwise we are displaying result zero or n and need the
* plural version text. After these two version text updates we are needlessly modifying
* the element displaying the versions text with the same version.
* @param {number} count - The number of query results
* @param {string} countFormatted - The number of query results formatted for display
* @returns {string}
*/
RenderCalendar.prototype.displayedCountStr = function(count, countFormatted) {
this.containers.countTextNode.data = countFormatted;
if (count === first) {
this.containers.versionsTextNode.data = this.queryInfo.calView
? this.text.version
: this.text.result;
} else if (!this.versionString) {
// case count = 0 or first nth record
this.versionString = this.queryInfo.calView
? this.text.versions
: this.text.results;
this.containers.versionsTextNode.data = this.versionString;
}
};