diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..ba0accc --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "app/bower_components" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8a80734 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a88969 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +temp +.tmp +dist +.sass-cache +app/bower_components +test/bower_components +package diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..06a890e --- /dev/null +++ b/.jshintrc @@ -0,0 +1,24 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 4, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "globals" : { + "chrome": true + } +} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..d2b75f5 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,327 @@ +// Generated on 2015-01-03 using generator-chrome-extension 0.2.9 +'use strict'; + +// # Globbing +// for performance reasons we're only matching one level down: +// 'test/spec/{,*/}*.js' +// use this if you want to recursively match all subfolders: +// 'test/spec/**/*.js' + +module.exports = function (grunt) { + + // Load grunt tasks automatically + require('load-grunt-tasks')(grunt); + + // Time how long tasks take. Can help when optimizing build times + require('time-grunt')(grunt); + + // Configurable paths + var config = { + app: 'app', + dist: 'dist' + }; + + grunt.initConfig({ + + // Project settings + config: config, + + // Watches files for changes and runs tasks based on the changed files + watch: { + bower: { + files: ['bower.json'], + tasks: ['bowerInstall'] + }, + js: { + files: ['<%= config.app %>/scripts/{,*/}*.js'], + // tasks: ['jshint'], + options: { + livereload: true + } + }, + gruntfile: { + files: ['Gruntfile.js'] + }, + styles: { + files: ['<%= config.app %>/styles/{,*/}*.css'], + tasks: [], + options: { + livereload: true + } + }, + livereload: { + options: { + livereload: '<%= connect.options.livereload %>' + }, + files: [ + '<%= config.app %>/*.html', + '<%= config.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', + '<%= config.app %>/manifest.json', + '<%= config.app %>/_locales/{,*/}*.json' + ] + } + }, + + // Grunt server and debug server setting + connect: { + options: { + port: 9000, + livereload: 35729, + // change this to '0.0.0.0' to access the server from outside + hostname: 'localhost' + }, + chrome: { + options: { + open: false, + base: [ + '<%= config.app %>' + ] + } + }, + test: { + options: { + open: false, + base: [ + 'test', + '<%= config.app %>' + ] + } + } + }, + + // Empties folders to start fresh + clean: { + chrome: { + }, + dist: { + files: [{ + dot: true, + src: [ + '<%= config.dist %>/*', + '!<%= config.dist %>/.git*' + ] + }] + } + }, + + // Make sure code styles are up to par and there are no obvious mistakes + jshint: { + options: { + jshintrc: '.jshintrc', + reporter: require('jshint-stylish') + }, + all: [ + 'Gruntfile.js', + '<%= config.app %>/scripts/{,*/}*.js', + '!<%= config.app %>/scripts/vendor/*', + 'test/spec/{,*/}*.js' + ] + }, + mocha: { + all: { + options: { + run: true, + urls: ['http://localhost:<%= connect.options.port %>/index.html'] + } + } + }, + + // Automatically inject Bower components into the HTML file + bowerInstall: { + app: { + src: [ + '<%= config.app %>/*.html' + ] + } + }, + + // Reads HTML for usemin blocks to enable smart builds that automatically + // concat, minify and revision files. Creates configurations in memory so + // additional tasks can operate on them + useminPrepare: { + options: { + dest: '<%= config.dist %>' + }, + html: [ + '<%= config.app %>/popup.html', + '<%= config.app %>/options.html' + ] + }, + + // Performs rewrites based on rev and the useminPrepare configuration + usemin: { + options: { + assetsDirs: ['<%= config.dist %>', '<%= config.dist %>/images'] + }, + html: ['<%= config.dist %>/{,*/}*.html'], + css: ['<%= config.dist %>/styles/{,*/}*.css'] + }, + + // The following *-min tasks produce minifies files in the dist folder + imagemin: { + dist: { + files: [{ + expand: true, + cwd: '<%= config.app %>/images', + src: '{,*/}*.{gif,jpeg,jpg,png}', + dest: '<%= config.dist %>/images' + }] + } + }, + + svgmin: { + dist: { + files: [{ + expand: true, + cwd: '<%= config.app %>/images', + src: '{,*/}*.svg', + dest: '<%= config.dist %>/images' + }] + } + }, + + htmlmin: { + dist: { + options: { + // removeCommentsFromCDATA: true, + // collapseWhitespace: true, + // collapseBooleanAttributes: true, + // removeAttributeQuotes: true, + // removeRedundantAttributes: true, + // useShortDoctype: true, + // removeEmptyAttributes: true, + // removeOptionalTags: true + }, + files: [{ + expand: true, + cwd: '<%= config.app %>', + src: '*.html', + dest: '<%= config.dist %>' + }] + } + }, + + // By default, your `index.html`'s will take care of + // minification. These next options are pre-configured if you do not wish + // to use the Usemin blocks. + // cssmin: { + // dist: { + // files: { + // '<%= config.dist %>/styles/main.css': [ + // '<%= config.app %>/styles/{,*/}*.css' + // ] + // } + // } + // }, + // uglify: { + // dist: { + // files: { + // '<%= config.dist %>/scripts/scripts.js': [ + // '<%= config.dist %>/scripts/scripts.js' + // ] + // } + // } + // }, + // concat: { + // dist: {} + // }, + + // Copies remaining files to places other tasks can use + copy: { + dist: { + files: [{ + expand: true, + dot: true, + cwd: '<%= config.app %>', + dest: '<%= config.dist %>', + src: [ + '*.{ico,png,txt}', + 'images/{,*/}*.{webp,gif}', + '{,*/}*.html', + 'styles/{,*/}*.css', + 'styles/fonts/{,*/}*.*', + '_locales/{,*/}*.json', + ] + }] + } + }, + + // Run some tasks in parallel to speed up build process + concurrent: { + chrome: [ + ], + dist: [ + 'imagemin', + 'svgmin' + ], + test: [ + ] + }, + + // Auto buildnumber, exclude debug files. smart builds that event pages + chromeManifest: { + dist: { + options: { + buildnumber: true, + background: { + target: 'scripts/background.js', + exclude: [ + 'scripts/chromereload.js' + ] + } + }, + src: '<%= config.app %>', + dest: '<%= config.dist %>' + } + }, + + // Compres dist files to package + compress: { + dist: { + options: { + archive: function() { + var manifest = grunt.file.readJSON('app/manifest.json'); + return 'package/Chrome Tab Search-' + manifest.version + '.zip'; + } + }, + files: [{ + expand: true, + cwd: 'dist/', + src: ['**'], + dest: '' + }] + } + } + }); + + grunt.registerTask('debug', function () { + grunt.task.run([ + 'concurrent:chrome', + 'connect:chrome', + 'watch' + ]); + }); + + grunt.registerTask('test', [ + 'connect:test', + 'mocha' + ]); + + grunt.registerTask('build', [ + 'clean:dist', + 'chromeManifest:dist', + 'useminPrepare', + 'concurrent:dist', + 'cssmin', + 'concat', + 'uglify', + 'copy', + 'usemin', + 'compress' + ]); + + grunt.registerTask('default', [ + 'test', + 'build' + ]); +}; diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json new file mode 100644 index 0000000..13646a9 --- /dev/null +++ b/app/_locales/en/messages.json @@ -0,0 +1,10 @@ +{ + "appName": { + "message": "Chrome Tab Search", + "description": "The name of the application" + }, + "appDescription": { + "message": "Hot key (Cmd/Ctrl + O) that pulls up spotlight-like interface for open tabs.", + "description": "The description of the application" + } +} diff --git a/app/images/icon-128.png b/app/images/icon-128.png new file mode 100644 index 0000000..2a5c977 Binary files /dev/null and b/app/images/icon-128.png differ diff --git a/app/images/icon-16.png b/app/images/icon-16.png new file mode 100644 index 0000000..1558361 Binary files /dev/null and b/app/images/icon-16.png differ diff --git a/app/images/icon-48.png b/app/images/icon-48.png new file mode 100644 index 0000000..45f05fd Binary files /dev/null and b/app/images/icon-48.png differ diff --git a/app/manifest.json b/app/manifest.json new file mode 100644 index 0000000..a2e7e31 --- /dev/null +++ b/app/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "__MSG_appName__", + "version": "0.0.2", + "manifest_version": 2, + "description": "__MSG_appDescription__", + "icons": { + "16": "images/icon-16.png", + "48": "images/icon-48.png", + "128": "images/icon-128.png" + }, + "default_locale": "en", + "background": { + "scripts": [ + "scripts/chromereload.js", + "scripts/background.js" + ] + }, + "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'", + "content_scripts": [ + { + "matches": [ + "http://*/*", + "https://*/*" + ], + "css": [ + "styles/main.css" + ], + "js": [ + "scripts/contentscript.js" + ], + "run_at": "document_end", + "all_frames": false + } + ], + "permissions": [ + "tabs", + "http://*/*", + "https://*/*" + ] +} \ No newline at end of file diff --git a/app/scripts/background.js b/app/scripts/background.js new file mode 100644 index 0000000..6b390c1 --- /dev/null +++ b/app/scripts/background.js @@ -0,0 +1,30 @@ +'use strict'; + +console.log('Tab Search starting!'); + +var _gaq = _gaq || []; +_gaq.push(['_setAccount', 'UA-58195300-1']); +_gaq.push(['_trackPageview']); + +(function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = 'https://ssl.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); +})(); + +function handleSpotlight(request) { + var port = this; + + if (request.hasOwnProperty('query')) { + chrome.tabs.query({active: false}, function(tabs) { + chrome.tabs.sendMessage(port.sender.tab.id, {tabs: tabs}); + }); + } else if (request.hasOwnProperty('activate')) { + _gaq.push(['_trackEvent', 'Background', 'Activate']); + chrome.tabs.update(request.activate, {active: true}); + } +} + +chrome.runtime.onConnect.addListener(function(port) { + port.onMessage.addListener(handleSpotlight.bind(port)); +}); diff --git a/app/scripts/chromereload.js b/app/scripts/chromereload.js new file mode 100644 index 0000000..cb07e47 --- /dev/null +++ b/app/scripts/chromereload.js @@ -0,0 +1,22 @@ +'use strict'; + +// Reload client for Chrome Apps & Extensions. +// The reload client has a compatibility with livereload. +// WARNING: only supports reload command. + +var LIVERELOAD_HOST = 'localhost:'; +var LIVERELOAD_PORT = 35729; +var connection = new WebSocket('ws://' + LIVERELOAD_HOST + LIVERELOAD_PORT + '/livereload'); + +connection.onerror = function (error) { + console.log('reload connection got error' + JSON.stringify(error)); +}; + +connection.onmessage = function (e) { + if (e.data) { + var data = JSON.parse(e.data); + if (data && data.command === 'reload') { + chrome.runtime.reload(); + } + } +}; diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js new file mode 100644 index 0000000..ca24652 --- /dev/null +++ b/app/scripts/contentscript.js @@ -0,0 +1,148 @@ +'use strict'; + +var holder, tabList, port; +var opened = false; +var _tabs = []; + +function _listTab(tab) { + var item = document.createElement('div'); + item.classList.add('tab-item'); + + var title = document.createElement('h2'); + title.classList.add('tab--title'); + title.innerText = tab.title; + + var link = document.createElement('p'); + link.classList.add('tab--link'); + link.innerText = tab.url; + + item.appendChild(title); + item.appendChild(link); + item.tabId = tab.id; + + function navigateTab() { + _closeSearch(); + port.postMessage({activate: item.tabId}); + } + + item.addEventListener('click', navigateTab); + item.navigate = navigateTab; + + tabList.appendChild(item); +} + +function _openSearch() { + opened = true; + holder = document.createElement('div'); + holder.classList.add('tab-search'); + tabList = document.createElement('div'); + tabList.classList.add('tab-list'); + + setTimeout(function() { + holder.classList.add('fadeIn'); + }, 0); + + var input = document.createElement('input'); + + input.placeholder = 'Tab Search'; + + holder.appendChild(input); + holder.appendChild(tabList); + document.body.appendChild(holder); + + input.focus(); + + input.addEventListener('click', function(e) { + e.stopPropagation(); + }, false); + input.addEventListener('keydown', function(e) { + var val = input.value.trim(); + + port.postMessage({ + query: '' + }); + + if (e.keyCode === 27) { + return _closeSearch(); + } else if (e.keyCode === 8 && val.length === 0) { + return _closeSearch(); + } else if (e.keyCode === 13 && val.length > 0) { + var tabs = _tabs.filter(function(tab) { + return tab.url.indexOf(val) >= 0 || tab.title.trim().toLowerCase().indexOf(val) >= 0; + }); + + if (tabs.length > 0) { + tabList.firstChild.navigate(); + } + } + }); + + input.addEventListener('keyup', function() { + var val = input.value.trim().toLowerCase(); + + while (tabList.firstChild) { + tabList.removeChild(tabList.firstChild); + } + + if (val.length === 0) { + return; + } + + var tabs = _tabs.filter(function(tab) { + return tab.url.indexOf(val) >= 0 || tab.title.trim().toLowerCase().indexOf(val) >= 0; + }); + + tabs.map(_listTab); + }); +} + +function _closeSearch() { + opened = false; + + if (holder) { + holder.classList.remove('fadeIn'); + } + setTimeout(function() { + if (holder) { + holder.parentNode.removeChild(holder); + } + + holder = null; + tabList = null; + }, 250); +} + +function _listenBack() { + _closeSearch(); + document.removeEventListener('click', _listenBack); +} + +function _listen() { + document.addEventListener('keydown', function handleKeyup(e) { + if ((e.ctrlKey || e.metaKey) && e.keyCode === 79) { + e.preventDefault(); + + if (!opened) { + _openSearch(); + document.addEventListener('click', _listenBack); + } + + return false; + } + }, false); +} + +function loadTabs(response) { + _tabs = response.tabs; +} + +function TabSearch() { + port = chrome.runtime.connect({name: 'tab-search'}); + + port.postMessage({query: ''}); + port.onMessage.addListener(loadTabs); + chrome.runtime.onMessage.addListener(loadTabs); +} + +_listen(); +TabSearch(); diff --git a/app/styles/main.css b/app/styles/main.css new file mode 100644 index 0000000..c943c0c --- /dev/null +++ b/app/styles/main.css @@ -0,0 +1,74 @@ +.tab-search, +.tab-search input, +.tab-list { + box-sizing: border-box; +} +.tab-search { + position: fixed; + top: 30%; + width: 70%; + height: 70px; + margin-left: 15%; + opacity: 0; + z-index: 500; + transform: translateY(20px) scale(.85); + transition: all .25s ease-out; +} +.tab-search.fadeIn { + opacity: 1; + transform: translateY(0) scale(.9999); +} +.tab-search input, +.tab-search input:focus { + position: relative; + width: 100%; + height: 100%; + margin: 0; + padding: 8px 16px; + font-size: 38px; + line-height: normal; + background: rgba(255,255,255,.96); + border: none; + border-radius: 4px; + z-index: 300; + box-shadow: 0 5px 30px rgba(50,50,50,.85); +} +.tab-search input:focus { + outline: none; +} +.tab-list { + max-height: 400px; + overflow-y: scroll; + padding-top: 15px; +} +.tab-item { + width: 90%; + margin: 0 auto 15px; + padding: 10px 20px; + background: white; + border-radius: 4px; + text-align: left; + box-shadow: 0 0 3px rgba(0,0,0,.15); + cursor: pointer; + transition: background .45s ease-out; +} +.tab-item:hover { + background: #CFD8DC; +} +.tab--title { + margin: 15px 0 0; + font-weight: 400; + font-size: 22px; + line-height: normal; + color: #455A64; + transition: color .15s ease-out; +} +.tab-item:hover .tab--title { + color: #37474F; +} +.tab--link { + margin: 8px 0; + font-size: 14px; + line-height: 18px; + color: #78909C; +} diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..2dbc60d --- /dev/null +++ b/bower.json @@ -0,0 +1,6 @@ +{ + "name": "chrome-tab-search", + "version": "0.0.0", + "dependencies": {}, + "devDependencies": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0ed0fed --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "chrome-tab-search", + "version": "0.0.1", + "dependencies": {}, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-copy": "~0.5.0", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-uglify": "~0.4.0", + "grunt-contrib-jshint": "~0.9.2", + "grunt-contrib-cssmin": "~0.9.0", + "grunt-contrib-connect": "~0.7.1", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-htmlmin": "~0.2.0", + "grunt-bower-install": "~1.0.0", + "grunt-contrib-imagemin": "~0.7.1", + "grunt-contrib-watch": "~0.6.1", + "grunt-usemin": "~2.1.0", + "grunt-mocha": "~0.4.10", + "grunt-svgmin": "~0.4.0", + "grunt-concurrent": "~0.5.0", + "load-grunt-tasks": "~0.4.0", + "time-grunt": "~0.3.1", + "jshint-stylish": "~0.1.5", + "grunt-chrome-manifest": "~0.2.0", + "grunt-contrib-compress": "~0.9.1" + }, + "engines": { + "node": ">=0.8.0" + } +} diff --git a/test/.bowerrc b/test/.bowerrc new file mode 100644 index 0000000..44491d3 --- /dev/null +++ b/test/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} diff --git a/test/bower.json b/test/bower.json new file mode 100644 index 0000000..db2ceb5 --- /dev/null +++ b/test/bower.json @@ -0,0 +1,9 @@ +{ + "name": "tab-search", + "private": true, + "dependencies": { + "chai": "~1.8.0", + "mocha": "~1.14.0" + }, + "devDependencies": {} +} diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..d18b7d3 --- /dev/null +++ b/test/index.html @@ -0,0 +1,27 @@ + + + + + + Mocha Spec Runner + + + +
+ + + + + + + + + + + + + diff --git a/test/spec/test.js b/test/spec/test.js new file mode 100644 index 0000000..adfd614 --- /dev/null +++ b/test/spec/test.js @@ -0,0 +1,13 @@ +/* global describe, it */ + +(function () { + 'use strict'; + + describe('Give it some context', function () { + describe('maybe a bit more context here', function () { + it('should run here few assertions', function () { + + }); + }); + }); +})();