├── .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 |
--------------------------------------------------------------------------------