Skip to content

Commit

Permalink
Merge pull request #1542 from CuBoulder/issue/ucb_article_syndication#3
Browse files Browse the repository at this point in the history
Adds Article Syndication "read more" article list
  • Loading branch information
patrickbrown-io authored Dec 10, 2024
2 parents d41f817 + ced0161 commit 324767d
Showing 1 changed file with 157 additions and 133 deletions.
290 changes: 157 additions & 133 deletions js/ucb-article-list.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,42 @@
(function (customElements) {

/**
* Creates a taxonomy filter group to filter by specific taxonomy terms.
*
* @param {string} params
* The URL params to add the filter group to.
* @param {number[]} termIds
* The terms to include in the filter (joined with OR).
* @param {string} groupName
* The name of the filter group.
* @param {string} fieldName
* The name of the Drupal field.
*/
function jsonAPICreateFilterGroup(params, termIds, groupName, fieldName) {
if (termIds.length > 0) {
params += `&filter[${groupName}][condition][path]=${fieldName}.meta.drupal_internal__target_id`;
params += `&filter[${groupName}][condition][operator]=IN`;
for (let i = 0; i < termIds.length; i++) {
params += `&filter[${groupName}][condition][value][${i + 1}]=${termIds[i]}`;
}
return params;
}
}

/**
* Converts a string to an array of term ids.
*
* @param {string | null} input
* The input string of term ids separated by a delimiter.
* @param {string} delimiter
* The delimiter.
* @returns {number[]}
* The term ids.
*/
function stringToTermIds(input, delimiter = ',') {
return input ? input.split(delimiter).map(Number) : [];
}

// Handles construction of endpoints, and fetching of articles and taxonomies
class ArticleListProvider {
static get noResultsMessage() {
Expand All @@ -9,13 +47,15 @@
return 'Error retrieving articles from the API endpoint. Please try again later.';
}

constructor(baseURI, includeCategories, includeTags, excludeCategories = '', excludeTags = '', pageCount = 10) {
constructor(baseURI, includeCategories = [], includeTags = [], excludeCategories = [], excludeTags = [], includeSyndicationAudiences = [], includeSyndicationUnits = [], pageCount = 10) {
this.baseURI = baseURI;
this.nextURL = '';
this.includeCategories = includeCategories || [];
this.includeTags = includeTags || [];
this.excludeCategories = excludeCategories || [];
this.excludeTags = excludeTags || [];
this.includeCategories = includeCategories;
this.includeTags = includeTags;
this.excludeCategories = excludeCategories;
this.excludeTags = excludeTags;
this.includeSyndicationAudiences = includeSyndicationAudiences;
this.includeSyndicationUnits = includeSyndicationUnits;
this.pageCount = pageCount;
this._categoryTerms = null;
this._tagTerms = null;
Expand All @@ -28,59 +68,28 @@
*/
buildEndpointPath() {
const endpoint = '';
let queryString = '';

// Include base fields
queryString += '/jsonapi/node/ucb_article?include[node--ucb_article]=uid,title,ucb_article_content,created,field_ucb_article_summary,field_ucb_article_categories,field_ucb_article_tags,field_ucb_article_thumbnail&include=field_ucb_article_thumbnail.field_media_image&fields[file--file]=uri,url';
let queryString = '/jsonapi/node/ucb_article?include[node--ucb_article]=uid,title,ucb_article_content,created,field_ucb_article_summary,field_ucb_article_categories,field_ucb_article_tags,field_ucb_article_thumbnail&include=field_ucb_article_thumbnail.field_media_image&fields[file--file]=uri,url';

// Add published filter
const publishedFilter = '&filter[status][value]=1';
queryString += '&filter[status][value]=1';

// Placeholder for filters
let IncludeFilter = '';
let IncludeCategoryFilter = '';
let IncludeTagsFilter = '';
let ExcludeCategoryFilter = '';
let ExcludeTagsFilter = '';

// Handle Include Categories Filter
if (this.includeCategories && this.includeCategories.length > 0) {
let includeGroupMembers = this.includeCategories.map(category => {
return `&filter[filter-cat${category}][condition][path]=field_ucb_article_categories.meta.drupal_internal__target_id&filter[filter-cat${category}][condition][value]=${category}&filter[filter-cat${category}][condition][memberOf]=cat-include`;
}).join('');
IncludeCategoryFilter = `&filter[cat-include][group][conjunction]=OR${includeGroupMembers}`;
}

// Handle Include Tags Filter
if (this.includeTags && this.includeTags.length > 0) {
let includeGroupMembers = this.includeTags.map(tag => {
return `&filter[filter-tag${tag}][condition][path]=field_ucb_article_tags.meta.drupal_internal__target_id&filter[filter-tag${tag}][condition][value]=${tag}&filter[filter-tag${tag}][condition][memberOf]=tag-include`;
}).join('');
IncludeTagsFilter = `&filter[tag-include][group][conjunction]=OR${includeGroupMembers}`;
}

// Combine Include Filters
if (IncludeCategoryFilter && IncludeTagsFilter) {
IncludeFilter = `${publishedFilter}&filter[include-group][group][conjunction]=AND&filter[include-group][group][memberOf]=published${IncludeCategoryFilter}${IncludeTagsFilter}`;
} else if (IncludeCategoryFilter) {
IncludeFilter = `${publishedFilter}${IncludeCategoryFilter}`;
} else if (IncludeTagsFilter) {
IncludeFilter = `${publishedFilter}${IncludeTagsFilter}`;
} else {
IncludeFilter = publishedFilter; // No include filters, just published
}
queryString += jsonAPICreateFilterGroup(queryString, this.includeCategories, 'category', 'field_ucb_article_categories');
queryString += jsonAPICreateFilterGroup(queryString, this.includeTags, 'tag', 'field_ucb_article_tags');
queryString += jsonAPICreateFilterGroup(queryString, this.includeSyndicationAudiences, 'audience', 'field_syndication_audience');
queryString += jsonAPICreateFilterGroup(queryString, this.includeSyndicationUnits, 'unit', 'field_syndication_unit');

// Pagination filter
const pageCountFilter = `&page[limit]=${this.pageCount}`;
queryString += `&page[limit]=${this.pageCount}`;

// Sorting filter
const sortFilter = "&sort[sort-created][path]=created&sort[sort-created][direction]=DESC";
queryString += '&sort[sort-created][path]=created&sort[sort-created][direction]=DESC';

// Combine all filters
const fullQueryString = `${queryString}${IncludeFilter}${ExcludeCategoryFilter}${ExcludeTagsFilter}${pageCountFilter}${sortFilter}`;

return `${this.baseURI}${endpoint}${fullQueryString}`;
return `${this.baseURI}${endpoint}${queryString}`;
}

// Method to fetch and cache taxonomies by their machine name
async fetchTaxonomies() {
if (!this._categoryTerms) {
Expand Down Expand Up @@ -133,9 +142,9 @@
throw error;
}
}

}
// Article List Component: Handles the rendering and client side exclusion filtering

// Article List Component: Handles the rendering and client side exclusion filtering
class ArticleListElement extends HTMLElement {
static get observedAttributes() {
return ['base-uri', 'exclude-categories', 'exclude-tags', 'include-categories', 'include-tags'];
Expand All @@ -144,22 +153,33 @@
constructor() {
super();
this._baseURI = this.getAttribute('base-uri');
// Store the initial values for resetting to 'All'
this._initialIncludeCategories = this.getAttribute('include-categories')
? this.getAttribute('include-categories').split(',').map(Number)
: [];
this._initialIncludeTags = this.getAttribute('include-tags')
? this.getAttribute('include-tags').split(',').map(Number)
: [];

this._includeCategories = [...this._initialIncludeCategories]; // Set the current categories to the initial state
this._excludeCategories = this.getAttribute('exclude-categories')
? this.getAttribute('exclude-categories').split(',').map(Number)
: [];
this._includeTags = [...this._initialIncludeTags]; // Set the current tags to the initial state
this._excludeTags = this.getAttribute('exclude-tags')
? this.getAttribute('exclude-tags').split(',').map(Number)
: [];

// Store the initial values for resetting to 'All'.
this._initialIncludeCategories = stringToTermIds(this.getAttribute('include-categories'));
this._initialIncludeTags = stringToTermIds(this.getAttribute('include-tags'));

const pathname = window.location.pathname.split('/');
// Adds the functionality for a "read more" page in article syndication.
if (pathname[pathname.length - 1] === 'syndicate') {
const params = new URL(window.location.href).searchParams;
const categories = stringToTermIds(params.get('category'), ' ');
if (categories.length > 0) {
this._initialIncludeCategories = categories;
}
this._includeSyndicationAudiences = stringToTermIds(params.get('audience'), ' ');
this._includeSyndicationUnits = stringToTermIds(params.get('unit'), ' ');
} else {
this._includeSyndicationAudiences = [];
this._includeSyndicationUnits = [];
}

// Set the current tags to the initial state.
this._includeTags = [...this._initialIncludeTags];
// Set the current categories to the initial state.
this._includeCategories = [...this._initialIncludeCategories];

this._excludeCategories = stringToTermIds(this.getAttribute('exclude-categories'))
this._excludeTags = stringToTermIds(this.getAttribute('exclude-tags'));

this._exposeCategory = this.getAttribute('expose-categories') === "True";
this._exposeTag = this.getAttribute('expose-tags') === "True";
Expand All @@ -169,7 +189,9 @@
this._includeCategories,
this._includeTags,
this._excludeCategories,
this._excludeTags
this._excludeTags,
this._includeSyndicationAudiences,
this._includeSyndicationUnits
);

// User Dropdown Form Element
Expand Down Expand Up @@ -224,6 +246,7 @@

this._nextURL = '';
}

// Part of web component API, called on attribute change
async connectedCallback() {
try {
Expand Down Expand Up @@ -251,7 +274,8 @@
: [];
}
}
// Generates the user-accessible filter form for categories and/or tags.

// Generates the user-accessible filter form for categories and/or tags.
generateFilterForm() {
this._filterFormElement.innerHTML = '';

Expand Down Expand Up @@ -352,10 +376,10 @@
resetButton.textContent = 'Reset Filters';
resetButton.onclick = () => {
// Reset dropdowns to default (All)
if(this._exposeCategory){
if (this._exposeCategory) {
document.getElementById('category-filter').value = '';
}
if(this._exposeTag){
if (this._exposeTag) {
document.getElementById('tag-filter').value = '';
}

Expand Down Expand Up @@ -402,38 +426,38 @@
this.toggleLoading(true);

try {
const response = await this._provider.fetchAllArticles(url);
const response = await this._provider.fetchAllArticles(url);

this.renderArticles(response.data, response.included, clearContent);
this.renderArticles(response.data, response.included, clearContent);

this._nextURL = this._provider._nextURL;
this.toggleLoadMoreButton(!!this._nextURL);
this.toggleLoading(false);
this._nextURL = this._provider._nextURL;
this.toggleLoadMoreButton(!!this._nextURL);
this.toggleLoading(false);
} catch (error) {
this.toggleError(true);
this.toggleLoading(false);
this.toggleError(true);
this.toggleLoading(false);
}
}

async loadMoreArticles() {
if (this._nextURL) {
try {
this.toggleLoading(true);
const response = await this._provider.fetchAllArticles(this._nextURL);

// Do not clear content for loadMoreArticles, append to existing content
this.renderArticles(response.data, response.included, false);

// Update the element’s next URL again after loading more articles
this._nextURL = this._provider._nextURL;

// Show or hide the Load More button based on the updated next URL
this.toggleLoadMoreButton(!!this._nextURL);
this.toggleLoading(false);
} catch (error) {
this.toggleError(true);
this.toggleLoading(false);
}
try {
this.toggleLoading(true);
const response = await this._provider.fetchAllArticles(this._nextURL);

// Do not clear content for loadMoreArticles, append to existing content
this.renderArticles(response.data, response.included, false);

// Update the element’s next URL again after loading more articles
this._nextURL = this._provider._nextURL;

// Show or hide the Load More button based on the updated next URL
this.toggleLoadMoreButton(!!this._nextURL);
this.toggleLoading(false);
} catch (error) {
this.toggleError(true);
this.toggleLoading(false);
}
}
}

Expand Down Expand Up @@ -651,49 +675,49 @@
return articleRow;
}

// Helper function to fetch the Article body if no summary and do the processing
// Fixes special characters such as & and nbsp
decodeHtmlEntities(text) {
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
return doc.documentElement.textContent;
}

// Responsible for fetching & processing the body of the Article if no summary provided
async getArticleParagraph(id) {
if (!id) {
return "";
}
// Helper function to fetch the Article body if no summary and do the processing
// Fixes special characters such as & and nbsp
decodeHtmlEntities(text) {
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
return doc.documentElement.textContent;
}

try {
const response = await fetch(
`${this._baseURI}/jsonapi/paragraph/article_content/${id}`
);
const data = await response.json();
if (!data.data.attributes.field_article_text) return ""; // needed for external articles
let htmlStrip = data.data.attributes.field_article_text.processed.replace(
/<\/?[^>]+(>|$)/g,
""
);
let lineBreakStrip = htmlStrip.replace(/(\r\n|\n|\r)/gm, "");
let decodedString = this.decodeHtmlEntities(lineBreakStrip); // Decode HTML entities here
let trimmedString = decodedString.substring(0, 250);

if (trimmedString.length > 100) {
trimmedString = trimmedString.substring(
0,
Math.min(trimmedString.length, trimmedString.lastIndexOf(" "))
);
// Responsible for fetching & processing the body of the Article if no summary provided
async getArticleParagraph(id) {
if (!id) {
return "";
}

try {
const response = await fetch(
`${this._baseURI}/jsonapi/paragraph/article_content/${id}`
);
const data = await response.json();
if (!data.data.attributes.field_article_text) return ""; // needed for external articles
let htmlStrip = data.data.attributes.field_article_text.processed.replace(
/<\/?[^>]+(>|$)/g,
""
);
let lineBreakStrip = htmlStrip.replace(/(\r\n|\n|\r)/gm, "");
let decodedString = this.decodeHtmlEntities(lineBreakStrip); // Decode HTML entities here
let trimmedString = decodedString.substring(0, 250);

if (trimmedString.length > 100) {
trimmedString = trimmedString.substring(
0,
Math.min(trimmedString.length, trimmedString.lastIndexOf(" "))
);
}
return trimmedString + "...";
} catch (Error) {
console.error(
"There was an error fetching Article Paragraph from the API - Please try again later."
);
console.error(Error);
return ""; // Return an empty string in case of error
}
}
return trimmedString + "...";
} catch (Error) {
console.error(
"There was an error fetching Article Paragraph from the API - Please try again later."
);
console.error(Error);
return ""; // Return an empty string in case of error
}
}

// Various toggles
toggleLoading(show) {
Expand Down

0 comments on commit 324767d

Please sign in to comment.