See what real customers are saying about LearnDash.
“I am incredibly thankful for LearnDash because they changed my life. I was in a dead-end job when I first heard about LearnDash. I instantly knew that I could create a business using LearnDash as my foundation. I quickly purchased one license and began growing my business. Fast forward a year later, LearnDash jumpstarted my business, updating from 1 to 10 licenses. Better yet, whenever I run into issues, they respond lightning fast. … Without them, I wouldn’t be able to do what I do and love it!”
Adam Barragato Trustpilot review
“I went from 0$ to 1000$ in the first month, and as I added more courses, I now have more than 500 new students every year from tech companies such as Airbus, Netflix, Google, Meta, and more.
What would you say to somebody who is on the fence about purchasing LearnDash? There is no better way than putting in action and releasing a course with LearnDash to see if it works”
Florent Poux learngeodata.eu
“As someone who looked around for months before deciding to go with LearnDash, I can say with confidence – if you’re trying to make a course to sell your expertise, LearnDash provides the structure, and so much more.”
Jay Postones jaypostones-drumlessons.com
“Don’t hesitate, best LMS platform out there.”
Abi Carmen casemanagementinstitute.com
NEW: LearnDash Reports and ProPanel 3.0
At LearnDash, we’re always striving to enhance your experience and deliver tools that make managing your online courses smoother and more insightful. That’s why we’re thrilled to announce new LearnDash reporting features—both in LearnDash LMS and in ProPanel, our advanced reporting add-on.
`;
} );
// Got more than 10 results? Return pagination
if ( total > 10 ) {
html += getPrevNextButtons( page, totalPages );
}
}
return html;
};
/**
* Function to handle the population of search results HTML, in two parts
*
* 1) Display the search result links
* 2) Display the results count number within the tabs (knowledgebase, dev docs, etc.)
*
* @param {Object} results - The search results data (post title, URL)
* @param {Integer} total - The total number of results for the search
* @param {String} tab - The tab (knowledgebase, dev docs, etc.)
*/
const populateResults = ( results, totalString, totalPagesString, tab, page ) => {
// Convert total to integer
const total = parseInt( totalString );
// Convert totalPages to integer
const totalPages = parseInt( totalPagesString );
// 1) Search Results
let html = '';
html += renderResults( results, page, total, totalPages );
// Which tab are we populating?
const resultsEl = searchSearched.querySelector( `[data-endpoint="${ tab }"]` );
// Update page attribute
resultsEl.setAttribute( 'data-page', page );
// Populate the tab with results
resultsEl.innerHTML = html;
startPrevNextListeners();
// Add event listener to "start new search" button if no results returned
if ( 0 === total ) {
const noResultsButton = document.querySelector( `.${ classPrefix }__searched > [data-endpoint="${ tab }"] .${ noResultsClass } > button` );
noResultsButton.addEventListener( 'click', noResultsButtonHandler );
}
// 2) Tab count
const countElement = document.querySelector( `.${ classPrefix }__tab[data-endpoint="${ tab }"] .${ classPrefix }__tab-count` );
countElement.innerText = total;
};
/**
* Function used to determine active tab on form submissions and recent search button clicks
* Just those two events *because* they are not explicitly searching on a particular tab
*
* @param {String} blogTotal - Blog result count
* @param {String} extensionsTotal - Extensions result count
* @param {String} kbTotal - KB result count
*
*/
const tabPrioritizer = ( blogTotal, extensionsTotal, kbTotal ) => {
// We have blog results, let's prioritize that and cease execution
if ( parseInt( blogTotal ) > 0 ) {
return 'blog';
}
// Do we have extensions content? Let's return that and cease execution
if ( parseInt( extensionsTotal ) > 0 ) {
return 'extensions';
}
// Do we have kb content? Okay, let's return that.
if ( parseInt( kbTotal ) > 0 ) {
return 'kb';
}
// At this point, we have 0 results across all tabs
// I guess we can return 'blog' again
return 'blog';
};
/**
* Function that gathers search results and sets active tab accordingly.
*
* @param {String} term - The search term
* @param {String} tab - The active tab
*/
const startSearching = ( term, tab = false, page = 1 ) => {
// If the term is an empty string, don't do anything
if ( isEmptyString( term ) ) {
return;
}
// I guess we're searching now. Set loading state.
setLoading();
// Might as well add the term to recent searches
addToRecentSearches( term );
// Get ready to show recent searches should we return to that state later
showRecentsOrNot();
// Retrieve blog results
const blogResults = getSearchResults( term, 'blog', page );
// Retrive knowledgebase results
const extensionResults = getSearchResults( term, 'extensions', page );
// Retrive kb results
const kbResults = getSearchResults( term, 'kb', page );
// Set searchTerm state
searchSearched.setAttribute( 'data-searchTerm', term );
// Promise we get back results of all endpoints
Promise.all( [ blogResults, extensionResults, kbResults ] ).then( ( [ blog, extensions, kb ] ) => {
// Populate results of knowledgebase tab
populateResults( blog.results, blog.total, blog.totalPages, 'blog', 1 );
// Populate results of extensions tab
populateResults( extensions.results, extensions.total, extensions.totalPages, 'extensions', 1 );
// Populate results of kb tab
populateResults( kb.results, kb.total, kb.totalPages, 'kb', 1 );
let activeTab;
if ( false === tab ) {
activeTab = tabPrioritizer( blog.total, extensions.total, kb.total );
} else {
activeTab = tab;
}
// Set the active tab
setActiveTab( activeTab );
} );
};
/**
* Function to handle when a "recent search item" button is clicked.
* These buttons appear in the "not searching" state. For example:
*
* [Magnifying Glass Icon] Most recently searched term
* [Magnifying Glass Icon] Second most recently searched term
*
* @param {Object} target - Destructured from the event
*/
const recentSearchButtonHandler = ( { target } ) => {
// Get the innerText property from the button, rename to "term"
const { innerText: term } = target.closest( 'button' );
// Let's update the searchInput value as if they actually typed it in.
searchInput.value = term;
// Start searching
startSearching( term );
};
/**
* Function to populate recent searches.
*/
const populateRecentSearches = () => {
// Get recent searches
const recentSearches = getRecentSearches();
// We don't have recent searches, let's chill
if ( ! recentSearches ) {
return;
}
// We do have recent searches, let's start writing HTML
let html = '';
// Build the HTML
recentSearches.forEach( search => {
// "Escape" the HTML
// @link: https://stackoverflow.com/a/22706073
const escapeStaging = document.createElement( 'p' );
escapeStaging.appendChild( document.createTextNode( search ) );
const { innerHTML: escapedSearch } = escapeStaging;
escapeStaging.remove();
// Build the HTML
html += ``;
} );
// Select the HTML element we're going to fill up with new HTML
const recentsEl = document.querySelector( `.${ classPrefix }__fresh-recents-searches` );
// Fill up with new HTML
recentsEl.innerHTML = html;
// Select all newly-created recent search buttons
const recentSearchButtons = recentsEl.querySelectorAll( `.${ recentSearchesButtonClass }` );
// Add event listeners to each button
recentSearchButtons.forEach( button => button.addEventListener( 'click', recentSearchButtonHandler ) );
};
/**
* Set the "notSearching" state.
* This is when either recent searches show up.
* Or, if no recent searches, a "No Recent Searches" view.
*/
const setNotSearching = () => {
// Make sure recent searches are up-to-date
populateRecentSearches();
// Set the state
app.setAttribute( 'data-state', 'notSearching' );
};
/**
* Function to handle when a "term button" is clicked.
* These buttons appear in the "searching" state. For example:
*
* "Test in Blog"
* "Test in Extensions"
* "Test in KB"
*
* @param {Object} target - Destructured from the event
* @since feature/TECCOM-1783-new-search-experience
*/
const termButtonClickHandler = ( { target } ) => {
// Destructure the data-endpoint value from the "searching" state buttons
const { endpoint } = target.closest( `.${ classPrefix }__searching-item` ).dataset;
// We're ready to start searching!
startSearching( searchInput.value, endpoint );
};
/**
* Function to set the "searching" state.
*
* This is before a search has actually taken place.
*
* As the user types, the user will see buttons like the following:
*
* "Test in Knowledgebase"
* "Test in Developer Docs"
* "Test in Blog Posts"
*
* @since feature/TECCOM-1783-new-search-experience
*/
const setSearching = () => {
app.setAttribute( 'data-state', 'searching' );
};
/**
* This happens in the "searching" state.
*
* This is what takes the searchInput value and plops that into the each term button.
*
* For example:
*
* "Test in Blog"
* "Test in Extensions"
* "Test in Knowledgebase"
*
*/
const teleportTermToDivs = () => {
blankTerms.forEach( term => {
term.innerText = searchInput.value;
} );
};
/**
* Function to handle searchInput keyup events.
*
* @param {String} oldValue - This is the value *before* the keyup event occurs.
* @param {String} key - This is the key (like "ArrowDown") that was pressed.
*/
const searchInputKeyHandler = oldValue => {
// The string hasn't changed, stop running
if ( oldValue === searchInput.value ) {
return;
}
// If the search input is empty, we're not searching anymore. Sorry.
if ( isEmptyString( searchInput.value ) ) {
setNotSearching();
return;
}
// Set searching state
setSearching();
// Make sure "term buttons" have been teleported accordingly
teleportTermToDivs();
};
const activateModal = () => {
// Let's make sure we grab recent searches
populateRecentSearches();
// Do we have recent searches? Let's make sure they show up as expected
showRecentsOrNot();
// Stateless? I guess we're notSearching
if ( ! app.dataset.state ) {
app.setAttribute( 'data-state', 'notSearching' );
}
// Focus on the search input
searchInput.focus();
// Listen for keystrokes on search input
let oldValue = '';
searchInput.addEventListener( 'keydown', function() {
oldValue = searchInput.value;
} );
searchInput.addEventListener( 'keyup', function( { key } ) {
searchInputKeyHandler( oldValue, key );
} );
};
/**
* While in the "searched" state, this function handles click events on the tabs.
*
* For example: Blog, Extensions, KB
*
*/
const tabClickHandler = ( { target } ) => {
// Bubble up to the button, if needed
const clickedTab = target.closest( 'button' );
// Do nothing if tab is already active
if ( clickedTab.classList.contains( tabActiveClass ) ) {
return;
}
// Destructure the data-endpoint value
const { dataset: { endpoint } } = clickedTab;
// Set the tab according to the endpoint (knowledgebase, tec.com, etc.)
setActiveTab( endpoint );
};
/**
* Function to handle search form submission events.
*/
const formHandler = event => {
// Hijack default browser behavior because this isn't a normal HTML form
event.preventDefault();
// Start searching
startSearching( searchInput.value );
};
// Activate modal on load
activateModal();
// Listen to form submissions
searchForm.addEventListener( 'submit', formHandler );
// Listen for clicks on those "Search for [term] in [Knowledgebase, Developer Docs, etc.] buttons"
termButtons.forEach( button => button.addEventListener( 'click', termButtonClickHandler ) );
// Listen for clicks on tabs like Knowledgebase or Developer Docs visible *after* a search has been run
tabs.forEach( tab => tab.addEventListener( 'click', tabClickHandler ) );
})();
Register now to get started
You’re just a few clicks away from experiencing LearnDash from a course creator’s perspective. We’ll create an account for you to access the course.
Get 40% off LearnDash LMS, LearnDash Cloud Annual plan, add-on’s, & bundles. $15 for the first 3 months of LearnDash Cloud Monthly plan. Sale ends 12/3/24 at 11:59 PM EST!