diff --git a/chrome_point_plus/js/background.js b/chrome_point_plus/js/background.js index 11ff391..c0874f1 100644 --- a/chrome_point_plus/js/background.js +++ b/chrome_point_plus/js/background.js @@ -1,10 +1,185 @@ +var VERSION = (function() { + /** + * @deprecated XMLHttpRequest in the background worker is deprecated + * according to the Chrome warning. But we definitely need synchronous + * AJAX here + */ + var xhr = new XMLHttpRequest(), + manifest; + + xhr.open('GET', chrome.extension.getURL('manifest.json'), false); + xhr.send(null); + + manifest = JSON.parse(xhr.responseText); + + return manifest.version; +})(); + +/** + * Вставка нескольких файлов друг за другом + * @param {Array} files Список файлов + * @param {Function} injectOne Функция вставки одного файла. Должна принимать file и callback + * @param {Function} [onAllInjected] Функция обработки ответа + * @param {Array} [results] Результаты вставки (их не нужно передавать при запуске извне) + */ +function injectFiles(files, injectOne, onAllInjected, results) { + results = results || []; + + if (files.length) { + injectOne(files.shift(), function(res) { + if (res) { + results.unshift(res[0]); + } + + injectFiles(files, injectOne, onAllInjected, results); + }); + } else { + onAllInjected(results); + } +} + +/** + * @constructor Менеджер сообщений + */ +function MessageListener() { + chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { + if (this.isMethodAvailable(message)) { + console.info('Call #%s() method for tab #%s', message.type, this.getTabId(sender)); + this[message.type].apply(this, arguments); + + return true; + } else { + console.warn('Method #%s() called from tab #%s does not exists', message.type, this.getTabId(sender)); + + return false; + } + }.bind(this)); +} + +/** + * @param {Object} message Сообщение + * @returns {Boolean} Есть ли необходимый метод в MessageListener + */ +MessageListener.prototype.isMethodAvailable = function(message) { + return message && message.type && typeof this[message.type] === 'function'; +}; + +/** + * @param {Object} sender + * @returns {Number|Null} Идентификатор вкладки, с которой пришло сообщение + */ +MessageListener.prototype.getTabId = function(sender) { + return sender.tab && sender.tab.id || null; +}; + +/** + * @param {Object} message Сообщение + * @param {Object} sender Отправитель + * @param {Function} sendResponse Коллбек для обработки результата + */ +MessageListener.prototype.showPageAction = function(message, sender, sendResponse) { + chrome.pageAction.show(this.getTabId(sender)); + sendResponse(true); +}; + +/** + * Показывает нотификацию + * @param {Object} message Сообщение + * @param {Object} sender Отправитель + * @param {Function} sendResponse Коллбек для обработки результата + */ +MessageListener.prototype.showNotification = function(message, sender, sendResponse) { + chrome.notifications.create( + message.notificationId, { + type: 'basic', + iconUrl: message.avatarUrl, + title: message.title, + message: message.text, + priority: 0, + isClickable: true + }, function(notificationId) { + console.info('Notification "%s" created', notificationId); + + sendResponse(true); + } + ); +}; + +/** + * Получает версию плагина из манифеста + * @param {Object} message Сообщение + * @param {Object} sender Отправитель + * @param {Function} sendResponse Коллбек для обработки результата + */ +MessageListener.prototype.getManifestVersion = function(message, sender, sendResponse) { + sendResponse({ version: VERSION }); +}; + +/** + * @param {Object} message Сообщение + */ +MessageListener.prototype.getFiles = function(message, defaultRunAt) { + var files; + + if ( ! message.files) { + return false; + } else { + files = Array.isArray(message.files) ? message.files : [ message.files ]; + + return files.map(function(file) { + return { + file: typeof file === 'string' ? file : file.file, + runAt: file.runAt || defaultRunAt + }; + }); + } +}; + +/** + * Вставляет JS-файлы во вкладку + * @param {Object} message Сообщение + * @param {Object} sender Отправитель + * @param {Function} sendResponse Коллбек для обработки результата + */ +MessageListener.prototype.executeJSFiles = function(message, sender, sendResponse) { + var tabId = this.getTabId(sender); + + injectFiles( + this.getFiles(message, 'document_end'), + function(file, callback) { + chrome.tabs.executeScript(tabId, file, callback) + }, + sendResponse + ); +}; + +/** + * Вставляет CSS-файлы во вкладку + * @param {Object} message Сообщение + * @param {Object} sender Отправитель + * @param {Function} sendResponse Коллбек для обработки результата + */ +MessageListener.prototype.injectCSSFiles = function(message, sender, sendResponse) { + var tabId = this.getTabId(sender); + + injectFiles( + this.getFiles(message), + function(file, callback) { + chrome.tabs.insertCSS(tabId, file, callback); + }, + sendResponse + ); + +}; + +new MessageListener(); + // Maintaining options version chrome.storage.sync.get('options_version', function(data) { - var pp_version = getVersion(); - console.info('Point+ %s. Options are for %s.', pp_version, data.options_version); - - if (data.options_version != pp_version) { - chrome.tabs.create({url: 'options.html'}); + console.info('Point+ %s. Options are for %s.', VERSION, data.options_version); + + if (data.options_version !== VERSION) { + chrome.tabs.create({ url: 'options.html' }); } }); @@ -24,175 +199,3 @@ chrome.notifications.onClicked.addListener(function(notificationId) { }); } }); - -// Crutches and bikes -/** - * Inject several JS files - * @param {number} tabId Unique ID of tab which requested injection - * @param {Object[]} files Array of objects of files to inject - * @param {function} onAllInjected allback function running when injection ends - */ -function injectJS(tabId, files, onAllInjected) { - var item = files.shift(); - if (item) { - console.log('Injecting JS "%s" to the tab #%s', item.file, tabId); - - if ('file' in item) { - chrome.tabs.executeScript(tabId ? tabId : null, { - file: item.file, - runAt: item.runAt || 'document_start' - }, function(result) { - console.info('"%s" injected to the tab #%s', item.file, tabId); - - injectJS(tabId, files, onAllInjected); - }); - } - } else { - onAllInjected(); - } -} - -// @todo Implement injectCSS (because JS execution working always after CSS injection) - -// Message listener -chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { - // @todo Check if sender.tab may be undefined in some cases - console.log('Received message from tab #%s: %O', sender.tab ? sender.tab.id : 'undefined', message); - - if (message) { - switch (message.type) { - case 'showPageAction': - chrome.pageAction.show(sender.tab.id); - sendResponse(true); - - console.log('Showed pageAction for tab #%s', sender.tab.id); - - // Fuck You, Chrome API documentation!!11 - return true; - break; - - case 'showNotification': - chrome.notifications.create( - message.notificationId, { - type: 'basic', - iconUrl: message.avatarUrl, - title: message.title, - message: message.text, - priority: 0, - isClickable: true - }, function(notificationId) { - console.info('Notification "%s" created', notificationId); - - sendResponse(true); - } - ); - - // Fuck You, Chrome API documentation!!11 - return true; - break; - - case 'getManifestVersion': - sendResponse({version: getVersion()}); - return true; - break; - - /** - * @deprecated since 1.19.1 - */ - case 'injectJSFile': - console.log('Executing JS: %s', message.file); - chrome.tabs.executeScript(sender.tab.id ? sender.tab.id : null, { - file: message.file, - runAt: message.runAt || 'document_start' - }, function() { - sendResponse(true); - - console.info('JS file executed: "%s"', message.file); - return true; - }); - - // Fuck You, Chrome API documentation! - return true; - break; - - // Inject several files - case 'executeJSFiles': - //console.debug('Received JS file list: %O', message.files); - - if (message.files.length) { - injectJS(sender.tab.id ? sender.tab.id : null, message.files, function() { - // @fixme does not sending response now! - console.info('All scripts executed'); - - sendResponse(true); - return true; - }); - } else { - /* - * May be not? - * But I don't want to block some shit-code execution - */ - sendResponse(false); - - console.warn('No scripts executed (empty script array)'); - } - - // Fuck You, Chrome API documentation! - return true; - break; - - /** - * @deprecated since 1.19.1 - */ - case 'injectCSSFile': - console.log('Injecting CSS: "%s"', message.file); - chrome.tabs.insertCSS(sender.tab.id ? sender.tab.id : null, { - file: message.file - }, function() { - // @todo message response callback processing - //sendResponse(true); - - console.info('CSS file "%s" injected', message.file); - }); - - // Fuck You, Chrome API documentation! - return true; - break; - - case 'injectCSSCode': - if (message.code !== undefined) { - chrome.tabs.insertCSS(sender.tab.id ? sender.tab.id : null, { - code: message.code - }, function() { - // @todo message response callback processing - //sendResponse(true); - - console.info('CSS code injected: \n%s', message.file); - }); - } - - // Fuck You, Chrome API documentation! - return true; - break; - - default: - sendResponse(false); - return true; - break; - } - } -}); - -// Getting version from manifest.json -function getVersion() { - /** - * @deprecated XMLHttpRequest in the background worker is deprecated - * according to the Chrome warning. But we definitely need synchronous - * AJAX here - */ - var xhr = new XMLHttpRequest(); - xhr.open('GET', chrome.extension.getURL('manifest.json'), false); - xhr.send(null); - var manifest = JSON.parse(xhr.responseText); - return manifest.version; -} diff --git a/chrome_point_plus/js/message-sender.js b/chrome_point_plus/js/message-sender.js new file mode 100644 index 0000000..38af910 --- /dev/null +++ b/chrome_point_plus/js/message-sender.js @@ -0,0 +1,20 @@ +function MessageSender() {} +function stub() {} + +MessageSender.prototype.css = function(files, callback) { + this.sendMessage({ + type: 'injectCSSFiles', + files: files + }, callback || stub); +}; + +MessageSender.prototype.js = function(files, callback) { + this.sendMessage({ + type: 'executeJSFiles', + files: files + }, callback || stub); +}; + +MessageSender.prototype.sendMessage = function() { + chrome.runtime.sendMessage.apply(chrome.runtime, arguments); +}; diff --git a/chrome_point_plus/js/options-manager.js b/chrome_point_plus/js/options-manager.js new file mode 100644 index 0000000..0a5072b --- /dev/null +++ b/chrome_point_plus/js/options-manager.js @@ -0,0 +1,37 @@ +/** + * Объект для получения опций + * @param {Object} options Хеш настроек + * @constructor + */ +function OptionsManager(options) { + this._options = options || {}; +} + +/** + * @param {String} optionName Имя опции + * @returns {Boolean|String|Null} Значение опции + */ +OptionsManager.prototype.get = function(optionName) { + return this._options.hasOwnProperty(optionName) ? this._options[optionName].value : null; +}; + +/** + * Проверяет, равна ли опция значению value. Если value не переданно, проверяет задана ли она и не равна ли false/'' + * @param {String} optionName Имя опции + * @param {Boolean|String} [value=true] Значение опции + * @returns {Boolean} + */ +OptionsManager.prototype.is = function(optionName, value) { + if (typeof value !== 'undefined') { + return this.get(optionName) === value; + } else { + return Boolean(this.get(optionName)); + } +}; + +/** + * @returns {Object} Хеш опций + */ +OptionsManager.prototype.getOptions = function() { + return this._options; +}; diff --git a/chrome_point_plus/js/point-plus.js b/chrome_point_plus/js/point-plus.js index 3be4249..8eea6cd 100644 --- a/chrome_point_plus/js/point-plus.js +++ b/chrome_point_plus/js/point-plus.js @@ -1,58 +1,23 @@ +var messenger = new MessageSender(); + // Showing page action -chrome.runtime.sendMessage({ +messenger.sendMessage({ type: 'showPageAction' -}, null, function(response) { +}, function(response) { console.debug('showPageAction response: %O', response); }); -// @todo Move OptionsManager to the separate file -/** - * Объект для получения опций - * @param {Object} options Хеш настроек - * @constructor - */ -function OptionsManager(options) { - this._options = options || {}; -} - -/** - * @param {String} optionName Имя опции - * @returns {Boolean|String|Null} Значение опции - */ -OptionsManager.prototype.get = function(optionName) { - return this._options.hasOwnProperty(optionName) ? this._options[optionName].value : null; -}; - -/** - * Проверяет, равна ли опция значению value. Если value не переданно, проверяет задана ли она и не равна ли false/'' - * @param {String} optionName Имя опции - * @param {Boolean|String} [value=true] Значение опции - * @returns {Boolean} - */ -OptionsManager.prototype.is = function(optionName, value) { - if (typeof value !== 'undefined') { - return this.get(optionName) === value; - } else { - return Boolean(this.get(optionName)); - } -}; - -/** - * @returns {Object} Хеш опций - */ -OptionsManager.prototype.getOptions = function() { - return this._options; -}; - -var ppVersion; - -chrome.runtime.sendMessage(null, { - type: 'getManifestVersion' -}, null, function(response) { - ppVersion = response.version || 'undefined'; +messenger.sendMessage({ + type: 'getManifestVersion' +}, function(response) { + $(document).ready(function() { + PointPlus(response.version || 'undefined') + }); }); -$(document).ready(function() { + +function PointPlus(ppVersion) { + // Grouping console log console.group('point-plus'); console.info('Point+ %s', ppVersion); @@ -72,6 +37,7 @@ $(document).ready(function() { draft_set_save_handler(); draft_restore(); + // Loading options chrome.storage.sync.get('options', function(sync_data) { var options = new OptionsManager(sync_data.options); @@ -106,13 +72,10 @@ $(document).ready(function() { // Soundcloud if (options.is('option_embedding_soundcloud')) { // Executing Soundcloud player JS API - chrome.runtime.sendMessage({ - type: 'executeJSFiles', - files: [{ - file: 'vendor/soundcloud/soundcloud.player.api.js', - runAt: 'document_end' - }] - }, null, function(response) { + messenger.js({ + file: 'vendor/soundcloud/soundcloud.player.api.js', + runAt: 'document_end' + }, function(response) { console.debug('Soundcloud injection response: %O', response); // If scripts are executed if (response) { @@ -161,28 +124,25 @@ $(document).ready(function() { // Injecting Fancybox to the page // CSS // @todo message response callback processing - chrome.runtime.sendMessage({ - type: 'injectCSSFile', - file: 'vendor/fancybox/source/jquery.fancybox.css' - }); - // @todo message response callback processing - chrome.runtime.sendMessage({ - type: 'injectCSSFile', - file: 'css/fancybox/style.css' - }); + messenger.css([ + 'vendor/fancybox/source/jquery.fancybox.css', + 'css/fancybox/style.css' + ]); + // JS - chrome.runtime.sendMessage(null, { - type: 'executeJSFiles', - files: [{ + messenger.js([ + { file: 'vendor/fancybox/source/jquery.fancybox.pack.js', runAt: 'document_end' - }, { + }, + { // @todo Move to the option_fancybox_videos section file: 'vendor/fancybox/source/helpers/jquery.fancybox-media.js', runAt: 'document_end' - }] - }, null, function(response) { + } + ], function(response) { // If all JS are executed + console.debug('Fancybox injection response: %O', response); if (response) { console.log('Fancybox executed. Processing...'); @@ -321,32 +281,23 @@ $(document).ready(function() { // CSS // @todo message response callback processing - chrome.runtime.sendMessage({ - type: 'injectCSSFile', - file: 'vendor/markitup/markitup/skins/markitup/style.css' - }); - // Fixes for extension - // @todo message response callback processing - chrome.runtime.sendMessage({ - type: 'injectCSSFile', - file: 'css/markitup/skins/markitup/style.css' - }); - // @todo message response callback processing - chrome.runtime.sendMessage({ - type: 'injectCSSFile', - file: 'css/markitup/sets/markdown/style.css' - }); + messenger.css([ + 'vendor/markitup/markitup/skins/markitup/style.css', + 'css/markitup/skins/markitup/style.css', + 'css/markitup/sets/markdown/style.css' + ]); + // JS - chrome.runtime.sendMessage({ - type: 'executeJSFiles', - files: [{ + messenger.js([ + { file: 'vendor/markitup/markitup/jquery.markitup.js', runAt: 'document_end' - }, { + }, + { file: 'js/markitup/sets/markdown/set.js', runAt: 'document_end' - }] - }, null, function(response) { + } + ], function(response) { console.debug('MarkItUp injection response: %O', response); // If scripts are executed if (response) { @@ -466,7 +417,7 @@ $(document).ready(function() { // Desktop notifications if (options.is('option_ws_comments_notifications')) { console.log('Showing desktop notification'); - chrome.runtime.sendMessage({ + messenger.sendMessage({ type: 'showNotification', notificationId: 'comment_' + wsMessage.post_id + '#' + wsMessage.comment_id, avatarUrl: getProtocol() + '//point.im/avatar/' + wsMessage.author + '/80', @@ -539,10 +490,7 @@ $(document).ready(function() { // @ before username if (options.is('option_at_before_username')) { // @todo message response callback processing - chrome.runtime.sendMessage({ - type: 'injectCSSFile', - file: 'css/modules/at_before_username.css' - }); + messenger.css('css/modules/at_before_username.css'); } if (options.is('option_ajax')) { @@ -666,7 +614,7 @@ $(document).ready(function() { $('#point-plus-debug').fadeOut(1000); }); -}); +} function getProtocol() { return ((location.protocol == 'http:') ? 'http:' : 'https:'); diff --git a/chrome_point_plus/manifest.json b/chrome_point_plus/manifest.json index aed0994..a4e88b2 100644 --- a/chrome_point_plus/manifest.json +++ b/chrome_point_plus/manifest.json @@ -30,7 +30,10 @@ "vendor/jquery/jquery.min.js", "js/bquery_ajax.js", - + + "js/options-manager.js", + "js/message-sender.js", + "js/point-plus.js" ], "css": [