Sindbad~EG File Manager
{"version":3,"file":"picker.min.js","sources":["../../src/emoji/picker.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Emoji picker.\n *\n * @module core/emoji/picker\n * @copyright 2019 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport LocalStorage from 'core/localstorage';\nimport * as EmojiData from 'core/emoji/data';\nimport {throttle, debounce} from 'core/utils';\nimport {get_string as getString} from 'core/str';\nimport {render as renderTemplate} from 'core/templates';\n\nconst VISIBLE_ROW_COUNT = 10;\nconst ROW_RENDER_BUFFER_COUNT = 5;\nconst RECENT_EMOJIS_STORAGE_KEY = 'moodle-recent-emojis';\nconst ROW_HEIGHT_RAW = 40;\nconst EMOJIS_PER_ROW = 7;\nconst MAX_RECENT_COUNT = EMOJIS_PER_ROW * 3;\nconst ROW_TYPE = {\n EMOJI: 0,\n HEADER: 1\n};\nconst SELECTORS = {\n CATEGORY_SELECTOR: '[data-action=\"show-category\"]',\n EMOJIS_CONTAINER: '[data-region=\"emojis-container\"]',\n EMOJI_PREVIEW: '[data-region=\"emoji-preview\"]',\n EMOJI_SHORT_NAME: '[data-region=\"emoji-short-name\"]',\n ROW_CONTAINER: '[data-region=\"row-container\"]',\n SEARCH_INPUT: '[data-region=\"search-input\"]',\n SEARCH_RESULTS_CONTAINER: '[data-region=\"search-results-container\"]'\n};\n\n/**\n * Create the row data for a category.\n *\n * @method\n * @param {String} categoryName The category name\n * @param {String} categoryDisplayName The category display name\n * @param {Array} emojis The emoji data\n * @param {Number} totalRowCount The total number of rows generated so far\n * @return {Array}\n */\nconst createRowDataForCategory = (categoryName, categoryDisplayName, emojis, totalRowCount) => {\n const rowData = [];\n rowData.push({\n index: totalRowCount + rowData.length,\n type: ROW_TYPE.HEADER,\n data: {\n name: categoryName,\n displayName: categoryDisplayName\n }\n });\n\n for (let i = 0; i < emojis.length; i += EMOJIS_PER_ROW) {\n const rowEmojis = emojis.slice(i, i + EMOJIS_PER_ROW);\n rowData.push({\n index: totalRowCount + rowData.length,\n type: ROW_TYPE.EMOJI,\n data: rowEmojis\n });\n }\n\n return rowData;\n};\n\n/**\n * Add each row's index to it's value in the row data.\n *\n * @method\n * @param {Array} rowData List of emoji row data\n * @return {Array}\n */\nconst addIndexesToRowData = (rowData) => {\n return rowData.map((data, index) => {\n return {...data, index};\n });\n};\n\n/**\n * Calculate the scroll position for the beginning of each category from\n * the row data.\n *\n * @method\n * @param {Array} rowData List of emoji row data\n * @return {Object}\n */\nconst getCategoryScrollPositionsFromRowData = (rowData) => {\n return rowData.reduce((carry, row, index) => {\n if (row.type === ROW_TYPE.HEADER) {\n carry[row.data.name] = index * ROW_HEIGHT_RAW;\n }\n return carry;\n }, {});\n};\n\n/**\n * Create a header row element for the category name.\n *\n * @method\n * @param {Number} rowIndex Index of the row in the row data\n * @param {String} name The category display name\n * @return {Element}\n */\nconst createHeaderRow = async(rowIndex, name) => {\n const context = {\n index: rowIndex,\n text: name\n };\n const html = await renderTemplate('core/emoji/header_row', context);\n const temp = document.createElement('div');\n temp.innerHTML = html;\n return temp.firstChild;\n};\n\n/**\n * Create an emoji row element.\n *\n * @method\n * @param {Number} rowIndex Index of the row in the row data\n * @param {Array} emojis The list of emoji data for the row\n * @return {Element}\n */\nconst createEmojiRow = async(rowIndex, emojis) => {\n const context = {\n index: rowIndex,\n emojis: emojis.map(emojiData => {\n const charCodes = emojiData.unified.split('-').map(code => `0x${code}`);\n const emojiText = String.fromCodePoint.apply(null, charCodes);\n return {\n shortnames: `:${emojiData.shortnames.join(': :')}:`,\n unified: emojiData.unified,\n text: emojiText,\n spacer: false\n };\n }),\n spacers: Array(EMOJIS_PER_ROW - emojis.length).fill(true)\n };\n const html = await renderTemplate('core/emoji/emoji_row', context);\n const temp = document.createElement('div');\n temp.innerHTML = html;\n return temp.firstChild;\n};\n\n/**\n * Check if the element is an emoji element.\n *\n * @method\n * @param {Element} element Element to check\n * @return {Bool}\n */\nconst isEmojiElement = element => element.getAttribute('data-short-names') !== null;\n\n/**\n * Search from an element and up through it's ancestors to fine the category\n * selector element and return it.\n *\n * @method\n * @param {Element} element Element to begin searching from\n * @return {Element|null}\n */\nconst findCategorySelectorFromElement = element => {\n if (!element) {\n return null;\n }\n\n if (element.getAttribute('data-action') === 'show-category') {\n return element;\n } else {\n return findCategorySelectorFromElement(element.parentElement);\n }\n};\n\nconst getCategorySelectorByCategoryName = (root, name) => {\n return root.querySelector(`[data-category=\"${name}\"]`);\n};\n\n/**\n * Sets the given category selector element as active.\n *\n * @method\n * @param {Element} root The root picker element\n * @param {Element} element The category selector element to make active\n */\nconst setCategorySelectorActive = (root, element) => {\n const allCategorySelectors = root.querySelectorAll(SELECTORS.CATEGORY_SELECTOR);\n\n for (let i = 0; i < allCategorySelectors.length; i++) {\n const selector = allCategorySelectors[i];\n selector.classList.remove('selected');\n }\n\n element.classList.add('selected');\n};\n\n/**\n * Get the category selector element and the scroll positions for the previous and\n * next categories for the given scroll position.\n *\n * @method\n * @param {Element} root The picker root element\n * @param {Number} position The position to get the category for\n * @param {Object} categoryScrollPositions Set of scroll positions for all categories\n * @return {Array}\n */\nconst getCategoryByScrollPosition = (root, position, categoryScrollPositions) => {\n let positions = [];\n\n if (position < 0) {\n position = 0;\n }\n\n // Get all of the category positions.\n for (const categoryName in categoryScrollPositions) {\n const categoryPosition = categoryScrollPositions[categoryName];\n positions.push([categoryPosition, categoryName]);\n }\n\n // Sort the positions in ascending order.\n positions.sort(([a], [b]) => {\n if (a < b) {\n return -1;\n } else if (a > b) {\n return 1;\n } else {\n return 0;\n }\n });\n\n // Get the current category name as well as the previous and next category\n // positions from the sorted list of positions.\n const {categoryName, previousPosition, nextPosition} = positions.reduce(\n (carry, candidate) => {\n const [categoryPosition, categoryName] = candidate;\n\n if (categoryPosition <= position) {\n carry.categoryName = categoryName;\n carry.previousPosition = carry.currentPosition;\n carry.currentPosition = position;\n } else if (carry.nextPosition === null) {\n carry.nextPosition = categoryPosition;\n }\n\n return carry;\n },\n {\n categoryName: null,\n currentPosition: null,\n previousPosition: null,\n nextPosition: null\n }\n );\n\n return [getCategorySelectorByCategoryName(root, categoryName), previousPosition, nextPosition];\n};\n\n/**\n * Get the list of recent emojis data from local storage.\n *\n * @method\n * @return {Array}\n */\nconst getRecentEmojis = () => {\n const storedData = LocalStorage.get(RECENT_EMOJIS_STORAGE_KEY);\n return storedData ? JSON.parse(storedData) : [];\n};\n\n/**\n * Save the list of recent emojis in local storage.\n *\n * @method\n * @param {Array} recentEmojis List of emoji data to save\n */\nconst saveRecentEmoji = (recentEmojis) => {\n LocalStorage.set(RECENT_EMOJIS_STORAGE_KEY, JSON.stringify(recentEmojis));\n};\n\n/**\n * Add an emoji data to the set of recent emojis. This function will update the row\n * data to ensure that the recent emoji rows are correct and all of the rows are\n * re-indexed.\n *\n * The new set of recent emojis are saved in local storage and the full set of updated\n * row data and new emoji row count are returned.\n *\n * @method\n * @param {Array} rowData The emoji rows data\n * @param {Number} recentEmojiRowCount Count of the recent emoji rows\n * @param {Object} newEmoji The emoji data for the emoji to add to the recent emoji list\n * @return {Array}\n */\nconst addRecentEmoji = (rowData, recentEmojiRowCount, newEmoji) => {\n // The first set of rows is always the recent emojis.\n const categoryName = rowData[0].data.name;\n const categoryDisplayName = rowData[0].data.displayName;\n const recentEmojis = getRecentEmojis();\n // Add the new emoji to the start of the list of recent emojis.\n let newRecentEmojis = [newEmoji, ...recentEmojis.filter(emoji => emoji.unified != newEmoji.unified)];\n // Limit the number of recent emojis.\n newRecentEmojis = newRecentEmojis.slice(0, MAX_RECENT_COUNT);\n const newRecentEmojiRowData = createRowDataForCategory(categoryName, categoryDisplayName, newRecentEmojis);\n\n // Save the new list in local storage.\n saveRecentEmoji(newRecentEmojis);\n\n return [\n // Return the new rowData and re-index it to make sure it's all correct.\n addIndexesToRowData(newRecentEmojiRowData.concat(rowData.slice(recentEmojiRowCount))),\n newRecentEmojiRowData.length\n ];\n};\n\n/**\n * Calculate which rows should be visible based on the given scroll position. Adds a\n * buffer to amount to either side of the total number of requested rows so that\n * scrolling the emoji rows container is smooth.\n *\n * @method\n * @param {Number} scrollPosition Scroll position within the emoji container\n * @param {Number} visibleRowCount How many rows should be visible\n * @param {Array} rowData The emoji rows data\n * @return {Array}\n */\nconst getRowsToRender = (scrollPosition, visibleRowCount, rowData) => {\n const minVisibleRow = scrollPosition > ROW_HEIGHT_RAW ? Math.floor(scrollPosition / ROW_HEIGHT_RAW) : 0;\n const start = minVisibleRow >= ROW_RENDER_BUFFER_COUNT ? minVisibleRow - ROW_RENDER_BUFFER_COUNT : minVisibleRow;\n const end = minVisibleRow + visibleRowCount + ROW_RENDER_BUFFER_COUNT;\n const rows = rowData.slice(start, end);\n return rows;\n};\n\n/**\n * Create a row element from the row data.\n *\n * @method\n * @param {Object} rowData The emoji row data\n * @return {Element}\n */\nconst createRowElement = async(rowData) => {\n let row = null;\n if (rowData.type === ROW_TYPE.HEADER) {\n row = await createHeaderRow(rowData.index, rowData.data.displayName);\n } else {\n row = await createEmojiRow(rowData.index, rowData.data);\n }\n\n row.style.position = 'absolute';\n row.style.left = 0;\n row.style.right = 0;\n row.style.top = `${rowData.index * ROW_HEIGHT_RAW}px`;\n\n return row;\n};\n\n/**\n * Check if the given rows match.\n *\n * @method\n * @param {Object} a The first row\n * @param {Object} b The second row\n * @return {Bool}\n */\nconst doRowsMatch = (a, b) => {\n if (a.index !== b.index) {\n return false;\n }\n\n if (a.type !== b.type) {\n return false;\n }\n\n if (typeof a.data != typeof b.data) {\n return false;\n }\n\n if (a.type === ROW_TYPE.HEADER) {\n return a.data.name === b.data.name;\n } else {\n if (a.data.length !== b.data.length) {\n return false;\n }\n\n for (let i = 0; i < a.data.length; i++) {\n if (a.data[i].unified != b.data[i].unified) {\n return false;\n }\n }\n }\n\n return true;\n};\n\n/**\n * Update the visible rows. Deletes any row elements that should no longer\n * be visible and creates the newly visible row elements. Any rows that haven't\n * changed visibility will be left untouched.\n *\n * @method\n * @param {Element} rowContainer The container element for the emoji rows\n * @param {Array} currentRows List of row data that matches the currently visible rows\n * @param {Array} nextRows List of row data containing the new list of rows to be made visible\n */\nconst renderRows = async(rowContainer, currentRows, nextRows) => {\n // We need to add any rows that are in nextRows but not in currentRows.\n const toAdd = nextRows.filter(nextRow => !currentRows.some(currentRow => doRowsMatch(currentRow, nextRow)));\n // Remember which rows will still be visible so that we can insert our element in the correct place in the DOM.\n let toKeep = currentRows.filter(currentRow => nextRows.some(nextRow => doRowsMatch(currentRow, nextRow)));\n // We need to remove any rows that are in currentRows but not in nextRows.\n const toRemove = currentRows.filter(currentRow => !nextRows.some(nextRow => doRowsMatch(currentRow, nextRow)));\n const toRemoveElements = toRemove.map(rowData => rowContainer.querySelectorAll(`[data-row=\"${rowData.index}\"]`));\n\n // Render all of the templates first.\n const rows = await Promise.all(toAdd.map(rowData => createRowElement(rowData)));\n\n rows.forEach((row, index) => {\n const rowData = toAdd[index];\n let nextRowIndex = null;\n\n for (let i = 0; i < toKeep.length; i++) {\n const candidate = toKeep[i];\n if (candidate.index > rowData.index) {\n nextRowIndex = i;\n break;\n }\n }\n\n // Make sure the elements get added to the DOM in the correct order (ascending by row data index)\n // so that they appear naturally in the tab order.\n if (nextRowIndex !== null) {\n const nextRowData = toKeep[nextRowIndex];\n const nextRowNode = rowContainer.querySelector(`[data-row=\"${nextRowData.index}\"]`);\n\n rowContainer.insertBefore(row, nextRowNode);\n toKeep.splice(nextRowIndex, 0, toKeep);\n } else {\n toKeep.push(rowData);\n rowContainer.appendChild(row);\n }\n });\n\n toRemoveElements.forEach(rows => {\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n rowContainer.removeChild(row);\n }\n });\n};\n\n/**\n * Build a function to render the visible emoji rows for a given scroll\n * position.\n *\n * @method\n * @param {Element} rowContainer The container element for the emoji rows\n * @return {Function}\n */\nconst generateRenderRowsAtPositionFunction = (rowContainer) => {\n let currentRows = [];\n let nextRows = [];\n let rowCount = 0;\n let isRendering = false;\n const renderNextRows = async() => {\n if (!nextRows.length) {\n return;\n }\n\n if (isRendering) {\n return;\n }\n\n isRendering = true;\n const nextRowsToRender = nextRows.slice();\n nextRows = [];\n\n await renderRows(rowContainer, currentRows, nextRowsToRender);\n currentRows = nextRowsToRender;\n isRendering = false;\n renderNextRows();\n };\n\n return (scrollPosition, rowData, rowLimit = VISIBLE_ROW_COUNT) => {\n nextRows = getRowsToRender(scrollPosition, rowLimit, rowData);\n renderNextRows();\n\n if (rowCount !== rowData.length) {\n // Adjust the height of the container to match the number of rows.\n rowContainer.style.height = `${rowData.length * ROW_HEIGHT_RAW}px`;\n }\n\n rowCount = rowData.length;\n };\n};\n\n/**\n * Show the search results container and hide the emoji container.\n *\n * @method\n * @param {Element} emojiContainer The emojis container\n * @param {Element} searchResultsContainer The search results container\n */\nconst showSearchResults = (emojiContainer, searchResultsContainer) => {\n searchResultsContainer.classList.remove('hidden');\n emojiContainer.classList.add('hidden');\n};\n\n/**\n * Hide the search result container and show the emojis container.\n *\n * @method\n * @param {Element} emojiContainer The emojis container\n * @param {Element} searchResultsContainer The search results container\n * @param {Element} searchInput The search input\n */\nconst clearSearch = (emojiContainer, searchResultsContainer, searchInput) => {\n searchResultsContainer.classList.add('hidden');\n emojiContainer.classList.remove('hidden');\n searchInput.value = '';\n};\n\n/**\n * Build function to handle mouse hovering an emoji. Shows the preview.\n *\n * @method\n * @param {Element} emojiPreview The emoji preview element\n * @param {Element} emojiShortName The emoji short name element\n * @return {Function}\n */\nconst getHandleMouseEnter = (emojiPreview, emojiShortName) => {\n return (e) => {\n const target = e.target;\n if (isEmojiElement(target)) {\n emojiShortName.textContent = target.getAttribute('data-short-names');\n emojiPreview.textContent = target.textContent;\n }\n };\n};\n\n/**\n * Build function to handle mouse leaving an emoji. Removes the preview.\n *\n * @method\n * @param {Element} emojiPreview The emoji preview element\n * @param {Element} emojiShortName The emoji short name element\n * @return {Function}\n */\nconst getHandleMouseLeave = (emojiPreview, emojiShortName) => {\n return (e) => {\n const target = e.target;\n if (isEmojiElement(target)) {\n emojiShortName.textContent = '';\n emojiPreview.textContent = '';\n }\n };\n};\n\n/**\n * Build the function to handle a user clicking something in the picker.\n *\n * The function currently handles clicking on the category selector or selecting\n * a specific emoji.\n *\n * @method\n * @param {Number} recentEmojiRowCount Number of rows of recent emojis\n * @param {Element} emojiContainer Container element for the visible of emojis\n * @param {Element} searchResultsContainer Contaienr element for the search results\n * @param {Element} searchInput Search input element\n * @param {Function} selectCallback Callback function to execute when a user selects an emoji\n * @param {Function} renderAtPosition Render function to display current visible emojis\n * @return {Function}\n */\nconst getHandleClick = (\n recentEmojiRowCount,\n emojiContainer,\n searchResultsContainer,\n searchInput,\n selectCallback,\n renderAtPosition\n) => {\n return (e, rowData, categoryScrollPositions) => {\n const target = e.target;\n let newRowData = rowData;\n let newCategoryScrollPositions = categoryScrollPositions;\n\n // Hide the search results if they are visible.\n clearSearch(emojiContainer, searchResultsContainer, searchInput);\n\n if (isEmojiElement(target)) {\n // Emoji selected.\n const unified = target.getAttribute('data-unified');\n const shortnames = target.getAttribute('data-short-names').replace(/:/g, '').split(' ');\n // Build the emoji data from the selected element.\n const emojiData = {unified, shortnames};\n const currentScrollTop = emojiContainer.scrollTop;\n const isRecentEmojiRowVisible = emojiContainer.querySelector(`[data-row=\"${recentEmojiRowCount - 1}\"]`) !== null;\n // Save the selected emoji in the recent emojis list.\n [newRowData, recentEmojiRowCount] = addRecentEmoji(rowData, recentEmojiRowCount, emojiData);\n // Re-index the category scroll positions because the additional recent emoji may have\n // changed their positions.\n newCategoryScrollPositions = getCategoryScrollPositionsFromRowData(newRowData);\n\n if (isRecentEmojiRowVisible) {\n // If the list of recent emojis is currently visible then we need to re-render the emojis\n // to update the display and show the newly selected recent emoji.\n renderAtPosition(currentScrollTop, newRowData);\n }\n\n // Call the client's callback function with the selected emoji.\n selectCallback(target.textContent);\n // Return the newly calculated row data and scroll positions.\n return [newRowData, newCategoryScrollPositions];\n }\n\n const categorySelector = findCategorySelectorFromElement(target);\n if (categorySelector) {\n // Category selector.\n const selectedCategory = categorySelector.getAttribute('data-category');\n const position = categoryScrollPositions[selectedCategory];\n // Scroll the container to the selected category. This will trigger the\n // on scroll handler to re-render the visibile emojis.\n emojiContainer.scrollTop = position;\n }\n\n return [newRowData, newCategoryScrollPositions];\n };\n};\n\n/**\n * Build the function that handles scrolling of the emoji container to display the\n * correct emojis.\n *\n * We render the emoji rows as they are needed rather than all up front so that we\n * can avoid adding tends of thousands of elements to the DOM unnecessarily which\n * would bog down performance.\n *\n * @method\n * @param {Element} root The picker root element\n * @param {Number} currentVisibleRowScrollPosition The current scroll position of the container\n * @param {Element} emojiContainer The emojis container element\n * @param {Object} initialCategoryScrollPositions Scroll positions for each category\n * @param {Function} renderAtPosition Function to render the appropriate emojis for a scroll position\n * @return {Function}\n */\nconst getHandleScroll = (\n root,\n currentVisibleRowScrollPosition,\n emojiContainer,\n initialCategoryScrollPositions,\n renderAtPosition\n) => {\n // Scope some local variables to track the scroll positions of the categories. We need to\n // recalculate these because adding recent emojis can change those positions by adding\n // additional rows.\n let [\n currentCategoryElement,\n previousCategoryPosition,\n nextCategoryPosition\n ] = getCategoryByScrollPosition(root, emojiContainer.scrollTop, initialCategoryScrollPositions);\n\n return (categoryScrollPositions, rowData) => {\n const newScrollPosition = emojiContainer.scrollTop;\n const upperScrollBound = currentVisibleRowScrollPosition + ROW_HEIGHT_RAW;\n const lowerScrollBound = currentVisibleRowScrollPosition - ROW_HEIGHT_RAW;\n // We only need to update the active category indicator if the user has scrolled into a\n // new category scroll position.\n const updateActiveCategory = (newScrollPosition >= nextCategoryPosition) ||\n (newScrollPosition < previousCategoryPosition);\n // We only need to render new emoji rows if the user has scrolled far enough that a new row\n // would be visible (i.e. they've scrolled up or down more than 40px - the height of a row).\n const updateRenderRows = (newScrollPosition < lowerScrollBound) || (newScrollPosition > upperScrollBound);\n\n if (updateActiveCategory) {\n // New category is visible so update the active category selector and re-index the\n // positions incase anything has changed.\n [\n currentCategoryElement,\n previousCategoryPosition,\n nextCategoryPosition\n ] = getCategoryByScrollPosition(root, newScrollPosition, categoryScrollPositions);\n setCategorySelectorActive(root, currentCategoryElement);\n }\n\n if (updateRenderRows) {\n // A new row should be visible so re-render the visible emojis at this new position.\n // We request an animation frame from the browser so that we're not blocking anything.\n // The animation only needs to occur as soon as the browser is ready not immediately.\n requestAnimationFrame(() => {\n renderAtPosition(newScrollPosition, rowData);\n // Remember the updated position.\n currentVisibleRowScrollPosition = newScrollPosition;\n });\n }\n };\n};\n\n/**\n * Build the function that handles search input from the user.\n *\n * @method\n * @param {Element} searchInput The search input element\n * @param {Element} searchResultsContainer Container element to display the search results\n * @param {Element} emojiContainer Container element for the emoji rows\n * @return {Function}\n */\nconst getHandleSearch = (searchInput, searchResultsContainer, emojiContainer) => {\n const rowContainer = searchResultsContainer.querySelector(SELECTORS.ROW_CONTAINER);\n // Build a render function for the search results.\n const renderSearchResultsAtPosition = generateRenderRowsAtPositionFunction(rowContainer);\n searchResultsContainer.appendChild(rowContainer);\n\n return async() => {\n const searchTerm = searchInput.value.toLowerCase();\n\n if (searchTerm) {\n // Display the search results container and hide the emojis container.\n showSearchResults(emojiContainer, searchResultsContainer);\n\n // Find which emojis match the user's search input.\n const matchingEmojis = Object.keys(EmojiData.byShortName).reduce((carry, shortName) => {\n if (shortName.includes(searchTerm)) {\n carry.push({\n shortnames: [shortName],\n unified: EmojiData.byShortName[shortName]\n });\n }\n return carry;\n }, []);\n\n const searchResultsString = await getString('searchresults', 'core');\n const rowData = createRowDataForCategory(searchResultsString, searchResultsString, matchingEmojis, 0);\n // Show the emoji rows for the search results.\n renderSearchResultsAtPosition(0, rowData, rowData.length);\n } else {\n // Hide the search container and show the emojis container.\n clearSearch(emojiContainer, searchResultsContainer, searchInput);\n }\n };\n};\n\n/**\n * Register the emoji picker event listeners.\n *\n * @method\n * @param {Element} root The picker root element\n * @param {Element} emojiContainer Root element containing the list of visible emojis\n * @param {Function} renderAtPosition Function to render the visible emojis at a given scroll position\n * @param {Number} currentVisibleRowScrollPosition What is the current scroll position\n * @param {Function} selectCallback Function to execute when the user picks an emoji\n * @param {Object} categoryScrollPositions Scroll positions for where each of the emoji categories begin\n * @param {Array} rowData Data representing each of the display rows for hte emoji container\n * @param {Number} recentEmojiRowCount Number of rows of recent emojis\n */\nconst registerEventListeners = (\n root,\n emojiContainer,\n renderAtPosition,\n currentVisibleRowScrollPosition,\n selectCallback,\n categoryScrollPositions,\n rowData,\n recentEmojiRowCount\n) => {\n const searchInput = root.querySelector(SELECTORS.SEARCH_INPUT);\n const searchResultsContainer = root.querySelector(SELECTORS.SEARCH_RESULTS_CONTAINER);\n const emojiPreview = root.querySelector(SELECTORS.EMOJI_PREVIEW);\n const emojiShortName = root.querySelector(SELECTORS.EMOJI_SHORT_NAME);\n // Build the click handler function.\n const clickHandler = getHandleClick(\n recentEmojiRowCount,\n emojiContainer,\n searchResultsContainer,\n searchInput,\n selectCallback,\n renderAtPosition\n );\n // Build the scroll handler function.\n const scrollHandler = getHandleScroll(\n root,\n currentVisibleRowScrollPosition,\n emojiContainer,\n categoryScrollPositions,\n renderAtPosition\n );\n const searchHandler = getHandleSearch(searchInput, searchResultsContainer, emojiContainer);\n\n // Mouse enter/leave events to show the emoji preview on hover or focus.\n root.addEventListener('focus', getHandleMouseEnter(emojiPreview, emojiShortName), true);\n root.addEventListener('blur', getHandleMouseLeave(emojiPreview, emojiShortName), true);\n root.addEventListener('mouseenter', getHandleMouseEnter(emojiPreview, emojiShortName), true);\n root.addEventListener('mouseleave', getHandleMouseLeave(emojiPreview, emojiShortName), true);\n // User selects an emoji or clicks on one of the emoji category selectors.\n root.addEventListener('click', e => {\n // Update the row data and category scroll positions because they may have changes if the\n // user selects an emoji which updates the recent emojis list.\n [rowData, categoryScrollPositions] = clickHandler(e, rowData, categoryScrollPositions);\n });\n // Throttle the scroll event to only execute once every 50 milliseconds to prevent performance issues\n // in the browser when re-rendering the picker emojis. The scroll event fires a lot otherwise.\n emojiContainer.addEventListener('scroll', throttle(() => scrollHandler(categoryScrollPositions, rowData), 50));\n // Debounce the search input so that it only executes 200 milliseconds after the user has finished typing.\n searchInput.addEventListener('input', debounce(searchHandler, 200));\n};\n\n/**\n * Initialise the emoji picker.\n *\n * @method\n * @param {Element} root The root element for the picker\n * @param {Function} selectCallback Callback for when the user selects an emoji\n */\nexport default (root, selectCallback) => {\n const emojiContainer = root.querySelector(SELECTORS.EMOJIS_CONTAINER);\n const rowContainer = emojiContainer.querySelector(SELECTORS.ROW_CONTAINER);\n const recentEmojis = getRecentEmojis();\n // Add the recent emojis category to the list of standard categories.\n const allData = [{\n name: 'Recent',\n emojis: recentEmojis\n }, ...EmojiData.byCategory];\n let rowData = [];\n let recentEmojiRowCount = 0;\n\n /**\n * Split categories data into rows which represent how they will be displayed in the\n * picker. Each category will add a row containing the display name for the category\n * and a row for every 9 emojis in the category. The row data will be used to calculate\n * which emojis should be visible in the picker at any given time.\n *\n * E.g.\n * input = [\n * {name: 'example1', emojis: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]},\n * {name: 'example2', emojis: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]},\n * ]\n * output = [\n * {type: 'categoryName': data: 'Example 1'},\n * {type: 'emojiRow': data: [1, 2, 3, 4, 5, 6, 7, 8, 9]},\n * {type: 'emojiRow': data: [10, 11, 12]},\n * {type: 'categoryName': data: 'Example 2'},\n * {type: 'emojiRow': data: [13, 14, 15, 16, 17, 18, 19, 20, 21]},\n * {type: 'emojiRow': data: [22, 23]},\n * ]\n */\n allData.forEach(category => {\n const categorySelector = getCategorySelectorByCategoryName(root, category.name);\n // Get the display name from the category selector button so that we don't need to\n // send an ajax request for the string.\n const categoryDisplayName = categorySelector.title;\n const categoryRowData = createRowDataForCategory(category.name, categoryDisplayName, category.emojis, rowData.length);\n\n if (category.name === 'Recent') {\n // Remember how many recent emoji rows there are because it needs to be used to\n // re-index the row data later when we're adding more recent emojis.\n recentEmojiRowCount = categoryRowData.length;\n }\n\n rowData = rowData.concat(categoryRowData);\n });\n\n // Index the row data so that we can calculate which rows should be visible.\n rowData = addIndexesToRowData(rowData);\n // Calculate the scroll positions for each of the categories within the emoji container.\n // These are used to know where to jump to when the user selects a specific category.\n const categoryScrollPositions = getCategoryScrollPositionsFromRowData(rowData);\n const renderAtPosition = generateRenderRowsAtPositionFunction(rowContainer);\n // Display the initial set of emojis.\n renderAtPosition(0, rowData);\n\n registerEventListeners(\n root,\n emojiContainer,\n renderAtPosition,\n 0,\n selectCallback,\n categoryScrollPositions,\n rowData,\n recentEmojiRowCount\n );\n};\n"],"names":["ROW_TYPE","SELECTORS","createRowDataForCategory","categoryName","categoryDisplayName","emojis","totalRowCount","rowData","push","index","length","type","data","name","displayName","i","rowEmojis","slice","addIndexesToRowData","map","getCategoryScrollPositionsFromRowData","reduce","carry","row","createHeaderRow","rowIndex","context","text","html","temp","document","createElement","innerHTML","firstChild","createEmojiRow","emojiData","charCodes","unified","split","code","emojiText","String","fromCodePoint","apply","shortnames","join","spacer","spacers","Array","fill","isEmojiElement","element","getAttribute","findCategorySelectorFromElement","parentElement","getCategorySelectorByCategoryName","root","querySelector","getCategoryByScrollPosition","position","categoryScrollPositions","positions","categoryPosition","sort","a","b","candidate","previousPosition","currentPosition","nextPosition","getRecentEmojis","storedData","LocalStorage","get","JSON","parse","addRecentEmoji","recentEmojiRowCount","newEmoji","recentEmojis","newRecentEmojis","filter","emoji","EMOJIS_PER_ROW","newRecentEmojiRowData","set","stringify","saveRecentEmoji","concat","getRowsToRender","scrollPosition","visibleRowCount","minVisibleRow","Math","floor","start","end","createRowElement","style","left","right","top","doRowsMatch","_typeof","renderRows","rowContainer","currentRows","nextRows","toAdd","nextRow","some","currentRow","toKeep","toRemove","toRemoveElements","querySelectorAll","Promise","all","forEach","nextRowIndex","nextRowData","nextRowNode","insertBefore","splice","appendChild","rows","removeChild","generateRenderRowsAtPositionFunction","rowCount","isRendering","renderNextRows","nextRowsToRender","rowLimit","height","showSearchResults","emojiContainer","searchResultsContainer","classList","remove","add","clearSearch","searchInput","value","getHandleMouseEnter","emojiPreview","emojiShortName","e","target","textContent","getHandleMouseLeave","getHandleScroll","currentVisibleRowScrollPosition","initialCategoryScrollPositions","renderAtPosition","scrollTop","currentCategoryElement","previousCategoryPosition","nextCategoryPosition","newScrollPosition","updateRenderRows","allCategorySelectors","setCategorySelectorActive","requestAnimationFrame","registerEventListeners","selectCallback","clickHandler","newRowData","newCategoryScrollPositions","replace","currentScrollTop","isRecentEmojiRowVisible","categorySelector","getHandleClick","scrollHandler","searchHandler","renderSearchResultsAtPosition","searchTerm","toLowerCase","matchingEmojis","Object","keys","EmojiData","byShortName","shortName","includes","searchResultsString","getHandleSearch","addEventListener","allData","byCategory","category","title","categoryRowData"],"mappings":"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"}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists