mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-09-18 21:50:56 +00:00
Training feeds and stories in feeds. Needs testing on ipad, in river, and on social.
This commit is contained in:
parent
a5652da4f1
commit
8fe95f47f3
13 changed files with 912 additions and 212 deletions
|
@ -293,6 +293,12 @@
|
|||
- (UIView *)makeFeedTitleGradient:(NSDictionary *)feed withRect:(CGRect)rect;
|
||||
- (UIView *)makeFeedTitle:(NSDictionary *)feed;
|
||||
- (UIButton *)makeRightFeedTitle:(NSDictionary *)feed;
|
||||
|
||||
- (void)toggleAuthorClassifier:(NSString *)author feedId:(NSString *)feedId;
|
||||
- (void)toggleTagClassifier:(NSString *)tag feedId:(NSString *)feedId;
|
||||
- (void)toggleTitleClassifier:(NSString *)title feedId:(NSString *)feedId score:(int)score;
|
||||
- (void)toggleFeedClassifier:(NSString *)feedId;
|
||||
|
||||
@end
|
||||
|
||||
@interface UnreadCounts : NSObject {
|
||||
|
|
|
@ -679,7 +679,7 @@
|
|||
|
||||
for (NSString *feed in [classifiers objectForKey:@"feeds"]) {
|
||||
if ([[intelligence objectForKey:@"feed"] intValue] <= 0 &&
|
||||
[[story objectForKey:@"story_feed_id"] isEqualToString:feed]) {
|
||||
[storyFeedId isEqualToString:feed]) {
|
||||
int score = [[[classifiers objectForKey:@"feeds"] objectForKey:feed] intValue];
|
||||
[intelligence setObject:[NSNumber numberWithInt:score] forKey:@"feed"];
|
||||
}
|
||||
|
@ -1669,6 +1669,181 @@
|
|||
return titleImageButton;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Classifiers
|
||||
|
||||
- (void)toggleAuthorClassifier:(NSString *)author feedId:(NSString *)feedId {
|
||||
int authorScore = [[[[self.activeClassifiers objectForKey:feedId]
|
||||
objectForKey:@"authors"]
|
||||
objectForKey:author] intValue];
|
||||
if (authorScore > 0) {
|
||||
authorScore = -1;
|
||||
} else if (authorScore < 0) {
|
||||
authorScore = 0;
|
||||
} else {
|
||||
authorScore = 1;
|
||||
}
|
||||
NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId]
|
||||
mutableCopy];
|
||||
NSMutableDictionary *authors = [[feedClassifiers objectForKey:@"authors"] mutableCopy];
|
||||
[authors setObject:[NSNumber numberWithInt:authorScore] forKey:author];
|
||||
[feedClassifiers setObject:authors forKey:@"authors"];
|
||||
[self.activeClassifiers setObject:feedClassifiers forKey:feedId];
|
||||
[self.storyPageControl refreshHeaders];
|
||||
[self.trainerViewController refresh];
|
||||
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
|
||||
NEWSBLUR_URL];
|
||||
NSURL *url = [NSURL URLWithString:urlString];
|
||||
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
|
||||
[request setPostValue:author
|
||||
forKey:authorScore >= 1 ? @"like_author" :
|
||||
authorScore <= -1 ? @"dislike_author" :
|
||||
@"remove_like_author"];
|
||||
[request setPostValue:feedId forKey:@"feed_id"];
|
||||
[request setCompletionBlock:^{
|
||||
[self.feedsViewController refreshFeedList:feedId];
|
||||
}];
|
||||
[request setDidFailSelector:@selector(requestFailed:)];
|
||||
[request setDelegate:self];
|
||||
[request startAsynchronous];
|
||||
|
||||
[self recalculateIntelligenceScores:feedId];
|
||||
[self.feedDetailViewController.storyTitlesTable reloadData];
|
||||
}
|
||||
|
||||
- (void)toggleTagClassifier:(NSString *)tag feedId:(NSString *)feedId {
|
||||
NSLog(@"toggleTagClassifier: %@", tag);
|
||||
int tagScore = [[[[self.activeClassifiers objectForKey:feedId]
|
||||
objectForKey:@"tags"]
|
||||
objectForKey:tag] intValue];
|
||||
|
||||
if (tagScore > 0) {
|
||||
tagScore = -1;
|
||||
} else if (tagScore < 0) {
|
||||
tagScore = 0;
|
||||
} else {
|
||||
tagScore = 1;
|
||||
}
|
||||
|
||||
NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId]
|
||||
mutableCopy];
|
||||
NSMutableDictionary *tags = [[feedClassifiers objectForKey:@"tags"] mutableCopy];
|
||||
[tags setObject:[NSNumber numberWithInt:tagScore] forKey:tag];
|
||||
[feedClassifiers setObject:tags forKey:@"tags"];
|
||||
[self.activeClassifiers setObject:feedClassifiers forKey:feedId];
|
||||
[self.storyPageControl refreshHeaders];
|
||||
[self.trainerViewController refresh];
|
||||
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
|
||||
NEWSBLUR_URL];
|
||||
NSURL *url = [NSURL URLWithString:urlString];
|
||||
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
|
||||
[request setPostValue:tag
|
||||
forKey:tagScore >= 1 ? @"like_tag" :
|
||||
tagScore <= -1 ? @"dislike_tag" :
|
||||
@"remove_like_tag"];
|
||||
[request setPostValue:feedId forKey:@"feed_id"];
|
||||
[request setCompletionBlock:^{
|
||||
[self.feedsViewController refreshFeedList:feedId];
|
||||
}];
|
||||
[request setDidFailSelector:@selector(requestFailed:)];
|
||||
[request setDelegate:self];
|
||||
[request startAsynchronous];
|
||||
|
||||
[self recalculateIntelligenceScores:feedId];
|
||||
[self.feedDetailViewController.storyTitlesTable reloadData];
|
||||
}
|
||||
|
||||
- (void)toggleTitleClassifier:(NSString *)title feedId:(NSString *)feedId score:(int)score {
|
||||
NSLog(@"toggle Title: %@ (%@) / %d", title, feedId, score);
|
||||
int titleScore = [[[[self.activeClassifiers objectForKey:feedId]
|
||||
objectForKey:@"titles"]
|
||||
objectForKey:title] intValue];
|
||||
|
||||
if (score) {
|
||||
titleScore = score;
|
||||
} else {
|
||||
if (titleScore > 0) {
|
||||
titleScore = -1;
|
||||
} else if (titleScore < 0) {
|
||||
titleScore = 0;
|
||||
} else {
|
||||
titleScore = 1;
|
||||
}
|
||||
}
|
||||
|
||||
NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId]
|
||||
mutableCopy];
|
||||
NSMutableDictionary *titles = [[feedClassifiers objectForKey:@"titles"] mutableCopy];
|
||||
[titles setObject:[NSNumber numberWithInt:titleScore] forKey:title];
|
||||
[feedClassifiers setObject:titles forKey:@"titles"];
|
||||
[self.activeClassifiers setObject:feedClassifiers forKey:feedId];
|
||||
[self.storyPageControl refreshHeaders];
|
||||
[self.trainerViewController refresh];
|
||||
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
|
||||
NEWSBLUR_URL];
|
||||
NSURL *url = [NSURL URLWithString:urlString];
|
||||
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
|
||||
[request setPostValue:title
|
||||
forKey:titleScore >= 1 ? @"like_title" :
|
||||
titleScore <= -1 ? @"dislike_title" :
|
||||
@"remove_like_title"];
|
||||
[request setPostValue:feedId forKey:@"feed_id"];
|
||||
[request setCompletionBlock:^{
|
||||
[self.feedsViewController refreshFeedList:feedId];
|
||||
}];
|
||||
[request setDidFailSelector:@selector(requestFailed:)];
|
||||
[request setDelegate:self];
|
||||
[request startAsynchronous];
|
||||
|
||||
[self recalculateIntelligenceScores:feedId];
|
||||
[self.feedDetailViewController.storyTitlesTable reloadData];
|
||||
}
|
||||
|
||||
- (void)toggleFeedClassifier:(NSString *)feedId {
|
||||
int feedScore = [[[[self.activeClassifiers objectForKey:feedId]
|
||||
objectForKey:@"feeds"]
|
||||
objectForKey:feedId] intValue];
|
||||
|
||||
if (feedScore > 0) {
|
||||
feedScore = -1;
|
||||
} else if (feedScore < 0) {
|
||||
feedScore = 0;
|
||||
} else {
|
||||
feedScore = 1;
|
||||
}
|
||||
|
||||
NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId]
|
||||
mutableCopy];
|
||||
NSMutableDictionary *feeds = [[feedClassifiers objectForKey:@"feeds"] mutableCopy];
|
||||
[feeds setObject:[NSNumber numberWithInt:feedScore] forKey:feedId];
|
||||
[feedClassifiers setObject:feeds forKey:@"feeds"];
|
||||
[self.activeClassifiers setObject:feedClassifiers forKey:feedId];
|
||||
[self.storyPageControl refreshHeaders];
|
||||
[self.trainerViewController refresh];
|
||||
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
|
||||
NEWSBLUR_URL];
|
||||
NSURL *url = [NSURL URLWithString:urlString];
|
||||
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
|
||||
[request setPostValue:feedId
|
||||
forKey:feedScore >= 1 ? @"like_feed" :
|
||||
feedScore <= -1 ? @"dislike_feed" :
|
||||
@"remove_like_feed"];
|
||||
[request setPostValue:feedId forKey:@"feed_id"];
|
||||
[request setCompletionBlock:^{
|
||||
[self.feedsViewController refreshFeedList:feedId];
|
||||
}];
|
||||
[request setDidFailSelector:@selector(requestFailed:)];
|
||||
[request setDelegate:self];
|
||||
[request startAsynchronous];
|
||||
|
||||
[self recalculateIntelligenceScores:feedId];
|
||||
[self.feedDetailViewController.storyTitlesTable reloadData];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
|
|
@ -287,8 +287,12 @@ static const CGFloat kFolderTitleHeight = 28;
|
|||
if ([request responseStatusCode] == 403) {
|
||||
return [appDelegate showLogin];
|
||||
} else if ([request responseStatusCode] == 404 ||
|
||||
[request responseStatusCode] == 429 ||
|
||||
[request responseStatusCode] >= 500) {
|
||||
[pull finishedLoading];
|
||||
if ([request responseStatusCode] == 429) {
|
||||
return [self informError:@"Slow down. You're rate-limited."];
|
||||
}
|
||||
return [self informError:@"The server barfed!"];
|
||||
}
|
||||
|
||||
|
|
|
@ -66,9 +66,6 @@
|
|||
- (NSString *)getAvatars:(NSString *)key;
|
||||
- (NSDictionary *)getUser:(int)user_id;
|
||||
|
||||
- (void)toggleAuthorClassifier:(NSString *)author;
|
||||
- (void)toggleTagClassifier:(NSString *)tag;
|
||||
- (void)finishTrain:(ASIHTTPRequest *)request;
|
||||
- (void)refreshHeader;
|
||||
|
||||
|
||||
|
|
|
@ -69,6 +69,10 @@
|
|||
|
||||
self.webView.scalesPageToFit = YES;
|
||||
self.webView.multipleTouchEnabled = NO;
|
||||
|
||||
[self.webView.scrollView setDelaysContentTouches:NO];
|
||||
[self.webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
|
||||
|
||||
self.pageIndex = -2;
|
||||
}
|
||||
|
||||
|
@ -831,6 +835,8 @@ shouldStartLoadWithRequest:(NSURLRequest *)request
|
|||
NSURL *url = [request URL];
|
||||
NSArray *urlComponents = [url pathComponents];
|
||||
NSString *action = @"";
|
||||
NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory
|
||||
objectForKey:@"story_feed_id"]];
|
||||
if ([urlComponents count] > 1) {
|
||||
action = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:1]];
|
||||
}
|
||||
|
@ -904,11 +910,11 @@ shouldStartLoadWithRequest:(NSURLRequest *)request
|
|||
return NO;
|
||||
} else if ([action isEqualToString:@"classify-author"]) {
|
||||
NSString *author = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
|
||||
[self toggleAuthorClassifier:author];
|
||||
[self.appDelegate toggleAuthorClassifier:author feedId:feedId];
|
||||
return NO;
|
||||
} else if ([action isEqualToString:@"classify-tag"]) {
|
||||
NSString *tag = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
|
||||
[self toggleTagClassifier:tag];
|
||||
[self.appDelegate toggleTagClassifier:tag feedId:feedId];
|
||||
return NO;
|
||||
} else if ([action isEqualToString:@"show-profile"]) {
|
||||
appDelegate.activeUserProfileId = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
|
||||
|
@ -1279,95 +1285,6 @@ shouldStartLoadWithRequest:(NSURLRequest *)request
|
|||
// self.webView.hidden = NO;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Classifiers
|
||||
|
||||
- (void)toggleAuthorClassifier:(NSString *)author {
|
||||
NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory
|
||||
objectForKey:@"story_feed_id"]];
|
||||
int authorScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
|
||||
objectForKey:@"authors"]
|
||||
objectForKey:author] intValue];
|
||||
if (authorScore > 0) {
|
||||
authorScore = -1;
|
||||
} else if (authorScore < 0) {
|
||||
authorScore = 0;
|
||||
} else {
|
||||
authorScore = 1;
|
||||
}
|
||||
NSMutableDictionary *feedClassifiers = [[appDelegate.activeClassifiers objectForKey:feedId]
|
||||
mutableCopy];
|
||||
NSMutableDictionary *authors = [[feedClassifiers objectForKey:@"authors"] mutableCopy];
|
||||
[authors setObject:[NSNumber numberWithInt:authorScore] forKey:author];
|
||||
[feedClassifiers setObject:authors forKey:@"authors"];
|
||||
[appDelegate.activeClassifiers setObject:feedClassifiers forKey:feedId];
|
||||
[appDelegate.storyPageControl refreshHeaders];
|
||||
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
|
||||
NEWSBLUR_URL];
|
||||
NSURL *url = [NSURL URLWithString:urlString];
|
||||
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
|
||||
[request setPostValue:author
|
||||
forKey:authorScore >= 1 ? @"like_author" :
|
||||
authorScore <= -1 ? @"dislike_author" :
|
||||
@"remove_like_author"];
|
||||
[request setPostValue:feedId forKey:@"feed_id"];
|
||||
[request setDidFinishSelector:@selector(finishTrain:)];
|
||||
[request setDidFailSelector:@selector(requestFailed:)];
|
||||
[request setDelegate:self];
|
||||
[request startAsynchronous];
|
||||
|
||||
[appDelegate recalculateIntelligenceScores:feedId];
|
||||
[appDelegate.feedDetailViewController.storyTitlesTable reloadData];
|
||||
}
|
||||
|
||||
- (void)toggleTagClassifier:(NSString *)tag {
|
||||
NSLog(@"toggleTagClassifier: %@", tag);
|
||||
NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory
|
||||
objectForKey:@"story_feed_id"]];
|
||||
int tagScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
|
||||
objectForKey:@"tags"]
|
||||
objectForKey:tag] intValue];
|
||||
|
||||
if (tagScore > 0) {
|
||||
tagScore = -1;
|
||||
} else if (tagScore < 0) {
|
||||
tagScore = 0;
|
||||
} else {
|
||||
tagScore = 1;
|
||||
}
|
||||
|
||||
NSMutableDictionary *feedClassifiers = [[appDelegate.activeClassifiers objectForKey:feedId]
|
||||
mutableCopy];
|
||||
NSMutableDictionary *tags = [[feedClassifiers objectForKey:@"tags"] mutableCopy];
|
||||
[tags setObject:[NSNumber numberWithInt:tagScore] forKey:tag];
|
||||
[feedClassifiers setObject:tags forKey:@"tags"];
|
||||
[appDelegate.activeClassifiers setObject:feedClassifiers forKey:feedId];
|
||||
[appDelegate.storyPageControl refreshHeaders];
|
||||
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
|
||||
NEWSBLUR_URL];
|
||||
NSURL *url = [NSURL URLWithString:urlString];
|
||||
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
|
||||
[request setPostValue:tag
|
||||
forKey:tagScore >= 1 ? @"like_tag" :
|
||||
tagScore <= -1 ? @"dislike_tag" :
|
||||
@"remove_like_tag"];
|
||||
[request setPostValue:feedId forKey:@"feed_id"];
|
||||
[request setDidFinishSelector:@selector(finishTrain:)];
|
||||
[request setDidFailSelector:@selector(requestFailed:)];
|
||||
[request setDelegate:self];
|
||||
[request startAsynchronous];
|
||||
|
||||
[appDelegate recalculateIntelligenceScores:feedId];
|
||||
[appDelegate.feedDetailViewController.storyTitlesTable reloadData];
|
||||
}
|
||||
|
||||
- (void)finishTrain:(ASIHTTPRequest *)request {
|
||||
id feedId = [self.activeStory objectForKey:@"story_feed_id"];
|
||||
[appDelegate.feedsViewController refreshFeedList:feedId];
|
||||
}
|
||||
|
||||
- (void)refreshHeader {
|
||||
NSString *headerString = [[[self getHeader] stringByReplacingOccurrencesOfString:@"\'" withString:@"\\'"]
|
||||
stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
|
||||
@interface TrainerWebView : UIWebView {}
|
||||
|
||||
- (void)changeTitle:(id)sender;
|
||||
- (void)focusTitle:(id)sender;
|
||||
- (void)hideTitle:(id)sender;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -37,6 +38,8 @@
|
|||
@property (nonatomic, assign) BOOL feedTrainer;
|
||||
@property (nonatomic, assign) BOOL storyTrainer;
|
||||
|
||||
- (void)refresh;
|
||||
- (NSString *)makeTrainerHTML;
|
||||
- (NSString *)makeTrainerSections;
|
||||
- (NSString *)makeStoryAuthor;
|
||||
- (NSString *)makeFeedAuthors;
|
||||
|
@ -47,6 +50,5 @@
|
|||
- (NSString *)makeClassifier:(NSString *)classifierName withType:(NSString *)classifierType score:(int)score;
|
||||
|
||||
- (IBAction)doCloseDialog:(id)sender;
|
||||
- (void)changeTitle:(id)sender;
|
||||
|
||||
@end
|
|
@ -34,6 +34,8 @@
|
|||
|
||||
navBar.tintColor = UIColorFromRGB(0x183353);
|
||||
[self hideGradientBackground:webView];
|
||||
[self.webView.scrollView setDelaysContentTouches:YES];
|
||||
[self.webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
|
||||
}
|
||||
- (void) hideGradientBackground:(UIView*)theView
|
||||
{
|
||||
|
@ -48,18 +50,33 @@
|
|||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[[UIMenuController sharedMenuController]
|
||||
setMenuItems:[NSArray arrayWithObjects:
|
||||
[[UIMenuItem alloc] initWithTitle:@"👎 Hide" action:@selector(changeTitle:)],
|
||||
[[UIMenuItem alloc] initWithTitle:@"👍 Focus" action:@selector(changeTitle:)],
|
||||
[[UIMenuItem alloc] initWithTitle:@"👎 Hide" action:@selector(hideTitle:)],
|
||||
[[UIMenuItem alloc] initWithTitle:@"👍 Focus" action:@selector(focusTitle:)],
|
||||
nil]];
|
||||
|
||||
UILabel *titleLabel = (UILabel *)[appDelegate makeFeedTitle:appDelegate.activeFeed];
|
||||
titleLabel.shadowColor = UIColorFromRGB(0x306070);
|
||||
navBar.topItem.titleView = titleLabel;
|
||||
|
||||
|
||||
NSString *path = [[NSBundle mainBundle] bundlePath];
|
||||
NSURL *baseURL = [NSURL fileURLWithPath:path];
|
||||
[self.webView loadHTMLString:[self makeTrainerHTML] baseURL:baseURL];
|
||||
}
|
||||
|
||||
- (void)refresh {
|
||||
if (self.view.hidden || self.view.superview == nil) {
|
||||
NSLog(@"Trainer hidden, ignoring redraw.");
|
||||
return;
|
||||
}
|
||||
NSString *headerString = [[[self makeTrainerSections]
|
||||
stringByReplacingOccurrencesOfString:@"\'" withString:@"\\'"]
|
||||
stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
|
||||
NSString *jsString = [NSString stringWithFormat:@"document.getElementById('NB-trainer').innerHTML = '%@';",
|
||||
headerString];
|
||||
|
||||
[self.webView loadHTMLString:[self makeTrainerSections] baseURL:baseURL];
|
||||
[self.webView stringByEvaluatingJavaScriptFromString:jsString];
|
||||
|
||||
[self.webView stringByEvaluatingJavaScriptFromString:@"attachFastClick({skipEvent: true});"];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
|
@ -74,11 +91,8 @@
|
|||
#pragma mark -
|
||||
#pragma mark Story layout
|
||||
|
||||
- (NSString *)makeTrainerSections {
|
||||
NSString *storyAuthor = self.feedTrainer ? [self makeFeedAuthors] : [self makeStoryAuthor];
|
||||
NSString *storyTags = self.feedTrainer ? [self makeFeedTags] : [self makeStoryTags];
|
||||
NSString *storyTitle = self.feedTrainer ? @"" : [self makeTitle];
|
||||
NSString *storyPublisher = [self makePublisher];
|
||||
- (NSString *)makeTrainerHTML {
|
||||
NSString *trainerSections = [self makeTrainerSections];
|
||||
|
||||
int contentWidth = self.view.frame.size.width;
|
||||
NSString *contentWidthClass;
|
||||
|
@ -105,21 +119,13 @@
|
|||
"<html>"
|
||||
"<head>%@</head>" // header string
|
||||
"<body id=\"trainer\" class=\"%@\">"
|
||||
" <div class=\"NB-trainer\"><div class=\"NB-trainer-inner\">"
|
||||
" <div class=\"NB-trainer-title NB-trainer-section\">%@</div>"
|
||||
" <div class=\"NB-trainer-author NB-trainer-section\">%@</div>"
|
||||
" <div class=\"NB-trainer-tags NB-trainer-section\">%@</div>"
|
||||
" <div class=\"NB-trainer-publisher NB-trainer-section\">%@</div>"
|
||||
" </div></div>"
|
||||
"<div class=\"NB-trainer\" id=\"NB-trainer\">%@</div>"
|
||||
"%@" // footer
|
||||
"</body>"
|
||||
"</html>",
|
||||
headerString,
|
||||
contentWidthClass,
|
||||
storyTitle,
|
||||
storyAuthor,
|
||||
storyTags,
|
||||
storyPublisher,
|
||||
trainerSections,
|
||||
footerString
|
||||
];
|
||||
|
||||
|
@ -128,6 +134,26 @@
|
|||
return htmlString;
|
||||
}
|
||||
|
||||
- (NSString *)makeTrainerSections {
|
||||
NSString *storyAuthor = self.feedTrainer ? [self makeFeedAuthors] : [self makeStoryAuthor];
|
||||
NSString *storyTags = self.feedTrainer ? [self makeFeedTags] : [self makeStoryTags];
|
||||
NSString *storyTitle = self.feedTrainer ? @"" : [self makeTitle];
|
||||
NSString *storyPublisher = [self makePublisher];
|
||||
|
||||
NSString *htmlString = [NSString stringWithFormat:@
|
||||
"<div class=\"NB-trainer-inner\">"
|
||||
" <div class=\"NB-trainer-title NB-trainer-section\">%@</div>"
|
||||
" <div class=\"NB-trainer-author NB-trainer-section\">%@</div>"
|
||||
" <div class=\"NB-trainer-tags NB-trainer-section\">%@</div>"
|
||||
" <div class=\"NB-trainer-publisher NB-trainer-section\">%@</div>"
|
||||
"</div>",
|
||||
storyTitle,
|
||||
storyAuthor,
|
||||
storyTags,
|
||||
storyPublisher];
|
||||
|
||||
return htmlString;
|
||||
}
|
||||
- (NSString *)makeStoryAuthor {
|
||||
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
|
||||
objectForKey:@"story_feed_id"]];
|
||||
|
@ -292,13 +318,13 @@
|
|||
" <div class=\"NB-trainer-section-title\">Publisher</div>"
|
||||
" <div class=\"NB-trainer-section-body\">"
|
||||
" <div class=\"NB-classifier-container\">"
|
||||
" <a href=\"http://ios.newsblur.com/classify-publisher/%@\" "
|
||||
" class=\"NB-story-publisher %@\">%@</a>"
|
||||
" <a href=\"http://ios.newsblur.com/classify-feed/%@\" "
|
||||
" class=\"NB-story-publisher NB-story-publisher-%@\">%@</a>"
|
||||
" </div>"
|
||||
" </div>"
|
||||
"</div",
|
||||
feedId,
|
||||
publisherScore > 0 ? @"NB-story-publisher-positive" : publisherScore < 0 ? @"NB-story-publisher-negative" : @"",
|
||||
publisherScore > 0 ? @"positive" : publisherScore < 0 ? @"negative" : @"",
|
||||
[self makeClassifier:feedTitle withType:@"publisher" score:publisherScore]];
|
||||
|
||||
return storyPublisher;
|
||||
|
@ -308,31 +334,42 @@
|
|||
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
|
||||
objectForKey:@"story_feed_id"]];
|
||||
NSString *storyTitle = [appDelegate.activeStory objectForKey:@"story_title"];
|
||||
|
||||
|
||||
if (!storyTitle) {
|
||||
return @"";
|
||||
}
|
||||
|
||||
NSMutableDictionary *titleClassifiers = [[appDelegate.activeClassifiers objectForKey:feedId]
|
||||
NSMutableDictionary *classifiers = [[appDelegate.activeClassifiers objectForKey:feedId]
|
||||
objectForKey:@"titles"];
|
||||
for (NSString *titleClassifier in titleClassifiers) {
|
||||
if ([storyTitle containsString:titleClassifier]) {
|
||||
int titleScore = [[titleClassifiers objectForKey:titleClassifier] intValue];
|
||||
storyTitle = [storyTitle
|
||||
stringByReplacingOccurrencesOfString:titleClassifier
|
||||
withString:[NSString stringWithFormat:@" <span class=\"NB-story-title-%@\">%@</span>",
|
||||
titleScore > 0 ? @"positive" : titleScore < 0 ? @"negative" : @"",
|
||||
titleClassifier]];
|
||||
NSMutableArray *titleStrings = [NSMutableArray array];
|
||||
for (NSString *title in classifiers) {
|
||||
if ([storyTitle containsString:title]) {
|
||||
int titleScore = [[classifiers objectForKey:title] intValue];
|
||||
NSString *titleClassifier = [NSString stringWithFormat:@
|
||||
"<div class=\"NB-classifier-container\">"
|
||||
" <a href=\"http://ios.newsblur.com/classify-title/%@\" "
|
||||
" class=\"NB-story-title NB-story-title-%@\">%@</a>"
|
||||
"</div>",
|
||||
title,
|
||||
titleScore > 0 ? @"positive" : titleScore < 0 ? @"negative" : @"",
|
||||
[self makeClassifier:title withType:@"title" score:titleScore]];
|
||||
[titleStrings addObject:titleClassifier];
|
||||
}
|
||||
}
|
||||
|
||||
NSString *titleClassifiers;
|
||||
if ([titleStrings count]) {
|
||||
titleClassifiers = [titleStrings componentsJoinedByString:@""];
|
||||
} else {
|
||||
titleClassifiers = @"<div class=\"NB-title-info\">Tap and hold the title</div>";
|
||||
}
|
||||
NSString *titleTrainer = [NSString stringWithFormat:@"<div class=\"NB-trainer-section-inner\">"
|
||||
" <div class=\"NB-trainer-section-title\">Story Title</div>"
|
||||
" <div class=\"NB-trainer-section-body NB-title\">"
|
||||
" <div class=\"NB-title-trainer\">%@</div>"
|
||||
" <div class=\"NB-title-info\">Tap and hold the title</div>"
|
||||
" %@"
|
||||
" </div>"
|
||||
"</div>", storyTitle];
|
||||
"</div>", storyTitle, titleClassifiers];
|
||||
return titleTrainer;
|
||||
}
|
||||
|
||||
|
@ -359,17 +396,51 @@
|
|||
[appDelegate.trainerViewController dismissModalViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
|
||||
if (action == @selector(changeTitle:)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
- (void)changeTitle:(id)sender score:(int)score {
|
||||
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
|
||||
objectForKey:@"story_feed_id"]];
|
||||
NSString *selectedTitle = [self.webView
|
||||
stringByEvaluatingJavaScriptFromString:@"window.getSelection().toString()"];
|
||||
|
||||
[self.appDelegate toggleTitleClassifier:selectedTitle feedId:feedId score:score];
|
||||
}
|
||||
|
||||
- (void)changeTitle:(id)sender {
|
||||
NSString *selectedTitle = [self.webView stringByEvaluatingJavaScriptFromString:@"window.getSelection().toString()"];
|
||||
NSLog(@"Selected: %@", selectedTitle);
|
||||
|
||||
- (BOOL)webView:(UIWebView *)webView
|
||||
shouldStartLoadWithRequest:(NSURLRequest *)request
|
||||
navigationType:(UIWebViewNavigationType)navigationType {
|
||||
NSURL *url = [request URL];
|
||||
NSArray *urlComponents = [url pathComponents];
|
||||
NSString *action = @"";
|
||||
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed
|
||||
objectForKey:@"id"]];
|
||||
if ([urlComponents count] > 1) {
|
||||
action = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:1]];
|
||||
}
|
||||
|
||||
NSLog(@"Tapped url: %@", url);
|
||||
if ([[url host] isEqualToString: @"ios.newsblur.com"]){
|
||||
|
||||
if ([action isEqualToString:@"classify-author"]) {
|
||||
NSString *author = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
|
||||
[self.appDelegate toggleAuthorClassifier:author feedId:feedId];
|
||||
return NO;
|
||||
} else if ([action isEqualToString:@"classify-tag"]) {
|
||||
NSString *tag = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
|
||||
[self.appDelegate toggleTagClassifier:tag feedId:feedId];
|
||||
return NO;
|
||||
} else if ([action isEqualToString:@"classify-title"]) {
|
||||
NSString *title = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
|
||||
[self.appDelegate toggleTitleClassifier:title feedId:feedId score:0];
|
||||
return NO;
|
||||
} else if ([action isEqualToString:@"classify-feed"]) {
|
||||
NSString *feedId = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
|
||||
[self.appDelegate toggleFeedClassifier:feedId];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -378,16 +449,21 @@
|
|||
@implementation TrainerWebView
|
||||
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
|
||||
if (action == @selector(changeTitle:)) {
|
||||
if (action == @selector(focusTitle:) || action == @selector(hideTitle:)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)changeTitle:(id)sender {
|
||||
- (void)focusTitle:(id)sender {
|
||||
NewsBlurAppDelegate *appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
|
||||
[appDelegate.trainerViewController changeTitle:sender];
|
||||
[appDelegate.trainerViewController changeTitle:sender score:1];
|
||||
}
|
||||
|
||||
- (void)hideTitle:(id)sender {
|
||||
NewsBlurAppDelegate *appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
|
||||
[appDelegate.trainerViewController changeTitle:sender score:-1];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -16,10 +16,23 @@
|
|||
landmarkName = "-applyNewIndex:pageController:"
|
||||
landmarkType = "5">
|
||||
</FileBreakpoint>
|
||||
<FileBreakpoint
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Classes/NewsBlurAppDelegate.m"
|
||||
timestampString = "378353700.864122"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "681"
|
||||
endingLineNumber = "681"
|
||||
landmarkName = "-recalculateIntelligenceScores:"
|
||||
landmarkType = "5">
|
||||
</FileBreakpoint>
|
||||
</FileBreakpoints>
|
||||
<SymbolicBreakpoints>
|
||||
<SymbolicBreakpoint
|
||||
shouldBeEnabled = "Yes"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
symbolName = "objc_exception_throw"
|
||||
|
@ -28,7 +41,7 @@
|
|||
</SymbolicBreakpoints>
|
||||
<ExceptionBreakpoints>
|
||||
<ExceptionBreakpoint
|
||||
shouldBeEnabled = "Yes"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
scope = "0"
|
||||
|
|
|
@ -4521,6 +4521,14 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
|
|||
</object>
|
||||
<int key="connectionID">25</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBCocoaTouchEventConnection" key="connection">
|
||||
<string key="label">doCloseDialog:</string>
|
||||
<reference key="source" ref="216767292"/>
|
||||
<reference key="destination" ref="372490531"/>
|
||||
</object>
|
||||
<int key="connectionID">26</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBCocoaTouchOutletConnection" key="connection">
|
||||
<string key="label">delegate</string>
|
||||
|
@ -4603,7 +4611,7 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
|
|||
<nil key="activeLocalization"/>
|
||||
<dictionary class="NSMutableDictionary" key="localizations"/>
|
||||
<nil key="sourceID"/>
|
||||
<int key="maxID">25</int>
|
||||
<int key="maxID">26</int>
|
||||
</object>
|
||||
<object class="IBClassDescriber" key="IBDocument.Classes">
|
||||
<array class="NSMutableArray" key="referencedPartialClassDescriptions">
|
||||
|
|
|
@ -1,85 +1,578 @@
|
|||
/**
|
||||
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
|
||||
*
|
||||
* @version 0.4.6
|
||||
* @codingstandard ftlabs-jsv2
|
||||
* @copyright The Financial Times Limited [All Rights Reserved]
|
||||
* @license MIT License (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
function NoClickDelay(el) {
|
||||
this.element = typeof el == 'object' ? el : document.getElementById(el);
|
||||
if( window.Touch ) {
|
||||
this.element.removeEventListener('touchstart', this.element.notouch, false);
|
||||
this.element.notouch = this;
|
||||
this.element.addEventListener('touchstart', this.element.notouch, false);
|
||||
}
|
||||
}
|
||||
NoClickDelay.prototype = {
|
||||
handleEvent: function(e) {
|
||||
switch(e.type) {
|
||||
case 'touchstart': this.onTouchStart(e); break;
|
||||
case 'touchmove': this.onTouchMove(e); break;
|
||||
case 'touchend': this.onTouchEnd(e); break;
|
||||
}
|
||||
},
|
||||
onTouchStart: function(e) {
|
||||
e.preventDefault();
|
||||
this.moved = false;
|
||||
this.x = e.targetTouches[0].clientX;
|
||||
this.y = e.targetTouches[0].clientY;
|
||||
this.theTarget = document.elementFromPoint(e.targetTouches[0].clientX, e.targetTouches[0].clientY);
|
||||
this.theTarget = $(this.theTarget).closest('a').get(0);
|
||||
// if(this.theTarget.nodeType == 3) this.theTarget = theTarget.parentNode;
|
||||
this.theTarget.className+= ' pressed';
|
||||
this.element.addEventListener('touchmove', this, false);
|
||||
this.element.addEventListener('touchend', this, false);
|
||||
},
|
||||
onTouchMove: function(e) {
|
||||
var x = e.targetTouches[0].clientX;
|
||||
var y = e.targetTouches[0].clientY;
|
||||
if( Math.sqrt(Math.pow(x-this.x,2)+Math.pow(y-this.y,2))>50){
|
||||
this.moved = true;
|
||||
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
|
||||
this.theTarget.className = this.theTarget.className.replace(/ ?active/gi, '');
|
||||
} else {
|
||||
if(this.moved==true){
|
||||
this.moved=false;
|
||||
this.theTarget.className+= ' pressed';
|
||||
}
|
||||
}
|
||||
},
|
||||
onTouchEnd: function(e) {
|
||||
this.element.removeEventListener('touchmove', this, false);
|
||||
this.element.removeEventListener('touchend', this, false);
|
||||
if( !this.moved && this.theTarget ) {
|
||||
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
|
||||
this.theTarget.className+= ' active';
|
||||
var theEvent = document.createEvent('MouseEvents');
|
||||
theEvent.initEvent('click', true, true);
|
||||
this.theTarget.dispatchEvent(theEvent);
|
||||
}
|
||||
this.theTarget = undefined;
|
||||
/*jslint browser:true, node:true*/
|
||||
/*global define, Event, Node*/
|
||||
|
||||
|
||||
/**
|
||||
* Instantiate fast-clicking listeners on the specificed layer.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Element} layer The layer to listen on
|
||||
*/
|
||||
function FastClick(layer) {
|
||||
'use strict';
|
||||
var oldOnClick, self = this;
|
||||
|
||||
|
||||
/**
|
||||
* Whether a click is currently being tracked.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
this.trackingClick = false;
|
||||
|
||||
|
||||
/**
|
||||
* Timestamp for when when click tracking started.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.trackingClickStart = 0;
|
||||
|
||||
|
||||
/**
|
||||
* The element being tracked for a click.
|
||||
*
|
||||
* @type EventTarget
|
||||
*/
|
||||
this.targetElement = null;
|
||||
|
||||
|
||||
/**
|
||||
* X-coordinate of touch start event.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.touchStartX = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Y-coordinate of touch start event.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.touchStartY = 0;
|
||||
|
||||
|
||||
/**
|
||||
* ID of the last touch, retrieved from Touch.identifier.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.lastTouchIdentifier = 0;
|
||||
|
||||
|
||||
/**
|
||||
* The FastClick layer.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
this.layer = layer;
|
||||
|
||||
if (!layer || !layer.nodeType) {
|
||||
throw new TypeError('Layer must be a document node');
|
||||
}
|
||||
|
||||
/** @type function() */
|
||||
this.onClick = function() { FastClick.prototype.onClick.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onTouchStart = function() { FastClick.prototype.onTouchStart.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onTouchMove = function() { FastClick.prototype.onTouchMove.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onTouchEnd = function() { FastClick.prototype.onTouchEnd.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onTouchCancel = function() { FastClick.prototype.onTouchCancel.apply(self, arguments); };
|
||||
|
||||
// Devices that don't support touch don't need FastClick
|
||||
if (typeof window.ontouchstart === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up event handlers as required
|
||||
layer.addEventListener('click', this.onClick, true);
|
||||
layer.addEventListener('touchstart', this.onTouchStart, false);
|
||||
layer.addEventListener('touchmove', this.onTouchMove, false);
|
||||
layer.addEventListener('touchend', this.onTouchEnd, false);
|
||||
layer.addEventListener('touchcancel', this.onTouchCancel, false);
|
||||
|
||||
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
|
||||
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
|
||||
// layer when they are cancelled.
|
||||
if (!Event.prototype.stopImmediatePropagation) {
|
||||
layer.removeEventListener = function(type, callback, capture) {
|
||||
var rmv = Node.prototype.removeEventListener;
|
||||
if (type === 'click') {
|
||||
rmv.call(layer, type, callback.hijacked || callback, capture);
|
||||
} else {
|
||||
rmv.call(layer, type, callback, capture);
|
||||
}
|
||||
};
|
||||
|
||||
layer.addEventListener = function(type, callback, capture) {
|
||||
var adv = Node.prototype.addEventListener;
|
||||
if (type === 'click') {
|
||||
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
|
||||
if (!event.propagationStopped) {
|
||||
callback(event);
|
||||
}
|
||||
}), capture);
|
||||
} else {
|
||||
adv.call(layer, type, callback, capture);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// If a handler is already declared in the element's onclick attribute, it will be fired before
|
||||
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
|
||||
// adding it as listener.
|
||||
if (typeof layer.onclick === 'function') {
|
||||
|
||||
// Android browser on at least 3.2 requires a new reference to the function in layer.onclick
|
||||
// - the old one won't work if passed to addEventListener directly.
|
||||
oldOnClick = layer.onclick;
|
||||
layer.addEventListener('click', function(event) {
|
||||
oldOnClick(event);
|
||||
}, false);
|
||||
layer.onclick = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Android requires an exception for labels.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
|
||||
|
||||
|
||||
/**
|
||||
* iOS requires an exception for alert confirm dialogs.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
|
||||
|
||||
|
||||
/**
|
||||
* iOS 4 requires an exception for select elements.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether a given element requires a native click.
|
||||
*
|
||||
* @param {EventTarget|Element} target Target DOM element
|
||||
* @returns {boolean} Returns true if the element needs a native click
|
||||
*/
|
||||
FastClick.prototype.needsClick = function(target) {
|
||||
'use strict';
|
||||
switch (target.nodeName.toLowerCase()) {
|
||||
case 'label':
|
||||
case 'video':
|
||||
return true;
|
||||
default:
|
||||
return (/\bneedsclick\b/).test(target.className);
|
||||
}
|
||||
};
|
||||
|
||||
function attachFastClick() {
|
||||
|
||||
/**
|
||||
* Determine whether a given element requires a call to focus to simulate click into element.
|
||||
*
|
||||
* @param {EventTarget|Element} target Target DOM element
|
||||
* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
|
||||
*/
|
||||
FastClick.prototype.needsFocus = function(target) {
|
||||
'use strict';
|
||||
switch (target.nodeName.toLowerCase()) {
|
||||
case 'textarea':
|
||||
case 'select':
|
||||
return true;
|
||||
case 'input':
|
||||
switch (target.type) {
|
||||
case 'button':
|
||||
case 'checkbox':
|
||||
case 'file':
|
||||
case 'image':
|
||||
case 'radio':
|
||||
case 'submit':
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return (/\bneedsfocus\b/).test(target.className);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Send a click event to the specified element.
|
||||
*
|
||||
* @param {EventTarget|Element} targetElement
|
||||
* @param {Event} event
|
||||
*/
|
||||
FastClick.prototype.sendClick = function(targetElement, event) {
|
||||
'use strict';
|
||||
var clickEvent, touch;
|
||||
|
||||
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
|
||||
if (document.activeElement && document.activeElement !== targetElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
touch = event.changedTouches[0];
|
||||
|
||||
// Synthesise a click event, with an extra attribute so it can be tracked
|
||||
clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
|
||||
clickEvent.forwardedTouchEvent = true;
|
||||
targetElement.dispatchEvent(clickEvent);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On touch start, record the position and scroll offset.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onTouchStart = function(event) {
|
||||
'use strict';
|
||||
var touch = event.targetTouches[0];
|
||||
|
||||
this.trackingClick = true;
|
||||
this.trackingClickStart = event.timeStamp;
|
||||
this.targetElement = event.target;
|
||||
this.theTarget = $(this.targetElement).closest('a').get(0);
|
||||
|
||||
this.theTarget.className += ' pressed';
|
||||
|
||||
this.touchStartX = touch.pageX;
|
||||
this.touchStartY = touch.pageY;
|
||||
this.startClickTime = new Date;
|
||||
|
||||
// Prevent phantom clicks on fast double-tap (issue #36)
|
||||
if ((event.timeStamp - this.lastClickTime) < 200) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.touchHasMoved = function(event) {
|
||||
'use strict';
|
||||
var touch = event.targetTouches[0];
|
||||
|
||||
if (Math.abs(touch.pageX - this.touchStartX) > 10 || Math.abs(touch.pageY - this.touchStartY) > 10) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update the last position.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onTouchMove = function(event) {
|
||||
'use strict';
|
||||
if (!this.trackingClick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the touch has moved, cancel the click tracking
|
||||
if (this.targetElement !== event.target || this.touchHasMoved(event)) {
|
||||
this.trackingClick = false;
|
||||
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
|
||||
this.targetElement = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to find the labelled control for the given label element.
|
||||
*
|
||||
* @param {EventTarget|HTMLLabelElement} labelElement
|
||||
* @returns {Element|null}
|
||||
*/
|
||||
FastClick.prototype.findControl = function(labelElement) {
|
||||
'use strict';
|
||||
|
||||
// Fast path for newer browsers supporting the HTML5 control attribute
|
||||
if (labelElement.control !== undefined) {
|
||||
return labelElement.control;
|
||||
}
|
||||
|
||||
// All browsers under test that support touch events also support the HTML5 htmlFor attribute
|
||||
if (labelElement.htmlFor) {
|
||||
return document.getElementById(labelElement.htmlFor);
|
||||
}
|
||||
|
||||
// If no for attribute exists, attempt to retrieve the first labellable descendant element
|
||||
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
|
||||
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On touch end, determine whether to send a click event at once.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onTouchEnd = function(event) {
|
||||
'use strict';
|
||||
var forElement, trackingClickStart, targetElement = this.targetElement, touch = event.changedTouches[0];
|
||||
|
||||
if (!this.trackingClick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
|
||||
// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
|
||||
// with the same identifier as the touch event that previously triggered the click that triggered the alert.
|
||||
if (this.deviceIsIOS) {
|
||||
if (touch.identifier === this.lastTouchIdentifier) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.lastTouchIdentifier = touch.identifier;
|
||||
}
|
||||
|
||||
// Prevent phantom clicks on fast double-tap (issue #36)
|
||||
if ((event.timeStamp - this.lastClickTime) < 200) {
|
||||
this.cancelNextClick = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((new Date - this.startClickTime) > 115) {
|
||||
return false;
|
||||
}
|
||||
this.lastClickTime = event.timeStamp;
|
||||
|
||||
trackingClickStart = this.trackingClickStart;
|
||||
this.trackingClick = false;
|
||||
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
|
||||
this.trackingClickStart = 0;
|
||||
|
||||
if (targetElement.nodeName.toLowerCase() === 'label') {
|
||||
forElement = this.findControl(targetElement);
|
||||
if (forElement) {
|
||||
targetElement.focus();
|
||||
if (this.deviceIsAndroid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.needsClick(forElement)) {
|
||||
event.preventDefault();
|
||||
this.sendClick(forElement, event);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} else if (this.needsFocus(targetElement)) {
|
||||
|
||||
// If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
|
||||
if ((event.timeStamp - trackingClickStart) > 100) {
|
||||
this.targetElement = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
targetElement.focus();
|
||||
|
||||
// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
|
||||
if (!this.deviceIsIOS4 || targetElement.tagName.toLowerCase() !== 'select') {
|
||||
this.targetElement = null;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent the actual click from going though - unless the target node is marked as requiring
|
||||
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
|
||||
if (!this.needsClick(targetElement)) {
|
||||
event.preventDefault();
|
||||
this.sendClick(targetElement, event);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On touch cancel, stop tracking the click.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
FastClick.prototype.onTouchCancel = function() {
|
||||
'use strict';
|
||||
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
|
||||
this.trackingClick = false;
|
||||
this.targetElement = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On actual clicks, determine whether this is a touch-generated click, a click action occurring
|
||||
* naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
|
||||
* an actual click which should be permitted.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onClick = function(event) {
|
||||
'use strict';
|
||||
|
||||
var oldTargetElement;
|
||||
|
||||
// If a target element was never set (because a touch event was never fired) allow the click
|
||||
if (!this.targetElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.forwardedTouchEvent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
oldTargetElement = this.targetElement;
|
||||
this.targetElement = null;
|
||||
|
||||
// It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
|
||||
if (this.trackingClick) {
|
||||
this.trackingClick = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Programmatically generated events targeting a specific element should be permitted
|
||||
if (!event.cancelable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
|
||||
if (event.target.type === 'submit' && event.detail === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Derive and check the target element to see whether the click needs to be permitted;
|
||||
// unless explicitly enabled, prevent non-touch click events from triggering actions,
|
||||
// to prevent ghost/doubleclicks.
|
||||
if (!this.needsClick(oldTargetElement) || this.cancelNextClick) {
|
||||
this.cancelNextClick = false;
|
||||
|
||||
// Prevent any user-added listeners declared on FastClick element from being fired.
|
||||
if (event.stopImmediatePropagation) {
|
||||
event.stopImmediatePropagation();
|
||||
} else {
|
||||
|
||||
// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
|
||||
event.propagationStopped = true;
|
||||
}
|
||||
|
||||
// Cancel the event
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If clicks are permitted, return true for the action to go through.
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove all FastClick's event listeners.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
FastClick.prototype.destroy = function() {
|
||||
'use strict';
|
||||
var layer = this.layer;
|
||||
|
||||
layer.removeEventListener('click', this.onClick, true);
|
||||
layer.removeEventListener('touchstart', this.onTouchStart, false);
|
||||
layer.removeEventListener('touchmove', this.onTouchMove, false);
|
||||
layer.removeEventListener('touchend', this.onTouchEnd, false);
|
||||
layer.removeEventListener('touchcancel', this.onTouchCancel, false);
|
||||
};
|
||||
|
||||
|
||||
if (typeof define !== 'undefined' && define.amd) {
|
||||
|
||||
// AMD. Register as an anonymous module.
|
||||
define(function() {
|
||||
'use strict';
|
||||
return FastClick;
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = function(layer) {
|
||||
'use strict';
|
||||
return new FastClick(layer);
|
||||
};
|
||||
|
||||
module.exports.FastClick = FastClick;
|
||||
}
|
||||
|
||||
|
||||
function attachFastClick(options) {
|
||||
options = options || {};
|
||||
var avatars = document.getElementsByClassName("NB-show-profile");
|
||||
Array.prototype.slice.call(avatars, 0).forEach(function(avatar) {
|
||||
new NoClickDelay(avatar);
|
||||
new FastClick(avatar, options);
|
||||
});
|
||||
var tags = document.getElementsByClassName("NB-story-tag");
|
||||
Array.prototype.slice.call(tags, 0).forEach(function(tag) {
|
||||
new NoClickDelay(tag);
|
||||
new FastClick(tag, options);
|
||||
});
|
||||
var authors = document.getElementsByClassName("NB-story-author");
|
||||
Array.prototype.slice.call(authors, 0).forEach(function(author) {
|
||||
new NoClickDelay(author);
|
||||
new FastClick(author, options);
|
||||
});
|
||||
var publishers = document.getElementsByClassName("NB-story-publisher");
|
||||
Array.prototype.slice.call(publishers, 0).forEach(function(publisher) {
|
||||
new NoClickDelay(publisher);
|
||||
new FastClick(publisher, options);
|
||||
});
|
||||
var titles = document.getElementsByClassName("NB-story-title");
|
||||
Array.prototype.slice.call(titles, 0).forEach(function(title) {
|
||||
new NoClickDelay(title);
|
||||
new FastClick(title, options);
|
||||
});
|
||||
|
||||
var author = document.getElementById("NB-story-author");
|
||||
if (author) {
|
||||
new NoClickDelay(author);
|
||||
new FastClick(author, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -313,6 +313,8 @@ div + p {
|
|||
text-shadow: 1px 1px 0 #EFEFEF;
|
||||
overflow: hidden;
|
||||
max-width: none;
|
||||
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.NB-iphone .NB-header {
|
||||
|
|
|
@ -53,9 +53,10 @@
|
|||
}
|
||||
|
||||
.NB-trainer-title .NB-title-trainer {
|
||||
padding: 12px 0;
|
||||
padding: 6px 0 12px;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
-webkit-user-select: auto;
|
||||
-webkit-touch-callout: default;
|
||||
-webkit-highlight: auto;
|
||||
|
@ -67,7 +68,7 @@
|
|||
color: #C0C0C0;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
padding: 12px 0;
|
||||
padding: 2px 0 4px;
|
||||
}
|
||||
|
||||
.NB-story-title-positive {
|
||||
|
@ -152,10 +153,14 @@
|
|||
top: 2px;
|
||||
opacity: .2;
|
||||
}
|
||||
.pressed .NB-classifier .NB-classifier-icon-like,
|
||||
.pressed .NB-classifier.NB-classifier-dislike .NB-classifier-icon-like {
|
||||
.NB-classifier.NB-classifier-like .NB-classifier-icon-like {
|
||||
opacity: 1;
|
||||
}
|
||||
.pressed .NB-classifier .NB-classifier-icon-like,
|
||||
.pressed .NB-classifier.NB-classifier-dislike .NB-classifier-icon-like,
|
||||
.pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-dislike {
|
||||
opacity: .8;
|
||||
}
|
||||
.pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-like {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -14,5 +14,7 @@ Trainer.prototype = {
|
|||
|
||||
Zepto(function($) {
|
||||
new Trainer();
|
||||
attachFastClick();
|
||||
attachFastClick({
|
||||
skipEvent: true
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue