The LearnDash demo lets you experience a LearnDash course from both a course creator’s and learner’s perspective. Explore the learner’s demo to see how courses look or spin up your course website first before buying.
Our demo lets you experience all LearnDash offers without any commitment to purchase. Additionally, we offer a 30-day money-back guarantee if LearnDash is not the right fit.
We don’t offer phone consultations in order to keep our prices as low as possible for our customer community. However, we do host a “Meet LearnDash” webinar every Tuesday to showcase the product to new customers. Sign up for our next webinar here.
Course creation
Anyone who needs to turn a WordPress site into a learning management system, including WordPress developers, training organizations, educational institutions, and content creators.
LearnDash is a premium WordPress plugin that works with most WordPress themes.
LearnDash comes built-in with Stripe, with Paypal, RazorPay, and 2Checkout as additional integrations.
ProPanel is an add-on that enhances your LearnDash admin experience by consolidating reporting and offering a quick view of your courses. Click here to learn more about what ProPanel does.
Product support
Pricing for the LearnDash plugin is annual. Pricing for the LearnDash Cloud is monthly or annual. An active license key is needed to continue to receive new feature updates, security patches, and support.
For any reason, you can turn off your monthly (LearnDash Cloud) or annual (LearnDash Plugin or Cloud) subscriptions auto-renewal in your account settings.
Yes. SCORM 1.2, SCORM 2004, and xAPI (Tin Can API) are supported when using a third-party integration.
LearnDash is translation ready, and we do have user-donated translations available.
`;
} );
// 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 ) );
})();
Wait! Before you go, don’t forget to join our community
Subscribe to our newsletter so you never miss out on product updates, insider tips, free webinars, and more!
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.