/** * @output wp-admin/js/dashboard.js */ /* global pagenow, ajaxurl, postboxes, wpActiveEditor:true, ajaxWidgets */ /* global ajaxPopulateWidgets, quickPressLoad, */ window.wp = window.wp || {}; window.communityEventsData = window.communityEventsData || {}; /** * Initializes the dashboard widget functionality. * * @since 2.7.0 */ jQuery(function ($) { /** * These widgets can be populated via ajax. * * @since 2.7.0 * * @type {string[]} * * @global */ window.ajaxWidgets = ['wpp_dashboard_primary']; /** * Triggers widget updates via Ajax. * * @since 2.7.0 * * @global * * @param {string} el Optional. Widget to fetch or none to update all. * * @return {void} */ window.ajaxPopulateWidgets = function (el) { /** * Fetch the latest representation of the widget via Ajax and show it. * * @param {number} i Number of half-seconds to use as the timeout. * @param {string} id ID of the element which is going to be checked for changes. * * @return {void} */ function show(i, id) { var p, e = $('#' + id + ' div.inside:visible').find('.widget-loading'); // If the element is found in the dom, queue to load latest representation. if (e.length) { p = e.parent(); setTimeout(function () { // Request the widget content. p.load(ajaxurl + '?action=wpp-dashboard-widgets&widget=' + id + '&pagenow=' + pagenow, '', function () { // Hide the parent and slide it out for visual fanciness. p.hide().slideDown('normal', function () { $(this).css('display', ''); }); }); }, i * 500); } } // If we have received a specific element to fetch, check if it is valid. if (el) { el = el.toString(); // If the element is available as Ajax widget, show it. if ($.inArray(el, ajaxWidgets) !== -1) { // Show element without any delay. show(0, el); } } else { // Walk through all ajaxWidgets, loading them after each other. $.each(ajaxWidgets, show); } }; // Initially populate ajax widgets. ajaxPopulateWidgets(); // Register ajax widgets as postbox toggles. postboxes.add_postbox_toggles(pagenow, {pbshow: ajaxPopulateWidgets}); }); jQuery(function ($) { 'use strict'; var communityEventsData = window.communityEventsData, dateI18n = wp.date.dateI18n, format = wp.date.format, sprintf = wp.i18n.sprintf, __ = wp.i18n.__, _x = wp.i18n._x, app; /** * Global Community Events namespace. * * @since 4.8.0 * * @memberOf wp * @namespace wp.communityEvents */ app = window.wp.communityEvents = /** @lends wp.communityEvents */{ initialized: false, model: null, /** * Initializes the wp.communityEvents object. * * @since 4.8.0 * * @return {void} */ init: function () { if (app.initialized) { return; } var $container = $('#community-events'); /* * When JavaScript is disabled, the errors container is shown, so * that "This widget requires JavaScript" message can be seen. * * When JS is enabled, the container is hidden at first, and then * revealed during the template rendering, if there actually are * errors to show. * * The display indicator switches from `hide-if-js` to `aria-hidden` * here in order to maintain consistency with all the other fields * that key off of `aria-hidden` to determine their visibility. * `aria-hidden` can't be used initially, because there would be no * way to set it to false when JavaScript is disabled, which would * prevent people from seeing the "This widget requires JavaScript" * message. */ $('.community-events-errors') .attr('aria-hidden', 'true') .removeClass('hide-if-js'); $container.on('click', '.community-events-toggle-location, .community-events-cancel', app.toggleLocationForm); /** * Filters events based on entered location. * * @return {void} */ $container.on('submit', '.community-events-form', function (event) { var location = $('#community-events-location').val().trim(); event.preventDefault(); /* * Don't trigger a search if the search field is empty or the * search term was made of only spaces before being trimmed. */ if (!location) { return; } app.getEvents({ location: location }); }); if (communityEventsData && communityEventsData.cache && communityEventsData.cache.location && communityEventsData.cache.events) { app.renderEventsTemplate(communityEventsData.cache, 'app'); } else { app.getEvents(); } app.initialized = true; }, /** * Toggles the visibility of the Edit Location form. * * @since 4.8.0 * * @param {event|string} action 'show' or 'hide' to specify a state; * or an event object to flip between states. * * @return {void} */ toggleLocationForm: function (action) { var $toggleButton = $('.community-events-toggle-location'), $cancelButton = $('.community-events-cancel'), $form = $('.community-events-form'), $target = $(); if ('object' === typeof action) { // The action is the event object: get the clicked element. $target = $(action.target); /* * Strict comparison doesn't work in this case because sometimes * we explicitly pass a string as value of aria-expanded and * sometimes a boolean as the result of an evaluation. */ action = 'true' === $toggleButton.attr('aria-expanded') ? 'hide' : 'show'; } if ('hide' === action) { $toggleButton.attr('aria-expanded', 'false'); $cancelButton.attr('aria-expanded', 'false'); $form.attr('aria-hidden', 'true'); /* * If the Cancel button has been clicked, bring the focus back * to the toggle button so users relying on screen readers don't * lose their place. */ if ($target.hasClass('community-events-cancel')) { $toggleButton.trigger('focus'); } } else { $toggleButton.attr('aria-expanded', 'true'); $cancelButton.attr('aria-expanded', 'true'); $form.attr('aria-hidden', 'false'); } }, /** * Sends REST API requests to fetch events for the widget. * * @since 4.8.0 * * @param {Object} requestParams REST API Request parameters object. * * @return {void} */ getEvents: function (requestParams) { var initiatedBy, app = this, $spinner = $('.community-events-form').children('.spinner'); requestParams = requestParams || {}; requestParams._wpnonce = communityEventsData.nonce; requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : ''; initiatedBy = requestParams.location ? 'user' : 'app'; $spinner.addClass('is-active'); wp.ajax.post('get-community-events', requestParams) .always(function () { $spinner.removeClass('is-active'); }) .done(function (response) { if ('no_location_available' === response.error) { if (requestParams.location) { response.unknownCity = requestParams.location; } else { /* * No location was passed, which means that this was an automatic query * based on IP, locale, and timezone. Since the user didn't initiate it, * it should fail silently. Otherwise, the error could confuse and/or * annoy them. */ delete response.error; } } app.renderEventsTemplate(response, initiatedBy); }) .fail(function () { app.renderEventsTemplate({ 'location': false, 'events': [], 'error': true }, initiatedBy); }); }, /** * Renders the template for the Events section of the Events & News widget. * * @since 4.8.0 * * @param {Object} templateParams The various parameters that will get passed to wp.template. * @param {string} initiatedBy 'user' to indicate that this was triggered manually by the user; * 'app' to indicate it was triggered automatically by the app itself. * * @return {void} */ renderEventsTemplate: function (templateParams, initiatedBy) { var template, elementVisibility, $toggleButton = $('.community-events-toggle-location'), $locationMessage = $('#community-events-location-message'), $results = $('.community-events-results'); templateParams.events = app.populateDynamicEventFields( templateParams.events, communityEventsData.time_format ); /* * Hide all toggleable elements by default, to keep the logic simple. * Otherwise, each block below would have to turn hide everything that * could have been shown at an earlier point. * * The exception to that is that the .community-events container is hidden * when the page is first loaded, because the content isn't ready yet, * but once we've reached this point, it should always be shown. */ elementVisibility = { '.community-events': true, '.community-events-loading': false, '.community-events-errors': false, '.community-events-error-occurred': false, '.community-events-could-not-locate': false, '#community-events-location-message': false, '.community-events-toggle-location': false, '.community-events-results': false }; /* * Determine which templates should be rendered and which elements * should be displayed. */ if (templateParams.location.ip) { /* * If the API determined the location by geolocating an IP, it will * provide events, but not a specific location. */ $locationMessage.text(__('Attend an upcoming event near you.')); if (templateParams.events.length) { template = wp.template('community-events-event-list'); $results.html(template(templateParams)); } else { template = wp.template('community-events-no-upcoming-events'); $results.html(template(templateParams)); } elementVisibility['#community-events-location-message'] = true; elementVisibility['.community-events-toggle-location'] = true; elementVisibility['.community-events-results'] = true; } else if (templateParams.location.description) { template = wp.template('community-events-attend-event-near'); $locationMessage.html(template(templateParams)); if (templateParams.events.length) { template = wp.template('community-events-event-list'); $results.html(template(templateParams)); } else { template = wp.template('community-events-no-upcoming-events'); $results.html(template(templateParams)); } if ('user' === initiatedBy) { wp.a11y.speak( sprintf( /* translators: %s: The name of a city. */ __('City updated. Listing events near %s.'), templateParams.location.description ), 'assertive' ); } elementVisibility['#community-events-location-message'] = true; elementVisibility['.community-events-toggle-location'] = true; elementVisibility['.community-events-results'] = true; } else if (templateParams.unknownCity) { template = wp.template('community-events-could-not-locate'); $('.community-events-could-not-locate').html(template(templateParams)); wp.a11y.speak( sprintf( /* * These specific examples were chosen to highlight the fact that a * state is not needed, even for cities whose name is not unique. * It would be too cumbersome to include that in the instructions * to the user, so it's left as an implication. */ /* * translators: %s is the name of the city we couldn't locate. * Replace the examples with cities related to your locale. Test that * they match the expected location and have upcoming events before * including them. If no cities related to your locale have events, * then use cities related to your locale that would be recognizable * to most users. Use only the city name itself, without any region * or country. Use the endonym (native locale name) instead of the * English name if possible. */ __('We couldn’t locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland.'), templateParams.unknownCity ) ); elementVisibility['.community-events-errors'] = true; elementVisibility['.community-events-could-not-locate'] = true; } else if (templateParams.error && 'user' === initiatedBy) { /* * Errors messages are only shown for requests that were initiated * by the user, not for ones that were initiated by the app itself. * Showing error messages for an event that user isn't aware of * could be confusing or unnecessarily distracting. */ wp.a11y.speak(__('An error occurred. Please try again.')); elementVisibility['.community-events-errors'] = true; elementVisibility['.community-events-error-occurred'] = true; } else { $locationMessage.text(__('Enter your closest city to find nearby events.')); elementVisibility['#community-events-location-message'] = true; elementVisibility['.community-events-toggle-location'] = true; } // Set the visibility of toggleable elements. _.each(elementVisibility, function (isVisible, element) { $(element).attr('aria-hidden', !isVisible); }); $toggleButton.attr('aria-expanded', elementVisibility['.community-events-toggle-location']); if (templateParams.location && (templateParams.location.ip || templateParams.location.latitude)) { // Hide the form when there's a valid location. app.toggleLocationForm('hide'); if ('user' === initiatedBy) { /* * When the form is programmatically hidden after a user search, * bring the focus back to the toggle button so users relying * on screen readers don't lose their place. */ $toggleButton.trigger('focus'); } } else { app.toggleLocationForm('show'); } }, /** * Populate event fields that have to be calculated on the fly. * * These can't be stored in the database, because they're dependent on * the user's current time zone, locale, etc. * * @since 5.5.2 * * @param {Array} rawEvents The events that should have dynamic fields added to them. * @param {string} timeFormat A time format acceptable by `wp.date.dateI18n()`. * * @returns {Array} */ populateDynamicEventFields: function (rawEvents, timeFormat) { // Clone the parameter to avoid mutating it, so that this can remain a pure function. var populatedEvents = JSON.parse(JSON.stringify(rawEvents)); $.each(populatedEvents, function (index, event) { var timeZone = app.getTimeZone(event.start_unix_timestamp * 1000); event.user_formatted_date = app.getFormattedDate( event.start_unix_timestamp * 1000, event.end_unix_timestamp * 1000, timeZone ); event.user_formatted_time = dateI18n( timeFormat, event.start_unix_timestamp * 1000, timeZone ); event.timeZoneAbbreviation = app.getTimeZoneAbbreviation(event.start_unix_timestamp * 1000); }); return populatedEvents; }, /** * Returns the user's local/browser time zone, in a form suitable for `wp.date.i18n()`. * * @since 5.5.2 * * @param startTimestamp * * @returns {string|number} */ getTimeZone: function (startTimestamp) { /* * Prefer a name like `Europe/Helsinki`, since that automatically tracks daylight savings. This * doesn't need to take `startTimestamp` into account for that reason. */ var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; /* * Fall back to an offset for IE11, which declares the property but doesn't assign a value. */ if ('undefined' === typeof timeZone) { /* * It's important to use the _event_ time, not the _current_ * time, so that daylight savings time is accounted for. */ timeZone = app.getFlippedTimeZoneOffset(startTimestamp); } return timeZone; }, /** * Get intuitive time zone offset. * * `Data.prototype.getTimezoneOffset()` returns a positive value for time zones * that are _behind_ UTC, and a _negative_ value for ones that are ahead. * * See https://stackoverflow.com/questions/21102435/why-does-javascript-date-gettimezoneoffset-consider-0500-as-a-positive-off. * * @since 5.5.2 * * @param {number} startTimestamp * * @returns {number} */ getFlippedTimeZoneOffset: function (startTimestamp) { return new Date(startTimestamp).getTimezoneOffset() * -1; }, /** * Get a short time zone name, like `PST`. * * @since 5.5.2 * * @param {number} startTimestamp * * @returns {string} */ getTimeZoneAbbreviation: function (startTimestamp) { var timeZoneAbbreviation, eventDateTime = new Date(startTimestamp); /* * Leaving the `locales` argument undefined is important, so that the browser * displays the abbreviation that's most appropriate for the current locale. For * some that will be `UTC{+|-}{n}`, and for others it will be a code like `PST`. * * This doesn't need to take `startTimestamp` into account, because a name like * `America/Chicago` automatically tracks daylight savings. */ var shortTimeStringParts = eventDateTime.toLocaleTimeString(undefined, {timeZoneName: 'short'}).split(' '); if (3 === shortTimeStringParts.length) { timeZoneAbbreviation = shortTimeStringParts[2]; } if ('undefined' === typeof timeZoneAbbreviation) { /* * It's important to use the _event_ time, not the _current_ * time, so that daylight savings time is accounted for. */ var timeZoneOffset = app.getFlippedTimeZoneOffset(startTimestamp), sign = -1 === Math.sign(timeZoneOffset) ? '' : '+'; // translators: Used as part of a string like `GMT+5` in the Events Widget. timeZoneAbbreviation = _x('GMT', 'Events widget offset prefix') + sign + (timeZoneOffset / 60); } return timeZoneAbbreviation; }, /** * Format a start/end date in the user's local time zone and locale. * * @since 5.5.2 * * @param {int} startDate The Unix timestamp in milliseconds when the the event starts. * @param {int} endDate The Unix timestamp in milliseconds when the the event ends. * @param {string} timeZone A time zone string or offset which is parsable by `wp.date.i18n()`. * * @returns {string} */ getFormattedDate: function (startDate, endDate, timeZone) { var formattedDate; /* * The `date_format` option is not used because it's important * in this context to keep the day of the week in the displayed date, * so that users can tell at a glance if the event is on a day they * are available, without having to open the link. * * The case of crossing a year boundary is intentionally not handled. * It's so rare in practice that it's not worth the complexity * tradeoff. The _ending_ year should be passed to * `multiple_month_event`, though, just in case. */ /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/manual/datetime.format.php */ var singleDayEvent = __('l, M j, Y'), /* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */ multipleDayEvent = __('%1$s %2$d–%3$d, %4$d'), /* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Ending year. */ multipleMonthEvent = __('%1$s %2$d – %3$s %4$d, %5$d'); // Detect single-day events. if (!endDate || format('Y-m-d', startDate) === format('Y-m-d', endDate)) { formattedDate = dateI18n(singleDayEvent, startDate, timeZone); // Multiple day events. } else if (format('Y-m', startDate) === format('Y-m', endDate)) { formattedDate = sprintf( multipleDayEvent, dateI18n(_x('F', 'upcoming events month format'), startDate, timeZone), dateI18n(_x('j', 'upcoming events day format'), startDate, timeZone), dateI18n(_x('j', 'upcoming events day format'), endDate, timeZone), dateI18n(_x('Y', 'upcoming events year format'), endDate, timeZone) ); // Multi-day events that cross a month boundary. } else { formattedDate = sprintf( multipleMonthEvent, dateI18n(_x('F', 'upcoming events month format'), startDate, timeZone), dateI18n(_x('j', 'upcoming events day format'), startDate, timeZone), dateI18n(_x('F', 'upcoming events month format'), endDate, timeZone), dateI18n(_x('j', 'upcoming events day format'), endDate, timeZone), dateI18n(_x('Y', 'upcoming events year format'), endDate, timeZone) ); } return formattedDate; } }; const wppWidget = $('#wpp_dashboard_primary'); if (wppWidget.is(':visible')) { app.init(); } else { $(document).on('postbox-toggled', function (event, postbox) { var $postbox = $(postbox); if ('wpp_dashboard_primary' === $postbox.attr('id') && $postbox.is(':visible')) { app.init(); } }); } }); const slidesContainer = document.getElementById('wpp_sponsorship'); const sponsorshipGuid = document.getElementById('sponsorship-guide'); const wppsPlaceholder = document.getElementById('wpp_sponsorship_placeholder'); const wppsQuestion = document.querySelector('#sponsorship-guide .question'); const sponsorshipUl = document.querySelector('#sponsorship-guide ul'); async function wppInitSponsors() { try { const response = await fetch(ajaxurl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ action: 'fetch_sponsorship_slides' }) }); const result = await response.json(); if (result.success) { const slidesData = Object.values(result.data); // Convert object to array wppsPlaceholder.remove(); createSlides(slidesData); new KeenSlider(slidesContainer, { loop: true, duration: 5000, controls: true, indicators: true, }); } else { console.error('Error fetching slides:', result.data); } } catch (error) { console.error('Error fetching slides:', error); } } function createSlides(slidesData) { slidesData.forEach((slide) => { const slideElement = document.createElement('div'); slideElement.classList.add('keen-slider__slide'); slideElement.innerHTML = '' + slide.image_alt + ''; slidesContainer.appendChild(slideElement); }); if (slidesData.length > 1) { initWPPSSlider(); } } function initWPPSSlider() { let wppsSlider = new KeenSlider(slidesContainer, { rtl: getComputedStyle(slidesContainer).direction, loop: true, duration: 3000, controls: true, indicators: true, }, [wppsNavigation, (slider) => { let timeout; let mouseOver = false; function clearNextTimeout() { clearTimeout(timeout); } function nextTimeout() { clearTimeout(timeout); if (mouseOver) { return; } timeout = setTimeout(() => { slider.next(); }, 4000); } slider.on('created', () => { slider.container.addEventListener('mouseover', () => { mouseOver = true; clearNextTimeout(); }); slider.container.addEventListener('mouseout', () => { mouseOver = false; nextTimeout(); }); nextTimeout(); }); slider.on('dragStarted', clearNextTimeout); slider.on('animationEnded', nextTimeout); slider.on('updated', nextTimeout); },]); } function wppsNavigation(slider) { let wrapper, dots, arrowLeft, arrowRight; function markup(remove) { wrapperMarkup(remove); dotMarkup(remove); arrowMarkup(remove); } function removeElement(elment) { elment.parentNode.removeChild(elment); } function createDiv(className) { const div = document.createElement('div'); let classNames = className.split(' '); classNames.forEach((name) => div.classList.add(name)); return div; } function arrowMarkup(remove) { if (remove) { removeElement(arrowLeft); removeElement(arrowRight); return; } arrowLeft = createDiv('arrow arrow--left'); arrowLeft.addEventListener('click', () => slider.prev()); arrowRight = createDiv('arrow arrow--right'); arrowRight.addEventListener('click', () => slider.next()); wrapper.appendChild(arrowLeft); wrapper.appendChild(arrowRight); } function wrapperMarkup(remove) { if (remove) { const parent = wrapper.parentNode; while (wrapper.firstChild) { parent.insertBefore(wrapper.firstChild, wrapper); } removeElement(wrapper); return; } wrapper = createDiv('navigation-wrapper'); slider.container.parentNode.prepend(wrapper); wrapper.appendChild(slider.container); } function dotMarkup(remove) { if (remove) { removeElement(dots); return; } dots = createDiv('dots'); slider.track.details.slides.forEach((_e, idx) => { const dot = createDiv('dot'); dot.addEventListener('click', () => slider.moveToIdx(idx)); dots.appendChild(dot); }); wrapper.appendChild(dots); } function updateClasses() { let slide = slider.track.details.rel; slide === 0 ? arrowLeft.classList.add('arrow--disabled') : arrowLeft.classList.remove('arrow--disabled'); slide === slider.track.details.slides.length - 1 ? arrowRight.classList.add('arrow--disabled') : arrowRight.classList.remove('arrow--disabled'); Array.from(dots.children).forEach(function (dot, idx) { idx === slide ? dot.classList.add('dot--active') : dot.classList.remove('dot--active'); }); } slider.on('created', () => { markup(); updateClasses(); }); slider.on('optionsChanged', () => { console.log(2); markup(true); markup(); updateClasses(); }); slider.on('slideChanged', () => { updateClasses(); }); slider.on('destroyed', () => { markup(true); }); } wppInitSponsors(); wppsQuestion.addEventListener('click', function (event) { sponsorshipUl.classList.toggle('show'); event.stopPropagation(); }); document.addEventListener('click', function (event) { if (!sponsorshipGuid.contains(event.target)) { sponsorshipUl.classList.remove('show'); } });