├── .gitignore ├── images ├── logo.jpg ├── download.png ├── icon19.png ├── icon48.png ├── loader.gif └── background.png ├── background.html ├── README.md ├── tests ├── index.html ├── main.js ├── test.utils.js ├── test.domain.js ├── test.services.js └── mocha.css ├── scripts ├── domain │ ├── User.js │ ├── Cue.js │ └── CueCategory.js ├── config.js ├── utils │ ├── Pageable.js │ ├── mode.js │ └── GA.js ├── core │ └── Router.js ├── view │ ├── View.js │ ├── Badge.js │ ├── Version.js │ ├── TopNavigation.js │ ├── Message.js │ ├── CuesView.js │ └── CategoriesView.js ├── background.js ├── popup.js ├── service │ ├── UserCueCategoryService.js │ ├── UserCueService.js │ ├── UserService.js │ └── CueCategoryService.js └── require.js ├── manifest.json ├── popup.html ├── dev └── build.php └── styles └── popup.css /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject 2 | .idea 3 | scripts/popup-build-*.js 4 | scripts/background-build-*.js 5 | -------------------------------------------------------------------------------- /images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/logo.jpg -------------------------------------------------------------------------------- /images/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/download.png -------------------------------------------------------------------------------- /images/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/icon19.png -------------------------------------------------------------------------------- /images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/icon48.png -------------------------------------------------------------------------------- /images/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/loader.gif -------------------------------------------------------------------------------- /background.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/background.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CueNation Google Chrome extension 2 | ============= 3 | 4 | An implementation for [CueNation API](https://github.com/dVaffection/cuenation-api) 5 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tests 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /scripts/domain/User.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function User(token) { 5 | if (! (this instanceof User)) { 6 | throw new Error('`this` must be an instance of domain.User'); 7 | } 8 | 9 | this.token = token; 10 | } 11 | 12 | return User; 13 | }); 14 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var conf = { 4 | "dev": { 5 | "api-server-url": "http://localhost:8080" 6 | }, 7 | "prod": { 8 | "api-server-url": "http://162.243.254.55:8080" 9 | } 10 | }; 11 | 12 | 13 | define(['scripts/utils/mode'], function (mode) { 14 | return conf[mode]; 15 | }); 16 | -------------------------------------------------------------------------------- /scripts/domain/Cue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function Cue(id, title, link, createdAt) { 5 | if (! (this instanceof Cue)) { 6 | throw new Error('`this` must be an instance of domain.Cue'); 7 | } 8 | 9 | this.id = id; 10 | this.title = title; 11 | this.link = link; 12 | this.createdAt = new Date(createdAt * 1000); 13 | } 14 | 15 | return Cue; 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/domain/CueCategory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function CueCategory(id, name, host, link) { 5 | if (! (this instanceof CueCategory)) { 6 | throw new Error('`this` must be an instance of domain.CueCategory'); 7 | } 8 | 9 | this.id = id; 10 | this.name = name; 11 | this.host = host; 12 | this.link = link; 13 | } 14 | 15 | return CueCategory; 16 | }); 17 | -------------------------------------------------------------------------------- /tests/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require.config({ 4 | baseUrl: '../', 5 | urlArgs: 'bust=' + (new Date()).getTime() 6 | }); 7 | 8 | require(['tests/mocha'], function () { 9 | // mocha is global 10 | mocha.setup('bdd'); 11 | mocha.checkLeaks(); 12 | 13 | require([ 14 | 'tests/test.utils', 15 | 'tests/test.domain', 16 | 'tests/test.services' 17 | ], 18 | function () { 19 | mocha.run(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /scripts/utils/Pageable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function Pageable(number, totalElements, totalPages, size) { 5 | if (! (this instanceof Pageable)) { 6 | throw new Error('`this` must be an instance of utils.Pageable'); 7 | } 8 | 9 | this.number = number; 10 | this.totalElements = totalElements; 11 | this.totalPages = totalPages; 12 | this.size = size || 10; 13 | } 14 | 15 | return Pageable; 16 | }); 17 | -------------------------------------------------------------------------------- /tests/test.utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function (require) { 4 | 5 | var chai = require('tests/chai'); 6 | var Pageable = require('scripts/utils/Pageable'); 7 | 8 | describe('utils', function () { 9 | describe('Pageable', function () { 10 | it('correct object', function () { 11 | var pageable = new Pageable(0, 10, 5); 12 | 13 | chai.assert.equal(pageable.number, 0); 14 | chai.assert.equal(pageable.totalElements, 10); 15 | chai.assert.equal(pageable.totalPages, 5); 16 | chai.assert.equal(pageable.size, 10); 17 | }); 18 | }); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /scripts/core/Router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function Router(cuesView, categoriesView) { 5 | if (! (this instanceof Router)) { 6 | throw new Error('`this` must be an instance of core.Router'); 7 | } 8 | 9 | var routes = { 10 | "cues": cuesView.render, 11 | "categories": categoriesView.render 12 | }; 13 | 14 | /** 15 | * @param {String} route 16 | * @returns {Function} 17 | */ 18 | this.getCallback = function (route) { 19 | if (! routes[route]) { 20 | throw new Error('Unrecognized route "' + route + '"'); 21 | } 22 | 23 | return routes[route]; 24 | } 25 | } 26 | 27 | return Router; 28 | }); 29 | -------------------------------------------------------------------------------- /scripts/view/View.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function View(container) { 5 | if (! (this instanceof View)) { 6 | throw new Error('`this` must be an instance of view.View'); 7 | } 8 | 9 | this.renderLoader = function () { 10 | var loader = document.createElement('img'); 11 | loader.setAttribute('src', 'images/loader.gif'); 12 | loader.setAttribute('width', '32'); 13 | loader.setAttribute('height', '32'); 14 | loader.setAttribute('id', 'loader'); 15 | 16 | container.innerHTML = ''; 17 | container.appendChild(loader); 18 | } 19 | 20 | this.render = function () { 21 | throw new Error('Not implemented!'); 22 | } 23 | } 24 | 25 | return View; 26 | }); 27 | -------------------------------------------------------------------------------- /scripts/view/Badge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function Badge(userCueService) { 5 | if (!(this instanceof Badge)) { 6 | throw new Error('`this` must be an instance of view.Badge'); 7 | } 8 | 9 | this.render = function (user) { 10 | userCueService.get(user.token, 0, function (err, cues, pageable) { 11 | var text; 12 | if (err) { 13 | text = 'err'; 14 | } else { 15 | text = pageable.totalElements ? pageable.totalElements.toString() : ''; 16 | } 17 | 18 | chrome.browserAction.setBadgeText({text: text}); 19 | chrome.browserAction.setBadgeBackgroundColor({color: "#807A60"}); 20 | }); 21 | } 22 | } 23 | 24 | return Badge; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/utils/mode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function getMode() { 4 | var mode; 5 | 6 | if (!mode) { 7 | var xhr = new XMLHttpRequest(); 8 | xhr.open("GET", chrome.runtime.getURL('manifest.json'), false); 9 | xhr.send(); 10 | 11 | if (xhr.status === 200) { 12 | var json = JSON.parse(xhr.responseText); 13 | // Chrome Web Store adds `update_url` when you upload your extension. 14 | // `http://stackoverflow.com/a/12833511/407986` 15 | mode = 'update_url' in json ? "prod" : "dev"; 16 | } 17 | } 18 | 19 | return mode; 20 | } 21 | 22 | define(function () { 23 | return getMode(); 24 | }); 25 | 26 | //define(function (callback) { 27 | // chrome.management.get(chrome.runtime.id, function (extensionInfo) { 28 | // var env = extensionInfo.installType === 'development' ? 'dev' : 'prod'; 29 | // 30 | // callback(conf[env]); 31 | // }); 32 | //}); -------------------------------------------------------------------------------- /scripts/utils/GA.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function GA(mode) { 4 | if (!(this instanceof GA)) { 5 | throw new Error('`this` must be an instance of utils.GA'); 6 | } 7 | 8 | if ('prod' === mode) { 9 | _gaq.push(['_setAccount', 'UA-10762441-2']); 10 | } 11 | 12 | this.trackPageview = function () { 13 | if ('prod' === mode) { 14 | _gaq.push(['_trackPageview']); 15 | } 16 | } 17 | 18 | this.trackClickEvent = function (name) { 19 | if ('prod' === mode) { 20 | _gaq.push(['_trackEvent', name, 'clicked']); 21 | } 22 | } 23 | } 24 | 25 | 26 | define(['google-analytics', 'scripts/utils/mode'], function (_, mode) { 27 | 28 | var ga; 29 | 30 | return { 31 | getInstance: function () { 32 | if (!(ga instanceof GA)) { 33 | ga = new GA(mode); 34 | } 35 | 36 | return ga; 37 | } 38 | }; 39 | 40 | }); -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CueNation", 3 | "version": "0.3.1", 4 | "manifest_version": 2, 5 | "description": "Let you subscribe and track new cue files of your desired radio shows on CueNation.com.", 6 | "icons": { 7 | "48": "images/icon48.png" 8 | }, 9 | "browser_action": { 10 | "default_icon": "images/icon19.png", 11 | "default_title": "CueNation", 12 | "default_popup": "popup.html" 13 | }, 14 | "background": { 15 | "page": "background.html" 16 | }, 17 | "permissions": [ 18 | "storage", 19 | "http://localhost:8080/*", 20 | "http://162.243.254.55:8080/*" 21 | ], 22 | "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'", 23 | "web_accessible_resources": [ 24 | "manifest.json" 25 | ] 26 | } -------------------------------------------------------------------------------- /scripts/view/Version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function Version() { 5 | if (!(this instanceof Version)) { 6 | throw new Error('`this` must be an instance of view.Version'); 7 | } 8 | 9 | function getVersion(callback) { 10 | var req = new XMLHttpRequest(); 11 | req.open("GET", chrome.runtime.getURL('manifest.json'), true); 12 | req.onreadystatechange = function () { 13 | if (4 === req.readyState) { 14 | if (200 === req.status) { 15 | var json = JSON.parse(req.responseText); 16 | 17 | callback(json.version); 18 | } 19 | } 20 | } 21 | req.send(); 22 | } 23 | 24 | getVersion(function (version) { 25 | var container = document.getElementById('version'); 26 | container.innerText = version; 27 | }); 28 | } 29 | 30 | return Version; 31 | }); 32 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 | 31 | 32 |
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /scripts/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require.config({ 4 | baseUrl: '..', 5 | paths: { 6 | "google-analytics": [ 7 | 'https://ssl.google-analytics.com/ga' 8 | ] 9 | } 10 | }); 11 | 12 | require([ 13 | 'scripts/utils/GA', 14 | 'scripts/service/UserService', 15 | 'scripts/service/UserCueService', 16 | 'scripts/view/Badge' 17 | ], 18 | function (GA, UserService, UserCueService, Badge) { 19 | // var ga = GA.getInstance(); 20 | // ga.trackPageview(); 21 | 22 | 23 | function start(err, user) { 24 | var userCueService = new UserCueService(), 25 | badge = new Badge(userCueService); 26 | 27 | badge.render(user); 28 | // once a minute is enough 29 | setInterval(function (badge, user) { 30 | badge.render(user); 31 | }, 60000, badge, user); 32 | } 33 | 34 | // "8c9f8cf4-1689-48ab-bf53-ee071a377f60" 35 | var userService = new UserService(); 36 | userService.init(chrome.storage.sync, start); 37 | }); 38 | -------------------------------------------------------------------------------- /scripts/view/TopNavigation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function TopNavigation(router, ga) { 5 | if (!(this instanceof TopNavigation)) { 6 | throw new Error('`this` must be an instance of view.TopNavigation'); 7 | } 8 | 9 | var forEach = Array.prototype.forEach; 10 | // listen to navigation links click event 11 | forEach.call(document.getElementById('menu').querySelectorAll('a.inner'), function (el) { 12 | el.addEventListener('click', function (e) { 13 | e.preventDefault(); 14 | 15 | // track clicks with GA 16 | ga.trackClickEvent(this.getAttribute('href')); 17 | 18 | 19 | forEach.call(document.getElementById('menu').children, function (el) { 20 | el.removeAttribute('class'); 21 | }); 22 | el.parentNode.setAttribute('class', 'active'); 23 | 24 | var callback = router.getCallback(this.getAttribute('href')); 25 | callback(); 26 | }); 27 | }); 28 | 29 | window.addEventListener('scroll', function () { 30 | var menu = document.getElementById('menu'); 31 | 32 | if (document.body.scrollTop >= 110) { 33 | menu.setAttribute('class', 'clear menu-fixed-to-top'); 34 | } else { 35 | menu.setAttribute('class', 'clear '); 36 | } 37 | }); 38 | } 39 | 40 | return TopNavigation; 41 | }); 42 | -------------------------------------------------------------------------------- /scripts/view/Message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function () { 4 | function Message() { 5 | if (! (this instanceof Message)) { 6 | throw new Error('`this` must be an instance of view.Message'); 7 | } 8 | 9 | var WINDOW_WIDTH = 630; 10 | 11 | this.show = function (status, message) { 12 | var messageEl = document.getElementById('flash-message'); 13 | messageEl.setAttribute('class', status); 14 | messageEl.innerText = message; 15 | messageEl.style.display = 'block'; 16 | 17 | // center messageEl depending on its width 18 | var leftOffset = WINDOW_WIDTH / 2 - Math.round(messageEl.offsetWidth / 2); 19 | messageEl.style.left = leftOffset + 'px'; 20 | // and always display on top regardless of the scroll position 21 | messageEl.style.top = document.body.scrollTop + 'px'; 22 | 23 | 24 | setTimeout(function () { 25 | messageEl.innerText = ''; 26 | messageEl.setAttribute('class', ''); 27 | messageEl.style.display = 'none'; 28 | }, 3000); 29 | 30 | window.addEventListener('scroll', function () { 31 | var messageEl = document.getElementById('flash-message'); 32 | if (messageEl.style.display !== 'none') { 33 | messageEl.style.top = document.body.scrollTop + 'px'; 34 | } 35 | }); 36 | } 37 | } 38 | 39 | Message.status = { 40 | ERROR: 'error', 41 | INFO: 'info' 42 | }; 43 | 44 | return Message; 45 | }); 46 | -------------------------------------------------------------------------------- /tests/test.domain.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function (require) { 4 | 5 | var chai = require('tests/chai'); 6 | var User = require('scripts/domain/User'), 7 | Cue = require('scripts/domain/Cue'), 8 | CueCategory = require('scripts/domain/CueCategory'); 9 | 10 | describe('domain', function () { 11 | describe('User', function () { 12 | it('correct object', function () { 13 | var user = new User('8c9f8cf4-1689-48ab-bf53-ee071a377f60'); 14 | 15 | chai.assert.equal(user.token, '8c9f8cf4-1689-48ab-bf53-ee071a377f60'); 16 | }); 17 | }); 18 | describe('Cue', function () { 19 | it('correct object', function () { 20 | var id = '53ef0f3844ae8cebf6152396', 21 | title = 'Bryan Kearney - KEARNAGE 060 (2014-08-05)', 22 | link = 'http://cuenation.com?page=tracklist&folder=kearnage&filename=Bryan+Kearney+-+KEARNAGE+060.cue', 23 | createdAt = 1407353640; 24 | 25 | var cue = new Cue(id, title, link, createdAt); 26 | 27 | chai.assert.equal(cue.id, id); 28 | chai.assert.equal(cue.title, title); 29 | chai.assert.equal(cue.link, link); 30 | chai.assert.instanceOf(cue.createdAt, Date); 31 | // yes, in milliseconds 32 | chai.assert.equal(cue.createdAt.getTime(), createdAt * 1000); 33 | }); 34 | }); 35 | describe('CueCategory', function () { 36 | it('correct object', function () { 37 | var id = '53e5bc5b837125a9f6149e4b', 38 | name = '#goldrushRADIO', 39 | host = 'with Ben Gold', 40 | link = 'http://cuenation.com/?page=cues&folder=goldrushradio'; 41 | 42 | var cueCategory = new CueCategory(id, name, host, link); 43 | 44 | chai.assert.equal(cueCategory.id, id); 45 | chai.assert.equal(cueCategory.name, name); 46 | chai.assert.equal(cueCategory.host, host); 47 | chai.assert.equal(cueCategory.link, link); 48 | }); 49 | }); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /scripts/popup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require.config({ 4 | baseUrl: '..', 5 | paths: { 6 | "google-analytics": [ 7 | 'https://ssl.google-analytics.com/ga' 8 | ] 9 | } 10 | }); 11 | 12 | require([ 13 | 'scripts/utils/GA', 14 | 'scripts/domain/User', 15 | 'scripts/core/Router', 16 | 'scripts/view/Version', 17 | 'scripts/view/TopNavigation', 18 | 'scripts/view/Message', 19 | 'scripts/view/CuesView', 20 | 'scripts/view/CategoriesView', 21 | 'scripts/service/UserService', 22 | 'scripts/service/UserCueService', 23 | 'scripts/service/CueCategoryService', 24 | 'scripts/service/UserCueCategoryService' 25 | ], 26 | function (GA, User, Router, Version, TopNavigation, MessageView, CuesView, CategoriesView, UserService, UserCueService, CueCategoryService, UserCueCategoryService) { 27 | function start(err, user) { 28 | var ga = GA.getInstance(); 29 | ga.trackPageview(); 30 | 31 | 32 | var versionView = new Version(), 33 | messageView = new MessageView(), 34 | userCueService = new UserCueService(), 35 | cueCategoryService = new CueCategoryService(), 36 | userCueCategoryService = new UserCueCategoryService(), 37 | container = document.getElementsByClassName('page-content').item(0); 38 | 39 | 40 | var cuesView = new CuesView(container, messageView, user, userCueService, userCueCategoryService), 41 | categoriesView = new CategoriesView(container, messageView, user, userCueService, cueCategoryService, userCueCategoryService), 42 | router = new Router(cuesView, categoriesView), 43 | nav = new TopNavigation(router, ga); 44 | 45 | 46 | if (err) { 47 | messageView.show('error', err); 48 | } else { 49 | // manually click first page 50 | document.querySelector('#menu .landing').click(); 51 | } 52 | } 53 | 54 | 55 | // "8c9f8cf4-1689-48ab-bf53-ee071a377f60" 56 | var userService = new UserService(); 57 | userService.init(chrome.storage.sync, start); 58 | }); 59 | -------------------------------------------------------------------------------- /scripts/service/UserCueCategoryService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['scripts/config', 'scripts/domain/CueCategory'], function (config, CueCategory) { 4 | function UserCueCategoryService() { 5 | if (! (this instanceof UserCueCategoryService)) { 6 | throw new Error('`this` must be an instance of service.UserCueCategoryService'); 7 | } 8 | 9 | var userCueCategoryService = this; 10 | var baseUrl = config['api-server-url']; 11 | 12 | /** 13 | * @param {String} token 14 | * @param {Function} callback({Error}, {domain.CueCategory[]}) 15 | */ 16 | this.get = function (token, callback) { 17 | var req = new XMLHttpRequest(); 18 | req.open('GET', baseUrl + '/user-tokens/' + token + '/cue-categories', true); 19 | req.onreadystatechange = function () { 20 | if (4 === req.readyState) { 21 | if (200 === req.status) { 22 | var response = JSON.parse(req.response); 23 | var cueCategories = []; 24 | 25 | var cueCategoriesData = response._embedded && response._embedded.userCueCategories || []; 26 | for (var i = 0; i < cueCategoriesData.length; i ++) { 27 | cueCategories[i] = new CueCategory( 28 | cueCategoriesData[i].id, 29 | cueCategoriesData[i].name, 30 | cueCategoriesData[i].host, 31 | cueCategoriesData[i].link); 32 | } 33 | 34 | callback(null, cueCategories); 35 | } else { 36 | callback(Error(req.statusText)); 37 | } 38 | } 39 | } 40 | req.send(null); 41 | } 42 | 43 | /** 44 | * @param {String} token 45 | * @param {String[]} ids 46 | * @param {Function} callback({Error}) 47 | */ 48 | this.put = function (token, ids, callback) { 49 | var req = new XMLHttpRequest(); 50 | req.open('PUT', baseUrl + '/user-tokens/' + token + '/cue-categories', true); 51 | req.onreadystatechange = function () { 52 | if (4 === req.readyState) { 53 | if (200 === req.status) { 54 | callback(null); 55 | } else { 56 | callback(Error(req.statusText)); 57 | } 58 | } 59 | } 60 | req.setRequestHeader('Content-Type', 'application/json'); 61 | req.send(JSON.stringify({ids: ids})); 62 | }; 63 | } 64 | 65 | return UserCueCategoryService; 66 | }); 67 | -------------------------------------------------------------------------------- /scripts/service/UserCueService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['scripts/config', 'scripts/domain/Cue', 'scripts/utils/Pageable'], function (config, Cue, Pageable) { 4 | function UserCueService() { 5 | if (! (this instanceof UserCueService)) { 6 | throw new Error('`this` must be an instance of service.UserCueService'); 7 | } 8 | 9 | var userCueService = this; 10 | var baseUrl = config['api-server-url']; 11 | 12 | /** 13 | * @param {String} token 14 | * @param {Number} page 15 | * @param {Function} callback({Error}, {domain.Cue[]}, {utils.Pageable}) 16 | */ 17 | this.get = function (token, page, callback) { 18 | var req = new XMLHttpRequest(); 19 | req.open('GET', baseUrl + '/user-tokens/' + token + '/cues?page=' + page + '&size=50', true); 20 | req.onreadystatechange = function () { 21 | if (4 === req.readyState) { 22 | if (200 === req.status) { 23 | var response = JSON.parse(req.response); 24 | var cues = []; 25 | 26 | var cuesData = response._embedded && response._embedded.userCues || []; 27 | for (var i = 0; i < cuesData.length; i ++) { 28 | cues[i] = new Cue(cuesData[i].id, cuesData[i].title, cuesData[i].link, 29 | cuesData[i].createdAt); 30 | } 31 | 32 | var pageable = new Pageable(response.page.number, response.page.totalElements, 33 | response.page.totalPages, response.page.size); 34 | 35 | callback(null, cues, pageable); 36 | } else { 37 | callback(Error(req.statusText)); 38 | } 39 | } 40 | } 41 | req.send(null); 42 | } 43 | 44 | /** 45 | * @param {String} token 46 | * @param {String[]} ids 47 | * @param {Function} callback({Error}) 48 | */ 49 | this.put = function (token, ids, callback) { 50 | var req = new XMLHttpRequest(); 51 | req.open('PUT', baseUrl + '/user-tokens/' + token + '/cues', true); 52 | req.onreadystatechange = function () { 53 | if (4 === req.readyState) { 54 | if (200 === req.status) { 55 | callback(null); 56 | } else { 57 | callback(Error(req.statusText)); 58 | } 59 | } 60 | } 61 | req.setRequestHeader('Content-Type', 'application/json'); 62 | req.send(JSON.stringify({ids: ids})); 63 | }; 64 | } 65 | 66 | return UserCueService; 67 | }); 68 | -------------------------------------------------------------------------------- /scripts/service/UserService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['scripts/config', 'scripts/domain/User'], function (config, User) { 4 | function UserService() { 5 | if (! (this instanceof UserService)) { 6 | throw new Error('`this` must be an instance of service.UserService'); 7 | } 8 | 9 | var userService = this; 10 | var baseUrl = config['api-server-url']; 11 | 12 | /** 13 | * @param {Function} callback({Error}, {domain.User}) 14 | */ 15 | this.post = function (callback) { 16 | var req = new XMLHttpRequest(); 17 | req.open('POST', baseUrl + '/user-tokens', true); 18 | req.onreadystatechange = function () { 19 | if (4 === req.readyState) { 20 | if (201 === req.status) { 21 | var url = req.getResponseHeader('Location'); 22 | var re = /([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})/; 23 | if (null === url.match(re)) { 24 | callback(Error("Can not find user token in the URL: " + url)); 25 | } else { 26 | var token = url.match(re)[0]; 27 | userService.get(token, callback); 28 | } 29 | } else { 30 | callback(Error(req.statusText)); 31 | } 32 | } 33 | }; 34 | req.send(null); 35 | } 36 | 37 | /** 38 | * @param {String} token 39 | * @param {Function} callback({Error}, {domain.User}) 40 | */ 41 | this.get = function (token, callback) { 42 | var req = new XMLHttpRequest(); 43 | req.open('GET', baseUrl + '/user-tokens/' + token, true); 44 | req.onreadystatechange = function () { 45 | if (4 === req.readyState) { 46 | if (200 === req.status) { 47 | var response = JSON.parse(req.response); 48 | var user = new User(response.token); 49 | callback(null, user); 50 | } else { 51 | callback(Error(req.statusText)); 52 | } 53 | } 54 | } 55 | req.send(null); 56 | } 57 | 58 | /** 59 | * @param {Object} storage - chrome.storage.sync (see https://developer.chrome.com/extensions/storage) 60 | * @param {Function} callback({Error}, {domain.User}) 61 | */ 62 | this.init = function (storage, callback) { 63 | var user; 64 | 65 | storage.get('token', function (obj) { 66 | if (obj.token) { 67 | user = new User(obj.token); 68 | callback(null, user); 69 | } else { 70 | userService.post(function (err, user) { 71 | if (err) { 72 | callback(err); 73 | } else { 74 | storage.set({"token": user.token}, function () { 75 | callback(null, user); 76 | }); 77 | } 78 | }); 79 | } 80 | }); 81 | } 82 | } 83 | 84 | return UserService; 85 | }); 86 | 87 | -------------------------------------------------------------------------------- /dev/build.php: -------------------------------------------------------------------------------- 1 | ', $argv[0]), PHP_EOL; 11 | exit; 12 | } 13 | 14 | $outputDir = rtrim($argv[1], '/'); 15 | 16 | createBuild(); 17 | copyFiles($outputDir); 18 | alterContent($outputDir); 19 | removeBuild(); 20 | 21 | 22 | function isRoot() 23 | { 24 | return file_exists(getcwd() . '/manifest.json'); 25 | } 26 | 27 | function execute($cmd) 28 | { 29 | $handle = popen($cmd, 'r'); 30 | if (false !== $handle) { 31 | while (false !== ($buffer = fgets($handle))) { 32 | echo $buffer; 33 | } 34 | $statusCode = pclose($handle); 35 | if (0 !== $statusCode) { 36 | echo 'Error while opening process file pointer, status code: ', $statusCode, PHP_EOL; 37 | exit(1); 38 | } 39 | } else { 40 | echo 'Failed to execute cmd: "', $cmd, '"', PHP_EOL; 41 | exit(1); 42 | } 43 | } 44 | 45 | function createBuild() 46 | { 47 | $cmd = sprintf('r.js -o baseUrl=. name=scripts/popup out=scripts/popup-build-%s.js paths.google-analytics=empty:', date('Y-m-d')); 48 | execute($cmd); 49 | 50 | $cmd = sprintf( 51 | 'r.js -o baseUrl=. cssIn=styles/popup.css out=styles/popup-build-%s.css optimizeCss=standard', date('Y-m-d') 52 | ); 53 | execute($cmd); 54 | 55 | $cmd = sprintf('r.js -o baseUrl=. name=scripts/background out=scripts/background-build-%s.js paths.google-analytics=empty:', date('Y-m-d')); 56 | execute($cmd); 57 | } 58 | 59 | function copyFiles($outputDir) 60 | { 61 | // images 62 | $cmd = sprintf('rm -rf %s/*', $outputDir); 63 | execute($cmd); 64 | 65 | $cmd = 'cp -r images ' . $outputDir; 66 | execute($cmd); 67 | 68 | // styles 69 | $cmd = sprintf('mkdir %s/styles', $outputDir); 70 | execute($cmd); 71 | 72 | $cmd = sprintf('cp styles/popup-build-%s.css %s/styles/', date('Y-m-d'), $outputDir); 73 | execute($cmd); 74 | 75 | // scripts 76 | $cmd = sprintf('mkdir %s/scripts', $outputDir); 77 | execute($cmd); 78 | 79 | $cmd = sprintf('cp scripts/require.js %s/scripts/', $outputDir); 80 | execute($cmd); 81 | 82 | $cmd = sprintf('cp scripts/popup-build-%s.js %s/scripts/', date('Y-m-d'), $outputDir); 83 | execute($cmd); 84 | 85 | $cmd = sprintf('cp scripts/background-build-%s.js %s/scripts/', date('Y-m-d'), $outputDir); 86 | execute($cmd); 87 | 88 | // the rest 89 | $cmd = 'cp popup.html ' . $outputDir; 90 | execute($cmd); 91 | 92 | $cmd = 'cp background.html ' . $outputDir; 93 | execute($cmd); 94 | 95 | $cmd = 'cp manifest.json ' . $outputDir; 96 | execute($cmd); 97 | } 98 | 99 | function alterContent($outputDir) 100 | { 101 | $change = function ($filename, $searchContent, $replaceContent) { 102 | $content = file_get_contents($filename); 103 | $content = str_replace($searchContent, $replaceContent, $content); 104 | file_put_contents($filename, $content); 105 | }; 106 | 107 | 108 | $change( 109 | $outputDir . '/popup.html', 'data-main="scripts/popup"', 110 | sprintf('data-main="scripts/popup-build-%s"', date('Y-m-d')) 111 | ); 112 | 113 | $change( 114 | $outputDir . '/popup.html', 'href="styles/popup.css"', 115 | sprintf('href="styles/popup-build-%s.css"', date('Y-m-d')) 116 | ); 117 | 118 | $change( 119 | $outputDir . '/background.html', 'data-main="scripts/background"', 120 | sprintf('data-main="scripts/background-build-%s"', date('Y-m-d')) 121 | ); 122 | 123 | // let's put it aside as we can't properly test packed extensions locally 124 | // $change($outputDir . '/manifest.json', '"http://localhost:8080/*",', ''); 125 | } 126 | 127 | function removeBuild() 128 | { 129 | $cmd = sprintf('rm scripts/popup-build-%s.js', date('Y-m-d')); 130 | execute($cmd); 131 | 132 | $cmd = sprintf('rm styles/popup-build-%s.css', date('Y-m-d')); 133 | execute($cmd); 134 | 135 | $cmd = sprintf('rm scripts/background-build-%s.js', date('Y-m-d')); 136 | execute($cmd); 137 | } 138 | -------------------------------------------------------------------------------- /scripts/service/CueCategoryService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['scripts/config', 'scripts/domain/CueCategory'], function (config, CueCategory) { 4 | function CueCategoryService() { 5 | if (!(this instanceof CueCategoryService)) { 6 | throw new Error('`this` must be an instance of service.CueCategoryService'); 7 | } 8 | 9 | var cueCategoryService = this; 10 | var baseUrl = config['api-server-url']; 11 | 12 | /** 13 | * @param {Function} callback({Error}, {domain.CueCategory[]}) 14 | */ 15 | this.get = function (callback) { 16 | var storage = chrome.storage.local; 17 | 18 | storage.get('cue-categories-e-tag', function (obj) { 19 | var eTag = ''; 20 | if (obj['cue-categories-e-tag']) { 21 | eTag = obj['cue-categories-e-tag']; 22 | } 23 | 24 | get(eTag, function (err, cueCategories, eTag) { 25 | if (err) { 26 | callback(err); 27 | // categories changes, let's re-cache them along with the new ETag 28 | } else if (cueCategories.length) { 29 | if (eTag) { 30 | setCache(cueCategories, eTag, function () { 31 | callback(null, cueCategories); 32 | }); 33 | } else { 34 | console.error('ETag was not set'); 35 | callback(null, cueCategories); 36 | } 37 | } else { 38 | // if categories didn't come then they didn't change, let's fetch them from cache 39 | storage.get('cue-categories', function (obj) { 40 | if (obj['cue-categories']) { 41 | callback(null, obj['cue-categories']); 42 | } else { 43 | // though if for some reason they don't exist in our cache let's fetch them from the server 44 | get('', function (err, cueCategories) { 45 | if (err) { 46 | callback(err); 47 | } else { 48 | setCache(cueCategories, eTag, function () { 49 | callback(null, cueCategories); 50 | }); 51 | } 52 | }); 53 | } 54 | }); 55 | } 56 | }); 57 | }); 58 | 59 | 60 | function setCache(cueCategories, eTag, callback) { 61 | storage.set({'cue-categories-e-tag': eTag}, function () { 62 | storage.set({'cue-categories': cueCategories}, function () { 63 | callback(); 64 | }); 65 | }); 66 | } 67 | 68 | /** 69 | * @param {String} ifNonMatch 70 | * @param {Function} callback({Error}, {domain.CueCategory[]}, {String}) 71 | */ 72 | function get(ifNonMatch, callback) { 73 | var req = new XMLHttpRequest(); 74 | req.open('GET', baseUrl + '/cue-categories', true); 75 | req.setRequestHeader('If-None-Match', ifNonMatch); 76 | req.onreadystatechange = function () { 77 | if (4 === req.readyState) { 78 | var cueCategories = [], 79 | eTag = null; 80 | 81 | if (200 === req.status) { 82 | eTag = req.getResponseHeader('ETag'); 83 | 84 | var response = JSON.parse(req.response); 85 | var cueCategoriesData = response._embedded && response._embedded.cueCategories || []; 86 | for (var i = 0; i < cueCategoriesData.length; i++) { 87 | cueCategories[i] = new CueCategory( 88 | cueCategoriesData[i].id, 89 | cueCategoriesData[i].name, 90 | cueCategoriesData[i].host, 91 | cueCategoriesData[i].link); 92 | } 93 | 94 | callback(null, cueCategories, eTag); 95 | } else if (304 === req.status) { 96 | callback(null, cueCategories, eTag); 97 | } else { 98 | callback(Error(req.statusText)); 99 | } 100 | } 101 | }; 102 | req.send(null); 103 | } 104 | } 105 | } 106 | 107 | return CueCategoryService; 108 | }); 109 | -------------------------------------------------------------------------------- /tests/test.services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(function (require) { 4 | // mocha.timeout(0); 5 | 6 | var chai = require('tests/chai'); 7 | var User = require('scripts/domain/User'), 8 | Cue = require('scripts/domain/Cue'), 9 | CueCategory = require('scripts/domain/CueCategory'), 10 | UserService = require('scripts/service/UserService'), 11 | CueCategoryService = require('scripts/service/CueCategoryService'), 12 | UserCueCategoryService = require('scripts/service/UserCueCategoryService'), 13 | UserCueService = require('scripts/service/UserCueService'); 14 | 15 | describe('service', function () { 16 | var token, 17 | cueCategoryIds = []; 18 | 19 | describe('CueCategoryService', function () { 20 | it('get', function (done) { 21 | 22 | var cueCategoryService = new CueCategoryService(); 23 | cueCategoryService.get(function (err, cueCategories) { 24 | chai.assert.isNull(err, err); 25 | chai.assert.isArray(cueCategories); 26 | 27 | // after json serialization CueCategory object becomes a plain hash 28 | // chai.assert.instanceOf(cueCategories[0], CueCategory); 29 | 30 | for (var i = 0; i < 5; i++) { 31 | cueCategoryIds[i] = cueCategories[i].id; 32 | } 33 | 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('UserService', function () { 40 | it('post and get', function (done) { 41 | var userService = new UserService(); 42 | userService.post(function (err, user) { 43 | chai.assert.isNull(err, err); 44 | chai.assert.instanceOf(user, User); 45 | 46 | token = user.token; 47 | 48 | userService.get(user.token, function (err, user) { 49 | chai.assert.isNull(err, err); 50 | chai.assert.instanceOf(user, User); 51 | 52 | done(); 53 | }); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('UserCueCategoryService', function () { 59 | it('put and get', function (done) { 60 | 61 | var userCueCategoryService = new UserCueCategoryService(); 62 | 63 | userCueCategoryService.put(token, cueCategoryIds, function (err) { 64 | chai.assert.isNull(err, err); 65 | 66 | userCueCategoryService.get(token, function (err, cueCategories) { 67 | chai.assert.isNull(err, err); 68 | chai.assert.isArray(cueCategories); 69 | 70 | var mustBeEmpty = cueCategoryIds; 71 | var index; 72 | for (var i = 0; i < cueCategories.length; i++) { 73 | index = mustBeEmpty.indexOf(cueCategories[i].id); 74 | if (-1 === index) { 75 | chai.assert.ok(false); 76 | } else { 77 | mustBeEmpty.splice(index, 1); 78 | } 79 | } 80 | 81 | chai.assert(0 === mustBeEmpty.length); 82 | 83 | 84 | done(); 85 | }); 86 | }); 87 | 88 | }); 89 | }); 90 | 91 | describe('UserCueService', function () { 92 | it('get and put', function (done) { 93 | 94 | var userCueService = new UserCueService(); 95 | 96 | userCueService.get(token, 0, function (err, cues) { 97 | chai.assert.isNull(err, err); 98 | chai.assert.isArray(cues); 99 | 100 | var initialNumberOfCues = cues.length; 101 | if (initialNumberOfCues < 2) { 102 | chai.assert(false, 'Can not test `userCueService.put` as we don\'t obtain enough cues ' + 103 | 'from the server in the first place'); 104 | } 105 | 106 | var cueIds = [cues[0].id]; 107 | userCueService.put(token, cueIds, function (err) { 108 | chai.assert.isNull(err, err); 109 | 110 | userCueService.get(token, 0, function (err, cues) { 111 | chai.assert.isNull(err, err); 112 | 113 | for (var i = 0; i < cues.length; i++) { 114 | if (cueIds.indexOf(cues[i].id) > -1) { 115 | chai.assert(false, 'We got a cue from server which we marked as viewed!'); 116 | } 117 | } 118 | 119 | done(); 120 | }); 121 | }); 122 | }); 123 | }); 124 | }); 125 | }); 126 | 127 | }); 128 | 129 | -------------------------------------------------------------------------------- /styles/popup.css: -------------------------------------------------------------------------------- 1 | .clear:after { 2 | content: " "; 3 | display: block; 4 | height: 0; 5 | clear: both; 6 | overflow: hidden; 7 | visibility: hidden; 8 | } 9 | 10 | body { 11 | background: #FDF5EA url("../images/background.png"); 12 | font-family: Verdana, Sans-serif; 13 | font-size: 12px; 14 | color: #333333; 15 | } 16 | 17 | h1 { 18 | font-size: 18px; 19 | margin: 0; 20 | } 21 | 22 | ul { 23 | margin: 0; 24 | padding: 0; 25 | } 26 | 27 | #flash-message { 28 | position: absolute; 29 | padding: 5px; 30 | display: none; 31 | z-index: 1; 32 | } 33 | 34 | #flash-message.info { 35 | color: #C09853; 36 | background-color: #FCF8E3; 37 | border: 1px solid #FBEED5; 38 | } 39 | 40 | #flash-message.error { 41 | color: #c01711; 42 | background-color: #fcd3db; 43 | border: 1px solid #fb879b; 44 | } 45 | 46 | #wrapper { 47 | background-color: #FFFFFF; 48 | padding: 5px; 49 | width: 600px; 50 | box-shadow: 5px 5px 5px #888888; 51 | } 52 | 53 | #version { 54 | position: absolute; 55 | top: 20px; 56 | left: 570px; 57 | padding-right: 1px; 58 | font-weight: bold; 59 | color: #807A60; 60 | box-shadow: 2px 2px 5px #888888; 61 | } 62 | 63 | #logo { 64 | margin: 0; 65 | display: block; 66 | } 67 | 68 | #menu-wrapper { 69 | height: 44px; 70 | } 71 | 72 | #menu { 73 | display: block; 74 | margin: 0; 75 | padding: 5px 0; 76 | width: 600px; 77 | background-color: #FFFFFF; 78 | border-bottom: solid 1px #EADDBB; 79 | } 80 | 81 | #menu li { 82 | float: left; 83 | list-style: none; 84 | background-color: #B7AF9C; 85 | border: 1px solid #000; 86 | margin-right: 5px; 87 | } 88 | 89 | #menu li a { 90 | cursor: pointer; 91 | display: block; 92 | background-color: #807A60; 93 | padding: 7px 15px; 94 | margin: 1px; 95 | color: #EDE8D4; 96 | text-decoration: none; 97 | font-size: 12px; 98 | font-weight: bold; 99 | } 100 | 101 | #menu li.active { 102 | background-color: #807A60; 103 | } 104 | 105 | #menu li.active a { 106 | background-color: #f3eeda; 107 | color: #807A60; 108 | } 109 | 110 | .menu-fixed-to-top { 111 | position: fixed; 112 | top: 0; 113 | } 114 | 115 | .page-content { 116 | width: 588px; 117 | border: solid 1px #EADDBB; 118 | border-top: none; 119 | margin: 0 0 15px 0; 120 | padding: 5px 5px 5px 5px; 121 | background-color: #f3eeda; 122 | } 123 | 124 | #loader { 125 | margin: 30px 0 30px 269px; 126 | } 127 | 128 | #no-cues { 129 | text-align: center; 130 | margin: 30px auto; 131 | color: #807a60; 132 | text-shadow: white 0px 1px 0px; 133 | font-weight: bold; 134 | } 135 | 136 | .delete-all { 137 | color: #AAA488; 138 | font-weight: bold; 139 | font-size: 10px; 140 | text-decoration: underline; 141 | cursor: pointer; 142 | text-align: right; 143 | margin-bottom: 10px; 144 | } 145 | 146 | .delete-all:hover { 147 | color: #807A60; 148 | } 149 | 150 | #recent-cues { 151 | list-style: none inside url('../images/download.png'); 152 | margin: 10px 0 0 0; 153 | } 154 | 155 | #recent-cues li { 156 | font-size: 12px; 157 | margin-bottom: 5px; 158 | } 159 | 160 | #recent-cues a { 161 | color: #AAA488; 162 | } 163 | 164 | #recent-cues a:hover { 165 | color: #807A60; 166 | } 167 | 168 | #recent-cues span.delete { 169 | color: #AAA488; 170 | font-weight: bold; 171 | font-size: 10px; 172 | text-decoration: underline; 173 | cursor: pointer; 174 | } 175 | 176 | #recent-cues span.delete:hover { 177 | color: #807A60; 178 | } 179 | 180 | #categories-navigation { 181 | display: block; 182 | float: left; 183 | } 184 | 185 | #categories-navigation li { 186 | float: left; 187 | list-style: none; 188 | margin-right: 2px; 189 | } 190 | 191 | #categories-navigation li span { 192 | cursor: pointer; 193 | color: #807A60; 194 | font-size: 14px; 195 | font-weight: bold; 196 | } 197 | 198 | #categories-navigation li span:hover { 199 | color: #AAA488; 200 | } 201 | 202 | #top-controls-container-wrapper { 203 | height: 60px; 204 | } 205 | 206 | .top-controls-container-fixed { 207 | position: fixed; 208 | top: 43px; 209 | width: 571px; 210 | } 211 | 212 | #checked-filter-container { 213 | float: left; 214 | font-style: italic; 215 | font-size: 10px; 216 | font-weight: bold; 217 | } 218 | 219 | #checked-filter-container input { 220 | vertical-align: middle; 221 | margin-left: 0; 222 | } 223 | 224 | button[name="save"] { 225 | display: block; 226 | float: right; 227 | margin: 0; 228 | border: 1px solid #306C90; 229 | padding: 3px 6px; 230 | } 231 | 232 | .letter-container { 233 | margin-bottom: 5px; 234 | border: solid 1px #EADDBB; 235 | padding: 5px 5px 5px 10px; 236 | background-color: #f7f7ef; 237 | } 238 | 239 | .letter-container .letter { 240 | font-size: 14px; 241 | font-weight: bold; 242 | } 243 | 244 | .letter-container .categories-list { 245 | padding: 10px 0 0 0; 246 | -webkit-column-count: 3; 247 | column-count: 3; 248 | } 249 | 250 | .letter-container .categories-list label { 251 | display: inline-block; 252 | padding-left: 15px; 253 | margin-bottom: 5px; 254 | color: #807A60; 255 | font-size: 11px; 256 | font-weight: bold; 257 | width: 100%; 258 | box-sizing: border-box; 259 | } 260 | 261 | .letter-container .categories-list label input { 262 | margin-left: -15px; 263 | vertical-align: middle; 264 | float: left; 265 | } 266 | 267 | .letter-container .categories-list label i { 268 | font-size: 10px; 269 | font-weight: normal; 270 | font-style: italic; 271 | color: #333333; 272 | } 273 | -------------------------------------------------------------------------------- /tests/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | } 138 | 139 | /** 140 | * (1): approximate for browsers not supporting calc 141 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 142 | * ^^ seriously 143 | */ 144 | #mocha .test pre { 145 | display: block; 146 | float: left; 147 | clear: left; 148 | font: 12px/1.5 monaco, monospace; 149 | margin: 5px; 150 | padding: 15px; 151 | border: 1px solid #eee; 152 | max-width: 85%; /*(1)*/ 153 | max-width: calc(100% - 42px); /*(2)*/ 154 | word-wrap: break-word; 155 | border-bottom-color: #ddd; 156 | -webkit-border-radius: 3px; 157 | -webkit-box-shadow: 0 1px 3px #eee; 158 | -moz-border-radius: 3px; 159 | -moz-box-shadow: 0 1px 3px #eee; 160 | border-radius: 3px; 161 | } 162 | 163 | #mocha .test h2 { 164 | position: relative; 165 | } 166 | 167 | #mocha .test a.replay { 168 | position: absolute; 169 | top: 3px; 170 | right: 0; 171 | text-decoration: none; 172 | vertical-align: middle; 173 | display: block; 174 | width: 15px; 175 | height: 15px; 176 | line-height: 15px; 177 | text-align: center; 178 | background: #eee; 179 | font-size: 15px; 180 | -moz-border-radius: 15px; 181 | border-radius: 15px; 182 | -webkit-transition: opacity 200ms; 183 | -moz-transition: opacity 200ms; 184 | transition: opacity 200ms; 185 | opacity: 0.3; 186 | color: #888; 187 | } 188 | 189 | #mocha .test:hover a.replay { 190 | opacity: 1; 191 | } 192 | 193 | #mocha-report.pass .test.fail { 194 | display: none; 195 | } 196 | 197 | #mocha-report.fail .test.pass { 198 | display: none; 199 | } 200 | 201 | #mocha-report.pending .test.pass, 202 | #mocha-report.pending .test.fail { 203 | display: none; 204 | } 205 | #mocha-report.pending .test.pass.pending { 206 | display: block; 207 | } 208 | 209 | #mocha-error { 210 | color: #c00; 211 | font-size: 1.5em; 212 | font-weight: 100; 213 | letter-spacing: 1px; 214 | } 215 | 216 | #mocha-stats { 217 | position: fixed; 218 | top: 15px; 219 | right: 10px; 220 | font-size: 12px; 221 | margin: 0; 222 | color: #888; 223 | z-index: 1; 224 | } 225 | 226 | #mocha-stats .progress { 227 | float: right; 228 | padding-top: 0; 229 | } 230 | 231 | #mocha-stats em { 232 | color: black; 233 | } 234 | 235 | #mocha-stats a { 236 | text-decoration: none; 237 | color: inherit; 238 | } 239 | 240 | #mocha-stats a:hover { 241 | border-bottom: 1px solid #eee; 242 | } 243 | 244 | #mocha-stats li { 245 | display: inline-block; 246 | margin: 0 5px; 247 | list-style: none; 248 | padding-top: 11px; 249 | } 250 | 251 | #mocha-stats canvas { 252 | width: 40px; 253 | height: 40px; 254 | } 255 | 256 | #mocha code .comment { color: #ddd; } 257 | #mocha code .init { color: #2f6fad; } 258 | #mocha code .string { color: #5890ad; } 259 | #mocha code .keyword { color: #8a6343; } 260 | #mocha code .number { color: #2f6fad; } 261 | 262 | @media screen and (max-device-width: 480px) { 263 | #mocha { 264 | margin: 60px 0px; 265 | } 266 | 267 | #mocha #stats { 268 | position: absolute; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /scripts/view/CuesView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define([ 4 | 'scripts/view/View', 5 | 'scripts/view/Message', 6 | 'scripts/view/Badge' 7 | ], function (View, Message, Badge) { 8 | function CuesView(container, messageView, user, userCueService, userCueCategoryService) { 9 | if (!(this instanceof CuesView)) { 10 | throw new Error('`this` must be an instance of view.CuesView'); 11 | } 12 | 13 | View.call(this, container); 14 | 15 | var cuesView = this, 16 | badge = new Badge(userCueService); 17 | 18 | /** 19 | * @param {domain.Cue[]} cues 20 | * @param {domain.CueCategory[]} userCategories 21 | */ 22 | function render(cues, userCategories) { 23 | var containerBody = document.createDocumentFragment(), 24 | dismissAllLink, 25 | cuesList, 26 | text; 27 | 28 | dismissAllLink = document.createElement('div'); 29 | dismissAllLink.setAttribute('class', 'delete-all'); 30 | dismissAllLink.innerText = '[Dismiss all]'; 31 | 32 | cuesList = document.createElement('ul'); 33 | cuesList.setAttribute('id', 'recent-cues'); 34 | 35 | function createCuesListItem(cue) { 36 | var li, 37 | a, 38 | span, 39 | whitespace; 40 | 41 | a = document.createElement('a'); 42 | a.setAttribute('href', cue.link); 43 | a.setAttribute('target', '_blank'); 44 | a.innerText = cue.title; 45 | 46 | span = document.createElement('span'); 47 | span.setAttribute('class', 'delete'); 48 | span.dataset.id = cue.id; 49 | span.innerText = '[Dismiss]'; 50 | 51 | whitespace = document.createTextNode(' '); 52 | 53 | li = document.createElement('li'); 54 | li.appendChild(a); 55 | li.appendChild(whitespace); 56 | li.appendChild(span); 57 | 58 | return li; 59 | } 60 | 61 | for (var i = 0; i < cues.length; i++) { 62 | cuesList.appendChild(createCuesListItem(cues[i])); 63 | } 64 | 65 | containerBody.appendChild(dismissAllLink); 66 | containerBody.appendChild(cuesList); 67 | 68 | 69 | container.innerHTML = ''; 70 | 71 | if (0 === cues.length) { 72 | text = document.createElement('div'); 73 | text.setAttribute('id', 'no-cues'); 74 | 75 | if (0 === userCategories.length) { 76 | text.innerText = 'You are not subscribed for any category yet!'; 77 | } else { 78 | text.innerText = 'No new cues :('; 79 | } 80 | container.appendChild(text); 81 | } else { 82 | container.appendChild(containerBody); 83 | listenToDelete(userCategories); 84 | } 85 | } 86 | 87 | function listenToDelete(userCategories) { 88 | var forEach = Array.prototype.forEach; 89 | 90 | // delete individual 91 | forEach.call(container.querySelectorAll('.delete'), function (el) { 92 | el.addEventListener('click', function (e) { 93 | var ids = [this.dataset.id]; 94 | 95 | // upon clicking "dismiss" there are 2 strategies: 96 | // - either we simply remove a link 97 | // - or we re-render the whole layout (actually only in order not to duplicate "No new cues" message) 98 | if (1 === container.querySelectorAll('.delete').length) { 99 | render([], userCategories); 100 | } else { 101 | this.parentElement.remove(); 102 | } 103 | 104 | userCueService.put(user.token, ids, function (err) { 105 | if (err) { 106 | messageView.show(Message.status.ERROR, err); 107 | } else { 108 | badge.render(user); 109 | } 110 | }); 111 | }); 112 | }); 113 | 114 | // delete all 115 | container.querySelector('.delete-all').addEventListener('click', function (e) { 116 | var ids = []; 117 | 118 | forEach.call(container.querySelectorAll('.delete'), function (el) { 119 | ids.push(el.dataset.id); 120 | }); 121 | 122 | // re-render the whole layout (actually only in order not to duplicate "No new cues" message) 123 | render([], userCategories); 124 | 125 | userCueService.put(user.token, ids, function (err) { 126 | if (err) { 127 | messageView.show(Message.status.ERROR, err); 128 | } else { 129 | badge.render(user); 130 | } 131 | }); 132 | }); 133 | } 134 | 135 | this.render = function () { 136 | cuesView.renderLoader(); 137 | 138 | var _cues, 139 | _userCategories; 140 | 141 | function caller() { 142 | if (typeof _cues != 'undefined' && typeof _userCategories != 'undefined') { 143 | render(_cues, _userCategories); 144 | } 145 | } 146 | 147 | userCueService.get(user.token, 0, function (err, cues, pageable) { 148 | if (err) { 149 | messageView.show('error', err); 150 | } else { 151 | _cues = cues; 152 | caller(); 153 | } 154 | }); 155 | 156 | userCueCategoryService.get(user.token, function (err, userCategories) { 157 | if (err) { 158 | messageView.show('error', err); 159 | } else { 160 | _userCategories = userCategories; 161 | caller(); 162 | } 163 | }); 164 | } 165 | } 166 | 167 | CuesView.prototype = Object.create(View.prototype); 168 | CuesView.prototype.constructor = CuesView; 169 | 170 | return CuesView; 171 | }); 172 | -------------------------------------------------------------------------------- /scripts/view/CategoriesView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define([ 4 | 'scripts/view/View', 5 | 'scripts/view/Message', 6 | 'scripts/view/Badge' 7 | ], function (View, Message, Badge) { 8 | function CategoriesView(container, messageView, user, userCueService, cueCategoryService, userCueCategoryService) { 9 | if (!(this instanceof CategoriesView)) { 10 | throw new Error('`this` must be an instance of view.CategoriesView'); 11 | } 12 | 13 | View.call(this, container); 14 | 15 | var categoriesView = this, 16 | badge = new Badge(userCueService); 17 | 18 | // @TODO: whn the page is scrolled down and user click "Show only selected" checkbox navigation panel disappears 19 | 20 | window.addEventListener('scroll', function () { 21 | var topControlsContainer = document.getElementById('top-controls-container'), 22 | scrollTop = document.getElementById('logo').offsetHeight 23 | + document.getElementById('menu').offsetHeight; 24 | 25 | if (!topControlsContainer) { 26 | return; 27 | } 28 | 29 | if (document.body.scrollTop >= scrollTop) { 30 | topControlsContainer.setAttribute('class', 'letter-container clear top-controls-container-fixed'); 31 | } else { 32 | topControlsContainer.setAttribute('class', 'letter-container clear '); 33 | } 34 | }); 35 | 36 | 37 | /** 38 | * Return categories which belong to the given letter 39 | * 40 | * @param {String} letter 41 | * @param {domain.CueCategory[]} categories 42 | * @returns {Array} 43 | */ 44 | function pickCategoriesWhichBelongToLetter(letter, categories) { 45 | var sliced = []; 46 | 47 | function equals(char, name) { 48 | if (!name.length) { 49 | return false; 50 | } 51 | 52 | if ('#' === char) { 53 | var re = /[^a-z]/i; 54 | return re.test(name[0]); 55 | } else { 56 | // name may start with "the" article, leave it out 57 | name = name.toLocaleLowerCase(); 58 | if ('the' === name.substr(0, 3)) { 59 | name = name.substr(3).trim(); 60 | } 61 | return name[0].toUpperCase() === char; 62 | } 63 | } 64 | 65 | for (var i = 0, j = 0; i < categories.length; i++) { 66 | if (equals(letter, categories[i].name)) { 67 | sliced[j] = categories[i]; 68 | j++; 69 | } 70 | } 71 | 72 | return sliced; 73 | } 74 | 75 | function createCategoriesNavigationElement(char) { 76 | var span = document.createElement('span'); 77 | span.dataset.id = 'abc-' + char; 78 | span.innerText = char; 79 | 80 | var li = document.createElement('li'); 81 | li.appendChild(span); 82 | 83 | return li; 84 | } 85 | 86 | /** 87 | * @param {String} char 88 | * @param {Array} categories 89 | * @param {Array} userCategories 90 | * @param {Boolean} checkedFilter 91 | * @returns {HTMLElement} only if it contains categories otherwise NULL 92 | */ 93 | function createLetterContainer(char, categories, userCategories, checkedFilter) { 94 | var label, input, name, host; 95 | 96 | function isChecked(id) { 97 | for (var i = 0; i < userCategories.length; i++) { 98 | if (userCategories[i].id === id) { 99 | return true; 100 | } 101 | } 102 | 103 | return false; 104 | } 105 | 106 | function createInput(category) { 107 | var el = document.createElement('input'); 108 | el.setAttribute('type', 'checkbox'); 109 | el.setAttribute('name', 'categories'); 110 | el.setAttribute('value', category.id); 111 | if (isChecked(category.id)) { 112 | el.setAttribute('checked', 'checked'); 113 | } 114 | 115 | return el; 116 | } 117 | 118 | function createName(category) { 119 | return document.createTextNode(category.name); 120 | } 121 | 122 | function createHost(category) { 123 | var el = document.createElement('i'); 124 | if (category.host) { 125 | el.innerText = category.host; 126 | } else { 127 | el.innerHTML = ' '; 128 | } 129 | 130 | return el; 131 | } 132 | 133 | function createLabel(input, name, host) { 134 | label = document.createElement('label'); 135 | label.appendChild(input); 136 | label.appendChild(name); 137 | label.appendChild(document.createElement('br')); 138 | label.appendChild(host); 139 | 140 | return label; 141 | } 142 | 143 | var letterContainer = document.createElement('div'); 144 | letterContainer.setAttribute('class', 'letter-container'); 145 | letterContainer.setAttribute('id', 'abc-' + char); 146 | 147 | var letter = document.createElement('div'); 148 | letter.setAttribute('class', char); 149 | letter.innerText = char; 150 | 151 | var categoriesList = document.createElement('div'); 152 | categoriesList.setAttribute('class', 'categories-list'); 153 | 154 | for (var i = 0; i < categories.length; i++) { 155 | if (checkedFilter && !isChecked(categories[i].id)) { 156 | continue; 157 | } 158 | 159 | input = createInput(categories[i]); 160 | name = createName(categories[i]); 161 | host = createHost(categories[i]); 162 | label = createLabel(input, name, host); 163 | 164 | categoriesList.appendChild(label); 165 | } 166 | 167 | 168 | letterContainer.appendChild(letter); 169 | letterContainer.appendChild(categoriesList); 170 | 171 | 172 | // return DOM element only if it contains categories 173 | return categoriesList.getElementsByTagName('label').length > 0 ? letterContainer : null; 174 | } 175 | 176 | 177 | function render(categories, userCategories, checkedFilter) { 178 | var containerBody = document.createDocumentFragment(), 179 | topControlsContainer, 180 | topControlsContainerRow, 181 | topControlsContainerWrapper, 182 | alphabet = [], 183 | categoriesNavigation, 184 | categoriesNavigationEl, 185 | letterContainers = document.createDocumentFragment(), 186 | letterContainer, 187 | button, 188 | checkedFilterContainer, 189 | checkedFilterInput; 190 | 191 | 192 | button = document.createElement('button'); 193 | button.setAttribute('name', 'save'); 194 | button.innerText = 'Save'; 195 | 196 | categoriesNavigation = document.createElement('ul'); 197 | categoriesNavigation.setAttribute('id', 'categories-navigation'); 198 | 199 | topControlsContainerRow = document.createElement('div'); 200 | topControlsContainerRow.setAttribute('class', 'clear'); 201 | topControlsContainerRow.appendChild(button); 202 | topControlsContainerRow.appendChild(categoriesNavigation); 203 | 204 | checkedFilterInput = document.createElement('input'); 205 | checkedFilterInput.setAttribute('type', 'checkbox'); 206 | checkedFilterInput.setAttribute('name', 'checked-filter'); 207 | if (checkedFilter) { 208 | checkedFilterInput.setAttribute('checked', 'checked'); 209 | } 210 | 211 | 212 | checkedFilterContainer = document.createElement('label'); 213 | checkedFilterContainer.setAttribute('id', 'checked-filter-container'); 214 | checkedFilterContainer.appendChild(checkedFilterInput); 215 | checkedFilterContainer.appendChild(document.createTextNode('Show only selected')); 216 | 217 | topControlsContainer = document.createElement('div'); 218 | topControlsContainer.setAttribute('id', 'top-controls-container'); 219 | topControlsContainer.setAttribute('class', 'letter-container clear'); 220 | topControlsContainer.appendChild(topControlsContainerRow); 221 | topControlsContainer.appendChild(checkedFilterContainer); 222 | 223 | topControlsContainerWrapper = document.createElement('div'); 224 | topControlsContainerWrapper.setAttribute('id', 'top-controls-container-wrapper'); 225 | topControlsContainerWrapper.appendChild(topControlsContainer); 226 | 227 | 228 | // first create alphabet 229 | alphabet.push('#'); 230 | for (var i = 65; i < 91; i++) { 231 | alphabet.push(String.fromCharCode(i)); 232 | } 233 | // then go through it and create DOM 234 | alphabet.forEach(function (char) { 235 | var slicedCategories = pickCategoriesWhichBelongToLetter(char, categories), 236 | slicedUserCategories = pickCategoriesWhichBelongToLetter(char, userCategories); 237 | 238 | if (slicedCategories.length > 0) { 239 | categoriesNavigationEl = createCategoriesNavigationElement(char); 240 | letterContainer = createLetterContainer(char, slicedCategories, slicedUserCategories, checkedFilter); 241 | 242 | if (letterContainer) { 243 | categoriesNavigation.appendChild(categoriesNavigationEl); 244 | letterContainers.appendChild(letterContainer); 245 | } 246 | } 247 | }); 248 | 249 | 250 | containerBody.appendChild(topControlsContainerWrapper); 251 | containerBody.appendChild(letterContainers); 252 | 253 | container.innerHTML = ''; 254 | container.appendChild(containerBody); 255 | 256 | 257 | listenToSave(categories, userCategories); 258 | listenToCategoriesNavigation(); 259 | listenToCategoryTick(categories, userCategories); 260 | listenToCheckedFilter(categories, userCategories); 261 | } 262 | 263 | function listenToSave(categories, userCategories) { 264 | var forEach = Array.prototype.forEach; 265 | 266 | forEach.call(container.querySelectorAll('button[name="save"]'), function (button) { 267 | button.addEventListener('click', function (e) { 268 | var ids = []; 269 | forEach.call(container.querySelectorAll('input[name="categories"]:checked'), function (input) { 270 | ids.push(input.value); 271 | }); 272 | 273 | 274 | messageView.show(Message.status.INFO, 'Saved'); 275 | userCueCategoryService.put(user.token, ids, function (err) { 276 | if (err) { 277 | messageView.show(Message.status.ERROR, err); 278 | } else { 279 | badge.render(user); 280 | 281 | 282 | // if we are in "show only selected" mode. In this case I want to "commit" changes 283 | // and in order to do that let's re-render the page content 284 | var filter = document.getElementsByName('checked-filter')[0]; 285 | if (filter.checked) { 286 | render(categories, userCategories, true); 287 | } 288 | } 289 | }); 290 | }); 291 | }); 292 | } 293 | 294 | function listenToCategoriesNavigation() { 295 | var forEach = Array.prototype.forEach, 296 | navigationOffset = document.getElementById('menu').offsetHeight 297 | + document.getElementById('top-controls-container').offsetHeight; 298 | 299 | forEach.call(document.getElementById('categories-navigation').querySelectorAll('span'), function (el) { 300 | el.addEventListener('click', function (e) { 301 | var offset = document.getElementById(el.dataset.id).offsetTop - navigationOffset; 302 | window.scrollTo(0, offset); 303 | }); 304 | }); 305 | } 306 | 307 | function listenToCategoryTick(categories, userCategories) { 308 | var forEach = Array.prototype.forEach; 309 | 310 | forEach.call(container.querySelectorAll('input[name="categories"]'), function (el) { 311 | el.addEventListener('change', function (e) { 312 | var id = el.value; 313 | var i; 314 | if (el.checked) { 315 | for (i = 0; i < categories.length; i++) { 316 | if (categories[i].id === id) { 317 | userCategories.push(categories[i]); 318 | break; 319 | } 320 | } 321 | } else { 322 | for (i = 0; i < userCategories.length; i++) { 323 | if (userCategories[i].id === id) { 324 | userCategories.splice(i, 1); 325 | break; 326 | } 327 | } 328 | } 329 | }); 330 | }); 331 | } 332 | 333 | function listenToCheckedFilter(categories, userCategories) { 334 | var filter = document.getElementsByName('checked-filter')[0]; 335 | filter.addEventListener('change', function (e) { 336 | if (this.checked) { 337 | render(categories, userCategories, true); 338 | } else { 339 | render(categories, userCategories, false); 340 | } 341 | }); 342 | } 343 | 344 | 345 | this.render = function () { 346 | categoriesView.renderLoader(); 347 | 348 | 349 | var _categories, 350 | _userCategories; 351 | 352 | function caller() { 353 | if (typeof _categories != 'undefined' && typeof _userCategories != 'undefined') { 354 | render(_categories, _userCategories, false); 355 | } 356 | } 357 | 358 | cueCategoryService.get(function (err, categories) { 359 | if (err) { 360 | messageView.show('error', err); 361 | } else { 362 | _categories = categories; 363 | caller(); 364 | } 365 | }); 366 | 367 | userCueCategoryService.get(user.token, function (err, userCategories) { 368 | if (err) { 369 | messageView.show('error', err); 370 | } else { 371 | _userCategories = userCategories; 372 | caller(); 373 | } 374 | }); 375 | } 376 | } 377 | 378 | CategoriesView.prototype = Object.create(View.prototype); 379 | CategoriesView.prototype.constructor = CategoriesView; 380 | 381 | return CategoriesView; 382 | }); 383 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------