diff --git a/README.md b/README.md index c98a4ea..9c430a5 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,4 @@ A RSS mobile client with which you can read your RSS feeds and mark them as read - Works also offline. - You can chose between 4 fresh color schemes. -To use this RSS client to read your feeds you need a backend server. As a backend you can use [TinyTinyRSS](http://tt-rss.org) or [ownCloud News](http://apps.owncloud.com/content/show.php/News?content=158434). This is not a stand alone application. +To use this RSS client to read your feeds you need a backend server. As a backend you can use [TinyTinyRSS](http://tt-rss.org), [ownCloud News](http://apps.owncloud.com/content/show.php/News?content=158434) or [Pond](https://github.com/ArturoVM/pond#pond). This is not a stand alone application. diff --git a/css/screen.css b/css/screen.css index cd9a860..15170fc 100644 --- a/css/screen.css +++ b/css/screen.css @@ -83,7 +83,7 @@ label { } .hidden { - display: none; + display: none !important; } section { @@ -120,11 +120,13 @@ section > header h1 { overflow: auto; } -.reload, .all-read, #setstarred, #setunread { +.reload, .all-read, #setstarred, #setunread, #setpublished { float: right; margin-left: 10px; } +.inactive { opacity: 0.4; } + .settings, .list { float: left; } @@ -138,22 +140,29 @@ section > footer { box-sizing: border-box; } +figure { + margin: 0; + padding: 0; +} + img { max-width: 100% !important; height: auto; } +#full > article pre { + overflow: auto; +} + @media screen and (width: 320px) { - #full > article * , #full > article pre { + #full > article * { max-width: 300px !important; - overflow: auto; } } @media screen and (width: 480px) { #full > article * { max-width: 460px !important; - overflow: auto; } } @@ -165,7 +174,6 @@ canvas { list-style-type: none; margin: 0; padding: 0; - word-wrap: break-word; } #list p { @@ -189,7 +197,6 @@ canvas { #list li { position: relative; - min-height: 3em; } .red #list li { border-bottom: 1px solid #c0392b; } @@ -201,7 +208,7 @@ canvas { content: ""; position: absolute; right: 7px; - top: 0; + top: 0.1em; font-weight: 100; font-size: 3em; font-family: "Entypo"; @@ -250,7 +257,6 @@ canvas { font-weight: normal; margin: 0; padding: 0; - display: none; } #full .wrapper { @@ -281,28 +287,9 @@ canvas { padding: 0; } -#full article header p:nth-child(1) { - float: left; -} - -#full article header p:nth-child(2) { - float: right; -} - -#full article header p:nth-child(3) { - clear: both; -} - -#full .article { - clear: both; - padding-top: 1em; - padding-bottom: 3em; - font-size: 1.3em; -} - #full footer.bar { margin: auto 0 0 0; - position: fixed; + position: relative; height: 3.8em; } diff --git a/index.html b/index.html index ff6889e..6d62518 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + FeedMonkey @@ -15,7 +15,7 @@ - + @@ -23,7 +23,7 @@
🔄 - +
@@ -71,11 +71,11 @@

- + - + - +

@@ -87,15 +87,16 @@

- - + + +

+

-

diff --git a/js/App.js b/js/App.js index d2cb11e..ff92dad 100644 --- a/js/App.js +++ b/js/App.js @@ -8,19 +8,12 @@ function App() { this.setColor(color); this.fontChange(); - var _this = this; - - window.onkeydown = function(e) { - if(e.keyCode == 39) { - _this.showNext(); - } else if(e.keyCode == 37) { - _this.showPrevious(); - } else if(e.keyCode == 13) { - _this.openInBrowser(); - } else if(e.keyCode == 82) { - _this.reload(); - } - } + var numArticles = localStorage.numArticles; + if(!numArticles) numArticles = 50; + this.numArticles(numArticles); + var maxDownload = localStorage.maxDownload; + if(!maxDownload) maxDownload = 500000; + this.maxDownload(maxDownload); }; App.prototype.authenticate = function() { @@ -28,11 +21,11 @@ App.prototype.authenticate = function() { }; App.prototype.after_login = function(backend) { - /* + var request = window.navigator.mozApps.getSelf(); request.onsuccess = function() { $("#version").innerHTML = request.result.manifest.version; - }*/ + } var _this = this; @@ -40,7 +33,7 @@ App.prototype.after_login = function(backend) { // do not reload page e.preventDefault(); - e.stopPropagation(); + e.stopPropagation(); var url = window.location.hash; @@ -61,6 +54,8 @@ App.prototype.after_login = function(backend) { _this.toggleCurrentUnread(); } else if(url == "#starred") { _this.toggleStarred(); + } else if(url == "#published") { + _this.togglePublished(); } else if(url == "#logout") { _this.logout(); } else if(url == "#reset-info") { @@ -98,11 +93,15 @@ App.prototype.after_login = function(backend) { } // set up swiping - jester($("#full")).flick(function(touches, direction) { - if(direction == "left") _this.showNext(); - else _this.showPrevious(); - }); - + var options = { + dragLockToAxis: true, + dragBlockHorizontal: true + }; + var hammertime = new Hammer($("#full"), options); + hammertime.on("dragleft", function(ev){ ev.gesture.preventDefault(); }); + hammertime.on("dragright", function(ev){ ev.gesture.preventDefault(); }); + hammertime.on("swipeleft", function(ev){ _this.showNext(); ev.gesture.preventDefault(); }); + hammertime.on("swiperight", function(ev){ _this.showPrevious(); ev.gesture.preventDefault(); }); this.changeToPage("#list"); @@ -112,7 +111,16 @@ App.prototype.after_login = function(backend) { this.backend = new Pond(this, localStorage.server_url, localStorage.session_id) } else { this.backend = new TinyTinyRSS(this, localStorage.server_url, localStorage.session_id); + $("#setpublished").removeClass("hidden"); } + + var numArticles = localStorage.numArticles; + if(!numArticles) numArticles = 50; + this.numArticles(numArticles); + var maxDownload = localStorage.maxDownload; + if(!maxDownload) maxDownload = 500000; + this.maxDownload(maxDownload); + this.reload(); }; @@ -150,30 +158,51 @@ App.prototype.setColor = function(color) { App.prototype.reload = function() { this.unread_articles = []; - $("#all-read").innerHTML = "❌"; - this.backend.reload(this.gotUnreadFeeds.bind(this)); + $("#all-read").addClass('inactive'); + var number=parseInt(localStorage.numArticles); + this.backend.reload(this.gotUnreadFeeds.bind(this),number); }; App.prototype.gotUnreadFeeds = function(new_articles) { - if(new_articles == null || !this.validate(new_articles)) { // on error load the saved unread articles. - - var old_articles = localStorage.unread_articles; - if(old_articles) { - this.unread_articles = JSON.parse(old_articles); + if(new_articles == null || !this.validate(new_articles)) { + + // Check if we did not get a NOT_LOGGED_IN error, and ask the + // user to login again if it is the case. + // This can happen with TT-RSS backend + if (new_articles.error && new_articles.error === "NOT_LOGGED_IN") { + alert("Your TinyTinyRSS session has expired. Please login again"); + this.login.fillLoginFormFromLocalStorage(); + this.login.log_in(); + } else { + // On other errors, load the saved unread articles. + var old_articles = localStorage.unread_articles; + if(old_articles) { + this.unread_articles = JSON.parse(old_articles); + } + this.populateList(); } - this.populateList(); - + } else { - + this.unread_articles = this.unread_articles.concat(new_articles); if(new_articles.length > 0) { - this.backend.getUnreadFeeds(this.gotUnreadFeeds.bind(this), this.unread_articles); - } else { - localStorage.unread_articles = JSON.stringify(this.unread_articles); + try { + //To check if when it fails it is the same + localStorage.unread_articles = JSON.stringify(this.unread_articles); + var size = parseInt(localStorage.maxDownload); + if(localStorage.unread_articles.length < size) { + var num = parseInt(localStorage.numArticles); + this.backend.getUnreadFeeds(this.gotUnreadFeeds.bind(this), this.unread_articles,num); + } else { + alert("Limit size reached: Downloaded: " + this.unread_articles.length + " articles. Reached: " + localStorage.unread_articles.length +" bytes"); + } + } catch (e) { + alert("Reached maximum memory by app " + e.name + " " + e.message + ". We will keep working in anycase with: " + localStorage.unread_articles.length); + } this.populateList(); - } + } } }; @@ -196,10 +225,8 @@ App.prototype.populateList = function() { html_str += ""; html_str += ""; html_str += "

" + article.feed_title + "

"; - var content = article.content.stripHTML(); - if(content.replace(/^\s+|\s+$/g,'').length == 0) content = article.title; - html_str += "

" + content + "

"; - //if(article.excerpt) html_str += "

" + article.excerpt + "

"; + html_str += "

" + article.title + "

"; + if(article.excerpt) html_str += "

" + article.excerpt + "

"; html_str += "
"; } @@ -222,9 +249,9 @@ App.prototype.updateList = function() { }, this); if(unread > 0) { - $("#all-read").innerHTML = "❌"; + $("#all-read").addClass('inactive'); } else { - $("#all-read").innerHTML = "✓"; + $("#all-read").removeClass('inactive'); } this.updatePieChart(); @@ -246,6 +273,7 @@ App.prototype.updatePieChart = function() { var bg = window.getComputedStyle($("body"), null).backgroundColor; var fg = window.getComputedStyle($(".bar"), null).backgroundColor; + var tx = window.getComputedStyle($(".bar"), null).color; var myColor = [bg, fg]; @@ -270,7 +298,7 @@ App.prototype.updatePieChart = function() { if(all > 0) { ctx.font = "12px FeuraSans, sans-serif"; - ctx.fillStyle = "#fff"; + ctx.fillStyle = tx; ctx.textAlign = "center"; var text = unread + "/" + all; var x = canvas.width / 2; @@ -290,9 +318,9 @@ App.prototype.showFull = function(article, slide_back) { $(page_id + " .date").innerHTML = (new Date(parseInt(article.updated, 10) * 1000)).toLocaleString(); - var link = $(page_id + " .link"); - link.innerHTML = article.link; - link.href = article.link; + var title = $(page_id + " .title"); + title.innerHTML = article.title; + title.href = article.link; $(page_id + " .feed_title").innerHTML = article.feed_title; @@ -300,23 +328,29 @@ App.prototype.showFull = function(article, slide_back) { if(article.author && article.author.length > 0) $(page_id + " .author").innerHTML = "– " + article.author; - var content = article.content; - if(content.replace(/^\s+|\s+$/g,'').length == 0) content = article.title; - $(page_id + " .article").innerHTML = content.urlify(); + $(page_id + " .article").innerHTML = article.content; + + // Open all links in browser $$(page_id + " .article a").forEach(function(o, i) { o.target = "_blank"; }); if(article.unread) { - $("#setunread").innerHTML = "❌"; + $("#setunread").addClass('inactive'); } else { - $("#setunread").innerHTML = "✓"; + $("#setunread").removeClass('inactive'); } - if(article.marked) { - $("#setstarred").innerHTML = "★"; + if(!article.marked) { + $("#setstarred").addClass('inactive'); } else { - $("#setstarred").innerHTML = "☆"; + $("#setstarred").removeClass('inactive'); + } + + if(!article.published) { + $("#setpublished").addClass('inactive'); + } else { + $("#setpublished").removeClass('inactive'); } }; @@ -343,10 +377,6 @@ App.prototype.showPrevious = function() { } }; -App.prototype.openInBrowser = function() { - $("#full .link").click(); -}; - App.prototype.setCurrentRead = function() { var article = this.unread_articles[this.currentIndex]; if(!article) return; // happens if we're not on a full article site @@ -358,7 +388,7 @@ App.prototype.setCurrentRead = function() { article.set_unread = false; - $("#setunread").innerHTML = "✓"; + $("#setunread").removeClass('inactive'); this.updatePieChart(); }; @@ -368,11 +398,11 @@ App.prototype.toggleCurrentUnread = function() { if(article.unread) { article.unread = false; article.set_unread = false; - $("#setunread").innerHTML = "✓"; + $("#setunread").removeClass('inactive'); } else { article.unread = true; article.set_unread = true; - $("#setunread").innerHTML = "❌"; + $("#setunread").addClass('inactive'); } this.updateList(); @@ -381,7 +411,7 @@ App.prototype.toggleCurrentUnread = function() { App.prototype.toggleAllRead = function() { - if($("#all-read").innerHTML == "❌") { // set all read + if($("#all-read").hasClass('inactive')) { // set all read var articles = []; for (var i = 0; i < this.unread_articles.length; i++) { @@ -390,10 +420,9 @@ App.prototype.toggleAllRead = function() { article.set_unread = false; articles.push(article); } - $("#all-read").innerHTML = "✓"; + $("#all-read").removeClass('inactive'); this.updateList(); - this.backend.setArticlesRead(articles); } else { @@ -405,11 +434,10 @@ App.prototype.toggleAllRead = function() { article.set_unread = false; articles.push(article); } - $("#all-read").innerHTML = "❌"; + $("#all-read").addClass('inactive'); this.updateList(); this.backend.setArticlesUnread(articles); - } }; @@ -421,13 +449,28 @@ App.prototype.toggleStarred = function() { article.marked = true; this.updateList(); this.backend.setArticleStarred(article); - $("#setstarred").innerHTML = "★"; - } - else { + $("#setstarred").removeClass('inactive'); + } else { article.marked = false; this.updateList(); this.backend.setArticleUnstarred(article); - $("#setstarred").innerHTML = "☆"; + $("#setstarred").addClass('inactive'); + } + +}; + +App.prototype.togglePublished = function() { + var article = this.unread_articles[this.currentIndex]; + if(!article) return; // happens if we're not on a full article site + + if(!article.published) { + article.published = true; + this.backend.setArticlePublished(article); + $("#setpublished").removeClass('inactive'); + } else { + article.published = false; + this.backend.setArticleUnpublished(article); + $("#setpublished").addClass('inactive'); } }; @@ -467,5 +510,20 @@ App.prototype.fontChange = function(size) { document.body.addClass("f" + i); } - +}; + +App.prototype.numArticles = function(askfor) { + if(askfor < 200 && askfor > 0) { + localStorage.numArticles=askfor; + } else { + localStorage.numArticles=100; + } +}; + +App.prototype.maxDownload = function(maxdata) { + if(maxdata < 5000000 && maxdata > 100000) { + localStorage.maxDownload=maxdata; + } else { + localStorage.maxDownload=500000; + } }; diff --git a/js/Login.js b/js/Login.js index bc05ba8..c786023 100644 --- a/js/Login.js +++ b/js/Login.js @@ -17,6 +17,19 @@ Login.prototype.is_logged_in = function() { Login.prototype.log_in = function() { this.app.changeToPage("#login"); + $("#login form").backend.forEach(function(o, i) { + o.addEventListener("change", function(e) { + if(e.target.checked) { + if(e.target.value == "OwnCloud") { + $("#url").placeholder = "http://example.com/owncloud/"; + } else if(e.target.value == "Pond") { + $("#url").placeholder = "http://example.com/pond/"; + } else { + $("#url").placeholder = "http://example.com/tt-rss/"; + } + } + }); + }); $("#login form").addEventListener('submit', this.authenticate.bind(this)); }; @@ -25,10 +38,11 @@ Login.prototype.authenticate = function(e) { e.preventDefault(); e.stopPropagation(); - var backend = "Pond"; + var backend = "TinyTinyRSS"; + if($("#login form").backend[1].checked) backend = "OwnCloud"; + else if($("#login form").backend[2].checked) backend = "Pond"; - var server_url = window.location.href.split("#")[0].replace(/\/FeedMonkey\//, ''); - console.log(server_url) + var server_url = $("#url").value; var user = $("#un").value; var password = $("#pw").value; @@ -53,6 +67,7 @@ Login.prototype.authenticate = function(e) { if(data.version) { var auth = btoa(user + ':' + password); localStorage.server_url = server_url; + localStorage.username = user; localStorage.session_id = auth; localStorage.backend = "OwnCloud"; _this.app.after_login(localStorage.backend); @@ -69,6 +84,7 @@ Login.prototype.authenticate = function(e) { Pond.login(server_url, user, password, function(data) { if(data.session_token) { localStorage.server_url = server_url; + localStorage.username = user; localStorage.session_id = data.session_token; localStorage.backend = "Pond"; _this.app.after_login(localStorage.backend); @@ -92,6 +108,7 @@ Login.prototype.authenticate = function(e) { } else { localStorage.server_url = server_url; + localStorage.username = user; localStorage.session_id = data.session_id; localStorage.backend = "TinyTinyRSS"; _this.app.after_login(localStorage.backend); @@ -106,7 +123,29 @@ Login.prototype.authenticate = function(e) { return false; }; +Login.prototype.fillLoginFormFromLocalStorage = function() { + var serverUrl = localStorage.server_url; + if (serverUrl) { + $("#url").value = serverUrl; + } + var userName = localStorage.username; + if (userName) { + $("#un").value = userName; + } + var backendName = localStorage.backend; + if (backendName === "TinyTinyRSS") { + $("#login form").backend[0].checked = true; + } + else if (backendName === "OwnCloud") { + $("#login form").backend[1].checked = true; + } + else if (backendName === "Pond") { + $("#login form").backend[2].checked = true; + } +} + Login.prototype.log_out = function() { + this.fillLoginFormFromLocalStorage(); localStorage.removeItem("server_url"); localStorage.removeItem("session_id"); localStorage.removeItem("unread_articles"); diff --git a/js/OwnCloud.js b/js/OwnCloud.js new file mode 100644 index 0000000..89a77b3 --- /dev/null +++ b/js/OwnCloud.js @@ -0,0 +1,259 @@ +function OwnCloud(app, server_url, user_pass_btoa) { + this.app = app; + this.server_url = server_url; + this.session_id = user_pass_btoa; + this.feeds = {}; + var feeds = localStorage.feeds; + if(feeds) this.feeds = JSON.parse(feeds); + + window.addEventListener("offline", this.onoffline.bind(this)); + window.addEventListener("online", this.ononline.bind(this)); +} + +OwnCloud.prototype.onoffline = function() { + // Do nothing +}; + +OwnCloud.prototype.ononline = function() { + + ["read", "unread", "starred", "unstarred"].forEach(function(type) { + var articles = localStorage[type + "_articles"]; + if(articles) { + var callback = function(ok) { if(ok) localStorage[type + "_articles"] = null } + this.call("setArticles" + type.capitalize(), [JSON.parse(articles), callback]); + } + }); +}; + +OwnCloud.prototype.doOperation = function(method, operation, new_options, callback) { + if(!navigator.onLine) { + callback(null); + return; + } + + var url = this.server_url + "/index.php/apps/news/api/v1-2/" + operation; + var options = {}; + + for (var key in new_options) { + options[key] = new_options[key]; + } + + if(method == "GET" || method == "HEAD") { + var a = []; + for(var key in options) { + a.push(key + "=" + options[key]); + } + url += "?" + a.join("&"); + } + + var xhr = new XMLHttpRequest({mozSystem: true}); + xhr.onreadystatechange = function() { + if(xhr.readyState == 4) { + if(xhr.status == 200) { + if(callback) + callback(JSON.parse(xhr.responseText)); + } else { + if(xhr.status != 0) alert("error: " + xhr.status + " " + xhr.statusText); + if(callback) callback(null); + } + } + } + xhr.open(method, url, true); + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.withCredentials = true; + xhr.setRequestHeader('Authorization', 'Basic ' + this.session_id); + var body = JSON.stringify(options); + xhr.send(body); +} + +OwnCloud.prototype.reload = function(callback,limit) { + var _this = this; + this.getFeeds(function() { _this.getUnreadFeeds(callback,0,limit); }); +}; + +OwnCloud.prototype.getUnreadFeeds = function(callback, skip, limit) { + if(skip) { + skip = skip[skip.length - 1].id; + } + + var options = { + batchSize: limit || 700, + offset: skip || 0, + type: 3, + id: 0, + getRead: false + }; + + var _this = this; + this.doOperation("GET", "items", options, function(data) { + + var items = data.items; + + function isFeedAvailable(o) { + return !!_this.feeds[o.feedId]; + } + + if(items.every(isFeedAvailable)) { + callback(items.map(_this.normalize_article, _this)); + } else { + _this.getFeeds(function() { + callback(items.map(_this.normalize_article, _this)); + }); + } + }); +}; + + +OwnCloud.prototype.toString = function() { + return "OwnCloud" +}; + +OwnCloud.prototype.getFeeds = function(callback) { + var _this = this; + this.doOperation("GET", "feeds", {}, function(data) { + + _this.feeds = {}; + for (var i = 0; i < data.feeds.length; i++) { + var feed = data.feeds[i]; + _this.feeds[feed.id] = feed; + } + + localStorage.feeds = JSON.stringify(_this.feeds); + callback(); + }); +}; + +OwnCloud.prototype.setArticlesRead = function(articles, callback) { + + var options = { + items: articles.map(function(o) { return o.id; }), + }; + + if (navigator.onLine) { + this.doOperation("PUT", "items/read/multiple", options, callback); + } else { + this.append("read_articles", articles); + } +} + +OwnCloud.prototype.setArticleRead = function(article, callback) { + this.setArticlesRead([article], callback); +} + +OwnCloud.prototype.setArticlesUnread = function(articles, callback) { + + var options = { + items: articles.map(function(o) { return o.id; }), + }; + + if (navigator.onLine) this.doOperation("PUT", "items/unread/multiple", options, callback); + else { + this.append("unread_articles", articles); + } +}; + +OwnCloud.prototype.setArticleUnread = function(article, callback) { + this.setArticlesUnread([article], callback); +} + +OwnCloud.prototype.setArticlesStarred = function(articles, callback) { + + var options = { + items: articles.map(function(o) { return { feedId: o.feed_id, guidHash: o.guid_hash }; }) + }; + + if (navigator.onLine) { + this.doOperation("PUT", "items/star/multiple", options, callback); + } else { + this.append("starred_articles", articles); + } +}; + +OwnCloud.prototype.setArticleStarred = function(article, callback) { + this.setArticlesStarred([article], callback); +} + +OwnCloud.prototype.setArticlesUnstarred = function(articles, callback) { + + var options = { + items: articles.map(function(o) { return { feedId: o.feed_id, guidHash: o.guid_hash }; }) + }; + + if (navigator.onLine) { + this.doOperation("PUT", "items/unstar/multiple", options, callback); + } else { + this.append("unstarred_articles", articles); + } +}; + +OwnCloud.prototype.setArticleUnstarred = function(articles, callback) { + this.setArticlesUnstarred([articles], callback); +} + +OwnCloud.prototype.normalize_article = function(article) { + var feed = this.feeds[article.feedId]; + var feed_title = ""; + if(feed) { + feed_title = feed.title; + } + + return { + id: article.id, + guid_hash: article.guidHash, + title: article.title, + content: article.body, + feed_title: feed_title, + feed_id: article.feedId, + excerpt: article.body.stripHTML().substring(0, 100), + updated: article.pubDate, + link: article.link, + marked: article.starred, + unread: article.unread + } +}; + +OwnCloud.prototype.logOut = function() { + this.doOperation("logout"); + localStorage.feeds = null; +}; + +OwnCloud.prototype.getFeedFor = function(o) { + return this.feeds[o.feedId]; +}; + +OwnCloud.prototype.append = function(key, array) { + + var tmp = localStorage[key]; + + if (typeof tmp !== "undefined") tmp = JSON.parse(tmp); + else tmp = []; + + tmp.concat(array); + localStorage[key] = JSON.stringify(tmp); +}; + +OwnCloud.login = function(server_url, user, password, callback) { + + var url = server_url + "/index.php/apps/news/api/v1-2/version"; + + var xhr = new XMLHttpRequest({mozSystem: true}); + xhr.onreadystatechange = function() { + if(xhr.readyState == 4) { + if(xhr.status == 200) { + callback(JSON.parse(xhr.responseText)) + } else { + if(xhr.status == 0) { + alert("Something went wrong, please check your credentials and the server address") + } else { + alert("error: " + xhr.status + " " + xhr.statusText); + } + } + } + } + + xhr.open("GET", url, true); + xhr.withCredentials = true; + var auth = btoa(user + ':' + password); + xhr.setRequestHeader('Authorization', 'Basic ' + auth); + xhr.send(); +} diff --git a/js/Pond.js b/js/Pond.js index 1c424a6..99abb41 100644 --- a/js/Pond.js +++ b/js/Pond.js @@ -15,7 +15,13 @@ Pond.prototype.onoffline = function() { }; Pond.prototype.ononline = function() { - // Send read + ["read", "unread"].forEach(function(type) { + var articles = localStorage[type + "_articles"]; + if(articles) { + var callback = function(ok) { if(ok) localStorage[type + "_articles"] = null } + this.call("setArticles" + type.capitalize(), [JSON.parse(articles), callback]); + } + }); }; Pond.prototype.toString = function() { @@ -69,15 +75,15 @@ Pond.prototype.doOperation = function(method, operation, new_options, callback) }; -Pond.prototype.reload = function(callback) { +Pond.prototype.reload = function(callback,limit) { var _this = this; - this.getFeeds(function() { _this.getUnreadFeeds(callback); }); + this.getFeeds(function() { _this.getUnreadFeeds(callback,0,limit); }); }; -Pond.prototype.getUnreadFeeds = function(callback, skip) { +Pond.prototype.getUnreadFeeds = function(callback, skip, limit) { var options = { status: "unread", - limit: 100 + limit: limit || 100 }; if(skip && skip.length > 0) { @@ -106,6 +112,8 @@ Pond.prototype.getUnreadFeeds = function(callback, skip) { Pond.prototype.getFeeds = function(callback) { var _this = this; this.doOperation("GET", "subscriptions", {}, function(feeds) { + + if(!feeds) return; _this.feeds = {}; for (var i = 0; i < feeds.length; i++) { @@ -157,11 +165,11 @@ Pond.prototype.setArticleStatus = function(article, callback, status) { var url = "subscriptions/" + article.feed_id + "/articles/" + article.id - if (navigator.onLine) this.doOperation("PUT", url, options, callback); - else { - this.append("unread_articles", articles); + if (navigator.onLine) { + this.doOperation("PUT", url, options, callback); + } else { + this.append(status + "_articles", articles); } - } Pond.prototype.setArticleRead = function(article, callback) { @@ -179,7 +187,7 @@ Pond.prototype.setArticlesRead = function(articles, callback) { } Pond.prototype.setArticlesUnread = function(articles, callback) { - articles.forEach(function(article) { + articles.forEach(function(article) { this.setArticleStatus(article, callback, "unread"); }) } @@ -192,6 +200,16 @@ Pond.prototype.setArticleUnstarred = function(articles, callback) { // not implemented yet in Pond } +TinyTinyRSS.prototype.append = function(key, array) { + var tmp = localStorage[key]; + + if (typeof tmp !== "undefined") tmp = JSON.parse(tmp); + else tmp = []; + + tmp.concat(array); + localStorage[key] = JSON.stringify(tmp); +}; + Pond.prototype.logOut = function() { this.doOperation("DELETE", "auth/sessions/" + this.session_token ); localStorage.feeds = null; @@ -213,7 +231,11 @@ Pond.login = function(server_url, user, password, callback) { if(xhr.status == 201) { callback(JSON.parse(xhr.responseText)) } else { - alert("error: " + typeof(xhr.status) + " " + xhr.statusText + "\n\n" + xhr.responseText) + if(xhr.status == 0) { + alert("Something went wrong, please check your credentials and the server address") + } else { + alert("error: " + typeof(xhr.status) + " " + xhr.statusText + "\n\n" + xhr.responseText); + } } } } @@ -222,4 +244,4 @@ Pond.login = function(server_url, user, password, callback) { xhr.setRequestHeader("Content-Length", options.length); xhr.setRequestHeader("Connection", "close"); xhr.send(options); -} \ No newline at end of file +} diff --git a/js/TinyTinyRSS.js b/js/TinyTinyRSS.js new file mode 100644 index 0000000..2644d02 --- /dev/null +++ b/js/TinyTinyRSS.js @@ -0,0 +1,225 @@ +function TinyTinyRSS(app, server_url, session_id) { + this.app = app; + this.server_url = server_url; + this.session_id = session_id; + + window.addEventListener("offline", this.onoffline.bind(this)); + window.addEventListener("online", this.ononline.bind(this)); +} + +TinyTinyRSS.prototype.onoffline = function() { + // Do nothing +}; + +TinyTinyRSS.prototype.ononline = function() { + + ["read", "unread", "starred", "unstarred", "published", "unpublished"].forEach(function(type) { + var articles = localStorage[type + "_articles"]; + if(articles) { + var callback = function(ok) { if(ok) localStorage[type + "_articles"] = null } + this.call("setArticles" + type.capitalize(), [JSON.parse(articles), callback]); + } + }); +}; + +TinyTinyRSS.prototype.doOperation = function(operation, new_options, callback) { + if(!navigator.onLine) { + callback(null); + return; + } + + var url = this.server_url + "/api/"; + var options = { + sid: this.session_id, + op: operation + }; + + for (var key in new_options) { + options[key] = new_options[key]; + } + + var xhr = new XMLHttpRequest({mozSystem: true}); + xhr.onreadystatechange = function() { + if(xhr.readyState == 4) { + if(xhr.status == 200) { + if(callback) + callback(JSON.parse(xhr.responseText).content); + } else { + if(xhr.status != 0) alert("error: " + xhr.status + " " + xhr.statusText); + if(callback) callback(null); + } + } + } + xhr.open("POST", url, true); + xhr.send(JSON.stringify(options)); +} + +TinyTinyRSS.prototype.reload = function(callback,limit) { + this.getUnreadFeeds(callback, 0, limit); +}; + +TinyTinyRSS.prototype.getUnreadFeeds = function(callback, skip, limit) { + skip = skip.length; + var options = { + show_excerpt: false, + view_mode: "unread", + show_content: true, + feed_id: -4, + limit: limit || 0, + skip: skip || 0 + }; + + this.doOperation("getHeadlines", options, callback); +} + +TinyTinyRSS.prototype.setArticlesRead = function(articles, callback) { + + var options = { + article_ids: articles.map(function(o) { return o.id }).join(","), + mode: 0, + field: 2 + }; + + if (navigator.onLine) { + this.doOperation("updateArticle", options, callback); + } else { + this.append("read_articles", articles); + } +}; + +TinyTinyRSS.prototype.setArticleRead = function(article, callback) { + this.setArticlesRead([article], callback); +}; + + +TinyTinyRSS.prototype.setArticlesUnread = function(articles, callback) { + + var options = { + article_ids: articles.map(function(o) { return o.id }).join(","), + mode: 1, + field: 2 + }; + + if (navigator.onLine) { + this.doOperation("updateArticle", options, callback); + } else { + this.append("unread_articles", articles); + } +}; + +TinyTinyRSS.prototype.setArticleUnread = function(article, callback) { + this.setArticlesUnread([article], callback); +}; + +TinyTinyRSS.prototype.setArticlesStarred = function(articles, callback) { + + var options = { + article_ids: articles.map(function(o) { return o.id }).join(","), + mode: 1, + field: 0 + }; + + if (navigator.onLine) { + this.doOperation("updateArticle", options); + } else { + this.append("starred_articles", articles); + } +}; + +TinyTinyRSS.prototype.setArticleStarred = function(article, callback) { + this.setArticlesStarred([article], callback); +}; + +TinyTinyRSS.prototype.setArticlesUnstarred = function(articles, callback) { + + var options = { + article_ids: articles.map(function(o) { return o.id}).join(","), + mode: 0, + field: 0 + }; + + if (navigator.onLine) { + this.doOperation("updateArticle", options, callback); + } else { + this.append("unstarred_articles", articles); + } +}; + +TinyTinyRSS.prototype.setArticleUnstarred = function(article, callback) { + this.setArticlesUnstarred([article], callback); +}; + +TinyTinyRSS.prototype.setArticlesPublished = function(articles, callback) { + + var options = { + article_ids: articles.map(function(o) { return o.id }).join(","), + mode: 1, + field: 1 + }; + + if (navigator.onLine) { + this.doOperation("updateArticle", options); + } else { + this.append("published_articles", articles); + } +}; + +TinyTinyRSS.prototype.setArticlePublished = function(article, callback) { + this.setArticlesPublished([article], callback); +}; + +TinyTinyRSS.prototype.setArticlesUnpublished = function(articles, callback) { + + var options = { + article_ids: articles.map(function(o) { return o.id}).join(","), + mode: 0, + field: 1 + }; + + if (navigator.onLine) { + this.doOperation("updateArticle", options, callback); + } else { + this.append("unpublished_articles", articles); + } +}; + +TinyTinyRSS.prototype.setArticleUnpublished = function(article, callback) { + this.setArticlesUnpublished([article], callback); +}; + +TinyTinyRSS.prototype.append = function(key, array) { + var tmp = localStorage[key]; + + if (typeof tmp !== "undefined") tmp = JSON.parse(tmp); + else tmp = []; + + tmp.concat(array); + localStorage[key] = JSON.stringify(tmp); +}; + +TinyTinyRSS.prototype.logOut = function() { + this.doOperation("logout"); +}; + +TinyTinyRSS.login = function(server_url, user, password, callback) { + + var url = server_url + "/api/"; + var options = {op: "login", user: user, password: password}; + + var xhr = new XMLHttpRequest({mozSystem: true}); + xhr.onreadystatechange = function() { + if(xhr.readyState == 4) { + if(xhr.status == 200) { + callback(JSON.parse(xhr.responseText).content) + } else { + if(xhr.status == 0) { + alert("Something went wrong, please check your credentials and the server address") + } else { + alert("error: " + xhr.status + " " + xhr.statusText) + } + } + } + } + xhr.open("POST", url, true); + xhr.send(JSON.stringify(options)); +} diff --git a/js/application.js b/js/application.js index ebacb08..5e38312 100644 --- a/js/application.js +++ b/js/application.js @@ -63,10 +63,5 @@ String.prototype.capitalize = function() { return this.charAt(0).toUpperCase() + this.slice(1); } -String.prototype.urlify = function() { - var exp = /[^\>](https?:\/\/[^\s\<]*)/ig; - return this.replace(exp," $1"); -} - if(!window.app) window.app = new App(); diff --git a/js/hammer.js b/js/hammer.js new file mode 100644 index 0000000..703ee55 --- /dev/null +++ b/js/hammer.js @@ -0,0 +1,2163 @@ +/*! Hammer.JS - v1.1.3 - 2014-05-22 + * http://eightmedia.github.io/hammer.js + * + * Copyright (c) 2014 Jorik Tangelder ; + * Licensed under the MIT license */ + +(function(window, undefined) { + 'use strict'; + +/** + * @main + * @module hammer + * + * @class Hammer + * @static + */ + +/** + * Hammer, use this to create instances + * ```` + * var hammertime = new Hammer(myElement); + * ```` + * + * @method Hammer + * @param {HTMLElement} element + * @param {Object} [options={}] + * @return {Hammer.Instance} + */ +var Hammer = function Hammer(element, options) { + return new Hammer.Instance(element, options || {}); +}; + +/** + * version, as defined in package.json + * the value will be set at each build + * @property VERSION + * @final + * @type {String} + */ +Hammer.VERSION = '1.1.3'; + +/** + * default settings. + * more settings are defined per gesture at `/gestures`. Each gesture can be disabled/enabled + * by setting it's name (like `swipe`) to false. + * You can set the defaults for all instances by changing this object before creating an instance. + * @example + * ```` + * Hammer.defaults.drag = false; + * Hammer.defaults.behavior.touchAction = 'pan-y'; + * delete Hammer.defaults.behavior.userSelect; + * ```` + * @property defaults + * @type {Object} + */ +Hammer.defaults = { + /** + * this setting object adds styles and attributes to the element to prevent the browser from doing + * its native behavior. The css properties are auto prefixed for the browsers when needed. + * @property defaults.behavior + * @type {Object} + */ + behavior: { + /** + * Disables text selection to improve the dragging gesture. When the value is `none` it also sets + * `onselectstart=false` for IE on the element. Mainly for desktop browsers. + * @property defaults.behavior.userSelect + * @type {String} + * @default 'none' + */ + userSelect: 'none', + + /** + * Specifies whether and how a given region can be manipulated by the user (for instance, by panning or zooming). + * Used by Chrome 35> and IE10>. By default this makes the element blocking any touch event. + * @property defaults.behavior.touchAction + * @type {String} + * @default: 'pan-y' + */ + touchAction: 'pan-y', + + /** + * Disables the default callout shown when you touch and hold a touch target. + * On iOS, when you touch and hold a touch target such as a link, Safari displays + * a callout containing information about the link. This property allows you to disable that callout. + * @property defaults.behavior.touchCallout + * @type {String} + * @default 'none' + */ + touchCallout: 'none', + + /** + * Specifies whether zooming is enabled. Used by IE10> + * @property defaults.behavior.contentZooming + * @type {String} + * @default 'none' + */ + contentZooming: 'none', + + /** + * Specifies that an entire element should be draggable instead of its contents. + * Mainly for desktop browsers. + * @property defaults.behavior.userDrag + * @type {String} + * @default 'none' + */ + userDrag: 'none', + + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in Safari on iPhone. This property obeys the alpha value, if specified. + * + * If you don't specify an alpha value, Safari on iPhone applies a default alpha value + * to the color. To disable tap highlighting, set the alpha value to 0 (invisible). + * If you set the alpha value to 1.0 (opaque), the element is not visible when tapped. + * @property defaults.behavior.tapHighlightColor + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' + } +}; + +/** + * hammer document where the base events are added at + * @property DOCUMENT + * @type {HTMLElement} + * @default window.document + */ +Hammer.DOCUMENT = document; + +/** + * detect support for pointer events + * @property HAS_POINTEREVENTS + * @type {Boolean} + */ +Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; + +/** + * detect support for touch events + * @property HAS_TOUCHEVENTS + * @type {Boolean} + */ +Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); + +/** + * detect mobile browsers + * @property IS_MOBILE + * @type {Boolean} + */ +Hammer.IS_MOBILE = /mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent); + +/** + * detect if we want to support mouseevents at all + * @property NO_MOUSEEVENTS + * @type {Boolean} + */ +Hammer.NO_MOUSEEVENTS = (Hammer.HAS_TOUCHEVENTS && Hammer.IS_MOBILE) || Hammer.HAS_POINTEREVENTS; + +/** + * interval in which Hammer recalculates current velocity/direction/angle in ms + * @property CALCULATE_INTERVAL + * @type {Number} + * @default 25 + */ +Hammer.CALCULATE_INTERVAL = 25; + +/** + * eventtypes per touchevent (start, move, end) are filled by `Event.determineEventTypes` on `setup` + * the object contains the DOM event names per type (`EVENT_START`, `EVENT_MOVE`, `EVENT_END`) + * @property EVENT_TYPES + * @private + * @writeOnce + * @type {Object} + */ +var EVENT_TYPES = {}; + +/** + * direction strings, for safe comparisons + * @property DIRECTION_DOWN|LEFT|UP|RIGHT + * @final + * @type {String} + * @default 'down' 'left' 'up' 'right' + */ +var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down'; +var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left'; +var DIRECTION_UP = Hammer.DIRECTION_UP = 'up'; +var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right'; + +/** + * pointertype strings, for safe comparisons + * @property POINTER_MOUSE|TOUCH|PEN + * @final + * @type {String} + * @default 'mouse' 'touch' 'pen' + */ +var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse'; +var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch'; +var POINTER_PEN = Hammer.POINTER_PEN = 'pen'; + +/** + * eventtypes + * @property EVENT_START|MOVE|END|RELEASE|TOUCH + * @final + * @type {String} + * @default 'start' 'change' 'move' 'end' 'release' 'touch' + */ +var EVENT_START = Hammer.EVENT_START = 'start'; +var EVENT_MOVE = Hammer.EVENT_MOVE = 'move'; +var EVENT_END = Hammer.EVENT_END = 'end'; +var EVENT_RELEASE = Hammer.EVENT_RELEASE = 'release'; +var EVENT_TOUCH = Hammer.EVENT_TOUCH = 'touch'; + +/** + * if the window events are set... + * @property READY + * @writeOnce + * @type {Boolean} + * @default false + */ +Hammer.READY = false; + +/** + * plugins namespace + * @property plugins + * @type {Object} + */ +Hammer.plugins = Hammer.plugins || {}; + +/** + * gestures namespace + * see `/gestures` for the definitions + * @property gestures + * @type {Object} + */ +Hammer.gestures = Hammer.gestures || {}; + +/** + * setup events to detect gestures on the document + * this function is called when creating an new instance + * @private + */ +function setup() { + if(Hammer.READY) { + return; + } + + // find what eventtypes we add listeners to + Event.determineEventTypes(); + + // Register all gestures inside Hammer.gestures + Utils.each(Hammer.gestures, function(gesture) { + Detection.register(gesture); + }); + + // Add touch events on the document + Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect); + Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect); + + // Hammer is ready...! + Hammer.READY = true; +} + +/** + * @module hammer + * + * @class Utils + * @static + */ +var Utils = Hammer.utils = { + /** + * extend method, could also be used for cloning when `dest` is an empty object. + * changes the dest object + * @method extend + * @param {Object} dest + * @param {Object} src + * @param {Boolean} [merge=false] do a merge + * @return {Object} dest + */ + extend: function extend(dest, src, merge) { + for(var key in src) { + if(!src.hasOwnProperty(key) || (dest[key] !== undefined && merge)) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, + + /** + * simple addEventListener wrapper + * @method on + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + */ + on: function on(element, type, handler) { + element.addEventListener(type, handler, false); + }, + + /** + * simple removeEventListener wrapper + * @method off + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + */ + off: function off(element, type, handler) { + element.removeEventListener(type, handler, false); + }, + + /** + * forEach over arrays and objects + * @method each + * @param {Object|Array} obj + * @param {Function} iterator + * @param {any} iterator.item + * @param {Number} iterator.index + * @param {Object|Array} iterator.obj the source object + * @param {Object} context value to use as `this` in the iterator + */ + each: function each(obj, iterator, context) { + var i, len; + + // native forEach on arrays + if('forEach' in obj) { + obj.forEach(iterator, context); + // arrays + } else if(obj.length !== undefined) { + for(i = 0, len = obj.length; i < len; i++) { + if(iterator.call(context, obj[i], i, obj) === false) { + return; + } + } + // objects + } else { + for(i in obj) { + if(obj.hasOwnProperty(i) && + iterator.call(context, obj[i], i, obj) === false) { + return; + } + } + } + }, + + /** + * find if a string contains the string using indexOf + * @method inStr + * @param {String} src + * @param {String} find + * @return {Boolean} found + */ + inStr: function inStr(src, find) { + return src.indexOf(find) > -1; + }, + + /** + * find if a array contains the object using indexOf or a simple polyfill + * @method inArray + * @param {String} src + * @param {String} find + * @return {Boolean|Number} false when not found, or the index + */ + inArray: function inArray(src, find) { + if(src.indexOf) { + var index = src.indexOf(find); + return (index === -1) ? false : index; + } else { + for(var i = 0, len = src.length; i < len; i++) { + if(src[i] === find) { + return i; + } + } + return false; + } + }, + + /** + * convert an array-like object (`arguments`, `touchlist`) to an array + * @method toArray + * @param {Object} obj + * @return {Array} + */ + toArray: function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + }, + + /** + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found + */ + hasParent: function hasParent(node, parent) { + while(node) { + if(node == parent) { + return true; + } + node = node.parentNode; + } + return false; + }, + + /** + * get the center of all the touches + * @method getCenter + * @param {Array} touches + * @return {Object} center contains `pageX`, `pageY`, `clientX` and `clientY` properties + */ + getCenter: function getCenter(touches) { + var pageX = [], + pageY = [], + clientX = [], + clientY = [], + min = Math.min, + max = Math.max; + + // no need to loop when only one touch + if(touches.length === 1) { + return { + pageX: touches[0].pageX, + pageY: touches[0].pageY, + clientX: touches[0].clientX, + clientY: touches[0].clientY + }; + } + + Utils.each(touches, function(touch) { + pageX.push(touch.pageX); + pageY.push(touch.pageY); + clientX.push(touch.clientX); + clientY.push(touch.clientY); + }); + + return { + pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2, + pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2, + clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2, + clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2 + }; + }, + + /** + * calculate the velocity between two points. unit is in px per ms. + * @method getVelocity + * @param {Number} deltaTime + * @param {Number} deltaX + * @param {Number} deltaY + * @return {Object} velocity `x` and `y` + */ + getVelocity: function getVelocity(deltaTime, deltaX, deltaY) { + return { + x: Math.abs(deltaX / deltaTime) || 0, + y: Math.abs(deltaY / deltaTime) || 0 + }; + }, + + /** + * calculate the angle between two coordinates + * @method getAngle + * @param {Touch} touch1 + * @param {Touch} touch2 + * @return {Number} angle + */ + getAngle: function getAngle(touch1, touch2) { + var x = touch2.clientX - touch1.clientX, + y = touch2.clientY - touch1.clientY; + + return Math.atan2(y, x) * 180 / Math.PI; + }, + + /** + * do a small comparision to get the direction between two touches. + * @method getDirection + * @param {Touch} touch1 + * @param {Touch} touch2 + * @return {String} direction matches `DIRECTION_LEFT|RIGHT|UP|DOWN` + */ + getDirection: function getDirection(touch1, touch2) { + var x = Math.abs(touch1.clientX - touch2.clientX), + y = Math.abs(touch1.clientY - touch2.clientY); + + if(x >= y) { + return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN; + }, + + /** + * calculate the distance between two touches + * @method getDistance + * @param {Touch}touch1 + * @param {Touch} touch2 + * @return {Number} distance + */ + getDistance: function getDistance(touch1, touch2) { + var x = touch2.clientX - touch1.clientX, + y = touch2.clientY - touch1.clientY; + + return Math.sqrt((x * x) + (y * y)); + }, + + /** + * calculate the scale factor between two touchLists + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @method getScale + * @param {Array} start array of touches + * @param {Array} end array of touches + * @return {Number} scale + */ + getScale: function getScale(start, end) { + // need two fingers... + if(start.length >= 2 && end.length >= 2) { + return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]); + } + return 1; + }, + + /** + * calculate the rotation degrees between two touchLists + * @method getRotation + * @param {Array} start array of touches + * @param {Array} end array of touches + * @return {Number} rotation + */ + getRotation: function getRotation(start, end) { + // need two fingers + if(start.length >= 2 && end.length >= 2) { + return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]); + } + return 0; + }, + + /** + * find out if the direction is vertical * + * @method isVertical + * @param {String} direction matches `DIRECTION_UP|DOWN` + * @return {Boolean} is_vertical + */ + isVertical: function isVertical(direction) { + return direction == DIRECTION_UP || direction == DIRECTION_DOWN; + }, + + /** + * set css properties with their prefixes + * @param {HTMLElement} element + * @param {String} prop + * @param {String} value + * @param {Boolean} [toggle=true] + * @return {Boolean} + */ + setPrefixedCss: function setPrefixedCss(element, prop, value, toggle) { + var prefixes = ['', 'Webkit', 'Moz', 'O', 'ms']; + prop = Utils.toCamelCase(prop); + + for(var i = 0; i < prefixes.length; i++) { + var p = prop; + // prefixes + if(prefixes[i]) { + p = prefixes[i] + p.slice(0, 1).toUpperCase() + p.slice(1); + } + + // test the style + if(p in element.style) { + element.style[p] = (toggle == null || toggle) && value || ''; + break; + } + } + }, + + /** + * toggle browser default behavior by setting css properties. + * `userSelect='none'` also sets `element.onselectstart` to false + * `userDrag='none'` also sets `element.ondragstart` to false + * + * @method toggleBehavior + * @param {HtmlElement} element + * @param {Object} props + * @param {Boolean} [toggle=true] + */ + toggleBehavior: function toggleBehavior(element, props, toggle) { + if(!props || !element || !element.style) { + return; + } + + // set the css properties + Utils.each(props, function(value, prop) { + Utils.setPrefixedCss(element, prop, value, toggle); + }); + + var falseFn = toggle && function() { + return false; + }; + + // also the disable onselectstart + if(props.userSelect == 'none') { + element.onselectstart = falseFn; + } + // and disable ondragstart + if(props.userDrag == 'none') { + element.ondragstart = falseFn; + } + }, + + /** + * convert a string with underscores to camelCase + * so prevent_default becomes preventDefault + * @param {String} str + * @return {String} camelCaseStr + */ + toCamelCase: function toCamelCase(str) { + return str.replace(/[_-]([a-z])/g, function(s) { + return s[1].toUpperCase(); + }); + } +}; + + +/** + * @module hammer + */ +/** + * @class Event + * @static + */ +var Event = Hammer.event = { + /** + * when touch events have been fired, this is true + * this is used to stop mouse events + * @property prevent_mouseevents + * @private + * @type {Boolean} + */ + preventMouseEvents: false, + + /** + * if EVENT_START has been fired + * @property started + * @private + * @type {Boolean} + */ + started: false, + + /** + * when the mouse is hold down, this is true + * @property should_detect + * @private + * @type {Boolean} + */ + shouldDetect: false, + + /** + * simple event binder with a hook and support for multiple types + * @method on + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + * @param {Function} [hook] + * @param {Object} hook.type + */ + on: function on(element, type, handler, hook) { + var types = type.split(' '); + Utils.each(types, function(type) { + Utils.on(element, type, handler); + hook && hook(type); + }); + }, + + /** + * simple event unbinder with a hook and support for multiple types + * @method off + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + * @param {Function} [hook] + * @param {Object} hook.type + */ + off: function off(element, type, handler, hook) { + var types = type.split(' '); + Utils.each(types, function(type) { + Utils.off(element, type, handler); + hook && hook(type); + }); + }, + + /** + * the core touch event handler. + * this finds out if we should to detect gestures + * @method onTouch + * @param {HTMLElement} element + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Function} handler + * @return onTouchHandler {Function} the core event handler + */ + onTouch: function onTouch(element, eventType, handler) { + var self = this; + + var onTouchHandler = function onTouchHandler(ev) { + var srcType = ev.type.toLowerCase(), + isPointer = Hammer.HAS_POINTEREVENTS, + isMouse = Utils.inStr(srcType, 'mouse'), + triggerType; + + // if we are in a mouseevent, but there has been a touchevent triggered in this session + // we want to do nothing. simply break out of the event. + if(isMouse && self.preventMouseEvents) { + return; + + // mousebutton must be down + } else if(isMouse && eventType == EVENT_START && ev.button === 0) { + self.preventMouseEvents = false; + self.shouldDetect = true; + } else if(isPointer && eventType == EVENT_START) { + self.shouldDetect = (ev.buttons === 1 || PointerEvent.matchType(POINTER_TOUCH, ev)); + // just a valid start event, but no mouse + } else if(!isMouse && eventType == EVENT_START) { + self.preventMouseEvents = true; + self.shouldDetect = true; + } + + // update the pointer event before entering the detection + if(isPointer && eventType != EVENT_END) { + PointerEvent.updatePointer(eventType, ev); + } + + // we are in a touch/down state, so allowed detection of gestures + if(self.shouldDetect) { + triggerType = self.doDetect.call(self, ev, eventType, element, handler); + } + + // ...and we are done with the detection + // so reset everything to start each detection totally fresh + if(triggerType == EVENT_END) { + self.preventMouseEvents = false; + self.shouldDetect = false; + PointerEvent.reset(); + // update the pointerevent object after the detection + } + + if(isPointer && eventType == EVENT_END) { + PointerEvent.updatePointer(eventType, ev); + } + }; + + this.on(element, EVENT_TYPES[eventType], onTouchHandler); + return onTouchHandler; + }, + + /** + * the core detection method + * this finds out what hammer-touch-events to trigger + * @method doDetect + * @param {Object} ev + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {HTMLElement} element + * @param {Function} handler + * @return {String} triggerType matches `EVENT_START|MOVE|END` + */ + doDetect: function doDetect(ev, eventType, element, handler) { + var touchList = this.getTouchList(ev, eventType); + var touchListLength = touchList.length; + var triggerType = eventType; + var triggerChange = touchList.trigger; // used by fakeMultitouch plugin + var changedLength = touchListLength; + + // at each touchstart-like event we want also want to trigger a TOUCH event... + if(eventType == EVENT_START) { + triggerChange = EVENT_TOUCH; + // ...the same for a touchend-like event + } else if(eventType == EVENT_END) { + triggerChange = EVENT_RELEASE; + + // keep track of how many touches have been removed + changedLength = touchList.length - ((ev.changedTouches) ? ev.changedTouches.length : 1); + } + + // after there are still touches on the screen, + // we just want to trigger a MOVE event. so change the START or END to a MOVE + // but only after detection has been started, the first time we actualy want a START + if(changedLength > 0 && this.started) { + triggerType = EVENT_MOVE; + } + + // detection has been started, we keep track of this, see above + this.started = true; + + // generate some event data, some basic information + var evData = this.collectEventData(element, triggerType, touchList, ev); + + // trigger the triggerType event before the change (TOUCH, RELEASE) events + // but the END event should be at last + if(eventType != EVENT_END) { + handler.call(Detection, evData); + } + + // trigger a change (TOUCH, RELEASE) event, this means the length of the touches changed + if(triggerChange) { + evData.changedLength = changedLength; + evData.eventType = triggerChange; + + handler.call(Detection, evData); + + evData.eventType = triggerType; + delete evData.changedLength; + } + + // trigger the END event + if(triggerType == EVENT_END) { + handler.call(Detection, evData); + + // ...and we are done with the detection + // so reset everything to start each detection totally fresh + this.started = false; + } + + return triggerType; + }, + + /** + * we have different events for each device/browser + * determine what we need and set them in the EVENT_TYPES constant + * the `onTouch` method is bind to these properties. + * @method determineEventTypes + * @return {Object} events + */ + determineEventTypes: function determineEventTypes() { + var types; + if(Hammer.HAS_POINTEREVENTS) { + if(window.PointerEvent) { + types = [ + 'pointerdown', + 'pointermove', + 'pointerup pointercancel lostpointercapture' + ]; + } else { + types = [ + 'MSPointerDown', + 'MSPointerMove', + 'MSPointerUp MSPointerCancel MSLostPointerCapture' + ]; + } + } else if(Hammer.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel' + ]; + } else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup' + ]; + } + + EVENT_TYPES[EVENT_START] = types[0]; + EVENT_TYPES[EVENT_MOVE] = types[1]; + EVENT_TYPES[EVENT_END] = types[2]; + return EVENT_TYPES; + }, + + /** + * create touchList depending on the event + * @method getTouchList + * @param {Object} ev + * @param {String} eventType + * @return {Array} touches + */ + getTouchList: function getTouchList(ev, eventType) { + // get the fake pointerEvent touchlist + if(Hammer.HAS_POINTEREVENTS) { + return PointerEvent.getTouchList(); + } + + // get the touchlist + if(ev.touches) { + if(eventType == EVENT_MOVE) { + return ev.touches; + } + + var identifiers = []; + var concat = [].concat(Utils.toArray(ev.touches), Utils.toArray(ev.changedTouches)); + var touchList = []; + + Utils.each(concat, function(touch) { + if(Utils.inArray(identifiers, touch.identifier) === false) { + touchList.push(touch); + } + identifiers.push(touch.identifier); + }); + + return touchList; + } + + // make fake touchList from mouse position + ev.identifier = 1; + return [ev]; + }, + + /** + * collect basic event data + * @method collectEventData + * @param {HTMLElement} element + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Array} touches + * @param {Object} ev + * @return {Object} ev + */ + collectEventData: function collectEventData(element, eventType, touches, ev) { + // find out pointerType + var pointerType = POINTER_TOUCH; + if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) { + pointerType = POINTER_MOUSE; + } else if(PointerEvent.matchType(POINTER_PEN, ev)) { + pointerType = POINTER_PEN; + } + + return { + center: Utils.getCenter(touches), + timeStamp: Date.now(), + target: ev.target, + touches: touches, + eventType: eventType, + pointerType: pointerType, + srcEvent: ev, + + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + var srcEvent = this.srcEvent; + srcEvent.preventManipulation && srcEvent.preventManipulation(); + srcEvent.preventDefault && srcEvent.preventDefault(); + }, + + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, + + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return Detection.stopDetect(); + } + }; + } +}; + + +/** + * @module hammer + * + * @class PointerEvent + * @static + */ +var PointerEvent = Hammer.PointerEvent = { + /** + * holds all pointers, by `identifier` + * @property pointers + * @type {Object} + */ + pointers: {}, + + /** + * get the pointers as an array + * @method getTouchList + * @return {Array} touchlist + */ + getTouchList: function getTouchList() { + var touchlist = []; + // we can use forEach since pointerEvents only is in IE10 + Utils.each(this.pointers, function(pointer) { + touchlist.push(pointer); + }); + + return touchlist; + }, + + /** + * update the position of a pointer + * @method updatePointer + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Object} pointerEvent + */ + updatePointer: function updatePointer(eventType, pointerEvent) { + if(eventType == EVENT_END) { + delete this.pointers[pointerEvent.pointerId]; + } else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } + }, + + /** + * check if ev matches pointertype + * @method matchType + * @param {String} pointerType matches `POINTER_MOUSE|TOUCH|PEN` + * @param {PointerEvent} ev + */ + matchType: function matchType(pointerType, ev) { + if(!ev.pointerType) { + return false; + } + + var pt = ev.pointerType, + types = {}; + + types[POINTER_MOUSE] = (pt === (ev.MSPOINTER_TYPE_MOUSE || POINTER_MOUSE)); + types[POINTER_TOUCH] = (pt === (ev.MSPOINTER_TYPE_TOUCH || POINTER_TOUCH)); + types[POINTER_PEN] = (pt === (ev.MSPOINTER_TYPE_PEN || POINTER_PEN)); + return types[pointerType]; + }, + + /** + * reset the stored pointers + * @method reset + */ + reset: function resetList() { + this.pointers = {}; + } +}; + + +/** + * @module hammer + * + * @class Detection + * @static + */ +var Detection = Hammer.detection = { + // contains all registred Hammer.gestures in the correct order + gestures: [], + + // data of the current Hammer.gesture detection session + current: null, + + // the previous Hammer.gesture session data + // is a full clone of the previous gesture.current object + previous: null, + + // when this becomes true, no gestures are fired + stopped: false, + + /** + * start Hammer.gesture detection + * @method startDetect + * @param {Hammer.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a Hammer.gesture detection on an element + if(this.current) { + return; + } + + this.stopped = false; + + // holds current session + this.current = { + inst: inst, // reference to HammerInstance we're working for + startEvent: Utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent: false, // last eventData + lastCalcEvent: false, // last eventData for calculations. + futureCalcEvent: false, // last eventData for calculations. + lastCalcData: {}, // last lastCalcData + name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; + + this.detect(eventData); + }, + + /** + * Hammer.gesture detection + * @method detect + * @param {Object} eventData + * @return {any} + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return; + } + + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); + + // hammer instance and instance options + var inst = this.current.inst, + instOptions = inst.options; + + // call Hammer.gesture handlers + Utils.each(this.gestures, function triggerGesture(gesture) { + // only when the instance options have enabled this gesture + if(!this.stopped && inst.enabled && instOptions[gesture.name]) { + gesture.handler.call(gesture, eventData, inst); + } + }, this); + + // store as previous event event + if(this.current) { + this.current.lastEvent = eventData; + } + + if(eventData.eventType == EVENT_END) { + this.stopDetect(); + } + + return eventData; + }, + + /** + * clear the Hammer.gesture vars + * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected + * to stop other Hammer.gestures from being fired + * @method stopDetect + */ + stopDetect: function stopDetect() { + // clone current data to the store as the previous gesture + // used for the double tap gesture, since this is an other gesture detect session + this.previous = Utils.extend({}, this.current); + + // reset the current + this.current = null; + this.stopped = true; + }, + + /** + * calculate velocity, angle and direction + * @method getVelocityData + * @param {Object} ev + * @param {Object} center + * @param {Number} deltaTime + * @param {Number} deltaX + * @param {Number} deltaY + */ + getCalculatedData: function getCalculatedData(ev, center, deltaTime, deltaX, deltaY) { + var cur = this.current, + recalc = false, + calcEv = cur.lastCalcEvent, + calcData = cur.lastCalcData; + + if(calcEv && ev.timeStamp - calcEv.timeStamp > Hammer.CALCULATE_INTERVAL) { + center = calcEv.center; + deltaTime = ev.timeStamp - calcEv.timeStamp; + deltaX = ev.center.clientX - calcEv.center.clientX; + deltaY = ev.center.clientY - calcEv.center.clientY; + recalc = true; + } + + if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { + cur.futureCalcEvent = ev; + } + + if(!cur.lastCalcEvent || recalc) { + calcData.velocity = Utils.getVelocity(deltaTime, deltaX, deltaY); + calcData.angle = Utils.getAngle(center, ev.center); + calcData.direction = Utils.getDirection(center, ev.center); + + cur.lastCalcEvent = cur.futureCalcEvent || ev; + cur.futureCalcEvent = ev; + } + + ev.velocityX = calcData.velocity.x; + ev.velocityY = calcData.velocity.y; + ev.interimAngle = calcData.angle; + ev.interimDirection = calcData.direction; + }, + + /** + * extend eventData for Hammer.gestures + * @method extendEventData + * @param {Object} ev + * @return {Object} ev + */ + extendEventData: function extendEventData(ev) { + var cur = this.current, + startEv = cur.startEvent, + lastEv = cur.lastEvent || startEv; + + // update the start touchlist to calculate the scale/rotation + if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { + startEv.touches = []; + Utils.each(ev.touches, function(touch) { + startEv.touches.push({ + clientX: touch.clientX, + clientY: touch.clientY + }); + }); + } + + var deltaTime = ev.timeStamp - startEv.timeStamp, + deltaX = ev.center.clientX - startEv.center.clientX, + deltaY = ev.center.clientY - startEv.center.clientY; + + this.getCalculatedData(ev, lastEv.center, deltaTime, deltaX, deltaY); + + Utils.extend(ev, { + startEvent: startEv, + + deltaTime: deltaTime, + deltaX: deltaX, + deltaY: deltaY, + + distance: Utils.getDistance(startEv.center, ev.center), + angle: Utils.getAngle(startEv.center, ev.center), + direction: Utils.getDirection(startEv.center, ev.center), + scale: Utils.getScale(startEv.touches, ev.touches), + rotation: Utils.getRotation(startEv.touches, ev.touches) + }); + + return ev; + }, + + /** + * register new gesture + * @method register + * @param {Object} gesture object, see `gestures/` for documentation + * @return {Array} gestures + */ + register: function register(gesture) { + // add an enable gesture options if there is no given + var options = gesture.defaults || {}; + if(options[gesture.name] === undefined) { + options[gesture.name] = true; + } + + // extend Hammer default options with the Hammer.gesture options + Utils.extend(Hammer.defaults, options, true); + + // set its index + gesture.index = gesture.index || 1000; + + // add Hammer.gesture to the list + this.gestures.push(gesture); + + // sort the list by index + this.gestures.sort(function(a, b) { + if(a.index < b.index) { + return -1; + } + if(a.index > b.index) { + return 1; + } + return 0; + }); + + return this.gestures; + } +}; + + +/** + * @module hammer + */ + +/** + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * + * @class Instance + * @constructor + * @param {HTMLElement} element + * @param {Object} [options={}] options are merged with `Hammer.defaults` + * @return {Hammer.Instance} + */ +Hammer.Instance = function(element, options) { + var self = this; + + // setup HammerJS window events and register all gestures + // this also sets up the default options + setup(); + + /** + * @property element + * @type {HTMLElement} + */ + this.element = element; + + /** + * @property enabled + * @type {Boolean} + * @protected + */ + this.enabled = true; + + /** + * options, merged with the defaults + * options with an _ are converted to camelCase + * @property options + * @type {Object} + */ + Utils.each(options, function(value, name) { + delete options[name]; + options[Utils.toCamelCase(name)] = value; + }); + + this.options = Utils.extend(Utils.extend({}, Hammer.defaults), options || {}); + + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.behavior) { + Utils.toggleBehavior(this.element, this.options.behavior, true); + } + + /** + * event start handler on the element to start the detection + * @property eventStartHandler + * @type {Object} + */ + this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) { + if(self.enabled && ev.eventType == EVENT_START) { + Detection.startDetect(self, ev); + } else if(ev.eventType == EVENT_TOUCH) { + Detection.detect(ev); + } + }); + + /** + * keep a list of user event handlers which needs to be removed when calling 'dispose' + * @property eventHandlers + * @type {Array} + */ + this.eventHandlers = []; +}; + +Hammer.Instance.prototype = { + /** + * bind events to the instance + * @method on + * @chainable + * @param {String} gestures multiple gestures by splitting with a space + * @param {Function} handler + * @param {Object} handler.ev event object + */ + on: function onEvent(gestures, handler) { + var self = this; + Event.on(self.element, gestures, handler, function(type) { + self.eventHandlers.push({ gesture: type, handler: handler }); + }); + return self; + }, + + /** + * unbind events to the instance + * @method off + * @chainable + * @param {String} gestures + * @param {Function} handler + */ + off: function offEvent(gestures, handler) { + var self = this; + + Event.off(self.element, gestures, handler, function(type) { + var index = Utils.inArray({ gesture: type, handler: handler }); + if(index !== false) { + self.eventHandlers.splice(index, 1); + } + }); + return self; + }, + + /** + * trigger gesture event + * @method trigger + * @chainable + * @param {String} gesture + * @param {Object} [eventData] + */ + trigger: function triggerEvent(gesture, eventData) { + // optional + if(!eventData) { + eventData = {}; + } + + // create DOM event + var event = Hammer.DOCUMENT.createEvent('Event'); + event.initEvent(gesture, true, true); + event.gesture = eventData; + + // trigger on the target if it is in the instance element, + // this is for event delegation tricks + var element = this.element; + if(Utils.hasParent(eventData.target, element)) { + element = eventData.target; + } + + element.dispatchEvent(event); + return this; + }, + + /** + * enable of disable hammer.js detection + * @method enable + * @chainable + * @param {Boolean} state + */ + enable: function enable(state) { + this.enabled = state; + return this; + }, + + /** + * dispose this hammer instance + * @method dispose + * @return {Null} + */ + dispose: function dispose() { + var i, eh; + + // undo all changes made by stop_browser_behavior + Utils.toggleBehavior(this.element, this.options.behavior, false); + + // unbind all custom event handlers + for(i = -1; (eh = this.eventHandlers[++i]);) { + Utils.off(this.element, eh.gesture, eh.handler); + } + + this.eventHandlers = []; + + // unbind the start event listener + Event.off(this.element, EVENT_TYPES[EVENT_START], this.eventStartHandler); + + return null; + } +}; + + +/** + * @module gestures + */ +/** + * Move with x fingers (default 1) around on the page. + * Preventing the default browser behavior is a good way to improve feel and working. + * ```` + * hammertime.on("drag", function(ev) { + * console.log(ev); + * ev.gesture.preventDefault(); + * }); + * ```` + * + * @class Drag + * @static + */ +/** + * @event drag + * @param {Object} ev + */ +/** + * @event dragstart + * @param {Object} ev + */ +/** + * @event dragend + * @param {Object} ev + */ +/** + * @event drapleft + * @param {Object} ev + */ +/** + * @event dragright + * @param {Object} ev + */ +/** + * @event dragup + * @param {Object} ev + */ +/** + * @event dragdown + * @param {Object} ev + */ + +/** + * @param {String} name + */ +(function(name) { + var triggered = false; + + function dragGesture(ev, inst) { + var cur = Detection.current; + + // max touches + if(inst.options.dragMaxTouches > 0 && + ev.touches.length > inst.options.dragMaxTouches) { + return; + } + + switch(ev.eventType) { + case EVENT_START: + triggered = false; + break; + + case EVENT_MOVE: + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.distance < inst.options.dragMinDistance && + cur.name != name) { + return; + } + + var startCenter = cur.startEvent.center; + + // we are dragging! + if(cur.name != name) { + cur.name = name; + if(inst.options.dragDistanceCorrection && ev.distance > 0) { + // When a drag is triggered, set the event center to dragMinDistance pixels from the original event center. + // Without this correction, the dragged distance would jumpstart at dragMinDistance pixels instead of at 0. + // It might be useful to save the original start point somewhere + var factor = Math.abs(inst.options.dragMinDistance / ev.distance); + startCenter.pageX += ev.deltaX * factor; + startCenter.pageY += ev.deltaY * factor; + startCenter.clientX += ev.deltaX * factor; + startCenter.clientY += ev.deltaY * factor; + + // recalculate event data using new start point + ev = Detection.extendEventData(ev); + } + } + + // lock drag to axis? + if(cur.lastEvent.dragLockToAxis || + ( inst.options.dragLockToAxis && + inst.options.dragLockMinDistance <= ev.distance + )) { + ev.dragLockToAxis = true; + } + + // keep direction on the axis that the drag gesture started on + var lastDirection = cur.lastEvent.direction; + if(ev.dragLockToAxis && lastDirection !== ev.direction) { + if(Utils.isVertical(lastDirection)) { + ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN; + } else { + ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + } + + // first time, trigger dragstart event + if(!triggered) { + inst.trigger(name + 'start', ev); + triggered = true; + } + + // trigger events + inst.trigger(name, ev); + inst.trigger(name + ev.direction, ev); + + var isVertical = Utils.isVertical(ev.direction); + + // block the browser events + if((inst.options.dragBlockVertical && isVertical) || + (inst.options.dragBlockHorizontal && !isVertical)) { + ev.preventDefault(); + } + break; + + case EVENT_RELEASE: + if(triggered && ev.changedLength <= inst.options.dragMaxTouches) { + inst.trigger(name + 'end', ev); + triggered = false; + } + break; + + case EVENT_END: + triggered = false; + break; + } + } + + Hammer.gestures.Drag = { + name: name, + index: 50, + handler: dragGesture, + defaults: { + /** + * minimal movement that have to be made before the drag event gets triggered + * @property dragMinDistance + * @type {Number} + * @default 10 + */ + dragMinDistance: 10, + + /** + * Set dragDistanceCorrection to true to make the starting point of the drag + * be calculated from where the drag was triggered, not from where the touch started. + * Useful to avoid a jerk-starting drag, which can make fine-adjustments + * through dragging difficult, and be visually unappealing. + * @property dragDistanceCorrection + * @type {Boolean} + * @default true + */ + dragDistanceCorrection: true, + + /** + * set 0 for unlimited, but this can conflict with transform + * @property dragMaxTouches + * @type {Number} + * @default 1 + */ + dragMaxTouches: 1, + + /** + * prevent default browser behavior when dragging occurs + * be careful with it, it makes the element a blocking element + * when you are using the drag gesture, it is a good practice to set this true + * @property dragBlockHorizontal + * @type {Boolean} + * @default false + */ + dragBlockHorizontal: false, + + /** + * same as `dragBlockHorizontal`, but for vertical movement + * @property dragBlockVertical + * @type {Boolean} + * @default false + */ + dragBlockVertical: false, + + /** + * dragLockToAxis keeps the drag gesture on the axis that it started on, + * It disallows vertical directions if the initial direction was horizontal, and vice versa. + * @property dragLockToAxis + * @type {Boolean} + * @default false + */ + dragLockToAxis: false, + + /** + * drag lock only kicks in when distance > dragLockMinDistance + * This way, locking occurs only when the distance has become large enough to reliably determine the direction + * @property dragLockMinDistance + * @type {Number} + * @default 25 + */ + dragLockMinDistance: 25 + } + }; +})('drag'); + +/** + * @module gestures + */ +/** + * trigger a simple gesture event, so you can do anything in your handler. + * only usable if you know what your doing... + * + * @class Gesture + * @static + */ +/** + * @event gesture + * @param {Object} ev + */ +Hammer.gestures.Gesture = { + name: 'gesture', + index: 1337, + handler: function releaseGesture(ev, inst) { + inst.trigger(this.name, ev); + } +}; + +/** + * @module gestures + */ +/** + * Touch stays at the same place for x time + * + * @class Hold + * @static + */ +/** + * @event hold + * @param {Object} ev + */ + +/** + * @param {String} name + */ +(function(name) { + var timer; + + function holdGesture(ev, inst) { + var options = inst.options, + current = Detection.current; + + switch(ev.eventType) { + case EVENT_START: + clearTimeout(timer); + + // set the gesture so we can check in the timeout if it still is + current.name = name; + + // set timer and if after the timeout it still is hold, + // we trigger the hold event + timer = setTimeout(function() { + if(current && current.name == name) { + inst.trigger(name, ev); + } + }, options.holdTimeout); + break; + + case EVENT_MOVE: + if(ev.distance > options.holdThreshold) { + clearTimeout(timer); + } + break; + + case EVENT_RELEASE: + clearTimeout(timer); + break; + } + } + + Hammer.gestures.Hold = { + name: name, + index: 10, + defaults: { + /** + * @property holdTimeout + * @type {Number} + * @default 500 + */ + holdTimeout: 500, + + /** + * movement allowed while holding + * @property holdThreshold + * @type {Number} + * @default 2 + */ + holdThreshold: 2 + }, + handler: holdGesture + }; +})('hold'); + +/** + * @module gestures + */ +/** + * when a touch is being released from the page + * + * @class Release + * @static + */ +/** + * @event release + * @param {Object} ev + */ +Hammer.gestures.Release = { + name: 'release', + index: Infinity, + handler: function releaseGesture(ev, inst) { + if(ev.eventType == EVENT_RELEASE) { + inst.trigger(this.name, ev); + } + } +}; + +/** + * @module gestures + */ +/** + * triggers swipe events when the end velocity is above the threshold + * for best usage, set `preventDefault` (on the drag gesture) to `true` + * ```` + * hammertime.on("dragleft swipeleft", function(ev) { + * console.log(ev); + * ev.gesture.preventDefault(); + * }); + * ```` + * + * @class Swipe + * @static + */ +/** + * @event swipe + * @param {Object} ev + */ +/** + * @event swipeleft + * @param {Object} ev + */ +/** + * @event swiperight + * @param {Object} ev + */ +/** + * @event swipeup + * @param {Object} ev + */ +/** + * @event swipedown + * @param {Object} ev + */ +Hammer.gestures.Swipe = { + name: 'swipe', + index: 40, + defaults: { + /** + * @property swipeMinTouches + * @type {Number} + * @default 1 + */ + swipeMinTouches: 1, + + /** + * @property swipeMaxTouches + * @type {Number} + * @default 1 + */ + swipeMaxTouches: 1, + + /** + * horizontal swipe velocity + * @property swipeVelocityX + * @type {Number} + * @default 0.6 + */ + swipeVelocityX: 0.2, + + /** + * vertical swipe velocity + * @property swipeVelocityY + * @type {Number} + * @default 0.6 + */ + swipeVelocityY: 0.2 + }, + + handler: function swipeGesture(ev, inst) { + if(ev.eventType == EVENT_RELEASE) { + var touches = ev.touches.length, + options = inst.options; + + // max touches + if(touches < options.swipeMinTouches || + touches > options.swipeMaxTouches) { + return; + } + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.velocityX > options.swipeVelocityX || + ev.velocityY > options.swipeVelocityY) { + // trigger swipe events + inst.trigger(this.name, ev); + inst.trigger(this.name + ev.direction, ev); + } + } + } +}; + +/** + * @module gestures + */ +/** + * Single tap and a double tap on a place + * + * @class Tap + * @static + */ +/** + * @event tap + * @param {Object} ev + */ +/** + * @event doubletap + * @param {Object} ev + */ + +/** + * @param {String} name + */ +(function(name) { + var hasMoved = false; + + function tapGesture(ev, inst) { + var options = inst.options, + current = Detection.current, + prev = Detection.previous, + sincePrev, + didDoubleTap; + + switch(ev.eventType) { + case EVENT_START: + hasMoved = false; + break; + + case EVENT_MOVE: + hasMoved = hasMoved || (ev.distance > options.tapMaxDistance); + break; + + case EVENT_END: + if(!Utils.inStr(ev.srcEvent.type, 'cancel') && ev.deltaTime < options.tapMaxTime && !hasMoved) { + // previous gesture, for the double tap since these are two different gesture detections + sincePrev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp; + didDoubleTap = false; + + // check if double tap + if(prev && prev.name == name && + (sincePrev && sincePrev < options.doubleTapInterval) && + ev.distance < options.doubleTapDistance) { + inst.trigger('doubletap', ev); + didDoubleTap = true; + } + + // do a single tap + if(!didDoubleTap || options.tapAlways) { + current.name = name; + inst.trigger(current.name, ev); + } + } + break; + } + } + + Hammer.gestures.Tap = { + name: name, + index: 100, + handler: tapGesture, + defaults: { + /** + * max time of a tap, this is for the slow tappers + * @property tapMaxTime + * @type {Number} + * @default 250 + */ + tapMaxTime: 250, + + /** + * max distance of movement of a tap, this is for the slow tappers + * @property tapMaxDistance + * @type {Number} + * @default 10 + */ + tapMaxDistance: 10, + + /** + * always trigger the `tap` event, even while double-tapping + * @property tapAlways + * @type {Boolean} + * @default true + */ + tapAlways: true, + + /** + * max distance between two taps + * @property doubleTapDistance + * @type {Number} + * @default 20 + */ + doubleTapDistance: 20, + + /** + * max time between two taps + * @property doubleTapInterval + * @type {Number} + * @default 300 + */ + doubleTapInterval: 300 + } + }; +})('tap'); + +/** + * @module gestures + */ +/** + * when a touch is being touched at the page + * + * @class Touch + * @static + */ +/** + * @event touch + * @param {Object} ev + */ +Hammer.gestures.Touch = { + name: 'touch', + index: -Infinity, + defaults: { + /** + * call preventDefault at touchstart, and makes the element blocking by disabling the scrolling of the page, + * but it improves gestures like transforming and dragging. + * be careful with using this, it can be very annoying for users to be stuck on the page + * @property preventDefault + * @type {Boolean} + * @default false + */ + preventDefault: false, + + /** + * disable mouse events, so only touch (or pen!) input triggers events + * @property preventMouse + * @type {Boolean} + * @default false + */ + preventMouse: false + }, + handler: function touchGesture(ev, inst) { + if(inst.options.preventMouse && ev.pointerType == POINTER_MOUSE) { + ev.stopDetect(); + return; + } + + if(inst.options.preventDefault) { + ev.preventDefault(); + } + + if(ev.eventType == EVENT_TOUCH) { + inst.trigger('touch', ev); + } + } +}; + +/** + * @module gestures + */ +/** + * User want to scale or rotate with 2 fingers + * Preventing the default browser behavior is a good way to improve feel and working. This can be done with the + * `preventDefault` option. + * + * @class Transform + * @static + */ +/** + * @event transform + * @param {Object} ev + */ +/** + * @event transformstart + * @param {Object} ev + */ +/** + * @event transformend + * @param {Object} ev + */ +/** + * @event pinchin + * @param {Object} ev + */ +/** + * @event pinchout + * @param {Object} ev + */ +/** + * @event rotate + * @param {Object} ev + */ + +/** + * @param {String} name + */ +(function(name) { + var triggered = false; + + function transformGesture(ev, inst) { + switch(ev.eventType) { + case EVENT_START: + triggered = false; + break; + + case EVENT_MOVE: + // at least multitouch + if(ev.touches.length < 2) { + return; + } + + var scaleThreshold = Math.abs(1 - ev.scale); + var rotationThreshold = Math.abs(ev.rotation); + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(scaleThreshold < inst.options.transformMinScale && + rotationThreshold < inst.options.transformMinRotation) { + return; + } + + // we are transforming! + Detection.current.name = name; + + // first time, trigger dragstart event + if(!triggered) { + inst.trigger(name + 'start', ev); + triggered = true; + } + + inst.trigger(name, ev); // basic transform event + + // trigger rotate event + if(rotationThreshold > inst.options.transformMinRotation) { + inst.trigger('rotate', ev); + } + + // trigger pinch event + if(scaleThreshold > inst.options.transformMinScale) { + inst.trigger('pinch', ev); + inst.trigger('pinch' + (ev.scale < 1 ? 'in' : 'out'), ev); + } + break; + + case EVENT_RELEASE: + if(triggered && ev.changedLength < 2) { + inst.trigger(name + 'end', ev); + triggered = false; + } + break; + } + } + + Hammer.gestures.Transform = { + name: name, + index: 45, + defaults: { + /** + * minimal scale factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 + * @property transformMinScale + * @type {Number} + * @default 0.01 + */ + transformMinScale: 0.01, + + /** + * rotation in degrees + * @property transformMinRotation + * @type {Number} + * @default 1 + */ + transformMinRotation: 1 + }, + + handler: transformGesture + }; +})('transform'); + +/** + * @module hammer + */ + +// AMD export +if(typeof define == 'function' && define.amd) { + define(function() { + return Hammer; + }); +// commonjs export +} else if(typeof module !== 'undefined' && module.exports) { + module.exports = Hammer; +// browser export +} else { + window.Hammer = Hammer; +} + +})(window); \ No newline at end of file diff --git a/js/jester.js b/js/jester.js deleted file mode 100644 index 4612443..0000000 --- a/js/jester.js +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Jester JavaScript Library v0.3 - * http://github.com/plainview/Jester - * - * Easy JavaScript gesture recognition. - * - * Released under MIT License - * - * Copyright (C) 2011 by Scott Seaward - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -(function(container, undefined) { - var Jester = container.Jester = { - cache : {}, - cacheId : "Jester" + (new Date()).getTime(), - guid : 0, - - // The Jester constructor - Watcher : function(element, options) { - - var that = this, - cacheId = Jester.cacheId, - cache = Jester.cache, - gestures = "swipe flick tap doubletap pinchnarrow pinchwiden pinchend"; - - if(!element || !element.nodeType) { - throw new TypeError("Jester: no element given."); - } - - // if this element hasn't had Jester called on it before, - // set it up with a cache entry and give it the expando - if(typeof element[cacheId] !== "number") { - element[cacheId] = Jester.guid; - Jester.guid++; - } - - var elementId = element[cacheId]; - - if(!(elementId in cache)) { - Jester.cache[elementId] = {}; - } - - var elementCache = Jester.cache[elementId]; - - if(!("options" in elementCache)) { - elementCache.options = {}; - } - - options = options || elementCache.options || {}; - - // cache the option values for reuse or, if options already - // exist for this element, replace those that have been - // specified - if(elementCache.options !== options) { - for(var prop in options) { - if(elementCache.options[prop]) { - if(elementCache.options[prop] !== options[prop]) { - elementCache.options[prop] = options[prop]; - } - } - else { - elementCache.options[prop] = options[prop]; - } - } - } - - if(!("eventSet" in elementCache) || !(elementCache.eventSet instanceof Jester.EventSet)) { - elementCache.eventSet = new Jester.EventSet(element); - } - - if(!elementCache.touchMonitor) { - elementCache.touchMonitor = new Jester.TouchMonitor(element); - } - - var events = elementCache.eventSet; - var touches = elementCache.touchMonitor; - - this.id = element[cacheId]; - - this.bind = function(evt, fn) { - if(evt && typeof evt === "string" && typeof fn === "function") { - events.register(evt, fn); - } - return this; - }; - - // create shortcut bind methods for all gestures - gestures.split(" ").forEach(function(gesture) { - this[gesture] = function(fn) { - return this.bind(gesture, fn); - }; - }, that); - - this.start = function(fn) { - return this.bind("start", fn); - }; - - this.during = function(fn) { - return this.bind("during", fn); - }; - - this.end = function(fn) { - return this.bind("end", fn); - }; - - // wrapper to cover all three pinch methods - this.pinch = function(fns) { - if(typeof fns !== "undefined") { - // if its just a function it gets assigned to pinchend - if(typeof fns === "function") { - that.pinchend(fns); - } - else if(typeof fns === "object") { - var method; - "narrow widen end".split(" ").forEach(function(eventExt) { - method = "pinch" + eventExt; - if(typeof fns[eventExt] === "function") { - that[method](fns[eventExt]); - } - }); - } - } - }; - - this.halt = function() { - touches.stopListening(); - events.clear(); - delete elementCache.eventSet; - delete elementCache.touchMonitor; - }; - }, - EventSet : function(element) { - // all event names and their associated functions in an array i.e. "swipe" : [fn1, fn2, fn2] - var eventsTable = {}; - this.eventsTable = eventsTable; - - // register a handler with an event - this.register = function(eventName, fn) { - // if the event exists and has handlers attached to it, add this one to the array of them - if(eventsTable[eventName] && eventsTable[eventName].push) { - // make sure multiple copies of the same handler aren't inserted - if(!~eventsTable[eventName].indexOf(fn)) { - eventsTable[eventName].push(fn); - } - } - else { - // create a new array bound to the event containing only the handler passed in - eventsTable[eventName] = [fn]; - } - }; - - this.release = function(eventName, fn) { - if(typeof eventName === "undefined") return; - - // if a handler hasn't been specified, remove all handlers - if(typeof fn === "undefined") { - for(var handlers in eventsTable.eventName) { - delete eventsTable.eventName[handlers]; - } - } - else { - // pull the given handler from the given event - if(eventsTable[eventName] && ~eventsTable[eventName].indexOf(fn)) - { - eventsTable[eventName].splice(eventsTable[eventName].indexOf(fn), 1); - } - } - - // if the event has no more handlers registered to it, get rid of the event completely - if(eventsTable[eventName] && eventsTable[eventName].length === 0) { - delete eventsTable[eventName]; - } - }; - - // completely remove all events and their handlers - this.clear = function() { - var events; - for(events in eventsTable) { - delete eventsTable[events]; - } - }; - - // get all the handlers associated with an event - // return an empty array if nothing is registered with the given event name - this.getHandlers = function(eventName) { - if(eventsTable[eventName] && eventsTable[eventName].length) { - return eventsTable[eventName]; - } - else { - return []; - } - }; - - // inject an array of handlers into the event table for the given event - // this will klobber all current handlers associated with the event - this.setHandlers = function(eventName, handlers) { - eventsTable[eventName] = handlers; - }; - - // execute all handlers associated with an event, passing each handler the arguments provided after the event's name. - this.execute = function(eventName) { - if(typeof eventName === "undefined") return; - - // if the event asked for exists in the events table - if(eventsTable[eventName] && eventsTable[eventName].length) { - // get the arguments sent to the function - var args = Array.prototype.slice.call(arguments, 1); - - // iterate throuh all the handlers - for(var i = 0; i < eventsTable[eventName].length; i++) { - // check current handler is a function - if(typeof eventsTable[eventName][i] === "function") { - // execute handler with the provided arguments - eventsTable[eventName][i].apply(element, args); - } - } - } - }; - }, - - TouchMonitor : function(element) - { - var cacheId = Jester.cacheId, - elementId = element[cacheId], - cache = Jester.cache, - elementCache = cache[elementId], - opts = elementCache.options; - - opts.move = opts.move || {}; - opts.scale = opts.scale || {}; - - opts.tapDistance = opts.tapDistance || 0; - opts.tapTime = opts.tapTime || 20; - - opts.doubleTapTime = opts.doubleTapTime || 300; - - opts.swipeDistance = opts.swipeDistance || 200; - - opts.flickTime = opts.flickTime || 300; - opts.flickDistance = opts.flickDistance || 100; - - opts.deadX = opts.deadX || 0; - opts.deadY = opts.deadY || 0; - - if(opts.capture !== false) opts.capture = true; - if(typeof opts.preventDefault !== "undefined" && opts.preventDefault !== false) opts.preventDefault = true; - if(typeof opts.preventDefault !== "undefined" && opts.stopPropagation !== false) opts.stopPropagation = true; - - var eventSet = elementCache.eventSet; - - var touches; - var previousTapTime = 0; - - var touchStart = function(evt) { - touches = new Jester.TouchGroup(evt); - - eventSet.execute("start", touches, evt); - - if(opts.preventDefault) evt.preventDefault(); - if(opts.stopPropagation) evt.stopPropagation(); - }; - - var touchMove = function(evt) { - touches.update(evt); - - eventSet.execute("during", touches, evt); - - if(opts.preventDefault) evt.preventDefault(); - if(opts.stopPropagation) evt.stopPropagation(); - - if(touches.numTouches() == 2) { - // pinchnarrow - if(touches.delta.scale() < 0.0) { - eventSet.execute("pinchnarrow", touches); - } - - // pinchwiden - else if(touches.delta.scale() > 0.0) { - eventSet.execute("pinchwiden", touches); - } - } - }; - - var touchEnd = function(evt) { - - var swipeDirection; - - eventSet.execute("end", touches, evt); - - if(opts.preventDefault) evt.preventDefault(); - if(opts.stopPropagation) evt.stopPropagation(); - - if(touches.numTouches() == 1) { - // tap - if(touches.touch(0).total.x() <= opts.tapDistance && touches.touch(0).total.y() <= opts.tapDistance && touches.touch(0).total.time() < opts.tapTime) { - eventSet.execute("tap", touches); - } - - // doubletap - if(touches.touch(0).total.time() < opts.tapTime) { - var now = (new Date()).getTime(); - if(now - previousTapTime <= opts.doubleTapTime) { - eventSet.execute("doubletap", touches); - } - previousTapTime = now; - } - - // swipe left/right - if(Math.abs(touches.touch(0).total.x()) >= opts.swipeDistance) { - swipeDirection = touches.touch(0).total.x() < 0 ? "left" : "right"; - eventSet.execute("swipe", touches, swipeDirection); - } - - // swipe up/down - if(Math.abs(touches.touch(0).total.y()) >= opts.swipeDistance) { - swipeDirection = touches.touch(0).total.y() < 0 ? "up" : "down"; - eventSet.execute("swipe", touches, swipeDirection); - } - - // flick - if(Math.abs(touches.touch(0).total.x()) >= opts.flickDistance && touches.touch(0).total.time() <= opts.flickTime) { - var flickDirection = touches.touch(0).total.x() < 0 ? "left" : "right"; - eventSet.execute("flick", touches, flickDirection); - } - } - else if(touches.numTouches() == 2) { - // pinchend - if(touches.current.scale() !== 1.0) { - var pinchDirection = touches.current.scale() < 1.0 ? "narrowed" : "widened"; - eventSet.execute("pinchend", touches, pinchDirection); - } - } - }; - - var stopListening = function() { - element.removeEventListener("touchstart", touchStart, opts.capture); - element.removeEventListener("touchmove", touchMove, opts.capture); - element.removeEventListener("touchend", touchEnd, opts.capture); - }; - - element.addEventListener("touchstart", touchStart, opts.capture); - element.addEventListener("touchmove", touchMove, opts.capture); - element.addEventListener("touchend", touchEnd, opts.capture); - - return { - stopListening: stopListening - }; - }, - - TouchGroup : function(event) { - var that = this; - - var numTouches = event.touches.length; - - var midpointX = 0; - var midpointY = 0; - - var scale = event.scale; - var prevScale = scale; - var deltaScale = scale; - - for(var i = 0; i < numTouches; i++) { - this["touch" + i] = new Jester.Touch(event.touches[i].pageX, event.touches[i].pageY); - midpointX = event.touches[i].pageX; - midpointY = event.touches[i].pageY; - } - - function getNumTouches() { - return numTouches; - } - - function getTouch(num) { - return that["touch" + num]; - } - - function getMidPointX() { - return midpointX; - } - function getMidPointY() { - return midpointY; - } - function getScale() { - return scale; - } - function getDeltaScale() { - return deltaScale; - } - - function updateTouches(event) { - var mpX = 0; - var mpY = 0; - - for(var i = 0; i < event.touches.length; i++) { - if(i < numTouches) { - that["touch" + i].update(event.touches[i].pageX, event.touches[i].pageY); - mpX += event.touches[i].pageX; - mpY += event.touches[i].pageY; - } - } - midpointX = mpX / numTouches; - midpointY = mpY / numTouches; - - prevScale = scale; - scale = event.scale; - deltaScale = scale - prevScale; - } - - return { - numTouches: getNumTouches, - touch: getTouch, - current: { - scale: getScale, - midX: getMidPointX, - midY: getMidPointY - }, - delta: { - scale: getDeltaScale - }, - update: updateTouches - }; - }, - - Touch : function(_startX, _startY) { - var startX = _startX, - startY = _startY, - startTime = now(), - currentX = startX, - currentY = startY, - currentTime = startTime, - currentSpeedX = 0, - currentSpeedY = 0, - prevX = startX, - prevY = startX, - prevTime = startTime, - prevSpeedX = 0, - prevSpeedY = 0, - deltaX = 0, - deltaY = 0, - deltaTime = 0, - deltaSpeedX = 0, - deltaSpeedY = 0, - totalX = 0, - totalY = 0, - totalTime = 0; - - // position getters - function getStartX() { - return startX; - } - function getStartY() { - return startY; - } - function getCurrentX() { - return currentX; - } - function getCurrentY() { - return currentY; - } - function getPrevX() { - return prevX; - } - function getPrevY() { - return prevY; - } - function getDeltaX() { - return deltaX; - } - function getDeltaY() { - return deltaY; - } - function getTotalX() { - return totalX; - } - function getTotalY() { - return totalY; - } - - // time getters - function now() { - return (new Date()).getTime(); - } - function getStartTime() { - return startTime; - } - function getCurrentTime() { - return currentTime; - } - function getPrevTime() { - return prevTime; - } - function getDeltaTime() { - return deltaTime; - } - function getTotalTime() { - return totalTime; - } - - // speed getters - function getCurrentSpeedX() { - return currentSpeedX; - } - function getCurrentSpeedY() { - return currentSpeedY; - } - function getPrevSpeedX() { - return prevSpeedX; - } - function getPrevSpeedY() { - return prevSpeedY; - } - function getDeltaSpeedX() { - return deltaSpeedX; - } - function getDeltaSpeedY() { - return deltaSpeedY; - } - - return { - start: { - x: getStartX, - y: getStartY, - speedX: 0, - speedY: 0, - time: getStartTime - }, - current: { - x: getCurrentX, - y: getCurrentY, - time: getCurrentTime, - speedX: getCurrentSpeedX, - speedY: getCurrentSpeedY - }, - prev: { - x: getPrevX, - y: getPrevY, - time: getPrevTime, - speedX: getPrevSpeedX, - speedY: getPrevSpeedY - }, - delta: { - x: getDeltaX, - y: getDeltaY, - speedX: getDeltaSpeedX, - speedY: getDeltaSpeedY, - time: getDeltaTime - }, - total: { - x: getTotalX, - y: getTotalY, - time: getTotalTime - }, - update: function(_x, _y) { - prevX = currentX; - prevY = currentY; - currentX = _x; - currentY = _y; - deltaX = currentX - prevX; - deltaY = currentY - prevY; - totalX = currentX - startX; - totalY = currentY - startY; - - prevTime = currentTime; - currentTime = now(); - deltaTime = currentTime - prevTime; - totalTime = currentTime - startTime; - - prevSpeedX = currentSpeedX; - prevSpeedY = currentSpeedY; - currentSpeedX = deltaX / (deltaTime/1000); - currentSpeedY = deltaY / (deltaTime/1000); - deltaSpeedX = currentSpeedX - prevSpeedX; - deltaSpeedY = currentSpeedY - prevSpeedY; - } - }; - } - }; - - container.jester = function(el, opts) { - return new Jester.Watcher(el, opts); - }; - -}(window)); diff --git a/manifest.webapp b/manifest.webapp index 2149404..a640452 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -1,6 +1,6 @@ { "name": "FeedMonkey", - "description": "A TinyTinyRSS mobile client with which you can read your RSS feeds and mark them as read on your server. Works also offline.", + "description": "A feed mobile client with which you can read your RSS feeds and mark them as read on your server. Works also offline.", "launch_path": "/index.html", "icons": { "58": "/img/icon-58.png", @@ -15,9 +15,9 @@ "type": "privileged", "permissions": { "systemXHR": { - "description": "Connection with your own TinyTinyRSS server." + "description": "Connection with your own server." } }, "installs_allowed_from": ["*"], - "version": "0.3.0" -} \ No newline at end of file + "version": "0.4.3" +}