{"version":3,"sources":["webpack:///./app/javascript/packs/followButtons.js","webpack:///./app/javascript/utilities/locale.js","webpack:///./app/javascript/topNavigation/utilities.js"],"names":["addButtonFollowText","button","style","JSON","parse","dataset","info","name","className","addAriaLabelToButton","followName","followType","textContent","locale","label","pressed","toLowerCase","setAttribute","length","removeAttribute","updateFollowingButton","verb","addButtonFollowingText","classList","remove","add","updateUserOwnFollowButton","updateFollowButton","newState","buttonInfo","followStyle","handleFollowButtonClick","matchingFollowButtons","target","contains","document","body","getAttribute","showLoginModal","Array","from","getElementsByClassName","filter","id","forEach","matchingButton","window","location","href","formData","FormData","append","getCsrfToken","then","sendFetch","response","status","showModalAfterError","element","action_ing","action_past","timeframe","updateInitialButtonUI","followStatus","initializeAllUserFollowButtons","buttons","querySelectorAll","idButtonHash","url","searchParams","userIds","fetched","userStatus","userId","push","Object","keys","URL","URLSearchParams","search","fetch","method","headers","Accept","csrfToken","credentials","json","idStatuses","initializeNonUserFollowButtons","nonUserFollowButtons","user","userData","followedTags","followed_tags","map","tag","followedTagIds","Set","has","text","fetchFollowButtonStatus","getElementById","addEventListener","followClicksInitialized","observer","MutationObserver","mutationsList","mutation","type","followButtonContainer","observe","childList","subtree","getInstantClick","ic","on","disconnect","translationsDiv","term","I18n","t","translations","defaultLocale","closeHeaderMenu","memberMenu","menuNavButton","clicked","firstItem","initializeMemberMenu","memberTopMenu","navigator","userAgent","_event","focus","focusFirstItem","activeElement","requestAnimationFrame","openHeaderMenu","test","e","key","querySelector","event","stopPropagation","closest","secondToLastNavLink","setTimeout","toggleBurgerMenu","leftNavState","showMoreMenu","nextElementSibling","waitTime","Promise","resolve","reject","failTimer","clearInterval","timer","Error","setInterval","InstantClick","clearTimeout","initializeMobileMenu","menuTriggers","moreMenus","trigger","onclick","setCurrentPageIconLink","currentPage","pageEntries","page","iconLink","blur"],"mappings":"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","file":"js/followButtons-92f9ba4418f60fd377d1.chunk.js","sourcesContent":["import { getInstantClick } from '../topNavigation/utilities';\nimport { locale } from '@utilities/locale';\n\n/* global showLoginModal userData showModalAfterError*/\n\n/**\n * Sets the text content of the button to the correct 'Follow' state\n *\n * @param {HTMLElement} button The Follow button to update\n * @param {string} style The style of the button from its \"info\" data attribute\n */\n\n\nfunction addButtonFollowText(button, style) {\n const { name, className } = JSON.parse(button.dataset.info);\n\n switch (style) {\n case 'small':\n addAriaLabelToButton({\n button,\n followName: name,\n followType: className,\n style: 'follow',\n });\n button.textContent = '+';\n break;\n case 'follow-back':\n addAriaLabelToButton({\n button,\n followName: name,\n followType: className,\n style: 'follow-back',\n });\n button.textContent = locale('core.follow_back');\n break;\n default:\n addAriaLabelToButton({\n button,\n followName: name,\n followType: className,\n style: 'follow',\n });\n button.textContent = locale('core.follow');\n }\n}\n\n/**\n * Sets the aria-label and aria-pressed value of the button\n *\n * @param {HTMLElement} button The Follow button to update.\n * @param {string} followType The followableType of the button.\n * @param {string} followName The name of the followable to be followed.\n * @param {string} style The style of the button from its \"info\" data attribute\n */\nfunction addAriaLabelToButton({ button, followType, followName, style = '' }) {\n let label = '';\n let pressed = '';\n switch (style) {\n case 'follow':\n label = `Follow ${followType.toLowerCase()}: ${followName}`;\n pressed = 'false';\n break;\n case 'follow-back':\n label = `Follow ${followType.toLowerCase()} back: ${followName}`;\n pressed = 'false';\n break;\n case 'following':\n label = `Follow ${followType.toLowerCase()}: ${followName}`;\n pressed = 'true';\n break;\n case 'self':\n label = `Edit profile`;\n break;\n default:\n label = `Follow ${followType.toLowerCase()}: ${followName}`;\n pressed = 'false';\n }\n button.setAttribute('aria-label', label);\n pressed.length === 0\n ? button.removeAttribute('aria-pressed')\n : button.setAttribute('aria-pressed', pressed);\n}\n\n/**\n * Sets the text content of the button to the correct 'Following' state\n *\n * @param {HTMLElement} button The Follow button to update\n * @param {string} style The style of the button from its \"info\" data attribute\n */\nfunction addButtonFollowingText(button, style) {\n button.textContent = style === 'small' ? '✓' : locale('core.following');\n}\n\n/**\n * Changes the visual appearance and 'verb' of the button to match the new state\n *\n * @param {HTMLElement} button The Follow button to be updated\n */\nfunction optimisticallyUpdateButtonUI(button) {\n const { verb: newState } = button.dataset;\n const buttonInfo = JSON.parse(button.dataset.info);\n const { style } = buttonInfo;\n\n // Often there are multiple follow buttons for the same followable item on the page\n // We collect all buttons which match the click, and update them all\n const matchingFollowButtons = Array.from(\n document.getElementsByClassName('follow-action-button'),\n ).filter((button) => {\n const { info } = button.dataset;\n if (info) {\n const { id } = JSON.parse(info);\n return id === buttonInfo.id;\n }\n return false;\n });\n\n matchingFollowButtons.forEach((matchingButton) => {\n matchingButton.classList.add('showing');\n\n switch (newState) {\n case 'follow':\n case 'follow-back':\n updateFollowButton(matchingButton, newState, buttonInfo);\n break;\n case 'login':\n addButtonFollowText(matchingButton, style);\n break;\n case 'self':\n updateUserOwnFollowButton(matchingButton);\n break;\n default:\n updateFollowingButton(matchingButton, style);\n }\n });\n}\n\n/**\n * Set the Follow button's UI to the 'following' state\n *\n * @param {HTMLElement} button The Follow button to be updated\n * @param {string} style Style of the follow button (e.g. 'small')\n */\nfunction updateFollowingButton(button, style) {\n const { name, className } = JSON.parse(button.dataset.info);\n button.dataset.verb = 'follow';\n addButtonFollowingText(button, style);\n button.classList.remove('crayons-btn--primary');\n button.classList.remove('crayons-btn--secondary');\n button.classList.add('crayons-btn--outlined');\n addAriaLabelToButton({\n button,\n followName: name,\n followType: className,\n style: 'following',\n });\n}\n\n/**\n * Update the UI of the given button to the user's own button - i.e. 'Edit profile'\n *\n * @param {HTMLElement} button The Follow button to be updated\n */\nfunction updateUserOwnFollowButton(button) {\n button.dataset.verb = 'self';\n button.textContent = locale('core.edit_profile');\n addAriaLabelToButton({\n button,\n followName: '',\n followType: '',\n style: 'self',\n });\n}\n\n/**\n * Update the UI of the given button to the 'follow' or 'follow-back' state\n *\n * @param {HTMLElement} button The Follow button to be updated\n * @param {string} newState The new follow state of the button\n * @param {Object} buttonInfo The parsed info object obtained from the button's dataset\n * @param {string} buttonInfo.style The style of the follow button (e.g 'small')\n * @param {string} buttonInfo.followStyle The crayons button variant (e.g 'primary')\n */\nfunction updateFollowButton(button, newState, buttonInfo) {\n const { style, followStyle } = buttonInfo;\n\n button.dataset.verb = 'unfollow';\n button.classList.remove('crayons-btn--outlined');\n\n if (followStyle === 'primary') {\n button.classList.add('crayons-btn--primary');\n } else if (followStyle === 'secondary') {\n button.classList.add('crayons-btn--secondary');\n }\n\n const nextButtonStyle = newState === 'follow-back' ? newState : style;\n addButtonFollowText(button, nextButtonStyle);\n}\n\n/**\n * Checks a click event's target, and if it is a follow button, triggers the appropriate follow action\n *\n * @param {HTMLElement} target The target of the click event\n */\nfunction handleFollowButtonClick({ target }) {\n if (\n target.classList.contains('follow-action-button') ||\n target.classList.contains('follow-user')\n ) {\n const userStatus = document.body.getAttribute('data-user-status');\n if (userStatus === 'logged-out') {\n showLoginModal();\n return;\n }\n\n optimisticallyUpdateButtonUI(target);\n\n const { verb } = target.dataset;\n\n if (verb === 'self') {\n window.location.href = '/settings';\n return;\n }\n\n const { className, id } = JSON.parse(target.dataset.info);\n const formData = new FormData();\n formData.append('followable_type', className);\n formData.append('followable_id', id);\n formData.append('verb', verb);\n getCsrfToken()\n .then(sendFetch('follow-creation', formData))\n .then((response) => {\n if (response.status !== 200) {\n showModalAfterError({\n response,\n element: 'user',\n action_ing: 'following',\n action_past: 'followed',\n timeframe: 'for a day',\n });\n }\n });\n }\n}\n\n/**\n * Adds an event listener to the inner page content, to handle any and all follow button clicks with a single handler\n */\nfunction listenForFollowButtonClicks() {\n document\n .getElementById('page-content-inner')\n .addEventListener('click', handleFollowButtonClick);\n\n document.getElementById(\n 'page-content-inner',\n ).dataset.followClicksInitialized = true;\n}\n\n/**\n * Sets the UI of the button based on the current following status\n *\n * @param {string} followStatus The current following status for the button\n * @param {HTMLElement} button The button to update\n */\nfunction updateInitialButtonUI(followStatus, button) {\n const buttonInfo = JSON.parse(button.dataset.info);\n const { style } = buttonInfo;\n button.classList.add('showing');\n\n switch (followStatus) {\n case 'true':\n case 'mutual':\n updateFollowingButton(button, style);\n break;\n case 'follow-back':\n addButtonFollowText(button, followStatus);\n break;\n case 'false':\n updateFollowButton(button, 'follow', buttonInfo);\n break;\n case 'self':\n updateUserOwnFollowButton(button);\n break;\n default:\n addButtonFollowText(button, style);\n }\n}\n\n/**\n * Fetches all user 'follow statuses' for the given userIds, and then updates the UI for all buttons related to each user\n *\n * @param {Object} idButtonHash A hash of user IDs and the array buttons which relate to them\n */\nfunction fetchUserFollowStatuses(idButtonHash) {\n const url = new URL('/follows/bulk_show', document.location);\n const searchParams = new URLSearchParams();\n Object.keys(idButtonHash).forEach((id) => {\n searchParams.append('ids[]', id);\n });\n searchParams.append('followable_type', 'User');\n url.search = searchParams;\n\n fetch(url, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n 'X-CSRF-Token': window.csrfToken,\n 'Content-Type': 'application/json',\n },\n credentials: 'same-origin',\n })\n .then((response) => response.json())\n .then((idStatuses) => {\n Object.keys(idStatuses).forEach((id) => {\n idButtonHash[id].forEach((button) => {\n updateInitialButtonUI(idStatuses[id], button);\n });\n });\n });\n}\n\n/**\n * Sets up the initial state of all user follow buttons on the page,\n * by obtaining the 'follow status' of each user and updating the associated buttons' UI.\n */\nfunction initializeAllUserFollowButtons() {\n const buttons = document.querySelectorAll(\n '.follow-action-button.follow-user:not([data-fetched])',\n );\n\n if (buttons.length === 0) {\n return;\n }\n\n const userIds = {};\n\n Array.from(buttons, (button) => {\n button.dataset.fetched = 'fetched';\n const { userStatus } = document.body.dataset;\n const buttonInfo = JSON.parse(button.dataset.info);\n const { name, className } = buttonInfo;\n\n if (userStatus === 'logged-out') {\n const { style } = JSON.parse(button.dataset.info);\n addButtonFollowText(button, style);\n } else {\n addAriaLabelToButton({ button, followType: className, followName: name });\n const { id: userId } = JSON.parse(button.dataset.info);\n if (userIds[userId]) {\n userIds[userId].push(button);\n } else {\n userIds[userId] = [button];\n }\n }\n });\n\n if (Object.keys(userIds).length > 0) {\n fetchUserFollowStatuses(userIds);\n }\n}\n\n/**\n * Individually fetches the current status of a follow button and updates the UI to match\n *\n * @param {HTMLElement} button\n * @param {Object} buttonInfo The parsed buttonInfo object obtained from the button's data-attribute\n */\nfunction fetchFollowButtonStatus(button, buttonInfo) {\n button.dataset.fetched = 'fetched';\n\n fetch(`/follows/${buttonInfo.id}?followable_type=${buttonInfo.className}`, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n 'X-CSRF-Token': window.csrfToken,\n 'Content-Type': 'application/json',\n },\n credentials: 'same-origin',\n })\n .then((response) => response.text())\n .then((followStatus) => {\n updateInitialButtonUI(followStatus, button);\n });\n}\n\n/**\n * Makes sure the initial state of follow buttons is fetched and presented in the UI.\n * User follow buttons are initialized separately via bulk request\n */\nfunction initializeNonUserFollowButtons() {\n const nonUserFollowButtons = document.querySelectorAll(\n '.follow-action-button:not(.follow-user):not([data-fetched])',\n );\n\n const userLoggedIn =\n document.body.getAttribute('data-user-status') === 'logged-in';\n\n const user = userLoggedIn ? userData() : null;\n\n const followedTags = user\n ? JSON.parse(user.followed_tags).map((tag) => tag.id)\n : [];\n\n const followedTagIds = new Set(followedTags);\n\n nonUserFollowButtons.forEach((button) => {\n const { info } = button.dataset;\n const buttonInfo = JSON.parse(info);\n const { className, name } = buttonInfo;\n addAriaLabelToButton({ button, followType: className, followName: name });\n if (className === 'Tag' && user) {\n // We don't need to make a network request to 'fetch' the status of tag buttons\n button.dataset.fetched = true;\n const initialButtonFollowState = followedTagIds.has(buttonInfo.id)\n ? 'true'\n : 'false';\n updateInitialButtonUI(initialButtonFollowState, button);\n } else {\n fetchFollowButtonStatus(button, buttonInfo);\n }\n });\n}\n\ninitializeAllUserFollowButtons();\ninitializeNonUserFollowButtons();\nlistenForFollowButtonClicks();\n\n// Some follow buttons are added to the DOM dynamically, e.g. search results,\n// So we listen for any new additions to be fetched\nconst observer = new MutationObserver((mutationsList) => {\n mutationsList.forEach((mutation) => {\n if (mutation.type === 'childList') {\n initializeAllUserFollowButtons();\n initializeNonUserFollowButtons();\n }\n });\n});\n\n// Any element containing the given data-attribute will be monitored for new follow buttons\ndocument\n .querySelectorAll('[data-follow-button-container]')\n .forEach((followButtonContainer) => {\n observer.observe(followButtonContainer, {\n childList: true,\n subtree: true,\n });\n });\n\ngetInstantClick().then((ic) => {\n ic.on('change', () => {\n observer.disconnect();\n });\n});\n\nwindow.addEventListener('beforeunload', () => {\n observer.disconnect();\n});\n","import I18n from \"i18n-js\"\nconst translationsDiv = document.getElementById('i18n-translations')\nif (translationsDiv) {\n I18n.translations = JSON.parse(translationsDiv.dataset.translations);\n}\nI18n.defaultLocale = 'en';\nI18n.locale = document.body.dataset.locale;\nexport function locale(term) {\n return I18n.t(term);\n}","function closeHeaderMenu(memberMenu, menuNavButton) {\n menuNavButton.setAttribute('aria-expanded', 'false');\n memberMenu.classList.remove('desktop', 'showing');\n delete memberMenu.dataset.clicked;\n}\n\nconst firstItem = document.getElementById('first-nav-link');\n\nfunction openHeaderMenu(memberMenu, menuNavButton) {\n menuNavButton.setAttribute('aria-expanded', 'true');\n memberMenu.classList.add('showing');\n\n if (!firstItem) {\n return;\n }\n\n // Focus on the first item in the menu\n (function focusFirstItem() {\n if (document.activeElement === firstItem) {\n // The first element has focus\n return;\n }\n\n firstItem.focus();\n // requestAnimationFrame is faster and more reliable than setTimeout\n // https://swizec.com/blog/how-to-wait-for-dom-elements-to-show-up-in-modern-browsers\n window.requestAnimationFrame(focusFirstItem);\n })();\n}\n\n/**\n * Determines whether or not a device is a touch device.\n *\n * @returns true if a touch device, otherwise false.\n */\nexport function isTouchDevice() {\n return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|DEV-Native-ios/i.test(\n navigator.userAgent,\n );\n}\n\n/**\n * Initializes the member navigation menu events.\n *\n * @param {HTMLElement} memberTopMenu The member menu in the top navigation.\n * @param {HTMLElement} menuNavButton The button to activate the member navigation menu.\n */\nexport function initializeMemberMenu(memberTopMenu, menuNavButton) {\n // Typically using CSS for hovering for the menu is the way to go. But... since we use InstantClick for\n // loading pages, the top header navigation never changes in terms of the DOM references.\n // Because of this, we're using mouse events to mouseover/mouseout on the member's avatar\n // to attach styles to get it to show the menu so that this works on desktop and mobile.\n if (navigator.userAgent === 'DEV-Native-ios') {\n document.body.classList.add('dev-ios-native-body');\n }\n const { classList } = memberTopMenu;\n menuNavButton.addEventListener('click', (_event) => {\n if (classList.contains('showing') && memberTopMenu.dataset.clicked) {\n closeHeaderMenu(memberTopMenu, menuNavButton);\n menuNavButton.focus();\n } else {\n openHeaderMenu(memberTopMenu, menuNavButton);\n memberTopMenu.dataset.clicked = 'clicked';\n }\n });\n\n if (isTouchDevice()) {\n memberTopMenu.addEventListener('focus', (_event) => {\n menuNavButton.setAttribute('aria-expanded', 'true');\n });\n } else {\n memberTopMenu.addEventListener('keyup', (e) => {\n if (e.key === 'Escape' && classList.contains('showing')) {\n closeHeaderMenu(memberTopMenu, menuNavButton);\n menuNavButton.focus();\n }\n });\n }\n\n memberTopMenu\n .querySelector('.crayons-header__menu__dropdown')\n .addEventListener('click', (event) => {\n // There is a click event listener on the body and we do not want\n // this click to be caught by it\n event.stopPropagation();\n\n // Close the menu if the user clicked or touched on mobile a link in the menu.\n closeHeaderMenu(memberTopMenu, menuNavButton);\n menuNavButton.focus();\n });\n\n document.addEventListener('click', (event) => {\n if (event.target.closest('#member-menu-button') === menuNavButton) {\n // The menu navigation button manages it's own click event.\n return;\n }\n\n // Close the menu if the user clicked or touched on mobile a link in the menu.\n closeHeaderMenu(memberTopMenu, menuNavButton);\n });\n\n const secondToLastNavLink = document.getElementById('second-last-nav-link');\n\n document\n .getElementById('last-nav-link')\n .addEventListener('blur', (_event) => {\n // When we tab out of the last link in the member menu, close\n // the menu.\n setTimeout(() => {\n if (document.activeElement === secondToLastNavLink) {\n return;\n }\n\n closeHeaderMenu(memberTopMenu, menuNavButton);\n }, 10);\n });\n}\n\nfunction toggleBurgerMenu() {\n const { leftNavState = 'closed' } = document.body.dataset;\n document.body.dataset.leftNavState =\n leftNavState === 'open' ? 'closed' : 'open';\n}\n\nfunction showMoreMenu({ target }) {\n target.nextElementSibling.classList.remove('hidden');\n target.classList.add('hidden');\n}\n\n/**\n * Gets a reference to InstantClick\n *\n * @param {number} [waitTime=2000] The amount of time to wait\n * until giving up waiting for InstantClick to exist\n *\n * @returns {Promise} The global instance of InstantClick.\n */\nexport async function getInstantClick(waitTime = 2000) {\n return new Promise((resolve, reject) => {\n const failTimer = setTimeout(() => {\n clearInterval(timer);\n reject(new Error('Unable to resolve InstantClick'));\n }, waitTime);\n\n const timer = setInterval(() => {\n if (typeof InstantClick !== 'undefined') {\n clearTimeout(failTimer);\n clearInterval(timer);\n resolve(InstantClick);\n }\n });\n });\n}\n\n/**\n * Initializes the hamburger menu for mobile navigation\n *\n * @param {HTMLElement[]} menuTriggers\n * @param {HTMLElement[]} moreMenus\n */\nexport function initializeMobileMenu(menuTriggers, moreMenus) {\n menuTriggers.forEach((trigger) => {\n trigger.onclick = toggleBurgerMenu;\n });\n\n moreMenus.forEach((trigger) => {\n trigger.onclick = showMoreMenu;\n });\n}\n\n/**\n * Sets the icon link visually for the current page if the current page\n * is one of the main icon links of the top navigation.\n *\n * @param {string} currentPage\n * @param {[string, HTMLElement][]} pageEntries\n */\nexport function setCurrentPageIconLink(currentPage, pageEntries) {\n pageEntries\n // Filter out nulls (means the user is logged out so most icons are not in the logged out view)\n .filter(([, iconLink]) => iconLink)\n .forEach(([page, iconLink]) => {\n if (currentPage === page) {\n iconLink.blur();\n iconLink.setAttribute('aria-current', 'page');\n } else {\n iconLink.removeAttribute('aria-current');\n }\n });\n}\n"],"sourceRoot":""}