Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"auto_complete.min.js","sources":["../../src/emoji/auto_complete.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 auto complete.\n *\n * @module core/emoji/auto_complete\n * @copyright  2019 Ryan Wyllie <ryan@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as EmojiData from 'core/emoji/data';\nimport {render as renderTemplate} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport LocalStorage from 'core/localstorage';\nimport KeyCodes from 'core/key_codes';\n\nconst INPUT_DEBOUNCE_TIMER = 200;\nconst SUGGESTION_LIMIT = 50;\nconst MAX_RECENT_COUNT = 27;\nconst RECENT_EMOJIS_STORAGE_KEY = 'moodle-recent-emojis';\n\nconst SELECTORS = {\n    EMOJI_BUTTON: '[data-region=\"emoji-button\"]',\n    ACTIVE_EMOJI_BUTTON: '[data-region=\"emoji-button\"].active',\n};\n\n/**\n * Get the list of recent emojis data from local storage.\n *\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 * Add an emoji data to the set of recent emojis. The new set of recent emojis are\n * saved in local storage.\n *\n * @param {String} unified The char chodes for the emoji\n * @param {String} shortName The emoji short name\n */\nconst addRecentEmoji = (unified, shortName) => {\n    const newEmoji = {\n        unified,\n        shortnames: [shortName]\n    };\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\n    LocalStorage.set(RECENT_EMOJIS_STORAGE_KEY, JSON.stringify(newRecentEmojis));\n};\n\n/**\n * Get the actual emoji string from the short name.\n *\n * @param {String} shortName Emoji short name\n * @return {String|null}\n */\nconst getEmojiTextFromShortName = (shortName) => {\n    const unified = EmojiData.byShortName[shortName];\n\n    if (unified) {\n        const charCodes = unified.split('-').map(code => `0x${code}`);\n        return String.fromCodePoint.apply(null, charCodes);\n    } else {\n        return null;\n    }\n};\n\n/**\n * Render the auto complete list for the given short names.\n *\n * @param {Element} root The root container for the emoji auto complete\n * @param {Array} shortNames The list of short names for emoji suggestions to show\n */\nconst render = async(root, shortNames) => {\n    const renderContext = {\n        emojis: shortNames.map((shortName, index) => {\n            return {\n                active: index === 0,\n                emojitext: getEmojiTextFromShortName(shortName),\n                displayshortname: `:${shortName}:`,\n                shortname: shortName,\n                unified: EmojiData.byShortName[shortName]\n            };\n        })\n    };\n    const html = await renderTemplate('core/emoji/auto_complete', renderContext);\n    root.innerHTML = html;\n};\n\n/**\n * Get the list of emoji short names that include the given search term. If\n * the search term is an empty string then the list of recently used emojis\n * will be returned.\n *\n * @param {String} searchTerm Text to match on\n * @param {Number} limit Maximum number of results to return\n * @return {Array}\n */\nconst searchEmojis = (searchTerm, limit) => {\n    if (searchTerm === '') {\n        return getRecentEmojis().map(data => data.shortnames[0]).slice(0, limit);\n    } else {\n        searchTerm = searchTerm.toLowerCase();\n        return Object.keys(EmojiData.byShortName)\n                .filter(shortName => shortName.includes(searchTerm))\n                .slice(0, limit);\n    }\n};\n\n/**\n * Get the current word at the given position (index) within the text.\n *\n * @param {String} text The text to process\n * @param {Number} position The position (index) within the text to match the word\n * @return {String}\n */\nconst getWordFromPosition = (text, position) => {\n    const startMatches = text.slice(0, position).match(/(\\S*)$/);\n    const endMatches = text.slice(position).match(/^(\\S*)/);\n    let startText = '';\n    let endText = '';\n\n    if (startMatches) {\n        startText = startMatches[startMatches.length - 1];\n    }\n\n    if (endMatches) {\n        endText = endMatches[endMatches.length - 1];\n    }\n\n    return `${startText}${endText}`;\n};\n\n/**\n * Check if the given text is a full short name, i.e. has leading and trialing colon\n * characters.\n *\n * @param {String} text The text to process\n * @return {Bool}\n */\nconst isCompleteShortName = text => /^:[^:\\s]+:$/.test(text);\n\n/**\n * Check if the given text is a partial short name, i.e. has a leading colon but no\n * trailing colon.\n *\n * @param {String} text The text to process\n * @return {Bool}\n */\nconst isPartialShortName = text => /^:[^:\\s]*$/.test(text);\n\n/**\n * Remove the colon characters from the given text.\n *\n * @param {String} text The text to process\n * @return {String}\n */\nconst getShortNameFromText = text => text.replace(/:/g, '');\n\n/**\n * Get the currently active emoji button element in the list of suggestions.\n *\n * @param {Element} root The emoji auto complete container element\n * @return {Element|null}\n */\nconst getActiveEmojiSuggestion = (root) => {\n    return root.querySelector(SELECTORS.ACTIVE_EMOJI_BUTTON);\n};\n\n/**\n * Make the previous sibling of the current active emoji active.\n *\n * @param {Element} root The emoji auto complete container element\n */\nconst selectPreviousEmojiSuggestion = (root) => {\n    const activeEmojiSuggestion = getActiveEmojiSuggestion(root);\n    const previousSuggestion = activeEmojiSuggestion.previousElementSibling;\n\n    if (previousSuggestion) {\n        activeEmojiSuggestion.classList.remove('active');\n        previousSuggestion.classList.add('active');\n        previousSuggestion.scrollIntoView({behaviour: 'smooth', inline: 'center'});\n    }\n};\n\n/**\n * Make the next sibling to the current active emoji active.\n *\n * @param {Element} root The emoji auto complete container element\n */\nconst selectNextEmojiSuggestion = (root) => {\n    const activeEmojiSuggestion = getActiveEmojiSuggestion(root);\n    const nextSuggestion = activeEmojiSuggestion.nextElementSibling;\n\n    if (nextSuggestion) {\n        activeEmojiSuggestion.classList.remove('active');\n        nextSuggestion.classList.add('active');\n        nextSuggestion.scrollIntoView({behaviour: 'smooth', inline: 'center'});\n    }\n};\n\n/**\n * Trigger the select callback for the given emoji button element.\n *\n * @param {Element} element The emoji button element\n * @param {Function} selectCallback The callback for when the user selects an emoji\n */\nconst selectEmojiElement = (element, selectCallback) => {\n    const shortName = element.getAttribute('data-short-name');\n    const unified = element.getAttribute('data-unified');\n    addRecentEmoji(unified, shortName);\n    selectCallback(element.innerHTML.trim());\n};\n\n/**\n * Initialise the emoji auto complete.\n *\n * @method\n * @param {Element} root The root container element for the auto complete\n * @param {Element} textArea The text area element to monitor for auto complete\n * @param {Function} hasSuggestionCallback Callback for when there are auto-complete suggestions\n * @param {Function} selectCallback Callback for when the user selects an emoji\n */\nexport default (root, textArea, hasSuggestionCallback, selectCallback) => {\n    let hasSuggestions = false;\n    let previousSearchText = '';\n\n    // Debounce the listener so that each keypress delays the execution of the handler. The\n    // handler should only run 200 milliseconds after the last keypress.\n    textArea.addEventListener('keyup', debounce(() => {\n        // This is a \"keyup\" listener so that it only executes after the text area value\n        // has been updated.\n        const text = textArea.value;\n        const cursorPos = textArea.selectionStart;\n        const searchText = getWordFromPosition(text, cursorPos);\n\n        if (searchText === previousSearchText) {\n            // Nothing has changed so no need to take any action.\n            return;\n        } else {\n            previousSearchText = searchText;\n        }\n\n        if (isCompleteShortName(searchText)) {\n            // If the user has entered a full short name (with leading and trialing colons)\n            // then see if we can find a match for it and auto complete it.\n            const shortName = getShortNameFromText(searchText);\n            const emojiText = getEmojiTextFromShortName(shortName);\n            hasSuggestions = false;\n            if (emojiText) {\n                addRecentEmoji(EmojiData.byShortName[shortName], shortName);\n                selectCallback(emojiText);\n            }\n        } else if (isPartialShortName(searchText)) {\n            // If the user has entered a partial short name (leading colon but no trailing) then\n            // search on the text to see if we can find some suggestions for them.\n            const suggestions = searchEmojis(getShortNameFromText(searchText), SUGGESTION_LIMIT);\n\n            if (suggestions.length) {\n                render(root, suggestions);\n                hasSuggestions = true;\n            } else {\n                hasSuggestions = false;\n            }\n        } else {\n            hasSuggestions = false;\n        }\n\n        hasSuggestionCallback(hasSuggestions);\n    }, INPUT_DEBOUNCE_TIMER));\n\n    textArea.addEventListener('keydown', (e) => {\n        if (hasSuggestions) {\n            const isModifierPressed = (e.shiftKey || e.metaKey || e.altKey || e.ctrlKey);\n            if (!isModifierPressed) {\n                switch (e.which) {\n                    case KeyCodes.escape:\n                        // Escape key closes the auto complete.\n                        hasSuggestions = false;\n                        hasSuggestionCallback(false);\n                        break;\n                    case KeyCodes.arrowLeft:\n                        // Arrow keys navigate through the list of suggetions.\n                        selectPreviousEmojiSuggestion(root);\n                        e.preventDefault();\n                        break;\n                    case KeyCodes.arrowRight:\n                        // Arrow keys navigate through the list of suggetions.\n                        selectNextEmojiSuggestion(root);\n                        e.preventDefault();\n                        break;\n                    case KeyCodes.enter:\n                        // Enter key selects the current suggestion.\n                        selectEmojiElement(getActiveEmojiSuggestion(root), selectCallback);\n                        e.preventDefault();\n                        e.stopPropagation();\n                        break;\n                }\n            }\n        }\n    });\n\n    root.addEventListener('click', (e) => {\n        const target = e.target;\n        if (target.matches(SELECTORS.EMOJI_BUTTON)) {\n            selectEmojiElement(target, selectCallback);\n        }\n    });\n};\n"],"names":["SELECTORS","getRecentEmojis","storedData","LocalStorage","get","JSON","parse","addRecentEmoji","unified","shortName","newEmoji","shortnames","recentEmojis","newRecentEmojis","filter","emoji","slice","set","stringify","getEmojiTextFromShortName","EmojiData","byShortName","charCodes","split","map","code","String","fromCodePoint","apply","getShortNameFromText","text","replace","getActiveEmojiSuggestion","root","querySelector","selectEmojiElement","element","selectCallback","getAttribute","innerHTML","trim","textArea","hasSuggestionCallback","hasSuggestions","previousSearchText","addEventListener","searchText","position","startMatches","match","endMatches","startText","endText","length","getWordFromPosition","value","selectionStart","test","isCompleteShortName","emojiText","isPartialShortName","suggestions","searchTerm","limit","data","toLowerCase","Object","keys","includes","async","shortNames","renderContext","emojis","index","active","emojitext","displayshortname","shortname","html","render","e","shiftKey","metaKey","altKey","ctrlKey","which","KeyCodes","escape","arrowLeft","activeEmojiSuggestion","previousSuggestion","previousElementSibling","classList","remove","add","scrollIntoView","behaviour","inline","selectPreviousEmojiSuggestion","preventDefault","arrowRight","nextSuggestion","nextElementSibling","selectNextEmojiSuggestion","enter","stopPropagation","target","matches"],"mappings":"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"}