├── .gitignore ├── images └── icon48.png ├── README.md ├── background.html ├── scripts ├── require-cs.js ├── google-analytics.js ├── content-script.js ├── background.js ├── service.js ├── storage.js ├── view.js ├── text.js └── require.js ├── manifest.json ├── templates └── dict-row.tpl └── dev └── build.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | scripts/background-build-*.js 3 | -------------------------------------------------------------------------------- /images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DmitryVarennikov/lingualeo-russian-search-chrome-ext/master/images/icon48.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lingualeo Russian Search Google Chrome extension 2 | ============= 3 | 4 | Позволяет искать английские слова по их переводу 5 | -------------------------------------------------------------------------------- /background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /scripts/require-cs.js: -------------------------------------------------------------------------------- 1 | require.load = function (context, moduleName, url) { 2 | var xhr = new XMLHttpRequest(), 3 | evalResponseText = function (xhr) { 4 | eval(xhr.responseText); 5 | context.completeLoad(moduleName); 6 | }; 7 | 8 | xhr.open("GET", url, true); 9 | xhr.onreadystatechange = function (e) { 10 | if (xhr.readyState === 4 && xhr.status === 200) { 11 | // we have to specifically pass the window context or underscore 12 | // will fail since it defines "root = this" 13 | evalResponseText.call(window, xhr); 14 | } 15 | }; 16 | xhr.send(null); 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/google-analytics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function GoogleAnalytics() { 4 | if (! (this instanceof GoogleAnalytics)) { 5 | throw new Error('`this` must be an instance of `GoogleAnalytics`'); 6 | } 7 | 8 | _gaq.push(['_setAccount', 'UA-61000540-1']); 9 | 10 | this.trackPageview = function () { 11 | _gaq.push(['_trackPageview']); 12 | }; 13 | 14 | this.trackEvent = function (name, value) { 15 | _gaq.push(['_trackEvent', name, value]); 16 | }; 17 | } 18 | 19 | 20 | define(['google-analytics'], function (_) { 21 | 22 | var ga; 23 | 24 | return { 25 | getInstance: function () { 26 | if (! (ga instanceof GoogleAnalytics)) { 27 | ga = new GoogleAnalytics(); 28 | } 29 | 30 | return ga; 31 | } 32 | }; 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /scripts/content-script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require( 4 | { 5 | baseUrl: chrome.extension.getURL("/"), 6 | paths: { 7 | text: "scripts/text" 8 | } 9 | }, 10 | [ 11 | 'scripts/storage', 12 | 'scripts/service', 13 | 'scripts/view' 14 | ], 15 | function (Storage, Service, View) { 16 | var service = new Service(), 17 | storage = new Storage(service), 18 | view = new View(storage, service); 19 | 20 | // as soon as we hit LinguaLeo dictionary/glossary update the latest words and listen to the search box 21 | storage.updateWords(); 22 | view.listenToSearchBox(); 23 | 24 | 25 | // because content script runs in the context of a web page we can not use Google Analytics here directly 26 | // let's communicate with bg page via messaging queue 27 | chrome.runtime.sendMessage({ga: "trackPageview"}, function (response) { 28 | // no feed back, though we don't need it 29 | }); 30 | } 31 | ); 32 | 33 | 34 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LinguaLeo Russian Search", 3 | "version": "0.2.1", 4 | "manifest_version": 2, 5 | "description": "Позволяет искать английские слова по их переводу в СВОЁМ словаре", 6 | "icons": { 7 | "48": "images/icon48.png" 8 | }, 9 | "background": { 10 | "page": "background.html" 11 | }, 12 | "permissions": [ 13 | "storage", 14 | "unlimitedStorage", 15 | "http://lingualeo.com/*", 16 | "https://lingualeo.com/*", 17 | "http://api.lingualeo.com/*", 18 | "https://api.lingualeo.com/*" 19 | ], 20 | "content_scripts": [ 21 | { 22 | "matches": [ 23 | "http://lingualeo.com/ru/userdict", 24 | "https://lingualeo.com/ru/userdict", 25 | "http://lingualeo.com/ru/userdict/wordSets/*", 26 | "https://lingualeo.com/ru/userdict/wordSets/*", 27 | "http://lingualeo.com/ru/glossary/learn/*", 28 | "https://lingualeo.com/ru/glossary/learn/*" 29 | ], 30 | "js": [ 31 | "scripts/require.js", 32 | "scripts/require-cs.js", 33 | "scripts/content-script.js" 34 | ] 35 | } 36 | ], 37 | "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'", 38 | "web_accessible_resources": [ 39 | "manifest.json", 40 | "scripts/storage.js", 41 | "scripts/service.js", 42 | "scripts/view.js", 43 | "scripts/text.js", 44 | "templates/dict-row.tpl", 45 | "scripts/google-analytics.js" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /templates/dict-row.tpl: -------------------------------------------------------------------------------- 1 | 3 | 4 |
6 | 7 |
8 |
9 | 11 |
12 |
13 | 15 |
16 |
18 |
19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 |  —  32 | 33 |
34 | -------------------------------------------------------------------------------- /scripts/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require( 4 | { 5 | baseUrl: chrome.extension.getURL("/"), 6 | paths: { 7 | "google-analytics": [ 8 | 'https://ssl.google-analytics.com/ga' 9 | ] 10 | } 11 | }, 12 | [ 13 | 'scripts/google-analytics', 14 | 'scripts/service', 15 | 'scripts/storage' 16 | ], 17 | function (GoogleAnalytics, Service, Storage) { 18 | var ga = GoogleAnalytics.getInstance(), 19 | service = new Service(), 20 | storage = new Storage(service); 21 | 22 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 23 | if ("trackPageview" === request.ga) { 24 | ga.trackPageview(); 25 | } 26 | }); 27 | 28 | function sync() { 29 | service.isAuthenticated(function (error, is) { 30 | if (error) { 31 | console.error(error); 32 | ga.trackEvent('sync', 'user authentication check error'); 33 | } else if (is) { 34 | storage.sync(); 35 | storage.setLastSyncDate(Date.now()); 36 | 37 | ga.trackEvent('sync', 'user is authenticated'); 38 | } else { 39 | ga.trackEvent('sync', 'user is not authenticated'); 40 | } 41 | }); 42 | } 43 | 44 | document.getElementsByName('sync')[0].addEventListener('click', function () { 45 | ga.trackEvent('sync', 'button-clicked'); 46 | 47 | sync(); 48 | }); 49 | 50 | 51 | // run synchronization every 24 hours, check every hour 52 | setInterval(function () { 53 | ga.trackEvent('sync', 'check'); 54 | 55 | storage.getLastSyncDate(function (lastSyncDate) { 56 | var date; 57 | if (lastSyncDate) { 58 | date = new Date(lastSyncDate); 59 | date.setHours(date.getHours() + 24); 60 | if (date < new Date()) { 61 | sync(); 62 | } 63 | } else { 64 | sync(); 65 | } 66 | }); 67 | }, 1000 * 60 * 60); 68 | 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /dev/build.php: -------------------------------------------------------------------------------- 1 | ', $argv[0]), PHP_EOL; 11 | exit; 12 | } 13 | 14 | $outputDir = rtrim($argv[1], '/'); 15 | 16 | createBuild(); 17 | copyFiles($outputDir); 18 | alterManifest($outputDir); 19 | alterContent($outputDir); 20 | removeBuild(); 21 | 22 | 23 | function isRoot() 24 | { 25 | return file_exists(getcwd() . '/manifest.json'); 26 | } 27 | 28 | function execute($cmd) 29 | { 30 | $handle = popen($cmd, 'r'); 31 | if (false !== $handle) { 32 | while (false !== ($buffer = fgets($handle))) { 33 | echo $buffer; 34 | } 35 | $statusCode = pclose($handle); 36 | if (0 !== $statusCode) { 37 | echo 'Error while opening process file pointer, status code: ', $statusCode, PHP_EOL; 38 | exit(1); 39 | } 40 | } else { 41 | echo 'Failed to execute cmd: "', $cmd, '"', PHP_EOL; 42 | exit(1); 43 | } 44 | } 45 | 46 | function createBuild() 47 | { 48 | $cmd = sprintf( 49 | 'r.js -o baseUrl=. name=scripts/content-script out=scripts/content-script-build-%s.js paths.text=scripts/text paths.google-analytics=empty:', 50 | date('Y-m-d') 51 | ); 52 | execute($cmd); 53 | 54 | $cmd = sprintf( 55 | 'r.js -o baseUrl=. name=scripts/background out=scripts/background-build-%s.js paths.google-analytics=empty:', 56 | date('Y-m-d') 57 | ); 58 | execute($cmd); 59 | } 60 | 61 | function copyFiles($outputDir) 62 | { 63 | $cmd = sprintf('rm -rf %s/*', $outputDir); 64 | execute($cmd); 65 | 66 | // images 67 | $cmd = 'cp -r images ' . $outputDir; 68 | execute($cmd); 69 | 70 | // scripts 71 | $cmd = sprintf('mkdir %s/scripts', $outputDir); 72 | execute($cmd); 73 | 74 | $cmd = sprintf('cp scripts/require.js %s/scripts/', $outputDir); 75 | execute($cmd); 76 | 77 | $cmd = sprintf('cp scripts/require-cs.js %s/scripts/', $outputDir); 78 | execute($cmd); 79 | 80 | $cmd = sprintf('cp scripts/content-script-build-%s.js %s/scripts/', date('Y-m-d'), $outputDir); 81 | execute($cmd); 82 | 83 | $cmd = sprintf('cp scripts/background-build-%s.js %s/scripts/', date('Y-m-d'), $outputDir); 84 | execute($cmd); 85 | 86 | $cmd = 'cp background.html ' . $outputDir; 87 | execute($cmd); 88 | 89 | $cmd = 'cp manifest.json ' . $outputDir; 90 | execute($cmd); 91 | } 92 | 93 | function alterManifest($outputDir) 94 | { 95 | $manifestFileName = $outputDir . '/manifest.json'; 96 | $content = json_decode(file_get_contents($manifestFileName), true); 97 | 98 | // replace main entry script with a built one 99 | $contentScriptBuildName = sprintf('scripts/content-script-build-%s.js', date('Y-m-d')); 100 | foreach ($content['content_scripts'][0]['js'] as $index => $scriptName) { 101 | if ('scripts/content-script.js' === $scriptName) { 102 | $content['content_scripts'][0]['js'][$index] = $contentScriptBuildName; 103 | break; 104 | } 105 | } 106 | 107 | // thin out web accessible resources 108 | foreach ($content['web_accessible_resources'] as $index => $resource) { 109 | if ('manifest.json' !== $resource) { 110 | unset($content['web_accessible_resources'][$index]); 111 | } 112 | } 113 | 114 | $bytes = file_put_contents( 115 | $manifestFileName, 116 | json_encode($content, ~JSON_HEX_APOS & ~JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) 117 | ); 118 | if (!$bytes) { 119 | echo 'Error: "' . $manifestFileName . '" was not saved!', PHP_EOL; 120 | } 121 | } 122 | 123 | function alterContent($outputDir) 124 | { 125 | $change = function ($filename, $searchContent, $replaceContent) { 126 | $content = file_get_contents($filename); 127 | $content = str_replace($searchContent, $replaceContent, $content); 128 | file_put_contents($filename, $content); 129 | }; 130 | 131 | $change( 132 | $outputDir . '/background.html', 133 | 'data-main="scripts/background"', 134 | sprintf('data-main="scripts/background-build-%s"', date('Y-m-d')) 135 | ); 136 | } 137 | 138 | function removeBuild() 139 | { 140 | $cmd = sprintf('rm scripts/content-script-build-%s.js', date('Y-m-d')); 141 | execute($cmd); 142 | 143 | $cmd = sprintf('rm scripts/background-build-%s.js', date('Y-m-d')); 144 | execute($cmd); 145 | } 146 | -------------------------------------------------------------------------------- /scripts/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define([], function () { 4 | function Service() { 5 | if (! (this instanceof Service)) { 6 | throw new Error('`this` must be an instance of Service'); 7 | } 8 | 9 | var req = new XMLHttpRequest(); 10 | 11 | /** 12 | * @param {Function} callback({String|null}, {Boolean}) 13 | */ 14 | this.isAuthenticated = function (callback) { 15 | req.open('GET', 'https://api.lingualeo.com/isauthorized', true); 16 | req.onreadystatechange = function () { 17 | if (4 === req.readyState && 200 === req.status) { 18 | try { 19 | var response = JSON.parse(req.response); 20 | if (response.error_msg) { 21 | callback(response.error_msg); 22 | } else { 23 | callback(null, response.is_authorized); 24 | } 25 | } catch (error) { 26 | callback(error); 27 | } 28 | 29 | } 30 | }; 31 | req.send(null); 32 | }; 33 | 34 | /** 35 | * @param {Function} callback({String|null}, {Array}) 36 | */ 37 | this.getGroups = function (callback) { 38 | req.open('GET', 'https://lingualeo.com/ru/userdict3/getWordSets', true); 39 | req.onreadystatechange = function () { 40 | if (4 === req.readyState && 200 === req.status) { 41 | try { 42 | var response = JSON.parse(req.response); 43 | if (response.error_msg) { 44 | callback(response.error_msg); 45 | } else { 46 | callback(null, response.result); 47 | } 48 | } catch (error) { 49 | callback(error); 50 | } 51 | 52 | } 53 | }; 54 | req.send(null); 55 | }; 56 | 57 | /** 58 | * Callback is called on every words batch. The last parameter tells if there is more words left 59 | * or that was the last batch. 60 | * 61 | * @param {Object|null} latestWord 62 | * @param {Function} callback({String|null}, {Array}, {Boolean}) 63 | */ 64 | this.downloadWordsRecursively = function (latestWord, callback) { 65 | downloadPartsRecursively(1, function (error, wordsBatch, isThereMoreWords) { 66 | var abort = false; 67 | 68 | if (error) { 69 | callback(error); 70 | } else { 71 | if (latestWord) { 72 | var newBatch = []; 73 | 74 | // grab all the words before the latest one 75 | for (var i = 0, word; i < wordsBatch.length; i ++) { 76 | word = wordsBatch[i]; 77 | 78 | if (word.word_id == latestWord.word_id) { 79 | abort = true; 80 | // if the latest word is found in a batch no need to go farther, notify caller about 81 | // the end 82 | isThereMoreWords = false; 83 | break; 84 | } else { 85 | newBatch.push(word); 86 | } 87 | } 88 | 89 | callback(null, newBatch, isThereMoreWords); 90 | } else { 91 | callback(null, wordsBatch, isThereMoreWords); 92 | } 93 | } 94 | 95 | // interrupt recursive dictionary download if the latest word was found in a batch 96 | return abort; 97 | }); 98 | }; 99 | 100 | function downloadPartsRecursively(page, callback) { 101 | console.info('Downloading page', page); 102 | 103 | req.open('GET', 'https://lingualeo.com/ru/userdict/json?page=' + page, true); 104 | req.onreadystatechange = function () { 105 | if (4 === req.readyState && 200 === req.status) { 106 | try { 107 | var response = JSON.parse(req.response), 108 | words = []; 109 | 110 | if (response.error_msg) { 111 | callback(response.error_msg); 112 | } else { 113 | response.userdict3.forEach(function (group) { 114 | words = words.concat(group.words); 115 | }); 116 | 117 | var abort = callback(null, words, response.show_more); 118 | 119 | if (! abort && response.show_more) { 120 | downloadPartsRecursively(++ page, callback); 121 | } 122 | } 123 | } catch (error) { 124 | callback(error); 125 | } 126 | } 127 | }; 128 | req.send(null); 129 | } 130 | 131 | 132 | } 133 | 134 | return Service; 135 | }); 136 | -------------------------------------------------------------------------------- /scripts/storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define([], function () { 4 | function Storage(service) { 5 | if (! (this instanceof Storage)) { 6 | throw new Error('`this` must be an instance of Storage'); 7 | } 8 | 9 | // sync storage has limitations on 4KB per item and 100KB in total 10 | // local storage is 5MB but we explicitly requested "unlimitedStorage" in manifest 11 | var storage = chrome.storage.local, 12 | words = []; 13 | 14 | 15 | /** 16 | * @param {String} term 17 | * @param {Number} groupId 18 | * @param {Function} callback({String}, {String}) 19 | */ 20 | this.search = function (term, groupId, callback) { 21 | var prevWordValue, 22 | resultNum = 0; 23 | 24 | words.forEach(function (word) { 25 | if (resultNum < 20) { 26 | 27 | if (groupId) { 28 | if (word.groups && word.groups.indexOf(groupId) > - 1) { 29 | searchTranslations(word, word.user_translates); 30 | } 31 | } else { 32 | searchTranslations(word, word.user_translates); 33 | } 34 | } 35 | }); 36 | 37 | function searchTranslations(word, translations) { 38 | translations.forEach(function (translation) { 39 | if (translation.translate_value.indexOf(term) > - 1) { 40 | 41 | if (prevWordValue != word.word_value) { 42 | prevWordValue = word.word_value; 43 | resultNum ++; 44 | 45 | callback(word, term); 46 | } 47 | } 48 | }); 49 | } 50 | }; 51 | 52 | this.sync = function () { 53 | var that = this; 54 | 55 | storage.remove('words', function () { 56 | words = []; 57 | that.updateWords(); 58 | }); 59 | }; 60 | 61 | this.updateWords = function () { 62 | var latestWord = null, 63 | nonEmptyBatchWasPresented = false; 64 | 65 | function downloadWordsRecursivelyCallback(error, wordsBatch, isThereMoreWords) { 66 | if (error) { 67 | console.error(error); 68 | 69 | // save all grabbed words so far 70 | setWords(words); 71 | } else { 72 | if (wordsBatch.length) { 73 | nonEmptyBatchWasPresented = true; 74 | } 75 | 76 | // !!!important: if we already have the latest word then add new batch on top of the basis, 77 | // otherwise merge down (it's an initial download) 78 | 79 | if (latestWord) { 80 | console.info("Latest word:", latestWord.word_value); 81 | words = wordsBatch.concat(words); 82 | } else { 83 | words = words.concat(wordsBatch); 84 | } 85 | 86 | // save only one time at the very end 87 | console.info('is there more words:', isThereMoreWords); 88 | // no need to re-save pre-loaded words if there was no new ones 89 | if (! isThereMoreWords && nonEmptyBatchWasPresented) { 90 | console.info('re-saving words...'); 91 | setWords(words); 92 | } 93 | } 94 | } 95 | 96 | if (words.length) { 97 | latestWord = words[0]; 98 | service.downloadWordsRecursively(latestWord, downloadWordsRecursivelyCallback); 99 | } else { 100 | // pre-load existing words into memory if there are none 101 | getWords(function (_words) { 102 | words = _words; 103 | 104 | if (words.length) { 105 | latestWord = words[0]; 106 | } 107 | 108 | service.downloadWordsRecursively(latestWord, downloadWordsRecursivelyCallback); 109 | }); 110 | } 111 | }; 112 | 113 | function getWords(callback) { 114 | var words = []; 115 | storage.get("words", function (obj) { 116 | if (obj.words) { 117 | words = obj.words; 118 | } 119 | 120 | callback(words); 121 | }); 122 | } 123 | 124 | function setWords(words) { 125 | storage.set({"words": words}, function () { 126 | if (chrome.runtime.lastError) { 127 | console.error(chrome.runtime.lastError); 128 | } 129 | }); 130 | } 131 | 132 | this.setLastSyncDate = function (lastSyncDate) { 133 | storage.set({"lastSyncDate": lastSyncDate}, function () { 134 | if (chrome.runtime.lastError) { 135 | console.error(chrome.runtime.lastError); 136 | } 137 | }); 138 | }; 139 | 140 | this.getLastSyncDate = function (callback) { 141 | storage.get("lastSyncDate", function (obj) { 142 | var lastSyncDate = null; 143 | 144 | if (obj.lastSyncDate) { 145 | lastSyncDate = obj.lastSyncDate; 146 | } 147 | 148 | callback(lastSyncDate); 149 | }); 150 | }; 151 | } 152 | 153 | return Storage; 154 | }); 155 | -------------------------------------------------------------------------------- /scripts/view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define([ 4 | 'text!templates/dict-row.tpl' 5 | ], function (dictRowTpl) { 6 | 7 | function View(storage, service) { 8 | if (! (this instanceof View)) { 9 | throw new Error('`this` must be an instance of View'); 10 | } 11 | 12 | // DOM elements 13 | var contentEl, 14 | searchResultsEl, 15 | searchBox, 16 | clearSearchBox; 17 | 18 | ['content', 'glossaryPage'].forEach(function (id) { 19 | if (document.getElementById(id)) { 20 | contentEl = document.getElementById(id); 21 | } 22 | }); 23 | 24 | if (contentEl) { 25 | // class name changes depending on where we are: either glossary or dictionary 26 | ['dict-content', 'sets-words'].forEach(function (className) { 27 | if (contentEl.getElementsByClassName(className).length) { 28 | searchResultsEl = contentEl.getElementsByClassName(className)[0].children[0]; 29 | 30 | searchBox = document.getElementsByName('search')[0]; 31 | clearSearchBox = contentEl.getElementsByClassName('clear-search')[0]; 32 | 33 | if ('sets-words' == className) { 34 | listenToTheBackLinkOnGlossaryPage(); 35 | } 36 | } 37 | }); 38 | } 39 | 40 | 41 | // load groups meanwhile 42 | var groups = []; 43 | service.getGroups(function (error, _groups) { 44 | if (error) { 45 | console.error(error); 46 | } else { 47 | groups = _groups; 48 | } 49 | }); 50 | 51 | 52 | function addClass(domEl, className) { 53 | if (! hasClass(domEl, className)) { 54 | domEl.className += " " + className; 55 | } 56 | } 57 | 58 | function removeClass(domEl, className) { 59 | var re = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g'); 60 | domEl.className = domEl.className.replace(re, ''); 61 | } 62 | 63 | function hasClass(domEl, className) { 64 | var re = new RegExp('(?:^|\\s)' + className + '(?!\\S)'); 65 | return domEl.className.match(re); 66 | } 67 | 68 | function cleanTranslations() { 69 | var children = searchResultsEl.children; 70 | 71 | var i = children.length - 1, 72 | child; 73 | while (i >= 0) { 74 | child = children[i]; 75 | 76 | if (hasClass(child, 'translation')) { 77 | searchResultsEl.removeChild(child); 78 | } 79 | 80 | i --; 81 | } 82 | } 83 | 84 | var prevSearchTerm, 85 | wordNumber; 86 | 87 | function updateSearchResults(word, searchTerm) { 88 | var translations = []; 89 | 90 | if (prevSearchTerm != searchTerm) { 91 | wordNumber = 1; 92 | prevSearchTerm = searchTerm; 93 | } else { 94 | wordNumber ++; 95 | } 96 | 97 | word.user_translates.forEach(function (translation) { 98 | translations.push(translation.translate_value); 99 | }); 100 | 101 | var wrapperEl = document.createElement('div'); 102 | wrapperEl.setAttribute('class', 'dict-item-word translation'); 103 | wrapperEl.dataset.wordId = word.word_id; 104 | wrapperEl.dataset.card = word.word_id; 105 | wrapperEl.dataset.wordValue = word.word_value; 106 | wrapperEl.dataset.wordNumber = wordNumber; 107 | wrapperEl.innerHTML = dictRowTpl; 108 | 109 | var deleteWrdEl = wrapperEl.getElementsByClassName('item-word-delete')[0]; 110 | deleteWrdEl.dataset.removeWord = word.word_id; 111 | 112 | if (word.picture_url) { 113 | var imageEl = wrapperEl.getElementsByClassName('pic-bl__img')[0]; 114 | imageEl.src = word.picture_url; 115 | } 116 | 117 | var progressEl = wrapperEl.getElementsByClassName('item-word-progress')[0]; 118 | progressEl.className += ' item-word-progress-' + word.progress_percent; 119 | progressEl.dataset.progressPercent = word.progress_percent; 120 | progressEl.dataset.tooltip = "Word progress: " + word.progress_percent + "%."; 121 | progressEl.dataset.showChangeProgress = word.word_id; 122 | 123 | 124 | var soundEl = wrapperEl.getElementsByClassName('item-word-sound')[0]; 125 | soundEl.dataset.voiceUrl = word.sound_url; 126 | soundEl.dataset.tooltip = word.transcription; 127 | 128 | var groupsEl = wrapperEl.getElementsByClassName('kits-name')[0]; 129 | if (word.groups) { 130 | var groupsAdded = 0; 131 | word.groups.forEach(function (wordGroup) { 132 | var groupLinkEl = document.createElement('a'); 133 | groupLinkEl.setAttribute('class', 't-ellps link-gray-dotted lrsce-group'); 134 | groupLinkEl.dataset.wordGroup = wordGroup; 135 | // @TODO: owner? 136 | groupLinkEl.dataset.wordGroupType = "owner"; 137 | 138 | groupLinkEl.innerText = "nameless"; 139 | groups.forEach(function (group) { 140 | if (group.id == wordGroup) { 141 | groupLinkEl.innerText = group.name; 142 | } 143 | }); 144 | 145 | // separate group names from each other 146 | if (groupsAdded) { 147 | var groupsSeparatorEl = document.createElement('span'); 148 | groupsSeparatorEl.innerText = ", "; 149 | groupsEl.appendChild(groupsSeparatorEl); 150 | } 151 | 152 | groupsEl.appendChild(groupLinkEl); 153 | groupsAdded ++; 154 | 155 | // don't forget to listen to the click so we can display relevant search results 156 | listenToTheGroupLinkClick(groupLinkEl); 157 | }); 158 | } 159 | 160 | var wordEl = wrapperEl.getElementsByClassName('item-word-translate')[0].getElementsByTagName('b')[0]; 161 | wordEl.innerText = word.word_value; 162 | 163 | var translationsEl = wrapperEl.getElementsByClassName('translates t-ellps')[0]; 164 | translationsEl.innerText = translations.join('; '); 165 | 166 | 167 | searchResultsEl.appendChild(wrapperEl); 168 | } 169 | 170 | this.listenToSearchBox = function () { 171 | if (! searchResultsEl) { 172 | console.info('`searchResultsEl` was not found on the page'); 173 | return; 174 | } 175 | 176 | clearSearchBox.addEventListener('click', function (e) { 177 | if (hasClass(searchResultsEl, 'translations')) { 178 | removeClass(searchResultsEl, 'translations'); 179 | cleanTranslations(); 180 | } 181 | }); 182 | 183 | searchBox.addEventListener('keyup', function (e) { 184 | if (this.value) { 185 | removeClass(clearSearchBox, 'vhidden'); 186 | } 187 | 188 | if (hasClass(searchResultsEl, 'translations')) { 189 | removeClass(searchResultsEl, 'translations'); 190 | cleanTranslations(); 191 | 192 | if (! this.value) { 193 | // trigger lingualeo dictionary results 194 | clearSearchBox.click(); 195 | } 196 | } 197 | 198 | 199 | if (this.value && isCyrillicInput(this.value)) { 200 | e.stopPropagation(); 201 | 202 | search(this.value); 203 | } 204 | }); 205 | }; 206 | 207 | function search(value) { 208 | var groupId = null; 209 | 210 | // support search in groups in word sets 211 | if (location.pathname.indexOf('/ru/userdict/wordSets/') > - 1) { 212 | groupId = location.pathname.replace('/ru/userdict/wordSets/', ''); 213 | // just in case there is a garbage after group id in the URL 214 | if (groupId.indexOf('/') > - 1) { 215 | groupId = groupId.substring(0, groupId.indexOf('/')); 216 | } 217 | groupId = Number(groupId); 218 | } 219 | 220 | // support search in groups in the glossary 221 | if (location.pathname.indexOf('/ru/glossary/learn/') > - 1) { 222 | groupId = location.pathname.replace('/ru/glossary/learn/', ''); 223 | // just in case there is a garbage after group id in the URL 224 | if (groupId.indexOf('/') > - 1) { 225 | groupId = groupId.substring(0, groupId.indexOf('/')); 226 | } 227 | groupId = Number(groupId); 228 | } 229 | 230 | addClass(searchResultsEl, 'translations'); 231 | searchResultsEl.style.display = "block"; 232 | searchResultsEl.innerHTML = ""; 233 | 234 | storage.search(value, groupId, updateSearchResults); 235 | } 236 | 237 | /** 238 | * Actually everything that we need to know is if the first character matches russian alphabet 239 | * 240 | * @param {String} input 241 | * @returns {boolean} 242 | */ 243 | function isCyrillicInput(input) { 244 | if (input) { 245 | return null !== input[0].match(/[а-я]+/ig); 246 | } else { 247 | return false; 248 | } 249 | } 250 | 251 | function listenToTheGroupLinkClick(groupLinkEl) { 252 | groupLinkEl.addEventListener('click', function (e) { 253 | var groupId = groupLinkEl.dataset.wordGroup, 254 | value = searchBox.value, 255 | backLinkEl = contentEl.getElementsByClassName('iconm-back-link')[0], 256 | dictTitleEl = contentEl.getElementsByClassName('dict-title-main')[0]; 257 | 258 | 259 | if (value && isCyrillicInput(value)) { 260 | e.stopPropagation(); 261 | 262 | window.history.pushState({}, '', '/ru/userdict/wordSets/' + groupId); 263 | search(value); 264 | 265 | // show link and remove data attribute so browser can reload page 266 | // and we don't think about refreshing search results 267 | backLinkEl.style.display = 'block'; 268 | // if URL ends on "/wordSets" lingualeo doesn't display group names next to words 269 | backLinkEl.setAttribute('href', '/ru/userdict'); 270 | delete backLinkEl.dataset.dictSwitchView; 271 | 272 | // title 273 | for (var i = 0; i < groups.length; i ++) { 274 | if (groups[i].id == groupId) { 275 | dictTitleEl.innerText = groups[i].name; 276 | break; 277 | } 278 | } 279 | } 280 | }); 281 | } 282 | 283 | /** 284 | * Clear search results on a back button (to all glossary groups) 285 | */ 286 | function listenToTheBackLinkOnGlossaryPage() { 287 | var backLinkEl = contentEl.getElementsByClassName('iconm-back-link')[1]; 288 | 289 | backLinkEl.addEventListener('click', function () { 290 | var value = searchBox.value; 291 | 292 | if (isCyrillicInput(value)) { 293 | var event = new MouseEvent('click', { 294 | 'view': window, 295 | 'bubbles': true, 296 | 'cancelable': true 297 | }); 298 | clearSearchBox.dispatchEvent(event); 299 | } 300 | }); 301 | } 302 | } 303 | 304 | return View; 305 | }); 306 | -------------------------------------------------------------------------------- /scripts/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS text 2.0.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/requirejs/text for details 5 | */ 6 | /*jslint regexp: true */ 7 | /*global require, XMLHttpRequest, ActiveXObject, 8 | define, window, process, Packages, 9 | java, location, Components, FileUtils */ 10 | 11 | define(['module'], function (module) { 12 | 'use strict'; 13 | 14 | var text, fs, Cc, Ci, 15 | progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 16 | xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, 17 | bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, 18 | hasLocation = typeof location !== 'undefined' && location.href, 19 | defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), 20 | defaultHostName = hasLocation && location.hostname, 21 | defaultPort = hasLocation && (location.port || undefined), 22 | buildMap = [], 23 | masterConfig = (module.config && module.config()) || {}; 24 | 25 | text = { 26 | version: '2.0.6', 27 | 28 | strip: function (content) { 29 | //Strips declarations so that external SVG and XML 30 | //documents can be added to a document without worry. Also, if the string 31 | //is an HTML document, only the part inside the body tag is returned. 32 | if (content) { 33 | content = content.replace(xmlRegExp, ""); 34 | var matches = content.match(bodyRegExp); 35 | if (matches) { 36 | content = matches[1]; 37 | } 38 | } else { 39 | content = ""; 40 | } 41 | return content; 42 | }, 43 | 44 | jsEscape: function (content) { 45 | return content.replace(/(['\\])/g, '\\$1') 46 | .replace(/[\f]/g, "\\f") 47 | .replace(/[\b]/g, "\\b") 48 | .replace(/[\n]/g, "\\n") 49 | .replace(/[\t]/g, "\\t") 50 | .replace(/[\r]/g, "\\r") 51 | .replace(/[\u2028]/g, "\\u2028") 52 | .replace(/[\u2029]/g, "\\u2029"); 53 | }, 54 | 55 | createXhr: masterConfig.createXhr || function () { 56 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 57 | var xhr, i, progId; 58 | if (typeof XMLHttpRequest !== "undefined") { 59 | return new XMLHttpRequest(); 60 | } else if (typeof ActiveXObject !== "undefined") { 61 | for (i = 0; i < 3; i += 1) { 62 | progId = progIds[i]; 63 | try { 64 | xhr = new ActiveXObject(progId); 65 | } catch (e) {} 66 | 67 | if (xhr) { 68 | progIds = [progId]; // so faster next time 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return xhr; 75 | }, 76 | 77 | /** 78 | * Parses a resource name into its component parts. Resource names 79 | * look like: module/name.ext!strip, where the !strip part is 80 | * optional. 81 | * @param {String} name the resource name 82 | * @returns {Object} with properties "moduleName", "ext" and "strip" 83 | * where strip is a boolean. 84 | */ 85 | parseName: function (name) { 86 | var modName, ext, temp, 87 | strip = false, 88 | index = name.indexOf("."), 89 | isRelative = name.indexOf('./') === 0 || 90 | name.indexOf('../') === 0; 91 | 92 | if (index !== -1 && (!isRelative || index > 1)) { 93 | modName = name.substring(0, index); 94 | ext = name.substring(index + 1, name.length); 95 | } else { 96 | modName = name; 97 | } 98 | 99 | temp = ext || modName; 100 | index = temp.indexOf("!"); 101 | if (index !== -1) { 102 | //Pull off the strip arg. 103 | strip = temp.substring(index + 1) === "strip"; 104 | temp = temp.substring(0, index); 105 | if (ext) { 106 | ext = temp; 107 | } else { 108 | modName = temp; 109 | } 110 | } 111 | 112 | return { 113 | moduleName: modName, 114 | ext: ext, 115 | strip: strip 116 | }; 117 | }, 118 | 119 | xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, 120 | 121 | /** 122 | * Is an URL on another domain. Only works for browser use, returns 123 | * false in non-browser environments. Only used to know if an 124 | * optimized .js version of a text resource should be loaded 125 | * instead. 126 | * @param {String} url 127 | * @returns Boolean 128 | */ 129 | useXhr: function (url, protocol, hostname, port) { 130 | var uProtocol, uHostName, uPort, 131 | match = text.xdRegExp.exec(url); 132 | if (!match) { 133 | return true; 134 | } 135 | uProtocol = match[2]; 136 | uHostName = match[3]; 137 | 138 | uHostName = uHostName.split(':'); 139 | uPort = uHostName[1]; 140 | uHostName = uHostName[0]; 141 | 142 | return (!uProtocol || uProtocol === protocol) && 143 | (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && 144 | ((!uPort && !uHostName) || uPort === port); 145 | }, 146 | 147 | finishLoad: function (name, strip, content, onLoad) { 148 | content = strip ? text.strip(content) : content; 149 | if (masterConfig.isBuild) { 150 | buildMap[name] = content; 151 | } 152 | onLoad(content); 153 | }, 154 | 155 | load: function (name, req, onLoad, config) { 156 | //Name has format: some.module.filext!strip 157 | //The strip part is optional. 158 | //if strip is present, then that means only get the string contents 159 | //inside a body tag in an HTML string. For XML/SVG content it means 160 | //removing the declarations so the content can be inserted 161 | //into the current doc without problems. 162 | 163 | // Do not bother with the work if a build and text will 164 | // not be inlined. 165 | if (config.isBuild && !config.inlineText) { 166 | onLoad(); 167 | return; 168 | } 169 | 170 | masterConfig.isBuild = config.isBuild; 171 | 172 | var parsed = text.parseName(name), 173 | nonStripName = parsed.moduleName + 174 | (parsed.ext ? '.' + parsed.ext : ''), 175 | url = req.toUrl(nonStripName), 176 | useXhr = (masterConfig.useXhr) || 177 | text.useXhr; 178 | 179 | //Load the text. Use XHR if possible and in a browser. 180 | if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { 181 | text.get(url, function (content) { 182 | text.finishLoad(name, parsed.strip, content, onLoad); 183 | }, function (err) { 184 | if (onLoad.error) { 185 | onLoad.error(err); 186 | } 187 | }); 188 | } else { 189 | //Need to fetch the resource across domains. Assume 190 | //the resource has been optimized into a JS module. Fetch 191 | //by the module name + extension, but do not include the 192 | //!strip part to avoid file system issues. 193 | req([nonStripName], function (content) { 194 | text.finishLoad(parsed.moduleName + '.' + parsed.ext, 195 | parsed.strip, content, onLoad); 196 | }); 197 | } 198 | }, 199 | 200 | write: function (pluginName, moduleName, write, config) { 201 | if (buildMap.hasOwnProperty(moduleName)) { 202 | var content = text.jsEscape(buildMap[moduleName]); 203 | write.asModule(pluginName + "!" + moduleName, 204 | "define(function () { return '" + 205 | content + 206 | "';});\n"); 207 | } 208 | }, 209 | 210 | writeFile: function (pluginName, moduleName, req, write, config) { 211 | var parsed = text.parseName(moduleName), 212 | extPart = parsed.ext ? '.' + parsed.ext : '', 213 | nonStripName = parsed.moduleName + extPart, 214 | //Use a '.js' file name so that it indicates it is a 215 | //script that can be loaded across domains. 216 | fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; 217 | 218 | //Leverage own load() method to load plugin value, but only 219 | //write out values that do not have the strip argument, 220 | //to avoid any potential issues with ! in file names. 221 | text.load(nonStripName, req, function (value) { 222 | //Use own write() method to construct full module value. 223 | //But need to create shell that translates writeFile's 224 | //write() to the right interface. 225 | var textWrite = function (contents) { 226 | return write(fileName, contents); 227 | }; 228 | textWrite.asModule = function (moduleName, contents) { 229 | return write.asModule(moduleName, fileName, contents); 230 | }; 231 | 232 | text.write(pluginName, nonStripName, textWrite, config); 233 | }, config); 234 | } 235 | }; 236 | 237 | if (masterConfig.env === 'node' || (!masterConfig.env && 238 | typeof process !== "undefined" && 239 | process.versions && 240 | !!process.versions.node)) { 241 | //Using special require.nodeRequire, something added by r.js. 242 | fs = require.nodeRequire('fs'); 243 | 244 | text.get = function (url, callback) { 245 | var file = fs.readFileSync(url, 'utf8'); 246 | //Remove BOM (Byte Mark Order) from utf8 files if it is there. 247 | if (file.indexOf('\uFEFF') === 0) { 248 | file = file.substring(1); 249 | } 250 | callback(file); 251 | }; 252 | } else if (masterConfig.env === 'xhr' || (!masterConfig.env && 253 | text.createXhr())) { 254 | text.get = function (url, callback, errback, headers) { 255 | var xhr = text.createXhr(), header; 256 | xhr.open('GET', url, true); 257 | 258 | //Allow plugins direct access to xhr headers 259 | if (headers) { 260 | for (header in headers) { 261 | if (headers.hasOwnProperty(header)) { 262 | xhr.setRequestHeader(header.toLowerCase(), headers[header]); 263 | } 264 | } 265 | } 266 | 267 | //Allow overrides specified in config 268 | if (masterConfig.onXhr) { 269 | masterConfig.onXhr(xhr, url); 270 | } 271 | 272 | xhr.onreadystatechange = function (evt) { 273 | var status, err; 274 | //Do not explicitly handle errors, those should be 275 | //visible via console output in the browser. 276 | if (xhr.readyState === 4) { 277 | status = xhr.status; 278 | if (status > 399 && status < 600) { 279 | //An http 4xx or 5xx error. Signal an error. 280 | err = new Error(url + ' HTTP status: ' + status); 281 | err.xhr = xhr; 282 | errback(err); 283 | } else { 284 | callback(xhr.responseText); 285 | } 286 | 287 | if (masterConfig.onXhrComplete) { 288 | masterConfig.onXhrComplete(xhr, url); 289 | } 290 | } 291 | }; 292 | xhr.send(null); 293 | }; 294 | } else if (masterConfig.env === 'rhino' || (!masterConfig.env && 295 | typeof Packages !== 'undefined' && typeof java !== 'undefined')) { 296 | //Why Java, why is this so awkward? 297 | text.get = function (url, callback) { 298 | var stringBuffer, line, 299 | encoding = "utf-8", 300 | file = new java.io.File(url), 301 | lineSeparator = java.lang.System.getProperty("line.separator"), 302 | input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), 303 | content = ''; 304 | try { 305 | stringBuffer = new java.lang.StringBuffer(); 306 | line = input.readLine(); 307 | 308 | // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 309 | // http://www.unicode.org/faq/utf_bom.html 310 | 311 | // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: 312 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 313 | if (line && line.length() && line.charAt(0) === 0xfeff) { 314 | // Eat the BOM, since we've already found the encoding on this file, 315 | // and we plan to concatenating this buffer with others; the BOM should 316 | // only appear at the top of a file. 317 | line = line.substring(1); 318 | } 319 | 320 | stringBuffer.append(line); 321 | 322 | while ((line = input.readLine()) !== null) { 323 | stringBuffer.append(lineSeparator); 324 | stringBuffer.append(line); 325 | } 326 | //Make sure we return a JavaScript string and not a Java string. 327 | content = String(stringBuffer.toString()); //String 328 | } finally { 329 | input.close(); 330 | } 331 | callback(content); 332 | }; 333 | } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && 334 | typeof Components !== 'undefined' && Components.classes && 335 | Components.interfaces)) { 336 | //Avert your gaze! 337 | Cc = Components.classes, 338 | Ci = Components.interfaces; 339 | Components.utils['import']('resource://gre/modules/FileUtils.jsm'); 340 | 341 | text.get = function (url, callback) { 342 | var inStream, convertStream, 343 | readData = {}, 344 | fileObj = new FileUtils.File(url); 345 | 346 | //XPCOM, you so crazy 347 | try { 348 | inStream = Cc['@mozilla.org/network/file-input-stream;1'] 349 | .createInstance(Ci.nsIFileInputStream); 350 | inStream.init(fileObj, 1, 0, false); 351 | 352 | convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] 353 | .createInstance(Ci.nsIConverterInputStream); 354 | convertStream.init(inStream, "utf-8", inStream.available(), 355 | Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); 356 | 357 | convertStream.readString(inStream.available(), readData); 358 | convertStream.close(); 359 | inStream.close(); 360 | callback(readData.value); 361 | } catch (e) { 362 | throw new Error((fileObj && fileObj.path || '') + ': ' + e); 363 | } 364 | }; 365 | } 366 | return text; 367 | }); -------------------------------------------------------------------------------- /scripts/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.1.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||g.onError!==ca)try{f=i.execCb(c,l,b,f)}catch(d){a=d}else f=i.execCb(c,l,b,f);this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports:this.usingExports&& 19 | (f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,g.onResourceLoad))g.onResourceLoad(i,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a= 20 | this.map,b=a.id,d=p(a.prefix);this.depMaps.push(d);q(d,"defined",u(this,function(f){var l,d;d=m(aa,this.map.id);var e=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,n=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(e=f.normalize(e,function(a){return c(a,P,!0)})||""),f=p(a.prefix+"!"+e,this.map.parentMap),q(f,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(h,f.id)){this.depMaps.push(f); 21 | if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=i.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(h,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,e=p(d),P=M;c&&(f=c);P&&(M=!1);s(e);t(j.config,b)&&(j.config[d]=j.config[b]);try{g.exec(f)}catch(h){return w(C("fromtexteval", 22 | "fromText eval for "+b+" failed: "+h,h,[b]))}P&&(M=!0);this.depMaps.push(e);i.completeLoad(d);n([d],l)}),f.load(a.name,n,l,j))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=p(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",u(this,function(a){this.defineDep(b, 23 | a);this.check()}));this.errback&&q(a,"error",u(this,this.errback))}c=a.id;f=h[c];!t(L,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,u(this,function(a){var b=m(h,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:j,contextName:b,registry:h,defined:r,urlFetched:S,defQueue:A,Module:Z,makeModuleMap:p, 24 | nextTick:g.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(j[b]||(j[b]={}),U(j[b],a,!0,!0)):j[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(aa[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),j.shim=b);a.packages&&v(a.packages,function(a){var b, 25 | a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(j.paths[b]=a.location);j.pkgs[b]=a.name+"/"+(a.main||"main").replace(ia,"").replace(Q,"")});B(h,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=p(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,e){function j(c,d,m){var n,q;e.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild= 26 | !0);if("string"===typeof c){if(G(d))return w(C("requireargs","Invalid require call"),m);if(a&&t(L,c))return L[c](h[a.id]);if(g.get)return g.get(i,c,a,j);n=p(c,a,!1,!0);n=n.id;return!t(r,n)?w(C("notloaded",'Module name "'+n+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[n]}J();i.nextTick(function(){J();q=s(p(null,a));q.skipMap=e.skipMap;q.init(c,d,m,{enabled:!0});D()});return j}e=e||{};U(j,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),k=b.split("/")[0];if(-1!== 27 | e&&(!("."===k||".."===k)||1e.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,e.attachEvent("onreadystatechange",b.onScriptLoad)): 34 | (e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)),e.src=d,J=e,D?y.insertBefore(e,D):y.appendChild(e),J=null,e;if(ea)try{importScripts(d),b.completeLoad(c)}catch(m){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,m,[c]))}};z&&!q.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return s=I,q.baseUrl||(E=s.split("/"),s=E.pop(),O=E.length?E.join("/")+"/":"./",q.baseUrl= 35 | O),s=s.replace(Q,""),g.jsExtRegExp.test(s)&&(s=I),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ka,"").replace(la,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b|| 36 | (b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}(g?g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(q)}})(this); 37 | --------------------------------------------------------------------------------